forked from ScoDoc/ScoDoc
Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
bd15b2f419 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -176,6 +176,3 @@ copy
|
|||||||
|
|
||||||
# Symlinks static ScoDoc
|
# Symlinks static ScoDoc
|
||||||
app/static/links/[0-9]*.*[0-9]
|
app/static/links/[0-9]*.*[0-9]
|
||||||
|
|
||||||
# Essais locaux
|
|
||||||
xp/
|
|
||||||
|
154
README.md
154
README.md
@ -1,8 +1,8 @@
|
|||||||
# ScoDoc - Gestion de la scolarité - Version ScoDoc 9
|
# ScoDoc - Gestion de la scolarité - Version ScoDoc 9
|
||||||
|
|
||||||
(c) Emmanuel Viennet 1999 - 2024 (voir LICENCE.txt).
|
(c) Emmanuel Viennet 1999 - 2022 (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>
|
Documentation utilisateur: <https://scodoc.org>
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ Flask, SQLAlchemy, au lien de Python2/Zope dans les versions précédentes).
|
|||||||
|
|
||||||
### Lignes de commandes
|
### Lignes de commandes
|
||||||
|
|
||||||
Voir [le guide de configuration](https://scodoc.org/GuideConfig).
|
Voir [https://scodoc.org/GuideConfig](le guide de configuration).
|
||||||
|
|
||||||
## Organisation des fichiers
|
## 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é.
|
Le répertoire `/opt/scodoc-data` doit être régulièrement sauvegardé.
|
||||||
|
|
||||||
Principaux contenus:
|
Principaux contenus:
|
||||||
```
|
|
||||||
/opt/scodoc-data
|
/opt/scodoc-data
|
||||||
/opt/scodoc-data/log # Fichiers de log ScoDoc
|
/opt/scodoc-data/log # Fichiers de log ScoDoc
|
||||||
/opt/scodoc-data/config # Fichiers de configuration
|
/opt/scodoc-data/config # Fichiers de configuration
|
||||||
.../config/logos # Logos de l'établissement
|
.../config/logos # Logos de l'établissement
|
||||||
.../config/depts # un fichier par département
|
.../config/depts # un fichier par département
|
||||||
/opt/scodoc-data/photos # Photos des étudiants
|
/opt/scodoc-data/photos # Photos des étudiants
|
||||||
/opt/scodoc-data/archives # Archives: PV de jury, maquettes Apogée, fichiers étudiants
|
/opt/scodoc-data/archives # Archives: PV de jury, maquettes Apogée, fichiers étudiants
|
||||||
```
|
|
||||||
## Pour les développeurs
|
## Pour les développeurs
|
||||||
|
|
||||||
### Installation du code
|
### Installation du code
|
||||||
|
|
||||||
Installer ScoDoc 9 normalement ([voir la doc](https://scodoc.org/GuideInstallDebian12)).
|
Installer ScoDoc 9 normalement ([voir la doc](https://scodoc.org/GuideInstallDebian11)).
|
||||||
|
|
||||||
Puis remplacer `/opt/scodoc` par un clone du git.
|
Puis remplacer `/opt/scodoc` par un clone du git.
|
||||||
```bash
|
|
||||||
sudo su
|
|
||||||
mv /opt/scodoc /opt/off-scodoc # ou ce que vous voulez
|
|
||||||
apt-get install git # si besoin
|
|
||||||
git clone https://scodoc.org/git/ScoDoc/ScoDoc.git /opt/scodoc
|
|
||||||
# (ou bien utiliser votre clone gitea si vous l'avez déjà créé !)
|
|
||||||
|
|
||||||
# Donner ce répertoire à l'utilisateur scodoc:
|
sudo su
|
||||||
chown -R scodoc:scodoc /opt/scodoc
|
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:
|
Il faut ensuite installer l'environnement et le fichier de configuration:
|
||||||
```bash
|
|
||||||
# Le plus simple est de piquer le virtualenv configuré par l'installeur:
|
# Le plus simple est de piquer le virtualenv configuré par l'installeur:
|
||||||
mv /opt/off-scodoc/venv /opt/scodoc
|
mv /opt/off-scodoc/venv /opt/scodoc
|
||||||
```
|
|
||||||
Et la config:
|
Et la config:
|
||||||
```bash
|
|
||||||
ln -s /opt/scodoc-data/.env /opt/scodoc
|
ln -s /opt/scodoc-data/.env /opt/scodoc
|
||||||
```
|
|
||||||
Cette dernière commande utilise le `.env` crée lors de l'install, ce qui
|
Cette dernière commande utilise le `.env` crée lors de l'install, ce qui
|
||||||
n'est pas toujours le plus judicieux: vous pouvez modifier son contenu, par
|
n'est pas toujours le plus judicieux: vous pouvez modifier son contenu, par
|
||||||
exemple pour travailler en mode "développement" avec `FLASK_ENV=development`.
|
exemple pour travailler en mode "développement" avec `FLASK_ENV=development`.
|
||||||
@ -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`.
|
Les tests unitaires utilisent normalement la base postgresql `SCODOC_TEST`.
|
||||||
Avant le premier lancement, créer cette base ainsi:
|
Avant le premier lancement, créer cette base ainsi:
|
||||||
```bash
|
|
||||||
./tools/create_database.sh SCODOC_TEST
|
./tools/create_database.sh SCODOC_TEST
|
||||||
export FLASK_ENV=test
|
export FLASK_ENV=test
|
||||||
flask db upgrade
|
flask db upgrade
|
||||||
```
|
|
||||||
Cette commande n'est nécessaire que la première fois (le contenu de la base
|
Cette commande n'est nécessaire que la première fois (le contenu de la base
|
||||||
est effacé au début de chaque test, mais son schéma reste) et aussi si des
|
est effacé au début de chaque test, mais son schéma reste) et aussi si des
|
||||||
migrations (changements de schéma) ont eu lieu dans le code.
|
migrations (changements de schéma) ont eu lieu dans le code.
|
||||||
@ -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
|
Certains tests ont besoin d'un département déjà créé, qui n'est pas créé par les
|
||||||
scripts de tests:
|
scripts de tests:
|
||||||
Lancer au préalable:
|
Lancer au préalable:
|
||||||
```bash
|
|
||||||
flask delete-dept -fy TEST00 && flask create-dept TEST00
|
flask delete-dept -fy TEST00 && flask create-dept TEST00
|
||||||
```
|
|
||||||
Puis dérouler les tests unitaires:
|
Puis dérouler les tests unitaires:
|
||||||
```bash
|
|
||||||
pytest tests/unit
|
pytest tests/unit
|
||||||
```
|
|
||||||
Ou avec couverture (`pip install pytest-cov`)
|
Ou avec couverture (`pip install pytest-cov`)
|
||||||
```bash
|
|
||||||
pytest --cov=app --cov-report=term-missing --cov-branch tests/unit/*
|
pytest --cov=app --cov-report=term-missing --cov-branch tests/unit/*
|
||||||
```
|
|
||||||
#### Utilisation des tests unitaires pour initialiser la base de dev
|
#### Utilisation des tests unitaires pour initialiser la base de dev
|
||||||
|
|
||||||
On peut aussi utiliser les tests unitaires pour mettre la base de données de
|
On peut aussi utiliser les tests unitaires pour mettre la base de données de
|
||||||
@ -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
|
Il suffit de positionner une variable d'environnement indiquant la BD utilisée
|
||||||
par les tests:
|
par les tests:
|
||||||
```bash
|
|
||||||
export SCODOC_TEST_DATABASE_URI=postgresql:///SCODOC_DEV
|
export SCODOC_TEST_DATABASE_URI=postgresql:///SCODOC_DEV
|
||||||
```
|
|
||||||
(si elle n'existe pas, voir plus loin pour la créer) puis de les lancer
|
(si elle n'existe pas, voir plus loin pour la créer) puis de les lancer
|
||||||
normalement, par exemple:
|
normalement, par exemple:
|
||||||
```bash
|
|
||||||
pytest tests/unit/test_sco_basic.py
|
pytest tests/unit/test_sco_basic.py
|
||||||
```
|
|
||||||
Il est en général nécessaire d'affecter ensuite un mot de passe à (au moins) un
|
Il est en général nécessaire d'affecter ensuite un mot de passe à (au moins) un
|
||||||
utilisateur:
|
utilisateur:
|
||||||
```bash
|
|
||||||
flask user-password admin
|
flask user-password admin
|
||||||
```
|
|
||||||
**Attention:** les tests unitaires **effacent** complètement le contenu de la
|
**Attention:** les tests unitaires **effacent** complètement le contenu de la
|
||||||
base de données (tous les départements, et les utilisateurs) avant de commencer !
|
base de données (tous les départements, et les utilisateurs) avant de commencer !
|
||||||
|
|
||||||
#### Modification du schéma de la base
|
#### Modification du schéma de la base
|
||||||
|
|
||||||
On utilise SQLAlchemy avec Alembic et Flask-Migrate.
|
On utilise SQLAlchemy avec Alembic et Flask-Migrate.
|
||||||
```bash
|
|
||||||
flask db migrate -m "message explicatif....."
|
flask db migrate -m "message explicatif....."
|
||||||
flask db upgrade
|
flask db upgrade
|
||||||
```
|
|
||||||
Ne pas oublier de d'ajouter le script de migration à git (`git add migrations/...`).
|
Ne pas oublier de d'ajouter le script de migration à git (`git add migrations/...`).
|
||||||
|
|
||||||
**Mémo**: séquence re-création d'une base (vérifiez votre `.env`
|
**Mémo**: séquence re-création d'une base (vérifiez votre `.env`
|
||||||
ou variables d'environnement pour interroger la bonne base !).
|
ou variables d'environnement pour interroger la bonne base !).
|
||||||
```bash
|
|
||||||
dropdb SCODOC_DEV
|
|
||||||
tools/create_database.sh SCODOC_DEV # créé base SQL
|
|
||||||
flask db upgrade # créé les tables à partir des migrations
|
|
||||||
flask sco-db-init # ajoute au besoin les constantes (fait en migration 0)
|
|
||||||
|
|
||||||
# puis imports:
|
dropdb SCODOC_DEV
|
||||||
flask import-scodoc7-users
|
tools/create_database.sh SCODOC_DEV # créé base SQL
|
||||||
flask import-scodoc7-dept STID SCOSTID
|
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
|
Si la base utilisée pour les dev n'est plus en phase avec les scripts de
|
||||||
migration, utiliser les commandes `flask db history`et `flask db stamp`pour se
|
migration, utiliser les commandes `flask db history`et `flask db stamp`pour se
|
||||||
positionner à la bonne étape.
|
positionner à la bonne étape.
|
||||||
@ -159,23 +163,23 @@ positionner à la bonne étape.
|
|||||||
### Profiling
|
### Profiling
|
||||||
|
|
||||||
Sur une machine de DEV, lancer
|
Sur une machine de DEV, lancer
|
||||||
```bash
|
|
||||||
flask profile --host 0.0.0.0 --length 32 --profile-dir /opt/scodoc-data
|
flask profile --host 0.0.0.0 --length 32 --profile-dir /opt/scodoc-data
|
||||||
```
|
|
||||||
le fichier `.prof` sera alors écrit dans `/opt/scodoc-data` (on peut aussi utiliser `/tmp`).
|
le fichier `.prof` sera alors écrit dans `/opt/scodoc-data` (on peut aussi utiliser `/tmp`).
|
||||||
|
|
||||||
Pour la visualisation, [snakeviz](https://jiffyclub.github.io/snakeviz/) est bien:
|
Pour la visualisation, [snakeviz](https://jiffyclub.github.io/snakeviz/) est bien:
|
||||||
```bash
|
|
||||||
pip install snakeviz
|
pip install snakeviz
|
||||||
```
|
|
||||||
puis
|
puis
|
||||||
```bash
|
|
||||||
snakeviz -s --hostname 0.0.0.0 -p 5555 /opt/scodoc-data/GET.ScoDoc......prof
|
snakeviz -s --hostname 0.0.0.0 -p 5555 /opt/scodoc-data/GET.ScoDoc......prof
|
||||||
```
|
|
||||||
## Paquet Debian 12
|
## Paquet Debian 12
|
||||||
|
|
||||||
Les scripts associés au paquet Debian (.deb) sont dans `tools/debian`. Le plus
|
Les scripts associés au paquet Debian (.deb) sont dans `tools/debian`. Le plus
|
||||||
important est `postinst` qui se charge de configurer le système (install ou
|
important est `postinst`qui se charge de configurer le système (install ou
|
||||||
upgrade de scodoc9).
|
upgrade de scodoc9).
|
||||||
|
|
||||||
La préparation d'une release se fait à l'aide du script
|
La préparation d'une release se fait à l'aide du script
|
||||||
|
@ -19,7 +19,11 @@ from flask import current_app, g, request
|
|||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask import abort, flash, has_request_context
|
from flask import abort, flash, has_request_context
|
||||||
from flask import render_template
|
from flask import render_template
|
||||||
|
|
||||||
|
# from flask.json import JSONEncoder
|
||||||
from flask.logging import default_handler
|
from flask.logging import default_handler
|
||||||
|
|
||||||
|
from flask_bootstrap import Bootstrap
|
||||||
from flask_caching import Cache
|
from flask_caching import Cache
|
||||||
from flask_json import FlaskJSON, json_response
|
from flask_json import FlaskJSON, json_response
|
||||||
from flask_login import LoginManager, current_user
|
from flask_login import LoginManager, current_user
|
||||||
@ -30,7 +34,6 @@ from flask_sqlalchemy import SQLAlchemy
|
|||||||
from jinja2 import select_autoescape
|
from jinja2 import select_autoescape
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
import werkzeug.debug
|
import werkzeug.debug
|
||||||
from wtforms.fields import HiddenField
|
|
||||||
|
|
||||||
from flask_cas import CAS
|
from flask_cas import CAS
|
||||||
|
|
||||||
@ -56,6 +59,8 @@ login.login_view = "auth.login"
|
|||||||
login.login_message = "Identifiez-vous pour accéder à cette page."
|
login.login_message = "Identifiez-vous pour accéder à cette page."
|
||||||
|
|
||||||
mail = Mail()
|
mail = Mail()
|
||||||
|
bootstrap = Bootstrap()
|
||||||
|
# moment = Moment()
|
||||||
|
|
||||||
CACHE_TYPE = os.environ.get("CACHE_TYPE")
|
CACHE_TYPE = os.environ.get("CACHE_TYPE")
|
||||||
cache = Cache(
|
cache = Cache(
|
||||||
@ -86,9 +91,8 @@ def handle_invalid_csrf(exc):
|
|||||||
return render_template("error_csrf.j2", exc=exc), 404
|
return render_template("error_csrf.j2", exc=exc), 404
|
||||||
|
|
||||||
|
|
||||||
# def handle_pdf_format_error(exc):
|
def handle_pdf_format_error(exc):
|
||||||
# return "ay ay ay"
|
return "ay ay ay"
|
||||||
handle_pdf_format_error = handle_sco_value_error
|
|
||||||
|
|
||||||
|
|
||||||
def internal_server_error(exc):
|
def internal_server_error(exc):
|
||||||
@ -300,6 +304,7 @@ def create_app(config_class=DevConfig):
|
|||||||
login.init_app(app)
|
login.init_app(app)
|
||||||
mail.init_app(app)
|
mail.init_app(app)
|
||||||
app.extensions["mail"].debug = 0 # disable copy of mails to stderr
|
app.extensions["mail"].debug = 0 # disable copy of mails to stderr
|
||||||
|
bootstrap.init_app(app)
|
||||||
cache.init_app(app)
|
cache.init_app(app)
|
||||||
sco_cache.CACHE = cache
|
sco_cache.CACHE = cache
|
||||||
if CACHE_TYPE: # non default
|
if CACHE_TYPE: # non default
|
||||||
@ -332,15 +337,8 @@ def create_app(config_class=DevConfig):
|
|||||||
from app.api import api_bp
|
from app.api import api_bp
|
||||||
from app.api import api_web_bp
|
from app.api import api_web_bp
|
||||||
|
|
||||||
# Jinja2 configuration
|
|
||||||
# Enable autoescaping of all templates, including .j2
|
# Enable autoescaping of all templates, including .j2
|
||||||
app.jinja_env.autoescape = select_autoescape(default_for_string=True, default=True)
|
app.jinja_env.autoescape = select_autoescape(default_for_string=True, default=True)
|
||||||
app.jinja_env.trim_blocks = True
|
|
||||||
app.jinja_env.lstrip_blocks = True
|
|
||||||
# previously in Flask-Bootstrap:
|
|
||||||
app.jinja_env.globals["bootstrap_is_hidden_field"] = lambda field: isinstance(
|
|
||||||
field, HiddenField
|
|
||||||
)
|
|
||||||
|
|
||||||
# https://scodoc.fr/ScoDoc
|
# https://scodoc.fr/ScoDoc
|
||||||
app.register_blueprint(scodoc_bp)
|
app.register_blueprint(scodoc_bp)
|
||||||
@ -551,8 +549,8 @@ def truncate_database():
|
|||||||
CREATE OR REPLACE FUNCTION reset_sequences(username IN VARCHAR) RETURNS void AS $$
|
CREATE OR REPLACE FUNCTION reset_sequences(username IN VARCHAR) RETURNS void AS $$
|
||||||
DECLARE
|
DECLARE
|
||||||
statements CURSOR FOR
|
statements CURSOR FOR
|
||||||
SELECT sequence_name
|
SELECT sequence_name
|
||||||
FROM information_schema.sequences
|
FROM information_schema.sequences
|
||||||
ORDER BY sequence_name ;
|
ORDER BY sequence_name ;
|
||||||
BEGIN
|
BEGIN
|
||||||
FOR stmt IN statements LOOP
|
FOR stmt IN statements LOOP
|
||||||
@ -637,12 +635,14 @@ def critical_error(msg):
|
|||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
|
|
||||||
log(f"\n*** CRITICAL ERROR: {msg}")
|
log(f"\n*** CRITICAL ERROR: {msg}")
|
||||||
subject = f"CRITICAL ERROR: {msg}".strip()[:68]
|
send_scodoc_alarm(f"CRITICAL ERROR: {msg}", msg)
|
||||||
send_scodoc_alarm(subject, msg)
|
|
||||||
clear_scodoc_cache()
|
clear_scodoc_cache()
|
||||||
raise ScoValueError(
|
raise ScoValueError(
|
||||||
f"""
|
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}
|
{msg}
|
||||||
"""
|
"""
|
||||||
|
@ -1,17 +1,11 @@
|
|||||||
"""api.__init__
|
"""api.__init__
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from functools import wraps
|
|
||||||
|
|
||||||
from flask_json import as_json
|
from flask_json import as_json
|
||||||
from flask import Blueprint
|
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 import db
|
||||||
from app.decorators import permission_required
|
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
from app.scodoc.sco_exceptions import AccessDenied, ScoException
|
from app.scodoc.sco_exceptions import AccessDenied, ScoException
|
||||||
from app.scodoc.sco_permissions import Permission
|
|
||||||
|
|
||||||
api_bp = Blueprint("api", __name__)
|
api_bp = Blueprint("api", __name__)
|
||||||
api_web_bp = Blueprint("apiweb", __name__)
|
api_web_bp = Blueprint("apiweb", __name__)
|
||||||
@ -20,28 +14,6 @@ api_web_bp = Blueprint("apiweb", __name__)
|
|||||||
API_CLIENT_ERROR = 400 # erreur dans les paramètres fournis par le client
|
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_bp.errorhandler(ScoException)
|
||||||
@api_web_bp.errorhandler(ScoException)
|
@api_web_bp.errorhandler(ScoException)
|
||||||
@api_bp.errorhandler(404)
|
@api_bp.errorhandler(404)
|
||||||
@ -76,35 +48,20 @@ def requested_format(default_format="json", allowed_formats=None):
|
|||||||
|
|
||||||
|
|
||||||
@as_json
|
@as_json
|
||||||
def get_model_api_object(
|
def get_model_api_object(model_cls: db.Model, model_id: int, join_cls: db.Model = None):
|
||||||
model_cls: db.Model,
|
|
||||||
model_id: int,
|
|
||||||
join_cls: db.Model = None,
|
|
||||||
restrict: bool | None = None,
|
|
||||||
):
|
|
||||||
"""
|
"""
|
||||||
Retourne une réponse contenant la représentation api de l'objet "Model[model_id]"
|
Retourne une réponse contenant la représentation api de l'objet "Model[model_id]"
|
||||||
|
|
||||||
Filtrage du département en fonction d'une classe de jointure (eg: Identite, Formsemestre) -> join_cls
|
Filtrage du département en fonction d'une classe de jointure (eg: Identite, Formsemstre) -> join_cls
|
||||||
|
|
||||||
exemple d'utilisation : fonction "justificatif()" -> app/api/justificatifs.py
|
exemple d'utilisation : fonction "justificatif()" -> app/api/justificatifs.py
|
||||||
|
|
||||||
L'agument restrict est passé to_dict, est signale que l'on veut une version restreinte
|
|
||||||
(sans données personnelles, ou sans informations sur le justificatif d'absence)
|
|
||||||
"""
|
"""
|
||||||
query = model_cls.query.filter_by(id=model_id)
|
query = model_cls.query.filter_by(id=model_id)
|
||||||
if g.scodoc_dept and join_cls is not None:
|
if g.scodoc_dept and join_cls is not None:
|
||||||
query = query.join(join_cls).filter_by(dept_id=g.scodoc_dept_id)
|
query = query.join(join_cls).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
unique: model_cls = query.first()
|
unique: model_cls = query.first_or_404()
|
||||||
|
|
||||||
if unique is None:
|
return unique.to_dict(format_api=True)
|
||||||
return scu.json_error(
|
|
||||||
404,
|
|
||||||
message=f"{model_cls.__name__} inexistant(e)",
|
|
||||||
)
|
|
||||||
if restrict is None:
|
|
||||||
return unique.to_dict(format_api=True)
|
|
||||||
return unique.to_dict(format_api=True, restrict=restrict)
|
|
||||||
|
|
||||||
|
|
||||||
from app.api import tokens
|
from app.api import tokens
|
||||||
|
@ -1,34 +1,30 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
"""ScoDoc 9 API : Assiduités"""
|
"""ScoDoc 9 API : Assiduités
|
||||||
|
"""
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from flask import g, request
|
from flask import g, request
|
||||||
from flask_json import as_json
|
from flask_json import as_json
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
from flask_sqlalchemy.query import Query
|
|
||||||
from sqlalchemy.orm.exc import ObjectDeletedError
|
|
||||||
from werkzeug.exceptions import HTTPException
|
|
||||||
|
|
||||||
from app import db, log, set_sco_dept
|
from app import db, log, set_sco_dept
|
||||||
import app.scodoc.sco_assiduites as scass
|
import app.scodoc.sco_assiduites as scass
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app.api import api_bp as bp
|
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_web_bp, get_model_api_object, tools
|
||||||
from app.api import api_permission_required as permission_required
|
from app.decorators import permission_required, scodoc
|
||||||
from app.decorators import scodoc
|
|
||||||
from app.models import (
|
from app.models import (
|
||||||
Assiduite,
|
Assiduite,
|
||||||
Evaluation,
|
|
||||||
FormSemestre,
|
FormSemestre,
|
||||||
Identite,
|
Identite,
|
||||||
ModuleImpl,
|
ModuleImpl,
|
||||||
Scolog,
|
Scolog,
|
||||||
)
|
)
|
||||||
|
from flask_sqlalchemy.query import Query
|
||||||
from app.models.assiduites import (
|
from app.models.assiduites import (
|
||||||
get_assiduites_justif,
|
get_assiduites_justif,
|
||||||
get_justifs_from_date,
|
get_justifs_from_date,
|
||||||
@ -43,13 +39,10 @@ from app.scodoc.sco_utils import json_error
|
|||||||
@api_web_bp.route("/assiduite/<int:assiduite_id>")
|
@api_web_bp.route("/assiduite/<int:assiduite_id>")
|
||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
|
||||||
def assiduite(assiduite_id: int = None):
|
def assiduite(assiduite_id: int = None):
|
||||||
"""Retourne un objet assiduité à partir de son id
|
"""Retourne un objet assiduité à partir de son id
|
||||||
|
|
||||||
Exemple de résultat:
|
Exemple de résultat:
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
{
|
||||||
"assiduite_id": 1,
|
"assiduite_id": 1,
|
||||||
"etudid": 2,
|
"etudid": 2,
|
||||||
@ -58,17 +51,11 @@ def assiduite(assiduite_id: int = None):
|
|||||||
"date_fin": "2022-10-31T10:00+01:00",
|
"date_fin": "2022-10-31T10:00+01:00",
|
||||||
"etat": "retard",
|
"etat": "retard",
|
||||||
"desc": "une description",
|
"desc": "une description",
|
||||||
"user_id": 1 or null,
|
"user_id: 1 or null,
|
||||||
"user_name" : login scodoc or null,
|
"user_name" : login scodoc or null
|
||||||
"user_nom_complet": "Marie Dupont",
|
"user_nom_complet": "Marie Dupont"
|
||||||
"est_just": False or True,
|
"est_just": False or True,
|
||||||
}
|
}
|
||||||
```
|
|
||||||
|
|
||||||
SAMPLES
|
|
||||||
-------
|
|
||||||
/assiduite/1;
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return get_model_api_object(Assiduite, assiduite_id, Identite)
|
return get_model_api_object(Assiduite, assiduite_id, Identite)
|
||||||
@ -86,26 +73,18 @@ def assiduite(assiduite_id: int = None):
|
|||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@as_json
|
||||||
def assiduite_justificatifs(assiduite_id: int = None, long: bool = False):
|
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:
|
Exemple de résultat:
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
[
|
||||||
1,
|
1,
|
||||||
2,
|
2,
|
||||||
3,
|
3,
|
||||||
...
|
...
|
||||||
]
|
]
|
||||||
```
|
|
||||||
SAMPLES
|
|
||||||
-------
|
|
||||||
/assiduite/1/justificatifs;
|
|
||||||
/assiduite/1/justificatifs/long;
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return get_assiduites_justif(assiduite_id, long)
|
return get_assiduites_justif(assiduite_id, True)
|
||||||
|
|
||||||
|
|
||||||
# etudid
|
# etudid
|
||||||
@ -136,42 +115,52 @@ def assiduite_justificatifs(assiduite_id: int = None, long: bool = False):
|
|||||||
@scodoc
|
@scodoc
|
||||||
@as_json
|
@as_json
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
def assiduites_count(
|
def count_assiduites(
|
||||||
etudid: int = None, nip: str = None, ine: str = None, with_query: bool = False
|
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;
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -183,7 +172,6 @@ def assiduites_count(
|
|||||||
404,
|
404,
|
||||||
message="étudiant inconnu",
|
message="étudiant inconnu",
|
||||||
)
|
)
|
||||||
set_sco_dept(etud.departement.acronym)
|
|
||||||
|
|
||||||
# Les filtres qui seront appliqués au comptage (type, date, etudid...)
|
# Les filtres qui seront appliqués au comptage (type, date, etudid...)
|
||||||
filtered: dict[str, object] = {}
|
filtered: dict[str, object] = {}
|
||||||
@ -228,35 +216,40 @@ def assiduites_count(
|
|||||||
def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False):
|
def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False):
|
||||||
"""
|
"""
|
||||||
Retourne toutes les assiduités d'un étudiant
|
Retourne toutes les assiduités d'un étudiant
|
||||||
|
chemin : /assiduites/<int:etudid>
|
||||||
|
|
||||||
QUERY
|
Un filtrage peut être donné avec une query
|
||||||
-----
|
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>
|
|
||||||
|
|
||||||
PARAMS
|
Les différents filtres :
|
||||||
-----
|
Etat (etat de l'étudiant -> absent, present ou retard):
|
||||||
user_id:l'id de l'auteur de l'assiduité
|
query?etat=[- liste des états séparé par une virgule -]
|
||||||
est_just:si l'assiduité est justifiée (fait aussi filtre abs/retard)
|
ex: .../query?etat=present,retard
|
||||||
moduleimpl_id:l'id du module concerné par l'assiduité
|
Date debut
|
||||||
date_debut:date de début de l'assiduité (supérieur ou égal)
|
(date de début de l'assiduité, sont affichés les assiduités
|
||||||
date_fin:date de fin de l'assiduité (inférieur ou égal)
|
dont la date de début est supérieur ou égale à la valeur donnée):
|
||||||
etat:etat de l'étudiant → absent, present ou retard
|
query?date_debut=[- date au format iso -]
|
||||||
formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité
|
ex: query?date_debut=2022-11-03T08:00+01:00
|
||||||
with_justif:ajoute les justificatifs liés à l'assiduité
|
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 +261,6 @@ def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False)
|
|||||||
404,
|
404,
|
||||||
message="étudiant inconnu",
|
message="étudiant inconnu",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Récupération des assiduités de l'étudiant
|
# Récupération des assiduités de l'étudiant
|
||||||
assiduites_query: Query = etud.assiduites
|
assiduites_query: Query = etud.assiduites
|
||||||
|
|
||||||
@ -291,108 +283,6 @@ def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False)
|
|||||||
return data_set
|
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})
|
@bp.route("/assiduites/group/query", defaults={"with_query": True})
|
||||||
@api_web_bp.route("/assiduites/group/query", defaults={"with_query": True})
|
@api_web_bp.route("/assiduites/group/query", defaults={"with_query": True})
|
||||||
@login_required
|
@login_required
|
||||||
@ -404,34 +294,38 @@ def assiduites_group(with_query: bool = False):
|
|||||||
Retourne toutes les assiduités d'un groupe d'étudiants
|
Retourne toutes les assiduités d'un groupe d'étudiants
|
||||||
chemin : /assiduites/group/query?etudids=1,2,3
|
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
|
Les différents filtres :
|
||||||
-----
|
Etat (etat de l'étudiant -> absent, present ou retard):
|
||||||
user_id:<int:user_id>
|
query?etat=[- liste des états séparé par une virgule -]
|
||||||
est_just:<bool:est_just>
|
ex: .../query?etat=present,retard
|
||||||
moduleimpl_id:<int:moduleimpl_id>
|
Date debut
|
||||||
date_debut:<string:date_debut_iso>
|
(date de début de l'assiduité, sont affichés les assiduités
|
||||||
date_fin:<string:date_fin_iso>
|
dont la date de début est supérieur ou égale à la valeur donnée):
|
||||||
etat:<array[string]:etat>
|
query?date_debut=[- date au format iso -]
|
||||||
etudids:<array[int]:etudids>
|
ex: query?date_debut=2022-11-03T08:00+01:00
|
||||||
formsemestre_id:<int:formsemestre_id>
|
Date fin
|
||||||
with_justif:<bool:with_justif>
|
(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;
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -441,7 +335,7 @@ def assiduites_group(with_query: bool = False):
|
|||||||
try:
|
try:
|
||||||
etuds = [int(etu) for etu in etuds]
|
etuds = [int(etu) for etu in etuds]
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return json_error(404, "Le champ etudids n'est pas correctement formé")
|
return json_error(404, "Le champs etudids n'est pas correctement formé")
|
||||||
|
|
||||||
# Vérification que tous les étudiants sont du même département
|
# Vérification que tous les étudiants sont du même département
|
||||||
query = Identite.query.filter(Identite.id.in_(etuds))
|
query = Identite.query.filter(Identite.id.in_(etuds))
|
||||||
@ -491,34 +385,7 @@ def assiduites_group(with_query: bool = False):
|
|||||||
@as_json
|
@as_json
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
|
def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
|
||||||
"""Retourne toutes les assiduités du formsemestre
|
"""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;
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Récupération du formsemestre à partir du formsemestre_id
|
# Récupération du formsemestre à partir du formsemestre_id
|
||||||
formsemestre: FormSemestre = None
|
formsemestre: FormSemestre = None
|
||||||
@ -565,42 +432,10 @@ def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
|
|||||||
@scodoc
|
@scodoc
|
||||||
@as_json
|
@as_json
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
def assiduites_formsemestre_count(
|
def count_assiduites_formsemestre(
|
||||||
formsemestre_id: int = None, with_query: bool = False
|
formsemestre_id: int = None, with_query: bool = False
|
||||||
):
|
):
|
||||||
"""Comptage des assiduités du formsemestre
|
"""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;
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Récupération du formsemestre à partir du formsemestre_id
|
# Récupération du formsemestre à partir du formsemestre_id
|
||||||
formsemestre: FormSemestre = None
|
formsemestre: FormSemestre = None
|
||||||
@ -609,8 +444,6 @@ def assiduites_formsemestre_count(
|
|||||||
if formsemestre is None:
|
if formsemestre is None:
|
||||||
return json_error(404, "le paramètre 'formsemestre_id' n'existe pas")
|
return json_error(404, "le paramètre 'formsemestre_id' n'existe pas")
|
||||||
|
|
||||||
set_sco_dept(formsemestre.departement.acronym)
|
|
||||||
|
|
||||||
# Récupération des étudiants du formsemestre
|
# Récupération des étudiants du formsemestre
|
||||||
etuds = formsemestre.etuds.all()
|
etuds = formsemestre.etuds.all()
|
||||||
etuds_id = [etud.id for etud in etuds]
|
etuds_id = [etud.id for etud in etuds]
|
||||||
@ -651,10 +484,7 @@ def assiduites_formsemestre_count(
|
|||||||
def assiduite_create(etudid: int = None, nip=None, ine=None):
|
def assiduite_create(etudid: int = None, nip=None, ine=None):
|
||||||
"""
|
"""
|
||||||
Enregistrement d'assiduités pour un étudiant (etudid)
|
Enregistrement d'assiduités pour un étudiant (etudid)
|
||||||
|
La requête doit avoir un content type "application/json":
|
||||||
DATA
|
|
||||||
----
|
|
||||||
```json
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"date_debut": str,
|
"date_debut": str,
|
||||||
@ -670,12 +500,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
|
# Récupération de l'étudiant
|
||||||
@ -729,10 +553,7 @@ def assiduite_create(etudid: int = None, nip=None, ine=None):
|
|||||||
def assiduites_create():
|
def assiduites_create():
|
||||||
"""
|
"""
|
||||||
Création d'une assiduité ou plusieurs assiduites
|
Création d'une assiduité ou plusieurs assiduites
|
||||||
|
La requête doit avoir un content type "application/json":
|
||||||
DATA
|
|
||||||
----
|
|
||||||
```json
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"date_debut": str,
|
"date_debut": str,
|
||||||
@ -745,17 +566,12 @@ def assiduites_create():
|
|||||||
"date_fin": str,
|
"date_fin": str,
|
||||||
"etat": str,
|
"etat": str,
|
||||||
"etudid":int,
|
"etudid":int,
|
||||||
|
|
||||||
"moduleimpl_id": int,
|
"moduleimpl_id": int,
|
||||||
"desc":str,
|
"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""}]
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -789,9 +605,9 @@ def _create_one(
|
|||||||
etud: Identite,
|
etud: Identite,
|
||||||
) -> tuple[int, object]:
|
) -> tuple[int, object]:
|
||||||
"""
|
"""
|
||||||
Création d'une assiduité à partir d'un dict
|
_create_one Création d'une assiduité à partir d'une représentation JSON
|
||||||
|
|
||||||
Cette fonction vérifie les données du dict (qui vient du JSON API)
|
Cette fonction vérifie la représentation JSON
|
||||||
|
|
||||||
Puis crée l'assiduité si la représentation est valide.
|
Puis crée l'assiduité si la représentation est valide.
|
||||||
|
|
||||||
@ -926,18 +742,13 @@ def assiduite_delete():
|
|||||||
"""
|
"""
|
||||||
Suppression d'une assiduité à partir de son id
|
Suppression d'une assiduité à partir de son id
|
||||||
|
|
||||||
DATA
|
Forme des données envoyées :
|
||||||
----
|
|
||||||
```json
|
|
||||||
[
|
[
|
||||||
<assiduite_id:int>,
|
<assiduite_id:int>,
|
||||||
...
|
...
|
||||||
]
|
]
|
||||||
```
|
|
||||||
|
|
||||||
SAMPLES
|
|
||||||
-------
|
|
||||||
/assiduite/delete;[2,2,3]
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Récupération des ids envoyés dans la liste
|
# Récupération des ids envoyés dans la liste
|
||||||
@ -1012,33 +823,25 @@ def _delete_one(assiduite_id: int) -> tuple[int, str]:
|
|||||||
def assiduite_edit(assiduite_id: int):
|
def assiduite_edit(assiduite_id: int):
|
||||||
"""
|
"""
|
||||||
Edition d'une assiduité à partir de son id
|
Edition d'une assiduité à partir de son id
|
||||||
|
La requête doit avoir un content type "application/json":
|
||||||
DATA
|
|
||||||
----
|
|
||||||
```json
|
|
||||||
{
|
{
|
||||||
"etat"?: str,
|
"etat"?: str,
|
||||||
"moduleimpl_id"?: int
|
"moduleimpl_id"?: int
|
||||||
"desc"?: str
|
"desc"?: str
|
||||||
"est_just"?: bool
|
"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
|
# Récupération de l'assiduité à modifier
|
||||||
assiduite_unique: Assiduite = Assiduite.query.filter_by(id=assiduite_id).first()
|
assiduite_unique: Assiduite = Assiduite.query.filter_by(
|
||||||
if assiduite_unique is None:
|
id=assiduite_id
|
||||||
return json_error(404, "Assiduité non existante")
|
).first_or_404()
|
||||||
# Récupération des valeurs à modifier
|
# Récupération des valeurs à modifier
|
||||||
data = request.get_json(force=True)
|
data = request.get_json(force=True)
|
||||||
|
|
||||||
|
# Préparation du retour
|
||||||
|
errors: list[str] = []
|
||||||
|
|
||||||
# Code 200 si modification réussie
|
# Code 200 si modification réussie
|
||||||
# Code 404 si raté + message d'erreur
|
# Code 404 si raté + message d'erreur
|
||||||
code, obj = _edit_one(assiduite_unique, data)
|
code, obj = _edit_one(assiduite_unique, data)
|
||||||
@ -1054,10 +857,7 @@ def assiduite_edit(assiduite_id: int):
|
|||||||
msg=f"assiduite: modif {assiduite_unique}",
|
msg=f"assiduite: modif {assiduite_unique}",
|
||||||
)
|
)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
try:
|
scass.simple_invalidate_cache(assiduite_unique.to_dict())
|
||||||
scass.simple_invalidate_cache(assiduite_unique.to_dict())
|
|
||||||
except ObjectDeletedError:
|
|
||||||
return json_error(404, "Assiduité supprimée / inexistante")
|
|
||||||
|
|
||||||
return {"OK": True}
|
return {"OK": True}
|
||||||
|
|
||||||
@ -1071,10 +871,7 @@ def assiduite_edit(assiduite_id: int):
|
|||||||
def assiduites_edit():
|
def assiduites_edit():
|
||||||
"""
|
"""
|
||||||
Edition de plusieurs assiduités
|
Edition de plusieurs assiduités
|
||||||
|
La requête doit avoir un content type "application/json":
|
||||||
DATA
|
|
||||||
----
|
|
||||||
```json
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"assiduite_id" : int,
|
"assiduite_id" : int,
|
||||||
@ -1084,13 +881,6 @@ def assiduites_edit():
|
|||||||
"est_just"?: bool
|
"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)
|
edit_list: list[object] = request.get_json(force=True)
|
||||||
|
|
||||||
@ -1198,7 +988,9 @@ def _edit_one(assiduite_unique: Assiduite, data: dict) -> tuple[int, str]:
|
|||||||
if moduleimpl is None:
|
if moduleimpl is None:
|
||||||
errors.append("param 'moduleimpl_id': invalide")
|
errors.append("param 'moduleimpl_id': invalide")
|
||||||
else:
|
else:
|
||||||
if not moduleimpl.est_inscrit(assiduite_unique.etudiant):
|
if not moduleimpl.est_inscrit(
|
||||||
|
Identite.query.filter_by(id=assiduite_unique.etudid).first()
|
||||||
|
):
|
||||||
errors.append("param 'moduleimpl_id': etud non inscrit")
|
errors.append("param 'moduleimpl_id': etud non inscrit")
|
||||||
else:
|
else:
|
||||||
# Mise à jour du moduleimpl
|
# Mise à jour du moduleimpl
|
||||||
@ -1214,9 +1006,7 @@ def _edit_one(assiduite_unique: Assiduite, data: dict) -> tuple[int, str]:
|
|||||||
if formsemestre:
|
if formsemestre:
|
||||||
force = scu.is_assiduites_module_forced(formsemestre_id=formsemestre.id)
|
force = scu.is_assiduites_module_forced(formsemestre_id=formsemestre.id)
|
||||||
else:
|
else:
|
||||||
force = scu.is_assiduites_module_forced(
|
force = scu.is_assiduites_module_forced(dept_id=etud.dept_id)
|
||||||
dept_id=assiduite_unique.etudiant.dept_id
|
|
||||||
)
|
|
||||||
|
|
||||||
external_data = (
|
external_data = (
|
||||||
external_data
|
external_data
|
||||||
@ -1224,9 +1014,7 @@ def _edit_one(assiduite_unique: Assiduite, data: dict) -> tuple[int, str]:
|
|||||||
else assiduite_unique.external_data
|
else assiduite_unique.external_data
|
||||||
)
|
)
|
||||||
|
|
||||||
if force and not (
|
if force and not external_data.get("module", False):
|
||||||
external_data is not None and external_data.get("module", False) != ""
|
|
||||||
):
|
|
||||||
errors.append(
|
errors.append(
|
||||||
"param 'moduleimpl_id' : le moduleimpl_id ne peut pas être nul"
|
"param 'moduleimpl_id' : le moduleimpl_id ne peut pas être nul"
|
||||||
)
|
)
|
||||||
@ -1444,8 +1232,8 @@ def _filter_manager(requested, assiduites_query: Query) -> Query:
|
|||||||
annee: int = scu.annee_scolaire()
|
annee: int = scu.annee_scolaire()
|
||||||
|
|
||||||
assiduites_query: Query = assiduites_query.filter(
|
assiduites_query: Query = assiduites_query.filter(
|
||||||
Assiduite.date_debut >= scu.date_debut_annee_scolaire(annee),
|
Assiduite.date_debut >= scu.date_debut_anne_scolaire(annee),
|
||||||
Assiduite.date_fin <= scu.date_fin_annee_scolaire(annee),
|
Assiduite.date_fin <= scu.date_fin_anne_scolaire(annee),
|
||||||
)
|
)
|
||||||
|
|
||||||
return assiduites_query
|
return assiduites_query
|
||||||
|
@ -1,16 +1,11 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
"""
|
"""
|
||||||
API : billets d'absences
|
API : billets d'absences
|
||||||
|
|
||||||
CATEGORY
|
|
||||||
--------
|
|
||||||
Billets d'absence
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import g, request
|
from flask import g, request
|
||||||
@ -34,7 +29,7 @@ from app.scodoc.sco_permissions import Permission
|
|||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@as_json
|
||||||
def billets_absence_etudiant(etudid: int):
|
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)
|
billets = sco_abs_billets.query_billets_etud(etudid)
|
||||||
return [billet.to_dict() for billet in billets]
|
return [billet.to_dict() for billet in billets]
|
||||||
|
|
||||||
@ -46,20 +41,7 @@ def billets_absence_etudiant(etudid: int):
|
|||||||
@permission_required(Permission.AbsAddBillet)
|
@permission_required(Permission.AbsAddBillet)
|
||||||
@as_json
|
@as_json
|
||||||
def billets_absence_create():
|
def billets_absence_create():
|
||||||
"""Ajout d'un billet d'absence. Renvoie le billet créé en json.
|
"""Ajout d'un billet d'absence"""
|
||||||
|
|
||||||
DATA
|
|
||||||
----
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"etudid" : int,
|
|
||||||
"abs_begin" : date_iso,
|
|
||||||
"abs_end" : date_iso,
|
|
||||||
"description" : string,
|
|
||||||
"justified" : bool
|
|
||||||
}
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||||
etudid = data.get("etudid")
|
etudid = data.get("etudid")
|
||||||
abs_begin = data.get("abs_begin")
|
abs_begin = data.get("abs_begin")
|
||||||
|
@ -1,19 +1,14 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
"""
|
"""
|
||||||
ScoDoc 9 API : accès aux départements
|
ScoDoc 9 API : accès aux départements
|
||||||
|
|
||||||
Note: les routes /departement[s] sont publiées sur l'API (/ScoDoc/api/),
|
Note: les routes /departement[s] sont publiées sur l'API (/ScoDoc/api/),
|
||||||
mais évidemment pas sur l'API web (/ScoDoc/<dept>/api).
|
mais évidemment pas sur l'API web (/ScoDoc/<dept>/api).
|
||||||
|
|
||||||
CATEGORY
|
|
||||||
--------
|
|
||||||
Département
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
@ -21,15 +16,26 @@ from flask import request
|
|||||||
from flask_json import as_json
|
from flask_json import as_json
|
||||||
from flask_login import login_required
|
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_bp as bp, API_CLIENT_ERROR
|
||||||
from app.api import api_permission_required as permission_required
|
from app.scodoc.sco_utils import json_error
|
||||||
from app.decorators import scodoc
|
from app.decorators import scodoc, permission_required
|
||||||
from app.models import Departement, FormSemestre
|
from app.models import Departement, FormSemestre
|
||||||
from app.models import departements
|
from app.models import departements
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
from app.scodoc.sco_permissions import Permission
|
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")
|
@bp.route("/departements")
|
||||||
@ -38,7 +44,7 @@ from app.scodoc.sco_utils import json_error
|
|||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@as_json
|
||||||
def departements_list():
|
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]
|
return [dept.to_dict(with_dept_name=True) for dept in Departement.query]
|
||||||
|
|
||||||
|
|
||||||
@ -48,7 +54,7 @@ def departements_list():
|
|||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@as_json
|
||||||
def departements_ids():
|
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]
|
return [dept.id for dept in Departement.query]
|
||||||
|
|
||||||
|
|
||||||
@ -57,12 +63,11 @@ def departements_ids():
|
|||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@as_json
|
||||||
def departement_by_acronym(acronym: str):
|
def departement(acronym: str):
|
||||||
"""
|
"""
|
||||||
Info sur un département. Accès par acronyme.
|
Info sur un département. Accès par acronyme.
|
||||||
|
|
||||||
Exemple de résultat :
|
Exemple de résultat :
|
||||||
```json
|
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"acronym": "TAPI",
|
"acronym": "TAPI",
|
||||||
@ -71,7 +76,6 @@ def departement_by_acronym(acronym: str):
|
|||||||
"visible": true,
|
"visible": true,
|
||||||
"date_creation": "Fri, 15 Apr 2022 12:19:28 GMT"
|
"date_creation": "Fri, 15 Apr 2022 12:19:28 GMT"
|
||||||
}
|
}
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||||
return dept.to_dict(with_dept_name=True)
|
return dept.to_dict(with_dept_name=True)
|
||||||
@ -98,15 +102,11 @@ def departement_by_id(dept_id: int):
|
|||||||
def departement_create():
|
def departement_create():
|
||||||
"""
|
"""
|
||||||
Création d'un département.
|
Création d'un département.
|
||||||
Le content type doit être `application/json`.
|
The request content type should be "application/json":
|
||||||
DATA
|
|
||||||
----
|
|
||||||
```json
|
|
||||||
{
|
{
|
||||||
"acronym": str,
|
"acronym": str,
|
||||||
"visible": bool,
|
"visible":bool,
|
||||||
}
|
}
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||||
acronym = str(data.get("acronym", ""))
|
acronym = str(data.get("acronym", ""))
|
||||||
@ -117,9 +117,6 @@ def departement_create():
|
|||||||
dept = departements.create_dept(acronym, visible=visible)
|
dept = departements.create_dept(acronym, visible=visible)
|
||||||
except ScoValueError as exc:
|
except ScoValueError as exc:
|
||||||
return json_error(500, exc.args[0] if exc.args else "")
|
return json_error(500, exc.args[0] if exc.args else "")
|
||||||
|
|
||||||
log(f"departement_create {dept.acronym}")
|
|
||||||
|
|
||||||
return dept.to_dict()
|
return dept.to_dict()
|
||||||
|
|
||||||
|
|
||||||
@ -130,12 +127,10 @@ def departement_create():
|
|||||||
@as_json
|
@as_json
|
||||||
def departement_edit(acronym):
|
def departement_edit(acronym):
|
||||||
"""
|
"""
|
||||||
Édition d'un département: seul le champ `visible` peut être modifié.
|
Edition d'un département: seul visible peut être modifié
|
||||||
|
The request content type should be "application/json":
|
||||||
DATA
|
|
||||||
----
|
|
||||||
{
|
{
|
||||||
"visible": bool,
|
"visible":bool,
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||||
@ -147,7 +142,6 @@ def departement_edit(acronym):
|
|||||||
dept.visible = visible
|
dept.visible = visible
|
||||||
db.session.add(dept)
|
db.session.add(dept)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
log(f"departement_edit {dept.acronym}")
|
|
||||||
return dept.to_dict()
|
return dept.to_dict()
|
||||||
|
|
||||||
|
|
||||||
@ -157,13 +151,11 @@ def departement_edit(acronym):
|
|||||||
@permission_required(Permission.ScoSuperAdmin)
|
@permission_required(Permission.ScoSuperAdmin)
|
||||||
def departement_delete(acronym):
|
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()
|
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||||
acronym = dept.acronym
|
|
||||||
db.session.delete(dept)
|
db.session.delete(dept)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
log(f"departement_delete {acronym}")
|
|
||||||
return {"OK": True}
|
return {"OK": True}
|
||||||
|
|
||||||
|
|
||||||
@ -172,16 +164,13 @@ def departement_delete(acronym):
|
|||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@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 :
|
Exemple de résultat :
|
||||||
```json
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"civilite": "M",
|
"civilite": "M",
|
||||||
@ -196,7 +185,6 @@ def departement_etudiants(acronym: str):
|
|||||||
},
|
},
|
||||||
...
|
...
|
||||||
]
|
]
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||||
return [etud.to_dict_short() for etud in dept.etudiants]
|
return [etud.to_dict_short() for etud in dept.etudiants]
|
||||||
@ -207,7 +195,7 @@ def departement_etudiants(acronym: str):
|
|||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@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é.
|
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
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@as_json
|
||||||
def departement_formsemestres_ids(acronym: str):
|
def dept_formsemestres_ids(acronym: str):
|
||||||
"""Liste des ids de tous les formsemestres du département."""
|
"""liste des ids formsemestre du département"""
|
||||||
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||||
return [formsemestre.id for formsemestre in dept.formsemestres]
|
return [formsemestre.id for formsemestre in dept.formsemestres]
|
||||||
|
|
||||||
@ -231,39 +219,100 @@ def departement_formsemestres_ids(acronym: str):
|
|||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@as_json
|
||||||
def departement_formsemestres_ids_by_id(dept_id: int):
|
def dept_formsemestres_ids_by_id(dept_id: int):
|
||||||
"""Liste des ids de tous les formsemestres du département."""
|
"""liste des ids formsemestre du département"""
|
||||||
dept = Departement.query.get_or_404(dept_id)
|
dept = Departement.query.get_or_404(dept_id)
|
||||||
return [formsemestre.id for formsemestre in dept.formsemestres]
|
return [formsemestre.id for formsemestre in dept.formsemestres]
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/departement/<string:acronym>/formsemestres_courants")
|
@bp.route("/departement/<string:acronym>/formsemestres_courants")
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
@as_json
|
||||||
|
def dept_formsemestres_courants(acronym: str):
|
||||||
|
"""
|
||||||
|
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()
|
||||||
|
date_courante = request.args.get("date_courante")
|
||||||
|
if date_courante:
|
||||||
|
test_date = datetime.fromisoformat(date_courante)
|
||||||
|
else:
|
||||||
|
test_date = app.db.func.now()
|
||||||
|
# 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.order_by(
|
||||||
|
FormSemestre.date_debut.desc(),
|
||||||
|
FormSemestre.modalite,
|
||||||
|
FormSemestre.semestre_id,
|
||||||
|
FormSemestre.titre,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/departement/id/<int:dept_id>/formsemestres_courants")
|
@bp.route("/departement/id/<int:dept_id>/formsemestres_courants")
|
||||||
@login_required
|
@login_required
|
||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@as_json
|
||||||
def departement_formsemestres_courants(acronym: str = "", dept_id: int | None = None):
|
def dept_formsemestres_courants_by_id(dept_id: int):
|
||||||
"""
|
"""
|
||||||
Liste les formsemestres du département indiqué (par son acronyme ou son id)
|
Liste des semestres actifs d'un département d'id donné
|
||||||
contenant la date courante, ou à défaut celle indiquée en argument
|
|
||||||
(au format ISO).
|
|
||||||
|
|
||||||
QUERY
|
|
||||||
-----
|
|
||||||
date_courante:<string:date_courante>
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
dept = (
|
# Le département, spécifié par un id ou un acronyme
|
||||||
Departement.query.filter_by(acronym=acronym).first_or_404()
|
dept = Departement.query.get_or_404(dept_id)
|
||||||
if acronym
|
|
||||||
else Departement.query.get_or_404(dept_id)
|
|
||||||
)
|
|
||||||
date_courante = request.args.get("date_courante")
|
date_courante = request.args.get("date_courante")
|
||||||
date_courante = datetime.fromisoformat(date_courante) if date_courante else None
|
if date_courante:
|
||||||
return [
|
test_date = datetime.fromisoformat(date_courante)
|
||||||
formsemestre.to_dict_api()
|
else:
|
||||||
for formsemestre in FormSemestre.get_dept_formsemestres_courants(
|
test_date = app.db.func.now()
|
||||||
dept, date_courante
|
# 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]
|
||||||
|
@ -1,20 +1,16 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
"""
|
"""
|
||||||
API : accès aux étudiants
|
API : accès aux étudiants
|
||||||
|
|
||||||
CATEGORY
|
|
||||||
--------
|
|
||||||
Étudiants
|
|
||||||
"""
|
"""
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
from flask import g, request, Response
|
from flask import g, request
|
||||||
from flask_json import as_json
|
from flask_json import as_json
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
@ -22,28 +18,24 @@ from sqlalchemy import desc, func, or_
|
|||||||
from sqlalchemy.dialects.postgresql import VARCHAR
|
from sqlalchemy.dialects.postgresql import VARCHAR
|
||||||
|
|
||||||
import app
|
import app
|
||||||
from app import db, log
|
|
||||||
from app.api import api_bp as bp, api_web_bp
|
from app.api import api_bp as bp, api_web_bp
|
||||||
from app.api import tools
|
from app.api import tools
|
||||||
from app.api import api_permission_required as permission_required
|
|
||||||
from app.but import bulletin_but_court
|
from app.but import bulletin_but_court
|
||||||
from app.decorators import scodoc
|
from app.decorators import scodoc, permission_required
|
||||||
from app.models import (
|
from app.models import (
|
||||||
Admission,
|
Admission,
|
||||||
Departement,
|
Departement,
|
||||||
EtudAnnotation,
|
|
||||||
FormSemestreInscription,
|
FormSemestreInscription,
|
||||||
FormSemestre,
|
FormSemestre,
|
||||||
Identite,
|
Identite,
|
||||||
ScolarNews,
|
|
||||||
)
|
)
|
||||||
from app.scodoc import sco_bulletins
|
from app.scodoc import sco_bulletins
|
||||||
from app.scodoc import sco_groups
|
from app.scodoc import sco_groups
|
||||||
from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud
|
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.sco_permissions import Permission
|
||||||
from app.scodoc import sco_photos
|
|
||||||
from app.scodoc.sco_utils import json_error, suppress_accents
|
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
|
import app.scodoc.sco_utils as scu
|
||||||
|
|
||||||
# Un exemple:
|
# Un exemple:
|
||||||
@ -59,32 +51,6 @@ import app.scodoc.sco_utils as scu
|
|||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
def _get_etud_by_code(
|
|
||||||
code_type: str, code: str, dept: Departement
|
|
||||||
) -> tuple[bool, Response | Identite]:
|
|
||||||
"""Get etud, using etudid, NIP or INE
|
|
||||||
Returns True, etud if ok, or False, error response.
|
|
||||||
"""
|
|
||||||
if code_type == "nip":
|
|
||||||
query = Identite.query.filter_by(code_nip=code)
|
|
||||||
elif code_type == "etudid":
|
|
||||||
try:
|
|
||||||
etudid = int(code)
|
|
||||||
except ValueError:
|
|
||||||
return False, json_error(404, "invalid etudid type")
|
|
||||||
query = Identite.query.filter_by(id=etudid)
|
|
||||||
elif code_type == "ine":
|
|
||||||
query = Identite.query.filter_by(code_ine=code)
|
|
||||||
else:
|
|
||||||
return False, json_error(404, "invalid code_type")
|
|
||||||
if dept:
|
|
||||||
query = query.filter_by(dept_id=dept.id)
|
|
||||||
etud = query.first()
|
|
||||||
if etud is None:
|
|
||||||
return False, json_error(404, message="etudiant inexistant")
|
|
||||||
return True, etud
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/etudiants/courants", defaults={"long": False})
|
@bp.route("/etudiants/courants", defaults={"long": False})
|
||||||
@bp.route("/etudiants/courants/long", defaults={"long": True})
|
@bp.route("/etudiants/courants/long", defaults={"long": True})
|
||||||
@api_web_bp.route("/etudiants/courants", defaults={"long": False})
|
@api_web_bp.route("/etudiants/courants", defaults={"long": False})
|
||||||
@ -93,20 +59,14 @@ def _get_etud_by_code(
|
|||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@as_json
|
||||||
def etudiants_courants(long: bool = False):
|
def etudiants_courants(long=False):
|
||||||
"""
|
"""
|
||||||
La liste des étudiants des semestres "courants".
|
La liste des étudiants des semestres "courants" (tous départements)
|
||||||
Considère tous les départements dans lesquels l'utilisateur a la
|
(date du jour comprise dans la période couverte par le sem.)
|
||||||
permission `ScoView` (donc tous si le dépt. du rôle est `None`),
|
dans lesquels l'utilisateur a la permission ScoView
|
||||||
et les formsemestres contenant la date courante,
|
(donc tous si le dept du rôle est None).
|
||||||
ou à défaut celle indiquée en argument (au format ISO).
|
|
||||||
|
|
||||||
QUERY
|
|
||||||
-----
|
|
||||||
date_courante:<string:date_courante>
|
|
||||||
|
|
||||||
Exemple de résultat :
|
Exemple de résultat :
|
||||||
```json
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": 1234,
|
"id": 1234,
|
||||||
@ -119,7 +79,6 @@ def etudiants_courants(long: bool = False):
|
|||||||
}
|
}
|
||||||
...
|
...
|
||||||
]
|
]
|
||||||
```
|
|
||||||
|
|
||||||
En format "long": voir documentation.
|
En format "long": voir documentation.
|
||||||
|
|
||||||
@ -142,10 +101,7 @@ def etudiants_courants(long: bool = False):
|
|||||||
or_(Departement.acronym == acronym for acronym in allowed_depts)
|
or_(Departement.acronym == acronym for acronym in allowed_depts)
|
||||||
)
|
)
|
||||||
if long:
|
if long:
|
||||||
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
data = [etud.to_dict_api() for etud in etuds]
|
||||||
data = [
|
|
||||||
etud.to_dict_api(restrict=restrict, with_annotations=True) for etud in etuds
|
|
||||||
]
|
|
||||||
else:
|
else:
|
||||||
data = [etud.to_dict_short() for etud in etuds]
|
data = [etud.to_dict_short() for etud in etuds]
|
||||||
return data
|
return data
|
||||||
@ -165,13 +121,10 @@ def etudiant(etudid: int = None, nip: str = None, ine: str = None):
|
|||||||
"""
|
"""
|
||||||
Retourne les informations de l'étudiant correspondant, ou 404 si non trouvé.
|
Retourne les informations de l'étudiant correspondant, ou 404 si non trouvé.
|
||||||
|
|
||||||
PARAMS
|
|
||||||
------
|
|
||||||
etudid : l'etudid de l'étudiant
|
etudid : l'etudid de l'étudiant
|
||||||
nip : le code nip de l'étudiant
|
nip : le code nip de l'étudiant
|
||||||
ine : le code ine 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.
|
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.
|
Si plusieurs objets ont le même code, on ramène le plus récemment inscrit.
|
||||||
"""
|
"""
|
||||||
@ -182,8 +135,8 @@ def etudiant(etudid: int = None, nip: str = None, ine: str = None):
|
|||||||
404,
|
404,
|
||||||
message="étudiant inconnu",
|
message="étudiant inconnu",
|
||||||
)
|
)
|
||||||
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
|
||||||
return etud.to_dict_api(restrict=restrict, with_annotations=True)
|
return etud.to_dict_api()
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/etudiant/etudid/<int:etudid>/photo")
|
@bp.route("/etudiant/etudid/<int:etudid>/photo")
|
||||||
@ -195,18 +148,11 @@ def etudiant(etudid: int = None, nip: str = None, ine: str = None):
|
|||||||
@login_required
|
@login_required
|
||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@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.
|
Retourne la photo de l'étudiant
|
||||||
Le paramètre `size` peut prendre la valeur `small` (taille réduite, hauteur
|
correspondant ou un placeholder si non existant.
|
||||||
environ 90 pixels) ou `orig` (défaut, image de la taille originale).
|
|
||||||
|
|
||||||
QUERY
|
|
||||||
-----
|
|
||||||
size:<string:size>
|
|
||||||
|
|
||||||
PARAMS
|
|
||||||
------
|
|
||||||
etudid : l'etudid de l'étudiant
|
etudid : l'etudid de l'étudiant
|
||||||
nip : le code nip de l'étudiant
|
nip : le code nip de l'étudiant
|
||||||
ine : le code ine de l'étudiant
|
ine : le code ine de l'étudiant
|
||||||
@ -236,7 +182,7 @@ def etudiant_get_photo_image(etudid: int = None, nip: str = None, ine: str = Non
|
|||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.EtudChangeAdr)
|
@permission_required(Permission.EtudChangeAdr)
|
||||||
@as_json
|
@as_json
|
||||||
def etudiant_set_photo_image(etudid: int = None):
|
def set_photo_image(etudid: int = None):
|
||||||
"""Enregistre la photo de l'étudiant."""
|
"""Enregistre la photo de l'étudiant."""
|
||||||
allowed_depts = current_user.get_depts_with_permission(Permission.EtudChangeAdr)
|
allowed_depts = current_user.get_depts_with_permission(Permission.EtudChangeAdr)
|
||||||
query = Identite.query.filter_by(id=etudid)
|
query = Identite.query.filter_by(id=etudid)
|
||||||
@ -279,12 +225,9 @@ def etudiant_set_photo_image(etudid: int = None):
|
|||||||
@as_json
|
@as_json
|
||||||
def etudiants(etudid: int = None, nip: str = None, ine: str = None):
|
def etudiants(etudid: int = None, nip: str = None, ine: str = None):
|
||||||
"""
|
"""
|
||||||
Info sur le ou les étudiants correspondants.
|
Info sur le ou les étudiants correspondant. Comme /etudiant mais renvoie
|
||||||
|
toujours une liste.
|
||||||
Comme `/etudiant` mais renvoie toujours une liste.
|
|
||||||
|
|
||||||
Si non trouvé, liste vide, pas d'erreur.
|
Si non trouvé, liste vide, pas d'erreur.
|
||||||
|
|
||||||
Dans 99% des cas, la liste contient un seul étudiant, mais si l'étudiant a
|
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.).
|
été inscrit dans plusieurs départements, on a plusieurs objets (1 par dept.).
|
||||||
"""
|
"""
|
||||||
@ -305,10 +248,7 @@ def etudiants(etudid: int = None, nip: str = None, ine: str = None):
|
|||||||
query = query.join(Departement).filter(
|
query = query.join(Departement).filter(
|
||||||
or_(Departement.acronym == acronym for acronym in allowed_depts)
|
or_(Departement.acronym == acronym for acronym in allowed_depts)
|
||||||
)
|
)
|
||||||
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
return [etud.to_dict_api() for etud in query]
|
||||||
return [
|
|
||||||
etud.to_dict_api(restrict=restrict, with_annotations=True) for etud in query
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/etudiants/name/<string:start>")
|
@bp.route("/etudiants/name/<string:start>")
|
||||||
@ -317,9 +257,8 @@ def etudiants(etudid: int = None, nip: str = None, ine: str = None):
|
|||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@as_json
|
||||||
def etudiants_by_name(start: str = "", min_len=3, limit=32):
|
def etudiants_by_name(start: str = "", min_len=3, limit=32):
|
||||||
"""Liste des étudiants dont le nom débute par `start`.
|
"""Liste des étudiants dont le nom débute par start.
|
||||||
|
Si start fait moins de min_len=3 caractères, liste vide.
|
||||||
Si `start` fait moins de `min_len=3` caractères, liste vide.
|
|
||||||
La casse et les accents sont ignorés.
|
La casse et les accents sont ignorés.
|
||||||
"""
|
"""
|
||||||
if len(start) < min_len:
|
if len(start) < min_len:
|
||||||
@ -336,11 +275,7 @@ def etudiants_by_name(start: str = "", min_len=3, limit=32):
|
|||||||
)
|
)
|
||||||
etuds = query.order_by(Identite.nom, Identite.prenom).limit(limit)
|
etuds = query.order_by(Identite.nom, Identite.prenom).limit(limit)
|
||||||
# Note: on raffine le tri pour les caractères spéciaux et nom usuel ici:
|
# Note: on raffine le tri pour les caractères spéciaux et nom usuel ici:
|
||||||
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
return [etud.to_dict_api() for etud in sorted(etuds, key=attrgetter("sort_key"))]
|
||||||
return [
|
|
||||||
etud.to_dict_api(restrict=restrict)
|
|
||||||
for etud in sorted(etuds, key=attrgetter("sort_key"))
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/etudiant/etudid/<int:etudid>/formsemestres")
|
@bp.route("/etudiant/etudid/<int:etudid>/formsemestres")
|
||||||
@ -354,13 +289,13 @@ def etudiants_by_name(start: str = "", min_len=3, limit=32):
|
|||||||
@as_json
|
@as_json
|
||||||
def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None):
|
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.
|
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`.
|
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:
|
if etudid is not None:
|
||||||
q_etud = Identite.query.filter_by(id=etudid)
|
q_etud = Identite.query.filter_by(id=etudid)
|
||||||
@ -421,43 +356,48 @@ def bulletin(
|
|||||||
code_type: str = "etudid",
|
code_type: str = "etudid",
|
||||||
code: str = None,
|
code: str = None,
|
||||||
formsemestre_id: int = None,
|
formsemestre_id: int = None,
|
||||||
version: str = "selectedevals",
|
version: str = "long",
|
||||||
pdf: bool = False,
|
pdf: bool = False,
|
||||||
with_img_signatures_pdf: bool = True,
|
with_img_signatures_pdf: bool = True,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Retourne le bulletin d'un étudiant dans un formsemestre.
|
Retourne le bulletin d'un étudiant dans un formsemestre.
|
||||||
|
|
||||||
PARAMS
|
|
||||||
------
|
|
||||||
formsemestre_id : l'id d'un formsemestre
|
formsemestre_id : l'id d'un formsemestre
|
||||||
code_type : "etudid", "nip" ou "ine"
|
code_type : "etudid", "nip" ou "ine"
|
||||||
code : valeur du code INE, NIP ou etudid, selon code_type.
|
code : valeur du code INE, NIP ou etudid, selon code_type.
|
||||||
version : type de bulletin (par défaut, "selectedevals"): short, long, selectedevals, butcourt
|
version : type de bulletin (par défaut, "long"): short, long, selectedevals, butcourt
|
||||||
pdf : si spécifié, bulletin au format PDF (et non JSON).
|
pdf : si spécifié, bulletin au format PDF (et non JSON).
|
||||||
|
|
||||||
|
Exemple de résultat : voir https://scodoc.org/ScoDoc9API/#bulletin
|
||||||
"""
|
"""
|
||||||
if version == "pdf":
|
if version == "pdf":
|
||||||
version = "long"
|
version = "long"
|
||||||
pdf = True
|
pdf = True
|
||||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
if version not in scu.BULLETINS_VERSIONS_BUT:
|
||||||
if version not in (
|
|
||||||
scu.BULLETINS_VERSIONS_BUT
|
|
||||||
if formsemestre.formation.is_apc()
|
|
||||||
else scu.BULLETINS_VERSIONS
|
|
||||||
):
|
|
||||||
return json_error(404, "version invalide")
|
return json_error(404, "version invalide")
|
||||||
if formsemestre.bul_hide_xml and pdf:
|
# return f"{code_type}={code}, version={version}, pdf={pdf}"
|
||||||
return json_error(403, "bulletin non disponible")
|
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first_or_404()
|
||||||
# note: la version json est réduite si bul_hide_xml
|
|
||||||
dept = Departement.query.filter_by(id=formsemestre.dept_id).first_or_404()
|
dept = Departement.query.filter_by(id=formsemestre.dept_id).first_or_404()
|
||||||
if g.scodoc_dept and dept.acronym != g.scodoc_dept:
|
if g.scodoc_dept and dept.acronym != g.scodoc_dept:
|
||||||
return json_error(404, "formsemestre inexistant")
|
return json_error(404, "formsemestre inexistant")
|
||||||
app.set_sco_dept(dept.acronym)
|
app.set_sco_dept(dept.acronym)
|
||||||
|
|
||||||
ok, etud = _get_etud_by_code(code_type, code, dept)
|
if code_type == "nip":
|
||||||
if not ok:
|
query = Identite.query.filter_by(code_nip=code, dept_id=dept.id)
|
||||||
return etud # json error
|
elif code_type == "etudid":
|
||||||
|
try:
|
||||||
|
etudid = int(code)
|
||||||
|
except ValueError:
|
||||||
|
return json_error(404, "invalid etudid type")
|
||||||
|
query = Identite.query.filter_by(id=etudid)
|
||||||
|
elif code_type == "ine":
|
||||||
|
query = Identite.query.filter_by(code_ine=code, dept_id=dept.id)
|
||||||
|
else:
|
||||||
|
return json_error(404, "invalid code_type")
|
||||||
|
etud = query.first()
|
||||||
|
if etud is None:
|
||||||
|
return json_error(404, message="etudiant inexistant")
|
||||||
|
|
||||||
if version == "butcourt":
|
if version == "butcourt":
|
||||||
if pdf:
|
if pdf:
|
||||||
@ -478,9 +418,9 @@ def bulletin(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/groups")
|
@bp.route(
|
||||||
@api_web_bp.route(
|
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/groups",
|
||||||
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/groups"
|
methods=["GET"],
|
||||||
)
|
)
|
||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@ -489,13 +429,10 @@ def etudiant_groups(formsemestre_id: int, etudid: int = None):
|
|||||||
"""
|
"""
|
||||||
Retourne la liste des groupes auxquels appartient l'étudiant dans le formsemestre indiqué
|
Retourne la liste des groupes auxquels appartient l'étudiant dans le formsemestre indiqué
|
||||||
|
|
||||||
PARAMS
|
|
||||||
------
|
|
||||||
formsemestre_id : l'id d'un formsemestre
|
formsemestre_id : l'id d'un formsemestre
|
||||||
etudid : l'etudid d'un étudiant
|
etudid : l'etudid d'un étudiant
|
||||||
|
|
||||||
Exemple de résultat :
|
Exemple de résultat :
|
||||||
```json
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"partition_id": 1,
|
"partition_id": 1,
|
||||||
@ -520,8 +457,8 @@ def etudiant_groups(formsemestre_id: int, etudid: int = None):
|
|||||||
"group_name": "A"
|
"group_name": "A"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
@ -538,204 +475,3 @@ def etudiant_groups(formsemestre_id: int, etudid: int = None):
|
|||||||
data = sco_groups.get_etud_groups(etud.id, formsemestre.id)
|
data = sco_groups.get_etud_groups(etud.id, formsemestre.id)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/etudiant/create", methods=["POST"], defaults={"force": False})
|
|
||||||
@bp.route("/etudiant/create/force", methods=["POST"], defaults={"force": True})
|
|
||||||
@api_web_bp.route("/etudiant/create", methods=["POST"], defaults={"force": False})
|
|
||||||
@api_web_bp.route("/etudiant/create/force", methods=["POST"], defaults={"force": True})
|
|
||||||
@scodoc
|
|
||||||
@permission_required(Permission.EtudInscrit)
|
|
||||||
@as_json
|
|
||||||
def etudiant_create(force=False):
|
|
||||||
"""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
|
|
||||||
dept = args.get("dept", None)
|
|
||||||
if not dept:
|
|
||||||
return scu.json_error(400, "dept requis")
|
|
||||||
dept_o = Departement.query.filter_by(acronym=dept).first()
|
|
||||||
if not dept_o:
|
|
||||||
return scu.json_error(400, "dept invalide")
|
|
||||||
if g.scodoc_dept and g.scodoc_dept_id != dept_o.id:
|
|
||||||
return scu.json_error(400, "dept invalide (route departementale)")
|
|
||||||
else:
|
|
||||||
app.set_sco_dept(dept)
|
|
||||||
args["dept_id"] = dept_o.id
|
|
||||||
# vérifie que le département de création est bien autorisé
|
|
||||||
if not current_user.has_permission(Permission.EtudInscrit, dept):
|
|
||||||
return json_error(403, "departement non autorisé")
|
|
||||||
nom = args.get("nom", None)
|
|
||||||
prenom = args.get("prenom", None)
|
|
||||||
ok, homonyms = sco_etud.check_nom_prenom_homonyms(nom=nom, prenom=prenom)
|
|
||||||
if not ok:
|
|
||||||
return scu.json_error(400, "nom ou prénom invalide")
|
|
||||||
if len(homonyms) > 0 and not force:
|
|
||||||
return scu.json_error(
|
|
||||||
400, f"{len(homonyms)} homonymes détectés. Vous pouvez utiliser /force."
|
|
||||||
)
|
|
||||||
etud = Identite.create_etud(**args)
|
|
||||||
db.session.flush()
|
|
||||||
# --- Données admission
|
|
||||||
admission_args = args.get("admission", None)
|
|
||||||
if admission_args:
|
|
||||||
etud.admission.from_dict(admission_args)
|
|
||||||
# --- Adresse
|
|
||||||
adresses = args.get("adresses", [])
|
|
||||||
if adresses:
|
|
||||||
# ne prend en compte que la première adresse
|
|
||||||
# car si la base est concue pour avoir plusieurs adresses par étudiant,
|
|
||||||
# l'application n'en gère plus qu'une seule.
|
|
||||||
adresse = etud.adresses.first()
|
|
||||||
adresse.from_dict(adresses[0])
|
|
||||||
|
|
||||||
# Poste une nouvelle dans le département concerné:
|
|
||||||
ScolarNews.add(
|
|
||||||
typ=ScolarNews.NEWS_INSCR,
|
|
||||||
text=f"Nouvel étudiant {etud.html_link_fiche()}",
|
|
||||||
url=etud.url_fiche(),
|
|
||||||
max_frequency=0,
|
|
||||||
dept_id=dept_o.id,
|
|
||||||
)
|
|
||||||
db.session.commit()
|
|
||||||
# Note: je ne comprends pas pourquoi un refresh est nécessaire ici
|
|
||||||
# sans ce refresh, etud.__dict__ est incomplet (pas de 'nom').
|
|
||||||
db.session.refresh(etud)
|
|
||||||
|
|
||||||
r = etud.to_dict_api(restrict=False) # pas de restriction, on vient de le créer
|
|
||||||
return r
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/etudiant/<string:code_type>/<string:code>/edit", methods=["POST"])
|
|
||||||
@api_web_bp.route("/etudiant/<string:code_type>/<string:code>/edit", methods=["POST"])
|
|
||||||
@scodoc
|
|
||||||
@permission_required(Permission.EtudInscrit)
|
|
||||||
@as_json
|
|
||||||
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
|
|
||||||
"""
|
|
||||||
ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept)
|
|
||||||
if not ok:
|
|
||||||
return etud # json error
|
|
||||||
#
|
|
||||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
|
||||||
etud.from_dict(args)
|
|
||||||
admission_args = args.get("admission", None)
|
|
||||||
if admission_args:
|
|
||||||
etud.admission.from_dict(admission_args)
|
|
||||||
# --- Adresse
|
|
||||||
adresses = args.get("adresses", [])
|
|
||||||
if adresses:
|
|
||||||
# ne prend en compte que la première adresse
|
|
||||||
# car si la base est concue pour avoir plusieurs adresses par étudiant,
|
|
||||||
# l'application n'en gère plus qu'une seule.
|
|
||||||
adresse = etud.adresses.first()
|
|
||||||
adresse.from_dict(adresses[0])
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
# Note: je ne comprends pas pourquoi un refresh est nécessaire ici
|
|
||||||
# sans ce refresh, etud.__dict__ est incomplet (pas de 'nom').
|
|
||||||
db.session.refresh(etud)
|
|
||||||
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
|
||||||
r = etud.to_dict_api(restrict=restrict)
|
|
||||||
return r
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/etudiant/<string:code_type>/<string:code>/annotation", methods=["POST"])
|
|
||||||
@api_web_bp.route(
|
|
||||||
"/etudiant/<string:code_type>/<string:code>/annotation", methods=["POST"]
|
|
||||||
)
|
|
||||||
@scodoc
|
|
||||||
@permission_required(Permission.EtudInscrit) # il faut en plus ViewEtudData
|
|
||||||
@as_json
|
|
||||||
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
|
|
||||||
}
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
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)
|
|
||||||
if not ok:
|
|
||||||
return etud # json error
|
|
||||||
#
|
|
||||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
|
||||||
comment = args.get("comment", None)
|
|
||||||
if not isinstance(comment, str):
|
|
||||||
return json_error(404, "invalid comment (expected string)")
|
|
||||||
if len(comment) > scu.MAX_TEXT_LEN:
|
|
||||||
return json_error(404, "invalid comment (too large)")
|
|
||||||
annotation = EtudAnnotation(comment=comment, author=current_user.user_name)
|
|
||||||
etud.annotations.append(annotation)
|
|
||||||
db.session.add(etud)
|
|
||||||
db.session.commit()
|
|
||||||
log(f"etudiant_annotation/{etud.id}/{annotation.id}")
|
|
||||||
return annotation.to_dict()
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route(
|
|
||||||
"/etudiant/<string:code_type>/<string:code>/annotation/<int:annotation_id>/delete",
|
|
||||||
methods=["POST"],
|
|
||||||
)
|
|
||||||
@api_web_bp.route(
|
|
||||||
"/etudiant/<string:code_type>/<string:code>/annotation/<int:annotation_id>/delete",
|
|
||||||
methods=["POST"],
|
|
||||||
)
|
|
||||||
@login_required
|
|
||||||
@scodoc
|
|
||||||
@as_json
|
|
||||||
@permission_required(Permission.EtudInscrit)
|
|
||||||
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
|
|
||||||
"""
|
|
||||||
ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept)
|
|
||||||
if not ok:
|
|
||||||
return etud # json error
|
|
||||||
annotation = EtudAnnotation.query.filter_by(
|
|
||||||
etudid=etud.id, id=annotation_id
|
|
||||||
).first()
|
|
||||||
if annotation is None:
|
|
||||||
return json_error(404, "annotation not found")
|
|
||||||
log(f"etudiant_annotation_delete/{etud.id}/{annotation.id}")
|
|
||||||
db.session.delete(annotation)
|
|
||||||
db.session.commit()
|
|
||||||
return "ok"
|
|
||||||
|
@ -1,15 +1,11 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
"""
|
"""
|
||||||
ScoDoc 9 API : accès aux évaluations
|
ScoDoc 9 API : accès aux évaluations
|
||||||
|
|
||||||
CATEGORY
|
|
||||||
--------
|
|
||||||
Évaluations
|
|
||||||
"""
|
"""
|
||||||
from flask import g, request
|
from flask import g, request
|
||||||
from flask_json import as_json
|
from flask_json import as_json
|
||||||
@ -18,8 +14,7 @@ from flask_login import current_user, login_required
|
|||||||
import app
|
import app
|
||||||
from app import log, db
|
from app import log, db
|
||||||
from app.api import api_bp as bp, api_web_bp
|
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, permission_required
|
||||||
from app.decorators import scodoc
|
|
||||||
from app.models import Evaluation, ModuleImpl, FormSemestre
|
from app.models import Evaluation, ModuleImpl, FormSemestre
|
||||||
from app.scodoc import sco_evaluation_db, sco_saisie_notes
|
from app.scodoc import sco_evaluation_db, sco_saisie_notes
|
||||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
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):
|
def get_evaluation(evaluation_id: int):
|
||||||
"""Description d'une évaluation.
|
"""Description d'une évaluation.
|
||||||
|
|
||||||
DATA
|
|
||||||
----
|
|
||||||
```json
|
|
||||||
{
|
{
|
||||||
'coefficient': 1.0,
|
'coefficient': 1.0,
|
||||||
'date_debut': '2016-01-04T08:30:00',
|
'date_debut': '2016-01-04T08:30:00',
|
||||||
'date_fin': '2016-01-04T12:30:00',
|
'date_fin': '2016-01-04T12:30:00',
|
||||||
'description': 'TP Température',
|
'description': 'TP NI9219 Température',
|
||||||
'evaluation_type': 0,
|
'evaluation_type': 0,
|
||||||
'id': 15797,
|
'id': 15797,
|
||||||
'moduleimpl_id': 1234,
|
'moduleimpl_id': 1234,
|
||||||
'note_max': 20.0,
|
'note_max': 20.0,
|
||||||
'numero': 3,
|
'numero': 3,
|
||||||
'poids': {
|
'poids': {
|
||||||
'UE1.1': 1.0,
|
'UE1.1': 1.0,
|
||||||
'UE1.2': 1.0,
|
'UE1.2': 1.0,
|
||||||
'UE1.3': 1.0
|
'UE1.3': 1.0
|
||||||
},
|
},
|
||||||
'publish_incomplete': False,
|
'publish_incomplete': False,
|
||||||
'visibulletin': True
|
'visibulletin': True
|
||||||
}
|
}
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
query = Evaluation.query.filter_by(id=evaluation_id)
|
query = Evaluation.query.filter_by(id=evaluation_id)
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
@ -76,18 +67,22 @@ def get_evaluation(evaluation_id: int):
|
|||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@as_json
|
||||||
def moduleimpl_evaluations(moduleimpl_id: int):
|
def 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
|
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)
|
query = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id)
|
||||||
return [evaluation.to_dict_api() for evaluation in modimpl.evaluations]
|
if g.scodoc_dept:
|
||||||
|
query = (
|
||||||
|
query.join(ModuleImpl)
|
||||||
|
.join(FormSemestre)
|
||||||
|
.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
)
|
||||||
|
return [e.to_dict_api() for e in query]
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/evaluation/<int:evaluation_id>/notes")
|
@bp.route("/evaluation/<int:evaluation_id>/notes")
|
||||||
@ -98,36 +93,30 @@ def moduleimpl_evaluations(moduleimpl_id: int):
|
|||||||
@as_json
|
@as_json
|
||||||
def evaluation_notes(evaluation_id: int):
|
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
|
evaluation_id : l'id de l'évaluation
|
||||||
|
|
||||||
Exemple de résultat :
|
Exemple de résultat :
|
||||||
```json
|
{
|
||||||
{
|
"11": {
|
||||||
"11": {
|
|
||||||
"etudid": 11,
|
"etudid": 11,
|
||||||
"evaluation_id": 1,
|
"evaluation_id": 1,
|
||||||
"value": 15.0,
|
"value": 15.0,
|
||||||
"note_max" : 20.0,
|
|
||||||
"comment": "",
|
"comment": "",
|
||||||
"date": "2024-07-19T19:08:44+02:00",
|
"date": "Wed, 20 Apr 2022 06:49:05 GMT",
|
||||||
"uid": 2
|
"uid": 2
|
||||||
},
|
},
|
||||||
"12": {
|
"12": {
|
||||||
"etudid": 12,
|
"etudid": 12,
|
||||||
"evaluation_id": 1,
|
"evaluation_id": 1,
|
||||||
"value": "ABS",
|
"value": 12.0,
|
||||||
"note_max" : 20.0,
|
|
||||||
"comment": "",
|
"comment": "",
|
||||||
"date": "2024-07-19T19:08:44+02:00",
|
"date": "Wed, 20 Apr 2022 06:49:06 GMT",
|
||||||
"uid": 2
|
"uid": 2
|
||||||
},
|
},
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
query = Evaluation.query.filter_by(id=evaluation_id)
|
query = Evaluation.query.filter_by(id=evaluation_id)
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
@ -159,20 +148,15 @@ def evaluation_notes(evaluation_id: int):
|
|||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.EnsView)
|
@permission_required(Permission.EnsView)
|
||||||
@as_json
|
@as_json
|
||||||
def evaluation_set_notes(evaluation_id: int): # evaluation-notes-set
|
def evaluation_set_notes(evaluation_id: int):
|
||||||
"""Écriture de notes dans une évaluation.
|
"""Écriture de notes dans une évaluation.
|
||||||
|
The request content type should be "application/json",
|
||||||
DATA
|
and contains:
|
||||||
----
|
|
||||||
```json
|
|
||||||
{
|
{
|
||||||
'notes' : [ [etudid, value], ... ],
|
'notes' : [ [etudid, value], ... ],
|
||||||
'comment' : optional string
|
'comment' : optional string
|
||||||
}
|
}
|
||||||
```
|
Result:
|
||||||
|
|
||||||
Résultat:
|
|
||||||
|
|
||||||
- nb_changed: nombre de notes changées
|
- nb_changed: nombre de notes changées
|
||||||
- nb_suppress: nombre de notes effacées
|
- nb_suppress: nombre de notes effacées
|
||||||
- etudids_with_decision: liste des etudiants dont la note a changé
|
- etudids_with_decision: liste des etudiants dont la note a changé
|
||||||
@ -207,9 +191,8 @@ def evaluation_set_notes(evaluation_id: int): # evaluation-notes-set
|
|||||||
@as_json
|
@as_json
|
||||||
def evaluation_create(moduleimpl_id: int):
|
def evaluation_create(moduleimpl_id: int):
|
||||||
"""Création d'une évaluation.
|
"""Création d'une évaluation.
|
||||||
|
The request content type should be "application/json",
|
||||||
DATA
|
and contains:
|
||||||
----
|
|
||||||
{
|
{
|
||||||
"description" : str,
|
"description" : str,
|
||||||
"evaluation_type" : int, // {0,1,2} default 0 (normale)
|
"evaluation_type" : int, // {0,1,2} default 0 (normale)
|
||||||
@ -222,8 +205,7 @@ def evaluation_create(moduleimpl_id: int):
|
|||||||
"coefficient" : float, // si non spécifié, 1.0
|
"coefficient" : float, // si non spécifié, 1.0
|
||||||
"poids" : { ue_id : poids } // optionnel
|
"poids" : { ue_id : poids } // optionnel
|
||||||
}
|
}
|
||||||
|
Result: l'évaluation créée.
|
||||||
Résultat: l'évaluation créée.
|
|
||||||
"""
|
"""
|
||||||
moduleimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id)
|
moduleimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id)
|
||||||
if not moduleimpl.can_edit_evaluation(current_user):
|
if not moduleimpl.can_edit_evaluation(current_user):
|
||||||
@ -273,7 +255,7 @@ def evaluation_create(moduleimpl_id: int):
|
|||||||
@as_json
|
@as_json
|
||||||
def evaluation_delete(evaluation_id: int):
|
def evaluation_delete(evaluation_id: int):
|
||||||
"""Suppression d'une évaluation.
|
"""Suppression d'une évaluation.
|
||||||
Efface aussi toutes ses notes.
|
Efface aussi toutes ses notes
|
||||||
"""
|
"""
|
||||||
query = Evaluation.query.filter_by(id=evaluation_id)
|
query = Evaluation.query.filter_by(id=evaluation_id)
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
|
@ -1,15 +1,11 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
"""
|
"""
|
||||||
ScoDoc 9 API : accès aux formations
|
ScoDoc 9 API : accès aux formations
|
||||||
|
|
||||||
CATEGORY
|
|
||||||
--------
|
|
||||||
Formations
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import flash, g, request
|
from flask import flash, g, request
|
||||||
@ -19,15 +15,12 @@ from flask_login import login_required
|
|||||||
import app
|
import app
|
||||||
from app import db, log
|
from app import db, log
|
||||||
from app.api import api_bp as bp, api_web_bp
|
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.scodoc.sco_utils import json_error
|
||||||
|
from app.decorators import scodoc, permission_required
|
||||||
from app.models import (
|
from app.models import (
|
||||||
ApcNiveau,
|
ApcNiveau,
|
||||||
ApcParcours,
|
ApcParcours,
|
||||||
Formation,
|
Formation,
|
||||||
Module,
|
|
||||||
UniteEns,
|
UniteEns,
|
||||||
)
|
)
|
||||||
from app.scodoc import sco_formations
|
from app.scodoc import sco_formations
|
||||||
@ -42,8 +35,7 @@ from app.scodoc.sco_permissions import Permission
|
|||||||
@as_json
|
@as_json
|
||||||
def formations():
|
def formations():
|
||||||
"""
|
"""
|
||||||
Retourne la liste de toutes les formations (tous départements,
|
Retourne la liste de toutes les formations (tous départements)
|
||||||
sauf si route départementale).
|
|
||||||
"""
|
"""
|
||||||
query = Formation.query
|
query = Formation.query
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
@ -60,10 +52,9 @@ def formations():
|
|||||||
@as_json
|
@as_json
|
||||||
def formations_ids():
|
def formations_ids():
|
||||||
"""
|
"""
|
||||||
Retourne la liste de toutes les id de formations
|
Retourne la liste de toutes les id de formations (tous départements)
|
||||||
(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
|
query = Formation.query
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
@ -79,26 +70,24 @@ def formations_ids():
|
|||||||
@as_json
|
@as_json
|
||||||
def formation_by_id(formation_id: int):
|
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 :
|
Exemple de résultat :
|
||||||
|
{
|
||||||
```json
|
"id": 1,
|
||||||
{
|
"acronyme": "BUT R&T",
|
||||||
"id": 1,
|
"titre_officiel": "Bachelor technologique réseaux et télécommunications",
|
||||||
"acronyme": "BUT R&T",
|
"formation_code": "V1RET",
|
||||||
"titre_officiel": "Bachelor technologique réseaux et télécommunications",
|
"code_specialite": null,
|
||||||
"formation_code": "V1RET",
|
"dept_id": 1,
|
||||||
"code_specialite": null,
|
"titre": "BUT R&T",
|
||||||
"dept_id": 1,
|
"version": 1,
|
||||||
"titre": "BUT R&T",
|
"type_parcours": 700,
|
||||||
"version": 1,
|
"referentiel_competence_id": null,
|
||||||
"type_parcours": 700,
|
"formation_id": 1
|
||||||
"referentiel_competence_id": null,
|
}
|
||||||
"formation_id": 1
|
|
||||||
}
|
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
query = Formation.query.filter_by(id=formation_id)
|
query = Formation.query.filter_by(id=formation_id)
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
@ -130,102 +119,97 @@ def formation_export_by_formation_id(formation_id: int, export_ids=False):
|
|||||||
"""
|
"""
|
||||||
Retourne la formation, avec UE, matières, modules
|
Retourne la formation, avec UE, matières, modules
|
||||||
|
|
||||||
PARAMS
|
|
||||||
------
|
|
||||||
formation_id : l'id d'une formation
|
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 :
|
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",
|
"id": 1,
|
||||||
"numero": 1,
|
"acronyme": "BUT R&T",
|
||||||
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
|
"titre_officiel": "Bachelor technologique r\u00e9seaux et t\u00e9l\u00e9communications",
|
||||||
"type": 0,
|
"formation_code": "V1RET",
|
||||||
"ue_code": "UCOD11",
|
"code_specialite": null,
|
||||||
"ects": 12.0,
|
"dept_id": 1,
|
||||||
"is_external": false,
|
"titre": "BUT R&T",
|
||||||
"code_apogee": "",
|
"version": 1,
|
||||||
"coefficient": 0.0,
|
"type_parcours": 700,
|
||||||
"semestre_idx": 1,
|
"referentiel_competence_id": null,
|
||||||
"color": "#B80004",
|
"formation_id": 1,
|
||||||
"reference": 1,
|
"ue": [
|
||||||
"matiere": [
|
|
||||||
{
|
{
|
||||||
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
|
"acronyme": "RT1.1",
|
||||||
"numero": 1,
|
"numero": 1,
|
||||||
"module": [
|
"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",
|
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
|
||||||
"abbrev": "Init aux r\u00e9seaux informatiques",
|
"numero": 1,
|
||||||
"code": "R101",
|
"module": [
|
||||||
"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",
|
"titre": "Initiation aux r\u00e9seaux informatiques",
|
||||||
"coef": "12.0"
|
"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",
|
"titre": "Se sensibiliser \u00e0 l'hygi\u00e8ne informatique...",
|
||||||
"coef": "4.0"
|
"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)
|
query = Formation.query.filter_by(id=formation_id)
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
@ -248,8 +232,11 @@ def formation_export_by_formation_id(formation_id: int, export_ids=False):
|
|||||||
@as_json
|
@as_json
|
||||||
def referentiel_competences(formation_id: int):
|
def referentiel_competences(formation_id: int):
|
||||||
"""
|
"""
|
||||||
Retourne le référentiel de compétences de la formation
|
Retourne le référentiel de compétences
|
||||||
ou null si pas de référentiel associé.
|
|
||||||
|
formation_id : l'id d'une formation
|
||||||
|
|
||||||
|
return null si pas de référentiel associé.
|
||||||
"""
|
"""
|
||||||
query = Formation.query.filter_by(id=formation_id)
|
query = Formation.query.filter_by(id=formation_id)
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
@ -260,22 +247,16 @@ def referentiel_competences(formation_id: int):
|
|||||||
return formation.referentiel_competence.to_dict()
|
return formation.referentiel_competence.to_dict()
|
||||||
|
|
||||||
|
|
||||||
@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("/formation/ue/<int:ue_id>/set_parcours", methods=["POST"])
|
@api_web_bp.route("/set_ue_parcours/<int:ue_id>", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.EditFormation)
|
@permission_required(Permission.EditFormation)
|
||||||
@as_json
|
@as_json
|
||||||
def ue_set_parcours(ue_id: int):
|
def set_ue_parcours(ue_id: int):
|
||||||
"""Associe UE et parcours BUT.
|
"""Associe UE et parcours BUT.
|
||||||
|
|
||||||
La liste des ids de parcours est passée en argument JSON.
|
La liste des ids de parcours est passée en argument JSON.
|
||||||
|
JSON arg: [parcour_id1, parcour_id2, ...]
|
||||||
DATA
|
|
||||||
----
|
|
||||||
```json
|
|
||||||
[ parcour_id1, parcour_id2, ... ]
|
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
query = UniteEns.query.filter_by(id=ue_id)
|
query = UniteEns.query.filter_by(id=ue_id)
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
@ -288,7 +269,7 @@ def ue_set_parcours(ue_id: int):
|
|||||||
parcours = [
|
parcours = [
|
||||||
ApcParcours.query.get_or_404(int(parcour_id)) for parcour_id in parcours_ids
|
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)
|
ok, error_message = ue.set_parcours(parcours)
|
||||||
if not ok:
|
if not ok:
|
||||||
return json_error(404, error_message)
|
return json_error(404, error_message)
|
||||||
@ -296,19 +277,19 @@ def ue_set_parcours(ue_id: int):
|
|||||||
|
|
||||||
|
|
||||||
@bp.route(
|
@bp.route(
|
||||||
"/formation/ue/<int:ue_id>/assoc_niveau/<int:niveau_id>",
|
"/assoc_ue_niveau/<int:ue_id>/<int:niveau_id>",
|
||||||
methods=["POST"],
|
methods=["POST"],
|
||||||
)
|
)
|
||||||
@api_web_bp.route(
|
@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"],
|
methods=["POST"],
|
||||||
)
|
)
|
||||||
@login_required
|
@login_required
|
||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.EditFormation)
|
@permission_required(Permission.EditFormation)
|
||||||
@as_json
|
@as_json
|
||||||
def ue_assoc_niveau(ue_id: int, niveau_id: int):
|
def assoc_ue_niveau(ue_id: int, niveau_id: int):
|
||||||
"""Associe l'UE au niveau de compétence."""
|
"""Associe l'UE au niveau de compétence"""
|
||||||
query = UniteEns.query.filter_by(id=ue_id)
|
query = UniteEns.query.filter_by(id=ue_id)
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
@ -325,278 +306,30 @@ def ue_assoc_niveau(ue_id: int, niveau_id: int):
|
|||||||
|
|
||||||
|
|
||||||
@bp.route(
|
@bp.route(
|
||||||
"/formation/ue/<int:ue_id>/desassoc_niveau",
|
"/desassoc_ue_niveau/<int:ue_id>",
|
||||||
methods=["POST"],
|
methods=["POST"],
|
||||||
)
|
)
|
||||||
@api_web_bp.route(
|
@api_web_bp.route(
|
||||||
"/formation/ue/<int:ue_id>/desassoc_niveau",
|
"/desassoc_ue_niveau/<int:ue_id>",
|
||||||
methods=["POST"],
|
methods=["POST"],
|
||||||
)
|
)
|
||||||
@login_required
|
@login_required
|
||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.EditFormation)
|
@permission_required(Permission.EditFormation)
|
||||||
@as_json
|
@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
|
"""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)
|
query = UniteEns.query.filter_by(id=ue_id)
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
ue: UniteEns = query.first_or_404()
|
ue: UniteEns = query.first_or_404()
|
||||||
ok, error_message = ue.set_niveau_competence(None)
|
ue.niveau_competence = None
|
||||||
if not ok:
|
db.session.add(ue)
|
||||||
if g.scodoc_dept: # "usage web"
|
db.session.commit()
|
||||||
flash(error_message, "error")
|
log(f"desassoc_ue_niveau: {ue}")
|
||||||
return json_error(404, error_message)
|
if g.scodoc_dept:
|
||||||
if g.scodoc_dept: # "usage web"
|
# "usage web"
|
||||||
flash(f"UE {ue.acronyme} dé-associée")
|
flash(f"UE {ue.acronyme} dé-associée")
|
||||||
return {"status": 0}
|
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
|
|
||||||
|
@ -1,29 +1,22 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
"""
|
"""
|
||||||
ScoDoc 9 API : accès aux formsemestres
|
ScoDoc 9 API : accès aux formsemestres
|
||||||
|
|
||||||
|
|
||||||
CATEGORY
|
|
||||||
--------
|
|
||||||
FormSemestre
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from operator import attrgetter, itemgetter
|
from operator import attrgetter, itemgetter
|
||||||
|
|
||||||
from flask import g, make_response, request
|
from flask import g, make_response, request
|
||||||
from flask_json import as_json
|
from flask_json import as_json
|
||||||
from flask_login import current_user, login_required
|
from flask_login import login_required
|
||||||
import sqlalchemy as sa
|
|
||||||
import app
|
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_bp as bp, api_web_bp, API_CLIENT_ERROR
|
||||||
from app.api import api_permission_required as permission_required
|
from app.decorators import scodoc, permission_required
|
||||||
from app.decorators import scodoc
|
|
||||||
from app.scodoc.sco_utils import json_error
|
from app.scodoc.sco_utils import json_error
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
from app.comp.moy_mod import ModuleImplResults
|
from app.comp.moy_mod import ModuleImplResults
|
||||||
@ -45,7 +38,7 @@ from app.scodoc import sco_groups
|
|||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
from app.scodoc.sco_utils import ModuleType
|
from app.scodoc.sco_utils import ModuleType
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app.tables.recap import TableRecap, RowRecap
|
from app.tables.recap import TableRecap
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/formsemestre/<int:formsemestre_id>")
|
@bp.route("/formsemestre/<int:formsemestre_id>")
|
||||||
@ -61,37 +54,35 @@ def formsemestre_infos(formsemestre_id: int):
|
|||||||
formsemestre_id : l'id du formsemestre
|
formsemestre_id : l'id du formsemestre
|
||||||
|
|
||||||
Exemple de résultat :
|
Exemple de résultat :
|
||||||
```json
|
{
|
||||||
{
|
"block_moyennes": false,
|
||||||
"block_moyennes": false,
|
"bul_bgcolor": "white",
|
||||||
"bul_bgcolor": "white",
|
"bul_hide_xml": false,
|
||||||
"bul_hide_xml": false,
|
"date_debut_iso": "2021-09-01",
|
||||||
"date_debut_iso": "2021-09-01",
|
"date_debut": "01/09/2021",
|
||||||
"date_debut": "01/09/2021",
|
"date_fin_iso": "2022-08-31",
|
||||||
"date_fin_iso": "2022-08-31",
|
"date_fin": "31/08/2022",
|
||||||
"date_fin": "31/08/2022",
|
"dept_id": 1,
|
||||||
"dept_id": 1,
|
"elt_annee_apo": null,
|
||||||
"elt_annee_apo": null,
|
"elt_sem_apo": null,
|
||||||
"elt_passage_apo" : null,
|
"ens_can_edit_eval": false,
|
||||||
"elt_sem_apo": null,
|
"etat": true,
|
||||||
"ens_can_edit_eval": false,
|
"formation_id": 1,
|
||||||
"etat": true,
|
"formsemestre_id": 1,
|
||||||
"formation_id": 1,
|
"gestion_compensation": false,
|
||||||
"formsemestre_id": 1,
|
"gestion_semestrielle": false,
|
||||||
"gestion_compensation": false,
|
"id": 1,
|
||||||
"gestion_semestrielle": false,
|
"modalite": "FI",
|
||||||
"id": 1,
|
"resp_can_change_ens": true,
|
||||||
"modalite": "FI",
|
"resp_can_edit": false,
|
||||||
"resp_can_change_ens": true,
|
"responsables": [1, 99], // uids
|
||||||
"resp_can_edit": false,
|
"scodoc7_id": null,
|
||||||
"responsables": [1, 99], // uids
|
"semestre_id": 1,
|
||||||
"scodoc7_id": null,
|
"titre_formation" : "BUT GEA",
|
||||||
"semestre_id": 1,
|
"titre_num": "BUT GEA semestre 1",
|
||||||
"titre_formation" : "BUT GEA",
|
"titre": "BUT GEA",
|
||||||
"titre_num": "BUT GEA semestre 1",
|
}
|
||||||
"titre": "BUT GEA",
|
|
||||||
}
|
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
@ -108,28 +99,15 @@ def formsemestre_infos(formsemestre_id: int):
|
|||||||
@as_json
|
@as_json
|
||||||
def formsemestres_query():
|
def formsemestres_query():
|
||||||
"""
|
"""
|
||||||
Retourne les formsemestres filtrés par étape Apogée ou année scolaire
|
Retourne les formsemestres filtrés par
|
||||||
ou département (acronyme ou id) ou état ou code étudiant.
|
é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
|
etape_apo : un code étape apogée
|
||||||
annee_scolaire : année de début de l'année scolaire
|
annee_scolaire : année de début de l'année scolaire
|
||||||
dept_acronym : acronyme du département (eg "RT")
|
dept_acronym : acronyme du département (eg "RT")
|
||||||
dept_id : id du département
|
dept_id : id du département
|
||||||
ine ou nip: code d'un étudiant: ramène alors tous les semestres auxquels il est inscrit.
|
ine ou nip: code d'un étudiant: ramène alors tous les semestres auxquels il est inscrit.
|
||||||
etat: 0 si verrouillé, 1 sinon
|
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")
|
etape_apo = request.args.get("etape_apo")
|
||||||
annee_scolaire = request.args.get("annee_scolaire")
|
annee_scolaire = request.args.get("annee_scolaire")
|
||||||
@ -146,8 +124,8 @@ def formsemestres_query():
|
|||||||
annee_scolaire_int = int(annee_scolaire)
|
annee_scolaire_int = int(annee_scolaire)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return json_error(API_CLIENT_ERROR, "invalid annee_scolaire: not int")
|
return json_error(API_CLIENT_ERROR, "invalid annee_scolaire: not int")
|
||||||
debut_annee = scu.date_debut_annee_scolaire(annee_scolaire_int)
|
debut_annee = scu.date_debut_anne_scolaire(annee_scolaire_int)
|
||||||
fin_annee = scu.date_fin_annee_scolaire(annee_scolaire_int)
|
fin_annee = scu.date_fin_anne_scolaire(annee_scolaire_int)
|
||||||
formsemestres = formsemestres.filter(
|
formsemestres = formsemestres.filter(
|
||||||
FormSemestre.date_fin >= debut_annee, FormSemestre.date_debut <= fin_annee
|
FormSemestre.date_fin >= debut_annee, FormSemestre.date_debut <= fin_annee
|
||||||
)
|
)
|
||||||
@ -193,221 +171,6 @@ def formsemestres_query():
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/formsemestre/<int:formsemestre_id>/edit", methods=["POST"])
|
|
||||||
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/edit", methods=["POST"])
|
|
||||||
@scodoc
|
|
||||||
@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,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
|
||||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
|
||||||
editable_keys = {
|
|
||||||
"semestre_id",
|
|
||||||
"titre",
|
|
||||||
"date_debut",
|
|
||||||
"date_fin",
|
|
||||||
"edt_id",
|
|
||||||
"etat",
|
|
||||||
"modalite",
|
|
||||||
"gestion_compensation",
|
|
||||||
"bul_hide_xml",
|
|
||||||
"block_moyennes",
|
|
||||||
"block_moyenne_generale",
|
|
||||||
"mode_calcul_moyennes",
|
|
||||||
"gestion_semestrielle",
|
|
||||||
"bul_bgcolor",
|
|
||||||
"resp_can_edit",
|
|
||||||
"resp_can_change_ens",
|
|
||||||
"ens_can_edit_eval",
|
|
||||||
"elt_sem_apo",
|
|
||||||
"elt_annee_apo",
|
|
||||||
}
|
|
||||||
formsemestre.from_dict({k: v for (k, v) in args.items() if k in editable_keys})
|
|
||||||
try:
|
|
||||||
db.session.commit()
|
|
||||||
except sa.exc.StatementError as exc:
|
|
||||||
return json_error(404, f"invalid argument(s): {exc.args[0]}")
|
|
||||||
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")
|
||||||
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins/<string:version>")
|
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins/<string:version>")
|
||||||
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
|
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
|
||||||
@ -418,12 +181,9 @@ def formsemestre_set_elt_passage_apo():
|
|||||||
@as_json
|
@as_json
|
||||||
def bulletins(formsemestre_id: int, version: str = "long"):
|
def bulletins(formsemestre_id: int, version: str = "long"):
|
||||||
"""
|
"""
|
||||||
Retourne les bulletins d'un formsemestre.
|
Retourne les bulletins d'un formsemestre donné
|
||||||
|
|
||||||
PARAMS
|
formsemestre_id : l'id d'un formesemestre
|
||||||
------
|
|
||||||
formsemestre_id : int
|
|
||||||
version : string ("long", "short", "selectedevals")
|
|
||||||
|
|
||||||
Exemple de résultat : liste, voir https://scodoc.org/ScoDoc9API/#bulletin
|
Exemple de résultat : liste, voir https://scodoc.org/ScoDoc9API/#bulletin
|
||||||
"""
|
"""
|
||||||
@ -455,67 +215,66 @@ def formsemestre_programme(formsemestre_id: int):
|
|||||||
"""
|
"""
|
||||||
Retourne la liste des UEs, ressources et SAEs d'un semestre
|
Retourne la liste des UEs, ressources et SAEs d'un semestre
|
||||||
|
|
||||||
|
formsemestre_id : l'id d'un formsemestre
|
||||||
|
|
||||||
Exemple de résultat :
|
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
|
||||||
|
},
|
||||||
...
|
...
|
||||||
},
|
],
|
||||||
...
|
"ressources": [
|
||||||
],
|
{
|
||||||
"modules" : [ ... les modules qui ne sont ni des SAEs ni des 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)
|
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
@ -579,16 +338,7 @@ def formsemestre_programme(formsemestre_id: int):
|
|||||||
def formsemestre_etudiants(
|
def formsemestre_etudiants(
|
||||||
formsemestre_id: int, with_query: bool = False, long: bool = False
|
formsemestre_id: int, with_query: bool = False, long: bool = False
|
||||||
):
|
):
|
||||||
"""Étudiants d'un formsemestre.
|
"""É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>
|
|
||||||
|
|
||||||
"""
|
|
||||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
@ -610,8 +360,7 @@ def formsemestre_etudiants(
|
|||||||
inscriptions = formsemestre.inscriptions
|
inscriptions = formsemestre.inscriptions
|
||||||
|
|
||||||
if long:
|
if long:
|
||||||
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
etuds = [ins.etud.to_dict_api() for ins in inscriptions]
|
||||||
etuds = [ins.etud.to_dict_api(restrict=restrict) for ins in inscriptions]
|
|
||||||
else:
|
else:
|
||||||
etuds = [ins.etud.to_dict_short() for ins in inscriptions]
|
etuds = [ins.etud.to_dict_short() for ins in inscriptions]
|
||||||
# Ajout des groupes de chaque étudiants
|
# Ajout des groupes de chaque étudiants
|
||||||
@ -630,13 +379,13 @@ def formsemestre_etudiants(
|
|||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@as_json
|
||||||
def formsemestre_etat_evaluations(formsemestre_id: int):
|
def etat_evals(formsemestre_id: int):
|
||||||
"""
|
"""
|
||||||
Informations sur l'état des évaluations d'un formsemestre.
|
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
|
"id": 1, // moduleimpl_id
|
||||||
@ -664,9 +413,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)
|
app.set_sco_dept(formsemestre.departement.acronym)
|
||||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
|
|
||||||
@ -674,7 +425,7 @@ def formsemestre_etat_evaluations(formsemestre_id: int):
|
|||||||
for modimpl_id in nt.modimpls_results:
|
for modimpl_id in nt.modimpls_results:
|
||||||
modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl_id]
|
modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl_id]
|
||||||
modimpl: ModuleImpl = ModuleImpl.query.get_or_404(modimpl_id)
|
modimpl: ModuleImpl = ModuleImpl.query.get_or_404(modimpl_id)
|
||||||
modimpl_dict = modimpl.to_dict(convert_objects=True, with_module=False)
|
modimpl_dict = modimpl.to_dict(convert_objects=True)
|
||||||
|
|
||||||
list_eval = []
|
list_eval = []
|
||||||
for evaluation_id in modimpl_results.evaluations_etat:
|
for evaluation_id in modimpl_results.evaluations_etat:
|
||||||
@ -716,13 +467,13 @@ def formsemestre_etat_evaluations(formsemestre_id: int):
|
|||||||
date_mediane = notes_sorted[len(notes_sorted) // 2].date
|
date_mediane = notes_sorted[len(notes_sorted) // 2].date
|
||||||
|
|
||||||
eval_dict["saisie_notes"] = {
|
eval_dict["saisie_notes"] = {
|
||||||
"datetime_debut": (
|
"datetime_debut": date_debut.isoformat()
|
||||||
date_debut.isoformat() if date_debut is not None else None
|
if date_debut is not None
|
||||||
),
|
else None,
|
||||||
"datetime_fin": date_fin.isoformat() if date_fin is not None else None,
|
"datetime_fin": date_fin.isoformat() if date_fin is not None else None,
|
||||||
"datetime_mediane": (
|
"datetime_mediane": date_mediane.isoformat()
|
||||||
date_mediane.isoformat() if date_mediane is not None else None
|
if date_mediane is not None
|
||||||
),
|
else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
list_eval.append(eval_dict)
|
list_eval.append(eval_dict)
|
||||||
@ -739,16 +490,8 @@ def formsemestre_etat_evaluations(formsemestre_id: int):
|
|||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@as_json
|
||||||
def formsemestre_resultat(formsemestre_id: int):
|
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.
|
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)
|
format_spec = request.args.get("format", None)
|
||||||
if format_spec is not None and format_spec != "raw":
|
if format_spec is not None and format_spec != "raw":
|
||||||
@ -761,41 +504,27 @@ def formsemestre_resultat(formsemestre_id: int):
|
|||||||
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
||||||
app.set_sco_dept(formsemestre.departement.acronym)
|
app.set_sco_dept(formsemestre.departement.acronym)
|
||||||
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
# Ajoute le groupe de chaque partition,
|
|
||||||
etud_groups = sco_groups.get_formsemestre_etuds_groups(formsemestre_id)
|
|
||||||
|
|
||||||
class RowRecapAPI(RowRecap):
|
|
||||||
"""Pour table avec partitions et sort_key"""
|
|
||||||
|
|
||||||
def add_etud_cols(self):
|
|
||||||
"""Ajoute colonnes étudiant: codes, noms"""
|
|
||||||
super().add_etud_cols()
|
|
||||||
self.add_cell("partitions", "partitions", etud_groups.get(self.etud.id, {}))
|
|
||||||
self.add_cell("sort_key", "sort_key", self.etud.sort_key)
|
|
||||||
|
|
||||||
table = TableRecap(
|
table = TableRecap(
|
||||||
res,
|
res, convert_values=convert_values, include_evaluations=False, mode_jury=False
|
||||||
convert_values=convert_values,
|
|
||||||
include_evaluations=False,
|
|
||||||
mode_jury=False,
|
|
||||||
row_class=RowRecapAPI,
|
|
||||||
)
|
)
|
||||||
|
# Supprime les champs inutiles (mise en forme)
|
||||||
rows = table.to_list()
|
rows = table.to_list()
|
||||||
|
# Ajoute le groupe de chaque partition:
|
||||||
|
etud_groups = sco_groups.get_formsemestre_etuds_groups(formsemestre_id)
|
||||||
|
for row in rows:
|
||||||
|
row["partitions"] = etud_groups.get(row["etudid"], {})
|
||||||
|
|
||||||
# for row in rows:
|
|
||||||
# row["partitions"] = etud_groups.get(row["etudid"], {})
|
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
|
||||||
@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>/groups_get_auto_assignment")
|
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/get_groups_auto_assignment")
|
||||||
@login_required
|
@login_required
|
||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@as_json
|
||||||
def groups_get_auto_assignment(formsemestre_id: int):
|
def get_groups_auto_assignment(formsemestre_id: int):
|
||||||
"""Rend les données stockées par `groups_save_auto_assignment`."""
|
"""rend les données"""
|
||||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
@ -806,27 +535,22 @@ def groups_get_auto_assignment(formsemestre_id: int):
|
|||||||
|
|
||||||
|
|
||||||
@bp.route(
|
@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(
|
@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
|
@login_required
|
||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@as_json
|
||||||
def groups_save_auto_assignment(formsemestre_id: int):
|
def save_groups_auto_assignment(formsemestre_id: int):
|
||||||
"""Enregistre les données, associées à ce formsemestre.
|
"""enregistre les données"""
|
||||||
Usage réservé aux fonctions de gestion des groupes, ne pas utiliser ailleurs.
|
|
||||||
"""
|
|
||||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
formsemestre: FormSemestre = query.first_or_404(formsemestre_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:
|
if len(request.data) > GROUPS_AUTO_ASSIGNMENT_DATA_MAX:
|
||||||
return json_error(413, "data too large")
|
return json_error(413, "data too large")
|
||||||
formsemestre.groups_auto_assignment_data = request.data
|
formsemestre.groups_auto_assignment_data = request.data
|
||||||
@ -841,23 +565,9 @@ def groups_save_auto_assignment(formsemestre_id: int):
|
|||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@as_json
|
||||||
def formsemestre_edt(formsemestre_id: int):
|
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.
|
|
||||||
"""
|
|
||||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
||||||
group_ids = request.args.getlist("group_ids", int)
|
return sco_edt_cal.formsemestre_edt_dict(formsemestre)
|
||||||
show_modules_titles = scu.to_bool(request.args.get("show_modules_titles", False))
|
|
||||||
return sco_edt_cal.formsemestre_edt_dict(
|
|
||||||
formsemestre, group_ids=group_ids, show_modules_titles=show_modules_titles
|
|
||||||
)
|
|
||||||
|
@ -1,16 +1,11 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
"""
|
"""
|
||||||
ScoDoc 9 API : jury WIP à compléter avec enregistrement décisions.
|
ScoDoc 9 API : jury WIP à compléter avec enregistrement décisions
|
||||||
|
|
||||||
|
|
||||||
CATEGORY
|
|
||||||
--------
|
|
||||||
Jury
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
@ -22,8 +17,7 @@ from flask_login import current_user, login_required
|
|||||||
import app
|
import app
|
||||||
from app import db, log
|
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_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, permission_required
|
||||||
from app.decorators import scodoc
|
|
||||||
from app.scodoc.sco_exceptions import ScoException
|
from app.scodoc.sco_exceptions import ScoException
|
||||||
from app.but import jury_but_results
|
from app.but import jury_but_results
|
||||||
from app.models import (
|
from app.models import (
|
||||||
@ -38,7 +32,6 @@ from app.models import (
|
|||||||
ScolarNews,
|
ScolarNews,
|
||||||
Scolog,
|
Scolog,
|
||||||
UniteEns,
|
UniteEns,
|
||||||
ValidationDUT120,
|
|
||||||
)
|
)
|
||||||
from app.scodoc import codes_cursus
|
from app.scodoc import codes_cursus
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
@ -56,11 +49,6 @@ def decisions_jury(formsemestre_id: int):
|
|||||||
"""Décisions du jury des étudiants du formsemestre."""
|
"""Décisions du jury des étudiants du formsemestre."""
|
||||||
# APC, pair:
|
# APC, pair:
|
||||||
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
|
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
|
||||||
if formsemestre is None:
|
|
||||||
return json_error(
|
|
||||||
404,
|
|
||||||
message="formsemestre inconnu",
|
|
||||||
)
|
|
||||||
if formsemestre.formation.is_apc():
|
if formsemestre.formation.is_apc():
|
||||||
app.set_sco_dept(formsemestre.departement.acronym)
|
app.set_sco_dept(formsemestre.departement.acronym)
|
||||||
rows = jury_but_results.get_jury_but_results(formsemestre)
|
rows = jury_but_results.get_jury_but_results(formsemestre)
|
||||||
@ -69,16 +57,16 @@ def decisions_jury(formsemestre_id: int):
|
|||||||
raise ScoException("non implemente")
|
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"
|
"génère news sur effacement décision"
|
||||||
# n'utilise pas g.scodoc_dept, pas toujours dispo en mode API
|
# n'utilise pas g.scodoc_dept, pas toujours dispo en mode API
|
||||||
url = url_for(
|
url = url_for(
|
||||||
"scolar.fiche_etud", scodoc_dept=etud.departement.acronym, etudid=etud.id
|
"scolar.ficheEtud", scodoc_dept=etud.departement.acronym, etudid=etud.id
|
||||||
)
|
)
|
||||||
ScolarNews.add(
|
ScolarNews.add(
|
||||||
typ=ScolarNews.NEWS_JURY,
|
typ=ScolarNews.NEWS_JURY,
|
||||||
obj=etud.id,
|
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,
|
url=url,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -96,7 +84,7 @@ def _news_delete_jury_etud(etud: Identite, detail: str = ""):
|
|||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@as_json
|
||||||
def validation_ue_delete(etudid: int, validation_id: int):
|
def validation_ue_delete(etudid: int, validation_id: int):
|
||||||
"Efface cette validation d'UE."
|
"Efface cette validation"
|
||||||
return _validation_ue_delete(etudid, validation_id)
|
return _validation_ue_delete(etudid, validation_id)
|
||||||
|
|
||||||
|
|
||||||
@ -113,7 +101,7 @@ def validation_ue_delete(etudid: int, validation_id: int):
|
|||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@as_json
|
||||||
def validation_formsemestre_delete(etudid: int, validation_id: int):
|
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)
|
# c'est la même chose (formations classiques)
|
||||||
return _validation_ue_delete(etudid, validation_id)
|
return _validation_ue_delete(etudid, validation_id)
|
||||||
|
|
||||||
@ -165,7 +153,7 @@ def _validation_ue_delete(etudid: int, validation_id: int):
|
|||||||
@permission_required(Permission.EtudInscrit)
|
@permission_required(Permission.EtudInscrit)
|
||||||
@as_json
|
@as_json
|
||||||
def autorisation_inscription_delete(etudid: int, validation_id: int):
|
def autorisation_inscription_delete(etudid: int, validation_id: int):
|
||||||
"Efface cette autorisation d'inscription."
|
"Efface cette validation"
|
||||||
etud = tools.get_etud(etudid)
|
etud = tools.get_etud(etudid)
|
||||||
if etud is None:
|
if etud is None:
|
||||||
return "étudiant inconnu", 404
|
return "étudiant inconnu", 404
|
||||||
@ -194,12 +182,8 @@ def autorisation_inscription_delete(etudid: int, validation_id: int):
|
|||||||
@as_json
|
@as_json
|
||||||
def validation_rcue_record(etudid: int):
|
def validation_rcue_record(etudid: int):
|
||||||
"""Enregistre une validation de RCUE.
|
"""Enregistre une validation de RCUE.
|
||||||
|
|
||||||
Si une validation existe déjà pour ce RCUE, la remplace.
|
Si une validation existe déjà pour ce RCUE, la remplace.
|
||||||
|
The request content type should be "application/json":
|
||||||
DATA
|
|
||||||
----
|
|
||||||
```json
|
|
||||||
{
|
{
|
||||||
"code" : str,
|
"code" : str,
|
||||||
"ue1_id" : int,
|
"ue1_id" : int,
|
||||||
@ -209,7 +193,6 @@ def validation_rcue_record(etudid: int):
|
|||||||
"date" : date_iso, // si non spécifié, now()
|
"date" : date_iso, // si non spécifié, now()
|
||||||
"parcours_id" :int,
|
"parcours_id" :int,
|
||||||
}
|
}
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
etud = tools.get_etud(etudid)
|
etud = tools.get_etud(etudid)
|
||||||
if etud is None:
|
if etud is None:
|
||||||
@ -301,12 +284,13 @@ def validation_rcue_record(etudid: int):
|
|||||||
db.session.add(validation)
|
db.session.add(validation)
|
||||||
# invalider bulletins (les autres résultats ne dépendent pas des RCUEs):
|
# invalider bulletins (les autres résultats ne dépendent pas des RCUEs):
|
||||||
sco_cache.invalidate_formsemestre_etud(etud)
|
sco_cache.invalidate_formsemestre_etud(etud)
|
||||||
|
db.session.commit()
|
||||||
Scolog.logdb(
|
Scolog.logdb(
|
||||||
method="validation_rcue_record",
|
method="validation_rcue_record",
|
||||||
etudid=etudid,
|
etudid=etudid,
|
||||||
msg=f"Enregistrement {validation}",
|
msg=f"Enregistrement {validation}",
|
||||||
|
commit=True,
|
||||||
)
|
)
|
||||||
db.session.commit()
|
|
||||||
log(f"{operation} {validation}")
|
log(f"{operation} {validation}")
|
||||||
return validation.to_dict()
|
return validation.to_dict()
|
||||||
|
|
||||||
@ -324,18 +308,18 @@ def validation_rcue_record(etudid: int):
|
|||||||
@permission_required(Permission.EtudInscrit)
|
@permission_required(Permission.EtudInscrit)
|
||||||
@as_json
|
@as_json
|
||||||
def validation_rcue_delete(etudid: int, validation_id: int):
|
def validation_rcue_delete(etudid: int, validation_id: int):
|
||||||
"Efface cette validation de RCUE."
|
"Efface cette validation"
|
||||||
etud = tools.get_etud(etudid)
|
etud = tools.get_etud(etudid)
|
||||||
if etud is None:
|
if etud is None:
|
||||||
return "étudiant inconnu", 404
|
return "étudiant inconnu", 404
|
||||||
validation = ApcValidationRCUE.query.filter_by(
|
validation = ApcValidationRCUE.query.filter_by(
|
||||||
id=validation_id, etudid=etudid
|
id=validation_id, etudid=etudid
|
||||||
).first_or_404()
|
).first_or_404()
|
||||||
log(f"delete validation_ue_delete: etuid={etudid} {validation}")
|
log(f"validation_ue_delete: etuid={etudid} {validation}")
|
||||||
db.session.delete(validation)
|
db.session.delete(validation)
|
||||||
sco_cache.invalidate_formsemestre_etud(etud)
|
sco_cache.invalidate_formsemestre_etud(etud)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
_news_delete_jury_etud(etud, detail="UE")
|
_news_delete_jury_etud(etud)
|
||||||
return "ok"
|
return "ok"
|
||||||
|
|
||||||
|
|
||||||
@ -352,45 +336,16 @@ def validation_rcue_delete(etudid: int, validation_id: int):
|
|||||||
@permission_required(Permission.EtudInscrit)
|
@permission_required(Permission.EtudInscrit)
|
||||||
@as_json
|
@as_json
|
||||||
def validation_annee_but_delete(etudid: int, validation_id: int):
|
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)
|
etud = tools.get_etud(etudid)
|
||||||
if etud is None:
|
if etud is None:
|
||||||
return "étudiant inconnu", 404
|
return "étudiant inconnu", 404
|
||||||
validation = ApcValidationAnnee.query.filter_by(
|
validation = ApcValidationAnnee.query.filter_by(
|
||||||
id=validation_id, etudid=etudid
|
id=validation_id, etudid=etudid
|
||||||
).first_or_404()
|
).first_or_404()
|
||||||
ordre = validation.ordre
|
log(f"validation_annee_but: etuid={etudid} {validation}")
|
||||||
log(f"delete validation_annee_but: etuid={etudid} {validation}")
|
|
||||||
db.session.delete(validation)
|
db.session.delete(validation)
|
||||||
sco_cache.invalidate_formsemestre_etud(etud)
|
sco_cache.invalidate_formsemestre_etud(etud)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
_news_delete_jury_etud(etud, detail=f"année BUT{ordre}")
|
_news_delete_jury_etud(etud)
|
||||||
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")
|
|
||||||
return "ok"
|
return "ok"
|
||||||
|
@ -3,26 +3,31 @@
|
|||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
"""ScoDoc 9 API : Justificatifs"""
|
"""ScoDoc 9 API : Justificatifs
|
||||||
|
"""
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from flask_json import as_json
|
from flask_json import as_json
|
||||||
from flask import g, request
|
from flask import g, request
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
from flask_sqlalchemy.query import Query
|
from flask_sqlalchemy.query import Query
|
||||||
from werkzeug.exceptions import NotFound
|
|
||||||
|
|
||||||
import app.scodoc.sco_assiduites as scass
|
import app.scodoc.sco_assiduites as scass
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app import db, set_sco_dept
|
from app import db
|
||||||
from app.api import api_bp as bp
|
from app.api import api_bp as bp
|
||||||
from app.api import api_web_bp
|
from app.api import api_web_bp
|
||||||
from app.api import get_model_api_object, tools
|
from app.api import get_model_api_object, tools
|
||||||
from app.api import api_permission_required as permission_required
|
from app.decorators import permission_required, scodoc
|
||||||
from app.decorators import scodoc
|
from app.models import (
|
||||||
from app.models import Identite, Justificatif, Departement, FormSemestre, Scolog
|
Identite,
|
||||||
|
Justificatif,
|
||||||
|
Departement,
|
||||||
|
FormSemestre,
|
||||||
|
FormSemestreInscription,
|
||||||
|
)
|
||||||
from app.models.assiduites import (
|
from app.models.assiduites import (
|
||||||
|
compute_assiduites_justified,
|
||||||
get_formsemestre_from_data,
|
get_formsemestre_from_data,
|
||||||
)
|
)
|
||||||
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
|
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
|
||||||
@ -38,11 +43,9 @@ from app.scodoc.sco_groups import get_group_members
|
|||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
def justificatif(justif_id: int = None):
|
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:
|
Exemple de résultat:
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
{
|
||||||
"justif_id": 1,
|
"justif_id": 1,
|
||||||
"etudid": 2,
|
"etudid": 2,
|
||||||
@ -50,24 +53,14 @@ def justificatif(justif_id: int = None):
|
|||||||
"date_fin": "2022-10-31T10:00+01:00",
|
"date_fin": "2022-10-31T10:00+01:00",
|
||||||
"etat": "valide",
|
"etat": "valide",
|
||||||
"fichier": "archive_id",
|
"fichier": "archive_id",
|
||||||
"raison": "une raison", // VIDE si pas le droit
|
"raison": "une raison",
|
||||||
"entry_date": "2022-10-31T08:00+01:00",
|
"entry_date": "2022-10-31T08:00+01:00",
|
||||||
"user_id": 1 or null,
|
"user_id": 1 or null,
|
||||||
}
|
}
|
||||||
```
|
|
||||||
|
|
||||||
SAMPLES
|
|
||||||
-------
|
|
||||||
/justificatif/1;
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return get_model_api_object(
|
return get_model_api_object(Justificatif, justif_id, Identite)
|
||||||
Justificatif,
|
|
||||||
justif_id,
|
|
||||||
Identite,
|
|
||||||
restrict=not current_user.has_permission(Permission.AbsJustifView),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# etudid
|
# etudid
|
||||||
@ -99,32 +92,28 @@ def justificatif(justif_id: int = None):
|
|||||||
def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = False):
|
def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = False):
|
||||||
"""
|
"""
|
||||||
Retourne toutes les assiduités d'un étudiant
|
Retourne toutes les assiduités d'un étudiant
|
||||||
|
chemin : /justificatifs/<int:etudid>
|
||||||
|
|
||||||
QUERY
|
Un filtrage peut être donné avec une query
|
||||||
-----
|
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;
|
|
||||||
|
|
||||||
|
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
|
# Récupération de l'étudiant
|
||||||
etud: Identite = tools.get_etud(etudid, nip, ine)
|
etud: Identite = tools.get_etud(etudid, nip, ine)
|
||||||
@ -144,9 +133,8 @@ def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = Fal
|
|||||||
|
|
||||||
# Mise en forme des données puis retour en JSON
|
# Mise en forme des données puis retour en JSON
|
||||||
data_set: list[dict] = []
|
data_set: list[dict] = []
|
||||||
restrict = not current_user.has_permission(Permission.AbsJustifView)
|
|
||||||
for just in justificatifs_query.all():
|
for just in justificatifs_query.all():
|
||||||
data = just.to_dict(format_api=True, restrict=restrict)
|
data = just.to_dict(format_api=True)
|
||||||
data_set.append(data)
|
data_set.append(data)
|
||||||
|
|
||||||
return data_set
|
return data_set
|
||||||
@ -163,41 +151,10 @@ def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = Fal
|
|||||||
@as_json
|
@as_json
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
def justificatifs_dept(dept_id: int = None, with_query: bool = False):
|
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
|
# Récupération du département et des étudiants du département
|
||||||
dept: Departement = Departement.query.get(dept_id)
|
dept: Departement = Departement.query.get_or_404(dept_id)
|
||||||
if dept is None:
|
|
||||||
return json_error(404, "Assiduité non existante")
|
|
||||||
etuds: list[int] = [etud.id for etud in dept.etudiants]
|
etuds: list[int] = [etud.id for etud in dept.etudiants]
|
||||||
|
|
||||||
# Récupération des justificatifs des étudiants du département
|
# Récupération des justificatifs des étudiants du département
|
||||||
@ -210,15 +167,14 @@ def justificatifs_dept(dept_id: int = None, with_query: bool = False):
|
|||||||
justificatifs_query: Query = _filter_manager(request, justificatifs_query)
|
justificatifs_query: Query = _filter_manager(request, justificatifs_query)
|
||||||
|
|
||||||
# Mise en forme des données et retour JSON
|
# Mise en forme des données et retour JSON
|
||||||
restrict = not current_user.has_permission(Permission.AbsJustifView)
|
|
||||||
data_set: list[dict] = []
|
data_set: list[dict] = []
|
||||||
for just in justificatifs_query:
|
for just in justificatifs_query:
|
||||||
data_set.append(_set_sems(just, restrict=restrict))
|
data_set.append(_set_sems(just))
|
||||||
|
|
||||||
return data_set
|
return data_set
|
||||||
|
|
||||||
|
|
||||||
def _set_sems(justi: Justificatif, restrict: bool) -> dict:
|
def _set_sems(justi: Justificatif) -> dict:
|
||||||
"""
|
"""
|
||||||
_set_sems Ajoute le formsemestre associé au justificatif s'il existe
|
_set_sems Ajoute le formsemestre associé au justificatif s'il existe
|
||||||
|
|
||||||
@ -231,7 +187,7 @@ def _set_sems(justi: Justificatif, restrict: bool) -> dict:
|
|||||||
dict: La représentation de l'assiduité en dictionnaire
|
dict: La représentation de l'assiduité en dictionnaire
|
||||||
"""
|
"""
|
||||||
# Conversion du justificatif en dictionnaire
|
# Conversion du justificatif en dictionnaire
|
||||||
data = justi.to_dict(format_api=True, restrict=restrict)
|
data = justi.to_dict(format_api=True)
|
||||||
|
|
||||||
# Récupération du formsemestre de l'assiduité
|
# Récupération du formsemestre de l'assiduité
|
||||||
formsemestre: FormSemestre = get_formsemestre_from_data(justi.to_dict())
|
formsemestre: FormSemestre = get_formsemestre_from_data(justi.to_dict())
|
||||||
@ -263,34 +219,7 @@ def _set_sems(justi: Justificatif, restrict: bool) -> dict:
|
|||||||
@as_json
|
@as_json
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False):
|
def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False):
|
||||||
"""Retourne tous les justificatifs du formsemestre
|
"""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;
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Récupération du formsemestre
|
# Récupération du formsemestre
|
||||||
formsemestre: FormSemestre = None
|
formsemestre: FormSemestre = None
|
||||||
@ -312,10 +241,9 @@ def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False):
|
|||||||
justificatifs_query: Query = _filter_manager(request, justificatifs_query)
|
justificatifs_query: Query = _filter_manager(request, justificatifs_query)
|
||||||
|
|
||||||
# Retour des justificatifs en JSON
|
# Retour des justificatifs en JSON
|
||||||
restrict = not current_user.has_permission(Permission.AbsJustifView)
|
|
||||||
data_set: list[dict] = []
|
data_set: list[dict] = []
|
||||||
for justi in justificatifs_query.all():
|
for justi in justificatifs_query.all():
|
||||||
data = justi.to_dict(format_api=True, restrict=restrict)
|
data = justi.to_dict(format_api=True)
|
||||||
data_set.append(data)
|
data_set.append(data)
|
||||||
|
|
||||||
return data_set
|
return data_set
|
||||||
@ -338,10 +266,7 @@ def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False):
|
|||||||
def justif_create(etudid: int = None, nip=None, ine=None):
|
def justif_create(etudid: int = None, nip=None, ine=None):
|
||||||
"""
|
"""
|
||||||
Création d'un justificatif pour l'étudiant (etudid)
|
Création d'un justificatif pour l'étudiant (etudid)
|
||||||
|
La requête doit avoir un content type "application/json":
|
||||||
DATA
|
|
||||||
----
|
|
||||||
```json
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"date_debut": str,
|
"date_debut": str,
|
||||||
@ -356,10 +281,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""}]
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -371,7 +292,6 @@ def justif_create(etudid: int = None, nip=None, ine=None):
|
|||||||
404,
|
404,
|
||||||
message="étudiant inconnu",
|
message="étudiant inconnu",
|
||||||
)
|
)
|
||||||
set_sco_dept(etud.departement.acronym)
|
|
||||||
|
|
||||||
# Récupération des justificatifs à créer
|
# Récupération des justificatifs à créer
|
||||||
create_list: list[object] = request.get_json(force=True)
|
create_list: list[object] = request.get_json(force=True)
|
||||||
@ -381,6 +301,7 @@ def justif_create(etudid: int = None, nip=None, ine=None):
|
|||||||
|
|
||||||
errors: list[dict] = []
|
errors: list[dict] = []
|
||||||
success: list[dict] = []
|
success: list[dict] = []
|
||||||
|
justifs: list[Justificatif] = []
|
||||||
|
|
||||||
# énumération des justificatifs
|
# énumération des justificatifs
|
||||||
for i, data in enumerate(create_list):
|
for i, data in enumerate(create_list):
|
||||||
@ -392,9 +313,11 @@ def justif_create(etudid: int = None, nip=None, ine=None):
|
|||||||
errors.append({"indice": i, "message": obj})
|
errors.append({"indice": i, "message": obj})
|
||||||
else:
|
else:
|
||||||
success.append({"indice": i, "message": obj})
|
success.append({"indice": i, "message": obj})
|
||||||
justi.justifier_assiduites()
|
justifs.append(justi)
|
||||||
scass.simple_invalidate_cache(data, etud.id)
|
scass.simple_invalidate_cache(data, etud.id)
|
||||||
|
|
||||||
|
# Actualisation des assiduités justifiées en fonction de tous les nouveaux justificatifs
|
||||||
|
compute_assiduites_justified(etud.etudid, justifs)
|
||||||
return {"errors": errors, "success": success}
|
return {"errors": errors, "success": success}
|
||||||
|
|
||||||
|
|
||||||
@ -413,10 +336,6 @@ def _create_one(
|
|||||||
errors.append("param 'etat': invalide")
|
errors.append("param 'etat': invalide")
|
||||||
|
|
||||||
etat: scu.EtatJustificatif = scu.EtatJustificatif.get(etat)
|
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
|
# cas 2 : date_debut
|
||||||
date_debut: str = data.get("date_debut", None)
|
date_debut: str = data.get("date_debut", None)
|
||||||
@ -455,7 +374,7 @@ def _create_one(
|
|||||||
date_debut=deb,
|
date_debut=deb,
|
||||||
date_fin=fin,
|
date_fin=fin,
|
||||||
etat=etat,
|
etat=etat,
|
||||||
etudiant=etud,
|
etud=etud,
|
||||||
raison=raison,
|
raison=raison,
|
||||||
user_id=current_user.id,
|
user_id=current_user.id,
|
||||||
external_data=external_data,
|
external_data=external_data,
|
||||||
@ -490,27 +409,20 @@ def _create_one(
|
|||||||
def justif_edit(justif_id: int):
|
def justif_edit(justif_id: int):
|
||||||
"""
|
"""
|
||||||
Edition d'un justificatif à partir de son id
|
Edition d'un justificatif à partir de son id
|
||||||
|
La requête doit avoir un content type "application/json":
|
||||||
|
|
||||||
DATA
|
|
||||||
----
|
|
||||||
```json
|
|
||||||
{
|
{
|
||||||
"etat"?: str,
|
"etat"?: str,
|
||||||
"raison"?: str
|
"raison"?: str
|
||||||
"date_debut"?: str
|
"date_debut"?: str
|
||||||
"date_fin"?: str
|
"date_fin"?: str
|
||||||
}
|
}
|
||||||
```
|
|
||||||
|
|
||||||
SAMPLES
|
|
||||||
-------
|
|
||||||
/justificatif/1/edit;{""etat"":""valide""}
|
|
||||||
/justificatif/1/edit;{""raison"":""MEDIC""}
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Récupération du justificatif à modifier
|
# Récupération du justificatif à modifier
|
||||||
justificatif_unique = Justificatif.get_justificatif(justif_id)
|
justificatif_unique: Query = Justificatif.query.filter_by(
|
||||||
|
id=justif_id
|
||||||
|
).first_or_404()
|
||||||
|
|
||||||
errors: list[str] = []
|
errors: list[str] = []
|
||||||
data = request.get_json(force=True)
|
data = request.get_json(force=True)
|
||||||
@ -525,10 +437,7 @@ def justif_edit(justif_id: int):
|
|||||||
if etat is None:
|
if etat is None:
|
||||||
errors.append("param 'etat': invalide")
|
errors.append("param 'etat': invalide")
|
||||||
else:
|
else:
|
||||||
if current_user.has_permission(Permission.JustifValidate):
|
justificatif_unique.etat = etat
|
||||||
justificatif_unique.etat = etat
|
|
||||||
else:
|
|
||||||
errors.append("param 'etat': non autorisé (Permission.JustifValidate)")
|
|
||||||
|
|
||||||
# Cas 2 : raison
|
# Cas 2 : raison
|
||||||
raison: str = data.get("raison", False)
|
raison: str = data.get("raison", False)
|
||||||
@ -579,13 +488,7 @@ def justif_edit(justif_id: int):
|
|||||||
return json_error(404, err)
|
return json_error(404, err)
|
||||||
|
|
||||||
# Mise à jour du justificatif
|
# Mise à jour du justificatif
|
||||||
justificatif_unique.dejustifier_assiduites()
|
|
||||||
db.session.add(justificatif_unique)
|
db.session.add(justificatif_unique)
|
||||||
Scolog.logdb(
|
|
||||||
method="edit_justificatif",
|
|
||||||
etudid=justificatif_unique.etudiant.id,
|
|
||||||
msg=f"justificatif modif: {justificatif_unique}",
|
|
||||||
)
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# Génération du dictionnaire de retour
|
# Génération du dictionnaire de retour
|
||||||
@ -595,7 +498,11 @@ def justif_edit(justif_id: int):
|
|||||||
retour = {
|
retour = {
|
||||||
"couverture": {
|
"couverture": {
|
||||||
"avant": avant_ids,
|
"avant": avant_ids,
|
||||||
"apres": justificatif_unique.justifier_assiduites(),
|
"après": compute_assiduites_justified(
|
||||||
|
justificatif_unique.etudid,
|
||||||
|
[justificatif_unique],
|
||||||
|
True,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
# Invalide le cache
|
# Invalide le cache
|
||||||
@ -613,18 +520,13 @@ def justif_delete():
|
|||||||
"""
|
"""
|
||||||
Suppression d'un justificatif à partir de son id
|
Suppression d'un justificatif à partir de son id
|
||||||
|
|
||||||
DATA
|
Forme des données envoyées :
|
||||||
----
|
|
||||||
```json
|
|
||||||
[
|
[
|
||||||
<justif_id:int>,
|
<justif_id:int>,
|
||||||
...
|
...
|
||||||
]
|
]
|
||||||
```
|
|
||||||
|
|
||||||
SAMPLES
|
|
||||||
-------
|
|
||||||
/justificatif/delete;[2, 2, 3]
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -660,10 +562,12 @@ def _delete_one(justif_id: int) -> tuple[int, str]:
|
|||||||
message : OK si réussi, message d'erreur sinon
|
message : OK si réussi, message d'erreur sinon
|
||||||
"""
|
"""
|
||||||
# Récupération du justificatif à supprimer
|
# Récupération du justificatif à supprimer
|
||||||
try:
|
justificatif_unique: Justificatif = Justificatif.query.filter_by(
|
||||||
justificatif_unique = Justificatif.get_justificatif(justif_id)
|
id=justif_id
|
||||||
except NotFound:
|
).first()
|
||||||
|
if justificatif_unique is None:
|
||||||
return (404, "Justificatif non existant")
|
return (404, "Justificatif non existant")
|
||||||
|
|
||||||
# Récupération de l'archive du justificatif
|
# Récupération de l'archive du justificatif
|
||||||
archive_name: str = justificatif_unique.fichier
|
archive_name: str = justificatif_unique.fichier
|
||||||
|
|
||||||
@ -677,15 +581,14 @@ def _delete_one(justif_id: int) -> tuple[int, str]:
|
|||||||
|
|
||||||
# On invalide le cache
|
# On invalide le cache
|
||||||
scass.simple_invalidate_cache(justificatif_unique.to_dict())
|
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
|
# On supprime le justificatif
|
||||||
db.session.delete(justificatif_unique)
|
db.session.delete(justificatif_unique)
|
||||||
|
# On actualise les assiduités justifiées de l'étudiant concerné
|
||||||
|
compute_assiduites_justified(
|
||||||
|
justificatif_unique.etudid,
|
||||||
|
Justificatif.query.filter_by(etudid=justificatif_unique.etudid).all(),
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
return (200, "OK")
|
return (200, "OK")
|
||||||
|
|
||||||
@ -700,8 +603,6 @@ def _delete_one(justif_id: int) -> tuple[int, str]:
|
|||||||
def justif_import(justif_id: int = None):
|
def justif_import(justif_id: int = None):
|
||||||
"""
|
"""
|
||||||
Importation d'un fichier (création d'archive)
|
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é
|
# On vérifie qu'un fichier a bien été envoyé
|
||||||
@ -712,7 +613,10 @@ def justif_import(justif_id: int = None):
|
|||||||
return json_error(404, "Il n'y a pas de fichier joint")
|
return json_error(404, "Il n'y a pas de fichier joint")
|
||||||
|
|
||||||
# On récupère le justificatif auquel on va importer le fichier
|
# On récupère le justificatif auquel on va importer le fichier
|
||||||
justificatif_unique = Justificatif.get_justificatif(justif_id)
|
query: Query = Justificatif.query.filter_by(id=justif_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
justificatif_unique: Justificatif = query.first_or_404()
|
||||||
|
|
||||||
# Récupération de l'archive si elle existe
|
# Récupération de l'archive si elle existe
|
||||||
archive_name: str = justificatif_unique.fichier
|
archive_name: str = justificatif_unique.fichier
|
||||||
@ -737,34 +641,26 @@ def justif_import(justif_id: int = None):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return {"filename": fname}
|
return {"filename": fname}
|
||||||
except ScoValueError as exc:
|
except ScoValueError as err:
|
||||||
# Si cela ne fonctionne pas on renvoie une erreur
|
# Si cela ne fonctionne pas on renvoie une erreur
|
||||||
return json_error(404, exc.args[0])
|
return json_error(404, err.args[0])
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/justificatif/<int:justif_id>/export/<filename>", methods=["GET", "POST"])
|
@bp.route("/justificatif/<int:justif_id>/export/<filename>", methods=["POST"])
|
||||||
@api_web_bp.route(
|
@api_web_bp.route("/justificatif/<int:justif_id>/export/<filename>", methods=["POST"])
|
||||||
"/justificatif/<int:justif_id>/export/<filename>", methods=["GET", "POST"]
|
|
||||||
)
|
|
||||||
@scodoc
|
@scodoc
|
||||||
@login_required
|
@login_required
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.AbsChange)
|
||||||
def justif_export(justif_id: int | None = None, filename: str | None = None):
|
def justif_export(justif_id: int = None, filename: str = None):
|
||||||
|
"""
|
||||||
|
Retourne un fichier d'une archive d'un justificatif
|
||||||
"""
|
"""
|
||||||
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é
|
# On récupère le justificatif concerné
|
||||||
justificatif_unique = Justificatif.get_justificatif(justif_id)
|
query: Query = Justificatif.query.filter_by(id=justif_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
# Vérification des permissions
|
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
if not (
|
justificatif_unique: Justificatif = query.first_or_404()
|
||||||
current_user.has_permission(Permission.AbsJustifView)
|
|
||||||
or justificatif_unique.user_id == current_user.id
|
|
||||||
):
|
|
||||||
return json_error(401, "non autorisé à voir ce fichier")
|
|
||||||
|
|
||||||
# On récupère l'archive concernée
|
# On récupère l'archive concernée
|
||||||
archive_name: str = justificatif_unique.fichier
|
archive_name: str = justificatif_unique.fichier
|
||||||
@ -792,27 +688,24 @@ def justif_export(justif_id: int | None = None, filename: str | None = None):
|
|||||||
def justif_remove(justif_id: int = None):
|
def justif_remove(justif_id: int = None):
|
||||||
"""
|
"""
|
||||||
Supression d'un fichier ou d'une archive
|
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"?: [
|
"filenames"?: [
|
||||||
<filename:str>,
|
<filename:str>,
|
||||||
...
|
...
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# On récupère le dictionnaire
|
# On récupère le dictionnaire
|
||||||
data: dict = request.get_json(force=True)
|
data: dict = request.get_json(force=True)
|
||||||
|
|
||||||
# On récupère le justificatif concerné
|
# On récupère le justificatif concerné
|
||||||
justificatif_unique = Justificatif.get_justificatif(justif_id)
|
query: Query = Justificatif.query.filter_by(id=justif_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
justificatif_unique: Justificatif = query.first_or_404()
|
||||||
|
|
||||||
# On récupère l'archive
|
# On récupère l'archive
|
||||||
archive_name: str = justificatif_unique.fichier
|
archive_name: str = justificatif_unique.fichier
|
||||||
@ -871,15 +764,13 @@ def justif_remove(justif_id: int = None):
|
|||||||
def justif_list(justif_id: int = None):
|
def justif_list(justif_id: int = None):
|
||||||
"""
|
"""
|
||||||
Liste les fichiers du justificatif
|
Liste les fichiers du justificatif
|
||||||
|
|
||||||
SAMPLES
|
|
||||||
-------
|
|
||||||
/justificatif/1/list;
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Récupération du justificatif concerné
|
# Récupération du justificatif concerné
|
||||||
justificatif_unique = Justificatif.get_justificatif(justif_id)
|
query: Query = Justificatif.query.filter_by(id=justif_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
justificatif_unique: Justificatif = query.first_or_404()
|
||||||
|
|
||||||
# Récupération de l'archive avec l'archiver
|
# Récupération de l'archive avec l'archiver
|
||||||
archive_name: str = justificatif_unique.fichier
|
archive_name: str = justificatif_unique.fichier
|
||||||
@ -918,15 +809,13 @@ def justif_list(justif_id: int = None):
|
|||||||
def justif_justifies(justif_id: int = None):
|
def justif_justifies(justif_id: int = None):
|
||||||
"""
|
"""
|
||||||
Liste assiduite_id justifiées par le justificatif
|
Liste assiduite_id justifiées par le justificatif
|
||||||
|
|
||||||
SAMPLES
|
|
||||||
-------
|
|
||||||
/justificatif/1/justifies;
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# On récupère le justificatif concerné
|
# On récupère le justificatif concerné
|
||||||
justificatif_unique = Justificatif.get_justificatif(justif_id)
|
query: Query = Justificatif.query.filter_by(id=justif_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
justificatif_unique: Justificatif = query.first_or_404()
|
||||||
|
|
||||||
# On récupère la liste des assiduités justifiées par le justificatif
|
# On récupère la liste des assiduités justifiées par le justificatif
|
||||||
assiduites_list: list[int] = scass.justifies(justificatif_unique)
|
assiduites_list: list[int] = scass.justifies(justificatif_unique)
|
||||||
@ -940,7 +829,6 @@ def justif_justifies(justif_id: int = None):
|
|||||||
def _filter_manager(requested, justificatifs_query: Query):
|
def _filter_manager(requested, justificatifs_query: Query):
|
||||||
"""
|
"""
|
||||||
Retourne les justificatifs entrés filtrés en fonction de la request
|
Retourne les justificatifs entrés filtrés en fonction de la request
|
||||||
et du département courant s'il y en a un
|
|
||||||
"""
|
"""
|
||||||
# cas 1 : etat justificatif
|
# cas 1 : etat justificatif
|
||||||
etat: str = requested.args.get("etat")
|
etat: str = requested.args.get("etat")
|
||||||
@ -975,7 +863,7 @@ def _filter_manager(requested, justificatifs_query: Query):
|
|||||||
formsemestre: FormSemestre = None
|
formsemestre: FormSemestre = None
|
||||||
try:
|
try:
|
||||||
formsemestre_id = int(formsemestre_id)
|
formsemestre_id = int(formsemestre_id)
|
||||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
|
||||||
justificatifs_query = scass.filter_by_formsemestre(
|
justificatifs_query = scass.filter_by_formsemestre(
|
||||||
justificatifs_query, Justificatif, formsemestre
|
justificatifs_query, Justificatif, formsemestre
|
||||||
)
|
)
|
||||||
@ -994,8 +882,8 @@ def _filter_manager(requested, justificatifs_query: Query):
|
|||||||
annee: int = scu.annee_scolaire()
|
annee: int = scu.annee_scolaire()
|
||||||
|
|
||||||
justificatifs_query: Query = justificatifs_query.filter(
|
justificatifs_query: Query = justificatifs_query.filter(
|
||||||
Justificatif.date_debut >= scu.date_debut_annee_scolaire(annee),
|
Justificatif.date_debut >= scu.date_debut_anne_scolaire(annee),
|
||||||
Justificatif.date_fin <= scu.date_fin_annee_scolaire(annee),
|
Justificatif.date_fin <= scu.date_fin_anne_scolaire(annee),
|
||||||
)
|
)
|
||||||
|
|
||||||
# cas 8 : group_id filtre les justificatifs d'un groupe d'étudiant
|
# cas 8 : group_id filtre les justificatifs d'un groupe d'étudiant
|
||||||
@ -1010,10 +898,4 @@ def _filter_manager(requested, justificatifs_query: Query):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
group_id = None
|
group_id = None
|
||||||
|
|
||||||
# Département
|
|
||||||
if g.scodoc_dept:
|
|
||||||
justificatifs_query = justificatifs_query.join(Identite).filter_by(
|
|
||||||
dept_id=g.scodoc_dept_id
|
|
||||||
)
|
|
||||||
|
|
||||||
return justificatifs_query
|
return justificatifs_query
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -34,13 +34,11 @@ from flask import Response, send_file
|
|||||||
from flask_json import as_json
|
from flask_json import as_json
|
||||||
|
|
||||||
from app.api import api_bp as bp
|
from app.api import api_bp as bp
|
||||||
from app.api import api_permission_required as permission_required
|
from app.scodoc.sco_utils import json_error
|
||||||
from app.decorators import scodoc
|
|
||||||
from app.models import Departement
|
from app.models import Departement
|
||||||
from app.scodoc.sco_logos import list_logos, find_logo
|
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_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)
|
# 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
|
@scodoc
|
||||||
@permission_required(Permission.ScoSuperAdmin)
|
@permission_required(Permission.ScoSuperAdmin)
|
||||||
@as_json
|
@as_json
|
||||||
def logo_list_globals():
|
def api_get_glob_logos():
|
||||||
"""Liste des noms des logos définis pour le site ScoDoc."""
|
"""Liste tous les logos"""
|
||||||
logos = list_logos()[None]
|
logos = list_logos()[None]
|
||||||
return list(logos.keys())
|
return list(logos.keys())
|
||||||
|
|
||||||
@ -58,12 +56,7 @@ def logo_list_globals():
|
|||||||
@bp.route("/logo/<string:logoname>")
|
@bp.route("/logo/<string:logoname>")
|
||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoSuperAdmin)
|
@permission_required(Permission.ScoSuperAdmin)
|
||||||
def logo_get_global(logoname):
|
def api_get_glob_logo(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.
|
|
||||||
"""
|
|
||||||
logo = find_logo(logoname=logoname)
|
logo = find_logo(logoname=logoname)
|
||||||
if logo is None:
|
if logo is None:
|
||||||
return json_error(404, message="logo not found")
|
return json_error(404, message="logo not found")
|
||||||
@ -84,10 +77,7 @@ def _core_get_logos(dept_id) -> list:
|
|||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoSuperAdmin)
|
@permission_required(Permission.ScoSuperAdmin)
|
||||||
@as_json
|
@as_json
|
||||||
def logo_get_local_by_acronym(departement):
|
def api_get_local_logos_by_acronym(departement):
|
||||||
"""Liste des noms des logos définis pour le département
|
|
||||||
désigné par son acronyme.
|
|
||||||
"""
|
|
||||||
dept_id = Departement.from_acronym(departement).id
|
dept_id = Departement.from_acronym(departement).id
|
||||||
return _core_get_logos(dept_id)
|
return _core_get_logos(dept_id)
|
||||||
|
|
||||||
@ -96,10 +86,7 @@ def logo_get_local_by_acronym(departement):
|
|||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoSuperAdmin)
|
@permission_required(Permission.ScoSuperAdmin)
|
||||||
@as_json
|
@as_json
|
||||||
def logo_get_local_by_id(dept_id):
|
def api_get_local_logos_by_id(dept_id):
|
||||||
"""Liste des noms des logos définis pour le département
|
|
||||||
désigné par son id.
|
|
||||||
"""
|
|
||||||
return _core_get_logos(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>")
|
@bp.route("/departement/<string:departement>/logo/<string:logoname>")
|
||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoSuperAdmin)
|
@permission_required(Permission.ScoSuperAdmin)
|
||||||
def logo_get_local_dept_by_acronym(departement, logoname):
|
def api_get_local_logo_dept_by_acronym(departement, logoname):
|
||||||
"""Le logo: image (format png ou jpg).
|
|
||||||
|
|
||||||
**Exemple d'utilisation:**
|
|
||||||
|
|
||||||
* `/ScoDoc/api/departement/MMI/logo/header`
|
|
||||||
"""
|
|
||||||
dept_id = Departement.from_acronym(departement).id
|
dept_id = Departement.from_acronym(departement).id
|
||||||
return _core_get_logo(dept_id, logoname)
|
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>")
|
@bp.route("/departement/id/<int:dept_id>/logo/<string:logoname>")
|
||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoSuperAdmin)
|
@permission_required(Permission.ScoSuperAdmin)
|
||||||
def logo_get_local_dept_by_id(dept_id, logoname):
|
def api_get_local_logo_dept_by_id(dept_id, logoname):
|
||||||
"""Le logo: image (format png ou jpg).
|
|
||||||
|
|
||||||
**Exemple d'utilisation:**
|
|
||||||
|
|
||||||
* `/ScoDoc/api/departement/id/3/logo/header`
|
|
||||||
"""
|
|
||||||
return _core_get_logo(dept_id, logoname)
|
return _core_get_logo(dept_id, logoname)
|
||||||
|
@ -1,26 +1,23 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
"""
|
"""
|
||||||
ScoDoc 9 API : accès aux moduleimpl
|
ScoDoc 9 API : accès aux moduleimpl
|
||||||
|
|
||||||
CATEGORY
|
|
||||||
--------
|
|
||||||
ModuleImpl
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from flask import g
|
||||||
from flask_json import as_json
|
from flask_json import as_json
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
|
|
||||||
import app
|
|
||||||
from app.api import api_bp as bp, api_web_bp
|
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, permission_required
|
||||||
from app.decorators import scodoc
|
from app.models import (
|
||||||
from app.models import ModuleImpl
|
FormSemestre,
|
||||||
from app.scodoc import sco_liste_notes
|
ModuleImpl,
|
||||||
|
)
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
|
|
||||||
|
|
||||||
@ -32,45 +29,43 @@ from app.scodoc.sco_permissions import Permission
|
|||||||
@as_json
|
@as_json
|
||||||
def moduleimpl(moduleimpl_id: int):
|
def moduleimpl(moduleimpl_id: int):
|
||||||
"""
|
"""
|
||||||
Retourne le moduleimpl.
|
Retourne un moduleimpl en fonction de son id
|
||||||
|
|
||||||
PARAMS
|
|
||||||
------
|
|
||||||
moduleimpl_id : l'id d'un moduleimpl
|
moduleimpl_id : l'id d'un moduleimpl
|
||||||
|
|
||||||
Exemple de résultat :
|
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,
|
"id": 1,
|
||||||
"ects": null,
|
"formsemestre_id": 1,
|
||||||
"abbrev": "Init aux réseaux informatiques",
|
"module_id": 1,
|
||||||
"ue_id": 1,
|
"responsable_id": 2,
|
||||||
"code": "R101",
|
"moduleimpl_id": 1,
|
||||||
"formation_id": 1,
|
"ens": [],
|
||||||
"heures_cours": 0,
|
"module": {
|
||||||
"matiere_id": 1,
|
"heures_tp": 0,
|
||||||
"heures_td": 0,
|
"code_apogee": "",
|
||||||
"semestre_id": 1,
|
"titre": "Initiation aux réseaux informatiques",
|
||||||
"numero": 10,
|
"coefficient": 1,
|
||||||
"module_id": 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)
|
query = ModuleImpl.query.filter_by(id=moduleimpl_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
modimpl: ModuleImpl = query.first_or_404()
|
||||||
return modimpl.to_dict(convert_objects=True)
|
return modimpl.to_dict(convert_objects=True)
|
||||||
|
|
||||||
|
|
||||||
@ -81,55 +76,19 @@ def moduleimpl(moduleimpl_id: int):
|
|||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@as_json
|
||||||
def moduleimpl_inscriptions(moduleimpl_id: int):
|
def moduleimpl_inscriptions(moduleimpl_id: int):
|
||||||
"""Liste des inscriptions à ce moduleimpl.
|
"""Liste des inscriptions à ce moduleimpl
|
||||||
|
|
||||||
Exemple de résultat :
|
Exemple de résultat :
|
||||||
|
[
|
||||||
```json
|
{
|
||||||
[
|
"id": 1,
|
||||||
{
|
"etudid": 666,
|
||||||
"id": 1,
|
"moduleimpl_id": 1234,
|
||||||
"etudid": 666,
|
},
|
||||||
"moduleimpl_id": 1234,
|
...
|
||||||
},
|
]
|
||||||
...
|
|
||||||
]
|
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
|
query = ModuleImpl.query.filter_by(id=moduleimpl_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
modimpl: ModuleImpl = query.first_or_404()
|
||||||
return [i.to_dict() for i in modimpl.inscriptions]
|
return [i.to_dict() for i in modimpl.inscriptions]
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/moduleimpl/<int:moduleimpl_id>/notes")
|
|
||||||
@api_web_bp.route("/moduleimpl/<int:moduleimpl_id>/notes")
|
|
||||||
@login_required
|
|
||||||
@scodoc
|
|
||||||
@permission_required(Permission.ScoView)
|
|
||||||
def moduleimpl_notes(moduleimpl_id: int):
|
|
||||||
"""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
|
|
||||||
},
|
|
||||||
...
|
|
||||||
]
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
|
|
||||||
app.set_sco_dept(modimpl.formsemestre.departement.acronym)
|
|
||||||
table, _ = sco_liste_notes.do_evaluation_listenotes(
|
|
||||||
moduleimpl_id=modimpl.id, fmt="json"
|
|
||||||
)
|
|
||||||
return table
|
|
||||||
|
@ -1,16 +1,11 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
"""
|
"""
|
||||||
ScoDoc 9 API : partitions
|
ScoDoc 9 API : partitions
|
||||||
|
|
||||||
CATEGORY
|
|
||||||
--------
|
|
||||||
Groupes et Partitions
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
@ -23,8 +18,7 @@ from sqlalchemy.exc import IntegrityError
|
|||||||
import app
|
import app
|
||||||
from app import db, log
|
from app import db, log
|
||||||
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
|
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, permission_required
|
||||||
from app.decorators import scodoc
|
|
||||||
from app.scodoc.sco_utils import json_error
|
from app.scodoc.sco_utils import json_error
|
||||||
from app.models import FormSemestre, FormSemestreInscription, Identite
|
from app.models import FormSemestre, FormSemestreInscription, Identite
|
||||||
from app.models import GroupDescr, Partition, Scolog
|
from app.models import GroupDescr, Partition, Scolog
|
||||||
@ -46,8 +40,7 @@ def partition_info(partition_id: int):
|
|||||||
"""Info sur une partition.
|
"""Info sur une partition.
|
||||||
|
|
||||||
Exemple de résultat :
|
Exemple de résultat :
|
||||||
|
```
|
||||||
```json
|
|
||||||
{
|
{
|
||||||
'bul_show_rank': False,
|
'bul_show_rank': False,
|
||||||
'formsemestre_id': 39,
|
'formsemestre_id': 39,
|
||||||
@ -77,11 +70,10 @@ def partition_info(partition_id: int):
|
|||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@as_json
|
||||||
def formsemestre_partitions(formsemestre_id: int):
|
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 : {
|
partition_id : {
|
||||||
"bul_show_rank": False,
|
"bul_show_rank": False,
|
||||||
@ -95,7 +87,7 @@ def formsemestre_partitions(formsemestre_id: int):
|
|||||||
},
|
},
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
@ -115,18 +107,13 @@ def formsemestre_partitions(formsemestre_id: int):
|
|||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@as_json
|
||||||
def group_etudiants(group_id: int):
|
def etud_in_group(group_id: int):
|
||||||
"""
|
"""
|
||||||
Retourne la liste des étudiants dans un groupe
|
Retourne la liste des étudiants dans un groupe
|
||||||
(inscrits au groupe et inscrits au semestre).
|
(inscrits au groupe et inscrits au semestre).
|
||||||
|
|
||||||
PARAMS
|
|
||||||
------
|
|
||||||
group_id : l'id d'un groupe
|
group_id : l'id d'un groupe
|
||||||
|
|
||||||
Exemple de résultat :
|
Exemple de résultat :
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
'civilite': 'M',
|
'civilite': 'M',
|
||||||
@ -139,7 +126,6 @@ def group_etudiants(group_id: int):
|
|||||||
},
|
},
|
||||||
...
|
...
|
||||||
]
|
]
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
query = GroupDescr.query.filter_by(id=group_id)
|
query = GroupDescr.query.filter_by(id=group_id)
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
@ -164,14 +150,8 @@ def group_etudiants(group_id: int):
|
|||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@as_json
|
||||||
def group_etudiants_query(group_id: int):
|
def etud_in_group_query(group_id: int):
|
||||||
"""Étudiants du groupe, filtrés par état (aucun, `I`, `D`, `DEF`)
|
"""Étudiants du groupe, filtrés par état"""
|
||||||
|
|
||||||
QUERY
|
|
||||||
-----
|
|
||||||
etat : string
|
|
||||||
|
|
||||||
"""
|
|
||||||
etat = request.args.get("etat")
|
etat = request.args.get("etat")
|
||||||
if etat not in {None, scu.INSCRIT, scu.DEMISSION, scu.DEF}:
|
if etat not in {None, scu.INSCRIT, scu.DEMISSION, scu.DEF}:
|
||||||
return json_error(API_CLIENT_ERROR, "etat: valeur invalide")
|
return json_error(API_CLIENT_ERROR, "etat: valeur invalide")
|
||||||
@ -198,8 +178,8 @@ def group_etudiants_query(group_id: int):
|
|||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@as_json
|
||||||
def group_set_etudiant(group_id: int, etudid: int):
|
def set_etud_group(etudid: int, group_id: int):
|
||||||
"""Affecte l'étudiant au groupe indiqué."""
|
"""Affecte l'étudiant au groupe indiqué"""
|
||||||
etud = Identite.query.get_or_404(etudid)
|
etud = Identite.query.get_or_404(etudid)
|
||||||
query = GroupDescr.query.filter_by(id=group_id)
|
query = GroupDescr.query.filter_by(id=group_id)
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
@ -261,8 +241,7 @@ def group_remove_etud(group_id: int, etudid: int):
|
|||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@as_json
|
||||||
def partition_remove_etud(partition_id: int, etudid: int):
|
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)
|
(NB: en principe, un étudiant ne doit être que dans 0 ou 1 groupe d'une partition)
|
||||||
"""
|
"""
|
||||||
etud = Identite.query.get_or_404(etudid)
|
etud = Identite.query.get_or_404(etudid)
|
||||||
@ -307,15 +286,12 @@ def partition_remove_etud(partition_id: int, etudid: int):
|
|||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@as_json
|
||||||
def group_create(partition_id: int): # partition-group-create
|
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
|
The request content type should be "application/json":
|
||||||
----
|
|
||||||
```json
|
|
||||||
{
|
{
|
||||||
"group_name" : nom_du_groupe,
|
"group_name" : nom_du_groupe,
|
||||||
}
|
}
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
query = Partition.query.filter_by(id=partition_id)
|
query = Partition.query.filter_by(id=partition_id)
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
@ -327,26 +303,15 @@ def group_create(partition_id: int): # partition-group-create
|
|||||||
return json_error(403, "partition non editable")
|
return json_error(403, "partition non editable")
|
||||||
if not partition.formsemestre.can_change_groups():
|
if not partition.formsemestre.can_change_groups():
|
||||||
return json_error(401, "opération non autorisée")
|
return json_error(401, "opération non autorisée")
|
||||||
|
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
group_name = data.get("group_name")
|
||||||
group_name = args.get("group_name")
|
if group_name is None:
|
||||||
if not isinstance(group_name, str):
|
|
||||||
return json_error(API_CLIENT_ERROR, "missing group name or invalid data format")
|
return json_error(API_CLIENT_ERROR, "missing group name or invalid data format")
|
||||||
args["group_name"] = args["group_name"].strip()
|
if not GroupDescr.check_name(partition, group_name):
|
||||||
if not GroupDescr.check_name(partition, args["group_name"]):
|
|
||||||
return json_error(API_CLIENT_ERROR, "invalid group_name")
|
return json_error(API_CLIENT_ERROR, "invalid group_name")
|
||||||
|
group_name = group_name.strip()
|
||||||
|
|
||||||
# le numero est optionnel
|
group = GroupDescr(group_name=group_name, partition_id=partition_id)
|
||||||
numero = args.get("numero")
|
|
||||||
if numero is None:
|
|
||||||
numeros = [gr.numero or 0 for gr in partition.groups]
|
|
||||||
numero = (max(numeros) + 1) if numeros else 0
|
|
||||||
args["numero"] = numero
|
|
||||||
args["partition_id"] = partition_id
|
|
||||||
try:
|
|
||||||
group = GroupDescr(**args)
|
|
||||||
except TypeError:
|
|
||||||
return json_error(API_CLIENT_ERROR, "invalid arguments")
|
|
||||||
db.session.add(group)
|
db.session.add(group)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
log(f"created group {group}")
|
log(f"created group {group}")
|
||||||
@ -362,7 +327,7 @@ def group_create(partition_id: int): # partition-group-create
|
|||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@as_json
|
||||||
def group_delete(group_id: int):
|
def group_delete(group_id: int):
|
||||||
"""Suppression d'un groupe."""
|
"""Suppression d'un groupe"""
|
||||||
query = GroupDescr.query.filter_by(id=group_id)
|
query = GroupDescr.query.filter_by(id=group_id)
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
query = (
|
query = (
|
||||||
@ -391,7 +356,7 @@ def group_delete(group_id: int):
|
|||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@as_json
|
||||||
def group_edit(group_id: int):
|
def group_edit(group_id: int):
|
||||||
"""Édition d'un groupe."""
|
"""Edit a group"""
|
||||||
query = GroupDescr.query.filter_by(id=group_id)
|
query = GroupDescr.query.filter_by(id=group_id)
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
query = (
|
query = (
|
||||||
@ -404,54 +369,21 @@ def group_edit(group_id: int):
|
|||||||
return json_error(403, "partition non editable")
|
return json_error(403, "partition non editable")
|
||||||
if not group.partition.formsemestre.can_change_groups():
|
if not group.partition.formsemestre.can_change_groups():
|
||||||
return json_error(401, "opération non autorisée")
|
return json_error(401, "opération non autorisée")
|
||||||
|
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
group_name = data.get("group_name")
|
||||||
if "group_name" in args:
|
if group_name is not None:
|
||||||
if not isinstance(args["group_name"], str):
|
group_name = group_name.strip()
|
||||||
return json_error(API_CLIENT_ERROR, "invalid data format for group_name")
|
if not GroupDescr.check_name(group.partition, group_name, existing=True):
|
||||||
args["group_name"] = args["group_name"].strip() if args["group_name"] else ""
|
|
||||||
if not GroupDescr.check_name(
|
|
||||||
group.partition, args["group_name"], existing=True
|
|
||||||
):
|
|
||||||
return json_error(API_CLIENT_ERROR, "invalid group_name")
|
return json_error(API_CLIENT_ERROR, "invalid group_name")
|
||||||
|
group.group_name = group_name
|
||||||
group.from_dict(args)
|
db.session.add(group)
|
||||||
db.session.add(group)
|
db.session.commit()
|
||||||
db.session.commit()
|
log(f"modified {group}")
|
||||||
log(f"modified {group}")
|
|
||||||
|
|
||||||
app.set_sco_dept(group.partition.formsemestre.departement.acronym)
|
app.set_sco_dept(group.partition.formsemestre.departement.acronym)
|
||||||
sco_cache.invalidate_formsemestre(group.partition.formsemestre_id)
|
sco_cache.invalidate_formsemestre(group.partition.formsemestre_id)
|
||||||
return group.to_dict(with_partition=True)
|
return group.to_dict(with_partition=True)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/group/<int:group_id>/set_edt_id/<string:edt_id>", methods=["POST"])
|
|
||||||
@api_web_bp.route("/group/<int:group_id>/set_edt_id/<string:edt_id>", methods=["POST"])
|
|
||||||
@login_required
|
|
||||||
@scodoc
|
|
||||||
@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é.
|
|
||||||
"""
|
|
||||||
query = GroupDescr.query.filter_by(id=group_id)
|
|
||||||
if g.scodoc_dept:
|
|
||||||
query = (
|
|
||||||
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
|
||||||
)
|
|
||||||
group: GroupDescr = query.first_or_404()
|
|
||||||
if not group.partition.formsemestre.can_change_groups():
|
|
||||||
return json_error(401, "opération non autorisée")
|
|
||||||
log(f"group_set_edt_id( {group_id}, '{edt_id}' )")
|
|
||||||
group.edt_id = edt_id
|
|
||||||
db.session.add(group)
|
|
||||||
db.session.commit()
|
|
||||||
return group.to_dict(with_partition=True)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/formsemestre/<int:formsemestre_id>/partition/create", methods=["POST"])
|
@bp.route("/formsemestre/<int:formsemestre_id>/partition/create", methods=["POST"])
|
||||||
@api_web_bp.route(
|
@api_web_bp.route(
|
||||||
"/formsemestre/<int:formsemestre_id>/partition/create", methods=["POST"]
|
"/formsemestre/<int:formsemestre_id>/partition/create", methods=["POST"]
|
||||||
@ -461,19 +393,16 @@ def group_set_edt_id(group_id: int, edt_id: str):
|
|||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@as_json
|
||||||
def partition_create(formsemestre_id: int):
|
def partition_create(formsemestre_id: int):
|
||||||
"""Création d'une partition dans un semestre.
|
"""Création d'une partition dans un semestre
|
||||||
|
|
||||||
DATA
|
The request content type should be "application/json":
|
||||||
----
|
|
||||||
```json
|
|
||||||
{
|
{
|
||||||
"partition_name": str,
|
"partition_name": str,
|
||||||
"numero": int,
|
"numero":int,
|
||||||
"bul_show_rank": bool,
|
"bul_show_rank":bool,
|
||||||
"show_in_lists": bool,
|
"show_in_lists":bool,
|
||||||
"groups_editable": bool
|
"groups_editable":bool
|
||||||
}
|
}
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
@ -528,14 +457,9 @@ def partition_create(formsemestre_id: int):
|
|||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@as_json
|
||||||
def formsemestre_set_partitions_order(formsemestre_id: int):
|
def formsemestre_order_partitions(formsemestre_id: int):
|
||||||
"""Modifie l'ordre des partitions du formsemestre.
|
"""Modifie l'ordre des partitions du formsemestre
|
||||||
|
JSON args: [partition_id1, partition_id2, ...]
|
||||||
DATA
|
|
||||||
----
|
|
||||||
```json
|
|
||||||
[ partition_id1, partition_id2, ... ]
|
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
@ -546,7 +470,7 @@ def formsemestre_set_partitions_order(formsemestre_id: int):
|
|||||||
if not formsemestre.can_change_groups():
|
if not formsemestre.can_change_groups():
|
||||||
return json_error(401, "opération non autorisée")
|
return json_error(401, "opération non autorisée")
|
||||||
partition_ids = request.get_json(force=True) # may raise 400 Bad Request
|
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
|
isinstance(x, int) for x in partition_ids
|
||||||
):
|
):
|
||||||
return json_error(
|
return json_error(
|
||||||
@ -560,7 +484,6 @@ def formsemestre_set_partitions_order(formsemestre_id: int):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
app.set_sco_dept(formsemestre.departement.acronym)
|
app.set_sco_dept(formsemestre.departement.acronym)
|
||||||
sco_cache.invalidate_formsemestre(formsemestre_id)
|
sco_cache.invalidate_formsemestre(formsemestre_id)
|
||||||
log(f"formsemestre_set_partitions_order({partition_ids})")
|
|
||||||
return [
|
return [
|
||||||
partition.to_dict()
|
partition.to_dict()
|
||||||
for partition in formsemestre.partitions.order_by(Partition.numero)
|
for partition in formsemestre.partitions.order_by(Partition.numero)
|
||||||
@ -575,13 +498,8 @@ def formsemestre_set_partitions_order(formsemestre_id: int):
|
|||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@as_json
|
||||||
def partition_order_groups(partition_id: int):
|
def partition_order_groups(partition_id: int):
|
||||||
"""Modifie l'ordre des groupes de la partition.
|
"""Modifie l'ordre des groupes de la partition
|
||||||
|
JSON args: [group_id1, group_id2, ...]
|
||||||
DATA
|
|
||||||
----
|
|
||||||
```json
|
|
||||||
[ group_id1, group_id2, ... ]
|
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
query = Partition.query.filter_by(id=partition_id)
|
query = Partition.query.filter_by(id=partition_id)
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
@ -592,7 +510,7 @@ def partition_order_groups(partition_id: int):
|
|||||||
if not partition.formsemestre.can_change_groups():
|
if not partition.formsemestre.can_change_groups():
|
||||||
return json_error(401, "opération non autorisée")
|
return json_error(401, "opération non autorisée")
|
||||||
group_ids = request.get_json(force=True) # may raise 400 Bad Request
|
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
|
isinstance(x, int) for x in group_ids
|
||||||
):
|
):
|
||||||
return json_error(
|
return json_error(
|
||||||
@ -617,13 +535,10 @@ def partition_order_groups(partition_id: int):
|
|||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@as_json
|
||||||
def partition_edit(partition_id: int):
|
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.
|
The request content type should be "application/json"
|
||||||
|
All fields are optional:
|
||||||
DATA
|
|
||||||
----
|
|
||||||
```json
|
|
||||||
{
|
{
|
||||||
"partition_name": str,
|
"partition_name": str,
|
||||||
"numero":int,
|
"numero":int,
|
||||||
@ -631,7 +546,6 @@ def partition_edit(partition_id: int):
|
|||||||
"show_in_lists":bool,
|
"show_in_lists":bool,
|
||||||
"groups_editable":bool
|
"groups_editable":bool
|
||||||
}
|
}
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
query = Partition.query.filter_by(id=partition_id)
|
query = Partition.query.filter_by(id=partition_id)
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
@ -695,9 +609,9 @@ def partition_edit(partition_id: int):
|
|||||||
def partition_delete(partition_id: int):
|
def partition_delete(partition_id: int):
|
||||||
"""Suppression d'une partition (et de tous ses groupes).
|
"""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.
|
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.
|
sont désinscrits des parcours.
|
||||||
"""
|
"""
|
||||||
query = Partition.query.filter_by(id=partition_id)
|
query = Partition.query.filter_by(id=partition_id)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -3,18 +3,12 @@ from app import db, log
|
|||||||
from app.api import api_bp as bp
|
from app.api import api_bp as bp
|
||||||
from app.auth.logic import basic_auth, token_auth
|
from app.auth.logic import basic_auth, token_auth
|
||||||
|
|
||||||
"""
|
|
||||||
CATEGORY
|
|
||||||
--------
|
|
||||||
Authentification API
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/tokens", methods=["POST"])
|
@bp.route("/tokens", methods=["POST"])
|
||||||
@basic_auth.login_required
|
@basic_auth.login_required
|
||||||
@as_json
|
@as_json
|
||||||
def token_get():
|
def get_token():
|
||||||
"Renvoie un jeton jwt pour l'utilisateur courant."
|
"renvoie un jeton jwt pour l'utilisateur courant"
|
||||||
token = basic_auth.current_user().get_token()
|
token = basic_auth.current_user().get_token()
|
||||||
log(f"API: giving token to {basic_auth.current_user()}")
|
log(f"API: giving token to {basic_auth.current_user()}")
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@ -23,8 +17,8 @@ def token_get():
|
|||||||
|
|
||||||
@bp.route("/tokens", methods=["DELETE"])
|
@bp.route("/tokens", methods=["DELETE"])
|
||||||
@token_auth.login_required
|
@token_auth.login_required
|
||||||
def token_revoke():
|
def revoke_token():
|
||||||
"Révoque le jeton de l'utilisateur courant."
|
"révoque le jeton de l'utilisateur courant"
|
||||||
user = token_auth.current_user()
|
user = token_auth.current_user()
|
||||||
user.revoke_token()
|
user.revoke_token()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
"""ScoDoc 9 API : outils
|
"""ScoDoc 9 API : outils
|
||||||
|
184
app/api/users.py
184
app/api/users.py
@ -1,31 +1,28 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
"""
|
"""
|
||||||
ScoDoc 9 API : accès aux utilisateurs
|
ScoDoc 9 API : accès aux utilisateurs
|
||||||
|
|
||||||
CATEGORY
|
|
||||||
--------
|
|
||||||
Utilisateurs
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
from flask import g, request
|
from flask import g, request
|
||||||
from flask_json import as_json
|
from flask_json import as_json
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
|
|
||||||
from app import db, log
|
from app import db, log
|
||||||
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
|
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.scodoc.sco_utils import json_error
|
||||||
from app.auth.models import User, Role, UserRole
|
from app.auth.models import User, Role, UserRole
|
||||||
from app.auth.models import is_valid_password
|
from app.auth.models import is_valid_password
|
||||||
from app.decorators import scodoc
|
from app.decorators import scodoc, permission_required
|
||||||
from app.models import Departement
|
from app.models import Departement
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
from app.scodoc.sco_permissions import Permission
|
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>")
|
@bp.route("/user/<int:uid>")
|
||||||
@ -36,7 +33,7 @@ from app.scodoc.sco_utils import json_error
|
|||||||
@as_json
|
@as_json
|
||||||
def user_info(uid: int):
|
def user_info(uid: int):
|
||||||
"""
|
"""
|
||||||
Info sur un compte utilisateur ScoDoc.
|
Info sur un compte utilisateur scodoc
|
||||||
"""
|
"""
|
||||||
user: User = db.session.get(User, uid)
|
user: User = db.session.get(User, uid)
|
||||||
if user is None:
|
if user is None:
|
||||||
@ -57,22 +54,11 @@ def user_info(uid: int):
|
|||||||
@as_json
|
@as_json
|
||||||
def users_info_query():
|
def users_info_query():
|
||||||
"""Utilisateurs, filtrés par dept, active ou début nom
|
"""Utilisateurs, filtrés par dept, active ou début nom
|
||||||
|
|
||||||
Exemple:
|
|
||||||
```
|
|
||||||
/users/query?departement=dept_acronym&active=1&starts_with=<string:nom>
|
/users/query?departement=dept_acronym&active=1&starts_with=<string:nom>
|
||||||
```
|
|
||||||
|
|
||||||
Seuls les utilisateurs "accessibles" (selon les permissions) sont retournés.
|
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
|
Si accès via API web, le département de l'URL est ignoré, seules
|
||||||
les permissions de l'utilisateur sont prises en compte.
|
les permissions de l'utilisateur sont prises en compte.
|
||||||
|
|
||||||
QUERY
|
|
||||||
-----
|
|
||||||
active: bool
|
|
||||||
departement: string
|
|
||||||
starts_with: string
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
query = User.query
|
query = User.query
|
||||||
active = request.args.get("active")
|
active = request.args.get("active")
|
||||||
@ -99,20 +85,6 @@ def users_info_query():
|
|||||||
return [user.to_dict() for user in query]
|
return [user.to_dict() for user in query]
|
||||||
|
|
||||||
|
|
||||||
def _is_allowed_user_edit(args: dict) -> tuple[bool, str]:
|
|
||||||
"Vrai si on peut"
|
|
||||||
if "cas_id" in args and not current_user.has_permission(
|
|
||||||
Permission.UsersChangeCASId
|
|
||||||
):
|
|
||||||
return False, "non autorise a changer cas_id"
|
|
||||||
|
|
||||||
if not current_user.is_administrator():
|
|
||||||
for field in ("cas_allow_login", "cas_allow_scodoc_login"):
|
|
||||||
if field in args:
|
|
||||||
return False, f"non autorise a changer {field}"
|
|
||||||
return True, ""
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/user/create", methods=["POST"])
|
@bp.route("/user/create", methods=["POST"])
|
||||||
@api_web_bp.route("/user/create", methods=["POST"])
|
@api_web_bp.route("/user/create", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
@ -121,28 +93,23 @@ def _is_allowed_user_edit(args: dict) -> tuple[bool, str]:
|
|||||||
@as_json
|
@as_json
|
||||||
def user_create():
|
def user_create():
|
||||||
"""Création d'un utilisateur
|
"""Création d'un utilisateur
|
||||||
|
The request content type should be "application/json":
|
||||||
DATA
|
|
||||||
----
|
|
||||||
```json
|
|
||||||
{
|
{
|
||||||
"active":bool (default True),
|
"user_name": str,
|
||||||
"dept": str or null,
|
"dept": str or null,
|
||||||
"nom": str,
|
"nom": str,
|
||||||
"prenom": str,
|
"prenom": str,
|
||||||
"user_name": str,
|
"active":bool (default True)
|
||||||
...
|
|
||||||
}
|
}
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||||
user_name = args.get("user_name")
|
user_name = data.get("user_name")
|
||||||
if not user_name:
|
if not user_name:
|
||||||
return json_error(404, "empty user_name")
|
return json_error(404, "empty user_name")
|
||||||
user = User.query.filter_by(user_name=user_name).first()
|
user = User.query.filter_by(user_name=user_name).first()
|
||||||
if user:
|
if user:
|
||||||
return json_error(404, f"user_create: user {user} already exists\n")
|
return json_error(404, f"user_create: user {user} already exists\n")
|
||||||
dept = args.get("dept")
|
dept = data.get("dept")
|
||||||
if dept == "@all":
|
if dept == "@all":
|
||||||
dept = None
|
dept = None
|
||||||
allowed_depts = current_user.get_depts_with_permission(Permission.UsersAdmin)
|
allowed_depts = current_user.get_depts_with_permission(Permission.UsersAdmin)
|
||||||
@ -152,12 +119,10 @@ def user_create():
|
|||||||
Departement.query.filter_by(acronym=dept).first() is None
|
Departement.query.filter_by(acronym=dept).first() is None
|
||||||
):
|
):
|
||||||
return json_error(404, "user_create: departement inexistant")
|
return json_error(404, "user_create: departement inexistant")
|
||||||
args["dept"] = dept
|
nom = data.get("nom")
|
||||||
ok, msg = _is_allowed_user_edit(args)
|
prenom = data.get("prenom")
|
||||||
if not ok:
|
active = scu.to_bool(data.get("active", True))
|
||||||
return json_error(403, f"user_create: {msg}")
|
user = User(user_name=user_name, active=active, dept=dept, nom=nom, prenom=prenom)
|
||||||
user = User(user_name=user_name)
|
|
||||||
user.from_dict(args, new_user=True)
|
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return user.to_dict()
|
return user.to_dict()
|
||||||
@ -170,24 +135,20 @@ def user_create():
|
|||||||
@permission_required(Permission.UsersAdmin)
|
@permission_required(Permission.UsersAdmin)
|
||||||
@as_json
|
@as_json
|
||||||
def user_edit(uid: int):
|
def user_edit(uid: int):
|
||||||
"""Modification d'un utilisateur.
|
"""Modification d'un utilisateur
|
||||||
|
|
||||||
Champs modifiables:
|
Champs modifiables:
|
||||||
```json
|
|
||||||
{
|
{
|
||||||
"dept": str or null,
|
"dept": str or null,
|
||||||
"nom": str,
|
"nom": str,
|
||||||
"prenom": str,
|
"prenom": str,
|
||||||
"active":bool
|
"active":bool
|
||||||
...
|
|
||||||
}
|
}
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||||
user: User = User.query.get_or_404(uid)
|
user: User = User.query.get_or_404(uid)
|
||||||
# L'utilisateur doit avoir le droit dans le département de départ et celui d'arrivée
|
# L'utilisateur doit avoir le droit dans le département de départ et celui d'arrivée
|
||||||
orig_dept = user.dept
|
orig_dept = user.dept
|
||||||
dest_dept = args.get("dept", False)
|
dest_dept = data.get("dept", False)
|
||||||
if dest_dept is not False:
|
if dest_dept is not False:
|
||||||
if dest_dept == "@all":
|
if dest_dept == "@all":
|
||||||
dest_dept = None
|
dest_dept = None
|
||||||
@ -203,11 +164,10 @@ def user_edit(uid: int):
|
|||||||
return json_error(404, "user_edit: departement inexistant")
|
return json_error(404, "user_edit: departement inexistant")
|
||||||
user.dept = dest_dept
|
user.dept = dest_dept
|
||||||
|
|
||||||
ok, msg = _is_allowed_user_edit(args)
|
user.nom = data.get("nom", user.nom)
|
||||||
if not ok:
|
user.prenom = data.get("prenom", user.prenom)
|
||||||
return json_error(403, f"user_edit: {msg}")
|
user.active = scu.to_bool(data.get("active", user.active))
|
||||||
|
|
||||||
user.from_dict(args)
|
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return user.to_dict()
|
return user.to_dict()
|
||||||
@ -220,15 +180,11 @@ def user_edit(uid: int):
|
|||||||
@permission_required(Permission.UsersAdmin)
|
@permission_required(Permission.UsersAdmin)
|
||||||
@as_json
|
@as_json
|
||||||
def user_password(uid: int):
|
def user_password(uid: int):
|
||||||
"""Modification du mot de passe d'un utilisateur.
|
"""Modification du mot de passe d'un utilisateur
|
||||||
|
|
||||||
Champs modifiables:
|
Champs modifiables:
|
||||||
```json
|
|
||||||
{
|
{
|
||||||
"password": str
|
"password": str
|
||||||
}
|
}
|
||||||
```.
|
|
||||||
|
|
||||||
Si le mot de passe ne convient pas, erreur 400.
|
Si le mot de passe ne convient pas, erreur 400.
|
||||||
"""
|
"""
|
||||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||||
@ -262,7 +218,7 @@ def user_password(uid: int):
|
|||||||
@permission_required(Permission.ScoSuperAdmin)
|
@permission_required(Permission.ScoSuperAdmin)
|
||||||
@as_json
|
@as_json
|
||||||
def user_role_add(uid: int, role_name: str, dept: str = None):
|
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)
|
user: User = User.query.get_or_404(uid)
|
||||||
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
||||||
if dept is not None: # check
|
if dept is not None: # check
|
||||||
@ -291,7 +247,7 @@ def user_role_add(uid: int, role_name: str, dept: str = None):
|
|||||||
@permission_required(Permission.ScoSuperAdmin)
|
@permission_required(Permission.ScoSuperAdmin)
|
||||||
@as_json
|
@as_json
|
||||||
def user_role_remove(uid: int, role_name: str, dept: str = None):
|
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)
|
user: User = User.query.get_or_404(uid)
|
||||||
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
||||||
if dept is not None: # check
|
if dept is not None: # check
|
||||||
@ -317,8 +273,8 @@ def user_role_remove(uid: int, role_name: str, dept: str = None):
|
|||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.UsersView)
|
@permission_required(Permission.UsersView)
|
||||||
@as_json
|
@as_json
|
||||||
def permissions_list():
|
def list_permissions():
|
||||||
"""Liste des noms de permissions définies."""
|
"""Liste des noms de permissions définies"""
|
||||||
return list(Permission.permission_by_name.keys())
|
return list(Permission.permission_by_name.keys())
|
||||||
|
|
||||||
|
|
||||||
@ -328,7 +284,7 @@ def permissions_list():
|
|||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.UsersView)
|
@permission_required(Permission.UsersView)
|
||||||
@as_json
|
@as_json
|
||||||
def role_get(role_name: str):
|
def list_role(role_name: str):
|
||||||
"""Un rôle"""
|
"""Un rôle"""
|
||||||
return Role.query.filter_by(name=role_name).first_or_404().to_dict()
|
return Role.query.filter_by(name=role_name).first_or_404().to_dict()
|
||||||
|
|
||||||
@ -339,8 +295,8 @@ def role_get(role_name: str):
|
|||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.UsersView)
|
@permission_required(Permission.UsersView)
|
||||||
@as_json
|
@as_json
|
||||||
def roles_list():
|
def list_roles():
|
||||||
"""Tous les rôles définis."""
|
"""Tous les rôles définis"""
|
||||||
return [role.to_dict() for role in Role.query]
|
return [role.to_dict() for role in Role.query]
|
||||||
|
|
||||||
|
|
||||||
@ -357,7 +313,7 @@ def roles_list():
|
|||||||
@permission_required(Permission.ScoSuperAdmin)
|
@permission_required(Permission.ScoSuperAdmin)
|
||||||
@as_json
|
@as_json
|
||||||
def role_permission_add(role_name: str, perm_name: str):
|
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()
|
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
||||||
permission = Permission.get_by_name(perm_name)
|
permission = Permission.get_by_name(perm_name)
|
||||||
if permission is None:
|
if permission is None:
|
||||||
@ -382,7 +338,7 @@ def role_permission_add(role_name: str, perm_name: str):
|
|||||||
@permission_required(Permission.ScoSuperAdmin)
|
@permission_required(Permission.ScoSuperAdmin)
|
||||||
@as_json
|
@as_json
|
||||||
def role_permission_remove(role_name: str, perm_name: str):
|
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()
|
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
||||||
permission = Permission.get_by_name(perm_name)
|
permission = Permission.get_by_name(perm_name)
|
||||||
if permission is None:
|
if permission is None:
|
||||||
@ -401,15 +357,10 @@ def role_permission_remove(role_name: str, perm_name: str):
|
|||||||
@permission_required(Permission.ScoSuperAdmin)
|
@permission_required(Permission.ScoSuperAdmin)
|
||||||
@as_json
|
@as_json
|
||||||
def role_create(role_name: str):
|
def role_create(role_name: str):
|
||||||
"""Création d'un nouveau rôle avec les permissions données.
|
"""Create a new role with permissions.
|
||||||
|
|
||||||
DATA
|
|
||||||
----
|
|
||||||
```json
|
|
||||||
{
|
{
|
||||||
"permissions" : [ 'ScoView', ... ]
|
"permissions" : [ 'ScoView', ... ]
|
||||||
}
|
}
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
role: Role = Role.query.filter_by(name=role_name).first()
|
role: Role = Role.query.filter_by(name=role_name).first()
|
||||||
if role:
|
if role:
|
||||||
@ -434,16 +385,11 @@ def role_create(role_name: str):
|
|||||||
@permission_required(Permission.ScoSuperAdmin)
|
@permission_required(Permission.ScoSuperAdmin)
|
||||||
@as_json
|
@as_json
|
||||||
def role_edit(role_name: str):
|
def role_edit(role_name: str):
|
||||||
"""Édition d'un rôle. On peut spécifier un nom et/ou des permissions.
|
"""Edit a role. On peut spécifier un nom et/ou des permissions.
|
||||||
|
|
||||||
DATA
|
|
||||||
----
|
|
||||||
```json
|
|
||||||
{
|
{
|
||||||
"name" : name
|
"name" : name
|
||||||
"permissions" : [ 'ScoView', ... ]
|
"permissions" : [ 'ScoView', ... ]
|
||||||
}
|
}
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
||||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||||
@ -471,68 +417,8 @@ def role_edit(role_name: str):
|
|||||||
@permission_required(Permission.ScoSuperAdmin)
|
@permission_required(Permission.ScoSuperAdmin)
|
||||||
@as_json
|
@as_json
|
||||||
def role_delete(role_name: str):
|
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()
|
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
||||||
db.session.delete(role)
|
db.session.delete(role)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return {"OK": True}
|
return {"OK": True}
|
||||||
|
|
||||||
|
|
||||||
# @bp.route("/user/<int:uid>/edt")
|
|
||||||
# @api_web_bp.route("/user/<int:uid>/edt")
|
|
||||||
# @login_required
|
|
||||||
# @scodoc
|
|
||||||
# @permission_required(Permission.ScoView)
|
|
||||||
# @as_json
|
|
||||||
# def user_edt(uid: int):
|
|
||||||
# """L'emploi du temps de l'utilisateur.
|
|
||||||
# Si ok, une liste d'évènements. Sinon, une chaine indiquant un message d'erreur.
|
|
||||||
|
|
||||||
# show_modules_titles affiche le titre complet du module (défaut), sinon juste le code.
|
|
||||||
|
|
||||||
# Il faut la permission ScoView + (UsersView ou bien être connecté comme l'utilisateur demandé)
|
|
||||||
# """
|
|
||||||
# if g.scodoc_dept is None: # route API non départementale
|
|
||||||
# if not current_user.has_permission(Permission.UsersView):
|
|
||||||
# return scu.json_error(403, "accès non autorisé")
|
|
||||||
# user: User = db.session.get(User, uid)
|
|
||||||
# if user is None:
|
|
||||||
# return json_error(404, "user not found")
|
|
||||||
# # Check permission
|
|
||||||
# if current_user.id != user.id:
|
|
||||||
# if g.scodoc_dept:
|
|
||||||
# allowed_depts = current_user.get_depts_with_permission(Permission.UsersView)
|
|
||||||
# if (None not in allowed_depts) and (user.dept not in allowed_depts):
|
|
||||||
# return json_error(404, "user not found")
|
|
||||||
|
|
||||||
# show_modules_titles = scu.to_bool(request.args.get("show_modules_titles", False))
|
|
||||||
|
|
||||||
# # Cherche ics
|
|
||||||
# if not user.edt_id:
|
|
||||||
# return json_error(404, "user not configured")
|
|
||||||
# ics_filename = sco_edt_cal.get_ics_user_edt_filename(user.edt_id)
|
|
||||||
# if not ics_filename:
|
|
||||||
# return json_error(404, "no calendar for this user")
|
|
||||||
|
|
||||||
# _, calendar = sco_edt_cal.load_calendar(ics_filename)
|
|
||||||
|
|
||||||
# # TODO:
|
|
||||||
# # - Construire mapping edt2modimpl: edt_id -> modimpl
|
|
||||||
# # pour cela, considérer tous les formsemestres de la période de l'edt
|
|
||||||
# # (soit on considère l'année scolaire du 1er event, ou celle courante,
|
|
||||||
# # soit on cherche min, max des dates des events)
|
|
||||||
# # - Modifier décodage des groupes dans convert_ics pour avoi run mapping
|
|
||||||
# # de groupe par semestre (retrouvé grâce au modimpl associé à l'event)
|
|
||||||
|
|
||||||
# raise NotImplementedError() # TODO XXX WIP
|
|
||||||
|
|
||||||
# events_scodoc, _ = sco_edt_cal.convert_ics(
|
|
||||||
# calendar,
|
|
||||||
# edt2group=edt2group,
|
|
||||||
# default_group=default_group,
|
|
||||||
# edt2modimpl=edt2modimpl,
|
|
||||||
# )
|
|
||||||
# edt_dict = sco_edt_cal.translate_calendar(
|
|
||||||
# events_scodoc, group_ids, show_modules_titles=show_modules_titles
|
|
||||||
# )
|
|
||||||
# return edt_dict
|
|
||||||
|
@ -35,9 +35,9 @@ def after_cas_login():
|
|||||||
if user.cas_allow_login:
|
if user.cas_allow_login:
|
||||||
current_app.logger.info(f"CAS: login {user.user_name}")
|
current_app.logger.info(f"CAS: login {user.user_name}")
|
||||||
if login_user(user):
|
if login_user(user):
|
||||||
flask.session["scodoc_cas_login_date"] = (
|
flask.session[
|
||||||
datetime.datetime.now().isoformat()
|
"scodoc_cas_login_date"
|
||||||
)
|
] = datetime.datetime.now().isoformat()
|
||||||
user.cas_last_login = datetime.datetime.utcnow()
|
user.cas_last_login = datetime.datetime.utcnow()
|
||||||
if flask.session.get("CAS_EDT_ID"):
|
if flask.session.get("CAS_EDT_ID"):
|
||||||
# essaie de récupérer l'edt_id s'il est présent
|
# 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`
|
# via l'expression `cas_edt_id_from_xml_regexp`
|
||||||
# voir flask_cas.routing
|
# voir flask_cas.routing
|
||||||
edt_id = flask.session.get("CAS_EDT_ID")
|
edt_id = flask.session.get("CAS_EDT_ID")
|
||||||
current_app.logger.info(
|
current_app.logger.info(f"""after_cas_login: storing edt_id for {
|
||||||
f"""after_cas_login: storing edt_id for {
|
user.user_name}: '{edt_id}'""")
|
||||||
user.user_name}: '{edt_id}'"""
|
|
||||||
)
|
|
||||||
user.edt_id = edt_id
|
user.edt_id = edt_id
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@ -57,17 +55,12 @@ def after_cas_login():
|
|||||||
current_app.logger.info(
|
current_app.logger.info(
|
||||||
f"CAS login denied for {user.user_name} (not allowed to use CAS)"
|
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(
|
current_app.logger.info(
|
||||||
f"""CAS login denied for {
|
f"""CAS login denied for {
|
||||||
user.user_name if user else ""
|
user.user_name if user else ""
|
||||||
} cas_id={cas_id} (unknown or inactive)"""
|
} 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:
|
else:
|
||||||
current_app.logger.info(
|
current_app.logger.info(
|
||||||
f"""CAS attribute '{ScoDocSiteConfig.get("cas_attribute_id")}' not found !
|
f"""CAS attribute '{ScoDocSiteConfig.get("cas_attribute_id")}' not found !
|
||||||
|
@ -12,30 +12,23 @@ from typing import Optional
|
|||||||
|
|
||||||
import cracklib # pylint: disable=import-error
|
import cracklib # pylint: disable=import-error
|
||||||
|
|
||||||
|
import flask
|
||||||
from flask import current_app, g
|
from flask import current_app, g
|
||||||
from flask_login import UserMixin, AnonymousUserMixin
|
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
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
|
|
||||||
from app import db, email, log, login
|
from app import db, email, log, login
|
||||||
from app.models import Departement, ScoDocModel
|
from app.models import Departement
|
||||||
from app.models import SHORT_STR_LEN, USERNAME_STR_LEN
|
from app.models import SHORT_STR_LEN, USERNAME_STR_LEN
|
||||||
from app.models.config import ScoDocSiteConfig
|
from app.models.config import ScoDocSiteConfig
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
from app.scodoc.sco_roles_default import SCO_ROLES_DEFAULTS
|
from app.scodoc.sco_roles_default import SCO_ROLES_DEFAULTS
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
|
from app.scodoc import sco_etud # a deplacer dans scu
|
||||||
|
|
||||||
VALID_LOGIN_EXP = re.compile(r"^[a-zA-Z0-9@\\\-_\.]+$")
|
VALID_LOGIN_EXP = re.compile(r"^[a-zA-Z0-9@\\\-_\.]+$")
|
||||||
|
|
||||||
@ -57,17 +50,16 @@ def is_valid_password(cleartxt) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def is_valid_user_name(user_name: str) -> bool:
|
def invalid_user_name(user_name: str) -> bool:
|
||||||
"Check that user_name (aka login) is valid"
|
"Check that user_name (aka login) is invalid"
|
||||||
return (
|
return (
|
||||||
user_name
|
(len(user_name) < 2)
|
||||||
and (len(user_name) >= 2)
|
or (len(user_name) >= USERNAME_STR_LEN)
|
||||||
and (len(user_name) < USERNAME_STR_LEN)
|
or not VALID_LOGIN_EXP.match(user_name)
|
||||||
and VALID_LOGIN_EXP.match(user_name)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class User(UserMixin, ScoDocModel):
|
class User(UserMixin, db.Model):
|
||||||
"""ScoDoc users, handled by Flask / SQLAlchemy"""
|
"""ScoDoc users, handled by Flask / SQLAlchemy"""
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
@ -99,7 +91,7 @@ class User(UserMixin, ScoDocModel):
|
|||||||
"""date du dernier login via CAS"""
|
"""date du dernier login via CAS"""
|
||||||
edt_id = db.Column(db.Text(), index=True, nullable=True)
|
edt_id = db.Column(db.Text(), index=True, nullable=True)
|
||||||
"identifiant emplois du temps (unicité non imposée)"
|
"identifiant emplois du temps (unicité non imposée)"
|
||||||
password_hash = db.Column(db.Text()) # les hashs modernes peuvent être très longs
|
password_hash = db.Column(db.String(128))
|
||||||
password_scodoc7 = db.Column(db.String(42))
|
password_scodoc7 = db.Column(db.String(42))
|
||||||
last_seen = db.Column(db.DateTime, default=datetime.utcnow)
|
last_seen = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
date_modif_passwd = db.Column(db.DateTime, default=datetime.utcnow)
|
date_modif_passwd = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
@ -111,8 +103,6 @@ class User(UserMixin, ScoDocModel):
|
|||||||
token = db.Column(db.Text(), index=True, unique=True)
|
token = db.Column(db.Text(), index=True, unique=True)
|
||||||
token_expiration = db.Column(db.DateTime)
|
token_expiration = db.Column(db.DateTime)
|
||||||
|
|
||||||
# Define the back reference from User to ModuleImpl
|
|
||||||
modimpls = db.relationship("ModuleImpl", back_populates="responsable")
|
|
||||||
roles = db.relationship("Role", secondary="user_role", viewonly=True)
|
roles = db.relationship("Role", secondary="user_role", viewonly=True)
|
||||||
Permission = Permission
|
Permission = Permission
|
||||||
|
|
||||||
@ -126,17 +116,12 @@ class User(UserMixin, ScoDocModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
"user_name:str is mandatory"
|
|
||||||
self.roles = []
|
self.roles = []
|
||||||
self.user_roles = []
|
self.user_roles = []
|
||||||
# check login:
|
# check login:
|
||||||
if not "user_name" in kwargs:
|
if kwargs.get("user_name") and invalid_user_name(kwargs["user_name"]):
|
||||||
raise ValueError("missing user_name argument")
|
|
||||||
if not is_valid_user_name(kwargs["user_name"]):
|
|
||||||
raise ValueError(f"invalid user_name: {kwargs['user_name']}")
|
raise ValueError(f"invalid user_name: {kwargs['user_name']}")
|
||||||
kwargs["nom"] = kwargs.get("nom", "") or ""
|
super(User, self).__init__(**kwargs)
|
||||||
kwargs["prenom"] = kwargs.get("prenom", "") or ""
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
# Ajoute roles:
|
# Ajoute roles:
|
||||||
if (
|
if (
|
||||||
not self.roles
|
not self.roles
|
||||||
@ -245,44 +230,33 @@ class User(UserMixin, ScoDocModel):
|
|||||||
return None
|
return None
|
||||||
return db.session.get(User, user_id)
|
return db.session.get(User, user_id)
|
||||||
|
|
||||||
def sort_key(self) -> tuple:
|
|
||||||
"sort key"
|
|
||||||
return (
|
|
||||||
(self.nom or "").upper(),
|
|
||||||
(self.prenom or "").upper(),
|
|
||||||
(self.user_name or "").upper(),
|
|
||||||
)
|
|
||||||
|
|
||||||
def to_dict(self, include_email=True):
|
def to_dict(self, include_email=True):
|
||||||
"""l'utilisateur comme un dict, avec des champs supplémentaires"""
|
"""l'utilisateur comme un dict, avec des champs supplémentaires"""
|
||||||
data = {
|
data = {
|
||||||
"date_expiration": (
|
"date_expiration": self.date_expiration.isoformat() + "Z"
|
||||||
self.date_expiration.isoformat() + "Z" if self.date_expiration else None
|
if self.date_expiration
|
||||||
),
|
else None,
|
||||||
"date_modif_passwd": (
|
"date_modif_passwd": self.date_modif_passwd.isoformat() + "Z"
|
||||||
self.date_modif_passwd.isoformat() + "Z"
|
if self.date_modif_passwd
|
||||||
if self.date_modif_passwd
|
else None,
|
||||||
else None
|
"date_created": self.date_created.isoformat() + "Z"
|
||||||
),
|
if self.date_created
|
||||||
"date_created": (
|
else None,
|
||||||
self.date_created.isoformat() + "Z" if self.date_created else None
|
|
||||||
),
|
|
||||||
"dept": self.dept,
|
"dept": self.dept,
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"active": self.active,
|
"active": self.active,
|
||||||
"cas_id": self.cas_id,
|
"cas_id": self.cas_id,
|
||||||
"cas_allow_login": self.cas_allow_login,
|
"cas_allow_login": self.cas_allow_login,
|
||||||
"cas_allow_scodoc_login": self.cas_allow_scodoc_login,
|
"cas_allow_scodoc_login": self.cas_allow_scodoc_login,
|
||||||
"cas_last_login": (
|
"cas_last_login": self.cas_last_login.isoformat() + "Z"
|
||||||
self.cas_last_login.isoformat() + "Z" if self.cas_last_login else None
|
if self.cas_last_login
|
||||||
),
|
else None,
|
||||||
"edt_id": self.edt_id,
|
|
||||||
"status_txt": "actif" if self.active else "fermé",
|
"status_txt": "actif" if self.active else "fermé",
|
||||||
"last_seen": self.last_seen.isoformat() + "Z" if self.last_seen else None,
|
"last_seen": self.last_seen.isoformat() + "Z" if self.last_seen else None,
|
||||||
"nom": self.nom or "",
|
"nom": (self.nom or ""), # sco8
|
||||||
"prenom": self.prenom or "",
|
"prenom": (self.prenom or ""), # sco8
|
||||||
"roles_string": self.get_roles_string(), # eg "Ens_RT, Ens_Info"
|
"roles_string": self.get_roles_string(), # eg "Ens_RT, Ens_Info"
|
||||||
"user_name": self.user_name,
|
"user_name": self.user_name, # sco8
|
||||||
# Les champs calculés:
|
# Les champs calculés:
|
||||||
"nom_fmt": self.get_nom_fmt(),
|
"nom_fmt": self.get_nom_fmt(),
|
||||||
"prenom_fmt": self.get_prenom_fmt(),
|
"prenom_fmt": self.get_prenom_fmt(),
|
||||||
@ -296,55 +270,37 @@ class User(UserMixin, ScoDocModel):
|
|||||||
data["email_institutionnel"] = self.email_institutionnel or ""
|
data["email_institutionnel"] = self.email_institutionnel or ""
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def convert_dict_fields(cls, args: dict) -> dict:
|
|
||||||
"""Convert fields in the given dict. No other side effect.
|
|
||||||
args: dict with args in application.
|
|
||||||
returns: dict to store in model's db.
|
|
||||||
Convert boolean values to bools.
|
|
||||||
"""
|
|
||||||
args_dict = args
|
|
||||||
# Dates
|
|
||||||
if "date_expiration" in args:
|
|
||||||
date_expiration = args.get("date_expiration")
|
|
||||||
if isinstance(date_expiration, str):
|
|
||||||
args["date_expiration"] = (
|
|
||||||
datetime.datetime.fromisoformat(date_expiration)
|
|
||||||
if date_expiration
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
# booléens:
|
|
||||||
for field in ("active", "cas_allow_login", "cas_allow_scodoc_login"):
|
|
||||||
if field in args:
|
|
||||||
args_dict[field] = scu.to_bool(args.get(field))
|
|
||||||
|
|
||||||
# chaines ne devant pas être NULLs
|
|
||||||
for field in ("nom", "prenom"):
|
|
||||||
if field in args:
|
|
||||||
args[field] = args[field] or ""
|
|
||||||
|
|
||||||
# chaines ne devant pas être vides mais au contraire null (unicité)
|
|
||||||
if "cas_id" in args:
|
|
||||||
args["cas_id"] = args["cas_id"] or None
|
|
||||||
|
|
||||||
return args_dict
|
|
||||||
|
|
||||||
def from_dict(self, data: dict, new_user=False):
|
def from_dict(self, data: dict, new_user=False):
|
||||||
"""Set users' attributes from given dict values.
|
"""Set users' attributes from given dict values.
|
||||||
- roles_string : roles, encoded like "Ens_RT, Secr_CJ"
|
Roles must be encoded as "roles_string", like "Ens_RT, Secr_CJ"
|
||||||
- date_expiration is a dateime object.
|
|
||||||
Does not check permissions here.
|
|
||||||
"""
|
"""
|
||||||
|
for field in [
|
||||||
|
"nom",
|
||||||
|
"prenom",
|
||||||
|
"dept",
|
||||||
|
"active",
|
||||||
|
"email",
|
||||||
|
"email_institutionnel",
|
||||||
|
"date_expiration",
|
||||||
|
"cas_id",
|
||||||
|
]:
|
||||||
|
if field in data:
|
||||||
|
setattr(self, field, data[field] or None)
|
||||||
|
# required boolean fields
|
||||||
|
for field in [
|
||||||
|
"cas_allow_login",
|
||||||
|
"cas_allow_scodoc_login",
|
||||||
|
]:
|
||||||
|
setattr(self, field, scu.to_bool(data.get(field, False)))
|
||||||
|
|
||||||
if new_user:
|
if new_user:
|
||||||
if "user_name" in data:
|
if "user_name" in data:
|
||||||
# never change name of existing users
|
# never change name of existing users
|
||||||
# (see change_user_name method to do that)
|
|
||||||
if not is_valid_user_name(data["user_name"]):
|
|
||||||
raise ValueError(f"invalid user_name: {data['user_name']}")
|
|
||||||
self.user_name = data["user_name"]
|
self.user_name = data["user_name"]
|
||||||
if "password" in data:
|
if "password" in data:
|
||||||
self.set_password(data["password"])
|
self.set_password(data["password"])
|
||||||
|
if invalid_user_name(self.user_name):
|
||||||
|
raise ValueError(f"invalid user_name: {self.user_name}")
|
||||||
# Roles: roles_string is "Ens_RT, Secr_RT, ..."
|
# Roles: roles_string is "Ens_RT, Secr_RT, ..."
|
||||||
if "roles_string" in data:
|
if "roles_string" in data:
|
||||||
self.user_roles = []
|
self.user_roles = []
|
||||||
@ -353,13 +309,11 @@ class User(UserMixin, ScoDocModel):
|
|||||||
role, dept = UserRole.role_dept_from_string(r_d)
|
role, dept = UserRole.role_dept_from_string(r_d)
|
||||||
self.add_role(role, dept)
|
self.add_role(role, dept)
|
||||||
|
|
||||||
super().from_dict(data, excluded={"user_name", "roles_string", "roles"})
|
|
||||||
|
|
||||||
# Set cas_id using regexp if configured:
|
# Set cas_id using regexp if configured:
|
||||||
exp = ScoDocSiteConfig.get("cas_uid_from_mail_regexp")
|
exp = ScoDocSiteConfig.get("cas_uid_from_mail_regexp")
|
||||||
if exp and self.email_institutionnel:
|
if exp and self.email_institutionnel:
|
||||||
cas_id = ScoDocSiteConfig.extract_cas_id(self.email_institutionnel)
|
cas_id = ScoDocSiteConfig.extract_cas_id(self.email_institutionnel)
|
||||||
if cas_id:
|
if cas_id is not None:
|
||||||
self.cas_id = cas_id
|
self.cas_id = cas_id
|
||||||
|
|
||||||
def get_token(self, expires_in=3600):
|
def get_token(self, expires_in=3600):
|
||||||
@ -487,12 +441,12 @@ class User(UserMixin, ScoDocModel):
|
|||||||
"""nomplogin est le nom en majuscules suivi du prénom et du login
|
"""nomplogin est le nom en majuscules suivi du prénom et du login
|
||||||
e.g. Dupont Pierre (dupont)
|
e.g. Dupont Pierre (dupont)
|
||||||
"""
|
"""
|
||||||
nom = scu.format_nom(self.nom) if self.nom else self.user_name.upper()
|
nom = sco_etud.format_nom(self.nom) if self.nom else self.user_name.upper()
|
||||||
return f"{nom} {scu.format_prenom(self.prenom)} ({self.user_name})"
|
return f"{nom} {sco_etud.format_prenom(self.prenom)} ({self.user_name})"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_user_from_nomplogin(nomplogin: str) -> Optional["User"]:
|
def get_user_id_from_nomplogin(nomplogin: str) -> Optional[int]:
|
||||||
"""Returns User instance from the string "Dupont Pierre (dupont)"
|
"""Returns id from the string "Dupont Pierre (dupont)"
|
||||||
or None if user does not exist
|
or None if user does not exist
|
||||||
"""
|
"""
|
||||||
match = re.match(r".*\((.*)\)", nomplogin.strip())
|
match = re.match(r".*\((.*)\)", nomplogin.strip())
|
||||||
@ -500,96 +454,38 @@ class User(UserMixin, ScoDocModel):
|
|||||||
user_name = match.group(1)
|
user_name = match.group(1)
|
||||||
u = User.query.filter_by(user_name=user_name).first()
|
u = User.query.filter_by(user_name=user_name).first()
|
||||||
if u:
|
if u:
|
||||||
return u
|
return u.id
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_nom_fmt(self):
|
def get_nom_fmt(self):
|
||||||
"""Nom formaté: "Martin" """
|
"""Nom formaté: "Martin" """
|
||||||
if self.nom:
|
if self.nom:
|
||||||
return scu.format_nom(self.nom, uppercase=False)
|
return sco_etud.format_nom(self.nom, uppercase=False)
|
||||||
else:
|
else:
|
||||||
return self.user_name
|
return self.user_name
|
||||||
|
|
||||||
def get_prenom_fmt(self):
|
def get_prenom_fmt(self):
|
||||||
"""Prénom formaté (minuscule capitalisées)"""
|
"""Prénom formaté (minuscule capitalisées)"""
|
||||||
return scu.format_prenom(self.prenom)
|
return sco_etud.format_prenom(self.prenom)
|
||||||
|
|
||||||
def get_nomprenom(self):
|
def get_nomprenom(self):
|
||||||
"""Nom capitalisé suivi de l'initiale du prénom:
|
"""Nom capitalisé suivi de l'initiale du prénom:
|
||||||
Viennet E.
|
Viennet E.
|
||||||
"""
|
"""
|
||||||
prenom_abbrv = scu.abbrev_prenom(scu.format_prenom(self.prenom))
|
prenom_abbrv = scu.abbrev_prenom(sco_etud.format_prenom(self.prenom))
|
||||||
return (self.get_nom_fmt() + " " + prenom_abbrv).strip()
|
return (self.get_nom_fmt() + " " + prenom_abbrv).strip()
|
||||||
|
|
||||||
def get_prenomnom(self):
|
def get_prenomnom(self):
|
||||||
"""L'initiale du prénom suivie du nom: "J.-C. Dupont" """
|
"""L'initiale du prénom suivie du nom: "J.-C. Dupont" """
|
||||||
prenom_abbrv = scu.abbrev_prenom(scu.format_prenom(self.prenom))
|
prenom_abbrv = scu.abbrev_prenom(sco_etud.format_prenom(self.prenom))
|
||||||
return (prenom_abbrv + " " + self.get_nom_fmt()).strip()
|
return (prenom_abbrv + " " + self.get_nom_fmt()).strip()
|
||||||
|
|
||||||
def get_nomcomplet(self):
|
def get_nomcomplet(self):
|
||||||
"Prénom et nom complets"
|
"Prénom et nom complets"
|
||||||
return scu.format_prenom(self.prenom) + " " + self.get_nom_fmt()
|
return sco_etud.format_prenom(self.prenom) + " " + self.get_nom_fmt()
|
||||||
|
|
||||||
# nomnoacc était le nom en minuscules sans accents (inutile)
|
# 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):
|
class AnonymousUser(AnonymousUserMixin):
|
||||||
"Notre utilisateur anonyme"
|
"Notre utilisateur anonyme"
|
||||||
@ -671,19 +567,8 @@ class Role(db.Model):
|
|||||||
"""Create default roles if missing, then, if reset_permissions,
|
"""Create default roles if missing, then, if reset_permissions,
|
||||||
reset their permissions to default values.
|
reset their permissions to default values.
|
||||||
"""
|
"""
|
||||||
Role.reset_roles_permissions(
|
|
||||||
SCO_ROLES_DEFAULTS, reset_permissions=reset_permissions
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def reset_roles_permissions(roles_perms: dict[str, tuple], reset_permissions=True):
|
|
||||||
"""Ajoute les permissions aux roles
|
|
||||||
roles_perms : { "role_name" : (permission, ...) }
|
|
||||||
reset_permissions : si vrai efface permissions déja existantes
|
|
||||||
Si le role n'existe pas, il est (re) créé.
|
|
||||||
"""
|
|
||||||
default_role = "Observateur"
|
default_role = "Observateur"
|
||||||
for role_name, permissions in roles_perms.items():
|
for role_name, permissions in SCO_ROLES_DEFAULTS.items():
|
||||||
role = Role.query.filter_by(name=role_name).first()
|
role = Role.query.filter_by(name=role_name).first()
|
||||||
if role is None:
|
if role is None:
|
||||||
role = Role(name=role_name)
|
role = Role(name=role_name)
|
||||||
|
@ -18,7 +18,7 @@ from app.auth.forms import (
|
|||||||
ResetPasswordRequestForm,
|
ResetPasswordRequestForm,
|
||||||
UserCreationForm,
|
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.auth.email import send_password_reset_email
|
||||||
from app.decorators import admin_required
|
from app.decorators import admin_required
|
||||||
from app.forms.generic import SimpleConfirmationForm
|
from app.forms.generic import SimpleConfirmationForm
|
||||||
@ -35,12 +35,10 @@ def _login_form():
|
|||||||
form = LoginForm()
|
form = LoginForm()
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
# note: ceci est la première requête SQL déclenchée par un utilisateur arrivant
|
# note: ceci est la première requête SQL déclenchée par un utilisateur arrivant
|
||||||
user = (
|
if invalid_user_name(form.user_name.data):
|
||||||
User.query.filter_by(user_name=form.user_name.data).first()
|
user = None
|
||||||
if is_valid_user_name(form.user_name.data)
|
else:
|
||||||
else None
|
user = User.query.filter_by(user_name=form.user_name.data).first()
|
||||||
)
|
|
||||||
|
|
||||||
if user is None or not user.check_password(form.password.data):
|
if user is None or not user.check_password(form.password.data):
|
||||||
current_app.logger.info("login: invalid (%s)", form.user_name.data)
|
current_app.logger.info("login: invalid (%s)", form.user_name.data)
|
||||||
flash(_("Nom ou mot de passe invalide"))
|
flash(_("Nom ou mot de passe invalide"))
|
||||||
@ -56,7 +54,6 @@ def _login_form():
|
|||||||
title=_("Sign In"),
|
title=_("Sign In"),
|
||||||
form=form,
|
form=form,
|
||||||
is_cas_enabled=ScoDocSiteConfig.is_cas_enabled(),
|
is_cas_enabled=ScoDocSiteConfig.is_cas_enabled(),
|
||||||
is_cas_forced=ScoDocSiteConfig.is_cas_forced(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -211,3 +208,5 @@ def cas_users_import_config():
|
|||||||
title=_("Importation configuration CAS utilisateurs"),
|
title=_("Importation configuration CAS utilisateurs"),
|
||||||
form=form,
|
form=form,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -21,9 +21,9 @@ def form_ue_choix_parcours(ue: UniteEns) -> str:
|
|||||||
return ""
|
return ""
|
||||||
ref_comp = ue.formation.referentiel_competence
|
ref_comp = ue.formation.referentiel_competence
|
||||||
if ref_comp is None:
|
if ref_comp is None:
|
||||||
return f"""<div class="scobox ue_advanced">
|
return f"""<div class="ue_advanced">
|
||||||
<div class="warning">Pas de référentiel de compétence associé à cette formation !</div>
|
<div class="warning">Pas de référentiel de compétence associé à cette formation !</div>
|
||||||
<div><a class="stdlink" href="{ url_for('notes.refcomp_assoc_formation',
|
<div><a class="stdlink" href="{ url_for('notes.refcomp_assoc_formation',
|
||||||
scodoc_dept=g.scodoc_dept, formation_id=ue.formation.id)
|
scodoc_dept=g.scodoc_dept, formation_id=ue.formation.id)
|
||||||
}">associer un référentiel de compétence</a>
|
}">associer un référentiel de compétence</a>
|
||||||
</div>
|
</div>
|
||||||
@ -31,36 +31,27 @@ def form_ue_choix_parcours(ue: UniteEns) -> str:
|
|||||||
|
|
||||||
H = [
|
H = [
|
||||||
"""
|
"""
|
||||||
<div class="scobox ue_advanced">
|
<div class="ue_advanced">
|
||||||
<div class="scobox-title">Parcours du BUT</div>
|
<h3>Parcours du BUT</h3>
|
||||||
"""
|
"""
|
||||||
]
|
]
|
||||||
# Choix des parcours
|
# Choix des parcours
|
||||||
ue_pids = [p.id for p in ue.parcours]
|
ue_pids = [p.id for p in ue.parcours]
|
||||||
H.append(
|
H.append("""<form id="choix_parcours">""")
|
||||||
"""
|
|
||||||
<div class="help">
|
|
||||||
Cocher tous les parcours dans lesquels cette UE est utilisée,
|
|
||||||
même si vous n'offrez pas ce parcours dans votre département.
|
|
||||||
Sans quoi, les UEs de Tronc Commun ne seront pas reconnues.
|
|
||||||
Ne cocher aucun parcours est équivalent à tous les cocher.
|
|
||||||
</div>
|
|
||||||
<form id="choix_parcours" style="margin-top: 12px;">
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
ects_differents = {
|
ects_differents = {
|
||||||
ue.get_ects(parcour, only_parcours=True) for parcour in ref_comp.parcours
|
ue.get_ects(parcour, only_parcours=True) for parcour in ref_comp.parcours
|
||||||
} != {None}
|
} != {None}
|
||||||
for parcour in ref_comp.parcours:
|
for parcour in ref_comp.parcours:
|
||||||
|
ects_parcour = ue.get_ects(parcour)
|
||||||
ects_parcour_txt = (
|
ects_parcour_txt = (
|
||||||
f" ({ue.get_ects(parcour):.3g} ects)" if ects_differents else ""
|
f" ({ue.get_ects(parcour):.3g} ects)" if ects_differents else ""
|
||||||
)
|
)
|
||||||
H.append(
|
H.append(
|
||||||
f"""<label><input type="checkbox" name="{parcour.id}" value="{parcour.id}"
|
f"""<label><input type="checkbox" name="{parcour.id}" value="{parcour.id}"
|
||||||
{'checked' if parcour.id in ue_pids else ""}
|
{'checked' if parcour.id in ue_pids else ""}
|
||||||
onclick="set_ue_parcour(this);"
|
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)}"
|
scodoc_dept=g.scodoc_dept, ue_id=ue.id)}"
|
||||||
>{parcour.code}{ects_parcour_txt}</label>"""
|
>{parcour.code}{ects_parcour_txt}</label>"""
|
||||||
)
|
)
|
||||||
@ -71,7 +62,7 @@ def form_ue_choix_parcours(ue: UniteEns) -> str:
|
|||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<a class="stdlink" href="{
|
<a class="stdlink" href="{
|
||||||
url_for("notes.ue_parcours_ects",
|
url_for("notes.ue_parcours_ects",
|
||||||
scodoc_dept=g.scodoc_dept, ue_id=ue.id)
|
scodoc_dept=g.scodoc_dept, ue_id=ue.id)
|
||||||
}">définir des ECTS différents dans chaque parcours</a>
|
}">définir des ECTS différents dans chaque parcours</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -9,14 +9,12 @@
|
|||||||
|
|
||||||
import collections
|
import collections
|
||||||
import datetime
|
import datetime
|
||||||
import pandas as pd
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from flask import g, has_request_context, url_for
|
from flask import g, has_request_context, url_for
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.comp.moy_mod import ModuleImplResults
|
|
||||||
from app.comp.res_but import ResultatsSemestreBUT
|
from app.comp.res_but import ResultatsSemestreBUT
|
||||||
from app.models import Evaluation, FormSemestre, Identite, ModuleImpl
|
from app.models import Evaluation, FormSemestre, Identite
|
||||||
from app.models.groups import GroupDescr
|
from app.models.groups import GroupDescr
|
||||||
from app.models.ues import UniteEns
|
from app.models.ues import UniteEns
|
||||||
from app.scodoc import sco_bulletins, sco_utils as scu
|
from app.scodoc import sco_bulletins, sco_utils as scu
|
||||||
@ -106,11 +104,9 @@ class BulletinBUT:
|
|||||||
"competence": None, # XXX TODO lien avec référentiel
|
"competence": None, # XXX TODO lien avec référentiel
|
||||||
"moyenne": None,
|
"moyenne": None,
|
||||||
# Le bonus sport appliqué sur cette UE
|
# Le bonus sport appliqué sur cette UE
|
||||||
"bonus": (
|
"bonus": fmt_note(res.bonus_ues[ue.id][etud.id])
|
||||||
fmt_note(res.bonus_ues[ue.id][etud.id])
|
if res.bonus_ues is not None and ue.id in res.bonus_ues
|
||||||
if res.bonus_ues is not None and ue.id in res.bonus_ues
|
else fmt_note(0.0),
|
||||||
else fmt_note(0.0)
|
|
||||||
),
|
|
||||||
"malus": fmt_note(res.malus[ue.id][etud.id]),
|
"malus": fmt_note(res.malus[ue.id][etud.id]),
|
||||||
"capitalise": None, # "AAAA-MM-JJ" TODO #sco93
|
"capitalise": None, # "AAAA-MM-JJ" TODO #sco93
|
||||||
"ressources": self.etud_ue_mod_results(etud, ue, res.ressources),
|
"ressources": self.etud_ue_mod_results(etud, ue, res.ressources),
|
||||||
@ -185,16 +181,14 @@ class BulletinBUT:
|
|||||||
"is_external": ue_capitalisee.is_external,
|
"is_external": ue_capitalisee.is_external,
|
||||||
"date_capitalisation": ue_capitalisee.event_date,
|
"date_capitalisation": ue_capitalisee.event_date,
|
||||||
"formsemestre_id": ue_capitalisee.formsemestre_id,
|
"formsemestre_id": ue_capitalisee.formsemestre_id,
|
||||||
"bul_orig_url": (
|
"bul_orig_url": url_for(
|
||||||
url_for(
|
"notes.formsemestre_bulletinetud",
|
||||||
"notes.formsemestre_bulletinetud",
|
scodoc_dept=g.scodoc_dept,
|
||||||
scodoc_dept=g.scodoc_dept,
|
etudid=etud.id,
|
||||||
etudid=etud.id,
|
formsemestre_id=ue_capitalisee.formsemestre_id,
|
||||||
formsemestre_id=ue_capitalisee.formsemestre_id,
|
)
|
||||||
)
|
if ue_capitalisee.formsemestre_id
|
||||||
if ue_capitalisee.formsemestre_id
|
else None,
|
||||||
else None
|
|
||||||
),
|
|
||||||
"ressources": {}, # sans détail en BUT
|
"ressources": {}, # sans détail en BUT
|
||||||
"saes": {},
|
"saes": {},
|
||||||
}
|
}
|
||||||
@ -231,17 +225,15 @@ class BulletinBUT:
|
|||||||
if res.modimpl_inscr_df[modimpl.id][etud.id]: # si inscrit
|
if res.modimpl_inscr_df[modimpl.id][etud.id]: # si inscrit
|
||||||
d[modimpl.module.code] = {
|
d[modimpl.module.code] = {
|
||||||
"id": modimpl.id,
|
"id": modimpl.id,
|
||||||
"titre": modimpl.module.titre_str(),
|
"titre": modimpl.module.titre,
|
||||||
"code_apogee": modimpl.module.code_apogee,
|
"code_apogee": modimpl.module.code_apogee,
|
||||||
"url": (
|
"url": url_for(
|
||||||
url_for(
|
"notes.moduleimpl_status",
|
||||||
"notes.moduleimpl_status",
|
scodoc_dept=g.scodoc_dept,
|
||||||
scodoc_dept=g.scodoc_dept,
|
moduleimpl_id=modimpl.id,
|
||||||
moduleimpl_id=modimpl.id,
|
)
|
||||||
)
|
if has_request_context()
|
||||||
if has_request_context()
|
else "na",
|
||||||
else "na"
|
|
||||||
),
|
|
||||||
"moyenne": {
|
"moyenne": {
|
||||||
# # moyenne indicative de module: moyenne des UE,
|
# # moyenne indicative de module: moyenne des UE,
|
||||||
# # ignorant celles sans notes (nan)
|
# # ignorant celles sans notes (nan)
|
||||||
@ -250,115 +242,68 @@ class BulletinBUT:
|
|||||||
# "max": fmt_note(moyennes_etuds.max()),
|
# "max": fmt_note(moyennes_etuds.max()),
|
||||||
# "moy": fmt_note(moyennes_etuds.mean()),
|
# "moy": fmt_note(moyennes_etuds.mean()),
|
||||||
},
|
},
|
||||||
"evaluations": (
|
"evaluations": [
|
||||||
self.etud_list_modimpl_evaluations(
|
self.etud_eval_results(etud, e)
|
||||||
etud, modimpl, modimpl_results, version
|
for e in modimpl.evaluations
|
||||||
|
if (e.visibulletin or version == "long")
|
||||||
|
and (e.id in modimpl_results.evaluations_etat)
|
||||||
|
and (
|
||||||
|
modimpl_results.evaluations_etat[e.id].is_complete
|
||||||
|
or self.prefs["bul_show_all_evals"]
|
||||||
)
|
)
|
||||||
if version != "short"
|
]
|
||||||
else []
|
if version != "short"
|
||||||
),
|
else [],
|
||||||
}
|
}
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def etud_list_modimpl_evaluations(
|
def etud_eval_results(self, etud, e: Evaluation) -> dict:
|
||||||
self,
|
|
||||||
etud: Identite,
|
|
||||||
modimpl: ModuleImpl,
|
|
||||||
modimpl_results: ModuleImplResults,
|
|
||||||
version: str,
|
|
||||||
) -> list[dict]:
|
|
||||||
"""Liste des résultats aux évaluations de ce modimpl à montrer pour cet étudiant"""
|
|
||||||
evaluation: Evaluation
|
|
||||||
eval_results = []
|
|
||||||
for evaluation in modimpl.evaluations:
|
|
||||||
if (
|
|
||||||
(evaluation.visibulletin or version == "long")
|
|
||||||
and (evaluation.id in modimpl_results.evaluations_etat)
|
|
||||||
and (
|
|
||||||
modimpl_results.evaluations_etat[evaluation.id].is_complete
|
|
||||||
or self.prefs["bul_show_all_evals"]
|
|
||||||
)
|
|
||||||
):
|
|
||||||
eval_notes = self.res.modimpls_results[modimpl.id].evals_notes[
|
|
||||||
evaluation.id
|
|
||||||
]
|
|
||||||
|
|
||||||
if (evaluation.evaluation_type == Evaluation.EVALUATION_NORMALE) or (
|
|
||||||
not np.isnan(eval_notes[etud.id])
|
|
||||||
):
|
|
||||||
eval_results.append(
|
|
||||||
self.etud_eval_results(etud, evaluation, eval_notes)
|
|
||||||
)
|
|
||||||
return eval_results
|
|
||||||
|
|
||||||
def etud_eval_results(
|
|
||||||
self, etud: Identite, evaluation: Evaluation, eval_notes: pd.DataFrame
|
|
||||||
) -> dict:
|
|
||||||
"dict resultats d'un étudiant à une évaluation"
|
"dict resultats d'un étudiant à une évaluation"
|
||||||
# eval_notes est une pd.Series avec toutes les notes des étudiants inscrits
|
# eval_notes est une pd.Series avec toutes les notes des étudiants inscrits
|
||||||
|
eval_notes = self.res.modimpls_results[e.moduleimpl_id].evals_notes[e.id]
|
||||||
notes_ok = eval_notes.where(eval_notes > scu.NOTES_ABSENCE).dropna()
|
notes_ok = eval_notes.where(eval_notes > scu.NOTES_ABSENCE).dropna()
|
||||||
modimpls_evals_poids = self.res.modimpls_evals_poids[evaluation.moduleimpl_id]
|
modimpls_evals_poids = self.res.modimpls_evals_poids[e.moduleimpl_id]
|
||||||
try:
|
try:
|
||||||
etud_ues_ids = self.res.etud_ues_ids(etud.id)
|
etud_ues_ids = self.res.etud_ues_ids(etud.id)
|
||||||
poids = {
|
poids = {
|
||||||
ue.acronyme: modimpls_evals_poids[ue.id][evaluation.id]
|
ue.acronyme: modimpls_evals_poids[ue.id][e.id]
|
||||||
for ue in self.res.ues
|
for ue in self.res.ues
|
||||||
if (ue.type != UE_SPORT) and (ue.id in etud_ues_ids)
|
if (ue.type != UE_SPORT) and (ue.id in etud_ues_ids)
|
||||||
}
|
}
|
||||||
except KeyError:
|
except KeyError:
|
||||||
poids = collections.defaultdict(lambda: 0.0)
|
poids = collections.defaultdict(lambda: 0.0)
|
||||||
d = {
|
d = {
|
||||||
"id": evaluation.id,
|
"id": e.id,
|
||||||
"coef": (
|
"coef": fmt_note(e.coefficient)
|
||||||
fmt_note(evaluation.coefficient)
|
if e.evaluation_type == scu.EVALUATION_NORMALE
|
||||||
if evaluation.evaluation_type == Evaluation.EVALUATION_NORMALE
|
else None,
|
||||||
else None
|
"date_debut": e.date_debut.isoformat() if e.date_debut else None,
|
||||||
),
|
"date_fin": e.date_fin.isoformat() if e.date_fin else None,
|
||||||
"date_debut": (
|
"description": e.description,
|
||||||
evaluation.date_debut.isoformat() if evaluation.date_debut else None
|
"evaluation_type": e.evaluation_type,
|
||||||
),
|
"note": {
|
||||||
"date_fin": (
|
"value": fmt_note(
|
||||||
evaluation.date_fin.isoformat() if evaluation.date_fin else None
|
eval_notes[etud.id],
|
||||||
),
|
note_max=e.note_max,
|
||||||
"description": evaluation.description,
|
),
|
||||||
"evaluation_type": evaluation.evaluation_type,
|
"min": fmt_note(notes_ok.min(), note_max=e.note_max),
|
||||||
"note": (
|
"max": fmt_note(notes_ok.max(), note_max=e.note_max),
|
||||||
{
|
"moy": fmt_note(notes_ok.mean(), note_max=e.note_max),
|
||||||
"value": fmt_note(
|
},
|
||||||
eval_notes[etud.id],
|
|
||||||
note_max=evaluation.note_max,
|
|
||||||
),
|
|
||||||
"min": fmt_note(notes_ok.min(), note_max=evaluation.note_max),
|
|
||||||
"max": fmt_note(notes_ok.max(), note_max=evaluation.note_max),
|
|
||||||
"moy": fmt_note(notes_ok.mean(), note_max=evaluation.note_max),
|
|
||||||
}
|
|
||||||
if not evaluation.is_blocked()
|
|
||||||
else {}
|
|
||||||
),
|
|
||||||
"poids": poids,
|
"poids": poids,
|
||||||
"url": (
|
"url": url_for(
|
||||||
url_for(
|
"notes.evaluation_listenotes",
|
||||||
"notes.evaluation_listenotes",
|
scodoc_dept=g.scodoc_dept,
|
||||||
scodoc_dept=g.scodoc_dept,
|
evaluation_id=e.id,
|
||||||
evaluation_id=evaluation.id,
|
)
|
||||||
)
|
if has_request_context()
|
||||||
if has_request_context()
|
else "na",
|
||||||
else "na"
|
|
||||||
),
|
|
||||||
# deprecated (supprimer avant #sco9.7)
|
# deprecated (supprimer avant #sco9.7)
|
||||||
"date": (
|
"date": e.date_debut.isoformat() if e.date_debut else None,
|
||||||
evaluation.date_debut.isoformat() if evaluation.date_debut else None
|
"heure_debut": e.date_debut.time().isoformat("minutes")
|
||||||
),
|
if e.date_debut
|
||||||
"heure_debut": (
|
else None,
|
||||||
evaluation.date_debut.time().isoformat("minutes")
|
"heure_fin": e.date_fin.time().isoformat("minutes") if e.date_fin else None,
|
||||||
if evaluation.date_debut
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
"heure_fin": (
|
|
||||||
evaluation.date_fin.time().isoformat("minutes")
|
|
||||||
if evaluation.date_fin
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
return d
|
return d
|
||||||
|
|
||||||
@ -398,18 +343,25 @@ class BulletinBUT:
|
|||||||
"short" : ne descend pas plus bas que les modules.
|
"short" : ne descend pas plus bas que les modules.
|
||||||
|
|
||||||
- Si force_publishing, rempli le bulletin même si bul_hide_xml est vrai
|
- Si force_publishing, rempli le bulletin même si bul_hide_xml est vrai
|
||||||
(bulletins non publiés sur la passerelle).
|
(bulletins non publiés).
|
||||||
"""
|
"""
|
||||||
if version not in scu.BULLETINS_VERSIONS_BUT:
|
if version not in scu.BULLETINS_VERSIONS:
|
||||||
raise ScoValueError("bulletin_etud: version de bulletin demandée invalide")
|
raise ScoValueError("version de bulletin demandée invalide")
|
||||||
res = self.res
|
res = self.res
|
||||||
formsemestre = res.formsemestre
|
formsemestre = res.formsemestre
|
||||||
|
etat_inscription = etud.inscription_etat(formsemestre.id)
|
||||||
|
nb_inscrits = self.res.get_inscriptions_counts()[scu.INSCRIT]
|
||||||
|
published = (not formsemestre.bul_hide_xml) or force_publishing
|
||||||
|
if formsemestre.formation.referentiel_competence is None:
|
||||||
|
etud_ues_ids = {ue.id for ue in res.ues if res.modimpls_in_ue(ue, etud.id)}
|
||||||
|
else:
|
||||||
|
etud_ues_ids = res.etud_ues_ids(etud.id)
|
||||||
|
|
||||||
d = {
|
d = {
|
||||||
"version": "0",
|
"version": "0",
|
||||||
"type": "BUT",
|
"type": "BUT",
|
||||||
"date": datetime.datetime.utcnow().isoformat() + "Z",
|
"date": datetime.datetime.utcnow().isoformat() + "Z",
|
||||||
"publie": not formsemestre.bul_hide_xml,
|
"publie": not formsemestre.bul_hide_xml,
|
||||||
"etat_inscription": etud.inscription_etat(formsemestre.id),
|
|
||||||
"etudiant": etud.to_dict_bul(),
|
"etudiant": etud.to_dict_bul(),
|
||||||
"formation": {
|
"formation": {
|
||||||
"id": formsemestre.formation.id,
|
"id": formsemestre.formation.id,
|
||||||
@ -418,21 +370,15 @@ class BulletinBUT:
|
|||||||
"titre": formsemestre.formation.titre,
|
"titre": formsemestre.formation.titre,
|
||||||
},
|
},
|
||||||
"formsemestre_id": formsemestre.id,
|
"formsemestre_id": formsemestre.id,
|
||||||
|
"etat_inscription": etat_inscription,
|
||||||
"options": sco_preferences.bulletin_option_affichage(
|
"options": sco_preferences.bulletin_option_affichage(
|
||||||
formsemestre, self.prefs
|
formsemestre, self.prefs
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
published = (not formsemestre.bul_hide_xml) or force_publishing
|
if not published:
|
||||||
if not published or d["etat_inscription"] is False:
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
nb_inscrits = self.res.get_inscriptions_counts()[scu.INSCRIT]
|
nbabs, nbabsjust = formsemestre.get_abs_count(etud.id)
|
||||||
if formsemestre.formation.referentiel_competence is None:
|
|
||||||
etud_ues_ids = {ue.id for ue in res.ues if res.modimpls_in_ue(ue, etud.id)}
|
|
||||||
else:
|
|
||||||
etud_ues_ids = res.etud_ues_ids(etud.id)
|
|
||||||
|
|
||||||
nbabsnj, nbabsjust, nbabs = formsemestre.get_abs_count(etud.id)
|
|
||||||
etud_groups = sco_groups.get_etud_formsemestre_groups(
|
etud_groups = sco_groups.get_etud_formsemestre_groups(
|
||||||
etud, formsemestre, only_to_show=True
|
etud, formsemestre, only_to_show=True
|
||||||
)
|
)
|
||||||
@ -447,7 +393,7 @@ class BulletinBUT:
|
|||||||
}
|
}
|
||||||
if self.prefs["bul_show_abs"]:
|
if self.prefs["bul_show_abs"]:
|
||||||
semestre_infos["absences"] = {
|
semestre_infos["absences"] = {
|
||||||
"injustifie": nbabsnj,
|
"injustifie": nbabs - nbabsjust,
|
||||||
"total": nbabs,
|
"total": nbabs,
|
||||||
"metrique": {
|
"metrique": {
|
||||||
"H.": "Heure(s)",
|
"H.": "Heure(s)",
|
||||||
@ -464,7 +410,7 @@ class BulletinBUT:
|
|||||||
semestre_infos.update(
|
semestre_infos.update(
|
||||||
sco_bulletins_json.dict_decision_jury(etud, formsemestre)
|
sco_bulletins_json.dict_decision_jury(etud, formsemestre)
|
||||||
)
|
)
|
||||||
if d["etat_inscription"] == scu.INSCRIT:
|
if etat_inscription == scu.INSCRIT:
|
||||||
# moyenne des moyennes générales du semestre
|
# moyenne des moyennes générales du semestre
|
||||||
semestre_infos["notes"] = {
|
semestre_infos["notes"] = {
|
||||||
"value": fmt_note(res.etud_moy_gen[etud.id]),
|
"value": fmt_note(res.etud_moy_gen[etud.id]),
|
||||||
@ -553,8 +499,10 @@ class BulletinBUT:
|
|||||||
d["etud"]["etat_civil"] = etud.etat_civil
|
d["etud"]["etat_civil"] = etud.etat_civil
|
||||||
d.update(self.res.sem)
|
d.update(self.res.sem)
|
||||||
etud_etat = self.res.get_etud_etat(etud.id)
|
etud_etat = self.res.get_etud_etat(etud.id)
|
||||||
d["filigranne"] = sco_bulletins_pdf.get_filigranne_apc(
|
d["filigranne"] = sco_bulletins_pdf.get_filigranne(
|
||||||
etud_etat, self.prefs, etud.id, res=self.res
|
etud_etat,
|
||||||
|
self.prefs,
|
||||||
|
decision_sem=d["semestre"].get("decision"),
|
||||||
)
|
)
|
||||||
if etud_etat == scu.DEMISSION:
|
if etud_etat == scu.DEMISSION:
|
||||||
d["demission"] = "(Démission)"
|
d["demission"] = "(Démission)"
|
||||||
@ -564,7 +512,7 @@ class BulletinBUT:
|
|||||||
d["demission"] = ""
|
d["demission"] = ""
|
||||||
|
|
||||||
# --- Absences
|
# --- Absences
|
||||||
_, d["nbabsjust"], d["nbabs"] = self.res.formsemestre.get_abs_count(etud.id)
|
d["nbabs"], d["nbabsjust"] = self.res.formsemestre.get_abs_count(etud.id)
|
||||||
|
|
||||||
# --- Decision Jury
|
# --- Decision Jury
|
||||||
infos, _ = sco_bulletins.etud_descr_situation_semestre(
|
infos, _ = sco_bulletins.etud_descr_situation_semestre(
|
||||||
@ -576,11 +524,12 @@ class BulletinBUT:
|
|||||||
show_uevalid=self.prefs["bul_show_uevalid"],
|
show_uevalid=self.prefs["bul_show_uevalid"],
|
||||||
show_mention=self.prefs["bul_show_mention"],
|
show_mention=self.prefs["bul_show_mention"],
|
||||||
)
|
)
|
||||||
|
|
||||||
d.update(infos)
|
d.update(infos)
|
||||||
# --- Rangs
|
# --- Rangs
|
||||||
d["rang_nt"] = (
|
d[
|
||||||
f"{d['semestre']['rang']['value']} / {d['semestre']['rang']['total']}"
|
"rang_nt"
|
||||||
)
|
] = f"{d['semestre']['rang']['value']} / {d['semestre']['rang']['total']}"
|
||||||
d["rang_txt"] = "Rang " + d["rang_nt"]
|
d["rang_txt"] = "Rang " + d["rang_nt"]
|
||||||
|
|
||||||
d.update(sco_bulletins.make_context_dict(self.res.formsemestre, d["etud"]))
|
d.update(sco_bulletins.make_context_dict(self.res.formsemestre, d["etud"]))
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ Ces données sont des objets passés au template.
|
|||||||
- `bul: dict` : le bulletin (dict, même structure que le json publié)
|
- `bul: dict` : le bulletin (dict, même structure que le json publié)
|
||||||
- `cursus: EtudCursusBUT`: infos sur le cursus BUT (niveaux validés etc)
|
- `cursus: EtudCursusBUT`: infos sur le cursus BUT (niveaux validés etc)
|
||||||
- `decision_ues: dict`: `{ acronyme_ue : { 'code' : 'ADM' }}` accès aux décisions
|
- `decision_ues: dict`: `{ acronyme_ue : { 'code' : 'ADM' }}` accès aux décisions
|
||||||
de jury d'UE
|
de jury d'UE
|
||||||
- `ects_total` : nombre d'ECTS validées dans ce cursus
|
- `ects_total` : nombre d'ECTS validées dans ce cursus
|
||||||
- `ue_validation_by_niveau : dict` : les validations d'UE de chaque niveau du cursus
|
- `ue_validation_by_niveau : dict` : les validations d'UE de chaque niveau du cursus
|
||||||
"""
|
"""
|
||||||
@ -39,7 +39,6 @@ from app.scodoc.codes_cursus import UE_STANDARD
|
|||||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
|
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
|
||||||
from app.scodoc.sco_logos import find_logo
|
from app.scodoc.sco_logos import find_logo
|
||||||
from app.scodoc.sco_permissions import Permission
|
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
|
import app.scodoc.sco_utils as scu
|
||||||
from app.views import notes_bp as bp
|
from app.views import notes_bp as bp
|
||||||
from app.views import ScoData
|
from app.views import ScoData
|
||||||
@ -66,48 +65,11 @@ def bulletin_but(formsemestre_id: int, etudid: int = None, fmt="html"):
|
|||||||
)
|
)
|
||||||
if not formsemestre.formation.is_apc():
|
if not formsemestre.formation.is_apc():
|
||||||
raise ScoValueError("formation non BUT")
|
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)
|
|
||||||
return scu.sendPDFFile(bul_pdf, filename + ".pdf")
|
|
||||||
|
|
||||||
return render_template(
|
|
||||||
"but/bulletin_court_page.j2",
|
|
||||||
datetime=datetime,
|
|
||||||
sco=ScoData(formsemestre=formsemestre, etud=etud),
|
|
||||||
time=time,
|
|
||||||
version="butcourt",
|
|
||||||
**args,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def bulletin_but_court_pdf_frag(
|
|
||||||
etud: Identite, formsemestre: FormSemestre, stand_alone=False
|
|
||||||
) -> bytes:
|
|
||||||
"""Le code PDF d'un bulletin BUT court, à intégrer dans un document
|
|
||||||
(pour les classeurs de tous les bulletins)
|
|
||||||
"""
|
|
||||||
args = _build_bulletin_but_infos(etud, formsemestre)
|
|
||||||
return bulletin_but_court_pdf.make_bulletin_but_court_pdf(
|
|
||||||
args, stand_alone=stand_alone
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _build_bulletin_but_infos(
|
|
||||||
etud: Identite, formsemestre: FormSemestre, fmt="pdf"
|
|
||||||
) -> dict:
|
|
||||||
"""Réuni toutes les information pour le contenu d'un bulletin BUT court.
|
|
||||||
On indique le format ("html" ou "pdf") car il y a moins d'infos en HTML.
|
|
||||||
"""
|
|
||||||
bulletins_sem = BulletinBUT(formsemestre)
|
bulletins_sem = BulletinBUT(formsemestre)
|
||||||
if fmt == "pdf":
|
if fmt == "pdf":
|
||||||
bul: dict = bulletins_sem.bulletin_etud_complet(etud)
|
bul: dict = bulletins_sem.bulletin_etud_complet(etud)
|
||||||
filigranne = bul["filigranne"]
|
|
||||||
else: # la même chose avec un peu moins d'infos
|
else: # la même chose avec un peu moins d'infos
|
||||||
bul: dict = bulletins_sem.bulletin_etud(etud, force_publishing=True)
|
bul: dict = bulletins_sem.bulletin_etud(etud, force_publishing=True)
|
||||||
filigranne = ""
|
|
||||||
decision_ues = (
|
decision_ues = (
|
||||||
{x["acronyme"]: x for x in bul["semestre"]["decision_ue"]}
|
{x["acronyme"]: x for x in bul["semestre"]["decision_ue"]}
|
||||||
if "semestre" in bul and "decision_ue" in bul["semestre"]
|
if "semestre" in bul and "decision_ue" in bul["semestre"]
|
||||||
@ -119,14 +81,6 @@ def _build_bulletin_but_infos(
|
|||||||
refcomp = formsemestre.formation.referentiel_competence
|
refcomp = formsemestre.formation.referentiel_competence
|
||||||
if refcomp is None:
|
if refcomp is None:
|
||||||
raise ScoNoReferentielCompetences(formation=formsemestre.formation)
|
raise ScoNoReferentielCompetences(formation=formsemestre.formation)
|
||||||
|
|
||||||
warn_html = cursus_but.formsemestre_warning_apc_setup(
|
|
||||||
formsemestre, bulletins_sem.res
|
|
||||||
)
|
|
||||||
if warn_html:
|
|
||||||
raise ScoValueError(
|
|
||||||
"<b>Formation mal configurée pour le BUT</b>" + warn_html, safe=True
|
|
||||||
)
|
|
||||||
ue_validation_by_niveau = validations_view.get_ue_validation_by_niveau(
|
ue_validation_by_niveau = validations_view.get_ue_validation_by_niveau(
|
||||||
refcomp, etud
|
refcomp, etud
|
||||||
)
|
)
|
||||||
@ -141,7 +95,6 @@ def _build_bulletin_but_infos(
|
|||||||
"decision_ues": decision_ues,
|
"decision_ues": decision_ues,
|
||||||
"ects_total": ects_total,
|
"ects_total": ects_total,
|
||||||
"etud": etud,
|
"etud": etud,
|
||||||
"filigranne": filigranne,
|
|
||||||
"formsemestre": formsemestre,
|
"formsemestre": formsemestre,
|
||||||
"logo": logo,
|
"logo": logo,
|
||||||
"prefs": bulletins_sem.prefs,
|
"prefs": bulletins_sem.prefs,
|
||||||
@ -153,5 +106,16 @@ def _build_bulletin_but_infos(
|
|||||||
if ue.type == UE_STANDARD and ue.acronyme in ue_acronyms
|
if ue.type == UE_STANDARD and ue.acronyme in ue_acronyms
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
add_dut120_infos(formsemestre, etud.id, args)
|
if fmt == "pdf":
|
||||||
return args
|
filename = scu.bul_filename(formsemestre, etud, prefix="bul-but")
|
||||||
|
bul_pdf = bulletin_but_court_pdf.make_bulletin_but_court_pdf(**args)
|
||||||
|
return scu.sendPDFFile(bul_pdf, filename + ".pdf")
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"but/bulletin_court_page.j2",
|
||||||
|
datetime=datetime,
|
||||||
|
sco=ScoData(formsemestre=formsemestre, etud=etud),
|
||||||
|
time=time,
|
||||||
|
version="butcourt",
|
||||||
|
**args,
|
||||||
|
)
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
"""Génération bulletin BUT PDF synthétique en une page
|
"""Génération bulletin BUT PDF synthétique en une page
|
||||||
|
|
||||||
On génère du PDF avec reportLab en utilisant les classes
|
On génère du PDF avec reportLab en utilisant les classes
|
||||||
ScoDoc BulletinGenerator et GenTable.
|
ScoDoc BulletinGenerator et GenTable.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -31,37 +31,28 @@ from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
|
|||||||
from app.scodoc.sco_logos import Logo
|
from app.scodoc.sco_logos import Logo
|
||||||
from app.scodoc.sco_pdf import PDFLOCK, SU
|
from app.scodoc.sco_pdf import PDFLOCK, SU
|
||||||
from app.scodoc.sco_preferences import SemPreferences
|
from app.scodoc.sco_preferences import SemPreferences
|
||||||
from app.scodoc import sco_utils as scu
|
|
||||||
|
|
||||||
|
|
||||||
def make_bulletin_but_court_pdf(
|
def make_bulletin_but_court_pdf(
|
||||||
args: dict,
|
bul: dict = None,
|
||||||
stand_alone: bool = True,
|
cursus: cursus_but.EtudCursusBUT = None,
|
||||||
|
decision_ues: dict = None,
|
||||||
|
ects_total: float = 0.0,
|
||||||
|
etud: Identite = None,
|
||||||
|
formsemestre: FormSemestre = None,
|
||||||
|
logo: Logo = None,
|
||||||
|
prefs: SemPreferences = None,
|
||||||
|
title: str = "",
|
||||||
|
ue_validation_by_niveau: dict[tuple[int, str], ScolarFormSemestreValidation] = None,
|
||||||
|
ues_acronyms: list[str] = None,
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
"""génère le bulletin court BUT en pdf.
|
"génère le bulletin court BUT en pdf"
|
||||||
Si stand_alone, génère un doc pdf complet (une page ici),
|
|
||||||
sinon un morceau (fragment) à intégrer dans un autre document.
|
|
||||||
|
|
||||||
args donne toutes les infos du contenu du bulletin:
|
|
||||||
bul: dict = None,
|
|
||||||
cursus: cursus_but.EtudCursusBUT = None,
|
|
||||||
decision_ues: dict = None,
|
|
||||||
ects_total: float = 0.0,
|
|
||||||
etud: Identite = None,
|
|
||||||
formsemestre: FormSemestre = None,
|
|
||||||
filigranne=""
|
|
||||||
logo: Logo = None,
|
|
||||||
prefs: SemPreferences = None,
|
|
||||||
title: str = "",
|
|
||||||
ue_validation_by_niveau: dict[tuple[int, str], ScolarFormSemestreValidation] = None,
|
|
||||||
ues_acronyms: list[str] = None,
|
|
||||||
"""
|
|
||||||
# A priori ce verrou n'est plus nécessaire avec Flask (multi-process)
|
# A priori ce verrou n'est plus nécessaire avec Flask (multi-process)
|
||||||
# mais...
|
# mais...
|
||||||
try:
|
try:
|
||||||
PDFLOCK.acquire()
|
PDFLOCK.acquire()
|
||||||
bul_generator = BulletinGeneratorBUTCourt(**args)
|
bul_generator = BulletinGeneratorBUTCourt(**locals())
|
||||||
bul_pdf = bul_generator.generate(fmt="pdf", stand_alone=stand_alone)
|
bul_pdf = bul_generator.generate(fmt="pdf")
|
||||||
finally:
|
finally:
|
||||||
PDFLOCK.release()
|
PDFLOCK.release()
|
||||||
return bul_pdf
|
return bul_pdf
|
||||||
@ -88,7 +79,6 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
|||||||
decision_ues: dict = None,
|
decision_ues: dict = None,
|
||||||
ects_total: float = 0.0,
|
ects_total: float = 0.0,
|
||||||
etud: Identite = None,
|
etud: Identite = None,
|
||||||
filigranne="",
|
|
||||||
formsemestre: FormSemestre = None,
|
formsemestre: FormSemestre = None,
|
||||||
logo: Logo = None,
|
logo: Logo = None,
|
||||||
prefs: SemPreferences = None,
|
prefs: SemPreferences = None,
|
||||||
@ -97,10 +87,8 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
|||||||
tuple[int, str], ScolarFormSemestreValidation
|
tuple[int, str], ScolarFormSemestreValidation
|
||||||
] = None,
|
] = None,
|
||||||
ues_acronyms: list[str] = None,
|
ues_acronyms: list[str] = None,
|
||||||
diplome_dut120: bool = False,
|
|
||||||
diplome_dut120_descr: str = "",
|
|
||||||
):
|
):
|
||||||
super().__init__(bul, authuser=current_user, filigranne=filigranne)
|
super().__init__(bul, authuser=current_user)
|
||||||
self.bul = bul
|
self.bul = bul
|
||||||
self.cursus = cursus
|
self.cursus = cursus
|
||||||
self.decision_ues = decision_ues
|
self.decision_ues = decision_ues
|
||||||
@ -112,8 +100,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
|||||||
self.title = title
|
self.title = title
|
||||||
self.ue_validation_by_niveau = ue_validation_by_niveau
|
self.ue_validation_by_niveau = ue_validation_by_niveau
|
||||||
self.ues_acronyms = ues_acronyms # sans UEs sport
|
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)
|
self.nb_ues = len(self.ues_acronyms)
|
||||||
# Styles PDF
|
# Styles PDF
|
||||||
self.style_base = styles.ParagraphStyle("style_base")
|
self.style_base = styles.ParagraphStyle("style_base")
|
||||||
@ -198,7 +185,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
|||||||
"""Génère la partie "titre" du bulletin de notes.
|
"""Génère la partie "titre" du bulletin de notes.
|
||||||
Renvoie une liste d'objets platypus
|
Renvoie une liste d'objets platypus
|
||||||
"""
|
"""
|
||||||
# comme les bulletins standards, mais avec notre préférence
|
# comme les bulletins standard, mais avec notre préférence
|
||||||
return super().bul_title_pdf(preference_field=preference_field)
|
return super().bul_title_pdf(preference_field=preference_field)
|
||||||
|
|
||||||
def bul_part_below(self, fmt="pdf") -> list:
|
def bul_part_below(self, fmt="pdf") -> list:
|
||||||
@ -246,17 +233,13 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
|||||||
)
|
)
|
||||||
table_abs_ues.hAlign = "RIGHT"
|
table_abs_ues.hAlign = "RIGHT"
|
||||||
# Ligne (en bas) avec table cursus et boite jury
|
# 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_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),
|
colWidths=(self.width_page_avail - 84 * mm, 84 * mm),
|
||||||
style=style_table_2cols,
|
style=style_table_2cols,
|
||||||
)
|
)
|
||||||
@ -351,11 +334,9 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
|||||||
for mod in self.bul[mod_type]:
|
for mod in self.bul[mod_type]:
|
||||||
row = [mod, bul[mod_type][mod]["titre"]]
|
row = [mod, bul[mod_type][mod]["titre"]]
|
||||||
row += [
|
row += [
|
||||||
(
|
bul["ues"][ue][mod_type][mod]["moyenne"]
|
||||||
bul["ues"][ue][mod_type][mod]["moyenne"]
|
if mod in bul["ues"][ue][mod_type]
|
||||||
if mod in bul["ues"][ue][mod_type]
|
else ""
|
||||||
else ""
|
|
||||||
)
|
|
||||||
for ue in self.ues_acronyms
|
for ue in self.ues_acronyms
|
||||||
]
|
]
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
@ -416,8 +397,6 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
|||||||
|
|
||||||
def boite_identite(self) -> list:
|
def boite_identite(self) -> list:
|
||||||
"Les informations sur l'identité et l'inscription de l'étudiant"
|
"Les informations sur l'identité et l'inscription de l'étudiant"
|
||||||
parcour = self.formsemestre.etuds_inscriptions[self.etud.id].parcour
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
Paragraph(
|
Paragraph(
|
||||||
SU(f"""{self.etud.nomprenom}"""),
|
SU(f"""{self.etud.nomprenom}"""),
|
||||||
@ -428,8 +407,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
|||||||
f"""
|
f"""
|
||||||
<b>{self.bul["demission"]}</b><br/>
|
<b>{self.bul["demission"]}</b><br/>
|
||||||
Formation: {self.formsemestre.titre_num()}<br/>
|
Formation: {self.formsemestre.titre_num()}<br/>
|
||||||
{'Parcours ' + parcour.code + '<br/>' if parcour else ''}
|
Année scolaire: {self.formsemestre.annee_scolaire_str()}<br/>
|
||||||
Année universitaire: {self.formsemestre.annee_scolaire_str()}<br/>
|
|
||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
style=self.style_base,
|
style=self.style_base,
|
||||||
@ -530,17 +508,14 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
|||||||
def boite_decisions_jury(self):
|
def boite_decisions_jury(self):
|
||||||
"""La boite en bas à droite avec jury"""
|
"""La boite en bas à droite avec jury"""
|
||||||
txt = f"""ECTS acquis en BUT : <b>{self.ects_total:g}</b><br/>"""
|
txt = f"""ECTS acquis en BUT : <b>{self.ects_total:g}</b><br/>"""
|
||||||
|
|
||||||
if self.bul["semestre"].get("decision_annee", None):
|
if self.bul["semestre"].get("decision_annee", None):
|
||||||
txt += f"""
|
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("%d/%m/%Y")
|
||||||
|
}, année BUT{self.bul["semestre"]["decision_annee"]["ordre"]}
|
||||||
<b>{self.bul["semestre"]["decision_annee"]["code"]}</b>.
|
<b>{self.bul["semestre"]["decision_annee"]["code"]}</b>.
|
||||||
<br/>
|
<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):
|
if self.bul["semestre"].get("autorisation_inscription", None):
|
||||||
txt += (
|
txt += (
|
||||||
"<br/>Autorisé à s'inscrire en <b>"
|
"<br/>Autorisé à s'inscrire en <b>"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -9,7 +9,7 @@
|
|||||||
La génération du bulletin PDF suit le chemin suivant:
|
La génération du bulletin PDF suit le chemin suivant:
|
||||||
|
|
||||||
- vue formsemestre_bulletinetud -> sco_bulletins.formsemestre_bulletinetud
|
- vue formsemestre_bulletinetud -> sco_bulletins.formsemestre_bulletinetud
|
||||||
|
|
||||||
bul_dict = bulletin_but.BulletinBUT(formsemestre).bulletin_etud_complet(etud)
|
bul_dict = bulletin_but.BulletinBUT(formsemestre).bulletin_etud_complet(etud)
|
||||||
|
|
||||||
- sco_bulletins_generator.make_formsemestre_bulletin_etud()
|
- sco_bulletins_generator.make_formsemestre_bulletin_etud()
|
||||||
@ -24,7 +24,7 @@ from reportlab.lib.colors import blue
|
|||||||
from reportlab.lib.units import cm, mm
|
from reportlab.lib.units import cm, mm
|
||||||
from reportlab.platypus import Paragraph, Spacer
|
from reportlab.platypus import Paragraph, Spacer
|
||||||
|
|
||||||
from app.models import Evaluation, ScoDocSiteConfig
|
from app.models import ScoDocSiteConfig
|
||||||
from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
|
from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
|
||||||
from app.scodoc import gen_tables
|
from app.scodoc import gen_tables
|
||||||
from app.scodoc.codes_cursus import UE_SPORT
|
from app.scodoc.codes_cursus import UE_SPORT
|
||||||
@ -73,7 +73,6 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
|||||||
html_class="notes_bulletin",
|
html_class="notes_bulletin",
|
||||||
html_class_ignore_default=True,
|
html_class_ignore_default=True,
|
||||||
html_with_td_classes=True,
|
html_with_td_classes=True,
|
||||||
table_id="bul-table",
|
|
||||||
)
|
)
|
||||||
table_objects = table.gen(fmt=fmt)
|
table_objects = table.gen(fmt=fmt)
|
||||||
objects += table_objects
|
objects += table_objects
|
||||||
@ -270,7 +269,7 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
|||||||
date_capitalisation = ue.get("date_capitalisation")
|
date_capitalisation = ue.get("date_capitalisation")
|
||||||
if date_capitalisation:
|
if date_capitalisation:
|
||||||
fields_bmr.append(
|
fields_bmr.append(
|
||||||
f"""Capitalisée le {date_capitalisation.strftime(scu.DATE_FMT)}"""
|
f"""Capitalisée le {date_capitalisation.strftime("%d/%m/%Y")}"""
|
||||||
)
|
)
|
||||||
t = {
|
t = {
|
||||||
"titre": " - ".join(fields_bmr),
|
"titre": " - ".join(fields_bmr),
|
||||||
@ -423,22 +422,16 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
|||||||
def evaluations_rows(self, rows, evaluations: list[dict], ue_acros=()):
|
def evaluations_rows(self, rows, evaluations: list[dict], ue_acros=()):
|
||||||
"lignes des évaluations"
|
"lignes des évaluations"
|
||||||
for e in evaluations:
|
for e in evaluations:
|
||||||
coef = (
|
coef = e["coef"] if e["evaluation_type"] == scu.EVALUATION_NORMALE else "*"
|
||||||
e["coef"]
|
|
||||||
if e["evaluation_type"] == Evaluation.EVALUATION_NORMALE
|
|
||||||
else "*"
|
|
||||||
)
|
|
||||||
note_value = e["note"].get("value", "")
|
|
||||||
t = {
|
t = {
|
||||||
"titre": f"{e['description'] or ''}",
|
"titre": f"{e['description'] or ''}",
|
||||||
"moyenne": note_value,
|
"moyenne": e["note"]["value"],
|
||||||
"_moyenne_pdf": Paragraph(f"""<para align=right>{note_value}</para>"""),
|
"_moyenne_pdf": Paragraph(
|
||||||
|
f"""<para align=right>{e["note"]["value"]}</para>"""
|
||||||
|
),
|
||||||
"coef": coef,
|
"coef": coef,
|
||||||
"_coef_pdf": Paragraph(
|
"_coef_pdf": Paragraph(
|
||||||
f"""<para align=right fontSize={self.small_fontsize}><i>{
|
f"<para align=right fontSize={self.small_fontsize}><i>{coef}</i></para>"
|
||||||
coef if e["evaluation_type"] != Evaluation.EVALUATION_BONUS
|
|
||||||
else "bonus"
|
|
||||||
}</i></para>"""
|
|
||||||
),
|
),
|
||||||
"_pdf_style": [
|
"_pdf_style": [
|
||||||
(
|
(
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -38,11 +38,14 @@ import datetime
|
|||||||
from xml.etree import ElementTree
|
from xml.etree import ElementTree
|
||||||
from xml.etree.ElementTree import Element
|
from xml.etree.ElementTree import Element
|
||||||
|
|
||||||
from app import db, log
|
from app import log
|
||||||
from app.but import bulletin_but
|
from app.but import bulletin_but
|
||||||
from app.models import BulAppreciations, FormSemestre, Identite, UniteEns
|
from app.models import BulAppreciations, FormSemestre, Identite
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
|
import app.scodoc.notesdb as ndb
|
||||||
from app.scodoc import codes_cursus
|
from app.scodoc import codes_cursus
|
||||||
|
from app.scodoc import sco_edit_ue
|
||||||
|
from app.scodoc import sco_etud
|
||||||
from app.scodoc import sco_photos
|
from app.scodoc import sco_photos
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
from app.scodoc import sco_xml
|
from app.scodoc import sco_xml
|
||||||
@ -199,12 +202,12 @@ def bulletin_but_xml_compat(
|
|||||||
if e.visibulletin or version == "long":
|
if e.visibulletin or version == "long":
|
||||||
x_eval = Element(
|
x_eval = Element(
|
||||||
"evaluation",
|
"evaluation",
|
||||||
date_debut=(
|
date_debut=e.date_debut.isoformat()
|
||||||
e.date_debut.isoformat() if e.date_debut else ""
|
if e.date_debut
|
||||||
),
|
else "",
|
||||||
date_fin=(
|
date_fin=e.date_fin.isoformat()
|
||||||
e.date_fin.isoformat() if e.date_fin else ""
|
if e.date_debut
|
||||||
),
|
else "",
|
||||||
coefficient=str(e.coefficient),
|
coefficient=str(e.coefficient),
|
||||||
# pas les poids en XML compat
|
# pas les poids en XML compat
|
||||||
evaluation_type=str(e.evaluation_type),
|
evaluation_type=str(e.evaluation_type),
|
||||||
@ -212,9 +215,9 @@ def bulletin_but_xml_compat(
|
|||||||
# notes envoyées sur 20, ceci juste pour garder trace:
|
# notes envoyées sur 20, ceci juste pour garder trace:
|
||||||
note_max_origin=str(e.note_max),
|
note_max_origin=str(e.note_max),
|
||||||
# --- deprecated
|
# --- deprecated
|
||||||
jour=(
|
jour=e.date_debut.isoformat()
|
||||||
e.date_debut.isoformat() if e.date_debut else ""
|
if e.date_debut
|
||||||
),
|
else "",
|
||||||
heure_debut=e.heure_debut(),
|
heure_debut=e.heure_debut(),
|
||||||
heure_fin=e.heure_fin(),
|
heure_fin=e.heure_fin(),
|
||||||
)
|
)
|
||||||
@ -241,7 +244,7 @@ def bulletin_but_xml_compat(
|
|||||||
|
|
||||||
# --- Absences
|
# --- Absences
|
||||||
if sco_preferences.get_preference("bul_show_abs", formsemestre_id):
|
if sco_preferences.get_preference("bul_show_abs", formsemestre_id):
|
||||||
_, nbabsjust, nbabs = formsemestre.get_abs_count(etud.id)
|
nbabs, nbabsjust = formsemestre.get_abs_count(etud.id)
|
||||||
doc.append(Element("absences", nbabs=str(nbabs), nbabsjust=str(nbabsjust)))
|
doc.append(Element("absences", nbabs=str(nbabs), nbabsjust=str(nbabsjust)))
|
||||||
|
|
||||||
# -------- LA SUITE EST COPIEE SANS MODIF DE sco_bulletins_xml.py ---------
|
# -------- LA SUITE EST COPIEE SANS MODIF DE sco_bulletins_xml.py ---------
|
||||||
@ -291,18 +294,17 @@ def bulletin_but_xml_compat(
|
|||||||
"decisions_ue"
|
"decisions_ue"
|
||||||
]: # and sco_preferences.get_preference( 'bul_show_uevalid', formsemestre_id): always publish (car utile pour export Apogee)
|
]: # and sco_preferences.get_preference( 'bul_show_uevalid', formsemestre_id): always publish (car utile pour export Apogee)
|
||||||
for ue_id in decision["decisions_ue"].keys():
|
for ue_id in decision["decisions_ue"].keys():
|
||||||
ue = db.session.get(UniteEns, ue_id)
|
ue = sco_edit_ue.ue_list({"ue_id": ue_id})[0]
|
||||||
if ue:
|
doc.append(
|
||||||
doc.append(
|
Element(
|
||||||
Element(
|
"decision_ue",
|
||||||
"decision_ue",
|
ue_id=str(ue["ue_id"]),
|
||||||
ue_id=str(ue.id),
|
numero=quote_xml_attr(ue["numero"]),
|
||||||
numero=quote_xml_attr(ue.numero),
|
acronyme=quote_xml_attr(ue["acronyme"]),
|
||||||
acronyme=quote_xml_attr(ue.acronyme),
|
titre=quote_xml_attr(ue["titre"]),
|
||||||
titre=quote_xml_attr(ue.titre or ""),
|
code=decision["decisions_ue"][ue_id]["code"],
|
||||||
code=decision["decisions_ue"][ue_id]["code"],
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
for aut in decision["autorisations"]:
|
for aut in decision["autorisations"]:
|
||||||
doc.append(
|
doc.append(
|
||||||
|
@ -1,92 +0,0 @@
|
|||||||
##############################################################################
|
|
||||||
# ScoDoc
|
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
|
||||||
# See LICENSE
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
"""Code expérimental: si deux référentiel sont presques identiques
|
|
||||||
(mêmes compétences, niveaux, parcours)
|
|
||||||
essaie de changer une formation de référentiel.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from app import clear_scodoc_cache, db
|
|
||||||
|
|
||||||
from app.models import (
|
|
||||||
ApcParcours,
|
|
||||||
ApcReferentielCompetences,
|
|
||||||
ApcValidationRCUE,
|
|
||||||
Formation,
|
|
||||||
FormSemestreInscription,
|
|
||||||
UniteEns,
|
|
||||||
)
|
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
|
||||||
|
|
||||||
|
|
||||||
def formation_change_referentiel(
|
|
||||||
formation: Formation, new_ref: ApcReferentielCompetences
|
|
||||||
):
|
|
||||||
"""Try to change ref."""
|
|
||||||
if not formation.referentiel_competence:
|
|
||||||
raise ScoValueError("formation non associée à un référentiel")
|
|
||||||
if not isinstance(new_ref, ApcReferentielCompetences):
|
|
||||||
raise ScoValueError("nouveau référentiel invalide")
|
|
||||||
|
|
||||||
r = formation.referentiel_competence.map_to_other_referentiel(new_ref)
|
|
||||||
if isinstance(r, str):
|
|
||||||
raise ScoValueError(f"référentiels incompatibles: {r}")
|
|
||||||
parcours_map, competences_map, niveaux_map = r
|
|
||||||
|
|
||||||
formation.referentiel_competence = new_ref
|
|
||||||
db.session.add(formation)
|
|
||||||
# UEs - Niveaux et UEs - parcours
|
|
||||||
for ue in formation.ues:
|
|
||||||
if ue.niveau_competence:
|
|
||||||
ue.niveau_competence_id = niveaux_map[ue.niveau_competence_id]
|
|
||||||
db.session.add(ue)
|
|
||||||
if ue.parcours:
|
|
||||||
new_list = [ApcParcours.query.get(parcours_map[p.id]) for p in ue.parcours]
|
|
||||||
ue.parcours.clear()
|
|
||||||
ue.parcours.extend(new_list)
|
|
||||||
db.session.add(ue)
|
|
||||||
# Modules / parcours et app_critiques
|
|
||||||
for module in formation.modules:
|
|
||||||
if module.parcours:
|
|
||||||
new_list = [
|
|
||||||
ApcParcours.query.get(parcours_map[p.id]) for p in module.parcours
|
|
||||||
]
|
|
||||||
module.parcours.clear()
|
|
||||||
module.parcours.extend(new_list)
|
|
||||||
db.session.add(module)
|
|
||||||
if module.app_critiques: # efface les apprentissages critiques
|
|
||||||
module.app_critiques.clear()
|
|
||||||
db.session.add(module)
|
|
||||||
# ApcValidationRCUE
|
|
||||||
for valid_rcue in ApcValidationRCUE.query.join(
|
|
||||||
UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id
|
|
||||||
).filter_by(formation_id=formation.id):
|
|
||||||
if valid_rcue.parcour:
|
|
||||||
valid_rcue.parcour_id = parcours_map[valid_rcue.parcour.id]
|
|
||||||
db.session.add(valid_rcue)
|
|
||||||
for valid_rcue in ApcValidationRCUE.query.join(
|
|
||||||
UniteEns, UniteEns.id == ApcValidationRCUE.ue2_id
|
|
||||||
).filter_by(formation_id=formation.id):
|
|
||||||
if valid_rcue.parcour:
|
|
||||||
valid_rcue.parcour_id = parcours_map[valid_rcue.parcour.id]
|
|
||||||
db.session.add(valid_rcue)
|
|
||||||
# FormSemestre / parcours_formsemestre
|
|
||||||
for formsemestre in formation.formsemestres:
|
|
||||||
new_list = [
|
|
||||||
ApcParcours.query.get(parcours_map[p.id]) for p in formsemestre.parcours
|
|
||||||
]
|
|
||||||
formsemestre.parcours.clear()
|
|
||||||
formsemestre.parcours.extend(new_list)
|
|
||||||
db.session.add(formsemestre)
|
|
||||||
# FormSemestreInscription.parcour_id
|
|
||||||
for inscr in FormSemestreInscription.query.filter_by(
|
|
||||||
formsemestre_id=formsemestre.id
|
|
||||||
).filter(FormSemestreInscription.parcour_id != None):
|
|
||||||
if inscr.parcour_id is not None:
|
|
||||||
inscr.parcour_id = parcours_map[inscr.parcour_id]
|
|
||||||
#
|
|
||||||
db.session.commit()
|
|
||||||
clear_scodoc_cache()
|
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -14,39 +14,45 @@ Classe raccordant avec ScoDoc 7:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import collections
|
import collections
|
||||||
from collections.abc import Iterable
|
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
from flask import g, url_for
|
from flask import g, url_for
|
||||||
from flask_sqlalchemy.query import Query
|
|
||||||
|
|
||||||
from app import db, log
|
from app import db, log
|
||||||
from app.comp.res_but import ResultatsSemestreBUT
|
from app.comp.res_but import ResultatsSemestreBUT
|
||||||
from app.comp.res_compat import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
|
|
||||||
from app.models.but_refcomp import (
|
from app.models.but_refcomp import (
|
||||||
|
ApcAnneeParcours,
|
||||||
ApcCompetence,
|
ApcCompetence,
|
||||||
ApcNiveau,
|
ApcNiveau,
|
||||||
ApcParcours,
|
ApcParcours,
|
||||||
|
ApcParcoursNiveauCompetence,
|
||||||
ApcReferentielCompetences,
|
ApcReferentielCompetences,
|
||||||
)
|
)
|
||||||
from app.models.ues import UEParcours
|
from app.models import Scolog, ScolarAutorisationInscription
|
||||||
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
from app.models.but_validations import (
|
||||||
|
ApcValidationAnnee,
|
||||||
|
ApcValidationRCUE,
|
||||||
|
)
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
from app.models.formations import Formation
|
from app.models.formations import Formation
|
||||||
from app.models.formsemestre import FormSemestre
|
from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
||||||
from app.models.ues import UniteEns
|
from app.models.ues import UniteEns
|
||||||
from app.models.validations import ScolarFormSemestreValidation
|
from app.models.validations import ScolarFormSemestreValidation
|
||||||
from app.scodoc import codes_cursus as sco_codes
|
from app.scodoc import codes_cursus as sco_codes
|
||||||
from app.scodoc.codes_cursus import code_ue_validant, CODES_UE_VALIDES, UE_STANDARD
|
from app.scodoc.codes_cursus import code_ue_validant, CODES_UE_VALIDES, UE_STANDARD
|
||||||
|
|
||||||
|
from app.scodoc import sco_utils as scu
|
||||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
|
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
|
||||||
|
|
||||||
from app.scodoc import sco_cursus_dut
|
from app.scodoc import sco_cursus_dut
|
||||||
|
|
||||||
|
|
||||||
class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
|
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)
|
super().__init__(etud, formsemestre_id, res)
|
||||||
# Ajustements pour le BUT
|
# Ajustements pour le BUT
|
||||||
self.can_compensate_with_prev = False # jamais de compensation à la mode DUT
|
self.can_compensate_with_prev = False # jamais de compensation à la mode DUT
|
||||||
@ -56,22 +62,8 @@ class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def parcours_validated(self):
|
def parcours_validated(self):
|
||||||
"True si le parcours (ici diplôme BUT) est validé"
|
"True si le parcours est validé"
|
||||||
return but_parcours_validated(
|
return False # XXX TODO
|
||||||
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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EtudCursusBUT:
|
class EtudCursusBUT:
|
||||||
@ -127,15 +119,8 @@ class EtudCursusBUT:
|
|||||||
|
|
||||||
self.validation_par_competence_et_annee = {}
|
self.validation_par_competence_et_annee = {}
|
||||||
"""{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
|
"""{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
|
||||||
validation_rcue: ApcValidationRCUE
|
|
||||||
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
||||||
niveau = validation_rcue.niveau()
|
niveau = validation_rcue.niveau()
|
||||||
if niveau is None:
|
|
||||||
raise ScoValueError(
|
|
||||||
"""UE d'un RCUE non associée à un niveau de compétence.
|
|
||||||
Vérifiez la formation et les associations de ses UEs.
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
if not niveau.competence.id in self.validation_par_competence_et_annee:
|
if not niveau.competence.id in self.validation_par_competence_et_annee:
|
||||||
self.validation_par_competence_et_annee[niveau.competence.id] = {}
|
self.validation_par_competence_et_annee[niveau.competence.id] = {}
|
||||||
previous_validation = self.validation_par_competence_et_annee.get(
|
previous_validation = self.validation_par_competence_et_annee.get(
|
||||||
@ -209,10 +194,6 @@ class EtudCursusBUT:
|
|||||||
# slow, utile pour affichage fiche
|
# slow, utile pour affichage fiche
|
||||||
return annee in [n.annee for n in self.competences[competence_id].niveaux]
|
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]]:
|
def load_validation_by_niveau(self) -> dict[int, list[ApcValidationRCUE]]:
|
||||||
"""Cherche les validations de jury enregistrées pour chaque niveau
|
"""Cherche les validations de jury enregistrées pour chaque niveau
|
||||||
Résultat: { niveau_id : [ ApcValidationRCUE ] }
|
Résultat: { niveau_id : [ ApcValidationRCUE ] }
|
||||||
@ -307,136 +288,104 @@ class FormSemestreCursusBUT:
|
|||||||
)
|
)
|
||||||
return niveaux_by_annee
|
return niveaux_by_annee
|
||||||
|
|
||||||
# def get_etud_validation_par_competence_et_annee(self, etud: Identite):
|
def get_etud_validation_par_competence_et_annee(self, etud: Identite):
|
||||||
# """{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
|
"""{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
|
||||||
# validation_par_competence_et_annee = {}
|
validation_par_competence_et_annee = {}
|
||||||
# for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
||||||
# # On s'assurer qu'elle concerne notre cursus !
|
# On s'assurer qu'elle concerne notre cursus !
|
||||||
# ue = validation_rcue.ue2
|
ue = validation_rcue.ue2
|
||||||
# if ue.id not in self.ue_ids:
|
if ue.id not in self.ue_ids:
|
||||||
# if (
|
if (
|
||||||
# ue.formation.referentiel_competences_id
|
ue.formation.referentiel_competences_id
|
||||||
# == self.referentiel_competences_id
|
== self.referentiel_competences_id
|
||||||
# ):
|
):
|
||||||
# self.ue_ids = ue.id
|
self.ue_ids = ue.id
|
||||||
# else:
|
else:
|
||||||
# continue # skip this validation
|
continue # skip this validation
|
||||||
# niveau = validation_rcue.niveau()
|
niveau = validation_rcue.niveau()
|
||||||
# if not niveau.competence.id in validation_par_competence_et_annee:
|
if not niveau.competence.id in validation_par_competence_et_annee:
|
||||||
# validation_par_competence_et_annee[niveau.competence.id] = {}
|
validation_par_competence_et_annee[niveau.competence.id] = {}
|
||||||
# previous_validation = validation_par_competence_et_annee.get(
|
previous_validation = validation_par_competence_et_annee.get(
|
||||||
# niveau.competence.id
|
niveau.competence.id
|
||||||
# ).get(validation_rcue.annee())
|
).get(validation_rcue.annee())
|
||||||
# # prend la "meilleure" validation
|
# prend la "meilleure" validation
|
||||||
# if (not previous_validation) or (
|
if (not previous_validation) or (
|
||||||
# sco_codes.BUT_CODES_ORDER[validation_rcue.code]
|
sco_codes.BUT_CODES_ORDER[validation_rcue.code]
|
||||||
# > sco_codes.BUT_CODES_ORDER[previous_validation["code"]]
|
> sco_codes.BUT_CODES_ORDER[previous_validation["code"]]
|
||||||
# ):
|
):
|
||||||
# self.validation_par_competence_et_annee[niveau.competence.id][
|
self.validation_par_competence_et_annee[niveau.competence.id][
|
||||||
# niveau.annee
|
niveau.annee
|
||||||
# ] = validation_rcue
|
] = validation_rcue
|
||||||
# return validation_par_competence_et_annee
|
return validation_par_competence_et_annee
|
||||||
|
|
||||||
# def list_etud_inscriptions(self, etud: Identite):
|
def list_etud_inscriptions(self, etud: Identite):
|
||||||
# "Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
|
"Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
|
||||||
# self.niveaux_by_annee = {}
|
self.niveaux_by_annee = {}
|
||||||
# "{ annee : liste des niveaux à valider }"
|
"{ annee : liste des niveaux à valider }"
|
||||||
# self.niveaux: dict[int, ApcNiveau] = {}
|
self.niveaux: dict[int, ApcNiveau] = {}
|
||||||
# "cache les niveaux"
|
"cache les niveaux"
|
||||||
# for annee in (1, 2, 3):
|
for annee in (1, 2, 3):
|
||||||
# niveaux_d = formation.referentiel_competence.get_niveaux_by_parcours(
|
niveaux_d = formation.referentiel_competence.get_niveaux_by_parcours(
|
||||||
# annee, [self.parcour] if self.parcour else None # XXX WIP
|
annee, [self.parcour] if self.parcour else None # XXX WIP
|
||||||
# )[1]
|
)[1]
|
||||||
# # groupe les niveaux de tronc commun et ceux spécifiques au parcour
|
# groupe les niveaux de tronc commun et ceux spécifiques au parcour
|
||||||
# self.niveaux_by_annee[annee] = niveaux_d["TC"] + (
|
self.niveaux_by_annee[annee] = niveaux_d["TC"] + (
|
||||||
# niveaux_d[self.parcour.id] if self.parcour else []
|
niveaux_d[self.parcour.id] if self.parcour else []
|
||||||
# )
|
)
|
||||||
# self.niveaux.update(
|
self.niveaux.update(
|
||||||
# {niveau.id: niveau for niveau in self.niveaux_by_annee[annee]}
|
{niveau.id: niveau for niveau in self.niveaux_by_annee[annee]}
|
||||||
# )
|
)
|
||||||
|
|
||||||
# self.validation_par_competence_et_annee = {}
|
self.validation_par_competence_et_annee = {}
|
||||||
# """{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
|
"""{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
|
||||||
# for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
||||||
# niveau = validation_rcue.niveau()
|
niveau = validation_rcue.niveau()
|
||||||
# if not niveau.competence.id in self.validation_par_competence_et_annee:
|
if not niveau.competence.id in self.validation_par_competence_et_annee:
|
||||||
# self.validation_par_competence_et_annee[niveau.competence.id] = {}
|
self.validation_par_competence_et_annee[niveau.competence.id] = {}
|
||||||
# previous_validation = self.validation_par_competence_et_annee.get(
|
previous_validation = self.validation_par_competence_et_annee.get(
|
||||||
# niveau.competence.id
|
niveau.competence.id
|
||||||
# ).get(validation_rcue.annee())
|
).get(validation_rcue.annee())
|
||||||
# # prend la "meilleure" validation
|
# prend la "meilleure" validation
|
||||||
# if (not previous_validation) or (
|
if (not previous_validation) or (
|
||||||
# sco_codes.BUT_CODES_ORDER[validation_rcue.code]
|
sco_codes.BUT_CODES_ORDER[validation_rcue.code]
|
||||||
# > sco_codes.BUT_CODES_ORDER[previous_validation["code"]]
|
> sco_codes.BUT_CODES_ORDER[previous_validation["code"]]
|
||||||
# ):
|
):
|
||||||
# self.validation_par_competence_et_annee[niveau.competence.id][
|
self.validation_par_competence_et_annee[niveau.competence.id][
|
||||||
# niveau.annee
|
niveau.annee
|
||||||
# ] = validation_rcue
|
] = validation_rcue
|
||||||
|
|
||||||
# self.competences = {
|
self.competences = {
|
||||||
# competence.id: competence
|
competence.id: competence
|
||||||
# for competence in (
|
for competence in (
|
||||||
# self.parcour.query_competences()
|
self.parcour.query_competences()
|
||||||
# if self.parcour
|
if self.parcour
|
||||||
# else self.formation.referentiel_competence.get_competences_tronc_commun()
|
else self.formation.referentiel_competence.get_competences_tronc_commun()
|
||||||
# )
|
)
|
||||||
# }
|
}
|
||||||
# "cache { competence_id : competence }"
|
"cache { competence_id : competence }"
|
||||||
|
|
||||||
|
|
||||||
def but_ects_valides(
|
def but_ects_valides(etud: Identite, referentiel_competence_id: int) -> float:
|
||||||
etud: Identite,
|
|
||||||
referentiel_competence_id: int,
|
|
||||||
annees_but: None | Iterable[str] = None,
|
|
||||||
) -> int:
|
|
||||||
"""Nombre d'ECTS validés par etud dans le BUT de référentiel indiqué.
|
"""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,
|
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.
|
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 = (
|
validations = (
|
||||||
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
|
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
|
||||||
.filter(ScolarFormSemestreValidation.ue_id != None)
|
.filter(ScolarFormSemestreValidation.ue_id != None)
|
||||||
.join(UniteEns)
|
.join(UniteEns)
|
||||||
.join(ApcNiveau)
|
.join(ApcNiveau)
|
||||||
)
|
.join(ApcCompetence)
|
||||||
# restreint à certaines années (utile pour les ECTS du DUT120)
|
.filter_by(referentiel_id=referentiel_competence_id)
|
||||||
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
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Tri (nb: fait en python pour gérer les validations externes qui n'ont pas de formsemestre)
|
ects_dict = {}
|
||||||
return sorted(
|
for v in validations:
|
||||||
validations,
|
key = (v.ue.semestre_idx, v.ue.niveau_competence.id)
|
||||||
key=lambda v: (
|
if v.code in CODES_UE_VALIDES:
|
||||||
(v.formsemestre.semestre_id, v.ue.numero, v.ue.acronyme)
|
ects_dict[key] = v.ue.ects
|
||||||
if v.formsemestre
|
|
||||||
else (v.ue.semestre_idx or -2, v.ue.numero, v.ue.acronyme)
|
return sum(ects_dict.values()) if ects_dict else 0.0
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def etud_ues_de_but1_non_validees(
|
def etud_ues_de_but1_non_validees(
|
||||||
@ -487,38 +436,15 @@ def formsemestre_warning_apc_setup(
|
|||||||
"""
|
"""
|
||||||
if not formsemestre.formation.is_apc():
|
if not formsemestre.formation.is_apc():
|
||||||
return ""
|
return ""
|
||||||
url_formation = url_for(
|
|
||||||
"notes.ue_table",
|
|
||||||
scodoc_dept=g.scodoc_dept,
|
|
||||||
formation_id=formsemestre.formation.id,
|
|
||||||
semestre_idx=formsemestre.semestre_id,
|
|
||||||
)
|
|
||||||
if formsemestre.formation.referentiel_competence is None:
|
if formsemestre.formation.referentiel_competence is None:
|
||||||
return f"""<div class="formsemestre_status_warning">
|
return f"""<div class="formsemestre_status_warning">
|
||||||
La <a class="stdlink" href="{url_formation}">formation
|
La <a class="stdlink" href="{
|
||||||
n'est pas associée à un référentiel de compétence.</a>
|
url_for("notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formsemestre.formation.id)
|
||||||
|
}">formation n'est pas associée à un référentiel de compétence.</a>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
H = []
|
|
||||||
# Le semestre n'a pas de parcours, mais les UE ont des parcours ?
|
|
||||||
if not formsemestre.parcours:
|
|
||||||
nb_ues_sans_parcours = len(
|
|
||||||
formsemestre.formation.query_ues_parcour(None)
|
|
||||||
.filter(UniteEns.semestre_idx == formsemestre.semestre_id)
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
nb_ues_tot = (
|
|
||||||
UniteEns.query.filter_by(formation=formsemestre.formation, type=UE_STANDARD)
|
|
||||||
.filter(UniteEns.semestre_idx == formsemestre.semestre_id)
|
|
||||||
.count()
|
|
||||||
)
|
|
||||||
if nb_ues_sans_parcours != nb_ues_tot:
|
|
||||||
H.append(
|
|
||||||
"""Le semestre n'est associé à aucun parcours,
|
|
||||||
mais les UEs de la formation ont des parcours
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
# Vérifie les niveaux de chaque parcours
|
# Vérifie les niveaux de chaque parcours
|
||||||
|
H = []
|
||||||
for parcour in formsemestre.parcours or [None]:
|
for parcour in formsemestre.parcours or [None]:
|
||||||
annee = (formsemestre.semestre_id + 1) // 2
|
annee = (formsemestre.semestre_id + 1) // 2
|
||||||
niveaux_ids = {
|
niveaux_ids = {
|
||||||
@ -543,8 +469,7 @@ def formsemestre_warning_apc_setup(
|
|||||||
if not H:
|
if not H:
|
||||||
return ""
|
return ""
|
||||||
return f"""<div class="formsemestre_status_warning">
|
return f"""<div class="formsemestre_status_warning">
|
||||||
Problème dans la
|
Problème dans la configuration de la formation:
|
||||||
<a class="stdlink" href="{url_formation}">configuration de la formation</a>:
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>{ '</li><li>'.join(H) }</li>
|
<li>{ '</li><li>'.join(H) }</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -557,79 +482,6 @@ def formsemestre_warning_apc_setup(
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def formation_semestre_niveaux_warning(formation: Formation, semestre_idx: int) -> str:
|
|
||||||
"""Vérifie que tous les niveaux de compétences de cette année de formation
|
|
||||||
ont bien des UEs.
|
|
||||||
Afin de ne pas générer trop de messages, on ne considère que les parcours
|
|
||||||
du référentiel de compétences pour lesquels au moins une UE a été associée.
|
|
||||||
|
|
||||||
Renvoie fragment de html
|
|
||||||
"""
|
|
||||||
annee = (semestre_idx - 1) // 2 + 1 # année BUT
|
|
||||||
ref_comp: ApcReferentielCompetences = formation.referentiel_competence
|
|
||||||
if not ref_comp:
|
|
||||||
return "" # détecté ailleurs...
|
|
||||||
niveaux_sans_ue_by_parcour = {} # { parcour.code : [ ue ... ] }
|
|
||||||
parcours_ids = {
|
|
||||||
uep.parcours_id
|
|
||||||
for uep in UEParcours.query.join(UniteEns).filter_by(
|
|
||||||
formation_id=formation.id, type=UE_STANDARD
|
|
||||||
)
|
|
||||||
}
|
|
||||||
for parcour in ref_comp.parcours:
|
|
||||||
if parcour.id not in parcours_ids:
|
|
||||||
continue # saute parcours associés à aucune UE (tous semestres)
|
|
||||||
niveaux_sans_ue = []
|
|
||||||
niveaux = ApcNiveau.niveaux_annee_de_parcours(parcour, annee, ref_comp)
|
|
||||||
# print(f"\n# Parcours {parcour.code} : {len(niveaux)} niveaux")
|
|
||||||
for niveau in niveaux:
|
|
||||||
ues = [ue for ue in formation.ues if ue.niveau_competence_id == niveau.id]
|
|
||||||
if not ues:
|
|
||||||
niveaux_sans_ue.append(niveau)
|
|
||||||
# print( niveau.competence.titre + " " + str(niveau.ordre) + "\t" + str(ue) )
|
|
||||||
if niveaux_sans_ue:
|
|
||||||
niveaux_sans_ue_by_parcour[parcour.code] = niveaux_sans_ue
|
|
||||||
#
|
|
||||||
H = []
|
|
||||||
for parcour_code, niveaux in niveaux_sans_ue_by_parcour.items():
|
|
||||||
H.append(
|
|
||||||
f"""<li>Parcours {parcour_code} : {
|
|
||||||
len(niveaux)} niveaux sans UEs :
|
|
||||||
<span class="niveau-nom"><span>
|
|
||||||
{ '</span>, <span>'.join( f'{niveau.competence.titre} {niveau.ordre}'
|
|
||||||
for niveau in niveaux
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
# Combien de compétences de tronc commun ?
|
|
||||||
_, niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(annee)
|
|
||||||
nb_niveaux_tc = len(niveaux_by_parcours["TC"])
|
|
||||||
nb_ues_tc = len(
|
|
||||||
formation.query_ues_parcour(None)
|
|
||||||
.filter(UniteEns.semestre_idx == semestre_idx)
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
if nb_niveaux_tc != nb_ues_tc:
|
|
||||||
H.append(
|
|
||||||
f"""<li>{nb_niveaux_tc} niveaux de compétences de tronc commun,
|
|
||||||
mais {nb_ues_tc} UEs de tronc commun ! (c'est normal si
|
|
||||||
vous avez des UEs différenciées par parcours)</li>"""
|
|
||||||
)
|
|
||||||
|
|
||||||
if H:
|
|
||||||
return f"""<div class="formation_semestre_niveaux_warning">
|
|
||||||
<div>Problèmes détectés à corriger :</div>
|
|
||||||
<ul>
|
|
||||||
{"".join(H)}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
return "" # no problem detected
|
|
||||||
|
|
||||||
|
|
||||||
def ue_associee_au_niveau_du_parcours(
|
def ue_associee_au_niveau_du_parcours(
|
||||||
ues_possibles: list[UniteEns], niveau: ApcNiveau, sem_name: str = "S"
|
ues_possibles: list[UniteEns], niveau: ApcNiveau, sem_name: str = "S"
|
||||||
) -> UniteEns:
|
) -> UniteEns:
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -10,11 +10,9 @@
|
|||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from flask_wtf.file import FileField, FileAllowed
|
from flask_wtf.file import FileField, FileAllowed
|
||||||
from wtforms import SelectField, SubmitField
|
from wtforms import SelectField, SubmitField
|
||||||
from wtforms.validators import DataRequired
|
|
||||||
|
|
||||||
|
|
||||||
class FormationRefCompForm(FlaskForm):
|
class FormationRefCompForm(FlaskForm):
|
||||||
"Choix d'un référentiel"
|
|
||||||
referentiel_competence = SelectField(
|
referentiel_competence = SelectField(
|
||||||
"Choisir parmi les référentiels déjà chargés :"
|
"Choisir parmi les référentiels déjà chargés :"
|
||||||
)
|
)
|
||||||
@ -23,7 +21,6 @@ class FormationRefCompForm(FlaskForm):
|
|||||||
|
|
||||||
|
|
||||||
class RefCompLoadForm(FlaskForm):
|
class RefCompLoadForm(FlaskForm):
|
||||||
"Upload d'un référentiel"
|
|
||||||
referentiel_standard = SelectField(
|
referentiel_standard = SelectField(
|
||||||
"Choisir un référentiel de compétences officiel BUT"
|
"Choisir un référentiel de compétences officiel BUT"
|
||||||
)
|
)
|
||||||
@ -50,12 +47,3 @@ class RefCompLoadForm(FlaskForm):
|
|||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class FormationChangeRefCompForm(FlaskForm):
|
|
||||||
"choix d'un nouveau ref. comp. pour une formation"
|
|
||||||
object_select = SelectField(
|
|
||||||
"Choisir le nouveau référentiel", validators=[DataRequired()]
|
|
||||||
)
|
|
||||||
submit = SubmitField("Changer le référentiel de la formation")
|
|
||||||
cancel = SubmitField("Annuler")
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
from xml.etree import ElementTree
|
from xml.etree import ElementTree
|
||||||
@ -23,12 +23,9 @@ from app.models.but_refcomp import (
|
|||||||
from app.scodoc.sco_exceptions import ScoFormatError, ScoValueError
|
from app.scodoc.sco_exceptions import ScoFormatError, ScoValueError
|
||||||
|
|
||||||
|
|
||||||
def orebut_import_refcomp(
|
def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None):
|
||||||
xml_data: str, dept_id: int, orig_filename=None
|
|
||||||
) -> ApcReferentielCompetences:
|
|
||||||
"""Importation XML Orébut
|
"""Importation XML Orébut
|
||||||
peut lever TypeError ou ScoFormatError
|
peut lever TypeError ou ScoFormatError
|
||||||
L'objet créé est ajouté et commité.
|
|
||||||
Résultat: instance de ApcReferentielCompetences
|
Résultat: instance de ApcReferentielCompetences
|
||||||
"""
|
"""
|
||||||
# Vérifie que le même fichier n'a pas déjà été chargé:
|
# Vérifie que le même fichier n'a pas déjà été chargé:
|
||||||
@ -36,7 +33,7 @@ def orebut_import_refcomp(
|
|||||||
scodoc_orig_filename=orig_filename, dept_id=dept_id
|
scodoc_orig_filename=orig_filename, dept_id=dept_id
|
||||||
).count():
|
).count():
|
||||||
raise ScoValueError(
|
raise ScoValueError(
|
||||||
f"""Un référentiel a déjà été chargé d'un fichier de même nom.
|
f"""Un référentiel a déjà été chargé d'un fichier de même nom.
|
||||||
({orig_filename})
|
({orig_filename})
|
||||||
Supprimez-le ou changez le nom du fichier."""
|
Supprimez-le ou changez le nom du fichier."""
|
||||||
)
|
)
|
||||||
@ -44,7 +41,7 @@ def orebut_import_refcomp(
|
|||||||
try:
|
try:
|
||||||
root = ElementTree.XML(xml_data)
|
root = ElementTree.XML(xml_data)
|
||||||
except ElementTree.ParseError as exc:
|
except ElementTree.ParseError as exc:
|
||||||
raise ScoFormatError(f"fichier XML Orébut invalide (2): {exc.args}") from exc
|
raise ScoFormatError(f"fichier XML Orébut invalide (2): {exc.args}")
|
||||||
if root.tag != "referentiel_competence":
|
if root.tag != "referentiel_competence":
|
||||||
raise ScoFormatError("élément racine 'referentiel_competence' manquant")
|
raise ScoFormatError("élément racine 'referentiel_competence' manquant")
|
||||||
args = ApcReferentielCompetences.attr_from_xml(root.attrib)
|
args = ApcReferentielCompetences.attr_from_xml(root.attrib)
|
||||||
@ -63,8 +60,7 @@ def orebut_import_refcomp(
|
|||||||
# ne devrait plus se produire car pas d'unicité de l'id: donc inutile
|
# ne devrait plus se produire car pas d'unicité de l'id: donc inutile
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
raise ScoValueError(
|
raise ScoValueError(
|
||||||
f"""Un référentiel a déjà été chargé avec les mêmes compétences ! ({
|
f"""Un référentiel a déjà été chargé avec les mêmes compétences ! ({competence.attrib["id"]})
|
||||||
competence.attrib["id"]})
|
|
||||||
"""
|
"""
|
||||||
) from exc
|
) from exc
|
||||||
ref.competences.append(c)
|
ref.competences.append(c)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -13,22 +13,22 @@ Utilisation:
|
|||||||
cherche les RCUEs de l'année (BUT1, 2, 3)
|
cherche les RCUEs de l'année (BUT1, 2, 3)
|
||||||
pour un redoublant, le RCUE peut considérer un formsemestre d'une année antérieure.
|
pour un redoublant, le RCUE peut considérer un formsemestre d'une année antérieure.
|
||||||
|
|
||||||
on instancie des DecisionsProposees pour les
|
on instancie des DecisionsProposees pour les
|
||||||
différents éléments (UEs, RCUEs, Année, Diplôme)
|
différents éléments (UEs, RCUEs, Année, Diplôme)
|
||||||
Cela donne
|
Cela donne
|
||||||
- les codes possibles (dans .codes)
|
- les codes possibles (dans .codes)
|
||||||
- le code actuel si une décision existe déjà (dans code_valide)
|
- le code actuel si une décision existe déjà (dans code_valide)
|
||||||
- pour les UEs, le rcue s'il y en a un)
|
- pour les UEs, le rcue s'il y en a un)
|
||||||
|
|
||||||
2) Validation pour l'utilisateur (form)) => enregistrement code
|
2) Validation pour l'utilisateur (form)) => enregistrement code
|
||||||
- on vérifie que le code soumis est bien dans les codes possibles
|
- on vérifie que le code soumis est bien dans les codes possibles
|
||||||
- on enregistre la décision (dans ScolarFormSemestreValidation pour les UE,
|
- on enregistre la décision (dans ScolarFormSemestreValidation pour les UE,
|
||||||
ApcValidationRCUE pour les RCUE, et ApcValidationAnnee pour les années)
|
ApcValidationRCUE pour les RCUE, et ApcValidationAnnee pour les années)
|
||||||
- Si RCUE validé, on déclenche d'éventuelles validations:
|
- Si RCUE validé, on déclenche d'éventuelles validations:
|
||||||
("La validation des deux UE du niveau d'une compétence emporte la validation
|
("La validation des deux UE du niveau d'une compétence emporte la validation
|
||||||
de l'ensemble des UE du niveau inférieur de cette même compétence.")
|
de l'ensemble des UE du niveau inférieur de cette même compétence.")
|
||||||
|
|
||||||
Les jurys de semestre BUT impairs entrainent systématiquement la génération d'une
|
Les jurys de semestre BUT impairs entrainent systématiquement la génération d'une
|
||||||
autorisation d'inscription dans le semestre pair suivant: `ScolarAutorisationInscription`.
|
autorisation d'inscription dans le semestre pair suivant: `ScolarAutorisationInscription`.
|
||||||
Les jurys de semestres pairs non (S2, S4, S6): il y a une décision sur l'année (ETP)
|
Les jurys de semestres pairs non (S2, S4, S6): il y a une décision sur l'année (ETP)
|
||||||
- autorisation en S_2n+1 (S3 ou S5) si: ADM, ADJ, PASD, PAS1CN
|
- autorisation en S_2n+1 (S3 ou S5) si: ADM, ADJ, PASD, PAS1CN
|
||||||
@ -39,8 +39,8 @@ Le formulaire permet de choisir des codes d'UE, RCUE et Année (ETP).
|
|||||||
Mais normalement, les codes d'UE sont à choisir: les RCUE et l'année s'en déduisent.
|
Mais normalement, les codes d'UE sont à choisir: les RCUE et l'année s'en déduisent.
|
||||||
Si l'utilisateur coche "décision manuelle", il peut alors choisir les codes RCUE et années.
|
Si l'utilisateur coche "décision manuelle", il peut alors choisir les codes RCUE et années.
|
||||||
|
|
||||||
La soumission du formulaire:
|
La soumission du formulaire:
|
||||||
- etud, formation
|
- etud, formation
|
||||||
- UEs: [(formsemestre, ue, code), ...]
|
- UEs: [(formsemestre, ue, code), ...]
|
||||||
- RCUE: [(formsemestre, ue, code), ...] le formsemestre est celui d'indice pair du niveau
|
- RCUE: [(formsemestre, ue, code), ...] le formsemestre est celui d'indice pair du niveau
|
||||||
(S2, S4 ou S6), il sera regoupé avec celui impair de la même année ou de la suivante.
|
(S2, S4 ou S6), il sera regoupé avec celui impair de la même année ou de la suivante.
|
||||||
@ -64,7 +64,6 @@ import re
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from flask import flash, g, url_for
|
from flask import flash, g, url_for
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app import log
|
from app import log
|
||||||
@ -78,14 +77,12 @@ from app.models.but_refcomp import (
|
|||||||
ApcNiveau,
|
ApcNiveau,
|
||||||
ApcParcours,
|
ApcParcours,
|
||||||
)
|
)
|
||||||
from app.models import Evaluation, ModuleImpl, Scolog, ScolarAutorisationInscription
|
from app.models import Scolog, ScolarAutorisationInscription
|
||||||
from app.models.but_validations import (
|
from app.models.but_validations import (
|
||||||
ApcValidationAnnee,
|
ApcValidationAnnee,
|
||||||
ApcValidationRCUE,
|
ApcValidationRCUE,
|
||||||
ValidationDUT120,
|
|
||||||
)
|
)
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
from app.models.formations import Formation
|
|
||||||
from app.models.formsemestre import FormSemestre
|
from app.models.formsemestre import FormSemestre
|
||||||
from app.models.ues import UniteEns
|
from app.models.ues import UniteEns
|
||||||
from app.models.validations import ScolarFormSemestreValidation
|
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 import codes_cursus as sco_codes
|
||||||
from app.scodoc.codes_cursus import (
|
from app.scodoc.codes_cursus import (
|
||||||
code_rcue_validant,
|
code_rcue_validant,
|
||||||
code_ue_validant,
|
|
||||||
BUT_CODES_ORDER,
|
BUT_CODES_ORDER,
|
||||||
CODES_RCUE_VALIDES,
|
CODES_RCUE_VALIDES,
|
||||||
CODES_UE_VALIDES,
|
CODES_UE_VALIDES,
|
||||||
@ -121,7 +117,7 @@ class NoRCUEError(ScoValueError):
|
|||||||
{warning_impair}
|
{warning_impair}
|
||||||
{warning_pair}
|
{warning_pair}
|
||||||
<div><b>UE {ue.acronyme}</b>: niveau {html.escape(str(ue.niveau_competence))}</div>
|
<div><b>UE {ue.acronyme}</b>: niveau {html.escape(str(ue.niveau_competence))}</div>
|
||||||
<div><b>UEs impaires:</b> {html.escape(', '.join(str(u.niveau_competence or "pas de niveau")
|
<div><b>UEs impaires:</b> {html.escape(', '.join(str(u.niveau_competence or "pas de niveau")
|
||||||
for u in deca.ues_impair))}
|
for u in deca.ues_impair))}
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
@ -264,11 +260,11 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||||||
else []
|
else []
|
||||||
)
|
)
|
||||||
# ---- Niveaux et RCUEs
|
# ---- Niveaux et RCUEs
|
||||||
niveaux_by_parcours = formsemestre.formation.referentiel_competence.get_niveaux_by_parcours(
|
niveaux_by_parcours = (
|
||||||
self.annee_but, [self.parcour] if self.parcour else None
|
formsemestre.formation.referentiel_competence.get_niveaux_by_parcours(
|
||||||
)[
|
self.annee_but, [self.parcour] if self.parcour else None
|
||||||
1
|
)[1]
|
||||||
]
|
)
|
||||||
self.niveaux_competences = niveaux_by_parcours["TC"] + (
|
self.niveaux_competences = niveaux_by_parcours["TC"] + (
|
||||||
niveaux_by_parcours[self.parcour.id] if self.parcour else []
|
niveaux_by_parcours[self.parcour.id] if self.parcour else []
|
||||||
)
|
)
|
||||||
@ -277,8 +273,8 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||||||
= niveaux du tronc commun + niveau du parcours de l'étudiant.
|
= niveaux du tronc commun + niveau du parcours de l'étudiant.
|
||||||
"""
|
"""
|
||||||
self.rcue_by_niveau = self._compute_rcues_annee()
|
self.rcue_by_niveau = self._compute_rcues_annee()
|
||||||
"""RCUEs de l'année
|
"""RCUEs de l'année
|
||||||
(peuvent être construits avec des UEs validées antérieurement: redoublants
|
(peuvent être construits avec des UEs validées antérieurement: redoublants
|
||||||
avec UEs capitalisées, validation "antérieures")
|
avec UEs capitalisées, validation "antérieures")
|
||||||
"""
|
"""
|
||||||
# ---- Décision année et autorisation
|
# ---- Décision année et autorisation
|
||||||
@ -362,17 +358,13 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||||||
# self.codes = [] # pas de décision annuelle sur semestres impairs
|
# self.codes = [] # pas de décision annuelle sur semestres impairs
|
||||||
elif self.inscription_etat != scu.INSCRIT:
|
elif self.inscription_etat != scu.INSCRIT:
|
||||||
self.codes = [
|
self.codes = [
|
||||||
(
|
sco_codes.DEM
|
||||||
sco_codes.DEM
|
if self.inscription_etat == scu.DEMISSION
|
||||||
if self.inscription_etat == scu.DEMISSION
|
else sco_codes.DEF,
|
||||||
else sco_codes.DEF
|
|
||||||
),
|
|
||||||
# propose aussi d'autres codes, au cas où...
|
# propose aussi d'autres codes, au cas où...
|
||||||
(
|
sco_codes.DEM
|
||||||
sco_codes.DEM
|
if self.inscription_etat != scu.DEMISSION
|
||||||
if self.inscription_etat != scu.DEMISSION
|
else sco_codes.DEF,
|
||||||
else sco_codes.DEF
|
|
||||||
),
|
|
||||||
sco_codes.ABAN,
|
sco_codes.ABAN,
|
||||||
sco_codes.ABL,
|
sco_codes.ABL,
|
||||||
sco_codes.EXCLU,
|
sco_codes.EXCLU,
|
||||||
@ -388,24 +380,14 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||||||
sco_codes.ADJ,
|
sco_codes.ADJ,
|
||||||
] + self.codes
|
] + self.codes
|
||||||
explanation += f" et {self.nb_rcues_under_8} < 8"
|
explanation += f" et {self.nb_rcues_under_8} < 8"
|
||||||
else: # autres cas: non admis, non passage, non dem, pas la moitié des rcue:
|
else:
|
||||||
if formsemestre.semestre_id % 2 and self.formsemestre_pair is None:
|
self.codes = [
|
||||||
# Si jury sur un seul semestre impair, ne propose pas redoublement
|
sco_codes.RED,
|
||||||
# et efface décision éventuellement existante
|
sco_codes.NAR,
|
||||||
codes = [None]
|
sco_codes.PAS1NCI,
|
||||||
else:
|
sco_codes.ADJ,
|
||||||
codes = []
|
sco_codes.PASD, # voir #488 (discutable, conventions locales)
|
||||||
self.codes = (
|
] + self.codes
|
||||||
codes
|
|
||||||
+ [
|
|
||||||
sco_codes.RED,
|
|
||||||
sco_codes.NAR,
|
|
||||||
sco_codes.PAS1NCI,
|
|
||||||
sco_codes.ADJ,
|
|
||||||
sco_codes.PASD, # voir #488 (discutable, conventions locales)
|
|
||||||
]
|
|
||||||
+ self.codes
|
|
||||||
)
|
|
||||||
explanation += f""" et {self.nb_rcues_under_8
|
explanation += f""" et {self.nb_rcues_under_8
|
||||||
} niveau{'x' if self.nb_rcues_under_8 > 1 else ''} < 8"""
|
} niveau{'x' if self.nb_rcues_under_8 > 1 else ''} < 8"""
|
||||||
|
|
||||||
@ -417,19 +399,15 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||||||
# Si validée par niveau supérieur:
|
# Si validée par niveau supérieur:
|
||||||
if self.code_valide == sco_codes.ADSUP:
|
if self.code_valide == sco_codes.ADSUP:
|
||||||
self.codes.insert(0, sco_codes.ADSUP)
|
self.codes.insert(0, sco_codes.ADSUP)
|
||||||
self.explanation = f'<div class="deca-expl">{explanation}</div>'
|
self.explanation = f"<div>{explanation}</div>"
|
||||||
messages = self.descr_pb_coherence()
|
messages = self.descr_pb_coherence()
|
||||||
if messages:
|
if messages:
|
||||||
self.explanation += (
|
self.explanation += (
|
||||||
'<div class="warning warning-info">'
|
'<div class="warning">'
|
||||||
+ '</div><div class="warning warning-info">'.join(messages)
|
+ '</div><div class="warning">'.join(messages)
|
||||||
+ "</div>"
|
+ "</div>"
|
||||||
)
|
)
|
||||||
|
self.codes = [self.codes[0]] + sorted(self.codes[1:])
|
||||||
# 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)
|
|
||||||
|
|
||||||
def passage_de_droit_en_but3(self) -> tuple[bool, str]:
|
def passage_de_droit_en_but3(self) -> tuple[bool, str]:
|
||||||
"""Vérifie si les conditions supplémentaires de passage BUT2 vers BUT3 sont satisfaites"""
|
"""Vérifie si les conditions supplémentaires de passage BUT2 vers BUT3 sont satisfaites"""
|
||||||
@ -536,21 +514,19 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||||||
"""Les deux formsemestres auquel est inscrit l'étudiant (ni DEM ni DEF)
|
"""Les deux formsemestres auquel est inscrit l'étudiant (ni DEM ni DEF)
|
||||||
du niveau auquel appartient formsemestre.
|
du niveau auquel appartient formsemestre.
|
||||||
|
|
||||||
-> S_impair, S_pair (de la même année scolaire)
|
-> S_impair, S_pair
|
||||||
|
|
||||||
Si l'origine est impair, S_impair est l'origine et S_pair est None
|
Si l'origine est impair, S_impair est l'origine et S_pair est None
|
||||||
Si l'origine est paire, S_pair est l'origine, et S_impair l'antérieur
|
Si l'origine est paire, S_pair est l'origine, et S_impair l'antérieur
|
||||||
suivi par cet étudiant (ou None).
|
suivi par cet étudiant (ou None).
|
||||||
|
|
||||||
Note: si l'option "block_moyennes" est activée, ne prend pas en compte le semestre.
|
|
||||||
"""
|
"""
|
||||||
if not formsemestre.formation.is_apc(): # garde fou
|
if not formsemestre.formation.is_apc(): # garde fou
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
if formsemestre.semestre_id % 2:
|
if formsemestre.semestre_id % 2:
|
||||||
idx_autre = formsemestre.semestre_id + 1 # impair, autre = suivant
|
idx_autre = formsemestre.semestre_id + 1
|
||||||
else:
|
else:
|
||||||
idx_autre = formsemestre.semestre_id - 1 # pair: autre = précédent
|
idx_autre = formsemestre.semestre_id - 1
|
||||||
|
|
||||||
# Cherche l'autre semestre de la même année scolaire:
|
# Cherche l'autre semestre de la même année scolaire:
|
||||||
autre_formsemestre = None
|
autre_formsemestre = None
|
||||||
@ -563,8 +539,6 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||||||
inscr.formsemestre.formation.referentiel_competence
|
inscr.formsemestre.formation.referentiel_competence
|
||||||
== formsemestre.formation.referentiel_competence
|
== formsemestre.formation.referentiel_competence
|
||||||
)
|
)
|
||||||
# Non bloqué
|
|
||||||
and not inscr.formsemestre.block_moyennes
|
|
||||||
# L'autre semestre
|
# L'autre semestre
|
||||||
and (inscr.formsemestre.semestre_id == idx_autre)
|
and (inscr.formsemestre.semestre_id == idx_autre)
|
||||||
# de la même année scolaire
|
# de la même année scolaire
|
||||||
@ -607,9 +581,11 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||||||
# Ordonne par numéro d'UE
|
# Ordonne par numéro d'UE
|
||||||
niv_rcue = sorted(
|
niv_rcue = sorted(
|
||||||
self.rcue_by_niveau.items(),
|
self.rcue_by_niveau.items(),
|
||||||
key=lambda x: (
|
key=lambda x: x[1].ue_1.numero
|
||||||
x[1].ue_1.numero if x[1].ue_1 else x[1].ue_2.numero if x[1].ue_2 else 0
|
if x[1].ue_1
|
||||||
),
|
else x[1].ue_2.numero
|
||||||
|
if x[1].ue_2
|
||||||
|
else 0,
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
niveau_id: DecisionsProposeesRCUE(self, rcue, self.inscription_etat)
|
niveau_id: DecisionsProposeesRCUE(self, rcue, self.inscription_etat)
|
||||||
@ -634,7 +610,6 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||||||
def next_semestre_ids(self, code: str) -> set[int]:
|
def next_semestre_ids(self, code: str) -> set[int]:
|
||||||
"""Les indices des semestres dans lequels l'étudiant est autorisé
|
"""Les indices des semestres dans lequels l'étudiant est autorisé
|
||||||
à poursuivre après le semestre courant.
|
à poursuivre après le semestre courant.
|
||||||
code: code jury sur année BUT
|
|
||||||
"""
|
"""
|
||||||
# La poursuite d'études dans un semestre pair d'une même année
|
# La poursuite d'études dans un semestre pair d'une même année
|
||||||
# est de droit pour tout étudiant.
|
# est de droit pour tout étudiant.
|
||||||
@ -678,8 +653,6 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||||||
|
|
||||||
Si les code_rcue et le code_annee ne sont pas fournis,
|
Si les code_rcue et le code_annee ne sont pas fournis,
|
||||||
et qu'il n'y en a pas déjà, enregistre ceux par défaut.
|
et qu'il n'y en a pas déjà, enregistre ceux par défaut.
|
||||||
|
|
||||||
Si le code_annee est None, efface le code déjà enregistré.
|
|
||||||
"""
|
"""
|
||||||
log("jury_but.DecisionsProposeesAnnee.record_form")
|
log("jury_but.DecisionsProposeesAnnee.record_form")
|
||||||
code_annee = self.codes[0] # si pas dans le form, valeur par defaut
|
code_annee = self.codes[0] # si pas dans le form, valeur par defaut
|
||||||
@ -724,7 +697,6 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||||||
def record(self, code: str, mark_recorded: bool = True) -> bool:
|
def record(self, code: str, mark_recorded: bool = True) -> bool:
|
||||||
"""Enregistre le code de l'année, et au besoin l'autorisation d'inscription.
|
"""Enregistre le code de l'année, et au besoin l'autorisation d'inscription.
|
||||||
Si l'étudiant est DEM ou DEF, ne fait rien.
|
Si l'étudiant est DEM ou DEF, ne fait rien.
|
||||||
Si le code est None, efface le code déjà enregistré.
|
|
||||||
Si mark_recorded est vrai, positionne self.recorded
|
Si mark_recorded est vrai, positionne self.recorded
|
||||||
"""
|
"""
|
||||||
if self.inscription_etat != scu.INSCRIT:
|
if self.inscription_etat != scu.INSCRIT:
|
||||||
@ -761,22 +733,20 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||||||
self.validation.date = datetime.now()
|
self.validation.date = datetime.now()
|
||||||
|
|
||||||
db.session.add(self.validation)
|
db.session.add(self.validation)
|
||||||
|
db.session.commit()
|
||||||
log(f"Recording {self}: {code}")
|
log(f"Recording {self}: {code}")
|
||||||
Scolog.logdb(
|
Scolog.logdb(
|
||||||
method="jury_but",
|
method="jury_but",
|
||||||
etudid=self.etud.id,
|
etudid=self.etud.id,
|
||||||
msg=f"Validation année BUT{self.annee_but}: {code}",
|
msg=f"Validation année BUT{self.annee_but}: {code}",
|
||||||
)
|
)
|
||||||
db.session.commit()
|
|
||||||
if mark_recorded:
|
if mark_recorded:
|
||||||
self.recorded = True
|
self.recorded = True
|
||||||
self.invalidate_formsemestre_cache()
|
self.invalidate_formsemestre_cache()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def record_autorisation_inscription(self, code: str):
|
def record_autorisation_inscription(self, code: str):
|
||||||
"""Autorisation d'inscription dans semestre suivant.
|
"""Autorisation d'inscription dans semestre suivant"""
|
||||||
code: code jury sur année BUT
|
|
||||||
"""
|
|
||||||
if self.autorisations_recorded:
|
if self.autorisations_recorded:
|
||||||
return
|
return
|
||||||
if self.inscription_etat != scu.INSCRIT:
|
if self.inscription_etat != scu.INSCRIT:
|
||||||
@ -804,33 +774,16 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||||||
if self.formsemestre_pair is not None:
|
if self.formsemestre_pair is not None:
|
||||||
sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre_pair.id)
|
sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre_pair.id)
|
||||||
|
|
||||||
def _get_current_res(self) -> ResultatsSemestreBUT:
|
def has_notes_en_attente(self) -> bool:
|
||||||
"Les res. du semestre d'origine du deca"
|
"Vrai si l'étudiant a au moins une note en attente dans le semestre origine de ce deca"
|
||||||
return (
|
res = (
|
||||||
self.res_pair
|
self.res_pair
|
||||||
if self.formsemestre_pair
|
if self.formsemestre_pair
|
||||||
and (self.formsemestre.id == self.formsemestre_pair.id)
|
and (self.formsemestre.id == self.formsemestre_pair.id)
|
||||||
else self.res_impair
|
else self.res_impair
|
||||||
)
|
)
|
||||||
|
|
||||||
def has_notes_en_attente(self) -> bool:
|
|
||||||
"Vrai si l'étudiant a au moins une note en attente dans le semestre origine de ce deca"
|
|
||||||
res = self._get_current_res()
|
|
||||||
return res and self.etud.id in res.get_etudids_attente()
|
return res and self.etud.id in res.get_etudids_attente()
|
||||||
|
|
||||||
def get_modimpls_attente(self) -> list[ModuleImpl]:
|
|
||||||
"Liste des ModuleImpl dans lesquels l'étudiant à au moins une note en ATTente"
|
|
||||||
res = self._get_current_res()
|
|
||||||
modimpls_results = [
|
|
||||||
modimpl_result
|
|
||||||
for modimpl_result in res.modimpls_results.values()
|
|
||||||
if self.etud.id in modimpl_result.etudids_attente
|
|
||||||
]
|
|
||||||
modimpls = [
|
|
||||||
db.session.get(ModuleImpl, mr.moduleimpl_id) for mr in modimpls_results
|
|
||||||
]
|
|
||||||
return sorted(modimpls, key=lambda mi: (mi.module.numero, mi.module.code))
|
|
||||||
|
|
||||||
def record_all(self, only_validantes: bool = False) -> bool:
|
def record_all(self, only_validantes: bool = False) -> bool:
|
||||||
"""Enregistre les codes qui n'ont pas été spécifiés par le formulaire,
|
"""Enregistre les codes qui n'ont pas été spécifiés par le formulaire,
|
||||||
et sont donc en mode "automatique".
|
et sont donc en mode "automatique".
|
||||||
@ -843,15 +796,9 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||||||
Return: True si au moins un code modifié et enregistré.
|
Return: True si au moins un code modifié et enregistré.
|
||||||
"""
|
"""
|
||||||
modif = False
|
modif = False
|
||||||
if only_validantes:
|
# Vérification notes en attente dans formsemestre origine
|
||||||
if self.has_notes_en_attente():
|
if only_validantes and self.has_notes_en_attente():
|
||||||
# notes en attente dans formsemestre origine
|
return False
|
||||||
return False
|
|
||||||
if Evaluation.get_evaluations_blocked_for_etud(
|
|
||||||
self.formsemestre, self.etud
|
|
||||||
):
|
|
||||||
# évaluation(s) qui seront débloquées dans le futur
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Toujours valider dans l'ordre UE, RCUE, Année
|
# Toujours valider dans l'ordre UE, RCUE, Année
|
||||||
annee_scolaire = self.formsemestre.annee_scolaire()
|
annee_scolaire = self.formsemestre.annee_scolaire()
|
||||||
@ -893,7 +840,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||||||
not only_validantes
|
not only_validantes
|
||||||
) or code in sco_codes.CODES_ANNEE_BUT_VALIDES_DE_DROIT:
|
) or code in sco_codes.CODES_ANNEE_BUT_VALIDES_DE_DROIT:
|
||||||
modif |= self.record(code)
|
modif |= self.record(code)
|
||||||
self.record_autorisation_inscription(code)
|
self.record_autorisation_inscription(code)
|
||||||
return modif
|
return modif
|
||||||
|
|
||||||
def erase(self, only_one_sem=False):
|
def erase(self, only_one_sem=False):
|
||||||
@ -904,8 +851,6 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||||||
Si only_one_sem, n'efface que les décisions UE et les
|
Si only_one_sem, n'efface que les décisions UE et les
|
||||||
autorisations de passage du semestre d'origine du deca.
|
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.
|
Dans tous les cas, efface les validations de l'année en cours.
|
||||||
(commite la session.)
|
(commite la session.)
|
||||||
"""
|
"""
|
||||||
@ -955,17 +900,6 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||||||
msg=f"Validation année BUT{self.annee_but}: effacée",
|
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
|
# Efface éventuelles validations de semestre
|
||||||
# (en principe inutilisées en BUT)
|
# (en principe inutilisées en BUT)
|
||||||
# et autres UEs (en cas de changement d'architecture de formation depuis le jury ?)
|
# et autres UEs (en cas de changement d'architecture de formation depuis le jury ?)
|
||||||
@ -1007,36 +941,6 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||||||
pour PV jurys
|
pour PV jurys
|
||||||
"""
|
"""
|
||||||
validations = []
|
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):
|
for res in (self.res_impair, self.res_pair):
|
||||||
if res:
|
if res:
|
||||||
dec_ues = [
|
dec_ues = [
|
||||||
@ -1045,10 +949,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||||||
if ue.type == UE_STANDARD and ue.id in self.decisions_ues
|
if ue.type == UE_STANDARD and ue.id in self.decisions_ues
|
||||||
]
|
]
|
||||||
valids = [dec_ue.descr_validation() for dec_ue in dec_ues]
|
valids = [dec_ue.descr_validation() for dec_ue in dec_ues]
|
||||||
# présentation de la liste des UEs:
|
validations.append(", ".join(v for v in valids if v))
|
||||||
if valids:
|
|
||||||
validations.append(", ".join(v for v in valids if v))
|
|
||||||
|
|
||||||
return line_sep.join(validations)
|
return line_sep.join(validations)
|
||||||
|
|
||||||
def descr_pb_coherence(self) -> list[str]:
|
def descr_pb_coherence(self) -> list[str]:
|
||||||
@ -1068,28 +969,24 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||||||
if dec_ue.code_valide not in CODES_UE_VALIDES:
|
if dec_ue.code_valide not in CODES_UE_VALIDES:
|
||||||
if (
|
if (
|
||||||
dec_ue.ue_status
|
dec_ue.ue_status
|
||||||
and dec_ue.ue_status["is_capitalized"]
|
and dec_ue.ue_status["was_capitalized"]
|
||||||
):
|
):
|
||||||
messages.append(
|
messages.append(
|
||||||
f"Information: l'UE {ue.acronyme} capitalisée est utilisée pour un RCUE cette année"
|
f"Information: l'UE {ue.acronyme} capitalisée est utilisée pour un RCUE cette année"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
messages.append(
|
messages.append(
|
||||||
f"L'UE {ue.acronyme} n'est pas validée mais son RCUE l'est (probablement une validation antérieure)"
|
f"L'UE {ue.acronyme} n'est pas validée mais son RCUE l'est !"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
messages.append(
|
messages.append(
|
||||||
f"L'UE {ue.acronyme} n'a pas décision (???)"
|
f"L'UE {ue.acronyme} n'a pas décision (???)"
|
||||||
)
|
)
|
||||||
# Voyons si on est dispensé de cette ue ?
|
|
||||||
res = self.res_impair if ue.semestre_idx % 2 else self.res_pair
|
|
||||||
if res and (self.etud.id, ue.id) in res.dispense_ues:
|
|
||||||
messages.append(f"Pas (ré)inscrit à l'UE {ue.acronyme}")
|
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
def valide_diplome(self) -> bool:
|
def valide_diplome(self) -> bool:
|
||||||
"Vrai si l'étudiant a validé son diplôme (décision enregistrée)"
|
"Vrai si l'étudiant à validé son diplôme"
|
||||||
return self.annee_but == 3 and sco_codes.code_annee_validant(self.code_valide)
|
return False # TODO XXX
|
||||||
|
|
||||||
|
|
||||||
def list_ue_parcour_etud(
|
def list_ue_parcour_etud(
|
||||||
@ -1228,12 +1125,13 @@ class DecisionsProposeesRCUE(DecisionsProposees):
|
|||||||
code=code,
|
code=code,
|
||||||
)
|
)
|
||||||
db.session.add(self.validation)
|
db.session.add(self.validation)
|
||||||
|
db.session.commit()
|
||||||
Scolog.logdb(
|
Scolog.logdb(
|
||||||
method="jury_but",
|
method="jury_but",
|
||||||
etudid=self.etud.id,
|
etudid=self.etud.id,
|
||||||
msg=f"Validation {self.rcue}: {code}",
|
msg=f"Validation {self.rcue}: {code}",
|
||||||
|
commit=True,
|
||||||
)
|
)
|
||||||
db.session.commit()
|
|
||||||
log(f"rcue.record {self}: {code}")
|
log(f"rcue.record {self}: {code}")
|
||||||
|
|
||||||
# Modifie au besoin les codes d'UE
|
# Modifie au besoin les codes d'UE
|
||||||
@ -1570,11 +1468,9 @@ class DecisionsProposeesUE(DecisionsProposees):
|
|||||||
self.validation = None # cache toute validation
|
self.validation = None # cache toute validation
|
||||||
self.explanation = "non inscrit (dem. ou déf.)"
|
self.explanation = "non inscrit (dem. ou déf.)"
|
||||||
self.codes = [
|
self.codes = [
|
||||||
(
|
sco_codes.DEM
|
||||||
sco_codes.DEM
|
if res.get_etud_etat(etud.id) == scu.DEMISSION
|
||||||
if res.get_etud_etat(etud.id) == scu.DEMISSION
|
else sco_codes.DEF
|
||||||
else sco_codes.DEF
|
|
||||||
)
|
|
||||||
]
|
]
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -1588,7 +1484,7 @@ class DecisionsProposeesUE(DecisionsProposees):
|
|||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"""<{self.__class__.__name__} ue={self.ue.acronyme} valid={self.code_valide
|
return f"""<{self.__class__.__name__} ue={self.ue.acronyme} valid={self.code_valide
|
||||||
} codes={self.codes} explanation="{self.explanation}">"""
|
} codes={self.codes} explanation={self.explanation}>"""
|
||||||
|
|
||||||
def compute_codes(self):
|
def compute_codes(self):
|
||||||
"""Calcul des .codes attribuables et de l'explanation associée"""
|
"""Calcul des .codes attribuables et de l'explanation associée"""
|
||||||
@ -1646,12 +1542,13 @@ class DecisionsProposeesUE(DecisionsProposees):
|
|||||||
moy_ue=self.moy_ue,
|
moy_ue=self.moy_ue,
|
||||||
)
|
)
|
||||||
db.session.add(self.validation)
|
db.session.add(self.validation)
|
||||||
|
db.session.commit()
|
||||||
Scolog.logdb(
|
Scolog.logdb(
|
||||||
method="jury_but",
|
method="jury_but",
|
||||||
etudid=self.etud.id,
|
etudid=self.etud.id,
|
||||||
msg=f"Validation UE {self.ue.id} {self.ue.acronyme}({self.moy_ue}): {code}",
|
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}")
|
log(f"DecisionsProposeesUE: recording {self.validation}")
|
||||||
|
|
||||||
sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre.id)
|
sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre.id)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -8,12 +8,11 @@
|
|||||||
"""
|
"""
|
||||||
from flask import g, request, url_for
|
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 import log
|
||||||
from app.but import jury_but
|
from app.but import jury_but
|
||||||
from app.but.cursus_but import but_ects_valides
|
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.etudiants import Identite
|
||||||
from app.models.formsemestre import FormSemestre
|
from app.models.formsemestre import FormSemestre
|
||||||
from app.scodoc.gen_tables import GenTable
|
from app.scodoc.gen_tables import GenTable
|
||||||
@ -56,21 +55,11 @@ def pvjury_page_but(formsemestre_id: int, fmt="html"):
|
|||||||
else:
|
else:
|
||||||
line_sep = "\n"
|
line_sep = "\n"
|
||||||
rows, titles = pvjury_table_but(formsemestre, line_sep=line_sep)
|
rows, titles = pvjury_table_but(formsemestre, line_sep=line_sep)
|
||||||
if fmt.startswith("xls"):
|
|
||||||
titles.update(
|
|
||||||
{
|
|
||||||
"etudid": "etudid",
|
|
||||||
"code_nip": "nip",
|
|
||||||
"code_ine": "ine",
|
|
||||||
"ects_but": "Total ECTS BUT",
|
|
||||||
"civilite": "Civ.",
|
|
||||||
"nom": "Nom",
|
|
||||||
"prenom": "Prénom",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
# Style excel... passages à la ligne sur \n
|
# Style excel... passages à la ligne sur \n
|
||||||
xls_style_base = sco_excel.excel_make_style()
|
xls_style_base = sco_excel.excel_make_style()
|
||||||
xls_style_base["alignment"] = Alignment(wrapText=True, vertical="top")
|
xls_style_base["alignment"] = Alignment(wrapText=True, vertical="top")
|
||||||
|
|
||||||
tab = GenTable(
|
tab = GenTable(
|
||||||
base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}",
|
base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}",
|
||||||
caption=title,
|
caption=title,
|
||||||
@ -80,7 +69,7 @@ def pvjury_page_but(formsemestre_id: int, fmt="html"):
|
|||||||
html_title=f"""<div style="margin-bottom: 8px;"><span
|
html_title=f"""<div style="margin-bottom: 8px;"><span
|
||||||
style="font-size: 120%; font-weight: bold;">{title}</span>
|
style="font-size: 120%; font-weight: bold;">{title}</span>
|
||||||
<span style="padding-left: 20px;">
|
<span style="padding-left: 20px;">
|
||||||
<a href="{url_for("notes.pvjury_page_but",
|
<a href="{url_for("notes.pvjury_page_but",
|
||||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, fmt="xlsx")}"
|
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, fmt="xlsx")}"
|
||||||
class="stdlink">version excel</a></span></div>
|
class="stdlink">version excel</a></span></div>
|
||||||
|
|
||||||
@ -127,7 +116,7 @@ def pvjury_table_but(
|
|||||||
"pas de référentiel de compétences associé à la formation de ce semestre !"
|
"pas de référentiel de compétences associé à la formation de ce semestre !"
|
||||||
)
|
)
|
||||||
titles = {
|
titles = {
|
||||||
"nom_pv": "Code" if anonymous else "Nom",
|
"nom": "Code" if anonymous else "Nom",
|
||||||
"cursus": "Cursus",
|
"cursus": "Cursus",
|
||||||
"ects": "ECTS",
|
"ects": "ECTS",
|
||||||
"ues": "UE validées",
|
"ues": "UE validées",
|
||||||
@ -155,64 +144,33 @@ def pvjury_table_but(
|
|||||||
except ScoValueError:
|
except ScoValueError:
|
||||||
deca = None
|
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 = {
|
row = {
|
||||||
"nom_pv": (
|
"nom": etud.code_ine or etud.code_nip or etud.id
|
||||||
etud.code_ine or etud.code_nip or etud.id
|
if anonymous # Mode anonyme: affiche INE ou sinon NIP, ou id
|
||||||
if anonymous # Mode anonyme: affiche INE ou sinon NIP, ou id
|
else etud.etat_civil_pv(
|
||||||
else etud.etat_civil_pv(
|
line_sep=line_sep, with_paragraph=with_paragraph_nom
|
||||||
line_sep=line_sep, with_paragraph=with_paragraph_nom
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
"_nom_pv_order": etud.sort_key,
|
"_nom_order": etud.sort_key,
|
||||||
"_nom_pv_target_attrs": f'class="etudinfo" id="{etud.id}"',
|
"_nom_target_attrs": f'class="etudinfo" id="{etud.id}"',
|
||||||
"_nom_pv_td_attrs": f'id="{etud.id}" class="etudinfo"',
|
"_nom_td_attrs": f'id="{etud.id}" class="etudinfo"',
|
||||||
"_nom_pv_target": url_for(
|
"_nom_target": url_for(
|
||||||
"scolar.fiche_etud",
|
"scolar.ficheEtud",
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
etudid=etud.id,
|
etudid=etud.id,
|
||||||
),
|
),
|
||||||
"cursus": _descr_cursus_but(etud),
|
"cursus": _descr_cursus_but(etud),
|
||||||
"ects": (
|
"ects": f"""{deca.ects_annee():g}<br><br>Tot. {but_ects_valides(etud, referentiel_competence_id ):g}""",
|
||||||
f"""{deca.ects_annee():g}<br><br>Tot. {ects_but_valides:g}"""
|
|
||||||
if deca
|
|
||||||
else ""
|
|
||||||
),
|
|
||||||
"_ects_xls": deca.ects_annee() if deca else "",
|
|
||||||
"ects_but": ects_but_valides,
|
|
||||||
"ues": deca.descr_ues_validation(line_sep=line_sep) if deca else "-",
|
"ues": deca.descr_ues_validation(line_sep=line_sep) if deca else "-",
|
||||||
"niveaux": (
|
"niveaux": deca.descr_niveaux_validation(line_sep=line_sep)
|
||||||
deca.descr_niveaux_validation(line_sep=line_sep) if deca else "-"
|
if deca
|
||||||
),
|
else "-",
|
||||||
"decision_but": deca.code_valide if deca else "",
|
"decision_but": deca.code_valide if deca else "",
|
||||||
"devenir": (
|
"devenir": ", ".join([f"S{i}" for i in deca.get_autorisations_passage()])
|
||||||
"Diplôme obtenu"
|
if deca
|
||||||
if has_diplome
|
else "",
|
||||||
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,
|
|
||||||
"prenom": etud.prenom_etat_civil or etud.prenom or "",
|
|
||||||
"etudid": etud.id,
|
|
||||||
"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.append(row)
|
||||||
|
|
||||||
rows.sort(key=lambda x: x["_nom_pv_order"])
|
rows.sort(key=lambda x: x["_nom_order"])
|
||||||
return rows, titles
|
return rows, titles
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -9,15 +9,15 @@
|
|||||||
from flask import g, url_for
|
from flask import g, url_for
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.but import jury_but, jury_dut120
|
from app.but import jury_but
|
||||||
from app.models import Identite, FormSemestre, ScolarNews, ValidationDUT120
|
from app.models import Identite, FormSemestre, ScolarNews
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
|
|
||||||
|
|
||||||
def formsemestre_validation_auto_but(
|
def formsemestre_validation_auto_but(
|
||||||
formsemestre: FormSemestre, only_adm: bool = True, dry_run=False, with_dut120=True
|
formsemestre: FormSemestre, only_adm: bool = True
|
||||||
) -> tuple[int, list[jury_but.DecisionsProposeesAnnee]]:
|
) -> int:
|
||||||
"""Calcul automatique des décisions de jury sur une "année" BUT.
|
"""Calcul automatique des décisions de jury sur une "année" BUT.
|
||||||
|
|
||||||
- N'enregistre jamais de décisions de l'année scolaire précédente, même
|
- N'enregistre jamais de décisions de l'année scolaire précédente, même
|
||||||
@ -27,27 +27,16 @@ def formsemestre_validation_auto_but(
|
|||||||
En revanche, si only_adm est faux, on enregistre la première décision proposée par ScoDoc
|
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)
|
(mode à n'utiliser que pour les tests unitaires vérifiant la saisie des jurys)
|
||||||
|
|
||||||
Enregistre aussi le DUT120.
|
Returns: nombre d'étudiants pour lesquels on a enregistré au moins un code.
|
||||||
|
|
||||||
Returns:
|
|
||||||
- En mode normal, (nombre d'étudiants pour lesquels on a enregistré au moins un code, []])
|
|
||||||
- En mode dry_run, (0, list[DecisionsProposeesAnnee])
|
|
||||||
"""
|
"""
|
||||||
if not formsemestre.formation.is_apc():
|
if not formsemestre.formation.is_apc():
|
||||||
raise ScoValueError("fonction réservée aux formations BUT")
|
raise ScoValueError("fonction réservée aux formations BUT")
|
||||||
nb_etud_modif = 0
|
nb_etud_modif = 0
|
||||||
decas = []
|
|
||||||
with sco_cache.DeferredSemCacheManager():
|
with sco_cache.DeferredSemCacheManager():
|
||||||
for etudid in formsemestre.etuds_inscriptions:
|
for etudid in formsemestre.etuds_inscriptions:
|
||||||
etud = Identite.get_etud(etudid)
|
etud = Identite.get_etud(etudid)
|
||||||
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
|
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
|
||||||
if not dry_run:
|
nb_etud_modif += deca.record_all(only_validantes=only_adm)
|
||||||
modified = deca.record_all(only_validantes=only_adm)
|
|
||||||
modified |= validation_dut120_auto(etud, formsemestre)
|
|
||||||
if modified:
|
|
||||||
nb_etud_modif += 1
|
|
||||||
else:
|
|
||||||
decas.append(deca)
|
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
ScolarNews.add(
|
ScolarNews.add(
|
||||||
@ -60,29 +49,4 @@ def formsemestre_validation_auto_but(
|
|||||||
formsemestre_id=formsemestre.id,
|
formsemestre_id=formsemestre.id,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return nb_etud_modif, decas
|
return nb_etud_modif
|
||||||
|
|
||||||
|
|
||||||
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,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -21,6 +21,8 @@ from app.but.jury_but import (
|
|||||||
DecisionsProposeesRCUE,
|
DecisionsProposeesRCUE,
|
||||||
DecisionsProposeesUE,
|
DecisionsProposeesUE,
|
||||||
)
|
)
|
||||||
|
from app.comp import res_sem
|
||||||
|
from app.comp.res_but import ResultatsSemestreBUT
|
||||||
from app.models import (
|
from app.models import (
|
||||||
ApcNiveau,
|
ApcNiveau,
|
||||||
FormSemestre,
|
FormSemestre,
|
||||||
@ -31,8 +33,11 @@ from app.models import (
|
|||||||
ScolarFormSemestreValidation,
|
ScolarFormSemestreValidation,
|
||||||
ScolarNews,
|
ScolarNews,
|
||||||
)
|
)
|
||||||
|
from app.models.config import ScoDocSiteConfig
|
||||||
|
from app.scodoc import html_sco_header
|
||||||
from app.scodoc import codes_cursus as sco_codes
|
from app.scodoc import codes_cursus as sco_codes
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
|
from app.scodoc import sco_preferences
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
@ -71,9 +76,9 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
|
|||||||
f"""
|
f"""
|
||||||
<div class="titre_niveaux">
|
<div class="titre_niveaux">
|
||||||
<b>Niveaux de compétences et unités d'enseignement du BUT{deca.annee_but}</b>
|
<b>Niveaux de compétences et unités d'enseignement du BUT{deca.annee_but}</b>
|
||||||
<a style="margin-left: 32px;" class="stdlink" target="_blank" rel="noopener noreferrer"
|
<a style="margin-left: 32px;" class="stdlink" target="_blank" rel="noopener noreferrer"
|
||||||
href={
|
href={
|
||||||
url_for("notes.validation_rcues", scodoc_dept=g.scodoc_dept,
|
url_for("notes.validation_rcues", scodoc_dept=g.scodoc_dept,
|
||||||
etudid=deca.etud.id,
|
etudid=deca.etud.id,
|
||||||
formsemestre_id=formsemestre_2.id if formsemestre_2 else formsemestre_1.id
|
formsemestre_id=formsemestre_2.id if formsemestre_2 else formsemestre_1.id
|
||||||
)
|
)
|
||||||
@ -92,7 +97,7 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
|
|||||||
<span class="avertissement_redoublement">{formsemestre_2.annee_scolaire_str()
|
<span class="avertissement_redoublement">{formsemestre_2.annee_scolaire_str()
|
||||||
if formsemestre_2 else ""}</span>
|
if formsemestre_2 else ""}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="titre" title="Décisions sur RCUEs enregistrées sur l'ensemble du cursus">RCUE</div>
|
<div class="titre">RCUE</div>
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
for dec_rcue in deca.get_decisions_rcues_annee():
|
for dec_rcue in deca.get_decisions_rcues_annee():
|
||||||
@ -104,32 +109,23 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
|
|||||||
</div>"""
|
</div>"""
|
||||||
)
|
)
|
||||||
ue_impair, ue_pair = rcue.ue_1, rcue.ue_2
|
ue_impair, ue_pair = rcue.ue_1, rcue.ue_2
|
||||||
# Les UEs à afficher : on regarde si read only et si dispense (non ré-inscription à l'UE)
|
# Les UEs à afficher,
|
||||||
# tuples (UniteEns, read_only, dispense)
|
# qui
|
||||||
ues_ro_dispense = [
|
ues_ro = [
|
||||||
(
|
(
|
||||||
ue_impair,
|
ue_impair,
|
||||||
rcue.ue_cur_impair is None,
|
rcue.ue_cur_impair is None,
|
||||||
deca.res_impair
|
|
||||||
and ue_impair
|
|
||||||
and (deca.etud.id, ue_impair.id) in deca.res_impair.dispense_ues,
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
ue_pair,
|
ue_pair,
|
||||||
rcue.ue_cur_pair is None,
|
rcue.ue_cur_pair is None,
|
||||||
deca.res_pair
|
|
||||||
and ue_pair
|
|
||||||
and (deca.etud.id, ue_pair.id) in deca.res_pair.dispense_ues,
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
# Ordonne selon les dates des 2 semestres considérés:
|
# Ordonne selon les dates des 2 semestres considérés:
|
||||||
if reverse_semestre:
|
if reverse_semestre:
|
||||||
ues_ro_dispense[0], ues_ro_dispense[1] = (
|
ues_ro[0], ues_ro[1] = ues_ro[1], ues_ro[0]
|
||||||
ues_ro_dispense[1],
|
|
||||||
ues_ro_dispense[0],
|
|
||||||
)
|
|
||||||
# Colonnes d'UE:
|
# Colonnes d'UE:
|
||||||
for ue, ue_read_only, ue_dispense in ues_ro_dispense:
|
for ue, ue_read_only in ues_ro:
|
||||||
if ue:
|
if ue:
|
||||||
H.append(
|
H.append(
|
||||||
_gen_but_niveau_ue(
|
_gen_but_niveau_ue(
|
||||||
@ -138,7 +134,6 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
|
|||||||
disabled=read_only or ue_read_only,
|
disabled=read_only or ue_read_only,
|
||||||
annee_prec=ue_read_only,
|
annee_prec=ue_read_only,
|
||||||
niveau_id=ue.niveau_competence.id,
|
niveau_id=ue.niveau_competence.id,
|
||||||
ue_dispense=ue_dispense,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -177,7 +172,7 @@ def _gen_but_select(
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
return f"""<select required name="{name}"
|
return f"""<select required name="{name}"
|
||||||
class="but_code {klass}"
|
class="but_code {klass}"
|
||||||
data-orig_code="{code_valide or (codes[0] if codes else '')}"
|
data-orig_code="{code_valide or (codes[0] if codes else '')}"
|
||||||
data-orig_recorded="{code_valide or ''}"
|
data-orig_recorded="{code_valide or ''}"
|
||||||
onchange="change_menu_code(this);"
|
onchange="change_menu_code(this);"
|
||||||
@ -193,30 +188,21 @@ def _gen_but_niveau_ue(
|
|||||||
disabled: bool = False,
|
disabled: bool = False,
|
||||||
annee_prec: bool = False,
|
annee_prec: bool = False,
|
||||||
niveau_id: int = None,
|
niveau_id: int = None,
|
||||||
ue_dispense: bool = False,
|
|
||||||
) -> str:
|
) -> str:
|
||||||
if dec_ue.ue_status and dec_ue.ue_status["is_capitalized"]:
|
if dec_ue.ue_status and dec_ue.ue_status["is_capitalized"]:
|
||||||
moy_ue_str = f"""<span class="ue_cap">{
|
moy_ue_str = f"""<span class="ue_cap">{
|
||||||
scu.fmt_note(dec_ue.moy_ue_with_cap)}</span>"""
|
scu.fmt_note(dec_ue.moy_ue_with_cap)}</span>"""
|
||||||
|
|
||||||
if ue_dispense:
|
|
||||||
etat_en_cours = """Non (ré)inscrit à cette UE"""
|
|
||||||
else:
|
|
||||||
etat_en_cours = f"""UE en cours
|
|
||||||
{ "sans notes" if np.isnan(dec_ue.moy_ue)
|
|
||||||
else
|
|
||||||
("avec moyenne <b>" + scu.fmt_note(dec_ue.moy_ue) + "</b>")
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
scoplement = f"""<div class="scoplement">
|
scoplement = f"""<div class="scoplement">
|
||||||
<div>
|
<div>
|
||||||
<b>UE {ue.acronyme} capitalisée </b>
|
<b>UE {ue.acronyme} capitalisée </b>
|
||||||
<span>le {dec_ue.ue_status["event_date"].strftime(scu.DATE_FMT)}
|
<span>le {dec_ue.ue_status["event_date"].strftime("%d/%m/%Y")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>UE en cours
|
||||||
{ etat_en_cours }
|
{ "sans notes" if np.isnan(dec_ue.moy_ue)
|
||||||
|
else
|
||||||
|
("avec moyenne <b>" + scu.fmt_note(dec_ue.moy_ue) + "</b>")
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
@ -228,7 +214,7 @@ def _gen_but_niveau_ue(
|
|||||||
<div>
|
<div>
|
||||||
<b>UE {ue.acronyme} antérieure </b>
|
<b>UE {ue.acronyme} antérieure </b>
|
||||||
<span>validée {dec_ue.validation.code}
|
<span>validée {dec_ue.validation.code}
|
||||||
le {dec_ue.validation.event_date.strftime(scu.DATE_FMT)}
|
le {dec_ue.validation.event_date.strftime("%d/%m/%Y")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>Non reprise dans l'année en cours</div>
|
<div>Non reprise dans l'année en cours</div>
|
||||||
@ -246,7 +232,9 @@ def _gen_but_niveau_ue(
|
|||||||
moy_ue_str = f"""<span>{scu.fmt_note(dec_ue.moy_ue)}</span>"""
|
moy_ue_str = f"""<span>{scu.fmt_note(dec_ue.moy_ue)}</span>"""
|
||||||
if dec_ue.code_valide:
|
if dec_ue.code_valide:
|
||||||
date_str = (
|
date_str = (
|
||||||
f"""enregistré le {dec_ue.validation.event_date.strftime(scu.DATEATIME_FMT)}"""
|
f"""enregistré le {dec_ue.validation.event_date.strftime("%d/%m/%Y")}
|
||||||
|
à {dec_ue.validation.event_date.strftime("%Hh%M")}
|
||||||
|
"""
|
||||||
if dec_ue.validation and dec_ue.validation.event_date
|
if dec_ue.validation and dec_ue.validation.event_date
|
||||||
else ""
|
else ""
|
||||||
)
|
)
|
||||||
@ -256,13 +244,7 @@ def _gen_but_niveau_ue(
|
|||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
else:
|
else:
|
||||||
if dec_ue.ue_status and dec_ue.ue_status["was_capitalized"]:
|
scoplement = ""
|
||||||
scoplement = """<div class="scoplement">
|
|
||||||
UE déjà capitalisée avec résultat moins favorable.
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
else:
|
|
||||||
scoplement = ""
|
|
||||||
|
|
||||||
ue_class = "" # 'recorded' if dec_ue.code_valide is not None else ''
|
ue_class = "" # 'recorded' if dec_ue.code_valide is not None else ''
|
||||||
if dec_ue.code_valide is not None and dec_ue.codes:
|
if dec_ue.code_valide is not None and dec_ue.codes:
|
||||||
@ -274,20 +256,20 @@ def _gen_but_niveau_ue(
|
|||||||
return f"""<div class="but_niveau_ue {ue_class}
|
return f"""<div class="but_niveau_ue {ue_class}
|
||||||
{'annee_prec' if annee_prec else ''}
|
{'annee_prec' if annee_prec else ''}
|
||||||
">
|
">
|
||||||
<div title="{ue.titre or ''}">{ue.acronyme}</div>
|
<div title="{ue.titre}">{ue.acronyme}</div>
|
||||||
<div class="but_note with_scoplement">
|
<div class="but_note with_scoplement">
|
||||||
<div>{moy_ue_str}</div>
|
<div>{moy_ue_str}</div>
|
||||||
{scoplement}
|
{scoplement}
|
||||||
</div>
|
</div>
|
||||||
<div class="but_code">{
|
<div class="but_code">{
|
||||||
_gen_but_select("code_ue_"+str(ue.id),
|
_gen_but_select("code_ue_"+str(ue.id),
|
||||||
dec_ue.codes,
|
dec_ue.codes,
|
||||||
dec_ue.code_valide,
|
dec_ue.code_valide,
|
||||||
disabled=disabled,
|
disabled=disabled,
|
||||||
klass=f"code_ue ue_rcue_{niveau_id}" if not disabled else ""
|
klass=f"code_ue ue_rcue_{niveau_id}" if not disabled else ""
|
||||||
)
|
)
|
||||||
}</div>
|
}</div>
|
||||||
|
|
||||||
</div>"""
|
</div>"""
|
||||||
|
|
||||||
|
|
||||||
@ -349,6 +331,250 @@ def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def jury_but_semestriel(
|
||||||
|
formsemestre: FormSemestre,
|
||||||
|
etud: Identite,
|
||||||
|
read_only: bool,
|
||||||
|
navigation_div: str = "",
|
||||||
|
) -> str:
|
||||||
|
"""Page: formulaire saisie décision d'UE d'un semestre BUT isolé (pas jury annuel)."""
|
||||||
|
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
|
||||||
|
parcour, ues = jury_but.list_ue_parcour_etud(formsemestre, etud, res)
|
||||||
|
inscription_etat = etud.inscription_etat(formsemestre.id)
|
||||||
|
semestre_terminal = (
|
||||||
|
formsemestre.semestre_id >= formsemestre.formation.get_cursus().NB_SEM
|
||||||
|
)
|
||||||
|
autorisations_passage = ScolarAutorisationInscription.query.filter_by(
|
||||||
|
etudid=etud.id,
|
||||||
|
origin_formsemestre_id=formsemestre.id,
|
||||||
|
).all()
|
||||||
|
# Par défaut: autorisé à passer dans le semestre suivant si sem. impair,
|
||||||
|
# ou si décision déjà enregistrée:
|
||||||
|
est_autorise_a_passer = (formsemestre.semestre_id % 2) or (
|
||||||
|
formsemestre.semestre_id + 1
|
||||||
|
) in (a.semestre_id for a in autorisations_passage)
|
||||||
|
decisions_ues = {
|
||||||
|
ue.id: DecisionsProposeesUE(etud, formsemestre, ue, inscription_etat)
|
||||||
|
for ue in ues
|
||||||
|
}
|
||||||
|
for dec_ue in decisions_ues.values():
|
||||||
|
dec_ue.compute_codes()
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
if not read_only:
|
||||||
|
for key in request.form:
|
||||||
|
code = request.form[key]
|
||||||
|
# Codes d'UE
|
||||||
|
code_match = re.match(r"^code_ue_(\d+)$", key)
|
||||||
|
if code_match:
|
||||||
|
ue_id = int(code_match.group(1))
|
||||||
|
dec_ue = decisions_ues.get(ue_id)
|
||||||
|
if not dec_ue:
|
||||||
|
raise ScoValueError(f"UE invalide ue_id={ue_id}")
|
||||||
|
dec_ue.record(code)
|
||||||
|
db.session.commit()
|
||||||
|
flash("codes enregistrés")
|
||||||
|
if not semestre_terminal:
|
||||||
|
if request.form.get("autorisation_passage"):
|
||||||
|
if not formsemestre.semestre_id + 1 in (
|
||||||
|
a.semestre_id for a in autorisations_passage
|
||||||
|
):
|
||||||
|
ScolarAutorisationInscription.delete_autorisation_etud(
|
||||||
|
etud.id, formsemestre.id
|
||||||
|
)
|
||||||
|
ScolarAutorisationInscription.autorise_etud(
|
||||||
|
etud.id,
|
||||||
|
formsemestre.formation.formation_code,
|
||||||
|
formsemestre.id,
|
||||||
|
formsemestre.semestre_id + 1,
|
||||||
|
)
|
||||||
|
db.session.commit()
|
||||||
|
flash(
|
||||||
|
f"""autorisation de passage en S{formsemestre.semestre_id + 1
|
||||||
|
} enregistrée"""
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if est_autorise_a_passer:
|
||||||
|
ScolarAutorisationInscription.delete_autorisation_etud(
|
||||||
|
etud.id, formsemestre.id
|
||||||
|
)
|
||||||
|
db.session.commit()
|
||||||
|
flash(
|
||||||
|
f"autorisation de passage en S{formsemestre.semestre_id + 1} annulée"
|
||||||
|
)
|
||||||
|
ScolarNews.add(
|
||||||
|
typ=ScolarNews.NEWS_JURY,
|
||||||
|
obj=formsemestre.id,
|
||||||
|
text=f"""Saisie décision jury dans {formsemestre.html_link_status()}""",
|
||||||
|
url=url_for(
|
||||||
|
"notes.formsemestre_status",
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
formsemestre_id=formsemestre.id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return flask.redirect(
|
||||||
|
url_for(
|
||||||
|
"notes.formsemestre_validation_but",
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
formsemestre_id=formsemestre.id,
|
||||||
|
etudid=etud.id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# GET
|
||||||
|
if formsemestre.semestre_id % 2 == 0:
|
||||||
|
warning = f"""<div class="warning">
|
||||||
|
Cet étudiant de S{formsemestre.semestre_id} ne peut pas passer
|
||||||
|
en jury BUT annuel car il lui manque le semestre précédent.
|
||||||
|
</div>"""
|
||||||
|
else:
|
||||||
|
warning = ""
|
||||||
|
H = [
|
||||||
|
html_sco_header.sco_header(
|
||||||
|
page_title=f"Validation BUT S{formsemestre.semestre_id}",
|
||||||
|
formsemestre_id=formsemestre.id,
|
||||||
|
etudid=etud.id,
|
||||||
|
cssstyles=("css/jury_but.css",),
|
||||||
|
javascripts=("js/jury_but.js",),
|
||||||
|
),
|
||||||
|
f"""
|
||||||
|
<div class="jury_but">
|
||||||
|
<div>
|
||||||
|
<div class="bull_head">
|
||||||
|
<div>
|
||||||
|
<div class="titre_parcours">Jury BUT S{formsemestre.id}
|
||||||
|
- Parcours {(parcour.libelle if parcour else False) or "non spécifié"}
|
||||||
|
</div>
|
||||||
|
<div class="nom_etud">{etud.nomprenom}</div>
|
||||||
|
</div>
|
||||||
|
<div class="bull_photo"><a href="{
|
||||||
|
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
|
||||||
|
}">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3>Jury sur un semestre BUT isolé (ne concerne que les UEs)</h3>
|
||||||
|
{warning}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post" class="jury_but_box" id="jury_but">
|
||||||
|
""",
|
||||||
|
]
|
||||||
|
|
||||||
|
erase_span = ""
|
||||||
|
if not read_only:
|
||||||
|
# Requête toutes les validations (pas seulement celles du deca courant),
|
||||||
|
# au cas où: changement d'architecture, saisie en mode classique, ...
|
||||||
|
validations = ScolarFormSemestreValidation.query.filter_by(
|
||||||
|
etudid=etud.id, formsemestre_id=formsemestre.id
|
||||||
|
).all()
|
||||||
|
if validations:
|
||||||
|
erase_span = f"""<a href="{
|
||||||
|
url_for("notes.formsemestre_jury_but_erase",
|
||||||
|
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id,
|
||||||
|
etudid=etud.id, only_one_sem=1)
|
||||||
|
}" class="stdlink">effacer les décisions enregistrées</a>"""
|
||||||
|
else:
|
||||||
|
erase_span = (
|
||||||
|
"Cet étudiant n'a aucune décision enregistrée pour ce semestre."
|
||||||
|
)
|
||||||
|
|
||||||
|
H.append(
|
||||||
|
f"""
|
||||||
|
<div class="but_section_annee">
|
||||||
|
</div>
|
||||||
|
<div><b>Unités d'enseignement de S{formsemestre.semestre_id}:</b></div>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
if not ues:
|
||||||
|
H.append(
|
||||||
|
"""<div class="warning">Aucune UE ! Vérifiez votre programme de
|
||||||
|
formation, et l'association UEs / Niveaux de compétences</div>"""
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
H.append(
|
||||||
|
"""
|
||||||
|
<div class="but_annee">
|
||||||
|
<div class="titre"></div>
|
||||||
|
<div class="titre"></div>
|
||||||
|
<div class="titre"></div>
|
||||||
|
<div class="titre"></div>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
for ue in ues:
|
||||||
|
dec_ue = decisions_ues[ue.id]
|
||||||
|
H.append("""<div class="but_niveau_titre"><div></div></div>""")
|
||||||
|
H.append(
|
||||||
|
_gen_but_niveau_ue(
|
||||||
|
ue,
|
||||||
|
dec_ue,
|
||||||
|
disabled=read_only,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
H.append(
|
||||||
|
"""<div style=""></div>
|
||||||
|
<div class=""></div>"""
|
||||||
|
)
|
||||||
|
H.append("</div>") # but_annee
|
||||||
|
|
||||||
|
div_autorisations_passage = (
|
||||||
|
f"""
|
||||||
|
<div class="but_autorisations_passage">
|
||||||
|
<span>Autorisé à passer en :</span>
|
||||||
|
{ ", ".join( ["S" + str(a.semestre_id or '') for a in autorisations_passage ] )}
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
if autorisations_passage
|
||||||
|
else """<div class="but_autorisations_passage but_explanation">pas d'autorisations de passage enregistrées.</div>"""
|
||||||
|
)
|
||||||
|
H.append(div_autorisations_passage)
|
||||||
|
|
||||||
|
if read_only:
|
||||||
|
H.append(
|
||||||
|
f"""<div class="but_explanation">
|
||||||
|
{"Vous n'avez pas la permission de modifier ces décisions."
|
||||||
|
if formsemestre.etat
|
||||||
|
else "Semestre verrouillé."}
|
||||||
|
Les champs entourés en vert sont enregistrés.
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if formsemestre.semestre_id < formsemestre.formation.get_cursus().NB_SEM:
|
||||||
|
H.append(
|
||||||
|
f"""
|
||||||
|
<div class="but_settings">
|
||||||
|
<input type="checkbox" name="autorisation_passage" value="1" {
|
||||||
|
"checked" if est_autorise_a_passer else ""}>
|
||||||
|
<em>autoriser à passer dans le semestre S{formsemestre.semestre_id+1}</em>
|
||||||
|
</input>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
H.append("""<div class="help">dernier semestre de la formation.</div>""")
|
||||||
|
H.append(
|
||||||
|
f"""
|
||||||
|
<div class="but_buttons">
|
||||||
|
<span><input type="submit" value="Enregistrer ces décisions"></span>
|
||||||
|
<span>{erase_span}</span>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
H.append(navigation_div)
|
||||||
|
H.append("</div>")
|
||||||
|
H.append(
|
||||||
|
render_template(
|
||||||
|
"but/documentation_codes_jury.j2",
|
||||||
|
nom_univ=f"""Export {sco_preferences.get_preference("InstituteName")
|
||||||
|
or sco_preferences.get_preference("UnivName")
|
||||||
|
or "Apogée"}""",
|
||||||
|
codes=ScoDocSiteConfig.get_codes_apo_dict(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return "\n".join(H)
|
||||||
|
|
||||||
|
|
||||||
# -------------
|
# -------------
|
||||||
def infos_fiche_etud_html(etudid: int) -> str:
|
def infos_fiche_etud_html(etudid: int) -> str:
|
||||||
"""Section html pour fiche etudiant
|
"""Section html pour fiche etudiant
|
||||||
|
@ -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,
|
|
||||||
)
|
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -10,25 +10,23 @@ Non spécifique au BUT.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import render_template
|
from flask import render_template
|
||||||
from flask_login import current_user
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from app.models import (
|
from app.models import (
|
||||||
ApcValidationAnnee,
|
ApcValidationAnnee,
|
||||||
ApcValidationRCUE,
|
ApcValidationRCUE,
|
||||||
Identite,
|
Identite,
|
||||||
|
UniteEns,
|
||||||
ScolarAutorisationInscription,
|
ScolarAutorisationInscription,
|
||||||
ScolarFormSemestreValidation,
|
ScolarFormSemestreValidation,
|
||||||
UniteEns,
|
|
||||||
ValidationDUT120,
|
|
||||||
)
|
)
|
||||||
from app.scodoc.sco_permissions import Permission
|
|
||||||
from app.views import ScoData
|
from app.views import ScoData
|
||||||
|
|
||||||
|
|
||||||
def jury_delete_manual(etud: Identite):
|
def jury_delete_manual(etud: Identite):
|
||||||
"""Vue présentant *toutes* les décisions de jury concernant cet étudiant
|
"""Vue (réservée au chef de dept.)
|
||||||
et permettant (si permission) de les supprimer une à une.
|
présentant *toutes* les décisions de jury concernant cet étudiant
|
||||||
|
et permettant de les supprimer une à une.
|
||||||
"""
|
"""
|
||||||
sem_vals = ScolarFormSemestreValidation.query.filter_by(
|
sem_vals = ScolarFormSemestreValidation.query.filter_by(
|
||||||
etudid=etud.id, ue_id=None
|
etudid=etud.id, ue_id=None
|
||||||
@ -62,12 +60,8 @@ def jury_delete_manual(etud: Identite):
|
|||||||
sem_vals=sem_vals,
|
sem_vals=sem_vals,
|
||||||
ue_vals=ue_vals,
|
ue_vals=ue_vals,
|
||||||
autorisations=autorisations,
|
autorisations=autorisations,
|
||||||
dut120_vals=ValidationDUT120.query.filter_by(etudid=etud.id).order_by(
|
|
||||||
ValidationDUT120.date
|
|
||||||
),
|
|
||||||
rcue_vals=rcue_vals,
|
rcue_vals=rcue_vals,
|
||||||
annee_but_vals=annee_but_vals,
|
annee_but_vals=annee_but_vals,
|
||||||
sco=ScoData(),
|
sco=ScoData(),
|
||||||
title=f"Toutes les décisions de jury enregistrées pour {etud.html_link_fiche()}",
|
title=f"Toutes les décisions de jury enregistrées pour {etud.html_link_fiche()}",
|
||||||
read_only=not current_user.has_permission(Permission.EtudInscrit),
|
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -75,7 +75,7 @@ class RegroupementCoherentUE:
|
|||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
# Autres validations pour les UEs paire/impaire
|
# Autres validations pour l'UE paire
|
||||||
self.validation_ue_best_pair = best_autre_ue_validation(
|
self.validation_ue_best_pair = best_autre_ue_validation(
|
||||||
etud.id,
|
etud.id,
|
||||||
niveau.id,
|
niveau.id,
|
||||||
@ -101,24 +101,14 @@ class RegroupementCoherentUE:
|
|||||||
"résultats formsemestre de l'UE si elle est courante, None sinon"
|
"résultats formsemestre de l'UE si elle est courante, None sinon"
|
||||||
self.ue_status_impair = None
|
self.ue_status_impair = None
|
||||||
if self.ue_cur_impair:
|
if self.ue_cur_impair:
|
||||||
# UE courante
|
|
||||||
ue_status = res_impair.get_etud_ue_status(etud.id, self.ue_cur_impair.id)
|
ue_status = res_impair.get_etud_ue_status(etud.id, self.ue_cur_impair.id)
|
||||||
self.moy_ue_1 = ue_status["moy"] if ue_status else None # avec capitalisée
|
self.moy_ue_1 = ue_status["moy"] if ue_status else None # avec capitalisée
|
||||||
self.ue_1 = self.ue_cur_impair
|
self.ue_1 = self.ue_cur_impair
|
||||||
self.res_impair = res_impair
|
self.res_impair = res_impair
|
||||||
self.ue_status_impair = ue_status
|
self.ue_status_impair = ue_status
|
||||||
elif self.validation_ue_best_impair:
|
elif self.validation_ue_best_impair:
|
||||||
# UE capitalisée
|
|
||||||
self.moy_ue_1 = self.validation_ue_best_impair.moy_ue
|
self.moy_ue_1 = self.validation_ue_best_impair.moy_ue
|
||||||
self.ue_1 = self.validation_ue_best_impair.ue
|
self.ue_1 = self.validation_ue_best_impair.ue
|
||||||
if (
|
|
||||||
res_impair
|
|
||||||
and self.validation_ue_best_impair
|
|
||||||
and self.validation_ue_best_impair.ue
|
|
||||||
):
|
|
||||||
self.ue_status_impair = res_impair.get_etud_ue_status(
|
|
||||||
etud.id, self.validation_ue_best_impair.ue.id
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
self.moy_ue_1, self.ue_1 = None, None
|
self.moy_ue_1, self.ue_1 = None, None
|
||||||
self.moy_ue_1_val = self.moy_ue_1 if self.moy_ue_1 is not None else 0.0
|
self.moy_ue_1_val = self.moy_ue_1 if self.moy_ue_1 is not None else 0.0
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# 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
|
from flask import render_template
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from app import log
|
from app import log
|
||||||
from app.but import cursus_but
|
from app.but import cursus_but
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -30,9 +30,7 @@ class StatsMoyenne:
|
|||||||
self.max = np.nanmax(vals)
|
self.max = np.nanmax(vals)
|
||||||
self.size = len(vals)
|
self.size = len(vals)
|
||||||
self.nb_vals = self.size - np.count_nonzero(np.isnan(vals))
|
self.nb_vals = self.size - np.count_nonzero(np.isnan(vals))
|
||||||
except (
|
except TypeError: # que des NaN dans un array d'objets, ou ce genre de choses exotiques...
|
||||||
TypeError
|
|
||||||
): # que des NaN dans un array d'objets, ou ce genre de choses exotiques...
|
|
||||||
self.moy = self.min = self.max = self.size = self.nb_vals = 0
|
self.moy = self.min = self.max = self.size = self.nb_vals = 0
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -667,12 +667,10 @@ class BonusCalais(BonusSportAdditif):
|
|||||||
sur 20 obtenus dans chacune des matières optionnelles sont cumulés
|
sur 20 obtenus dans chacune des matières optionnelles sont cumulés
|
||||||
dans la limite de 10 points. 6% de ces points cumulés s'ajoutent :
|
dans la limite de 10 points. 6% de ces points cumulés s'ajoutent :
|
||||||
<ul>
|
<ul>
|
||||||
<li><b>en BUT</b> à la moyenne de chaque UE;
|
<li><b>en DUT</b> à la moyenne générale du semestre déjà obtenue par l'étudiant.
|
||||||
</li>
|
</li>
|
||||||
<li><b>en DUT</b> à la moyenne générale du semestre déjà obtenue par l'étudiant;
|
<li><b>en BUT et LP</b> à la moyenne des UE dont l'acronyme fini par <b>BS</b>
|
||||||
</li>
|
(ex : UE2.1BS, UE32BS)
|
||||||
<li><b>en LP</b>, et en BUT avant 2023-2024, à la moyenne de chaque UE dont
|
|
||||||
l'acronyme termine par <b>BS</b> (comme UE2.1BS, UE32BS).
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
"""
|
"""
|
||||||
@ -694,17 +692,12 @@ class BonusCalais(BonusSportAdditif):
|
|||||||
else:
|
else:
|
||||||
self.classic_use_bonus_ues = True # pour les LP
|
self.classic_use_bonus_ues = True # pour les LP
|
||||||
super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
|
super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
|
||||||
if (
|
ues = self.formsemestre.get_ues(with_sport=False)
|
||||||
self.formsemestre.annee_scolaire() < 2023
|
ues_sans_bs = [
|
||||||
or not self.formsemestre.formation.is_apc()
|
ue for ue in ues if ue.acronyme[-2:].upper() != "BS"
|
||||||
):
|
] # les 2 derniers cars forcés en majus
|
||||||
# LP et anciens semestres: ne s'applique qu'aux UE dont l'acronyme termine par BS
|
for ue in ues_sans_bs:
|
||||||
ues = self.formsemestre.get_ues(with_sport=False)
|
self.bonus_ues[ue.id] = 0.0
|
||||||
ues_sans_bs = [
|
|
||||||
ue for ue in ues if ue.acronyme[-2:].upper() != "BS"
|
|
||||||
] # les 2 derniers cars forcés en majus
|
|
||||||
for ue in ues_sans_bs:
|
|
||||||
self.bonus_ues[ue.id] = 0.0
|
|
||||||
|
|
||||||
|
|
||||||
class BonusColmar(BonusSportAdditif):
|
class BonusColmar(BonusSportAdditif):
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -27,7 +27,6 @@
|
|||||||
|
|
||||||
"""caches pour tables APC
|
"""caches pour tables APC
|
||||||
"""
|
"""
|
||||||
from flask import g
|
|
||||||
|
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
|
|
||||||
@ -48,27 +47,3 @@ class EvaluationsPoidsCache(sco_cache.ScoDocCache):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
prefix = "EPC"
|
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)
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -23,7 +23,6 @@ from app.models import (
|
|||||||
)
|
)
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
from app.scodoc import codes_cursus
|
from app.scodoc import codes_cursus
|
||||||
from app.scodoc import sco_utils as scu
|
|
||||||
|
|
||||||
|
|
||||||
class ValidationsSemestre(ResultatsCache):
|
class ValidationsSemestre(ResultatsCache):
|
||||||
@ -39,7 +38,7 @@ class ValidationsSemestre(ResultatsCache):
|
|||||||
super().__init__(formsemestre, sco_cache.ValidationsSemestreCache)
|
super().__init__(formsemestre, sco_cache.ValidationsSemestreCache)
|
||||||
|
|
||||||
self.decisions_jury = {}
|
self.decisions_jury = {}
|
||||||
"""Décisions prises dans ce semestre:
|
"""Décisions prises dans ce semestre:
|
||||||
{ etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }}"""
|
{ etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }}"""
|
||||||
self.decisions_jury_ues = {}
|
self.decisions_jury_ues = {}
|
||||||
"""Décisions sur des UEs dans ce semestre:
|
"""Décisions sur des UEs dans ce semestre:
|
||||||
@ -85,7 +84,7 @@ class ValidationsSemestre(ResultatsCache):
|
|||||||
"code": decision.code,
|
"code": decision.code,
|
||||||
"assidu": decision.assidu,
|
"assidu": decision.assidu,
|
||||||
"compense_formsemestre_id": decision.compense_formsemestre_id,
|
"compense_formsemestre_id": decision.compense_formsemestre_id,
|
||||||
"event_date": decision.event_date.strftime(scu.DATE_FMT),
|
"event_date": decision.event_date.strftime("%d/%m/%Y"),
|
||||||
}
|
}
|
||||||
self.decisions_jury = decisions_jury
|
self.decisions_jury = decisions_jury
|
||||||
|
|
||||||
@ -108,7 +107,7 @@ class ValidationsSemestre(ResultatsCache):
|
|||||||
decisions_jury_ues[decision.etudid][decision.ue.id] = {
|
decisions_jury_ues[decision.etudid][decision.ue.id] = {
|
||||||
"code": decision.code,
|
"code": decision.code,
|
||||||
"ects": ects, # 0. si UE non validée
|
"ects": ects, # 0. si UE non validée
|
||||||
"event_date": decision.event_date.strftime(scu.DATE_FMT),
|
"event_date": decision.event_date.strftime("%d/%m/%Y"),
|
||||||
}
|
}
|
||||||
|
|
||||||
self.decisions_jury_ues = decisions_jury_ues
|
self.decisions_jury_ues = decisions_jury_ues
|
||||||
@ -146,11 +145,11 @@ def formsemestre_get_ue_capitalisees(formsemestre: FormSemestre) -> pd.DataFrame
|
|||||||
query = sa.text(
|
query = sa.text(
|
||||||
"""
|
"""
|
||||||
SELECT DISTINCT SFV.*, ue.ue_code
|
SELECT DISTINCT SFV.*, ue.ue_code
|
||||||
FROM
|
FROM
|
||||||
notes_ue ue,
|
notes_ue ue,
|
||||||
notes_formations nf,
|
notes_formations nf,
|
||||||
notes_formations nf2,
|
notes_formations nf2,
|
||||||
scolar_formsemestre_validation SFV,
|
scolar_formsemestre_validation SFV,
|
||||||
notes_formsemestre sem,
|
notes_formsemestre sem,
|
||||||
notes_formsemestre_inscription ins
|
notes_formsemestre_inscription ins
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -35,6 +35,7 @@ moyenne générale d'une UE.
|
|||||||
"""
|
"""
|
||||||
import dataclasses
|
import dataclasses
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
@ -45,6 +46,7 @@ from app.models import Evaluation, EvaluationUEPoids, ModuleImpl
|
|||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
from app.scodoc.codes_cursus import UE_SPORT
|
from app.scodoc.codes_cursus import UE_SPORT
|
||||||
|
from app.scodoc.sco_exceptions import ScoBugCatcher
|
||||||
from app.scodoc.sco_utils import ModuleType
|
from app.scodoc.sco_utils import ModuleType
|
||||||
|
|
||||||
|
|
||||||
@ -54,7 +56,6 @@ class EvaluationEtat:
|
|||||||
|
|
||||||
evaluation_id: int
|
evaluation_id: int
|
||||||
nb_attente: int
|
nb_attente: int
|
||||||
nb_notes: int # nb notes d'étudiants inscrits au semestre et au modimpl
|
|
||||||
is_complete: bool
|
is_complete: bool
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
@ -71,15 +72,7 @@ class ModuleImplResults:
|
|||||||
les caches sont gérés par ResultatsSemestre.
|
les caches sont gérés par ResultatsSemestre.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, moduleimpl: ModuleImpl):
|
||||||
self, moduleimpl: ModuleImpl, etudids: list[int], etudids_actifs: set[int]
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Args:
|
|
||||||
- etudids : liste des etudids, qui donne l'index du dataframe
|
|
||||||
(doit être tous les étudiants inscrits au semestre incluant les DEM et DEF)
|
|
||||||
- etudids_actifs l'ensemble des étudiants inscrits au semestre, non DEM/DEF.
|
|
||||||
"""
|
|
||||||
self.moduleimpl_id = moduleimpl.id
|
self.moduleimpl_id = moduleimpl.id
|
||||||
self.module_id = moduleimpl.module.id
|
self.module_id = moduleimpl.module.id
|
||||||
self.etudids = None
|
self.etudids = None
|
||||||
@ -112,23 +105,14 @@ class ModuleImplResults:
|
|||||||
"""
|
"""
|
||||||
self.evals_etudids_sans_note = {}
|
self.evals_etudids_sans_note = {}
|
||||||
"""dict: evaluation_id : set des etudids non notés dans cette eval, sans les démissions."""
|
"""dict: evaluation_id : set des etudids non notés dans cette eval, sans les démissions."""
|
||||||
self.evals_type = {}
|
self.load_notes()
|
||||||
"""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)
|
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"""
|
"""1 bool par etud, indique si sa moyenne de module vient de la session2"""
|
||||||
self.etuds_use_rattrapage = pd.Series(False, index=self.evals_notes.index)
|
self.etuds_use_rattrapage = pd.Series(False, index=self.evals_notes.index)
|
||||||
"""1 bool par etud, indique si sa moyenne de module utilise la note de rattrapage"""
|
"""1 bool par etud, indique si sa moyenne de module utilise la note de rattrapage"""
|
||||||
|
|
||||||
def load_notes(
|
def load_notes(self): # ré-écriture de df_load_modimpl_notes
|
||||||
self, etudids: list[int], etudids_actifs: set[int]
|
|
||||||
): # ré-écriture de df_load_modimpl_notes
|
|
||||||
"""Charge toutes les notes de toutes les évaluations du module.
|
"""Charge toutes les notes de toutes les évaluations du module.
|
||||||
Args:
|
|
||||||
- etudids : liste des etudids, qui donne l'index du dataframe
|
|
||||||
(doit être tous les étudiants inscrits au semestre incluant les DEM et DEF)
|
|
||||||
- etudids_actifs l'ensemble des étudiants inscrits au semestre, non DEM/DEF.
|
|
||||||
|
|
||||||
Dataframe evals_notes
|
Dataframe evals_notes
|
||||||
colonnes: le nom de la colonne est l'evaluation_id (int)
|
colonnes: le nom de la colonne est l'evaluation_id (int)
|
||||||
index (lignes): etudid (int)
|
index (lignes): etudid (int)
|
||||||
@ -151,12 +135,12 @@ class ModuleImplResults:
|
|||||||
qui ont des notes ATT.
|
qui ont des notes ATT.
|
||||||
"""
|
"""
|
||||||
moduleimpl = db.session.get(ModuleImpl, self.moduleimpl_id)
|
moduleimpl = db.session.get(ModuleImpl, self.moduleimpl_id)
|
||||||
self.etudids = etudids
|
self.etudids = self._etudids()
|
||||||
|
|
||||||
# --- Calcul nombre d'inscrits pour déterminer les évaluations "completes":
|
# --- Calcul nombre d'inscrits pour déterminer les évaluations "completes":
|
||||||
# on prend les inscrits au module ET au semestre (donc sans démissionnaires)
|
# on prend les inscrits au module ET au semestre (donc sans démissionnaires)
|
||||||
inscrits_module = {ins.etud.id for ins in moduleimpl.inscriptions}.intersection(
|
inscrits_module = {ins.etud.id for ins in moduleimpl.inscriptions}.intersection(
|
||||||
etudids_actifs
|
moduleimpl.formsemestre.etudids_actifs
|
||||||
)
|
)
|
||||||
self.nb_inscrits_module = len(inscrits_module)
|
self.nb_inscrits_module = len(inscrits_module)
|
||||||
|
|
||||||
@ -164,24 +148,19 @@ class ModuleImplResults:
|
|||||||
evals_notes = pd.DataFrame(index=self.etudids, dtype=float)
|
evals_notes = pd.DataFrame(index=self.etudids, dtype=float)
|
||||||
self.evaluations_completes = []
|
self.evaluations_completes = []
|
||||||
self.evaluations_completes_dict = {}
|
self.evaluations_completes_dict = {}
|
||||||
self.etudids_attente = set() # empty
|
|
||||||
self.evals_type = {}
|
|
||||||
evaluation: Evaluation
|
|
||||||
for evaluation in moduleimpl.evaluations:
|
for evaluation in moduleimpl.evaluations:
|
||||||
self.evals_type[evaluation.id] = evaluation.evaluation_type
|
|
||||||
eval_df = self._load_evaluation_notes(evaluation)
|
eval_df = self._load_evaluation_notes(evaluation)
|
||||||
# is_complete ssi
|
# is_complete ssi tous les inscrits (non dem) au semestre ont une note
|
||||||
# tous les inscrits (non dem) au module ont une note
|
# ou évaluation déclarée "à prise en compte immédiate"
|
||||||
# ou évaluation déclarée "à prise en compte immédiate"
|
# Les évaluations de rattrapage et 2eme session sont toujours complètes
|
||||||
# ou rattrapage, 2eme session, bonus
|
|
||||||
# ET pas bloquée par date (is_blocked)
|
|
||||||
is_blocked = evaluation.is_blocked()
|
|
||||||
etudids_sans_note = inscrits_module - set(eval_df.index) # sans les dem.
|
etudids_sans_note = inscrits_module - set(eval_df.index) # sans les dem.
|
||||||
is_complete = (
|
is_complete = (
|
||||||
(evaluation.evaluation_type != Evaluation.EVALUATION_NORMALE)
|
(evaluation.evaluation_type == scu.EVALUATION_RATTRAPAGE)
|
||||||
|
or (evaluation.evaluation_type == scu.EVALUATION_SESSION2)
|
||||||
or (evaluation.publish_incomplete)
|
or (evaluation.publish_incomplete)
|
||||||
or (not etudids_sans_note)
|
or (not etudids_sans_note)
|
||||||
) and not is_blocked
|
)
|
||||||
self.evaluations_completes.append(is_complete)
|
self.evaluations_completes.append(is_complete)
|
||||||
self.evaluations_completes_dict[evaluation.id] = is_complete
|
self.evaluations_completes_dict[evaluation.id] = is_complete
|
||||||
self.evals_etudids_sans_note[evaluation.id] = etudids_sans_note
|
self.evals_etudids_sans_note[evaluation.id] = etudids_sans_note
|
||||||
@ -189,39 +168,25 @@ class ModuleImplResults:
|
|||||||
# NULL en base => ABS (= -999)
|
# NULL en base => ABS (= -999)
|
||||||
eval_df.fillna(scu.NOTES_ABSENCE, inplace=True)
|
eval_df.fillna(scu.NOTES_ABSENCE, inplace=True)
|
||||||
# Ce merge ne garde que les étudiants inscrits au module
|
# Ce merge ne garde que les étudiants inscrits au module
|
||||||
# et met à NULL (NaN) les notes non présentes
|
# et met à NULL les notes non présentes
|
||||||
# (notes non saisies ou etuds non inscrits au module):
|
# (notes non saisies ou etuds non inscrits au module):
|
||||||
evals_notes = evals_notes.merge(
|
evals_notes = evals_notes.merge(
|
||||||
eval_df, how="left", left_index=True, right_index=True
|
eval_df, how="left", left_index=True, right_index=True
|
||||||
)
|
)
|
||||||
# Notes en attente: (ne prend en compte que les inscrits, non démissionnaires)
|
# Notes en attente: (ne prend en compte que les inscrits, non démissionnaires)
|
||||||
eval_notes_inscr = evals_notes[str(evaluation.id)][list(inscrits_module)]
|
eval_notes_inscr = evals_notes[str(evaluation.id)][list(inscrits_module)]
|
||||||
# Nombre de notes (non vides, incluant ATT etc) des inscrits:
|
eval_etudids_attente = set(
|
||||||
nb_notes = eval_notes_inscr.notna().sum()
|
eval_notes_inscr.iloc[
|
||||||
|
(eval_notes_inscr == scu.NOTES_ATTENTE).to_numpy()
|
||||||
if is_blocked:
|
].index
|
||||||
eval_etudids_attente = set()
|
)
|
||||||
else:
|
|
||||||
# Etudiants avec notes en attente:
|
|
||||||
# = ceux avec note ATT
|
|
||||||
eval_etudids_attente = set(
|
|
||||||
eval_notes_inscr.iloc[
|
|
||||||
(eval_notes_inscr == scu.NOTES_ATTENTE).to_numpy()
|
|
||||||
].index
|
|
||||||
)
|
|
||||||
if evaluation.publish_incomplete:
|
|
||||||
# et en "immédiat", tous ceux sans note
|
|
||||||
eval_etudids_attente |= etudids_sans_note
|
|
||||||
|
|
||||||
# Synthèse pour état du module:
|
|
||||||
self.etudids_attente |= eval_etudids_attente
|
self.etudids_attente |= eval_etudids_attente
|
||||||
self.evaluations_etat[evaluation.id] = EvaluationEtat(
|
self.evaluations_etat[evaluation.id] = EvaluationEtat(
|
||||||
evaluation_id=evaluation.id,
|
evaluation_id=evaluation.id,
|
||||||
nb_attente=len(eval_etudids_attente),
|
nb_attente=len(eval_etudids_attente),
|
||||||
nb_notes=int(nb_notes),
|
|
||||||
is_complete=is_complete,
|
is_complete=is_complete,
|
||||||
)
|
)
|
||||||
# au moins une note en attente (ATT ou manquante en mode "immédiat") dans ce modimpl:
|
# au moins une note en ATT dans ce modimpl:
|
||||||
self.en_attente = bool(self.etudids_attente)
|
self.en_attente = bool(self.etudids_attente)
|
||||||
|
|
||||||
# Force columns names to integers (evaluation ids)
|
# Force columns names to integers (evaluation ids)
|
||||||
@ -254,44 +219,36 @@ class ModuleImplResults:
|
|||||||
eval_df[str(evaluation.id)] = pd.to_numeric(eval_df[str(evaluation.id)])
|
eval_df[str(evaluation.id)] = pd.to_numeric(eval_df[str(evaluation.id)])
|
||||||
return eval_df
|
return eval_df
|
||||||
|
|
||||||
def get_evaluations_coefs(self, modimpl: ModuleImpl) -> np.array:
|
def _etudids(self):
|
||||||
|
"""L'index du dataframe est la liste de tous les étudiants inscrits au semestre
|
||||||
|
(incluant les DEM et DEF)
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
inscr.etudid
|
||||||
|
for inscr in db.session.get(
|
||||||
|
ModuleImpl, self.moduleimpl_id
|
||||||
|
).formsemestre.inscriptions
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_evaluations_coefs(self, moduleimpl: ModuleImpl) -> np.array:
|
||||||
"""Coefficients des évaluations.
|
"""Coefficients des évaluations.
|
||||||
Les coefs des évals incomplètes, rattrapage, session 2, bonus sont forcés à zéro.
|
Les coefs des évals incomplètes et non "normales" (session 2, rattrapage)
|
||||||
|
sont zéro.
|
||||||
Résultat: 2d-array of floats, shape (nb_evals, 1)
|
Résultat: 2d-array of floats, shape (nb_evals, 1)
|
||||||
"""
|
"""
|
||||||
return (
|
return (
|
||||||
np.array(
|
np.array(
|
||||||
[
|
[
|
||||||
(
|
e.coefficient
|
||||||
e.coefficient
|
if e.evaluation_type == scu.EVALUATION_NORMALE
|
||||||
if e.evaluation_type == Evaluation.EVALUATION_NORMALE
|
else 0.0
|
||||||
else 0.0
|
for e in moduleimpl.evaluations
|
||||||
)
|
|
||||||
for e in modimpl.evaluations
|
|
||||||
],
|
],
|
||||||
dtype=float,
|
dtype=float,
|
||||||
)
|
)
|
||||||
* self.evaluations_completes
|
* self.evaluations_completes
|
||||||
).reshape(-1, 1)
|
).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
|
# was _list_notes_evals_titles
|
||||||
def get_evaluations_completes(self, moduleimpl: ModuleImpl) -> list[Evaluation]:
|
def get_evaluations_completes(self, moduleimpl: ModuleImpl) -> list[Evaluation]:
|
||||||
"Liste des évaluations complètes"
|
"Liste des évaluations complètes"
|
||||||
@ -309,7 +266,7 @@ class ModuleImplResults:
|
|||||||
) / [e.note_max / 20.0 for e in moduleimpl.evaluations]
|
) / [e.note_max / 20.0 for e in moduleimpl.evaluations]
|
||||||
|
|
||||||
def get_eval_notes_dict(self, evaluation_id: int) -> dict:
|
def get_eval_notes_dict(self, evaluation_id: int) -> dict:
|
||||||
"""Notes d'une évaluation, brutes, sous forme d'un dict
|
"""Notes d'une évaulation, brutes, sous forme d'un dict
|
||||||
{ etudid : valeur }
|
{ etudid : valeur }
|
||||||
avec les valeurs float, ou "ABS" ou EXC
|
avec les valeurs float, ou "ABS" ou EXC
|
||||||
"""
|
"""
|
||||||
@ -318,55 +275,44 @@ class ModuleImplResults:
|
|||||||
for (etudid, x) in self.evals_notes[evaluation_id].items()
|
for (etudid, x) in self.evals_notes[evaluation_id].items()
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_evaluations_rattrapage(self, moduleimpl: ModuleImpl) -> list[Evaluation]:
|
def get_evaluation_rattrapage(self, moduleimpl: ModuleImpl):
|
||||||
"""Les évaluations de rattrapage de ce module.
|
"""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
|
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
|
e
|
||||||
for e in moduleimpl.evaluations
|
for e in moduleimpl.evaluations
|
||||||
if e.evaluation_type == Evaluation.EVALUATION_RATTRAPAGE
|
if e.evaluation_type == scu.EVALUATION_RATTRAPAGE
|
||||||
]
|
]
|
||||||
|
if eval_list:
|
||||||
|
return eval_list[0]
|
||||||
|
return None
|
||||||
|
|
||||||
def get_evaluations_session2(self, moduleimpl: ModuleImpl) -> list[Evaluation]:
|
def get_evaluation_session2(self, moduleimpl: ModuleImpl):
|
||||||
"""Les évaluations de deuxième session de ce module, ou None s'il n'en a pas.
|
"""L'évaluation 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.
|
Session 2: remplace la note de moyenne des autres évals.
|
||||||
"""
|
"""
|
||||||
return [
|
eval_list = [
|
||||||
e
|
e
|
||||||
for e in moduleimpl.evaluations
|
for e in moduleimpl.evaluations
|
||||||
if e.evaluation_type == Evaluation.EVALUATION_SESSION2
|
if e.evaluation_type == scu.EVALUATION_SESSION2
|
||||||
]
|
|
||||||
|
|
||||||
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."""
|
|
||||||
return [
|
|
||||||
e
|
|
||||||
for e in modimpl.evaluations
|
|
||||||
if e.evaluation_type == Evaluation.EVALUATION_BONUS and not e.is_blocked()
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_evaluations_bonus_idx(self, modimpl: ModuleImpl) -> list[int]:
|
|
||||||
"""Les indices des évaluations bonus non bloquées"""
|
|
||||||
return [
|
|
||||||
i
|
|
||||||
for (i, e) in enumerate(modimpl.evaluations)
|
|
||||||
if e.evaluation_type == Evaluation.EVALUATION_BONUS and not e.is_blocked()
|
|
||||||
]
|
]
|
||||||
|
if eval_list:
|
||||||
|
return eval_list[0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class ModuleImplResultsAPC(ModuleImplResults):
|
class ModuleImplResultsAPC(ModuleImplResults):
|
||||||
"Calcul des moyennes de modules à la mode BUT"
|
"Calcul des moyennes de modules à la mode BUT"
|
||||||
|
|
||||||
def compute_module_moy(
|
def compute_module_moy(
|
||||||
self, evals_poids_df: pd.DataFrame, modimpl_coefs_df: pd.DataFrame
|
self,
|
||||||
|
evals_poids_df: pd.DataFrame,
|
||||||
) -> pd.DataFrame:
|
) -> pd.DataFrame:
|
||||||
"""Calcule les moyennes des étudiants dans ce module
|
"""Calcule les moyennes des étudiants dans ce module
|
||||||
|
|
||||||
Argument:
|
Argument: evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs
|
||||||
evals_poids: DataFrame, colonnes: UEs, lignes: EVALs
|
|
||||||
modimpl_coefs_df: DataFrame, colonnes: modimpl_id, lignes: ue_id
|
|
||||||
|
|
||||||
Résultat: DataFrame, colonnes UE, lignes etud
|
Résultat: DataFrame, colonnes UE, lignes etud
|
||||||
= la note de l'étudiant dans chaque UE pour ce module.
|
= la note de l'étudiant dans chaque UE pour ce module.
|
||||||
@ -387,7 +333,6 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
|||||||
return pd.DataFrame(index=[], columns=evals_poids_df.columns)
|
return pd.DataFrame(index=[], columns=evals_poids_df.columns)
|
||||||
if nb_ues == 0:
|
if nb_ues == 0:
|
||||||
return pd.DataFrame(index=self.evals_notes.index, columns=[])
|
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_coefs = self.get_evaluations_coefs(modimpl)
|
||||||
evals_poids = evals_poids_df.values * evals_coefs
|
evals_poids = evals_poids_df.values * evals_coefs
|
||||||
# -> evals_poids shape : (nb_evals, nb_ues)
|
# -> evals_poids shape : (nb_evals, nb_ues)
|
||||||
@ -401,7 +346,7 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
|||||||
# et dans dans evals_poids_etuds
|
# et dans dans evals_poids_etuds
|
||||||
# (rappel: la comparaison est toujours false face à un NaN)
|
# (rappel: la comparaison est toujours false face à un NaN)
|
||||||
# shape: (nb_etuds, nb_evals, nb_ues)
|
# shape: (nb_etuds, nb_evals, nb_ues)
|
||||||
poids_stacked = np.stack([evals_poids] * nb_etuds) # nb_etuds, nb_evals, nb_ues
|
poids_stacked = np.stack([evals_poids] * nb_etuds)
|
||||||
evals_poids_etuds = np.where(
|
evals_poids_etuds = np.where(
|
||||||
np.stack([self.evals_notes.values] * nb_ues, axis=2) > scu.NOTES_NEUTRALISE,
|
np.stack([self.evals_notes.values] * nb_ues, axis=2) > scu.NOTES_NEUTRALISE,
|
||||||
poids_stacked,
|
poids_stacked,
|
||||||
@ -409,61 +354,51 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
|||||||
)
|
)
|
||||||
# Calcule la moyenne pondérée sur les notes disponibles:
|
# Calcule la moyenne pondérée sur les notes disponibles:
|
||||||
evals_notes_stacked = np.stack([evals_notes_20] * nb_ues, axis=2)
|
evals_notes_stacked = np.stack([evals_notes_20] * nb_ues, axis=2)
|
||||||
# evals_notes_stacked shape: nb_etuds, nb_evals, nb_ues
|
|
||||||
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
|
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
|
||||||
etuds_moy_module = np.sum(
|
etuds_moy_module = np.sum(
|
||||||
evals_poids_etuds * evals_notes_stacked, axis=1
|
evals_poids_etuds * evals_notes_stacked, axis=1
|
||||||
) / np.sum(evals_poids_etuds, axis=1)
|
) / np.sum(evals_poids_etuds, axis=1)
|
||||||
# etuds_moy_module shape: nb_etuds x nb_ues
|
|
||||||
|
|
||||||
evals_session2 = self.get_evaluations_session2(modimpl)
|
# Session2 : quand elle existe, remplace la note de module
|
||||||
evals_rat = self.get_evaluations_rattrapage(modimpl)
|
eval_session2 = self.get_evaluation_session2(modimpl)
|
||||||
if evals_session2:
|
if eval_session2:
|
||||||
# Session2 : quand elle existe, remplace la note de module
|
notes_session2 = self.evals_notes[eval_session2.id].values
|
||||||
# Calcul moyenne notes session2 et remplace (si la note session 2 existe)
|
# n'utilise que les notes valides (pas ATT, EXC, ABS, NaN)
|
||||||
etuds_moy_module_s2 = self._compute_moy_special(
|
etuds_use_session2 = notes_session2 > scu.NOTES_ABSENCE
|
||||||
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_moy_module = np.where(
|
||||||
etuds_use_session2[:, np.newaxis],
|
etuds_use_session2[:, np.newaxis],
|
||||||
etuds_moy_module_s2,
|
np.tile(
|
||||||
|
(notes_session2 / (eval_session2.note_max / 20.0))[:, np.newaxis],
|
||||||
|
nb_ues,
|
||||||
|
),
|
||||||
etuds_moy_module,
|
etuds_moy_module,
|
||||||
)
|
)
|
||||||
self.etuds_use_session2 = pd.Series(
|
self.etuds_use_session2 = pd.Series(
|
||||||
etuds_use_session2, index=self.evals_notes.index
|
etuds_use_session2, index=self.evals_notes.index
|
||||||
)
|
)
|
||||||
elif evals_rat:
|
else:
|
||||||
etuds_moy_module_rat = self._compute_moy_special(
|
# Rattrapage: remplace la note de module ssi elle est supérieure
|
||||||
modimpl,
|
eval_rat = self.get_evaluation_rattrapage(modimpl)
|
||||||
evals_notes_stacked,
|
if eval_rat:
|
||||||
evals_poids_df,
|
notes_rat = self.evals_notes[eval_rat.id].values
|
||||||
Evaluation.EVALUATION_RATTRAPAGE,
|
# remplace les notes invalides (ATT, EXC...) par des NaN
|
||||||
)
|
notes_rat = np.where(
|
||||||
etuds_ue_use_rattrapage = (
|
notes_rat > scu.NOTES_ABSENCE,
|
||||||
etuds_moy_module_rat > etuds_moy_module
|
notes_rat / (eval_rat.note_max / 20.0),
|
||||||
) # etud x UE
|
np.nan,
|
||||||
etuds_moy_module = np.where(
|
)
|
||||||
etuds_ue_use_rattrapage, etuds_moy_module_rat, etuds_moy_module
|
# "É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
|
||||||
self.etuds_use_rattrapage = pd.Series(
|
notes_rat_ues = np.stack([notes_rat] * nb_ues, axis=1)
|
||||||
np.any(etuds_ue_use_rattrapage, axis=1), index=self.evals_notes.index
|
# prend le max
|
||||||
)
|
etuds_use_rattrapage = notes_rat_ues > etuds_moy_module
|
||||||
# Application des évaluations bonus:
|
etuds_moy_module = np.where(
|
||||||
etuds_moy_module = self.apply_bonus(
|
etuds_use_rattrapage, notes_rat_ues, etuds_moy_module
|
||||||
etuds_moy_module,
|
)
|
||||||
modimpl,
|
# Serie indiquant que l'étudiant utilise une note de rattrapage sur l'une des UE:
|
||||||
evals_poids_df,
|
self.etuds_use_rattrapage = pd.Series(
|
||||||
evals_notes_stacked,
|
etuds_use_rattrapage.any(axis=1), index=self.evals_notes.index
|
||||||
)
|
)
|
||||||
self.etuds_moy_module = pd.DataFrame(
|
self.etuds_moy_module = pd.DataFrame(
|
||||||
etuds_moy_module,
|
etuds_moy_module,
|
||||||
index=self.evals_notes.index,
|
index=self.evals_notes.index,
|
||||||
@ -471,58 +406,6 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
|||||||
)
|
)
|
||||||
return self.etuds_moy_module
|
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,
|
|
||||||
modimpl: ModuleImpl,
|
|
||||||
evals_poids_df: pd.DataFrame,
|
|
||||||
evals_notes_stacked: np.ndarray,
|
|
||||||
):
|
|
||||||
"""Ajoute les points des évaluations bonus.
|
|
||||||
Il peut y avoir un nb quelconque d'évaluations bonus.
|
|
||||||
Les points sont directement ajoutés (ils peuvent être négatifs).
|
|
||||||
"""
|
|
||||||
evals_bonus = self.get_evaluations_bonus(modimpl)
|
|
||||||
if not evals_bonus:
|
|
||||||
return etuds_moy_module
|
|
||||||
poids_stacked = np.stack([evals_poids_df.values] * len(etuds_moy_module))
|
|
||||||
for evaluation in evals_bonus:
|
|
||||||
eval_idx = evals_poids_df.index.get_loc(evaluation.id)
|
|
||||||
etuds_moy_module += (
|
|
||||||
evals_notes_stacked[:, eval_idx, :] * poids_stacked[:, eval_idx, :]
|
|
||||||
)
|
|
||||||
# Clip dans [0,20]
|
|
||||||
etuds_moy_module.clip(0, 20, out=etuds_moy_module)
|
|
||||||
return etuds_moy_module
|
|
||||||
|
|
||||||
|
|
||||||
def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
|
def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
|
||||||
"""Charge poids des évaluations d'un module et retourne un dataframe
|
"""Charge poids des évaluations d'un module et retourne un dataframe
|
||||||
@ -536,10 +419,11 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
|
|||||||
|
|
||||||
Résultat: (evals_poids, liste de UEs du semestre sauf le sport)
|
Résultat: (evals_poids, liste de UEs du semestre sauf le sport)
|
||||||
"""
|
"""
|
||||||
modimpl = db.session.get(ModuleImpl, moduleimpl_id)
|
modimpl: ModuleImpl = db.session.get(ModuleImpl, moduleimpl_id)
|
||||||
|
evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all()
|
||||||
ues = modimpl.formsemestre.get_ues(with_sport=False)
|
ues = modimpl.formsemestre.get_ues(with_sport=False)
|
||||||
ue_ids = [ue.id for ue in ues]
|
ue_ids = [ue.id for ue in ues]
|
||||||
evaluation_ids = [evaluation.id for evaluation in modimpl.evaluations]
|
evaluation_ids = [evaluation.id for evaluation in evaluations]
|
||||||
evals_poids = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float)
|
evals_poids = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float)
|
||||||
if (
|
if (
|
||||||
modimpl.module.module_type == ModuleType.RESSOURCE
|
modimpl.module.module_type == ModuleType.RESSOURCE
|
||||||
@ -550,7 +434,7 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
|
|||||||
).filter_by(moduleimpl_id=moduleimpl_id):
|
).filter_by(moduleimpl_id=moduleimpl_id):
|
||||||
try:
|
try:
|
||||||
evals_poids[ue_poids.ue_id][ue_poids.evaluation_id] = ue_poids.poids
|
evals_poids[ue_poids.ue_id][ue_poids.evaluation_id] = ue_poids.poids
|
||||||
except KeyError:
|
except KeyError as exc:
|
||||||
pass # poids vers des UE qui n'existent plus ou sont dans un autre semestre...
|
pass # poids vers des UE qui n'existent plus ou sont dans un autre semestre...
|
||||||
|
|
||||||
# Initialise poids non enregistrés:
|
# Initialise poids non enregistrés:
|
||||||
@ -571,7 +455,6 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
|
|||||||
return evals_poids, ues
|
return evals_poids, ues
|
||||||
|
|
||||||
|
|
||||||
# appelé par ModuleImpl.check_apc_conformity()
|
|
||||||
def moduleimpl_is_conforme(
|
def moduleimpl_is_conforme(
|
||||||
moduleimpl, evals_poids: pd.DataFrame, modimpl_coefs_df: pd.DataFrame
|
moduleimpl, evals_poids: pd.DataFrame, modimpl_coefs_df: pd.DataFrame
|
||||||
) -> bool:
|
) -> bool:
|
||||||
@ -593,12 +476,12 @@ def moduleimpl_is_conforme(
|
|||||||
if len(modimpl_coefs_df) != nb_ues:
|
if len(modimpl_coefs_df) != nb_ues:
|
||||||
# il arrive (#bug) que le cache ne soit pas à jour...
|
# il arrive (#bug) que le cache ne soit pas à jour...
|
||||||
sco_cache.invalidate_formsemestre()
|
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:
|
if moduleimpl.id not in modimpl_coefs_df:
|
||||||
# soupçon de bug cache coef ?
|
# soupçon de bug cache coef ?
|
||||||
sco_cache.invalidate_formsemestre()
|
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
|
module_evals_poids = evals_poids.transpose().sum(axis=1) != 0
|
||||||
return all((modimpl_coefs_df[moduleimpl.id] != 0).eq(module_evals_poids))
|
return all((modimpl_coefs_df[moduleimpl.id] != 0).eq(module_evals_poids))
|
||||||
@ -640,87 +523,42 @@ class ModuleImplResultsClassic(ModuleImplResults):
|
|||||||
evals_coefs_etuds * evals_notes_20, axis=1
|
evals_coefs_etuds * evals_notes_20, axis=1
|
||||||
) / np.sum(evals_coefs_etuds, axis=1)
|
) / np.sum(evals_coefs_etuds, axis=1)
|
||||||
|
|
||||||
evals_session2 = self.get_evaluations_session2(modimpl)
|
# Session2 : quand elle existe, remplace la note de module
|
||||||
evals_rat = self.get_evaluations_rattrapage(modimpl)
|
eval_session2 = self.get_evaluation_session2(modimpl)
|
||||||
if evals_session2:
|
if eval_session2:
|
||||||
# Session2 : quand elle existe, remplace la note de module
|
notes_session2 = self.evals_notes[eval_session2.id].values
|
||||||
# Calcule la moyenne des évaluations de session2
|
# n'utilise que les notes valides (pas ATT, EXC, ABS, NaN)
|
||||||
etuds_moy_module_s2 = self._compute_moy_special(
|
etuds_use_session2 = notes_session2 > scu.NOTES_ABSENCE
|
||||||
modimpl, evals_notes_20, Evaluation.EVALUATION_SESSION2
|
|
||||||
)
|
|
||||||
etuds_use_session2 = np.isfinite(etuds_moy_module_s2)
|
|
||||||
etuds_moy_module = np.where(
|
etuds_moy_module = np.where(
|
||||||
etuds_use_session2,
|
etuds_use_session2,
|
||||||
etuds_moy_module_s2,
|
notes_session2 / (eval_session2.note_max / 20.0),
|
||||||
etuds_moy_module,
|
etuds_moy_module,
|
||||||
)
|
)
|
||||||
self.etuds_use_session2 = pd.Series(
|
self.etuds_use_session2 = pd.Series(
|
||||||
etuds_use_session2, index=self.evals_notes.index
|
etuds_use_session2, index=self.evals_notes.index
|
||||||
)
|
)
|
||||||
elif evals_rat:
|
else:
|
||||||
# Rattrapage: remplace la note de module ssi elle est supérieure
|
# Rattrapage: remplace la note de module ssi elle est supérieure
|
||||||
# Calcule la moyenne des évaluations de rattrapage
|
eval_rat = self.get_evaluation_rattrapage(modimpl)
|
||||||
etuds_moy_module_rat = self._compute_moy_special(
|
if eval_rat:
|
||||||
modimpl, evals_notes_20, Evaluation.EVALUATION_RATTRAPAGE
|
notes_rat = self.evals_notes[eval_rat.id].values
|
||||||
)
|
# remplace les notes invalides (ATT, EXC...) par des NaN
|
||||||
etuds_use_rattrapage = etuds_moy_module_rat > etuds_moy_module
|
notes_rat = np.where(
|
||||||
etuds_moy_module = np.where(
|
notes_rat > scu.NOTES_ABSENCE,
|
||||||
etuds_use_rattrapage, etuds_moy_module_rat, etuds_moy_module
|
notes_rat / (eval_rat.note_max / 20.0),
|
||||||
)
|
np.nan,
|
||||||
self.etuds_use_rattrapage = pd.Series(
|
)
|
||||||
etuds_use_rattrapage, index=self.evals_notes.index
|
# prend le max
|
||||||
)
|
etuds_use_rattrapage = notes_rat > etuds_moy_module
|
||||||
|
etuds_moy_module = np.where(
|
||||||
# Application des évaluations bonus:
|
etuds_use_rattrapage, notes_rat, etuds_moy_module
|
||||||
etuds_moy_module = self.apply_bonus(
|
)
|
||||||
etuds_moy_module,
|
self.etuds_use_rattrapage = pd.Series(
|
||||||
modimpl,
|
etuds_use_rattrapage, index=self.evals_notes.index
|
||||||
evals_notes_20,
|
)
|
||||||
)
|
|
||||||
self.etuds_moy_module = pd.Series(
|
self.etuds_moy_module = pd.Series(
|
||||||
etuds_moy_module,
|
etuds_moy_module,
|
||||||
index=self.evals_notes.index,
|
index=self.evals_notes.index,
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.etuds_moy_module
|
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,
|
|
||||||
modimpl: ModuleImpl,
|
|
||||||
evals_notes_20: np.ndarray,
|
|
||||||
):
|
|
||||||
"""Ajoute les points des évaluations bonus.
|
|
||||||
Il peut y avoir un nb quelconque d'évaluations bonus.
|
|
||||||
Les points sont directement ajoutés (ils peuvent être négatifs).
|
|
||||||
"""
|
|
||||||
evals_bonus_idx = self.get_evaluations_bonus_idx(modimpl)
|
|
||||||
if not evals_bonus_idx:
|
|
||||||
return etuds_moy_module
|
|
||||||
for eval_idx in evals_bonus_idx:
|
|
||||||
etuds_moy_module += evals_notes_20[:, eval_idx]
|
|
||||||
# Clip dans [0,20]
|
|
||||||
etuds_moy_module.clip(0, 20, out=etuds_moy_module)
|
|
||||||
return etuds_moy_module
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -89,7 +89,7 @@ def compute_sem_moys_apc_using_ects(
|
|||||||
flash(
|
flash(
|
||||||
Markup(
|
Markup(
|
||||||
f"""Calcul moyenne générale impossible: ECTS des UE manquants !<br>
|
f"""Calcul moyenne générale impossible: ECTS des UE manquants !<br>
|
||||||
(formation: <a href="{url_for("notes.ue_table",
|
(formation: <a href="{url_for("notes.ue_table",
|
||||||
scodoc_dept=g.scodoc_dept, formation_id=formation_id)}">{formation.get_titre_version()}</a>)"""
|
scodoc_dept=g.scodoc_dept, formation_id=formation_id)}">{formation.get_titre_version()}</a>)"""
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -100,7 +100,7 @@ def compute_sem_moys_apc_using_ects(
|
|||||||
|
|
||||||
|
|
||||||
def comp_ranks_series(notes: pd.Series) -> tuple[pd.Series, pd.Series]:
|
def comp_ranks_series(notes: pd.Series) -> tuple[pd.Series, pd.Series]:
|
||||||
"""Calcul rangs à partir d'une série ("vecteur") de notes (index etudid, valeur
|
"""Calcul rangs à partir d'une séries ("vecteur") de notes (index etudid, valeur
|
||||||
numérique) en tenant compte des ex-aequos.
|
numérique) en tenant compte des ex-aequos.
|
||||||
|
|
||||||
Result: couple (tuple)
|
Result: couple (tuple)
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -99,11 +99,9 @@ def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.Data
|
|||||||
# 0 pour modules normaux, 1. pour bonus (car par défaut, on veut qu'un bonus agisse
|
# 0 pour modules normaux, 1. pour bonus (car par défaut, on veut qu'un bonus agisse
|
||||||
# sur toutes les UE)
|
# sur toutes les UE)
|
||||||
default_poids = {
|
default_poids = {
|
||||||
mod.id: (
|
mod.id: 1.0
|
||||||
1.0
|
if (mod.module_type == ModuleType.STANDARD) and (mod.ue.type == UE_SPORT)
|
||||||
if (mod.module_type == ModuleType.STANDARD) and (mod.ue.type == UE_SPORT)
|
else 0.0
|
||||||
else 0.0
|
|
||||||
)
|
|
||||||
for mod in modules
|
for mod in modules
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,12 +148,10 @@ def df_load_modimpl_coefs(
|
|||||||
# 0 pour modules normaux, 1. pour bonus (car par défaut, on veut qu'un bonus agisse
|
# 0 pour modules normaux, 1. pour bonus (car par défaut, on veut qu'un bonus agisse
|
||||||
# sur toutes les UE)
|
# sur toutes les UE)
|
||||||
default_poids = {
|
default_poids = {
|
||||||
modimpl.id: (
|
modimpl.id: 1.0
|
||||||
1.0
|
if (modimpl.module.module_type == ModuleType.STANDARD)
|
||||||
if (modimpl.module.module_type == ModuleType.STANDARD)
|
and (modimpl.module.ue.type == UE_SPORT)
|
||||||
and (modimpl.module.ue.type == UE_SPORT)
|
else 0.0
|
||||||
else 0.0
|
|
||||||
)
|
|
||||||
for modimpl in formsemestre.modimpls_sorted
|
for modimpl in formsemestre.modimpls_sorted
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,9 +179,7 @@ def notes_sem_assemble_cube(modimpls_notes: list[pd.DataFrame]) -> np.ndarray:
|
|||||||
return modimpls_notes.swapaxes(0, 1)
|
return modimpls_notes.swapaxes(0, 1)
|
||||||
|
|
||||||
|
|
||||||
def notes_sem_load_cube(
|
def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
|
||||||
formsemestre: FormSemestre, modimpl_coefs_df: pd.DataFrame
|
|
||||||
) -> tuple:
|
|
||||||
"""Construit le "cube" (tenseur) des notes du semestre.
|
"""Construit le "cube" (tenseur) des notes du semestre.
|
||||||
Charge toutes les notes (sql), calcule les moyennes des modules
|
Charge toutes les notes (sql), calcule les moyennes des modules
|
||||||
et assemble le cube.
|
et assemble le cube.
|
||||||
@ -206,11 +200,10 @@ def notes_sem_load_cube(
|
|||||||
modimpls_results = {}
|
modimpls_results = {}
|
||||||
modimpls_evals_poids = {}
|
modimpls_evals_poids = {}
|
||||||
modimpls_notes = []
|
modimpls_notes = []
|
||||||
etudids, etudids_actifs = formsemestre.etudids_actifs()
|
|
||||||
for modimpl in formsemestre.modimpls_sorted:
|
for modimpl in formsemestre.modimpls_sorted:
|
||||||
mod_results = moy_mod.ModuleImplResultsAPC(modimpl, etudids, etudids_actifs)
|
mod_results = moy_mod.ModuleImplResultsAPC(modimpl)
|
||||||
evals_poids = modimpl.get_evaluations_poids()
|
evals_poids, _ = moy_mod.load_evaluations_poids(modimpl.id)
|
||||||
etuds_moy_module = mod_results.compute_module_moy(evals_poids, modimpl_coefs_df)
|
etuds_moy_module = mod_results.compute_module_moy(evals_poids)
|
||||||
modimpls_results[modimpl.id] = mod_results
|
modimpls_results[modimpl.id] = mod_results
|
||||||
modimpls_evals_poids[modimpl.id] = evals_poids
|
modimpls_evals_poids[modimpl.id] = evals_poids
|
||||||
modimpls_notes.append(etuds_moy_module)
|
modimpls_notes.append(etuds_moy_module)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -59,17 +59,16 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
|
|
||||||
def compute(self):
|
def compute(self):
|
||||||
"Charge les notes et inscriptions et calcule les moyennes d'UE et gen."
|
"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.sem_cube,
|
||||||
self.modimpls_evals_poids,
|
self.modimpls_evals_poids,
|
||||||
self.modimpls_results,
|
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.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre)
|
||||||
self.ues_inscr_parcours_df = self.load_ues_inscr_parcours()
|
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
|
# l'idx de la colonne du mod modimpl.id est
|
||||||
# modimpl_coefs_df.columns.get_loc(modimpl.id)
|
# modimpl_coefs_df.columns.get_loc(modimpl.id)
|
||||||
# idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id)
|
# idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id)
|
||||||
@ -274,7 +273,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
return s.index[s.notna()]
|
return s.index[s.notna()]
|
||||||
|
|
||||||
def etud_parcours_ues_ids(self, etudid: int) -> set[int]:
|
def etud_parcours_ues_ids(self, etudid: int) -> set[int]:
|
||||||
"""Ensemble des id des UEs que l'étudiant doit valider dans ce semestre compte tenu
|
"""Ensemble les id des UEs que l'étudiant doit valider dans ce semestre compte tenu
|
||||||
du parcours dans lequel il est inscrit.
|
du parcours dans lequel il est inscrit.
|
||||||
Se base sur le parcours dans ce semestre, et le référentiel de compétences.
|
Se base sur le parcours dans ce semestre, et le référentiel de compétences.
|
||||||
Note: il n'est pas nécessairement inscrit à toutes ces UEs.
|
Note: il n'est pas nécessairement inscrit à toutes ces UEs.
|
||||||
@ -308,7 +307,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
|
|
||||||
return ues_ids
|
return ues_ids
|
||||||
|
|
||||||
def etud_has_decision(self, etudid, include_rcues=True) -> bool:
|
def etud_has_decision(self, etudid) -> bool:
|
||||||
"""True s'il y a une décision (quelconque) de jury
|
"""True s'il y a une décision (quelconque) de jury
|
||||||
émanant de ce formsemestre pour cet étudiant.
|
émanant de ce formsemestre pour cet étudiant.
|
||||||
prend aussi en compte les autorisations de passage.
|
prend aussi en compte les autorisations de passage.
|
||||||
@ -319,12 +318,9 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
or ApcValidationAnnee.query.filter_by(
|
or ApcValidationAnnee.query.filter_by(
|
||||||
formsemestre_id=self.formsemestre.id, etudid=etudid
|
formsemestre_id=self.formsemestre.id, etudid=etudid
|
||||||
).count()
|
).count()
|
||||||
or (
|
or ApcValidationRCUE.query.filter_by(
|
||||||
include_rcues
|
formsemestre_id=self.formsemestre.id, etudid=etudid
|
||||||
and ApcValidationRCUE.query.filter_by(
|
).count()
|
||||||
formsemestre_id=self.formsemestre.id, etudid=etudid
|
|
||||||
).count()
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_validations_annee(self) -> dict[int, ApcValidationAnnee]:
|
def get_validations_annee(self) -> dict[int, ApcValidationAnnee]:
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -234,7 +234,7 @@ class ResultatsSemestreClassic(NotesTableCompat):
|
|||||||
raise ScoValueError(
|
raise ScoValueError(
|
||||||
f"""<div class="scovalueerror"><p>Coefficient de l'UE capitalisée {ue.acronyme}
|
f"""<div class="scovalueerror"><p>Coefficient de l'UE capitalisée {ue.acronyme}
|
||||||
impossible à déterminer pour l'étudiant <a href="{
|
impossible à déterminer pour l'étudiant <a href="{
|
||||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||||
}" class="discretelink">{etud.nom_disp()}</a></p>
|
}" class="discretelink">{etud.nom_disp()}</a></p>
|
||||||
<p>Il faut <a href="{
|
<p>Il faut <a href="{
|
||||||
url_for("notes.formsemestre_edit_uecoefs", scodoc_dept=g.scodoc_dept,
|
url_for("notes.formsemestre_edit_uecoefs", scodoc_dept=g.scodoc_dept,
|
||||||
@ -242,8 +242,7 @@ class ResultatsSemestreClassic(NotesTableCompat):
|
|||||||
)
|
)
|
||||||
}">saisir le coefficient de cette UE avant de continuer</a></p>
|
}">saisir le coefficient de cette UE avant de continuer</a></p>
|
||||||
</div>
|
</div>
|
||||||
""",
|
"""
|
||||||
safe=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -257,9 +256,8 @@ def notes_sem_load_matrix(formsemestre: FormSemestre) -> tuple[np.ndarray, dict]
|
|||||||
"""
|
"""
|
||||||
modimpls_results = {}
|
modimpls_results = {}
|
||||||
modimpls_notes = []
|
modimpls_notes = []
|
||||||
etudids, etudids_actifs = formsemestre.etudids_actifs()
|
|
||||||
for modimpl in formsemestre.modimpls_sorted:
|
for modimpl in formsemestre.modimpls_sorted:
|
||||||
mod_results = moy_mod.ModuleImplResultsClassic(modimpl, etudids, etudids_actifs)
|
mod_results = moy_mod.ModuleImplResultsClassic(modimpl)
|
||||||
etuds_moy_module = mod_results.compute_module_moy()
|
etuds_moy_module = mod_results.compute_module_moy()
|
||||||
modimpls_results[modimpl.id] = mod_results
|
modimpls_results[modimpl.id] = mod_results
|
||||||
modimpls_notes.append(etuds_moy_module)
|
modimpls_notes.append(etuds_moy_module)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -9,13 +9,12 @@
|
|||||||
|
|
||||||
from collections import Counter, defaultdict
|
from collections import Counter, defaultdict
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
import datetime
|
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import sqlalchemy as sa
|
|
||||||
from flask import g, url_for
|
from flask import g, url_for
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
@ -23,19 +22,14 @@ from app.comp import res_sem
|
|||||||
from app.comp.res_cache import ResultatsCache
|
from app.comp.res_cache import ResultatsCache
|
||||||
from app.comp.jury import ValidationsSemestre
|
from app.comp.jury import ValidationsSemestre
|
||||||
from app.comp.moy_mod import ModuleImplResults
|
from app.comp.moy_mod import ModuleImplResults
|
||||||
from app.models import (
|
from app.models import FormSemestre, FormSemestreUECoef
|
||||||
Evaluation,
|
from app.models import Identite
|
||||||
FormSemestre,
|
from app.models import ModuleImpl, ModuleImplInscription
|
||||||
FormSemestreUECoef,
|
from app.models import ScolarAutorisationInscription
|
||||||
Identite,
|
from app.models.ues import UniteEns
|
||||||
ModuleImpl,
|
|
||||||
ModuleImplInscription,
|
|
||||||
ScolarAutorisationInscription,
|
|
||||||
UniteEns,
|
|
||||||
)
|
|
||||||
from app.scodoc.sco_cache import ResultatsSemestreCache
|
from app.scodoc.sco_cache import ResultatsSemestreCache
|
||||||
from app.scodoc.codes_cursus import UE_SPORT
|
from app.scodoc.codes_cursus import UE_SPORT
|
||||||
from app.scodoc.sco_exceptions import ScoValueError, ScoTemporaryError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
@ -150,7 +144,7 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
def etud_ects_tot_sem(self, etudid: int) -> float:
|
def etud_ects_tot_sem(self, etudid: int) -> float:
|
||||||
"""Le total des ECTS associées à ce semestre (que l'étudiant peut ou non valider)"""
|
"""Le total des ECTS associées à ce semestre (que l'étudiant peut ou non valider)"""
|
||||||
etud_ues = self.etud_ues(etudid)
|
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:
|
def modimpl_notes(self, modimpl_id: int, ue_id: int) -> np.ndarray:
|
||||||
"""Les notes moyennes des étudiants du sem. à ce modimpl dans cette ue.
|
"""Les notes moyennes des étudiants du sem. à ce modimpl dans cette ue.
|
||||||
@ -198,86 +192,16 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
*[mr.etudids_attente for mr in self.modimpls_results.values()]
|
*[mr.etudids_attente for mr in self.modimpls_results.values()]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Etat des évaluations
|
# # Etat des évaluations
|
||||||
def get_evaluation_etat(self, evaluation: Evaluation) -> dict:
|
# # (se substitue à do_evaluation_etat, sans les moyennes par groupes)
|
||||||
"""État d'une évaluation
|
# def get_evaluations_etats(evaluation_id: int) -> dict:
|
||||||
{
|
# """Renvoie dict avec les clés:
|
||||||
"coefficient" : float, # 0 si None
|
# last_modif
|
||||||
"description" : str, # de l'évaluation, "" si None
|
# nb_evals_completes
|
||||||
"etat" {
|
# nb_evals_en_cours
|
||||||
"blocked" : bool, # vrai si prise en compte bloquée
|
# nb_evals_vides
|
||||||
"evalcomplete" : bool,
|
# attente
|
||||||
"last_modif" : datetime.datetime | None, # saisie de note la plus récente
|
# """
|
||||||
"nb_notes" : int, # nb notes d'étudiants inscrits
|
|
||||||
"nb_attente" : int, # nb de notes en ATTente (même si bloquée)
|
|
||||||
},
|
|
||||||
"evaluation_id" : int,
|
|
||||||
"jour" : datetime.datetime, # e.date_debut or datetime.datetime(1900, 1, 1)
|
|
||||||
"publish_incomplete" : bool,
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
mod_results = self.modimpls_results.get(evaluation.moduleimpl_id)
|
|
||||||
if mod_results is None:
|
|
||||||
raise ScoTemporaryError() # argh !
|
|
||||||
etat = mod_results.evaluations_etat.get(evaluation.id)
|
|
||||||
if etat is None:
|
|
||||||
raise ScoTemporaryError() # argh !
|
|
||||||
# Date de dernière saisie de note
|
|
||||||
cursor = db.session.execute(
|
|
||||||
sa.text(
|
|
||||||
"SELECT MAX(date) FROM notes_notes WHERE evaluation_id = :evaluation_id"
|
|
||||||
),
|
|
||||||
{"evaluation_id": evaluation.id},
|
|
||||||
)
|
|
||||||
date_modif = cursor.one_or_none()
|
|
||||||
last_modif = date_modif[0] if date_modif else None
|
|
||||||
return {
|
|
||||||
"coefficient": evaluation.coefficient,
|
|
||||||
"description": evaluation.description,
|
|
||||||
"etat": {
|
|
||||||
"blocked": evaluation.is_blocked(),
|
|
||||||
"evalcomplete": etat.is_complete,
|
|
||||||
"nb_attente": etat.nb_attente,
|
|
||||||
"nb_notes": etat.nb_notes,
|
|
||||||
"last_modif": last_modif,
|
|
||||||
},
|
|
||||||
"evaluation_id": evaluation.id,
|
|
||||||
"jour": evaluation.date_debut or datetime.datetime(1900, 1, 1),
|
|
||||||
"publish_incomplete": evaluation.publish_incomplete,
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_mod_evaluation_etat_list(self, modimpl: ModuleImpl) -> list[dict]:
|
|
||||||
"""Liste des états des évaluations de ce module
|
|
||||||
[ evaluation_etat, ... ] (voir get_evaluation_etat)
|
|
||||||
trié par (numero desc, date_debut desc)
|
|
||||||
"""
|
|
||||||
# nouvelle version 2024-02-02
|
|
||||||
return list(
|
|
||||||
reversed(
|
|
||||||
[
|
|
||||||
self.get_evaluation_etat(evaluation)
|
|
||||||
for evaluation in modimpl.evaluations
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# modernisation de get_mod_evaluation_etat_list
|
|
||||||
# utilisé par:
|
|
||||||
# sco_evaluations.do_evaluation_etat_in_mod
|
|
||||||
# e["etat"]["evalcomplete"]
|
|
||||||
# e["etat"]["nb_notes"]
|
|
||||||
# e["etat"]["last_modif"]
|
|
||||||
#
|
|
||||||
# sco_formsemestre_status.formsemestre_description_table
|
|
||||||
# "jour" (qui est e.date_debut or datetime.date(1900, 1, 1))
|
|
||||||
# "description"
|
|
||||||
# "coefficient"
|
|
||||||
# e["etat"]["evalcomplete"]
|
|
||||||
# publish_incomplete
|
|
||||||
#
|
|
||||||
# sco_formsemestre_status.formsemestre_tableau_modules
|
|
||||||
# e["etat"]["nb_notes"]
|
|
||||||
#
|
|
||||||
|
|
||||||
# --- JURY...
|
# --- JURY...
|
||||||
def get_formsemestre_validations(self) -> ValidationsSemestre:
|
def get_formsemestre_validations(self) -> ValidationsSemestre:
|
||||||
@ -436,28 +360,11 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
ue_cap_dict["compense_formsemestre_id"] = None
|
ue_cap_dict["compense_formsemestre_id"] = None
|
||||||
return ue_cap_dict
|
return ue_cap_dict
|
||||||
|
|
||||||
def get_etud_ue_status(self, etudid: int, ue_id: int) -> dict | None:
|
def get_etud_ue_status(self, etudid: int, ue_id: int) -> dict:
|
||||||
"""L'état de l'UE pour cet étudiant.
|
"""L'état de l'UE pour cet étudiant.
|
||||||
Result: dict, ou None si l'UE n'existe pas ou n'est pas dans ce semestre.
|
Result: dict, ou None si l'UE n'est pas dans ce semestre.
|
||||||
{
|
|
||||||
"is_capitalized": # vrai si la version capitalisée est celle prise en compte (meilleure)
|
|
||||||
"was_capitalized":# si elle a été capitalisée (meilleure ou pas)
|
|
||||||
"is_external": # si UE externe
|
|
||||||
"coef_ue": 0.0,
|
|
||||||
"cur_moy_ue": 0.0, # moyenne de l'UE courante
|
|
||||||
"moy": 0.0, # moyenne prise en compte
|
|
||||||
"event_date": # date de la capiltalisation éventuelle (ou None)
|
|
||||||
"ue": ue_dict, # l'UE, comme un dict
|
|
||||||
"formsemestre_id": None,
|
|
||||||
"capitalized_ue_id": None, # l'id de l'UE capitalisée, ou None
|
|
||||||
"ects_pot": 0.0, # deprecated (les ECTS liés à cette UE)
|
|
||||||
"ects": 0.0, # les ECTS acquis grace à cette UE
|
|
||||||
"ects_ue": # les ECTS liés à cette UE
|
|
||||||
}
|
|
||||||
"""
|
"""
|
||||||
ue: UniteEns = db.session.get(UniteEns, ue_id)
|
ue: UniteEns = db.session.get(UniteEns, ue_id)
|
||||||
if not ue:
|
|
||||||
return None
|
|
||||||
ue_dict = ue.to_dict()
|
ue_dict = ue.to_dict()
|
||||||
|
|
||||||
if ue.type == UE_SPORT:
|
if ue.type == UE_SPORT:
|
||||||
@ -476,7 +383,7 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
"ects": 0.0,
|
"ects": 0.0,
|
||||||
"ects_ue": ue.ects,
|
"ects_ue": ue.ects,
|
||||||
}
|
}
|
||||||
if not ue_id in self.etud_moy_ue or not etudid in self.etud_moy_ue[ue_id]:
|
if not ue_id in self.etud_moy_ue:
|
||||||
return None
|
return None
|
||||||
if not self.validations:
|
if not self.validations:
|
||||||
self.validations = res_sem.load_formsemestre_validations(self.formsemestre)
|
self.validations = res_sem.load_formsemestre_validations(self.formsemestre)
|
||||||
@ -518,8 +425,7 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
Corrigez ou faite corriger le programme
|
Corrigez ou faite corriger le programme
|
||||||
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
|
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
|
||||||
formation_id=ue_capitalized.formation_id)}">via cette page</a>.
|
formation_id=ue_capitalized.formation_id)}">via cette page</a>.
|
||||||
""",
|
"""
|
||||||
safe=True,
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Coefs de l'UE capitalisée en formation classique:
|
# Coefs de l'UE capitalisée en formation classique:
|
||||||
@ -534,13 +440,11 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
"is_external": ue_cap["is_external"] if is_capitalized else ue.is_external,
|
"is_external": ue_cap["is_external"] if is_capitalized else ue.is_external,
|
||||||
"coef_ue": coef_ue,
|
"coef_ue": coef_ue,
|
||||||
"ects_pot": ue.ects or 0.0,
|
"ects_pot": ue.ects or 0.0,
|
||||||
"ects": (
|
"ects": self.validations.decisions_jury_ues.get(etudid, {})
|
||||||
self.validations.decisions_jury_ues.get(etudid, {})
|
.get(ue.id, {})
|
||||||
.get(ue.id, {})
|
.get("ects", 0.0)
|
||||||
.get("ects", 0.0)
|
if self.validations.decisions_jury_ues
|
||||||
if self.validations.decisions_jury_ues
|
else 0.0,
|
||||||
else 0.0
|
|
||||||
),
|
|
||||||
"ects_ue": ue.ects,
|
"ects_ue": ue.ects,
|
||||||
"cur_moy_ue": cur_moy_ue,
|
"cur_moy_ue": cur_moy_ue,
|
||||||
"moy": moy_ue,
|
"moy": moy_ue,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -58,6 +58,7 @@ class NotesTableCompat(ResultatsSemestre):
|
|||||||
self.moy_moy = "NA"
|
self.moy_moy = "NA"
|
||||||
self.moy_gen_rangs_by_group = {} # { group_id : (Series, Series) }
|
self.moy_gen_rangs_by_group = {} # { group_id : (Series, Series) }
|
||||||
self.ue_rangs_by_group = {} # { ue_id : {group_id : (Series, Series)}}
|
self.ue_rangs_by_group = {} # { ue_id : {group_id : (Series, Series)}}
|
||||||
|
self.expr_diagnostics = ""
|
||||||
self.parcours = self.formsemestre.formation.get_cursus()
|
self.parcours = self.formsemestre.formation.get_cursus()
|
||||||
self._modimpls_dict_by_ue = {} # local cache
|
self._modimpls_dict_by_ue = {} # local cache
|
||||||
|
|
||||||
@ -216,9 +217,9 @@ class NotesTableCompat(ResultatsSemestre):
|
|||||||
# Rangs / UEs:
|
# Rangs / UEs:
|
||||||
for ue in ues:
|
for ue in ues:
|
||||||
group_moys_ue = self.etud_moy_ue[ue.id][group_members]
|
group_moys_ue = self.etud_moy_ue[ue.id][group_members]
|
||||||
self.ue_rangs_by_group.setdefault(ue.id, {})[group.id] = (
|
self.ue_rangs_by_group.setdefault(ue.id, {})[
|
||||||
moy_sem.comp_ranks_series(group_moys_ue * mask_inscr)
|
group.id
|
||||||
)
|
] = moy_sem.comp_ranks_series(group_moys_ue * mask_inscr)
|
||||||
|
|
||||||
def get_etud_rang(self, etudid: int) -> str:
|
def get_etud_rang(self, etudid: int) -> str:
|
||||||
"""Le rang (classement) de l'étudiant dans le semestre.
|
"""Le rang (classement) de l'étudiant dans le semestre.
|
||||||
@ -289,10 +290,9 @@ class NotesTableCompat(ResultatsSemestre):
|
|||||||
]
|
]
|
||||||
return etudids
|
return etudids
|
||||||
|
|
||||||
def etud_has_decision(self, etudid, include_rcues=True) -> bool:
|
def etud_has_decision(self, etudid) -> bool:
|
||||||
"""True s'il y a une décision de jury pour cet étudiant émanant de ce formsemestre.
|
"""True s'il y a une décision de jury pour cet étudiant émanant de ce formsemestre.
|
||||||
prend aussi en compte les autorisations de passage.
|
prend aussi en compte les autorisations de passage.
|
||||||
Si include_rcues, prend en compte les validation d'RCUEs en BUT (pas d'effet en classic).
|
|
||||||
Sous-classée en BUT pour les RCUEs et années.
|
Sous-classée en BUT pour les RCUEs et années.
|
||||||
"""
|
"""
|
||||||
return bool(
|
return bool(
|
||||||
@ -322,7 +322,7 @@ class NotesTableCompat(ResultatsSemestre):
|
|||||||
validations = self.get_formsemestre_validations()
|
validations = self.get_formsemestre_validations()
|
||||||
return validations.decisions_jury_ues.get(etudid, None)
|
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.
|
"""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.
|
NB: avant jury, rien d'enregistré, donc zéro ECTS.
|
||||||
Optimisation: si decisions_ues est passé, l'utilise, sinon appelle get_etud_decisions_ue()
|
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)
|
decisions_ues = self.get_etud_decisions_ue(etudid)
|
||||||
if not decisions_ues:
|
if not decisions_ues:
|
||||||
return 0.0
|
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:
|
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.
|
"""Decision du jury semestre prise pour cet etudiant, ou None s'il n'y en pas eu.
|
||||||
@ -407,7 +407,7 @@ class NotesTableCompat(ResultatsSemestre):
|
|||||||
de ce module.
|
de ce module.
|
||||||
Évaluation "complete" ssi toutes notes saisies ou en attente.
|
Évaluation "complete" ssi toutes notes saisies ou en attente.
|
||||||
"""
|
"""
|
||||||
modimpl: ModuleImpl = db.session.get(ModuleImpl, moduleimpl_id)
|
modimpl = db.session.get(ModuleImpl, moduleimpl_id)
|
||||||
modimpl_results = self.modimpls_results.get(moduleimpl_id)
|
modimpl_results = self.modimpls_results.get(moduleimpl_id)
|
||||||
if not modimpl_results:
|
if not modimpl_results:
|
||||||
return [] # safeguard
|
return [] # safeguard
|
||||||
@ -422,37 +422,30 @@ class NotesTableCompat(ResultatsSemestre):
|
|||||||
)
|
)
|
||||||
return evaluations
|
return evaluations
|
||||||
|
|
||||||
def get_evaluations_etats(self) -> dict[int, dict]:
|
def get_evaluations_etats(self) -> list[dict]:
|
||||||
""" "état" de chaque évaluation du semestre
|
"""Liste de toutes les évaluations du semestre
|
||||||
{
|
[ {...evaluation et son etat...} ]"""
|
||||||
evaluation_id : {
|
# TODO: à moderniser (voir dans ResultatsSemestre)
|
||||||
"evalcomplete" : bool,
|
# utilisé par
|
||||||
"last_modif" : datetime | None
|
# do_evaluation_etat_in_sem
|
||||||
"nb_notes" : int,
|
|
||||||
}, ...
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
# utilisé par do_evaluation_etat_in_sem
|
|
||||||
evaluations_etats = {}
|
|
||||||
for modimpl in self.formsemestre.modimpls_sorted:
|
|
||||||
for evaluation in modimpl.evaluations:
|
|
||||||
evaluation_etat = self.get_evaluation_etat(evaluation)
|
|
||||||
evaluations_etats[evaluation.id] = evaluation_etat["etat"]
|
|
||||||
return evaluations_etats
|
|
||||||
|
|
||||||
# ancienne version < 2024-02-02
|
from app.scodoc import sco_evaluations
|
||||||
# def get_mod_evaluation_etat_list(self, moduleimpl_id) -> list[dict]:
|
|
||||||
# """Liste des états des évaluations de ce module
|
if not hasattr(self, "_evaluations_etats"):
|
||||||
# ordonnée selon (numero desc, date_debut desc)
|
self._evaluations_etats = sco_evaluations.do_evaluation_list_in_sem(
|
||||||
# """
|
self.formsemestre.id
|
||||||
# # à moderniser: lent, recharge des données que l'on a déjà...
|
)
|
||||||
# # remplacemé par ResultatsSemestre.get_mod_evaluation_etat_list
|
|
||||||
# #
|
return self._evaluations_etats
|
||||||
# return [
|
|
||||||
# e
|
def get_mod_evaluation_etat_list(self, moduleimpl_id) -> list[dict]:
|
||||||
# for e in self.get_evaluations_etats()
|
"""Liste des états des évaluations de ce module"""
|
||||||
# if e["moduleimpl_id"] == moduleimpl_id
|
# XXX TODO à moderniser: lent, recharge des données que l'on a déjà...
|
||||||
# ]
|
return [
|
||||||
|
e
|
||||||
|
for e in self.get_evaluations_etats()
|
||||||
|
if e["moduleimpl_id"] == moduleimpl_id
|
||||||
|
]
|
||||||
|
|
||||||
def get_moduleimpls_attente(self):
|
def get_moduleimpls_attente(self):
|
||||||
"""Liste des modimpls du semestre ayant des notes en attente"""
|
"""Liste des modimpls du semestre ayant des notes en attente"""
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -84,9 +84,6 @@ def scodoc(func):
|
|||||||
|
|
||||||
|
|
||||||
def permission_required(permission):
|
def permission_required(permission):
|
||||||
"""Vérifie les permissions"""
|
|
||||||
|
|
||||||
# Attention: l'API utilise api_permission_required
|
|
||||||
def decorator(f):
|
def decorator(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
|
24
app/email.py
24
app/email.py
@ -1,17 +1,16 @@
|
|||||||
# -*- coding: UTF-8 -*
|
# -*- coding: UTF-8 -*
|
||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
import datetime
|
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
||||||
from flask import current_app, g
|
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.departements import Departement
|
||||||
from app.models.config import ScoDocSiteConfig
|
from app.models.config import ScoDocSiteConfig
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
@ -20,15 +19,7 @@ from app.scodoc import sco_preferences
|
|||||||
def send_async_email(app, msg):
|
def send_async_email(app, msg):
|
||||||
"Send an email, async"
|
"Send an email, async"
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
try:
|
mail.send(msg)
|
||||||
mail.send(msg)
|
|
||||||
except BadHeaderError:
|
|
||||||
log(
|
|
||||||
f"""send_async_email: BadHeaderError
|
|
||||||
msg={msg}
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
def send_email(
|
def send_email(
|
||||||
@ -92,12 +83,9 @@ Adresses d'origine:
|
|||||||
\n\n"""
|
\n\n"""
|
||||||
+ msg.body
|
+ msg.body
|
||||||
)
|
)
|
||||||
now = datetime.datetime.now()
|
|
||||||
formatted_time = now.strftime("%Y-%m-%d %H:%M:%S") + ".{:03d}".format(
|
|
||||||
now.microsecond // 1000
|
|
||||||
)
|
|
||||||
current_app.logger.info(
|
current_app.logger.info(
|
||||||
f"""[{formatted_time}] email sent to{
|
f"""email sent to{
|
||||||
' (mode test)' if email_test_mode_address else ''
|
' (mode test)' if email_test_mode_address else ''
|
||||||
}: {msg.recipients}
|
}: {msg.recipients}
|
||||||
from sender {msg.sender}
|
from sender {msg.sender}
|
||||||
|
@ -6,7 +6,6 @@ from flask import Blueprint
|
|||||||
from app.scodoc import sco_etud
|
from app.scodoc import sco_etud
|
||||||
from app.auth.models import User
|
from app.auth.models import User
|
||||||
from app.models import Departement
|
from app.models import Departement
|
||||||
import app.scodoc.sco_utils as scu
|
|
||||||
|
|
||||||
bp = Blueprint("entreprises", __name__)
|
bp = Blueprint("entreprises", __name__)
|
||||||
|
|
||||||
@ -16,12 +15,12 @@ SIRET_PROVISOIRE_START = "xx"
|
|||||||
|
|
||||||
@bp.app_template_filter()
|
@bp.app_template_filter()
|
||||||
def format_prenom(s):
|
def format_prenom(s):
|
||||||
return scu.format_prenom(s)
|
return sco_etud.format_prenom(s)
|
||||||
|
|
||||||
|
|
||||||
@bp.app_template_filter()
|
@bp.app_template_filter()
|
||||||
def format_nom(s):
|
def format_nom(s):
|
||||||
return scu.format_nom(s)
|
return sco_etud.format_nom(s)
|
||||||
|
|
||||||
|
|
||||||
@bp.app_template_filter()
|
@bp.app_template_filter()
|
||||||
@ -59,4 +58,3 @@ def check_taxe_now(taxes):
|
|||||||
|
|
||||||
|
|
||||||
from app.entreprises import routes
|
from app.entreprises import routes
|
||||||
from app.entreprises.activate import activate_module
|
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
##############################################################################
|
|
||||||
# ScoDoc
|
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
|
||||||
# See LICENSE
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
"""Activation du module entreprises
|
|
||||||
|
|
||||||
L'affichage du module est contrôlé par la config ScoDocConfig.enable_entreprises
|
|
||||||
|
|
||||||
Au moment de l'activation, il est en général utile de proposer de configurer les
|
|
||||||
permissions de rôles standards: AdminEntreprise UtilisateurEntreprise ObservateurEntreprise
|
|
||||||
|
|
||||||
Voir associations dans sco_roles_default
|
|
||||||
|
|
||||||
"""
|
|
||||||
from app.auth.models import Role
|
|
||||||
from app.models import ScoDocSiteConfig
|
|
||||||
from app.scodoc.sco_roles_default import SCO_ROLES_ENTREPRISES_DEFAULT
|
|
||||||
|
|
||||||
|
|
||||||
def activate_module(
|
|
||||||
enable: bool = True, set_default_roles_permission: bool = False
|
|
||||||
) -> bool:
|
|
||||||
"""Active le module et en option donne les permissions aux rôles standards.
|
|
||||||
True si l'état d'activation a changé.
|
|
||||||
"""
|
|
||||||
change = ScoDocSiteConfig.enable_entreprises(enable)
|
|
||||||
if enable and set_default_roles_permission:
|
|
||||||
Role.reset_roles_permissions(SCO_ROLES_ENTREPRISES_DEFAULT)
|
|
||||||
return change
|
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -151,7 +151,7 @@ class EntrepriseHistorique(db.Model):
|
|||||||
__tablename__ = "are_historique"
|
__tablename__ = "are_historique"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
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)
|
entreprise_id = db.Column(db.Integer)
|
||||||
object = db.Column(db.Text)
|
object = db.Column(db.Text)
|
||||||
object_id = db.Column(db.Integer)
|
object_id = db.Column(db.Integer)
|
||||||
|
@ -338,11 +338,9 @@ def add_entreprise():
|
|||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
entreprise = Entreprise(
|
entreprise = Entreprise(
|
||||||
nom=form.nom_entreprise.data.strip(),
|
nom=form.nom_entreprise.data.strip(),
|
||||||
siret=(
|
siret=form.siret.data.strip()
|
||||||
form.siret.data.strip()
|
if 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
|
||||||
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,
|
siret_provisoire=False if form.siret.data.strip() else True,
|
||||||
association=form.association.data,
|
association=form.association.data,
|
||||||
adresse=form.adresse.data.strip(),
|
adresse=form.adresse.data.strip(),
|
||||||
@ -354,7 +352,7 @@ def add_entreprise():
|
|||||||
db.session.add(entreprise)
|
db.session.add(entreprise)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
db.session.refresh(entreprise)
|
db.session.refresh(entreprise)
|
||||||
except Exception:
|
except:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
flash("Une erreur est survenue veuillez réessayer.")
|
flash("Une erreur est survenue veuillez réessayer.")
|
||||||
return render_template(
|
return render_template(
|
||||||
@ -806,9 +804,9 @@ def add_offre(entreprise_id):
|
|||||||
missions=form.missions.data.strip(),
|
missions=form.missions.data.strip(),
|
||||||
duree=form.duree.data.strip(),
|
duree=form.duree.data.strip(),
|
||||||
expiration_date=form.expiration_date.data,
|
expiration_date=form.expiration_date.data,
|
||||||
correspondant_id=(
|
correspondant_id=form.correspondant.data
|
||||||
form.correspondant.data if form.correspondant.data != "" else None
|
if form.correspondant.data != ""
|
||||||
),
|
else None,
|
||||||
)
|
)
|
||||||
db.session.add(offre)
|
db.session.add(offre)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@ -1330,11 +1328,9 @@ def add_contact(entreprise_id):
|
|||||||
).first_or_404(description=f"entreprise {entreprise_id} inconnue")
|
).first_or_404(description=f"entreprise {entreprise_id} inconnue")
|
||||||
form = ContactCreationForm(
|
form = ContactCreationForm(
|
||||||
date=f"{datetime.now().strftime('%Y-%m-%dT%H:%M')}",
|
date=f"{datetime.now().strftime('%Y-%m-%dT%H:%M')}",
|
||||||
utilisateur=(
|
utilisateur=f"{current_user.nom} {current_user.prenom} ({current_user.user_name})"
|
||||||
f"{current_user.nom} {current_user.prenom} ({current_user.user_name})"
|
if current_user.nom and current_user.prenom
|
||||||
if current_user.nom and current_user.prenom
|
else "",
|
||||||
else ""
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
if request.method == "POST" and form.cancel.data:
|
if request.method == "POST" and form.cancel.data:
|
||||||
return redirect(url_for("entreprises.contacts", entreprise_id=entreprise_id))
|
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_debut=form.date_debut.data,
|
||||||
date_fin=form.date_fin.data,
|
date_fin=form.date_fin.data,
|
||||||
formation_text=formation.formsemestre.titre if formation else None,
|
formation_text=formation.formsemestre.titre if formation else None,
|
||||||
formation_scodoc=(
|
formation_scodoc=formation.formsemestre.formsemestre_id
|
||||||
formation.formsemestre.formsemestre_id if formation else None
|
if formation
|
||||||
),
|
else None,
|
||||||
notes=form.notes.data.strip(),
|
notes=form.notes.data.strip(),
|
||||||
)
|
)
|
||||||
db.session.add(stage_apprentissage)
|
db.session.add(stage_apprentissage)
|
||||||
@ -1584,8 +1580,8 @@ def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
elif request.method == "GET":
|
elif request.method == "GET":
|
||||||
form.etudiant.data = f"""{scu.format_nom(etudiant.nom)} {
|
form.etudiant.data = f"""{sco_etud.format_nom(etudiant.nom)} {
|
||||||
scu.format_prenom(etudiant.prenom)}"""
|
sco_etud.format_prenom(etudiant.prenom)}"""
|
||||||
form.etudid.data = etudiant.id
|
form.etudid.data = etudiant.id
|
||||||
form.type_offre.data = stage_apprentissage.type_offre
|
form.type_offre.data = stage_apprentissage.type_offre
|
||||||
form.date_debut.data = stage_apprentissage.date_debut
|
form.date_debut.data = stage_apprentissage.date_debut
|
||||||
@ -1703,7 +1699,7 @@ def json_etudiants():
|
|||||||
list = []
|
list = []
|
||||||
for etudiant in etudiants:
|
for etudiant in etudiants:
|
||||||
content = {}
|
content = {}
|
||||||
value = f"{scu.format_nom(etudiant.nom)} {scu.format_prenom(etudiant.prenom)}"
|
value = f"{sco_etud.format_nom(etudiant.nom)} {sco_etud.format_prenom(etudiant.prenom)}"
|
||||||
if etudiant.inscription_courante() is not None:
|
if etudiant.inscription_courante() is not None:
|
||||||
content = {
|
content = {
|
||||||
"id": f"{etudiant.id}",
|
"id": f"{etudiant.id}",
|
||||||
@ -1806,7 +1802,7 @@ def import_donnees():
|
|||||||
db.session.add(entreprise)
|
db.session.add(entreprise)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
db.session.refresh(entreprise)
|
db.session.refresh(entreprise)
|
||||||
except Exception:
|
except:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
flash("Une erreur est survenue veuillez réessayer.")
|
flash("Une erreur est survenue veuillez réessayer.")
|
||||||
return render_template(
|
return render_template(
|
||||||
|
@ -1,176 +0,0 @@
|
|||||||
# -*- 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 ajout d'une "assiduité" sur un étudiant
|
|
||||||
Formulaire ajout d'un justificatif sur un étudiant
|
|
||||||
"""
|
|
||||||
|
|
||||||
from flask_wtf import FlaskForm
|
|
||||||
from flask_wtf.file import MultipleFileField
|
|
||||||
from wtforms import (
|
|
||||||
BooleanField,
|
|
||||||
SelectField,
|
|
||||||
StringField,
|
|
||||||
SubmitField,
|
|
||||||
RadioField,
|
|
||||||
TextAreaField,
|
|
||||||
validators,
|
|
||||||
)
|
|
||||||
from wtforms.validators import DataRequired
|
|
||||||
from app.scodoc import sco_utils as scu
|
|
||||||
|
|
||||||
|
|
||||||
class AjoutAssiOrJustForm(FlaskForm):
|
|
||||||
"""Elements communs aux deux formulaires ajout
|
|
||||||
assiduité et justificatif
|
|
||||||
"""
|
|
||||||
|
|
||||||
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}
|
|
||||||
|
|
||||||
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 (si plusieurs jours)",
|
|
||||||
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})
|
|
||||||
|
|
||||||
|
|
||||||
class AjoutAssiduiteEtudForm(AjoutAssiOrJustForm):
|
|
||||||
"Formulaire de saisie d'une assiduité pour un étudiant"
|
|
||||||
|
|
||||||
description = TextAreaField(
|
|
||||||
"Description",
|
|
||||||
render_kw={
|
|
||||||
"id": "description",
|
|
||||||
"cols": 75,
|
|
||||||
"rows": 4,
|
|
||||||
"maxlength": 500,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
assi_etat = RadioField(
|
|
||||||
"Signaler:",
|
|
||||||
choices=[("absent", "absence"), ("retard", "retard"), ("present", "présence")],
|
|
||||||
default="absent",
|
|
||||||
validators=[
|
|
||||||
validators.DataRequired("spécifiez le type d'évènement à signaler"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
modimpl = SelectField(
|
|
||||||
"Module",
|
|
||||||
choices={}, # will be populated dynamically
|
|
||||||
)
|
|
||||||
est_just = BooleanField("Justifiée")
|
|
||||||
|
|
||||||
|
|
||||||
class AjoutJustificatifEtudForm(AjoutAssiOrJustForm):
|
|
||||||
"Formulaire de saisie d'un justificatif pour un étudiant"
|
|
||||||
|
|
||||||
raison = TextAreaField(
|
|
||||||
"Raison",
|
|
||||||
render_kw={
|
|
||||||
"id": "raison",
|
|
||||||
"cols": 75,
|
|
||||||
"rows": 4,
|
|
||||||
"maxlength": 500,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
etat = SelectField(
|
|
||||||
"État du justificatif",
|
|
||||||
choices=[], # sera rempli dynamiquement
|
|
||||||
validators=[DataRequired(message="This field is required.")],
|
|
||||||
)
|
|
||||||
fichiers = MultipleFileField(label="Ajouter des fichiers")
|
|
@ -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})
|
|
@ -17,7 +17,7 @@ def UEParcoursECTSForm(ue: UniteEns) -> FlaskForm:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
parcours: list[ApcParcours] = ue.formation.referentiel_competence.parcours
|
parcours: list[ApcParcours] = ue.formation.referentiel_competence.parcours
|
||||||
# Initialise un champ de saisie par parcours
|
# Initialise un champs de saisie par parcours
|
||||||
for parcour in parcours:
|
for parcour in parcours:
|
||||||
ects = ue.get_ects(parcour, only_parcours=True)
|
ects = ue.get_ects(parcour, only_parcours=True)
|
||||||
setattr(
|
setattr(
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
#
|
#
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -43,7 +43,6 @@ def gen_formsemestre_change_formation_form(
|
|||||||
formations: list[Formation],
|
formations: list[Formation],
|
||||||
) -> FormSemestreChangeFormationForm:
|
) -> FormSemestreChangeFormationForm:
|
||||||
"Create our dynamical form"
|
"Create our dynamical form"
|
||||||
|
|
||||||
# see https://wtforms.readthedocs.io/en/2.3.x/specific_problems/#dynamic-form-composition
|
# see https://wtforms.readthedocs.io/en/2.3.x/specific_problems/#dynamic-form-composition
|
||||||
class F(FormSemestreChangeFormationForm):
|
class F(FormSemestreChangeFormationForm):
|
||||||
pass
|
pass
|
||||||
|
@ -4,7 +4,7 @@ Formulaire configuration des codes Apo et EDT des modimps d'un formsemestre
|
|||||||
|
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import validators
|
from wtforms import validators
|
||||||
from wtforms.fields.simple import StringField, SubmitField
|
from wtforms.fields.simple import BooleanField, StringField, SubmitField
|
||||||
|
|
||||||
from app.models import FormSemestre, ModuleImpl
|
from app.models import FormSemestre, ModuleImpl
|
||||||
|
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
"""
|
|
||||||
Formulaire activation module entreprises
|
|
||||||
"""
|
|
||||||
|
|
||||||
from flask_wtf import FlaskForm
|
|
||||||
from wtforms.fields.simple import BooleanField, SubmitField
|
|
||||||
|
|
||||||
from app.models import ScoDocSiteConfig
|
|
||||||
|
|
||||||
|
|
||||||
class ActivateEntreprisesForm(FlaskForm):
|
|
||||||
"Formulaire activation module entreprises"
|
|
||||||
set_default_roles_permission = BooleanField(
|
|
||||||
"(re)mettre les rôles 'Entreprise' à leurs valeurs par défaut"
|
|
||||||
)
|
|
||||||
submit = SubmitField("Valider")
|
|
||||||
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -29,16 +29,56 @@
|
|||||||
Formulaire configuration Module Assiduités
|
Formulaire configuration Module Assiduités
|
||||||
"""
|
"""
|
||||||
import datetime
|
import datetime
|
||||||
import re
|
|
||||||
|
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import DecimalField, SubmitField, ValidationError
|
from wtforms import DecimalField, SubmitField, ValidationError
|
||||||
from wtforms.fields.simple import StringField
|
from wtforms.fields.simple import StringField
|
||||||
from wtforms.validators import Optional, Length
|
from wtforms.validators import Optional
|
||||||
|
|
||||||
from wtforms.widgets import TimeInput
|
from wtforms.widgets import TimeInput
|
||||||
|
|
||||||
|
|
||||||
|
class TimeField(StringField):
|
||||||
|
"""HTML5 time input.
|
||||||
|
tiré de : https://gist.github.com/tachyondecay/6016d32f65a996d0d94f
|
||||||
|
"""
|
||||||
|
|
||||||
|
widget = TimeInput()
|
||||||
|
|
||||||
|
def __init__(self, label=None, validators=None, fmt="%H:%M:%S", **kwargs):
|
||||||
|
super(TimeField, self).__init__(label, validators, **kwargs)
|
||||||
|
self.fmt = fmt
|
||||||
|
self.data = None
|
||||||
|
|
||||||
|
def _value(self):
|
||||||
|
if self.raw_data:
|
||||||
|
return " ".join(self.raw_data)
|
||||||
|
if self.data and isinstance(self.data, str):
|
||||||
|
self.data = datetime.time(*map(int, self.data.split(":")))
|
||||||
|
return self.data and self.data.strftime(self.fmt) or ""
|
||||||
|
|
||||||
|
def process_formdata(self, valuelist):
|
||||||
|
if valuelist:
|
||||||
|
time_str = " ".join(valuelist)
|
||||||
|
try:
|
||||||
|
components = time_str.split(":")
|
||||||
|
hour = 0
|
||||||
|
minutes = 0
|
||||||
|
seconds = 0
|
||||||
|
if len(components) in range(2, 4):
|
||||||
|
hour = int(components[0])
|
||||||
|
minutes = int(components[1])
|
||||||
|
|
||||||
|
if len(components) == 3:
|
||||||
|
seconds = int(components[2])
|
||||||
|
else:
|
||||||
|
raise ValueError
|
||||||
|
self.data = datetime.time(hour, minutes, seconds)
|
||||||
|
except ValueError as exc:
|
||||||
|
self.data = None
|
||||||
|
raise ValueError(self.gettext("Not a valid time string")) from exc
|
||||||
|
|
||||||
|
|
||||||
def check_tick_time(form, field):
|
def check_tick_time(form, field):
|
||||||
"""Le tick_time doit être entre 0 et 60 minutes"""
|
"""Le tick_time doit être entre 0 et 60 minutes"""
|
||||||
if field.data < 1 or field.data > 59:
|
if field.data < 1 or field.data > 59:
|
||||||
@ -58,57 +98,14 @@ def check_ics_path(form, field):
|
|||||||
raise ValidationError("Le chemin vers les ics doit utiliser {edt_id}")
|
raise ValidationError("Le chemin vers les ics doit utiliser {edt_id}")
|
||||||
|
|
||||||
|
|
||||||
def check_ics_field(form, field):
|
|
||||||
"""Vérifie que c'est un nom de champ crédible: un mot alphanumérique"""
|
|
||||||
if not re.match(r"^[a-zA-Z\-_0-9]+$", field.data):
|
|
||||||
raise ValidationError("nom de champ ics invalide")
|
|
||||||
|
|
||||||
|
|
||||||
def check_ics_regexp(form, field):
|
|
||||||
"""Vérifie que field est une expresssion régulière"""
|
|
||||||
value = field.data.strip()
|
|
||||||
# check that it compiles
|
|
||||||
try:
|
|
||||||
_ = re.compile(value)
|
|
||||||
except re.error as exc:
|
|
||||||
raise ValidationError("expression invalide") from exc
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigAssiduitesForm(FlaskForm):
|
class ConfigAssiduitesForm(FlaskForm):
|
||||||
"Formulaire paramétrage Module Assiduité"
|
"Formulaire paramétrage Module Assiduité"
|
||||||
assi_morning_time = StringField(
|
|
||||||
"Début de la journée",
|
|
||||||
default="",
|
|
||||||
validators=[Length(max=5)],
|
|
||||||
render_kw={
|
|
||||||
"class": "timepicker",
|
|
||||||
"size": 5,
|
|
||||||
"id": "assi_morning_time",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
assi_lunch_time = StringField(
|
|
||||||
"Heure de midi (date pivot entre matin et après-midi)",
|
|
||||||
default="",
|
|
||||||
validators=[Length(max=5)],
|
|
||||||
render_kw={
|
|
||||||
"class": "timepicker",
|
|
||||||
"size": 5,
|
|
||||||
"id": "assi_lunch_time",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
assi_afternoon_time = StringField(
|
|
||||||
"Fin de la journée",
|
|
||||||
validators=[Length(max=5)],
|
|
||||||
default="",
|
|
||||||
render_kw={
|
|
||||||
"class": "timepicker",
|
|
||||||
"size": 5,
|
|
||||||
"id": "assi_afternoon_time",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assi_tick_time = DecimalField(
|
morning_time = TimeField("Début de la journée")
|
||||||
|
lunch_time = TimeField("Heure de midi (date pivot entre Matin et Après Midi)")
|
||||||
|
afternoon_time = TimeField("Fin de la journée")
|
||||||
|
|
||||||
|
tick_time = DecimalField(
|
||||||
"Granularité de la timeline (temps en minutes)",
|
"Granularité de la timeline (temps en minutes)",
|
||||||
places=0,
|
places=0,
|
||||||
validators=[check_tick_time],
|
validators=[check_tick_time],
|
||||||
@ -122,69 +119,6 @@ class ConfigAssiduitesForm(FlaskForm):
|
|||||||
Si ce champ n'est pas renseigné, les emplois du temps ne seront pas utilisés.""",
|
Si ce champ n'est pas renseigné, les emplois du temps ne seront pas utilisés.""",
|
||||||
validators=[Optional(), check_ics_path],
|
validators=[Optional(), check_ics_path],
|
||||||
)
|
)
|
||||||
edt_ics_user_path = StringField(
|
|
||||||
label="Chemin vers les ics des utilisateurs (enseignants)",
|
|
||||||
description="""Optionnel. Chemin absolu unix sur le serveur vers le fichier ics donnant l'emploi
|
|
||||||
du temps d'un enseignant. La balise <tt>{edt_id}</tt> sera remplacée par l'edt_id du
|
|
||||||
de l'utilisateur.
|
|
||||||
Dans certains cas (XXX), ScoDoc peut générer ces fichiers et les écrira suivant
|
|
||||||
ce chemin (avec edt_id).
|
|
||||||
""",
|
|
||||||
validators=[Optional(), check_ics_path],
|
|
||||||
)
|
|
||||||
|
|
||||||
edt_ics_title_field = StringField(
|
|
||||||
label="Champ contenant le titre",
|
|
||||||
description="""champ de l'évènement calendrier: DESCRIPTION, SUMMARY, ...""",
|
|
||||||
validators=[Optional(), check_ics_field],
|
|
||||||
)
|
|
||||||
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.
|
|
||||||
Exemple: <tt>Matière : \w+ - ([\w\.\s']+)</tt>
|
|
||||||
""",
|
|
||||||
validators=[Optional(), check_ics_regexp],
|
|
||||||
)
|
|
||||||
edt_ics_group_field = StringField(
|
|
||||||
label="Champ contenant le groupe",
|
|
||||||
description="""champ de l'évènement calendrier: DESCRIPTION, SUMMARY, ...""",
|
|
||||||
validators=[Optional(), check_ics_field],
|
|
||||||
)
|
|
||||||
edt_ics_group_regexp = StringField(
|
|
||||||
label="Extraction du groupe",
|
|
||||||
description=r"""expression régulière python dont le premier groupe doit
|
|
||||||
correspondre à l'identifiant de groupe de l'emploi du temps.
|
|
||||||
Exemple: <tt>.*- ([\w\s]+)$</tt>
|
|
||||||
""",
|
|
||||||
validators=[Optional(), check_ics_regexp],
|
|
||||||
)
|
|
||||||
edt_ics_mod_field = StringField(
|
|
||||||
label="Champ contenant le module",
|
|
||||||
description="""champ de l'évènement calendrier: DESCRIPTION, SUMMARY, ...""",
|
|
||||||
validators=[Optional(), check_ics_field],
|
|
||||||
)
|
|
||||||
edt_ics_mod_regexp = StringField(
|
|
||||||
label="Extraction du module",
|
|
||||||
description=r"""expression régulière python dont le premier groupe doit
|
|
||||||
correspondre à l'identifiant (code) du module de l'emploi du temps.
|
|
||||||
Exemple: <tt>Matière : ([A-Z][A-Z0-9]+)</tt>
|
|
||||||
""",
|
|
||||||
validators=[Optional(), check_ics_regexp],
|
|
||||||
)
|
|
||||||
edt_ics_uid_field = StringField(
|
|
||||||
label="Champ contenant les enseignants",
|
|
||||||
description="""champ de l'évènement calendrier: DESCRIPTION, SUMMARY, ...""",
|
|
||||||
validators=[Optional(), check_ics_field],
|
|
||||||
)
|
|
||||||
edt_ics_uid_regexp = StringField(
|
|
||||||
label="Extraction des enseignants",
|
|
||||||
description=r"""expression régulière python permettant d'extraire les
|
|
||||||
identifiants des enseignants associés à l'évènement.
|
|
||||||
(contrairement aux autres champs, il peut y avoir plusieurs enseignants par évènement.)
|
|
||||||
Exemple: <tt>[0-9]+</tt>
|
|
||||||
""",
|
|
||||||
validators=[Optional(), check_ics_regexp],
|
|
||||||
)
|
|
||||||
submit = SubmitField("Valider")
|
submit = SubmitField("Valider")
|
||||||
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -82,7 +82,7 @@ class ConfigCASForm(FlaskForm):
|
|||||||
|
|
||||||
cas_attribute_id = StringField(
|
cas_attribute_id = StringField(
|
||||||
label="Attribut CAS utilisé comme id (laissez vide pour prendre l'id par défaut)",
|
label="Attribut CAS utilisé comme id (laissez vide pour prendre l'id par défaut)",
|
||||||
description="""Le champ CAS qui sera considéré comme l'id unique des
|
description="""Le champs CAS qui sera considéré comme l'id unique des
|
||||||
comptes utilisateurs.""",
|
comptes utilisateurs.""",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -48,15 +48,13 @@ class BonusConfigurationForm(FlaskForm):
|
|||||||
for (name, displayed_name) in ScoDocSiteConfig.get_bonus_sport_class_list()
|
for (name, displayed_name) in ScoDocSiteConfig.get_bonus_sport_class_list()
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
submit_bonus = SubmitField("Enregistrer ce bonus")
|
submit_bonus = SubmitField("Valider")
|
||||||
cancel_bonus = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
cancel_bonus = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
||||||
|
|
||||||
|
|
||||||
class ScoDocConfigurationForm(FlaskForm):
|
class ScoDocConfigurationForm(FlaskForm):
|
||||||
"Panneau de configuration avancée"
|
"Panneau de configuration avancée"
|
||||||
disable_passerelle = BooleanField( # disable car par défaut activée
|
enable_entreprises = BooleanField("activer le module <em>entreprises</em>")
|
||||||
"""cacher les fonctions liées à une passerelle de publication des résultats vers les étudiants ("œil"). N'affecte pas l'API, juste la présentation."""
|
|
||||||
)
|
|
||||||
month_debut_annee_scolaire = SelectField(
|
month_debut_annee_scolaire = SelectField(
|
||||||
label="Mois de début des années scolaires",
|
label="Mois de début des années scolaires",
|
||||||
description="""Date pivot. En France métropolitaine, août.
|
description="""Date pivot. En France métropolitaine, août.
|
||||||
@ -79,13 +77,8 @@ class ScoDocConfigurationForm(FlaskForm):
|
|||||||
Attention: si ce champ peut aussi être défini dans chaque département.""",
|
Attention: si ce champ peut aussi être défini dans chaque département.""",
|
||||||
validators=[Optional(), Email()],
|
validators=[Optional(), Email()],
|
||||||
)
|
)
|
||||||
user_require_email_institutionnel = BooleanField(
|
disable_bul_pdf = BooleanField("empêcher les exports des bulletins en PDF")
|
||||||
"imposer la saisie du mail institutionnel dans le formulaire de création utilisateur"
|
submit_scodoc = SubmitField("Valider")
|
||||||
)
|
|
||||||
disable_bul_pdf = BooleanField(
|
|
||||||
"interdire les exports des bulletins en PDF (déconseillé)"
|
|
||||||
)
|
|
||||||
submit_scodoc = SubmitField("Enregistrer ces paramètres")
|
|
||||||
cancel_scodoc = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
cancel_scodoc = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
||||||
|
|
||||||
|
|
||||||
@ -100,12 +93,10 @@ def configuration():
|
|||||||
form_scodoc = ScoDocConfigurationForm(
|
form_scodoc = ScoDocConfigurationForm(
|
||||||
data={
|
data={
|
||||||
"enable_entreprises": ScoDocSiteConfig.is_entreprises_enabled(),
|
"enable_entreprises": ScoDocSiteConfig.is_entreprises_enabled(),
|
||||||
"disable_passerelle": ScoDocSiteConfig.is_passerelle_disabled(),
|
|
||||||
"month_debut_annee_scolaire": ScoDocSiteConfig.get_month_debut_annee_scolaire(),
|
"month_debut_annee_scolaire": ScoDocSiteConfig.get_month_debut_annee_scolaire(),
|
||||||
"month_debut_periode2": ScoDocSiteConfig.get_month_debut_periode2(),
|
"month_debut_periode2": ScoDocSiteConfig.get_month_debut_periode2(),
|
||||||
"email_from_addr": ScoDocSiteConfig.get("email_from_addr"),
|
"email_from_addr": ScoDocSiteConfig.get("email_from_addr"),
|
||||||
"disable_bul_pdf": ScoDocSiteConfig.is_bul_pdf_disabled(),
|
"disable_bul_pdf": ScoDocSiteConfig.is_bul_pdf_disabled(),
|
||||||
"user_require_email_institutionnel": ScoDocSiteConfig.is_user_require_email_institutionnel_enabled(),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if request.method == "POST" and (
|
if request.method == "POST" and (
|
||||||
@ -126,12 +117,12 @@ def configuration():
|
|||||||
flash("Fonction bonus inchangée.")
|
flash("Fonction bonus inchangée.")
|
||||||
return redirect(url_for("scodoc.index"))
|
return redirect(url_for("scodoc.index"))
|
||||||
elif form_scodoc.submit_scodoc.data and form_scodoc.validate():
|
elif form_scodoc.submit_scodoc.data and form_scodoc.validate():
|
||||||
if ScoDocSiteConfig.disable_passerelle(
|
if ScoDocSiteConfig.enable_entreprises(
|
||||||
disabled=form_scodoc.data["disable_passerelle"]
|
enabled=form_scodoc.data["enable_entreprises"]
|
||||||
):
|
):
|
||||||
flash(
|
flash(
|
||||||
"Fonction passerelle "
|
"Module entreprise "
|
||||||
+ ("cachées" if form_scodoc.data["disable_passerelle"] else "montrées")
|
+ ("activé" if form_scodoc.data["enable_entreprises"] else "désactivé")
|
||||||
)
|
)
|
||||||
if ScoDocSiteConfig.set_month_debut_annee_scolaire(
|
if ScoDocSiteConfig.set_month_debut_annee_scolaire(
|
||||||
int(form_scodoc.data["month_debut_annee_scolaire"])
|
int(form_scodoc.data["month_debut_annee_scolaire"])
|
||||||
@ -158,23 +149,10 @@ def configuration():
|
|||||||
"Exports PDF "
|
"Exports PDF "
|
||||||
+ ("désactivés" if form_scodoc.data["disable_bul_pdf"] else "réactivés")
|
+ ("désactivés" if form_scodoc.data["disable_bul_pdf"] else "réactivés")
|
||||||
)
|
)
|
||||||
if ScoDocSiteConfig.set(
|
|
||||||
"user_require_email_institutionnel",
|
|
||||||
"on" if form_scodoc.data["user_require_email_institutionnel"] else "",
|
|
||||||
):
|
|
||||||
flash(
|
|
||||||
(
|
|
||||||
"impose"
|
|
||||||
if form_scodoc.data["user_require_email_institutionnel"]
|
|
||||||
else "n'impose pas"
|
|
||||||
)
|
|
||||||
+ " la saisie du mail institutionnel des utilisateurs"
|
|
||||||
)
|
|
||||||
return redirect(url_for("scodoc.index"))
|
return redirect(url_for("scodoc.index"))
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"configuration.j2",
|
"configuration.j2",
|
||||||
is_entreprises_enabled=ScoDocSiteConfig.is_entreprises_enabled(),
|
|
||||||
form_bonus=form_bonus,
|
form_bonus=form_bonus,
|
||||||
form_scodoc=form_scodoc,
|
form_scodoc=form_scodoc,
|
||||||
scu=scu,
|
scu=scu,
|
||||||
|
@ -1,49 +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 configuration RGPD
|
|
||||||
"""
|
|
||||||
|
|
||||||
from flask_wtf import FlaskForm
|
|
||||||
from wtforms import SubmitField
|
|
||||||
from wtforms.fields.simple import TextAreaField
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigRGPDForm(FlaskForm):
|
|
||||||
"Formulaire paramétrage RGPD"
|
|
||||||
rgpd_coordonnees_dpo = TextAreaField(
|
|
||||||
label="Optionnel: coordonnées du DPO",
|
|
||||||
description="""Le délégué à la protection des données (DPO) est chargé de mettre en œuvre
|
|
||||||
la conformité au règlement européen sur la protection des données (RGPD) au sein de l’organisme.
|
|
||||||
Indiquer ici les coordonnées (format libre) qui seront affichées aux utilisateurs de ScoDoc.
|
|
||||||
""",
|
|
||||||
render_kw={"rows": 5, "cols": 72},
|
|
||||||
)
|
|
||||||
|
|
||||||
submit = SubmitField("Valider")
|
|
||||||
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
|
@ -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 ""
|
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -1,65 +0,0 @@
|
|||||||
##############################################################################
|
|
||||||
#
|
|
||||||
# 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 options génération table poursuite études (PE)
|
|
||||||
"""
|
|
||||||
|
|
||||||
from flask_wtf import FlaskForm
|
|
||||||
from wtforms import BooleanField, HiddenField, SubmitField
|
|
||||||
|
|
||||||
|
|
||||||
class ParametrageClasseurPE(FlaskForm):
|
|
||||||
"Formulaire paramétrage génération classeur PE"
|
|
||||||
# cohorte_restreinte = BooleanField(
|
|
||||||
# "Restreindre aux étudiants inscrits dans le semestre (sans interclassement de promotion) (à venir)"
|
|
||||||
# )
|
|
||||||
moyennes_tags = BooleanField(
|
|
||||||
"Générer les moyennes sur les tags de modules personnalisés (cf. programme de formation)",
|
|
||||||
default=True,
|
|
||||||
render_kw={"checked": ""},
|
|
||||||
)
|
|
||||||
moyennes_ue_res_sae = BooleanField(
|
|
||||||
"Générer les moyennes des ressources et des SAEs",
|
|
||||||
default=True,
|
|
||||||
render_kw={"checked": ""},
|
|
||||||
)
|
|
||||||
moyennes_ues_rcues = BooleanField(
|
|
||||||
"Générer les moyennes par RCUEs (compétences) et leurs synthèses HTML étudiant par étudiant",
|
|
||||||
default=True,
|
|
||||||
render_kw={"checked": ""},
|
|
||||||
)
|
|
||||||
|
|
||||||
min_max_moy = BooleanField("Afficher les colonnes min/max/moy")
|
|
||||||
|
|
||||||
# synthese_individuelle_etud = BooleanField(
|
|
||||||
# "Générer (suppose les RCUES)"
|
|
||||||
# )
|
|
||||||
publipostage = BooleanField(
|
|
||||||
"Nomme les moyennes pour publipostage",
|
|
||||||
# default=False,
|
|
||||||
# render_kw={"checked": ""},
|
|
||||||
)
|
|
||||||
submit = SubmitField("Générer les classeurs poursuites d'études")
|
|
||||||
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
|
@ -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
|
"""Modèles base de données ScoDoc
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import abort, g
|
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
import app
|
|
||||||
from app import db
|
from app import db
|
||||||
|
|
||||||
CODE_STR_LEN = 16 # chaine pour les codes
|
CODE_STR_LEN = 16 # chaine pour les codes
|
||||||
@ -25,17 +23,8 @@ convention = {
|
|||||||
metadata_obj = sqlalchemy.MetaData(naming_convention=convention)
|
metadata_obj = sqlalchemy.MetaData(naming_convention=convention)
|
||||||
|
|
||||||
|
|
||||||
class ScoDocModel(db.Model):
|
class ScoDocModel:
|
||||||
"""Superclass for our models. Add some useful methods for editing, cloning, etc.
|
"Mixin class for our models. Add somme useful methods for editing, cloning, etc."
|
||||||
- clone() : clone object and add copy to session, do not commit.
|
|
||||||
- create_from_dict() : create instance from given dict, applying conversions.
|
|
||||||
- convert_dict_fields() : convert dict values, called before instance creation.
|
|
||||||
By default, do nothing.
|
|
||||||
- from_dict() : update object using data from dict. data is first converted.
|
|
||||||
- edit() : update from wtf form.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__abstract__ = True # declare an abstract class for SQLAlchemy
|
|
||||||
|
|
||||||
def clone(self, not_copying=()):
|
def clone(self, not_copying=()):
|
||||||
"""Clone, not copying the given attrs
|
"""Clone, not copying the given attrs
|
||||||
@ -51,28 +40,21 @@ class ScoDocModel(db.Model):
|
|||||||
return copy
|
return copy
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_from_dict(cls, data: dict) -> "ScoDocModel":
|
def create_from_dict(cls, data: dict):
|
||||||
"""Create a new instance of the model with attributes given in dict.
|
"""Create a new instance of the model with attributes given in dict.
|
||||||
The instance is added to the session (but not flushed nor committed).
|
The instance is added to the session (but not flushed nor committed).
|
||||||
Use only relevant attributes for the given model and ignore others.
|
Use only relevant arributes for the given model and ignore others.
|
||||||
"""
|
"""
|
||||||
if data:
|
args = cls.convert_dict_fields(cls.filter_model_attributes(data))
|
||||||
args = cls.convert_dict_fields(cls.filter_model_attributes(data))
|
obj = cls(**args)
|
||||||
if args:
|
|
||||||
obj = cls(**args)
|
|
||||||
else:
|
|
||||||
obj = cls()
|
|
||||||
else:
|
|
||||||
obj = cls()
|
|
||||||
db.session.add(obj)
|
db.session.add(obj)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def filter_model_attributes(cls, data: 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.
|
"""Returns a copy of dict with only the keys belonging to the Model and not in excluded.
|
||||||
Add 'id' to excluded."""
|
By default, excluded == { 'id' }"""
|
||||||
excluded = excluded or set()
|
excluded = {"id"} if excluded is None else set()
|
||||||
excluded.add("id") # always exclude id
|
|
||||||
# Les attributs du modèle qui sont des variables: (élimine les __ et les alias comme adm_id)
|
# Les attributs du modèle qui sont des variables: (élimine les __ et les alias comme adm_id)
|
||||||
my_attributes = [
|
my_attributes = [
|
||||||
a
|
a
|
||||||
@ -88,7 +70,7 @@ class ScoDocModel(db.Model):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def convert_dict_fields(cls, args: dict) -> dict:
|
def convert_dict_fields(cls, args: dict) -> dict:
|
||||||
"""Convert fields from the given dict to model's attributes values. No side effect.
|
"""Convert fields in the given dict. No side effect.
|
||||||
By default, do nothing, but is overloaded by some subclasses.
|
By default, do nothing, but is overloaded by some subclasses.
|
||||||
args: dict with args in application.
|
args: dict with args in application.
|
||||||
returns: dict to store in model's db.
|
returns: dict to store in model's db.
|
||||||
@ -96,63 +78,13 @@ class ScoDocModel(db.Model):
|
|||||||
# virtual, by default, do nothing
|
# virtual, by default, do nothing
|
||||||
return args
|
return args
|
||||||
|
|
||||||
def from_dict(self, args: dict, excluded: set[str] | None = None) -> bool:
|
def from_dict(self, args: dict):
|
||||||
"""Update object's fields given in dict. Add to session but don't commit.
|
"Update object's fields given in dict. Add to session but don't commit."
|
||||||
True if modification.
|
args_dict = self.convert_dict_fields(self.filter_model_attributes(args))
|
||||||
"""
|
|
||||||
args_dict = self.convert_dict_fields(
|
|
||||||
self.filter_model_attributes(args, excluded=excluded)
|
|
||||||
)
|
|
||||||
modified = False
|
|
||||||
for key, value in args_dict.items():
|
for key, value in args_dict.items():
|
||||||
if hasattr(self, key) and value != getattr(self, key):
|
if hasattr(self, key):
|
||||||
setattr(self, key, value)
|
setattr(self, key, value)
|
||||||
modified = True
|
|
||||||
db.session.add(self)
|
db.session.add(self)
|
||||||
return modified
|
|
||||||
|
|
||||||
def edit_from_form(self, form) -> bool:
|
|
||||||
"""Generic edit method for updating model instance.
|
|
||||||
True if modification.
|
|
||||||
"""
|
|
||||||
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.absences import Absence, AbsenceNotification, BilletAbsence
|
||||||
@ -198,6 +130,7 @@ from app.models.notes import (
|
|||||||
NotesNotesLog,
|
NotesNotesLog,
|
||||||
)
|
)
|
||||||
from app.models.validations import (
|
from app.models.validations import (
|
||||||
|
ScolarEvent,
|
||||||
ScolarFormSemestreValidation,
|
ScolarFormSemestreValidation,
|
||||||
ScolarAutorisationInscription,
|
ScolarAutorisationInscription,
|
||||||
)
|
)
|
||||||
@ -211,13 +144,8 @@ from app.models.but_refcomp import (
|
|||||||
ApcReferentielCompetences,
|
ApcReferentielCompetences,
|
||||||
ApcSituationPro,
|
ApcSituationPro,
|
||||||
)
|
)
|
||||||
from app.models.but_validations import (
|
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
||||||
ApcValidationAnnee,
|
|
||||||
ApcValidationRCUE,
|
|
||||||
ValidationDUT120,
|
|
||||||
)
|
|
||||||
|
|
||||||
from app.models.config import ScoDocSiteConfig
|
from app.models.config import ScoDocSiteConfig
|
||||||
|
|
||||||
from app.models.assiduites import Assiduite, Justificatif
|
from app.models.assiduites import Assiduite, Justificatif
|
||||||
from app.models.scolar_event import ScolarEvent
|
|
||||||
|
@ -7,10 +7,7 @@ from app import db
|
|||||||
|
|
||||||
|
|
||||||
class Absence(db.Model):
|
class Absence(db.Model):
|
||||||
"""LEGACY
|
"""une absence (sur une demi-journée)"""
|
||||||
Ce modèle n'est PLUS UTILISE depuis ScoDoc 9.6 et remplacé par assiduité.
|
|
||||||
une absence (sur une demi-journée)
|
|
||||||
"""
|
|
||||||
|
|
||||||
__tablename__ = "absences"
|
__tablename__ = "absences"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
@ -1,38 +1,25 @@
|
|||||||
# -*- coding: UTF-8 -*
|
# -*- coding: UTF-8 -*
|
||||||
"""Gestion de l'assiduité (assiduités + justificatifs)"""
|
"""Gestion de l'assiduité (assiduités + justificatifs)
|
||||||
|
"""
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from flask_login import current_user
|
from app import db, log
|
||||||
from flask_sqlalchemy.query import Query
|
from app.models import ModuleImpl, Scolog, FormSemestre, FormSemestreInscription
|
||||||
|
|
||||||
from app import db, log, g, set_sco_dept
|
|
||||||
from app.models import (
|
|
||||||
ModuleImpl,
|
|
||||||
Module,
|
|
||||||
Scolog,
|
|
||||||
FormSemestre,
|
|
||||||
FormSemestreInscription,
|
|
||||||
ScoDocModel,
|
|
||||||
)
|
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
from app.auth.models import User
|
from app.auth.models import User
|
||||||
from app.scodoc import sco_abs_notification
|
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_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 (
|
from app.scodoc.sco_utils import (
|
||||||
EtatAssiduite,
|
EtatAssiduite,
|
||||||
EtatJustificatif,
|
EtatJustificatif,
|
||||||
localize_datetime,
|
localize_datetime,
|
||||||
is_assiduites_module_forced,
|
is_assiduites_module_forced,
|
||||||
NonWorkDays,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from flask_sqlalchemy.query import Query
|
||||||
|
|
||||||
class Assiduite(ScoDocModel):
|
|
||||||
|
class Assiduite(db.Model):
|
||||||
"""
|
"""
|
||||||
Représente une assiduité:
|
Représente une assiduité:
|
||||||
- une plage horaire lié à un état et un étudiant
|
- une plage horaire lié à un état et un étudiant
|
||||||
@ -90,14 +77,10 @@ class Assiduite(ScoDocModel):
|
|||||||
lazy="select",
|
lazy="select",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Argument "restrict" obligatoire car on override la fonction "to_dict" de ScoDocModel
|
def to_dict(self, format_api=True) -> dict:
|
||||||
# pylint: disable-next=unused-argument
|
"""Retourne la représentation json de l'assiduité"""
|
||||||
def to_dict(self, format_api=True, restrict: bool | None = None) -> dict:
|
|
||||||
"""Retourne la représentation json de l'assiduité
|
|
||||||
restrict n'est pas utilisé ici.
|
|
||||||
"""
|
|
||||||
etat = self.etat
|
etat = self.etat
|
||||||
user: User | None = None
|
user: User = None
|
||||||
if format_api:
|
if format_api:
|
||||||
# format api utilise les noms "present,absent,retard" au lieu des int
|
# format api utilise les noms "present,absent,retard" au lieu des int
|
||||||
etat = EtatAssiduite.inverse().get(self.etat).name
|
etat = EtatAssiduite.inverse().get(self.etat).name
|
||||||
@ -115,9 +98,9 @@ class Assiduite(ScoDocModel):
|
|||||||
"entry_date": self.entry_date,
|
"entry_date": self.entry_date,
|
||||||
"user_id": None if user is None else user.id, # l'uid
|
"user_id": None if user is None else user.id, # l'uid
|
||||||
"user_name": None if user is None else user.user_name, # le login
|
"user_name": None if user is None else user.user_name, # le login
|
||||||
"user_nom_complet": (
|
"user_nom_complet": None
|
||||||
None if user is None else user.get_nomcomplet()
|
if user is None
|
||||||
), # "Marie Dupont"
|
else user.get_nomcomplet(), # "Marie Dupont"
|
||||||
"est_just": self.est_just,
|
"est_just": self.est_just,
|
||||||
"external_data": self.external_data,
|
"external_data": self.external_data,
|
||||||
}
|
}
|
||||||
@ -152,56 +135,16 @@ class Assiduite(ScoDocModel):
|
|||||||
external_data: dict = None,
|
external_data: dict = None,
|
||||||
notify_mail=False,
|
notify_mail=False,
|
||||||
) -> "Assiduite":
|
) -> "Assiduite":
|
||||||
"""Créer une nouvelle assiduité pour l'étudiant.
|
"""Créer une nouvelle assiduité pour l'étudiant"""
|
||||||
Les datetime doivent être en timezone serveur.
|
|
||||||
Raises ScoValueError en cas de conflit ou erreur.
|
|
||||||
"""
|
|
||||||
if date_debut.tzinfo is None:
|
if date_debut.tzinfo is None:
|
||||||
log(
|
log(
|
||||||
f"Warning: create_assiduite: date_debut without timezone ({date_debut})"
|
f"Warning: create_assiduite: date_debut without timezone ({date_debut})"
|
||||||
)
|
)
|
||||||
if date_fin.tzinfo is None:
|
if date_fin.tzinfo is None:
|
||||||
log(f"Warning: create_assiduite: date_fin without timezone ({date_fin})")
|
log(f"Warning: create_assiduite: date_fin without timezone ({date_fin})")
|
||||||
|
|
||||||
# Vérification jours non travaillés
|
|
||||||
# -> vérifie si la date de début ou la date de fin est sur un jour non travaillé
|
|
||||||
# On récupère les formsemestres des dates de début et de fin
|
|
||||||
formsemestre_date_debut: FormSemestre = get_formsemestre_from_data(
|
|
||||||
{
|
|
||||||
"etudid": etud.id,
|
|
||||||
"date_debut": date_debut,
|
|
||||||
"date_fin": date_debut,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
formsemestre_date_fin: FormSemestre = get_formsemestre_from_data(
|
|
||||||
{
|
|
||||||
"etudid": etud.id,
|
|
||||||
"date_debut": date_fin,
|
|
||||||
"date_fin": date_fin,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if date_debut.weekday() in NonWorkDays.get_all_non_work_days(
|
|
||||||
formsemestre_id=formsemestre_date_debut
|
|
||||||
):
|
|
||||||
raise ScoValueError("La date de début n'est pas un jour travaillé")
|
|
||||||
if date_fin.weekday() in NonWorkDays.get_all_non_work_days(
|
|
||||||
formsemestre_id=formsemestre_date_fin
|
|
||||||
):
|
|
||||||
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
|
# Vérification de non duplication des périodes
|
||||||
assiduites: Query = etud.assiduites
|
assiduites: Query = etud.assiduites
|
||||||
if is_period_conflicting(date_debut, date_fin, assiduites, Assiduite):
|
if is_period_conflicting(date_debut, date_fin, assiduites, Assiduite):
|
||||||
log(
|
|
||||||
f"""create_assiduite: period_conflicting etudid={etud.id} date_debut={
|
|
||||||
date_debut} date_fin={date_fin}"""
|
|
||||||
)
|
|
||||||
raise ScoValueError(
|
raise ScoValueError(
|
||||||
"Duplication: la période rentre en conflit avec une plage enregistrée"
|
"Duplication: la période rentre en conflit avec une plage enregistrée"
|
||||||
)
|
)
|
||||||
@ -251,8 +194,7 @@ class Assiduite(ScoDocModel):
|
|||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
)
|
)
|
||||||
db.session.add(nouv_assiduite)
|
db.session.add(nouv_assiduite)
|
||||||
db.session.flush()
|
log(f"create_assiduite: {etud.id} {nouv_assiduite}")
|
||||||
log(f"create_assiduite: {etud.id} id={nouv_assiduite.id} {nouv_assiduite}")
|
|
||||||
Scolog.logdb(
|
Scolog.logdb(
|
||||||
method="create_assiduite",
|
method="create_assiduite",
|
||||||
etudid=etud.id,
|
etudid=etud.id,
|
||||||
@ -262,166 +204,8 @@ class Assiduite(ScoDocModel):
|
|||||||
sco_abs_notification.abs_notify(etud.id, nouv_assiduite.date_debut)
|
sco_abs_notification.abs_notify(etud.id, nouv_assiduite.date_debut)
|
||||||
return nouv_assiduite
|
return nouv_assiduite
|
||||||
|
|
||||||
def set_moduleimpl(self, moduleimpl_id: int | str):
|
|
||||||
"""Mise à jour du moduleimpl_id
|
|
||||||
Les valeurs du champ "moduleimpl_id" possibles sont :
|
|
||||||
- <int> (un id classique)
|
|
||||||
- <str> ("autre" ou "<id>")
|
|
||||||
- "" (pas de moduleimpl_id)
|
|
||||||
Si la valeur est "autre" il faut:
|
|
||||||
- mettre à None assiduité.moduleimpl_id
|
|
||||||
- mettre à jour assiduite.external_data["module"] = "autre"
|
|
||||||
En fonction de la configuration du semestre (option force_module) la valeur "" peut-être
|
|
||||||
considérée comme invalide.
|
|
||||||
- Il faudra donc vérifier que ce n'est pas le cas avant de mettre à jour l'assiduité
|
|
||||||
"""
|
|
||||||
moduleimpl: ModuleImpl = None
|
|
||||||
if moduleimpl_id == "autre":
|
|
||||||
# Configuration de external_data pour Module Autre
|
|
||||||
# Si self.external_data None alors on créé un dictionnaire {"module": "autre"}
|
|
||||||
# Sinon on met à jour external_data["module"] à "autre"
|
|
||||||
|
|
||||||
if self.external_data is None:
|
class Justificatif(db.Model):
|
||||||
self.external_data = {"module": "autre"}
|
|
||||||
else:
|
|
||||||
self.external_data["module"] = "autre"
|
|
||||||
|
|
||||||
# Dans tous les cas une fois fait, assiduite.moduleimpl_id doit être None
|
|
||||||
self.moduleimpl_id = None
|
|
||||||
|
|
||||||
# Ici pas de vérification du force module car on l'a mis dans "external_data"
|
|
||||||
return
|
|
||||||
|
|
||||||
if moduleimpl_id != "":
|
|
||||||
try:
|
|
||||||
moduleimpl_id = int(moduleimpl_id)
|
|
||||||
except ValueError as exc:
|
|
||||||
raise ScoValueError("Module non reconnu") from exc
|
|
||||||
moduleimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id)
|
|
||||||
|
|
||||||
# ici moduleimpl est None si non spécifié
|
|
||||||
|
|
||||||
# Vérification ModuleImpl not None (raise ScoValueError)
|
|
||||||
if moduleimpl is None:
|
|
||||||
self._check_force_module()
|
|
||||||
# Ici uniquement si on est autorisé à ne pas avoir de module
|
|
||||||
self.moduleimpl_id = None
|
|
||||||
return
|
|
||||||
|
|
||||||
# Vérification Inscription ModuleImpl (raise ScoValueError)
|
|
||||||
if moduleimpl.est_inscrit(self.etudiant):
|
|
||||||
self.moduleimpl_id = moduleimpl.id
|
|
||||||
else:
|
|
||||||
raise ScoValueError("L'étudiant n'est pas inscrit au module")
|
|
||||||
|
|
||||||
def supprime(self):
|
|
||||||
"Supprime l'assiduité. Log et commit."
|
|
||||||
|
|
||||||
# Obligatoire car import circulaire sinon
|
|
||||||
# pylint: disable-next=import-outside-toplevel
|
|
||||||
from app.scodoc import sco_assiduites as scass
|
|
||||||
|
|
||||||
if g.scodoc_dept is None and self.etudiant.dept_id is not None:
|
|
||||||
# route sans département
|
|
||||||
set_sco_dept(self.etudiant.departement.acronym)
|
|
||||||
obj_dict: dict = self.to_dict()
|
|
||||||
# Suppression de l'objet et LOG
|
|
||||||
log(f"delete_assidutite: {self.etudiant.id} {self}")
|
|
||||||
Scolog.logdb(
|
|
||||||
method="delete_assiduite",
|
|
||||||
etudid=self.etudiant.id,
|
|
||||||
msg=f"Assiduité: {self}",
|
|
||||||
)
|
|
||||||
db.session.delete(self)
|
|
||||||
db.session.commit()
|
|
||||||
# Invalidation du cache
|
|
||||||
scass.simple_invalidate_cache(obj_dict)
|
|
||||||
|
|
||||||
def get_formsemestre(self) -> FormSemestre:
|
|
||||||
"""Le formsemestre associé.
|
|
||||||
Attention: en cas d'inscription multiple prend arbitrairement l'un des semestres.
|
|
||||||
A utiliser avec précaution !
|
|
||||||
"""
|
|
||||||
return get_formsemestre_from_data(self.to_dict())
|
|
||||||
|
|
||||||
def get_module(self, traduire: bool = False) -> Module | str:
|
|
||||||
"""
|
|
||||||
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
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self.moduleimpl_id is not None:
|
|
||||||
modimpl: ModuleImpl = ModuleImpl.query.get(self.moduleimpl_id)
|
|
||||||
mod: Module = Module.query.get(modimpl.module_id)
|
|
||||||
if traduire:
|
|
||||||
return f"{mod.code} {mod.titre}"
|
|
||||||
return mod
|
|
||||||
|
|
||||||
if self.external_data is not None and "module" in self.external_data:
|
|
||||||
return (
|
|
||||||
"Autre module (pas dans la liste)"
|
|
||||||
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
|
|
||||||
|
|
||||||
def get_saisie(self) -> str:
|
|
||||||
"""
|
|
||||||
retourne le texte "saisie le <date> par <User>"
|
|
||||||
"""
|
|
||||||
|
|
||||||
date: str = self.entry_date.strftime(scu.DATEATIME_FMT)
|
|
||||||
utilisateur: str = ""
|
|
||||||
if self.user is not None:
|
|
||||||
self.user: User
|
|
||||||
utilisateur = f"par {self.user.get_prenomnom()}"
|
|
||||||
|
|
||||||
return f"saisie le {date} {utilisateur}"
|
|
||||||
|
|
||||||
def _check_force_module(self):
|
|
||||||
"""Vérification si module forcé:
|
|
||||||
Si le module est requis, raise ScoValueError
|
|
||||||
sinon ne fait rien.
|
|
||||||
"""
|
|
||||||
# cherche le formsemestre affecté pour utiliser ses préférences
|
|
||||||
formsemestre: FormSemestre = get_formsemestre_from_data(
|
|
||||||
{
|
|
||||||
"etudid": self.etudid,
|
|
||||||
"date_debut": self.date_debut,
|
|
||||||
"date_fin": self.date_fin,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
formsemestre_id = formsemestre.id if formsemestre else None
|
|
||||||
# si pas de formsemestre, utilisera les prefs globales du département
|
|
||||||
dept_id = self.etudiant.dept_id
|
|
||||||
force = is_assiduites_module_forced(
|
|
||||||
formsemestre_id=formsemestre_id, dept_id=dept_id
|
|
||||||
)
|
|
||||||
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):
|
|
||||||
"""
|
"""
|
||||||
Représente un justificatif:
|
Représente un justificatif:
|
||||||
- une plage horaire lié à un état et un étudiant
|
- une plage horaire lié à un état et un étudiant
|
||||||
@ -453,8 +237,6 @@ class Justificatif(ScoDocModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
entry_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
entry_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||||
"date de création de l'élément: date de saisie"
|
|
||||||
# pourrait devenir date de dépôt au secrétariat, si différente
|
|
||||||
|
|
||||||
user_id = db.Column(
|
user_id = db.Column(
|
||||||
db.Integer,
|
db.Integer,
|
||||||
@ -473,35 +255,23 @@ class Justificatif(ScoDocModel):
|
|||||||
etudiant = db.relationship(
|
etudiant = db.relationship(
|
||||||
"Identite", back_populates="justificatifs", lazy="joined"
|
"Identite", back_populates="justificatifs", lazy="joined"
|
||||||
)
|
)
|
||||||
# En revanche, user est rarement accédé:
|
|
||||||
user = db.relationship(
|
|
||||||
"User",
|
|
||||||
backref=db.backref(
|
|
||||||
"justificatifs", lazy="select", order_by="Justificatif.entry_date"
|
|
||||||
),
|
|
||||||
lazy="select",
|
|
||||||
)
|
|
||||||
|
|
||||||
external_data = db.Column(db.JSON, nullable=True)
|
external_data = db.Column(db.JSON, nullable=True)
|
||||||
|
|
||||||
@classmethod
|
def to_dict(self, format_api: bool = False) -> dict:
|
||||||
def get_justificatif(cls, justif_id: int) -> "Justificatif":
|
"""transformation de l'objet en dictionnaire sérialisable"""
|
||||||
"""Justificatif ou 404, cherche uniquement dans le département courant"""
|
|
||||||
query = Justificatif.query.filter_by(id=justif_id)
|
|
||||||
if g.scodoc_dept:
|
|
||||||
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
|
||||||
return query.first_or_404()
|
|
||||||
|
|
||||||
def to_dict(self, format_api: bool = False, restrict: bool = False) -> dict:
|
|
||||||
"""L'objet en dictionnaire sérialisable.
|
|
||||||
Si restrict, ne donne par la raison et les fichiers et external_data
|
|
||||||
"""
|
|
||||||
|
|
||||||
etat = self.etat
|
etat = self.etat
|
||||||
user: User = self.user if self.user_id is not None else None
|
username = self.user_id
|
||||||
|
|
||||||
if format_api:
|
if format_api:
|
||||||
etat = EtatJustificatif.inverse().get(self.etat).name
|
etat = EtatJustificatif.inverse().get(self.etat).name
|
||||||
|
if self.user_id is not None:
|
||||||
|
user: User = db.session.get(User, self.user_id)
|
||||||
|
if user is None:
|
||||||
|
username = "Non renseigné"
|
||||||
|
else:
|
||||||
|
username = user.get_prenomnom()
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"justif_id": self.justif_id,
|
"justif_id": self.justif_id,
|
||||||
@ -510,49 +280,30 @@ class Justificatif(ScoDocModel):
|
|||||||
"date_debut": self.date_debut,
|
"date_debut": self.date_debut,
|
||||||
"date_fin": self.date_fin,
|
"date_fin": self.date_fin,
|
||||||
"etat": etat,
|
"etat": etat,
|
||||||
"raison": None if restrict else self.raison,
|
"raison": self.raison,
|
||||||
"fichier": None if restrict else self.fichier,
|
"fichier": self.fichier,
|
||||||
"entry_date": self.entry_date,
|
"entry_date": self.entry_date,
|
||||||
"user_id": None if user is None else user.id, # l'uid
|
"user_id": username,
|
||||||
"user_name": None if user is None else user.user_name, # le login
|
"external_data": self.external_data,
|
||||||
"user_nom_complet": None if user is None else user.get_nomcomplet(),
|
|
||||||
"external_data": None if restrict else self.external_data,
|
|
||||||
}
|
}
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __str__(self) -> str:
|
||||||
"chaine pour journaux et debug (lisible par humain français)"
|
"chaine pour journaux et debug (lisible par humain français)"
|
||||||
try:
|
try:
|
||||||
etat_str = EtatJustificatif(self.etat).name
|
etat_str = EtatJustificatif(self.etat).name
|
||||||
except ValueError:
|
except ValueError:
|
||||||
etat_str = "Invalide"
|
etat_str = "Invalide"
|
||||||
return f"""Justificatif id={self.id} {etat_str} de {
|
return f"""Justificatif {etat_str} de {
|
||||||
self.date_debut.strftime("%d/%m/%Y %Hh%M")
|
self.date_debut.strftime("%d/%m/%Y %Hh%M")
|
||||||
} à {
|
} à {
|
||||||
self.date_fin.strftime("%d/%m/%Y %Hh%M")
|
self.date_fin.strftime("%d/%m/%Y %Hh%M")
|
||||||
}"""
|
}"""
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def convert_dict_fields(cls, args: dict) -> dict:
|
|
||||||
"""Convert fields. Called by ScoDocModel's create_from_dict, edit and from_dict
|
|
||||||
Raises ScoValueError si paramètres incorrects.
|
|
||||||
"""
|
|
||||||
if not isinstance(args["date_debut"], datetime) or not isinstance(
|
|
||||||
args["date_fin"], datetime
|
|
||||||
):
|
|
||||||
raise ScoValueError("type date incorrect")
|
|
||||||
if args["date_fin"] <= args["date_debut"]:
|
|
||||||
raise ScoValueError("dates incompatibles")
|
|
||||||
if args["entry_date"] and not isinstance(args["entry_date"], datetime):
|
|
||||||
raise ScoValueError("type entry_date incorrect")
|
|
||||||
return args
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_justificatif(
|
def create_justificatif(
|
||||||
cls,
|
cls,
|
||||||
etudiant: Identite,
|
etud: Identite,
|
||||||
# On a besoin des arguments mais on utilise "locals" pour les récupérer
|
|
||||||
# pylint: disable=unused-argument
|
|
||||||
date_debut: datetime,
|
date_debut: datetime,
|
||||||
date_fin: datetime,
|
date_fin: datetime,
|
||||||
etat: EtatJustificatif,
|
etat: EtatJustificatif,
|
||||||
@ -561,163 +312,38 @@ class Justificatif(ScoDocModel):
|
|||||||
user_id: int = None,
|
user_id: int = None,
|
||||||
external_data: dict = None,
|
external_data: dict = None,
|
||||||
) -> "Justificatif":
|
) -> "Justificatif":
|
||||||
"""Créer un nouveau justificatif pour l'étudiant.
|
"""Créer un nouveau justificatif pour l'étudiant"""
|
||||||
Raises ScoValueError si paramètres incorrects.
|
nouv_justificatif = Justificatif(
|
||||||
"""
|
date_debut=date_debut,
|
||||||
nouv_justificatif = cls.create_from_dict(locals())
|
date_fin=date_fin,
|
||||||
log(f"create_justificatif: etudid={etudiant.id} {nouv_justificatif}")
|
etat=etat,
|
||||||
|
etudiant=etud,
|
||||||
|
raison=raison,
|
||||||
|
entry_date=entry_date,
|
||||||
|
user_id=user_id,
|
||||||
|
external_data=external_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(nouv_justificatif)
|
||||||
|
|
||||||
|
log(f"create_justificatif: {etud.id} {nouv_justificatif}")
|
||||||
Scolog.logdb(
|
Scolog.logdb(
|
||||||
method="create_justificatif",
|
method="create_justificatif",
|
||||||
etudid=etudiant.id,
|
etudid=etud.id,
|
||||||
msg=f"justificatif: {nouv_justificatif}",
|
msg=f"justificatif: {nouv_justificatif}",
|
||||||
)
|
)
|
||||||
db.session.commit()
|
|
||||||
return nouv_justificatif
|
return nouv_justificatif
|
||||||
|
|
||||||
def supprime(self):
|
|
||||||
"Supprime le justificatif. Log et commit."
|
|
||||||
|
|
||||||
# Obligatoire car import circulaire sinon
|
|
||||||
# pylint: disable-next=import-outside-toplevel
|
|
||||||
from app.scodoc import sco_assiduites as scass
|
|
||||||
|
|
||||||
# Récupération de l'archive du justificatif
|
|
||||||
archive_name: str = self.fichier
|
|
||||||
|
|
||||||
if archive_name is not None:
|
|
||||||
# Si elle existe : on essaye de la supprimer
|
|
||||||
archiver: JustificatifArchiver = JustificatifArchiver()
|
|
||||||
try:
|
|
||||||
archiver.delete_justificatif(self.etudiant, archive_name)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
if g.scodoc_dept is None and self.etudiant.dept_id is not None:
|
|
||||||
# route sans département
|
|
||||||
set_sco_dept(self.etudiant.departement.acronym)
|
|
||||||
# On invalide le cache
|
|
||||||
scass.simple_invalidate_cache(self.to_dict())
|
|
||||||
# Suppression de l'objet et LOG
|
|
||||||
log(f"delete_justificatif: {self.etudiant.id} {self}")
|
|
||||||
Scolog.logdb(
|
|
||||||
method="delete_justificatif",
|
|
||||||
etudid=self.etudiant.id,
|
|
||||||
msg=f"Justificatif: {self}",
|
|
||||||
)
|
|
||||||
db.session.delete(self)
|
|
||||||
db.session.commit()
|
|
||||||
# On actualise les assiduités justifiées de l'étudiant concerné
|
|
||||||
self.dejustifier_assiduites()
|
|
||||||
|
|
||||||
def get_fichiers(self) -> tuple[list[str], int]:
|
|
||||||
"""Renvoie la liste des noms de fichiers justicatifs
|
|
||||||
accessibles par l'utilisateur courant et le nombre total
|
|
||||||
de fichiers.
|
|
||||||
(ces fichiers sont dans l'archive associée)
|
|
||||||
"""
|
|
||||||
if self.fichier is None:
|
|
||||||
return [], 0
|
|
||||||
archive_name: str = self.fichier
|
|
||||||
archiver: JustificatifArchiver = JustificatifArchiver()
|
|
||||||
filenames = archiver.list_justificatifs(archive_name, self.etudiant)
|
|
||||||
accessible_filenames = []
|
|
||||||
#
|
|
||||||
for filename in filenames:
|
|
||||||
if int(filename[1]) == current_user.id or current_user.has_permission(
|
|
||||||
Permission.AbsJustifView
|
|
||||||
):
|
|
||||||
accessible_filenames.append(filename[0])
|
|
||||||
return accessible_filenames, len(filenames)
|
|
||||||
|
|
||||||
def justifier_assiduites(
|
|
||||||
self,
|
|
||||||
) -> list[int]:
|
|
||||||
"""Justifie les assiduités sur la période de validité du justificatif"""
|
|
||||||
log(f"justifier_assiduites: {self}")
|
|
||||||
assiduites_justifiees: list[int] = []
|
|
||||||
if self.etat != EtatJustificatif.VALIDE:
|
|
||||||
return []
|
|
||||||
# On récupère les assiduités de l'étudiant sur la période donnée
|
|
||||||
assiduites: Query = self.etudiant.assiduites.filter(
|
|
||||||
Assiduite.date_debut >= self.date_debut,
|
|
||||||
Assiduite.date_fin <= self.date_fin,
|
|
||||||
Assiduite.etat != EtatAssiduite.PRESENT,
|
|
||||||
)
|
|
||||||
# Pour chaque assiduité, on la justifie
|
|
||||||
for assi in assiduites:
|
|
||||||
assi.est_just = True
|
|
||||||
assiduites_justifiees.append(assi.assiduite_id)
|
|
||||||
db.session.add(assi)
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
return assiduites_justifiees
|
|
||||||
|
|
||||||
def dejustifier_assiduites(self) -> list[int]:
|
|
||||||
"""
|
|
||||||
Déjustifie les assiduités sur la période du justificatif
|
|
||||||
"""
|
|
||||||
assiduites_dejustifiees: list[int] = []
|
|
||||||
|
|
||||||
# On récupère les assiduités de l'étudiant sur la période donnée
|
|
||||||
assiduites: Query = self.etudiant.assiduites.filter(
|
|
||||||
Assiduite.date_debut >= self.date_debut,
|
|
||||||
Assiduite.date_fin <= self.date_fin,
|
|
||||||
Assiduite.etat != EtatAssiduite.PRESENT,
|
|
||||||
)
|
|
||||||
assi: Assiduite
|
|
||||||
for assi in assiduites:
|
|
||||||
# On récupère les justificatifs qui justifient l'assiduité `assi`
|
|
||||||
assi_justifs: list[int] = get_justifs_from_date(
|
|
||||||
self.etudiant.etudid,
|
|
||||||
assi.date_debut,
|
|
||||||
assi.date_fin,
|
|
||||||
long=False,
|
|
||||||
valid=True,
|
|
||||||
)
|
|
||||||
# Si il n'y a pas d'autre justificatif valide, on déjustifie l'assiduité
|
|
||||||
if len(assi_justifs) == 0 or (
|
|
||||||
len(assi_justifs) == 1 and assi_justifs[0] == self.justif_id
|
|
||||||
):
|
|
||||||
assi.est_just = False
|
|
||||||
assiduites_dejustifiees.append(assi.assiduite_id)
|
|
||||||
db.session.add(assi)
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
return assiduites_dejustifiees
|
|
||||||
|
|
||||||
def get_assiduites(self) -> Query:
|
|
||||||
"""
|
|
||||||
get_assiduites Récupère les assiduités qui sont concernées par le justificatif
|
|
||||||
(Concernée ≠ Justifiée, mais qui sont sur la même période)
|
|
||||||
Ne prends pas en compte les Présences
|
|
||||||
Returns:
|
|
||||||
Query: Les assiduités concernées
|
|
||||||
"""
|
|
||||||
|
|
||||||
assiduites_query = Assiduite.query.filter(
|
|
||||||
Assiduite.etudid == self.etudid,
|
|
||||||
Assiduite.date_debut >= self.date_debut,
|
|
||||||
Assiduite.date_fin <= self.date_fin,
|
|
||||||
Assiduite.etat != EtatAssiduite.PRESENT,
|
|
||||||
)
|
|
||||||
|
|
||||||
return assiduites_query
|
|
||||||
|
|
||||||
|
|
||||||
def is_period_conflicting(
|
def is_period_conflicting(
|
||||||
date_debut: datetime,
|
date_debut: datetime,
|
||||||
date_fin: datetime,
|
date_fin: datetime,
|
||||||
collection: Query,
|
collection: Query,
|
||||||
collection_cls: Assiduite | Justificatif,
|
collection_cls: Assiduite | Justificatif,
|
||||||
obj_id: int = -1,
|
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Vérifie si une date n'entre pas en collision
|
Vérifie si une date n'entre pas en collision
|
||||||
avec les justificatifs ou assiduites déjà présentes
|
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
|
# On s'assure que les dates soient avec TimeZone
|
||||||
@ -725,14 +351,74 @@ def is_period_conflicting(
|
|||||||
date_fin = localize_datetime(date_fin)
|
date_fin = localize_datetime(date_fin)
|
||||||
|
|
||||||
count: int = collection.filter(
|
count: int = collection.filter(
|
||||||
collection_cls.date_debut < date_fin,
|
collection_cls.date_debut < date_fin, collection_cls.date_fin > date_debut
|
||||||
collection_cls.date_fin > date_debut,
|
|
||||||
collection_cls.id != obj_id,
|
|
||||||
).count()
|
).count()
|
||||||
|
|
||||||
return count > 0
|
return count > 0
|
||||||
|
|
||||||
|
|
||||||
|
def compute_assiduites_justified(
|
||||||
|
etudid: int, justificatifs: list[Justificatif] = None, reset: bool = False
|
||||||
|
) -> list[int]:
|
||||||
|
"""
|
||||||
|
compute_assiduites_justified_faster
|
||||||
|
|
||||||
|
Args:
|
||||||
|
etudid (int): l'identifiant de l'étudiant
|
||||||
|
justificatifs (list[Justificatif]): La liste des justificatifs qui seront utilisés
|
||||||
|
reset (bool, optional): remet à false les assiduites non justifiés. Defaults to False.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[int]: la liste des assiduités qui ont été justifiées.
|
||||||
|
"""
|
||||||
|
# Si on ne donne pas de justificatifs on prendra par défaut tous les justificatifs de l'étudiant
|
||||||
|
if justificatifs is None:
|
||||||
|
justificatifs: list[Justificatif] = Justificatif.query.filter_by(
|
||||||
|
etudid=etudid
|
||||||
|
).all()
|
||||||
|
|
||||||
|
# On ne prend que les justificatifs valides
|
||||||
|
justificatifs = [j for j in justificatifs if j.etat == EtatJustificatif.VALIDE]
|
||||||
|
|
||||||
|
# On récupère les assiduités de l'étudiant
|
||||||
|
assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid)
|
||||||
|
|
||||||
|
assiduites_justifiees: list[int] = []
|
||||||
|
|
||||||
|
for assi in assiduites:
|
||||||
|
# On ne justifie pas les Présences
|
||||||
|
if assi.etat == EtatAssiduite.PRESENT:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# On récupère les justificatifs qui justifient l'assiduité `assi`
|
||||||
|
assi_justificatifs = Justificatif.query.filter(
|
||||||
|
Justificatif.etudid == assi.etudid,
|
||||||
|
Justificatif.date_debut <= assi.date_debut,
|
||||||
|
Justificatif.date_fin >= assi.date_fin,
|
||||||
|
Justificatif.etat == EtatJustificatif.VALIDE,
|
||||||
|
).all()
|
||||||
|
|
||||||
|
# Si au moins un justificatif possède une période qui couvre l'assiduité
|
||||||
|
if any(
|
||||||
|
assi.date_debut >= j.date_debut and assi.date_fin <= j.date_fin
|
||||||
|
for j in justificatifs + assi_justificatifs
|
||||||
|
):
|
||||||
|
# On justifie l'assiduité
|
||||||
|
# On ajoute l'id de l'assiduité à la liste des assiduités justifiées
|
||||||
|
assi.est_just = True
|
||||||
|
assiduites_justifiees.append(assi.assiduite_id)
|
||||||
|
db.session.add(assi)
|
||||||
|
elif reset:
|
||||||
|
# Si le paramètre reset est Vrai alors les assiduités non justifiées
|
||||||
|
# sont remise en "non justifiée"
|
||||||
|
assi.est_just = False
|
||||||
|
db.session.add(assi)
|
||||||
|
# On valide la session
|
||||||
|
db.session.commit()
|
||||||
|
# On renvoie la liste des assiduite_id des assiduités justifiées
|
||||||
|
return assiduites_justifiees
|
||||||
|
|
||||||
|
|
||||||
def get_assiduites_justif(assiduite_id: int, long: bool) -> list[int | dict]:
|
def get_assiduites_justif(assiduite_id: int, long: bool) -> list[int | dict]:
|
||||||
"""
|
"""
|
||||||
get_assiduites_justif Récupération des justificatifs d'une assiduité
|
get_assiduites_justif Récupération des justificatifs d'une assiduité
|
||||||
@ -743,8 +429,7 @@ def get_assiduites_justif(assiduite_id: int, long: bool) -> list[int | dict]:
|
|||||||
des identifiants des justificatifs
|
des identifiants des justificatifs
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list[int | dict]: La liste des justificatifs (par défaut uniquement
|
list[int | dict]: La liste des justificatifs (par défaut uniquement les identifiants, sinon les Dict si long est vrai)
|
||||||
les identifiants, sinon les dict si long est vrai)
|
|
||||||
"""
|
"""
|
||||||
assi: Assiduite = Assiduite.query.get_or_404(assiduite_id)
|
assi: Assiduite = Assiduite.query.get_or_404(assiduite_id)
|
||||||
return get_justifs_from_date(assi.etudid, assi.date_debut, assi.date_fin, long)
|
return get_justifs_from_date(assi.etudid, assi.date_debut, assi.date_fin, long)
|
||||||
@ -774,8 +459,7 @@ def get_justifs_from_date(
|
|||||||
Defaults to False.
|
Defaults to False.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list[int | dict]: La liste des justificatifs (par défaut uniquement
|
list[int | dict]: La liste des justificatifs (par défaut uniquement les identifiants, sinon les Dict si long est vrai)
|
||||||
les identifiants, sinon les dict si long est vrai)
|
|
||||||
"""
|
"""
|
||||||
# On récupère les justificatifs d'un étudiant couvrant la période donnée
|
# On récupère les justificatifs d'un étudiant couvrant la période donnée
|
||||||
justifs: Query = Justificatif.query.filter(
|
justifs: Query = Justificatif.query.filter(
|
||||||
@ -788,20 +472,16 @@ def get_justifs_from_date(
|
|||||||
if valid:
|
if valid:
|
||||||
justifs = justifs.filter(Justificatif.etat == EtatJustificatif.VALIDE)
|
justifs = justifs.filter(Justificatif.etat == EtatJustificatif.VALIDE)
|
||||||
|
|
||||||
# On renvoie la liste des id des justificatifs si long est Faux,
|
# On renvoie la liste des id des justificatifs si long est Faux, sinon on renvoie les dicts des justificatifs
|
||||||
# sinon on renvoie les dicts des justificatifs
|
return [j.justif_id if not long else j.to_dict(True) for j in justifs]
|
||||||
if long:
|
|
||||||
return [j.to_dict(True) for j in justifs]
|
|
||||||
return [j.justif_id for j in justifs]
|
|
||||||
|
|
||||||
|
|
||||||
def get_formsemestre_from_data(data: dict[str, datetime | int]) -> FormSemestre:
|
def get_formsemestre_from_data(data: dict[str, datetime | int]) -> FormSemestre:
|
||||||
"""
|
"""
|
||||||
get_formsemestre_from_data récupère un formsemestre en fonction des données passées
|
get_formsemestre_from_data récupère un formsemestre en fonction des données passées
|
||||||
Si l'étudiant est inscrit à plusieurs formsemestre, prend le premier.
|
|
||||||
Args:
|
Args:
|
||||||
data (dict[str, datetime | int]): Une représentation simplifiée d'une
|
data (dict[str, datetime | int]): Une réprésentation simplifiée d'une assiduité ou d'un justificatif
|
||||||
assiduité ou d'un justificatif
|
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"etudid" : int,
|
"etudid" : int,
|
||||||
@ -824,29 +504,3 @@ def get_formsemestre_from_data(data: dict[str, datetime | int]) -> FormSemestre:
|
|||||||
)
|
)
|
||||||
.first()
|
.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
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
"""ScoDoc 9 models : Référentiel Compétence BUT 2021
|
"""ScoDoc 9 models : Référentiel Compétence BUT 2021
|
||||||
@ -8,19 +8,16 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import functools
|
import functools
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
import yaml
|
|
||||||
|
|
||||||
from flask import g
|
from flask import g
|
||||||
from flask_sqlalchemy.query import Query
|
from flask_sqlalchemy.query import Query
|
||||||
from sqlalchemy.orm import class_mapper
|
from sqlalchemy.orm import class_mapper
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
|
||||||
from app import db, log
|
from app import db
|
||||||
|
|
||||||
from app.scodoc.sco_utils import ModuleType
|
from app.scodoc.sco_utils import ModuleType
|
||||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
|
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences
|
||||||
|
|
||||||
REFCOMP_EQUIVALENCE_FILENAME = "ressources/referentiels/equivalences.yaml"
|
|
||||||
|
|
||||||
|
|
||||||
# from https://stackoverflow.com/questions/2537471/method-of-iterating-over-sqlalchemy-models-defined-columns
|
# from https://stackoverflow.com/questions/2537471/method-of-iterating-over-sqlalchemy-models-defined-columns
|
||||||
@ -107,11 +104,6 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<ApcReferentielCompetences {self.id} {self.specialite!r} {self.departement!r}>"
|
return f"<ApcReferentielCompetences {self.id} {self.specialite!r} {self.departement!r}>"
|
||||||
|
|
||||||
def get_title(self) -> str:
|
|
||||||
"Titre affichable"
|
|
||||||
# utilise type_titre (B.U.T.), spécialité, version
|
|
||||||
return f"{self.type_titre} {self.specialite} {self.get_version()}"
|
|
||||||
|
|
||||||
def get_version(self) -> str:
|
def get_version(self) -> str:
|
||||||
"La version, normalement sous forme de date iso yyy-mm-dd"
|
"La version, normalement sous forme de date iso yyy-mm-dd"
|
||||||
if not self.version_orebut:
|
if not self.version_orebut:
|
||||||
@ -132,11 +124,9 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
|||||||
"type_departement": self.type_departement,
|
"type_departement": self.type_departement,
|
||||||
"type_titre": self.type_titre,
|
"type_titre": self.type_titre,
|
||||||
"version_orebut": self.version_orebut,
|
"version_orebut": self.version_orebut,
|
||||||
"scodoc_date_loaded": (
|
"scodoc_date_loaded": self.scodoc_date_loaded.isoformat() + "Z"
|
||||||
self.scodoc_date_loaded.isoformat() + "Z"
|
if self.scodoc_date_loaded
|
||||||
if self.scodoc_date_loaded
|
else "",
|
||||||
else ""
|
|
||||||
),
|
|
||||||
"scodoc_orig_filename": self.scodoc_orig_filename,
|
"scodoc_orig_filename": self.scodoc_orig_filename,
|
||||||
"competences": {
|
"competences": {
|
||||||
x.titre: x.to_dict(with_app_critiques=with_app_critiques)
|
x.titre: x.to_dict(with_app_critiques=with_app_critiques)
|
||||||
@ -244,100 +234,6 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
|||||||
|
|
||||||
return parcours_info
|
return parcours_info
|
||||||
|
|
||||||
def equivalents(self) -> set["ApcReferentielCompetences"]:
|
|
||||||
"""Ensemble des référentiels du même département
|
|
||||||
qui peuvent être considérés comme "équivalents", au sens
|
|
||||||
une formation de ce référentiel pourrait changer vers un équivalent,
|
|
||||||
en ignorant les apprentissages critiques.
|
|
||||||
Pour cela, il faut avoir le même type, etc et les mêmes compétences,
|
|
||||||
niveaux et parcours (voir map_to_other_referentiel).
|
|
||||||
"""
|
|
||||||
candidats = ApcReferentielCompetences.query.filter_by(
|
|
||||||
dept_id=self.dept_id
|
|
||||||
).filter(ApcReferentielCompetences.id != self.id)
|
|
||||||
return {
|
|
||||||
referentiel
|
|
||||||
for referentiel in candidats
|
|
||||||
if not isinstance(self.map_to_other_referentiel(referentiel), str)
|
|
||||||
}
|
|
||||||
|
|
||||||
def map_to_other_referentiel(
|
|
||||||
self, other: "ApcReferentielCompetences"
|
|
||||||
) -> str | tuple[dict[int, int], dict[int, int], dict[int, int]]:
|
|
||||||
"""Build mapping between this referentiel and ref2.
|
|
||||||
If successful, returns 3 dicts mapping self ids to other ids.
|
|
||||||
Else return a string, error message.
|
|
||||||
"""
|
|
||||||
if self.type_structure != other.type_structure:
|
|
||||||
return "type_structure mismatch"
|
|
||||||
if self.type_departement != other.type_departement:
|
|
||||||
return "type_departement mismatch"
|
|
||||||
# Table d'équivalences entre refs:
|
|
||||||
equiv = self._load_config_equivalences()
|
|
||||||
# Mê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}
|
|
||||||
parcours_by_code_2 = {
|
|
||||||
eq_parcours.get(p.code, p.code): p for p in other.parcours
|
|
||||||
}
|
|
||||||
if parcours_by_code_1.keys() != parcours_by_code_2.keys():
|
|
||||||
return "parcours mismatch"
|
|
||||||
parcours_map = {
|
|
||||||
parcours_by_code_1[eq_parcours.get(code, code)]
|
|
||||||
.id: parcours_by_code_2[eq_parcours.get(code, code)]
|
|
||||||
.id
|
|
||||||
for code in parcours_by_code_1
|
|
||||||
}
|
|
||||||
# mêmes compétences ?
|
|
||||||
competence_by_code_1 = {c.titre: c for c in self.competences}
|
|
||||||
competence_by_code_2 = {c.titre: c for c in other.competences}
|
|
||||||
if competence_by_code_1.keys() != competence_by_code_2.keys():
|
|
||||||
return "competences mismatch"
|
|
||||||
competences_map = {
|
|
||||||
competence_by_code_1[titre].id: competence_by_code_2[titre].id
|
|
||||||
for titre in competence_by_code_1
|
|
||||||
}
|
|
||||||
# mêmes niveaux (dans chaque compétence) ?
|
|
||||||
niveaux_map = {}
|
|
||||||
for titre in competence_by_code_1:
|
|
||||||
c1 = competence_by_code_1[titre]
|
|
||||||
c2 = competence_by_code_2[titre]
|
|
||||||
niveau_by_attr_1 = {(n.annee, n.ordre, n.libelle): n for n in c1.niveaux}
|
|
||||||
niveau_by_attr_2 = {(n.annee, n.ordre, n.libelle): n for n in c2.niveaux}
|
|
||||||
if niveau_by_attr_1.keys() != niveau_by_attr_2.keys():
|
|
||||||
return f"niveaux mismatch in comp. '{titre}'"
|
|
||||||
niveaux_map.update(
|
|
||||||
{
|
|
||||||
niveau_by_attr_1[a].id: niveau_by_attr_2[a].id
|
|
||||||
for a in niveau_by_attr_1
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return parcours_map, competences_map, niveaux_map
|
|
||||||
|
|
||||||
def _load_config_equivalences(self) -> dict:
|
|
||||||
"""Load config file ressources/referentiels/equivalences.yaml
|
|
||||||
used to define equivalences between distinct referentiels
|
|
||||||
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:
|
|
||||||
doc = yaml.safe_load(f.read())
|
|
||||||
except FileNotFoundError:
|
|
||||||
log(f"_load_config_equivalences: {REFCOMP_EQUIVALENCE_FILENAME} not found")
|
|
||||||
return {}
|
|
||||||
except yaml.parser.ParserError as exc:
|
|
||||||
raise ScoValueError(
|
|
||||||
f"erreur dans le fichier {REFCOMP_EQUIVALENCE_FILENAME}"
|
|
||||||
) from exc
|
|
||||||
return doc.get(self.specialite, {})
|
|
||||||
|
|
||||||
|
|
||||||
class ApcCompetence(db.Model, XMLModel):
|
class ApcCompetence(db.Model, XMLModel):
|
||||||
"Compétence"
|
"Compétence"
|
||||||
@ -478,11 +374,9 @@ class ApcNiveau(db.Model, XMLModel):
|
|||||||
"libelle": self.libelle,
|
"libelle": self.libelle,
|
||||||
"annee": self.annee,
|
"annee": self.annee,
|
||||||
"ordre": self.ordre,
|
"ordre": self.ordre,
|
||||||
"app_critiques": (
|
"app_critiques": {x.code: x.to_dict() for x in self.app_critiques}
|
||||||
{x.code: x.to_dict() for x in self.app_critiques}
|
if with_app_critiques
|
||||||
if with_app_critiques
|
else {},
|
||||||
else {}
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def to_dict_bul(self):
|
def to_dict_bul(self):
|
||||||
@ -570,9 +464,9 @@ class ApcNiveau(db.Model, XMLModel):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
if competence is None:
|
if competence is None:
|
||||||
parcour_niveaux: list[ApcParcoursNiveauCompetence] = (
|
parcour_niveaux: list[
|
||||||
annee_parcour.niveaux_competences
|
ApcParcoursNiveauCompetence
|
||||||
)
|
] = annee_parcour.niveaux_competences
|
||||||
niveaux: list[ApcNiveau] = [
|
niveaux: list[ApcNiveau] = [
|
||||||
pn.competence.niveaux.filter_by(ordre=pn.niveau).first()
|
pn.competence.niveaux.filter_by(ordre=pn.niveau).first()
|
||||||
for pn in parcour_niveaux
|
for pn in parcour_niveaux
|
||||||
@ -604,7 +498,6 @@ app_critiques_modules = db.Table(
|
|||||||
db.ForeignKey("apc_app_critique.id", ondelete="CASCADE"),
|
db.ForeignKey("apc_app_critique.id", ondelete="CASCADE"),
|
||||||
primary_key=True,
|
primary_key=True,
|
||||||
),
|
),
|
||||||
db.UniqueConstraint("module_id", "app_crit_id", name="uix_module_id_app_crit_id"),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,21 +2,17 @@
|
|||||||
|
|
||||||
"""Décisions de jury (validations) des RCUE et années du BUT
|
"""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 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.but_refcomp import ApcNiveau
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
|
from app.models.formations import Formation
|
||||||
from app.models.formsemestre import FormSemestre
|
from app.models.formsemestre import FormSemestre
|
||||||
from app.models.ues import UniteEns
|
from app.models.ues import UniteEns
|
||||||
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
|
"""Validation des niveaux de compétences
|
||||||
|
|
||||||
aka "regroupements cohérents d'UE" dans le jargon BUT.
|
aka "regroupements cohérents d'UE" dans le jargon BUT.
|
||||||
@ -61,33 +57,30 @@ class ApcValidationRCUE(ScoDocModel):
|
|||||||
ue2 = db.relationship("UniteEns", foreign_keys=ue2_id)
|
ue2 = db.relationship("UniteEns", foreign_keys=ue2_id)
|
||||||
parcour = db.relationship("ApcParcours")
|
parcour = db.relationship("ApcParcours")
|
||||||
|
|
||||||
_sco_dept_relations = ("Identite",) # pour accéder au département
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"""<{self.__class__.__name__} {self.id} {self.etud} {
|
return f"""<{self.__class__.__name__} {self.id} {self.etud} {
|
||||||
self.ue1}/{self.ue2}:{self.code!r}>"""
|
self.ue1}/{self.ue2}:{self.code!r}>"""
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}: {
|
return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}: {
|
||||||
self.code} enregistrée le {self.date.strftime(scu.DATE_FMT)}"""
|
self.code} enregistrée le {self.date.strftime("%d/%m/%Y")}"""
|
||||||
|
|
||||||
def html(self) -> str:
|
def html(self) -> str:
|
||||||
"description en HTML"
|
"description en HTML"
|
||||||
return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}:
|
return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}:
|
||||||
<b>{self.code}</b>
|
<b>{self.code}</b>
|
||||||
<em>enregistrée le {self.date.strftime(scu.DATEATIME_FMT)}</em>"""
|
<em>enregistrée le {self.date.strftime("%d/%m/%Y")}
|
||||||
|
à {self.date.strftime("%Hh%M")}</em>"""
|
||||||
|
|
||||||
def annee(self) -> str:
|
def annee(self) -> str:
|
||||||
"""l'année BUT concernée: "BUT1", "BUT2" ou "BUT3" """
|
"""l'année BUT concernée: "BUT1", "BUT2" ou "BUT3" """
|
||||||
niveau = self.niveau()
|
niveau = self.niveau()
|
||||||
return niveau.annee if niveau else None
|
return niveau.annee if niveau else None
|
||||||
|
|
||||||
def niveau(self) -> ApcNiveau | None:
|
def niveau(self) -> ApcNiveau:
|
||||||
"""Le niveau de compétence associé à cet RCUE."""
|
"""Le niveau de compétence associé à cet RCUE."""
|
||||||
# Par convention, il est donné par la seconde UE
|
# Par convention, il est donné par la seconde UE
|
||||||
# à défaut (si l'UE a été désacciée entre temps), la première
|
return self.ue2.niveau_competence
|
||||||
# et à défaut, renvoie None
|
|
||||||
return self.ue2.niveau_competence or self.ue1.niveau_competence
|
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
"as a dict"
|
"as a dict"
|
||||||
@ -118,14 +111,8 @@ class ApcValidationRCUE(ScoDocModel):
|
|||||||
"formsemestre_id": self.formsemestre_id,
|
"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(db.Model):
|
||||||
class ApcValidationAnnee(ScoDocModel):
|
|
||||||
"""Validation des années du BUT"""
|
"""Validation des années du BUT"""
|
||||||
|
|
||||||
__tablename__ = "apc_validation_annee"
|
__tablename__ = "apc_validation_annee"
|
||||||
@ -156,8 +143,6 @@ class ApcValidationAnnee(ScoDocModel):
|
|||||||
etud = db.relationship("Identite", backref="apc_validations_annees")
|
etud = db.relationship("Identite", backref="apc_validations_annees")
|
||||||
formsemestre = db.relationship("FormSemestre", 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):
|
def __repr__(self):
|
||||||
return f"""<{self.__class__.__name__} {self.id} {self.etud
|
return f"""<{self.__class__.__name__} {self.id} {self.etud
|
||||||
} BUT{self.ordre}/{self.annee_scolaire}:{self.code!r}>"""
|
} BUT{self.ordre}/{self.annee_scolaire}:{self.code!r}>"""
|
||||||
@ -177,7 +162,7 @@ class ApcValidationAnnee(ScoDocModel):
|
|||||||
def html(self) -> str:
|
def html(self) -> str:
|
||||||
"Affichage html"
|
"Affichage html"
|
||||||
date_str = (
|
date_str = (
|
||||||
f"""le {self.date.strftime(scu.DATEATIME_FMT)}"""
|
f"""le {self.date.strftime("%d/%m/%Y")} à {self.date.strftime("%Hh%M")}"""
|
||||||
if self.date
|
if self.date
|
||||||
else "(sans date)"
|
else "(sans date)"
|
||||||
)
|
)
|
||||||
@ -215,9 +200,17 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
|
|||||||
.order_by(UniteEns.numero, UniteEns.acronyme)
|
.order_by(UniteEns.numero, UniteEns.acronyme)
|
||||||
)
|
)
|
||||||
decisions["decision_rcue"] = [v.to_dict_bul() for v in validations_rcues]
|
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"] = ", ".join(titres_rcues)
|
||||||
decisions["descr_decisions_rcue_list"] = titres_rcues
|
|
||||||
decisions["descr_decisions_niveaux"] = (
|
decisions["descr_decisions_niveaux"] = (
|
||||||
"Niveaux de compétences: " + decisions["descr_decisions_rcue"]
|
"Niveaux de compétences: " + decisions["descr_decisions_rcue"]
|
||||||
)
|
)
|
||||||
@ -226,127 +219,15 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
|
|||||||
decisions["descr_decisions_rcue"] = ""
|
decisions["descr_decisions_rcue"] = ""
|
||||||
decisions["descr_decisions_niveaux"] = ""
|
decisions["descr_decisions_niveaux"] = ""
|
||||||
# --- Année: prend la validation pour l'année scolaire et l'ordre de ce semestre
|
# --- Année: prend la validation pour l'année scolaire et l'ordre de ce semestre
|
||||||
if sco_preferences.get_preference("bul_but_code_annuel", formsemestre.id):
|
annee_but = (formsemestre.semestre_id + 1) // 2
|
||||||
annee_but = (formsemestre.semestre_id + 1) // 2
|
validation = ApcValidationAnnee.query.filter_by(
|
||||||
validation = ApcValidationAnnee.query.filter_by(
|
etudid=etud.id,
|
||||||
etudid=etud.id,
|
annee_scolaire=formsemestre.annee_scolaire(),
|
||||||
annee_scolaire=formsemestre.annee_scolaire(),
|
ordre=annee_but,
|
||||||
ordre=annee_but,
|
referentiel_competence_id=formsemestre.formation.referentiel_competence_id,
|
||||||
referentiel_competence_id=formsemestre.formation.referentiel_competence_id,
|
).first()
|
||||||
).first()
|
if validation:
|
||||||
if validation:
|
decisions["decision_annee"] = validation.to_dict_bul()
|
||||||
decisions["decision_annee"] = validation.to_dict_bul()
|
|
||||||
else:
|
|
||||||
decisions["decision_annee"] = None
|
|
||||||
else:
|
else:
|
||||||
decisions["decision_annee"] = None
|
decisions["decision_annee"] = None
|
||||||
return decisions
|
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}
|
|
||||||
"""
|
|
||||||
|
@ -92,11 +92,9 @@ class ScoDocSiteConfig(db.Model):
|
|||||||
"INSTITUTION_CITY": str,
|
"INSTITUTION_CITY": str,
|
||||||
"DEFAULT_PDF_FOOTER_TEMPLATE": str,
|
"DEFAULT_PDF_FOOTER_TEMPLATE": str,
|
||||||
"enable_entreprises": bool,
|
"enable_entreprises": bool,
|
||||||
"disable_passerelle": bool, # remplace pref. bul_display_publication
|
|
||||||
"month_debut_annee_scolaire": int,
|
"month_debut_annee_scolaire": int,
|
||||||
"month_debut_periode2": int,
|
"month_debut_periode2": int,
|
||||||
"disable_bul_pdf": bool,
|
"disable_bul_pdf": bool,
|
||||||
"user_require_email_institutionnel": bool,
|
|
||||||
# CAS
|
# CAS
|
||||||
"cas_enable": bool,
|
"cas_enable": bool,
|
||||||
"cas_server": str,
|
"cas_server": str,
|
||||||
@ -233,52 +231,49 @@ class ScoDocSiteConfig(db.Model):
|
|||||||
cfg = ScoDocSiteConfig.query.filter_by(name="cas_enable").first()
|
cfg = ScoDocSiteConfig.query.filter_by(name="cas_enable").first()
|
||||||
return cfg is not None and cfg.value
|
return cfg is not None and cfg.value
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def is_cas_forced(cls) -> bool:
|
|
||||||
"""True si CAS forcé"""
|
|
||||||
cfg = ScoDocSiteConfig.query.filter_by(name="cas_force").first()
|
|
||||||
return cfg is not None and cfg.value
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_entreprises_enabled(cls) -> bool:
|
def is_entreprises_enabled(cls) -> bool:
|
||||||
"""True si on doit activer le module entreprise"""
|
"""True si on doit activer le module entreprise"""
|
||||||
cfg = ScoDocSiteConfig.query.filter_by(name="enable_entreprises").first()
|
cfg = ScoDocSiteConfig.query.filter_by(name="enable_entreprises").first()
|
||||||
return cfg is not None and cfg.value
|
return cfg is not None and cfg.value
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def is_passerelle_disabled(cls):
|
|
||||||
"""True si on doit cacher les fonctions passerelle ("oeil")."""
|
|
||||||
cfg = ScoDocSiteConfig.query.filter_by(name="disable_passerelle").first()
|
|
||||||
return cfg is not None and cfg.value
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def is_user_require_email_institutionnel_enabled(cls) -> bool:
|
|
||||||
"""True si impose saisie email_institutionnel"""
|
|
||||||
cfg = ScoDocSiteConfig.query.filter_by(
|
|
||||||
name="user_require_email_institutionnel"
|
|
||||||
).first()
|
|
||||||
return cfg is not None and cfg.value
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_bul_pdf_disabled(cls) -> bool:
|
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()
|
cfg = ScoDocSiteConfig.query.filter_by(name="disable_bul_pdf").first()
|
||||||
return cfg is not None and cfg.value
|
return cfg is not None and cfg.value
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def enable_entreprises(cls, enabled: bool = True) -> bool:
|
def enable_entreprises(cls, enabled=True) -> bool:
|
||||||
"""Active (ou déactive) le module entreprises. True si changement."""
|
"""Active (ou déactive) le module entreprises. True si changement."""
|
||||||
return cls.set("enable_entreprises", "on" if enabled else "")
|
if enabled != ScoDocSiteConfig.is_entreprises_enabled():
|
||||||
|
cfg = ScoDocSiteConfig.query.filter_by(name="enable_entreprises").first()
|
||||||
@classmethod
|
if cfg is None:
|
||||||
def disable_passerelle(cls, disabled: bool = True) -> bool:
|
cfg = ScoDocSiteConfig(
|
||||||
"""Désactive (ou active) les fonctions liées à la présence d'une passerelle. True si changement."""
|
name="enable_entreprises", value="on" if enabled else ""
|
||||||
return cls.set("disable_passerelle", "on" if disabled else "")
|
)
|
||||||
|
else:
|
||||||
|
cfg.value = "on" if enabled else ""
|
||||||
|
db.session.add(cfg)
|
||||||
|
db.session.commit()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def disable_bul_pdf(cls, enabled=True) -> bool:
|
def disable_bul_pdf(cls, enabled=True) -> bool:
|
||||||
"""Interdit (ou autorise) les exports PDF. True si changement."""
|
"""Interedit (ou autorise) les exports PDF. True si changement."""
|
||||||
return cls.set("disable_bul_pdf", "on" if enabled else "")
|
if enabled != ScoDocSiteConfig.is_bul_pdf_disabled():
|
||||||
|
cfg = ScoDocSiteConfig.query.filter_by(name="disable_bul_pdf").first()
|
||||||
|
if cfg is None:
|
||||||
|
cfg = ScoDocSiteConfig(
|
||||||
|
name="disable_bul_pdf", value="on" if enabled else ""
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cfg.value = "on" if enabled else ""
|
||||||
|
db.session.add(cfg)
|
||||||
|
db.session.commit()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get(cls, name: str, default: str = "") -> str:
|
def get(cls, name: str, default: str = "") -> str:
|
||||||
@ -297,10 +292,9 @@ class ScoDocSiteConfig(db.Model):
|
|||||||
if cfg is None:
|
if cfg is None:
|
||||||
cfg = ScoDocSiteConfig(name=name, value=value_str)
|
cfg = ScoDocSiteConfig(name=name, value=value_str)
|
||||||
else:
|
else:
|
||||||
cfg.value = value_str
|
cfg.value = str(value or "")
|
||||||
current_app.logger.info(
|
current_app.logger.info(
|
||||||
f"""ScoDocSiteConfig: recording {cfg.name}='{cfg.value[:32]}{
|
f"""ScoDocSiteConfig: recording {cfg.name}='{cfg.value[:32]}...'"""
|
||||||
'...' if len(cfg.value)>32 else ''}'"""
|
|
||||||
)
|
)
|
||||||
db.session.add(cfg)
|
db.session.add(cfg)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@ -309,7 +303,7 @@ class ScoDocSiteConfig(db.Model):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_int_field(cls, name: str, default=None) -> int:
|
def _get_int_field(cls, name: str, default=None) -> int:
|
||||||
"""Valeur d'un champ integer"""
|
"""Valeur d'un champs integer"""
|
||||||
cfg = ScoDocSiteConfig.query.filter_by(name=name).first()
|
cfg = ScoDocSiteConfig.query.filter_by(name=name).first()
|
||||||
if (cfg is None) or cfg.value is None:
|
if (cfg is None) or cfg.value is None:
|
||||||
return default
|
return default
|
||||||
@ -323,7 +317,7 @@ class ScoDocSiteConfig(db.Model):
|
|||||||
default=None,
|
default=None,
|
||||||
range_values: tuple = (),
|
range_values: tuple = (),
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Set champ integer. True si changement."""
|
"""Set champs integer. True si changement."""
|
||||||
if value != cls._get_int_field(name, default=default):
|
if value != cls._get_int_field(name, default=default):
|
||||||
if not isinstance(value, int) or (
|
if not isinstance(value, int) or (
|
||||||
range_values and (value < range_values[0]) or (value > range_values[1])
|
range_values and (value < range_values[0]) or (value > range_values[1])
|
||||||
|
@ -52,17 +52,6 @@ class Departement(db.Model):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__name__}(id={self.id}, acronym='{self.acronym}')>"
|
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):
|
def to_dict(self, with_dept_name=True, with_dept_preferences=False):
|
||||||
data = {
|
data = {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
|
@ -15,15 +15,14 @@ from sqlalchemy import desc, text
|
|||||||
|
|
||||||
from app import db, log
|
from app import db, log
|
||||||
from app import models
|
from app import models
|
||||||
from app.models.departements import Departement
|
|
||||||
from app.models.scolar_event import ScolarEvent
|
|
||||||
from app.scodoc import notesdb as ndb
|
from app.scodoc import notesdb as ndb
|
||||||
from app.scodoc.sco_bac import Baccalaureat
|
from app.scodoc.sco_bac import Baccalaureat
|
||||||
from app.scodoc.sco_exceptions import ScoGenError, ScoInvalidParamError, ScoValueError
|
from app.scodoc.sco_exceptions import ScoInvalidParamError, ScoValueError
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
class Identite(models.ScoDocModel):
|
class Identite(db.Model, models.ScoDocModel):
|
||||||
"""étudiant"""
|
"""étudiant"""
|
||||||
|
|
||||||
__tablename__ = "identite"
|
__tablename__ = "identite"
|
||||||
@ -101,12 +100,7 @@ class Identite(models.ScoDocModel):
|
|||||||
adresses = db.relationship(
|
adresses = db.relationship(
|
||||||
"Adresse", back_populates="etud", cascade="all,delete", lazy="dynamic"
|
"Adresse", back_populates="etud", cascade="all,delete", lazy="dynamic"
|
||||||
)
|
)
|
||||||
annotations = db.relationship(
|
|
||||||
"EtudAnnotation",
|
|
||||||
backref="etudiant",
|
|
||||||
cascade="all, delete-orphan",
|
|
||||||
lazy="dynamic",
|
|
||||||
)
|
|
||||||
billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic")
|
billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic")
|
||||||
#
|
#
|
||||||
dispense_ues = db.relationship(
|
dispense_ues = db.relationship(
|
||||||
@ -124,9 +118,6 @@ class Identite(models.ScoDocModel):
|
|||||||
"Justificatif", back_populates="etudiant", lazy="dynamic", cascade="all, delete"
|
"Justificatif", back_populates="etudiant", lazy="dynamic", cascade="all, delete"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Champs "protégés" par ViewEtudData (RGPD)
|
|
||||||
protected_attrs = {"boursier", "nationalite"}
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return (
|
return (
|
||||||
f"<Etud {self.id}/{self.departement.acronym} {self.nom!r} {self.prenom!r}>"
|
f"<Etud {self.id}/{self.departement.acronym} {self.nom!r} {self.prenom!r}>"
|
||||||
@ -179,15 +170,9 @@ class Identite(models.ScoDocModel):
|
|||||||
|
|
||||||
def html_link_fiche(self) -> str:
|
def html_link_fiche(self) -> str:
|
||||||
"lien vers la fiche"
|
"lien vers la fiche"
|
||||||
return (
|
return f"""<a class="stdlink" href="{
|
||||||
f"""<a class="etudlink" href="{self.url_fiche()}">{self.nom_prenom()}</a>"""
|
url_for("scolar.ficheEtud", scodoc_dept=self.departement.acronym, etudid=self.id)
|
||||||
)
|
}">{self.nomprenom}</a>"""
|
||||||
|
|
||||||
def url_fiche(self) -> str:
|
|
||||||
"url de la fiche étudiant"
|
|
||||||
return url_for(
|
|
||||||
"scolar.fiche_etud", scodoc_dept=self.departement.acronym, etudid=self.id
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_request(cls, etudid=None, code_nip=None) -> "Identite":
|
def from_request(cls, etudid=None, code_nip=None) -> "Identite":
|
||||||
@ -199,28 +184,13 @@ class Identite(models.ScoDocModel):
|
|||||||
return cls.query.filter_by(**args).first_or_404()
|
return cls.query.filter_by(**args).first_or_404()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_etud(cls, etudid: int, accept_none=False) -> "Identite":
|
def get_etud(cls, etudid: int) -> "Identite":
|
||||||
"""Etudiant ou 404 (ou None si accept_none),
|
"""Etudiant ou 404, cherche uniquement dans le département courant"""
|
||||||
cherche uniquement dans le département courant.
|
if g.scodoc_dept:
|
||||||
Si accept_none, return None si l'id est invalide ou ne correspond
|
return cls.query.filter_by(
|
||||||
pas à un étudiant.
|
id=etudid, dept_id=g.scodoc_dept_id
|
||||||
"""
|
).first_or_404()
|
||||||
if not isinstance(etudid, int):
|
return cls.query.filter_by(id=etudid).first_or_404()
|
||||||
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()
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_etud(cls, **args) -> "Identite":
|
def create_etud(cls, **args) -> "Identite":
|
||||||
@ -230,48 +200,19 @@ class Identite(models.ScoDocModel):
|
|||||||
return cls.create_from_dict(args)
|
return cls.create_from_dict(args)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_from_dict(cls, args) -> "Identite":
|
def create_from_dict(cls, data) -> "Identite":
|
||||||
"""Crée un étudiant à partir d'un dict, avec admission et adresse vides.
|
"""Crée un étudiant à partir d'un dict, avec admission et adresse vides.
|
||||||
If required dept_id or dept are not specified, set it to the current dept.
|
|
||||||
args: dict with args in application.
|
|
||||||
Les clés adresses et admission ne SONT PAS utilisées.
|
|
||||||
(added to session but not flushed nor commited)
|
(added to session but not flushed nor commited)
|
||||||
"""
|
"""
|
||||||
if not "dept_id" in args:
|
etud: Identite = super(cls, cls).create_from_dict(data)
|
||||||
if "dept" in args:
|
if (data.get("admission_id", None) is None) and (
|
||||||
departement = Departement.query.filter_by(acronym=args["dept"]).first()
|
data.get("admission", None) is None
|
||||||
if departement:
|
):
|
||||||
args["dept_id"] = departement.id
|
|
||||||
if not "dept_id" in args:
|
|
||||||
args["dept_id"] = g.scodoc_dept_id
|
|
||||||
etud: Identite = super().create_from_dict(args)
|
|
||||||
if args.get("admission_id", None) is None:
|
|
||||||
etud.admission = Admission()
|
etud.admission = Admission()
|
||||||
etud.adresses.append(Adresse(typeadresse="domicile"))
|
etud.adresses.append(Adresse(typeadresse="domicile"))
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
|
||||||
event = ScolarEvent(etud=etud, event_type="CREATION")
|
|
||||||
db.session.add(event)
|
|
||||||
log(f"Identite.create {etud}")
|
|
||||||
return etud
|
return etud
|
||||||
|
|
||||||
def from_dict(self, args, **kwargs) -> bool:
|
|
||||||
"""Check arguments, then modify.
|
|
||||||
Add to session but don't commit.
|
|
||||||
True if modification.
|
|
||||||
"""
|
|
||||||
check_etud_duplicate_code(args, "code_nip")
|
|
||||||
check_etud_duplicate_code(args, "code_ine")
|
|
||||||
return super().from_dict(args, **kwargs)
|
|
||||||
|
|
||||||
@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."""
|
|
||||||
return super().filter_model_attributes(
|
|
||||||
data,
|
|
||||||
excluded=(excluded or set()) | {"adresses", "admission", "departement"},
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def civilite_str(self) -> str:
|
def civilite_str(self) -> str:
|
||||||
"""returns civilité usuelle: 'M.' ou 'Mme' ou '' (pour le genre neutre,
|
"""returns civilité usuelle: 'M.' ou 'Mme' ou '' (pour le genre neutre,
|
||||||
@ -314,24 +255,18 @@ class Identite(models.ScoDocModel):
|
|||||||
else:
|
else:
|
||||||
return self.nom
|
return self.nom
|
||||||
|
|
||||||
@property
|
@cached_property
|
||||||
def nomprenom(self, reverse=False) -> str:
|
def nomprenom(self, reverse=False) -> str:
|
||||||
"""DEPRECATED: préférer nom_prenom()
|
"""Civilité/nom/prenom pour affichages: "M. Pierre Dupont"
|
||||||
Civilité/prénom/nom pour affichages: "M. Pierre Dupont"
|
|
||||||
Si reverse, "Dupont Pierre", sans civilité.
|
Si reverse, "Dupont Pierre", sans civilité.
|
||||||
Prend l'identité courante et non celle de l'état civil si elles diffèrent.
|
|
||||||
"""
|
"""
|
||||||
nom = self.nom_usuel or self.nom
|
nom = self.nom_usuel or self.nom
|
||||||
prenom = self.prenom_str
|
prenom = self.prenom_str
|
||||||
if reverse:
|
if reverse:
|
||||||
return f"{nom} {prenom}".strip()
|
fields = (nom, prenom)
|
||||||
return f"{self.civilite_str} {prenom} {nom}".strip()
|
else:
|
||||||
|
fields = (self.civilite_str, prenom, nom)
|
||||||
def nom_prenom(self) -> str:
|
return " ".join([x for x in fields if x])
|
||||||
"""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
|
@property
|
||||||
def prenom_str(self):
|
def prenom_str(self):
|
||||||
@ -347,10 +282,12 @@ class Identite(models.ScoDocModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def etat_civil(self) -> str:
|
def etat_civil(self) -> str:
|
||||||
"M. PRÉNOM NOM, utilisant les données état civil si présentes, usuelles sinon."
|
"M. Prénom NOM, utilisant les données état civil si présentes, usuelles sinon."
|
||||||
return f"""{self.civilite_etat_civil_str} {
|
if self.prenom_etat_civil:
|
||||||
self.prenom_etat_civil or self.prenom or ''} {
|
civ = {"M": "M.", "F": "Mme", "X": ""}[self.civilite_etat_civil]
|
||||||
self.nom or ''}""".strip()
|
return f"{civ} {self.prenom_etat_civil} {self.nom}"
|
||||||
|
else:
|
||||||
|
return self.nomprenom
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def nom_short(self):
|
def nom_short(self):
|
||||||
@ -358,81 +295,34 @@ class Identite(models.ScoDocModel):
|
|||||||
return f"{(self.nom_usuel or self.nom or '?').upper()} {(self.prenom or '')[:2].capitalize()}."
|
return f"{(self.nom_usuel or self.nom or '?').upper()} {(self.prenom or '')[:2].capitalize()}."
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def sort_key(self) -> str:
|
def sort_key(self) -> tuple:
|
||||||
"clé pour tris par ordre alphabétique"
|
"clé pour tris par ordre alphabétique"
|
||||||
# Note: scodoc7 utilisait sco_etud.etud_sort_key, à mettre à jour
|
return (
|
||||||
# si on modifie cette méthode.
|
scu.sanitize_string(
|
||||||
return scu.sanitize_string(
|
self.nom_usuel or self.nom or "", remove_spaces=False
|
||||||
(self.nom_usuel or self.nom or "") + ";" + (self.prenom or ""),
|
).lower(),
|
||||||
remove_spaces=False,
|
scu.sanitize_string(self.prenom or "", remove_spaces=False).lower(),
|
||||||
).lower()
|
)
|
||||||
|
|
||||||
def get_first_email(self, field="email") -> str:
|
def get_first_email(self, field="email") -> str:
|
||||||
"Le mail associé à la première adresse de l'étudiant, ou None"
|
"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
|
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,
|
"""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)
|
triée par date_debut
|
||||||
(si recent_first=False, le plus ancien en tête)
|
|
||||||
"""
|
"""
|
||||||
return sorted(
|
return sorted(
|
||||||
[ins.formsemestre for ins in self.formsemestre_inscriptions],
|
[ins.formsemestre for ins in self.formsemestre_inscriptions],
|
||||||
key=attrgetter("date_debut"),
|
key=attrgetter("date_debut"),
|
||||||
reverse=recent_first,
|
reverse=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_modimpls_by_formsemestre(
|
|
||||||
self, annee_scolaire: int
|
|
||||||
) -> dict[int, list["ModuleImpl"]]:
|
|
||||||
"""Pour chaque semestre de l'année indiquée dans lequel l'étudiant
|
|
||||||
est inscrit à des moduleimpls, liste ceux ci.
|
|
||||||
{ formsemestre_id : [ modimpl, ... ] }
|
|
||||||
annee_scolaire est un nombre: eg 2023
|
|
||||||
"""
|
|
||||||
date_debut_annee = scu.date_debut_annee_scolaire(annee_scolaire)
|
|
||||||
date_fin_annee = scu.date_fin_annee_scolaire(annee_scolaire)
|
|
||||||
modimpls = (
|
|
||||||
ModuleImpl.query.join(ModuleImplInscription)
|
|
||||||
.join(FormSemestre)
|
|
||||||
.filter(
|
|
||||||
(FormSemestre.date_debut <= date_fin_annee)
|
|
||||||
& (FormSemestre.date_fin >= date_debut_annee)
|
|
||||||
)
|
|
||||||
.join(Identite)
|
|
||||||
.filter_by(id=self.id)
|
|
||||||
)
|
|
||||||
# Tri, par semestre puis par module, suivant le type de formation:
|
|
||||||
formsemestres = sorted(
|
|
||||||
{m.formsemestre for m in modimpls}, key=lambda s: s.sort_key()
|
|
||||||
)
|
|
||||||
modimpls_by_formsemestre = {}
|
|
||||||
for formsemestre in formsemestres:
|
|
||||||
modimpls_sem = [m for m in modimpls if m.formsemestre_id == formsemestre.id]
|
|
||||||
if formsemestre.formation.is_apc():
|
|
||||||
modimpls_sem.sort(key=lambda m: m.module.sort_key_apc())
|
|
||||||
else:
|
|
||||||
modimpls_sem.sort(
|
|
||||||
key=lambda m: (m.module.ue.numero or 0, m.module.numero or 0)
|
|
||||||
)
|
|
||||||
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
|
@classmethod
|
||||||
def convert_dict_fields(cls, args: dict) -> dict:
|
def convert_dict_fields(cls, args: dict) -> dict:
|
||||||
"""Convert fields in the given dict. No other side effect.
|
"""Convert fields in the given dict. No other side effect.
|
||||||
|
If required dept_id is not specified, set it to the current dept.
|
||||||
|
args: dict with args in application.
|
||||||
returns: dict to store in model's db.
|
returns: dict to store in model's db.
|
||||||
"""
|
"""
|
||||||
# Les champs qui sont toujours stockés en majuscules:
|
# Les champs qui sont toujours stockés en majuscules:
|
||||||
@ -451,6 +341,8 @@ class Identite(models.ScoDocModel):
|
|||||||
"code_ine",
|
"code_ine",
|
||||||
}
|
}
|
||||||
args_dict = {}
|
args_dict = {}
|
||||||
|
if not "dept_id" in args:
|
||||||
|
args["dept_id"] = g.scodoc_dept_id
|
||||||
for key, value in args.items():
|
for key, value in args.items():
|
||||||
if hasattr(cls, key) and not isinstance(getattr(cls, key, None), property):
|
if hasattr(cls, key) and not isinstance(getattr(cls, key, None), property):
|
||||||
# compat scodoc7 (mauvaise idée de l'époque)
|
# compat scodoc7 (mauvaise idée de l'époque)
|
||||||
@ -463,14 +355,14 @@ class Identite(models.ScoDocModel):
|
|||||||
elif key == "civilite_etat_civil":
|
elif key == "civilite_etat_civil":
|
||||||
value = input_civilite_etat_civil(value)
|
value = input_civilite_etat_civil(value)
|
||||||
elif key == "boursier":
|
elif key == "boursier":
|
||||||
value = scu.to_bool(value)
|
value = bool(value)
|
||||||
elif key == "date_naissance":
|
elif key == "date_naissance":
|
||||||
value = ndb.DateDMYtoISO(value)
|
value = ndb.DateDMYtoISO(value)
|
||||||
args_dict[key] = value
|
args_dict[key] = value
|
||||||
return args_dict
|
return args_dict
|
||||||
|
|
||||||
def to_dict_short(self) -> dict:
|
def to_dict_short(self) -> dict:
|
||||||
"""Les champs essentiels (aucune donnée perso protégée)"""
|
"""Les champs essentiels"""
|
||||||
return {
|
return {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"civilite": self.civilite,
|
"civilite": self.civilite,
|
||||||
@ -485,11 +377,9 @@ class Identite(models.ScoDocModel):
|
|||||||
"prenom_etat_civil": self.prenom_etat_civil,
|
"prenom_etat_civil": self.prenom_etat_civil,
|
||||||
}
|
}
|
||||||
|
|
||||||
def to_dict_scodoc7(self, restrict=False, with_inscriptions=False) -> dict:
|
def to_dict_scodoc7(self) -> dict:
|
||||||
"""Représentation dictionnaire,
|
"""Représentation dictionnaire,
|
||||||
compatible ScoDoc7 mais sans infos admission.
|
compatible ScoDoc7 mais sans infos admission
|
||||||
Si restrict, cache les infos "personnelles" si pas permission ViewEtudData
|
|
||||||
Si with_inscriptions, inclut les champs "inscription"
|
|
||||||
"""
|
"""
|
||||||
e_dict = self.__dict__.copy() # dict(self.__dict__)
|
e_dict = self.__dict__.copy() # dict(self.__dict__)
|
||||||
e_dict.pop("_sa_instance_state", None)
|
e_dict.pop("_sa_instance_state", None)
|
||||||
@ -500,9 +390,7 @@ class Identite(models.ScoDocModel):
|
|||||||
e_dict["nomprenom"] = self.nomprenom
|
e_dict["nomprenom"] = self.nomprenom
|
||||||
adresse = self.adresses.first()
|
adresse = self.adresses.first()
|
||||||
if adresse:
|
if adresse:
|
||||||
e_dict.update(adresse.to_dict(restrict=restrict))
|
e_dict.update(adresse.to_dict())
|
||||||
if with_inscriptions:
|
|
||||||
e_dict.update(self.inscription_descr())
|
|
||||||
return {k: v or "" for k, v in e_dict.items()} # convert_null_outputs_to_empty
|
return {k: v or "" for k, v in e_dict.items()} # convert_null_outputs_to_empty
|
||||||
|
|
||||||
def to_dict_bul(self, include_urls=True):
|
def to_dict_bul(self, include_urls=True):
|
||||||
@ -517,11 +405,9 @@ class Identite(models.ScoDocModel):
|
|||||||
"civilite": self.civilite,
|
"civilite": self.civilite,
|
||||||
"code_ine": self.code_ine or "",
|
"code_ine": self.code_ine or "",
|
||||||
"code_nip": self.code_nip or "",
|
"code_nip": self.code_nip or "",
|
||||||
"date_naissance": (
|
"date_naissance": self.date_naissance.strftime("%d/%m/%Y")
|
||||||
self.date_naissance.strftime(scu.DATE_FMT)
|
if self.date_naissance
|
||||||
if self.date_naissance
|
else "",
|
||||||
else ""
|
|
||||||
),
|
|
||||||
"dept_acronym": self.departement.acronym,
|
"dept_acronym": self.departement.acronym,
|
||||||
"dept_id": self.dept_id,
|
"dept_id": self.dept_id,
|
||||||
"dept_naissance": self.dept_naissance or "",
|
"dept_naissance": self.dept_naissance or "",
|
||||||
@ -539,7 +425,7 @@ class Identite(models.ScoDocModel):
|
|||||||
if include_urls and has_request_context():
|
if include_urls and has_request_context():
|
||||||
# test request context so we can use this func in tests under the flask shell
|
# test request context so we can use this func in tests under the flask shell
|
||||||
d["fiche_url"] = url_for(
|
d["fiche_url"] = url_for(
|
||||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=self.id
|
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=self.id
|
||||||
)
|
)
|
||||||
d["photo_url"] = sco_photos.get_etud_photo_url(self.id)
|
d["photo_url"] = sco_photos.get_etud_photo_url(self.id)
|
||||||
adresse = self.adresses.first()
|
adresse = self.adresses.first()
|
||||||
@ -548,37 +434,22 @@ class Identite(models.ScoDocModel):
|
|||||||
d["id"] = self.id # a été écrasé par l'id de adresse
|
d["id"] = self.id # a été écrasé par l'id de adresse
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def to_dict_api(self, restrict=False, with_annotations=False) -> dict:
|
def to_dict_api(self) -> dict:
|
||||||
"""Représentation dictionnaire pour export API, avec adresses et admission.
|
"""Représentation dictionnaire pour export API, avec adresses et admission."""
|
||||||
Si restrict, supprime les infos "personnelles" (boursier)
|
|
||||||
"""
|
|
||||||
e = dict(self.__dict__)
|
e = dict(self.__dict__)
|
||||||
e.pop("_sa_instance_state", None)
|
e.pop("_sa_instance_state", None)
|
||||||
admission = self.admission
|
admission = self.admission
|
||||||
e["admission"] = admission.to_dict() if admission is not None else None
|
e["admission"] = admission.to_dict() if admission is not None else None
|
||||||
e["adresses"] = [adr.to_dict(restrict=restrict) for adr in self.adresses]
|
e["adresses"] = [adr.to_dict() for adr in self.adresses]
|
||||||
e["dept_acronym"] = self.departement.acronym
|
e["dept_acronym"] = self.departement.acronym
|
||||||
e.pop("departement", None)
|
e.pop("departement", None)
|
||||||
e["sort_key"] = self.sort_key
|
e["sort_key"] = self.sort_key
|
||||||
if with_annotations:
|
|
||||||
e["annotations"] = (
|
|
||||||
[
|
|
||||||
annot.to_dict()
|
|
||||||
for annot in EtudAnnotation.query.filter_by(
|
|
||||||
etudid=self.id
|
|
||||||
).order_by(desc(EtudAnnotation.date))
|
|
||||||
]
|
|
||||||
if not restrict
|
|
||||||
else []
|
|
||||||
)
|
|
||||||
if restrict:
|
|
||||||
# Met à None les attributs protégés:
|
|
||||||
for attr in self.protected_attrs:
|
|
||||||
e[attr] = None
|
|
||||||
return e
|
return e
|
||||||
|
|
||||||
def inscriptions(self) -> list["FormSemestreInscription"]:
|
def inscriptions(self) -> list["FormSemestreInscription"]:
|
||||||
"Liste des inscriptions à des formsemestres, triée, la plus récente en tête"
|
"Liste des inscriptions à des formsemestres, triée, la plus récente en tête"
|
||||||
|
from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
||||||
|
|
||||||
return (
|
return (
|
||||||
FormSemestreInscription.query.join(FormSemestreInscription.formsemestre)
|
FormSemestreInscription.query.join(FormSemestreInscription.formsemestre)
|
||||||
.filter(
|
.filter(
|
||||||
@ -588,7 +459,7 @@ class Identite(models.ScoDocModel):
|
|||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
def inscription_courante(self) -> "FormSemestreInscription | None":
|
def inscription_courante(self):
|
||||||
"""La première inscription à un formsemestre _actuellement_ en cours.
|
"""La première inscription à un formsemestre _actuellement_ en cours.
|
||||||
None s'il n'y en a pas (ou plus, ou pas encore).
|
None s'il n'y en a pas (ou plus, ou pas encore).
|
||||||
"""
|
"""
|
||||||
@ -604,6 +475,8 @@ class Identite(models.ScoDocModel):
|
|||||||
(il est rare qu'il y en ai plus d'une, mais c'est possible).
|
(il est rare qu'il y en ai plus d'une, mais c'est possible).
|
||||||
Triées par date de début de semestre décroissante (le plus récent en premier).
|
Triées par date de début de semestre décroissante (le plus récent en premier).
|
||||||
"""
|
"""
|
||||||
|
from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
||||||
|
|
||||||
return (
|
return (
|
||||||
FormSemestreInscription.query.join(FormSemestreInscription.formsemestre)
|
FormSemestreInscription.query.join(FormSemestreInscription.formsemestre)
|
||||||
.filter(
|
.filter(
|
||||||
@ -626,9 +499,7 @@ class Identite(models.ScoDocModel):
|
|||||||
return r[0] if r else None
|
return r[0] if r else None
|
||||||
|
|
||||||
def inscription_descr(self) -> dict:
|
def inscription_descr(self) -> dict:
|
||||||
"""Description de l'état d'inscription
|
"""Description de l'état d'inscription"""
|
||||||
avec champs compatibles templates ScoDoc7
|
|
||||||
"""
|
|
||||||
inscription_courante = self.inscription_courante()
|
inscription_courante = self.inscription_courante()
|
||||||
if inscription_courante:
|
if inscription_courante:
|
||||||
titre_sem = inscription_courante.formsemestre.titre_mois()
|
titre_sem = inscription_courante.formsemestre.titre_mois()
|
||||||
@ -639,7 +510,7 @@ class Identite(models.ScoDocModel):
|
|||||||
else:
|
else:
|
||||||
inscr_txt = "Inscrit en"
|
inscr_txt = "Inscrit en"
|
||||||
|
|
||||||
result = {
|
return {
|
||||||
"etat_in_cursem": inscription_courante.etat,
|
"etat_in_cursem": inscription_courante.etat,
|
||||||
"inscription_courante": inscription_courante,
|
"inscription_courante": inscription_courante,
|
||||||
"inscription": titre_sem,
|
"inscription": titre_sem,
|
||||||
@ -662,20 +533,15 @@ class Identite(models.ScoDocModel):
|
|||||||
inscription = "ancien"
|
inscription = "ancien"
|
||||||
situation = "ancien élève"
|
situation = "ancien élève"
|
||||||
else:
|
else:
|
||||||
inscription = "non inscrit"
|
inscription = ("non inscrit",)
|
||||||
situation = inscription
|
situation = inscription
|
||||||
result = {
|
return {
|
||||||
"etat_in_cursem": "?",
|
"etat_in_cursem": "?",
|
||||||
"inscription_courante": None,
|
"inscription_courante": None,
|
||||||
"inscription": inscription,
|
"inscription": inscription,
|
||||||
"inscription_str": inscription,
|
"inscription_str": inscription,
|
||||||
"situation": situation,
|
"situation": situation,
|
||||||
}
|
}
|
||||||
# aliases pour compat templates ScoDoc7
|
|
||||||
result["etatincursem"] = result["etat_in_cursem"]
|
|
||||||
result["inscriptionstr"] = result["inscription_str"]
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def inscription_etat(self, formsemestre_id: int) -> str:
|
def inscription_etat(self, formsemestre_id: int) -> str:
|
||||||
"""État de l'inscription de cet étudiant au semestre:
|
"""État de l'inscription de cet étudiant au semestre:
|
||||||
@ -772,7 +638,7 @@ class Identite(models.ScoDocModel):
|
|||||||
"""
|
"""
|
||||||
if with_paragraph:
|
if with_paragraph:
|
||||||
return f"""{self.etat_civil}{line_sep}n°{self.code_nip or ""}{line_sep}né{self.e} le {
|
return f"""{self.etat_civil}{line_sep}n°{self.code_nip or ""}{line_sep}né{self.e} le {
|
||||||
self.date_naissance.strftime(scu.DATE_FMT) if self.date_naissance else ""}{
|
self.date_naissance.strftime("%d/%m/%Y") if self.date_naissance else ""}{
|
||||||
line_sep}à {self.lieu_naissance or ""}"""
|
line_sep}à {self.lieu_naissance or ""}"""
|
||||||
return self.etat_civil
|
return self.etat_civil
|
||||||
|
|
||||||
@ -796,58 +662,6 @@ class Identite(models.ScoDocModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def check_etud_duplicate_code(args, code_name, edit=True):
|
|
||||||
"""Vérifie que le code n'est pas dupliqué.
|
|
||||||
Raises ScoGenError si problème.
|
|
||||||
"""
|
|
||||||
etudid = args.get("etudid", None)
|
|
||||||
if not args.get(code_name, None):
|
|
||||||
return
|
|
||||||
etuds = Identite.query.filter_by(
|
|
||||||
**{code_name: str(args[code_name]), "dept_id": g.scodoc_dept_id}
|
|
||||||
).all()
|
|
||||||
duplicate = False
|
|
||||||
if edit:
|
|
||||||
duplicate = (len(etuds) > 1) or ((len(etuds) == 1) and etuds[0].id != etudid)
|
|
||||||
else:
|
|
||||||
duplicate = len(etuds) > 0
|
|
||||||
if duplicate:
|
|
||||||
listh = [] # liste des doubles
|
|
||||||
for etud in etuds:
|
|
||||||
listh.append(f"Autre étudiant: {etud.html_link_fiche()}")
|
|
||||||
if etudid:
|
|
||||||
submit_label = "retour à la fiche étudiant"
|
|
||||||
dest_endpoint = "scolar.fiche_etud"
|
|
||||||
parameters = {"etudid": etudid}
|
|
||||||
else:
|
|
||||||
if "tf_submitted" in args:
|
|
||||||
del args["tf_submitted"]
|
|
||||||
submit_label = "Continuer"
|
|
||||||
dest_endpoint = "scolar.etudident_create_form"
|
|
||||||
parameters = args
|
|
||||||
else:
|
|
||||||
submit_label = "Annuler"
|
|
||||||
dest_endpoint = "notes.index_html"
|
|
||||||
parameters = {}
|
|
||||||
|
|
||||||
err_page = f"""<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>
|
|
||||||
<ul><li>
|
|
||||||
{ '</li><li>'.join(listh) }
|
|
||||||
</li></ul>
|
|
||||||
<p>
|
|
||||||
<a href="{ url_for(dest_endpoint, scodoc_dept=g.scodoc_dept, **parameters) }
|
|
||||||
">{submit_label}</a>
|
|
||||||
</p>
|
|
||||||
"""
|
|
||||||
|
|
||||||
log(f"*** error: code {code_name} duplique: {args[code_name]}")
|
|
||||||
|
|
||||||
raise ScoGenError(err_page, safe=True)
|
|
||||||
|
|
||||||
|
|
||||||
def make_etud_args(
|
def make_etud_args(
|
||||||
etudid=None, code_nip=None, use_request=True, raise_exc=False, abort_404=True
|
etudid=None, code_nip=None, use_request=True, raise_exc=False, abort_404=True
|
||||||
) -> dict:
|
) -> dict:
|
||||||
@ -928,7 +742,7 @@ def pivot_year(y) -> int:
|
|||||||
return y
|
return y
|
||||||
|
|
||||||
|
|
||||||
class Adresse(models.ScoDocModel):
|
class Adresse(db.Model, models.ScoDocModel):
|
||||||
"""Adresse d'un étudiant
|
"""Adresse d'un étudiant
|
||||||
(le modèle permet plusieurs adresses, mais l'UI n'en gère qu'une seule)
|
(le modèle permet plusieurs adresses, mais l'UI n'en gère qu'une seule)
|
||||||
"""
|
"""
|
||||||
@ -955,29 +769,16 @@ class Adresse(models.ScoDocModel):
|
|||||||
)
|
)
|
||||||
description = db.Column(db.Text)
|
description = db.Column(db.Text)
|
||||||
|
|
||||||
# Champs "protégés" par ViewEtudData (RGPD)
|
def to_dict(self, convert_nulls_to_str=False):
|
||||||
protected_attrs = {
|
"""Représentation dictionnaire,"""
|
||||||
"emailperso",
|
|
||||||
"domicile",
|
|
||||||
"codepostaldomicile",
|
|
||||||
"villedomicile",
|
|
||||||
"telephone",
|
|
||||||
"telephonemobile",
|
|
||||||
"fax",
|
|
||||||
}
|
|
||||||
|
|
||||||
def to_dict(self, convert_nulls_to_str=False, restrict=False):
|
|
||||||
"""Représentation dictionnaire. Si restrict, filtre les champs protégés (RGPD)."""
|
|
||||||
e = dict(self.__dict__)
|
e = dict(self.__dict__)
|
||||||
e.pop("_sa_instance_state", None)
|
e.pop("_sa_instance_state", None)
|
||||||
if convert_nulls_to_str:
|
if convert_nulls_to_str:
|
||||||
e = {k: v or "" for k, v in e.items()}
|
return {k: e[k] or "" for k in e}
|
||||||
if restrict:
|
|
||||||
e = {k: v for (k, v) in e.items() if k not in self.protected_attrs}
|
|
||||||
return e
|
return e
|
||||||
|
|
||||||
|
|
||||||
class Admission(models.ScoDocModel):
|
class Admission(db.Model, models.ScoDocModel):
|
||||||
"""Informations liées à l'admission d'un étudiant"""
|
"""Informations liées à l'admission d'un étudiant"""
|
||||||
|
|
||||||
__tablename__ = "admissions"
|
__tablename__ = "admissions"
|
||||||
@ -1028,16 +829,12 @@ class Admission(models.ScoDocModel):
|
|||||||
# classement (1..Ngr) par le jury dans le groupe APB
|
# classement (1..Ngr) par le jury dans le groupe APB
|
||||||
apb_classement_gr = db.Column(db.Integer)
|
apb_classement_gr = db.Column(db.Integer)
|
||||||
|
|
||||||
# Tous les champs sont "protégés" par ViewEtudData (RGPD)
|
|
||||||
# sauf:
|
|
||||||
not_protected_attrs = {"bac", "specialite", "anne_bac"}
|
|
||||||
|
|
||||||
def get_bac(self) -> Baccalaureat:
|
def get_bac(self) -> Baccalaureat:
|
||||||
"Le bac. utiliser bac.abbrev() pour avoir une chaine de caractères."
|
"Le bac. utiliser bac.abbrev() pour avoir une chaine de caractères."
|
||||||
return Baccalaureat(self.bac, specialite=self.specialite)
|
return Baccalaureat(self.bac, specialite=self.specialite)
|
||||||
|
|
||||||
def to_dict(self, no_nulls=False, restrict=False):
|
def to_dict(self, no_nulls=False):
|
||||||
"""Représentation dictionnaire. Si restrict, filtre les champs protégés (RGPD)."""
|
"""Représentation dictionnaire,"""
|
||||||
d = dict(self.__dict__)
|
d = dict(self.__dict__)
|
||||||
d.pop("_sa_instance_state", None)
|
d.pop("_sa_instance_state", None)
|
||||||
if no_nulls:
|
if no_nulls:
|
||||||
@ -1052,8 +849,6 @@ class Admission(models.ScoDocModel):
|
|||||||
d[key] = 0
|
d[key] = 0
|
||||||
elif isinstance(col_type, sqlalchemy.Boolean):
|
elif isinstance(col_type, sqlalchemy.Boolean):
|
||||||
d[key] = False
|
d[key] = False
|
||||||
if restrict:
|
|
||||||
d = {k: v for (k, v) in d.items() if k in self.not_protected_attrs}
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -1080,9 +875,8 @@ class Admission(models.ScoDocModel):
|
|||||||
return args_dict
|
return args_dict
|
||||||
|
|
||||||
|
|
||||||
class ItemSuivi(models.ScoDocModel):
|
# Suivi scolarité / débouchés
|
||||||
"""Suivi scolarité / débouchés"""
|
class ItemSuivi(db.Model):
|
||||||
|
|
||||||
__tablename__ = "itemsuivi"
|
__tablename__ = "itemsuivi"
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
@ -1094,8 +888,6 @@ class ItemSuivi(models.ScoDocModel):
|
|||||||
item_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
item_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||||
situation = db.Column(db.Text)
|
situation = db.Column(db.Text)
|
||||||
|
|
||||||
_sco_dept_relations = ("Identite",) # accès au dept_id
|
|
||||||
|
|
||||||
|
|
||||||
class ItemSuiviTag(db.Model):
|
class ItemSuiviTag(db.Model):
|
||||||
__tablename__ = "itemsuivi_tags"
|
__tablename__ = "itemsuivi_tags"
|
||||||
@ -1117,25 +909,13 @@ itemsuivi_tags_assoc = db.Table(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class EtudAnnotation(models.ScoDocModel):
|
class EtudAnnotation(db.Model):
|
||||||
"""Annotation sur un étudiant"""
|
"""Annotation sur un étudiant"""
|
||||||
|
|
||||||
__tablename__ = "etud_annotations"
|
__tablename__ = "etud_annotations"
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||||
etudid = db.Column(db.Integer, db.ForeignKey(Identite.id))
|
etudid = db.Column(db.Integer) # sans contrainte (compat ScoDoc 7))
|
||||||
author = db.Column(db.Text) # le pseudo (user_name), was zope_authenticated_user
|
author = db.Column(db.Text) # le pseudo (user_name), was zope_authenticated_user
|
||||||
comment = db.Column(db.Text)
|
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__)
|
|
||||||
e.pop("_sa_instance_state", None)
|
|
||||||
return e
|
|
||||||
|
|
||||||
|
|
||||||
from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
|
||||||
from app.models.moduleimpls import ModuleImpl, ModuleImplInscription
|
|
||||||
|
@ -5,15 +5,16 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
from flask import abort, g, url_for
|
from flask import g, url_for
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from app import db, log
|
from app import db, log
|
||||||
from app import models
|
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
from app.models.events import ScolarNews
|
from app.models.events import ScolarNews
|
||||||
|
from app.models.moduleimpls import ModuleImpl
|
||||||
from app.models.notes import NotesNotes
|
from app.models.notes import NotesNotes
|
||||||
|
from app.models.ues import UniteEns
|
||||||
|
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||||
@ -24,8 +25,10 @@ MAX_EVALUATION_DURATION = datetime.timedelta(days=365)
|
|||||||
NOON = datetime.time(12, 00)
|
NOON = datetime.time(12, 00)
|
||||||
DEFAULT_EVALUATION_TIME = datetime.time(8, 0)
|
DEFAULT_EVALUATION_TIME = datetime.time(8, 0)
|
||||||
|
|
||||||
|
VALID_EVALUATION_TYPES = {0, 1, 2}
|
||||||
|
|
||||||
class Evaluation(models.ScoDocModel):
|
|
||||||
|
class Evaluation(db.Model):
|
||||||
"""Evaluation (contrôle, examen, ...)"""
|
"""Evaluation (contrôle, examen, ...)"""
|
||||||
|
|
||||||
__tablename__ = "notes_evaluation"
|
__tablename__ = "notes_evaluation"
|
||||||
@ -37,9 +40,9 @@ class Evaluation(models.ScoDocModel):
|
|||||||
)
|
)
|
||||||
date_debut = db.Column(db.DateTime(timezone=True), nullable=True)
|
date_debut = db.Column(db.DateTime(timezone=True), nullable=True)
|
||||||
date_fin = db.Column(db.DateTime(timezone=True), nullable=True)
|
date_fin = db.Column(db.DateTime(timezone=True), nullable=True)
|
||||||
description = db.Column(db.Text, nullable=False)
|
description = db.Column(db.Text)
|
||||||
note_max = db.Column(db.Float, nullable=False)
|
note_max = db.Column(db.Float)
|
||||||
coefficient = db.Column(db.Float, nullable=False)
|
coefficient = db.Column(db.Float)
|
||||||
visibulletin = db.Column(
|
visibulletin = db.Column(
|
||||||
db.Boolean, nullable=False, default=True, server_default="true"
|
db.Boolean, nullable=False, default=True, server_default="true"
|
||||||
)
|
)
|
||||||
@ -47,41 +50,15 @@ class Evaluation(models.ScoDocModel):
|
|||||||
publish_incomplete = db.Column(
|
publish_incomplete = db.Column(
|
||||||
db.Boolean, nullable=False, default=False, server_default="false"
|
db.Boolean, nullable=False, default=False, server_default="false"
|
||||||
)
|
)
|
||||||
"prise en compte immédiate"
|
# type d'evaluation: 0 normale, 1 rattrapage, 2 "2eme session"
|
||||||
evaluation_type = db.Column(
|
evaluation_type = db.Column(
|
||||||
db.Integer, nullable=False, default=0, server_default="0"
|
db.Integer, nullable=False, default=0, server_default="0"
|
||||||
)
|
)
|
||||||
"type d'evaluation: 0 normale, 1 rattrapage, 2 2eme session, 3 bonus"
|
|
||||||
blocked_until = db.Column(db.DateTime(timezone=True), nullable=True)
|
|
||||||
"date de prise en compte"
|
|
||||||
BLOCKED_FOREVER = datetime.datetime(2666, 12, 31, tzinfo=scu.TIME_ZONE)
|
|
||||||
# ordre de presentation (par défaut, le plus petit numero
|
# ordre de presentation (par défaut, le plus petit numero
|
||||||
# est la plus ancienne eval):
|
# est la plus ancienne eval):
|
||||||
numero = db.Column(db.Integer, nullable=False, default=0)
|
numero = db.Column(db.Integer, nullable=False, default=0)
|
||||||
ues = db.relationship("UniteEns", secondary="evaluation_ue_poids", viewonly=True)
|
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
|
|
||||||
EVALUATION_BONUS = 3
|
|
||||||
VALID_EVALUATION_TYPES = {
|
|
||||||
EVALUATION_NORMALE,
|
|
||||||
EVALUATION_RATTRAPAGE,
|
|
||||||
EVALUATION_SESSION2,
|
|
||||||
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):
|
def __repr__(self):
|
||||||
return f"""<Evaluation {self.id} {
|
return f"""<Evaluation {self.id} {
|
||||||
self.date_debut.isoformat() if self.date_debut else ''} "{
|
self.date_debut.isoformat() if self.date_debut else ''} "{
|
||||||
@ -90,22 +67,20 @@ class Evaluation(models.ScoDocModel):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def create(
|
def create(
|
||||||
cls,
|
cls,
|
||||||
moduleimpl: "ModuleImpl" = None,
|
moduleimpl: ModuleImpl = None,
|
||||||
date_debut: datetime.datetime = None,
|
date_debut: datetime.datetime = None,
|
||||||
date_fin: datetime.datetime = None,
|
date_fin: datetime.datetime = None,
|
||||||
description=None,
|
description=None,
|
||||||
note_max=None,
|
note_max=None,
|
||||||
blocked_until=None,
|
|
||||||
coefficient=None,
|
coefficient=None,
|
||||||
visibulletin=None,
|
visibulletin=None,
|
||||||
publish_incomplete=None,
|
publish_incomplete=None,
|
||||||
evaluation_type=None,
|
evaluation_type=None,
|
||||||
numero=None,
|
numero=None,
|
||||||
**kw, # ceci pour absorber les éventuel arguments excedentaires
|
**kw, # ceci pour absorber les éventuel arguments excedentaires
|
||||||
) -> "Evaluation":
|
):
|
||||||
"""Create an evaluation. Check permission and all arguments.
|
"""Create an evaluation. Check permission and all arguments.
|
||||||
Ne crée pas les poids vers les UEs.
|
Ne crée pas les poids vers les UEs.
|
||||||
Add to session, do not commit.
|
|
||||||
"""
|
"""
|
||||||
if not moduleimpl.can_edit_evaluation(current_user):
|
if not moduleimpl.can_edit_evaluation(current_user):
|
||||||
raise AccessDenied(
|
raise AccessDenied(
|
||||||
@ -114,15 +89,13 @@ class Evaluation(models.ScoDocModel):
|
|||||||
args = locals()
|
args = locals()
|
||||||
del args["cls"]
|
del args["cls"]
|
||||||
del args["kw"]
|
del args["kw"]
|
||||||
check_and_convert_evaluation_args(args, moduleimpl)
|
check_convert_evaluation_args(moduleimpl, args)
|
||||||
# Check numeros
|
# Check numeros
|
||||||
Evaluation.moduleimpl_evaluation_renumber(moduleimpl, only_if_unumbered=True)
|
Evaluation.moduleimpl_evaluation_renumber(moduleimpl, only_if_unumbered=True)
|
||||||
if not "numero" in args or args["numero"] is None:
|
if not "numero" in args or args["numero"] is None:
|
||||||
args["numero"] = cls.get_new_numero(moduleimpl, args["date_debut"])
|
args["numero"] = cls.get_new_numero(moduleimpl, args["date_debut"])
|
||||||
#
|
#
|
||||||
evaluation = Evaluation(**args)
|
evaluation = Evaluation(**args)
|
||||||
db.session.add(evaluation)
|
|
||||||
db.session.flush()
|
|
||||||
sco_cache.invalidate_formsemestre(formsemestre_id=moduleimpl.formsemestre_id)
|
sco_cache.invalidate_formsemestre(formsemestre_id=moduleimpl.formsemestre_id)
|
||||||
url = url_for(
|
url = url_for(
|
||||||
"notes.moduleimpl_status",
|
"notes.moduleimpl_status",
|
||||||
@ -141,7 +114,7 @@ class Evaluation(models.ScoDocModel):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_new_numero(
|
def get_new_numero(
|
||||||
cls, moduleimpl: "ModuleImpl", date_debut: datetime.datetime
|
cls, moduleimpl: ModuleImpl, date_debut: datetime.datetime
|
||||||
) -> int:
|
) -> int:
|
||||||
"""Get a new numero for an evaluation in this moduleimpl
|
"""Get a new numero for an evaluation in this moduleimpl
|
||||||
If necessary, renumber existing evals to make room for a new one.
|
If necessary, renumber existing evals to make room for a new one.
|
||||||
@ -172,7 +145,7 @@ class Evaluation(models.ScoDocModel):
|
|||||||
"delete evaluation (commit) (check permission)"
|
"delete evaluation (commit) (check permission)"
|
||||||
from app.scodoc import sco_evaluation_db
|
from app.scodoc import sco_evaluation_db
|
||||||
|
|
||||||
modimpl: "ModuleImpl" = self.moduleimpl
|
modimpl: ModuleImpl = self.moduleimpl
|
||||||
if not modimpl.can_edit_evaluation(current_user):
|
if not modimpl.can_edit_evaluation(current_user):
|
||||||
raise AccessDenied(
|
raise AccessDenied(
|
||||||
f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
|
f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
|
||||||
@ -213,24 +186,18 @@ class Evaluation(models.ScoDocModel):
|
|||||||
# ScoDoc7 output_formators
|
# ScoDoc7 output_formators
|
||||||
e_dict["evaluation_id"] = self.id
|
e_dict["evaluation_id"] = self.id
|
||||||
e_dict["date_debut"] = self.date_debut.isoformat() if self.date_debut else None
|
e_dict["date_debut"] = self.date_debut.isoformat() if self.date_debut else None
|
||||||
e_dict["date_fin"] = self.date_fin.isoformat() if self.date_fin else None
|
e_dict["date_fin"] = self.date_debut.isoformat() if self.date_fin else None
|
||||||
e_dict["numero"] = self.numero or 0
|
e_dict["numero"] = self.numero or 0
|
||||||
e_dict["poids"] = self.get_ue_poids_dict() # { ue_id : poids }
|
e_dict["poids"] = self.get_ue_poids_dict() # { ue_id : poids }
|
||||||
|
|
||||||
# Deprecated
|
# Deprecated
|
||||||
e_dict["jour"] = (
|
e_dict["jour"] = self.date_debut.strftime("%d/%m/%Y") if self.date_debut else ""
|
||||||
self.date_debut.strftime(scu.DATE_FMT) if self.date_debut else ""
|
|
||||||
)
|
|
||||||
|
|
||||||
return evaluation_enrich_dict(self, e_dict)
|
return evaluation_enrich_dict(self, e_dict)
|
||||||
|
|
||||||
def to_dict_api(self) -> dict:
|
def to_dict_api(self) -> dict:
|
||||||
"Représentation dict pour API JSON"
|
"Représentation dict pour API JSON"
|
||||||
return {
|
return {
|
||||||
"blocked": self.is_blocked(),
|
|
||||||
"blocked_until": (
|
|
||||||
self.blocked_until.isoformat() if self.blocked_until else ""
|
|
||||||
),
|
|
||||||
"coefficient": self.coefficient,
|
"coefficient": self.coefficient,
|
||||||
"date_debut": self.date_debut.isoformat() if self.date_debut else "",
|
"date_debut": self.date_debut.isoformat() if self.date_debut else "",
|
||||||
"date_fin": self.date_fin.isoformat() if self.date_fin else "",
|
"date_fin": self.date_fin.isoformat() if self.date_fin else "",
|
||||||
@ -245,9 +212,9 @@ class Evaluation(models.ScoDocModel):
|
|||||||
"visibulletin": self.visibulletin,
|
"visibulletin": self.visibulletin,
|
||||||
# Deprecated (supprimer avant #sco9.7)
|
# Deprecated (supprimer avant #sco9.7)
|
||||||
"date": self.date_debut.date().isoformat() if self.date_debut else "",
|
"date": self.date_debut.date().isoformat() if self.date_debut else "",
|
||||||
"heure_debut": (
|
"heure_debut": self.date_debut.time().isoformat()
|
||||||
self.date_debut.time().isoformat() if self.date_debut else ""
|
if self.date_debut
|
||||||
),
|
else "",
|
||||||
"heure_fin": self.date_fin.time().isoformat() if self.date_fin else "",
|
"heure_fin": self.date_fin.time().isoformat() if self.date_fin else "",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -267,28 +234,14 @@ class Evaluation(models.ScoDocModel):
|
|||||||
|
|
||||||
return e_dict
|
return e_dict
|
||||||
|
|
||||||
@classmethod
|
def from_dict(self, data):
|
||||||
def get_evaluation(
|
"""Set evaluation attributes from given dict values."""
|
||||||
cls, evaluation_id: int | str, dept_id: int = None, accept_none=False
|
check_convert_evaluation_args(self.moduleimpl, data)
|
||||||
) -> "Evaluation":
|
if data.get("numero") is None:
|
||||||
"""Evaluation ou 404, cherche uniquement dans le département spécifié ou le courant.
|
data["numero"] = Evaluation.get_max_numero(self.moduleimpl.id) + 1
|
||||||
Si accept_none, return None si l'id est invalide ou n'existe pas.
|
for k in self.__dict__.keys():
|
||||||
"""
|
if k != "_sa_instance_state" and k != "id" and k in data:
|
||||||
from app.models import FormSemestre
|
setattr(self, k, data[k])
|
||||||
|
|
||||||
if not isinstance(evaluation_id, int):
|
|
||||||
try:
|
|
||||||
evaluation_id = int(evaluation_id)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
abort(404, "evaluation_id invalide")
|
|
||||||
if g.scodoc_dept:
|
|
||||||
dept_id = dept_id if dept_id is not None else g.scodoc_dept_id
|
|
||||||
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
|
@classmethod
|
||||||
def get_max_numero(cls, moduleimpl_id: int) -> int:
|
def get_max_numero(cls, moduleimpl_id: int) -> int:
|
||||||
@ -304,7 +257,7 @@ class Evaluation(models.ScoDocModel):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def moduleimpl_evaluation_renumber(
|
def moduleimpl_evaluation_renumber(
|
||||||
cls, moduleimpl: "ModuleImpl", only_if_unumbered=False
|
cls, moduleimpl: ModuleImpl, only_if_unumbered=False
|
||||||
):
|
):
|
||||||
"""Renumber evaluations in this moduleimpl, according to their date. (numero=0: oldest one)
|
"""Renumber evaluations in this moduleimpl, according to their date. (numero=0: oldest one)
|
||||||
Needed because previous versions of ScoDoc did not have eval numeros
|
Needed because previous versions of ScoDoc did not have eval numeros
|
||||||
@ -314,9 +267,7 @@ class Evaluation(models.ScoDocModel):
|
|||||||
evaluations = moduleimpl.evaluations.order_by(
|
evaluations = moduleimpl.evaluations.order_by(
|
||||||
Evaluation.date_debut, Evaluation.numero
|
Evaluation.date_debut, Evaluation.numero
|
||||||
).all()
|
).all()
|
||||||
numeros_distincts = {e.numero for e in evaluations if e.numero is not None}
|
all_numbered = all(e.numero is not None for e in evaluations)
|
||||||
# pas de None, pas de dupliqués
|
|
||||||
all_numbered = len(numeros_distincts) == len(evaluations)
|
|
||||||
if all_numbered and only_if_unumbered:
|
if all_numbered and only_if_unumbered:
|
||||||
return # all ok
|
return # all ok
|
||||||
|
|
||||||
@ -327,15 +278,14 @@ class Evaluation(models.ScoDocModel):
|
|||||||
db.session.add(e)
|
db.session.add(e)
|
||||||
i += 1
|
i += 1
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
sco_cache.invalidate_formsemestre(moduleimpl.formsemestre_id)
|
|
||||||
|
|
||||||
def descr_heure(self) -> str:
|
def descr_heure(self) -> str:
|
||||||
"Description de la plage horaire pour affichages ('de 13h00 à 14h00')"
|
"Description de la plage horaire pour affichages ('de 13h00 à 14h00')"
|
||||||
if self.date_debut and (not self.date_fin or self.date_fin == self.date_debut):
|
if self.date_debut and (not self.date_fin or self.date_fin == self.date_debut):
|
||||||
return f"""à {self.date_debut.strftime(scu.TIME_FMT)}"""
|
return f"""à {self.date_debut.strftime("%Hh%M")}"""
|
||||||
elif self.date_debut and self.date_fin:
|
elif self.date_debut and self.date_fin:
|
||||||
return f"""de {self.date_debut.strftime(scu.TIME_FMT)
|
return f"""de {self.date_debut.strftime("%Hh%M")
|
||||||
} à {self.date_fin.strftime(scu.TIME_FMT)}"""
|
} à {self.date_fin.strftime("%Hh%M")}"""
|
||||||
else:
|
else:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@ -362,15 +312,13 @@ class Evaluation(models.ScoDocModel):
|
|||||||
|
|
||||||
def _h(dt: datetime.datetime) -> str:
|
def _h(dt: datetime.datetime) -> str:
|
||||||
if dt.minute:
|
if dt.minute:
|
||||||
return dt.strftime(scu.TIME_FMT)
|
return dt.strftime("%Hh%M")
|
||||||
return f"{dt.hour}h"
|
return f"{dt.hour}h"
|
||||||
|
|
||||||
if self.date_fin is None:
|
if self.date_fin is None:
|
||||||
return f"le {self.date_debut.strftime('%d/%m/%Y')} à {_h(self.date_debut)}"
|
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.date() == self.date_fin.date(): # même jour
|
||||||
if self.date_debut.time() == self.date_fin.time():
|
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 (
|
return (
|
||||||
f"le {self.date_debut.strftime('%d/%m/%Y')} à {_h(self.date_debut)}"
|
f"le {self.date_debut.strftime('%d/%m/%Y')} à {_h(self.date_debut)}"
|
||||||
)
|
)
|
||||||
@ -390,6 +338,19 @@ class Evaluation(models.ScoDocModel):
|
|||||||
Chaine vide si non renseignée."""
|
Chaine vide si non renseignée."""
|
||||||
return self.date_fin.time().isoformat("minutes") if self.date_fin else ""
|
return self.date_fin.time().isoformat("minutes") if self.date_fin else ""
|
||||||
|
|
||||||
|
def clone(self, not_copying=()):
|
||||||
|
"""Clone, not copying the given attrs
|
||||||
|
Attention: la copie n'a pas d'id avant le prochain commit
|
||||||
|
"""
|
||||||
|
d = dict(self.__dict__)
|
||||||
|
d.pop("id") # get rid of id
|
||||||
|
d.pop("_sa_instance_state") # get rid of SQLAlchemy special attr
|
||||||
|
for k in not_copying:
|
||||||
|
d.pop(k)
|
||||||
|
copy = self.__class__(**d)
|
||||||
|
db.session.add(copy)
|
||||||
|
return copy
|
||||||
|
|
||||||
def is_matin(self) -> bool:
|
def is_matin(self) -> bool:
|
||||||
"Evaluation commençant le matin (faux si pas de date)"
|
"Evaluation commençant le matin (faux si pas de date)"
|
||||||
if not self.date_debut:
|
if not self.date_debut:
|
||||||
@ -402,14 +363,6 @@ class Evaluation(models.ScoDocModel):
|
|||||||
return False
|
return False
|
||||||
return self.date_debut.time() >= NOON
|
return self.date_debut.time() >= NOON
|
||||||
|
|
||||||
def is_blocked(self, now=None) -> bool:
|
|
||||||
"True si prise en compte bloquée"
|
|
||||||
if self.blocked_until is None:
|
|
||||||
return False
|
|
||||||
if now is None:
|
|
||||||
now = datetime.datetime.now(scu.TIME_ZONE)
|
|
||||||
return self.blocked_until > now
|
|
||||||
|
|
||||||
def set_default_poids(self) -> bool:
|
def set_default_poids(self) -> bool:
|
||||||
"""Initialize les poids vers les UE à leurs valeurs par défaut
|
"""Initialize les poids vers les UE à leurs valeurs par défaut
|
||||||
C'est à dire à 1 si le coef. module/UE est non nul, 0 sinon.
|
C'est à dire à 1 si le coef. module/UE est non nul, 0 sinon.
|
||||||
@ -434,28 +387,22 @@ class Evaluation(models.ScoDocModel):
|
|||||||
return modified
|
return modified
|
||||||
|
|
||||||
def set_ue_poids(self, ue, poids: float) -> None:
|
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})
|
self.update_ue_poids_dict({ue.id: poids})
|
||||||
|
|
||||||
def set_ue_poids_dict(self, ue_poids_dict: dict) -> None:
|
def set_ue_poids_dict(self, ue_poids_dict: dict) -> None:
|
||||||
"""set poids vers les UE (remplace existants)
|
"""set poids vers les UE (remplace existants)
|
||||||
ue_poids_dict = { ue_id : poids }
|
ue_poids_dict = { ue_id : poids }
|
||||||
Commit session.
|
|
||||||
"""
|
"""
|
||||||
from app.models.ues import UniteEns
|
|
||||||
|
|
||||||
L = []
|
L = []
|
||||||
for ue_id, poids in ue_poids_dict.items():
|
for ue_id, poids in ue_poids_dict.items():
|
||||||
ue = db.session.get(UniteEns, ue_id)
|
ue = db.session.get(UniteEns, ue_id)
|
||||||
if ue is None:
|
if ue is None:
|
||||||
raise ScoValueError("poids vers une UE inexistante")
|
raise ScoValueError("poids vers une UE inexistante")
|
||||||
ue_poids = EvaluationUEPoids(evaluation=self, ue=ue, poids=poids)
|
ue_poids = EvaluationUEPoids(evaluation=self, ue=ue, poids=poids)
|
||||||
db.session.add(ue_poids)
|
|
||||||
L.append(ue_poids)
|
L.append(ue_poids)
|
||||||
|
db.session.add(ue_poids)
|
||||||
self.ue_poids = L # backref # pylint:disable=attribute-defined-outside-init
|
self.ue_poids = L # backref # pylint:disable=attribute-defined-outside-init
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
self.moduleimpl.invalidate_evaluations_poids() # inval cache
|
self.moduleimpl.invalidate_evaluations_poids() # inval cache
|
||||||
|
|
||||||
def update_ue_poids_dict(self, ue_poids_dict: dict) -> None:
|
def update_ue_poids_dict(self, ue_poids_dict: dict) -> None:
|
||||||
@ -480,8 +427,8 @@ class Evaluation(models.ScoDocModel):
|
|||||||
|
|
||||||
def get_ue_poids_str(self) -> str:
|
def get_ue_poids_str(self) -> str:
|
||||||
"""string describing poids, for excel cells and pdfs
|
"""string describing poids, for excel cells and pdfs
|
||||||
Note: les poids nuls ou non initialisés (poids par défaut),
|
Note: si les poids ne sont pas initialisés (poids par défaut),
|
||||||
ne sont pas affichés.
|
ils ne sont pas affichés.
|
||||||
"""
|
"""
|
||||||
# restreint aux UE du semestre dans lequel est cette évaluation
|
# restreint aux UE du semestre dans lequel est cette évaluation
|
||||||
# au cas où le module ait changé de semestre et qu'il reste des poids
|
# au cas où le module ait changé de semestre et qu'il reste des poids
|
||||||
@ -492,7 +439,7 @@ class Evaluation(models.ScoDocModel):
|
|||||||
for p in sorted(
|
for p in sorted(
|
||||||
self.ue_poids, key=lambda p: (p.ue.numero or 0, p.ue.acronyme)
|
self.ue_poids, key=lambda p: (p.ue.numero or 0, p.ue.acronyme)
|
||||||
)
|
)
|
||||||
if evaluation_semestre_idx == p.ue.semestre_idx and (p.poids or 0) > 0
|
if evaluation_semestre_idx == p.ue.semestre_idx
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -502,29 +449,6 @@ class Evaluation(models.ScoDocModel):
|
|||||||
"""
|
"""
|
||||||
return NotesNotes.query.filter_by(etudid=etud.id, evaluation_id=self.id).first()
|
return NotesNotes.query.filter_by(etudid=etud.id, evaluation_id=self.id).first()
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_evaluations_blocked_for_etud(
|
|
||||||
cls, formsemestre, etud: Identite
|
|
||||||
) -> list["Evaluation"]:
|
|
||||||
"""Liste des évaluations de ce semestre avec note pour cet étudiant et date blocage
|
|
||||||
et date blocage < FOREVER.
|
|
||||||
Si non vide, une note apparaitra dans le futur pour cet étudiant: il faut
|
|
||||||
donc interdire la saisie du jury.
|
|
||||||
"""
|
|
||||||
now = datetime.datetime.now(scu.TIME_ZONE)
|
|
||||||
return (
|
|
||||||
Evaluation.query.filter(
|
|
||||||
Evaluation.blocked_until != None, # pylint: disable=C0121
|
|
||||||
Evaluation.blocked_until >= now,
|
|
||||||
)
|
|
||||||
.join(ModuleImpl)
|
|
||||||
.filter_by(formsemestre_id=formsemestre.id)
|
|
||||||
.join(ModuleImplInscription)
|
|
||||||
.filter_by(etudid=etud.id)
|
|
||||||
.join(NotesNotes)
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EvaluationUEPoids(db.Model):
|
class EvaluationUEPoids(db.Model):
|
||||||
"""Poids des évaluations (BUT)
|
"""Poids des évaluations (BUT)
|
||||||
@ -550,7 +474,7 @@ class EvaluationUEPoids(db.Model):
|
|||||||
backref=db.backref("ue_poids", cascade="all, delete-orphan"),
|
backref=db.backref("ue_poids", cascade="all, delete-orphan"),
|
||||||
)
|
)
|
||||||
ue = db.relationship(
|
ue = db.relationship(
|
||||||
"UniteEns",
|
UniteEns,
|
||||||
backref=db.backref("evaluation_ue_poids", cascade="all, delete-orphan"),
|
backref=db.backref("evaluation_ue_poids", cascade="all, delete-orphan"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -562,8 +486,8 @@ class EvaluationUEPoids(db.Model):
|
|||||||
def evaluation_enrich_dict(e: Evaluation, e_dict: dict):
|
def evaluation_enrich_dict(e: Evaluation, e_dict: dict):
|
||||||
"""add or convert some fields in an evaluation dict"""
|
"""add or convert some fields in an evaluation dict"""
|
||||||
# For ScoDoc7 compat
|
# For ScoDoc7 compat
|
||||||
e_dict["heure_debut"] = e.date_debut.strftime(scu.TIME_FMT) if e.date_debut else ""
|
e_dict["heure_debut"] = e.date_debut.strftime("%Hh%M") if e.date_debut else ""
|
||||||
e_dict["heure_fin"] = e.date_fin.strftime(scu.TIME_FMT) if e.date_fin else ""
|
e_dict["heure_fin"] = e.date_fin.strftime("%Hh%M") if e.date_fin else ""
|
||||||
e_dict["jour_iso"] = e.date_debut.isoformat() if e.date_debut else ""
|
e_dict["jour_iso"] = e.date_debut.isoformat() if e.date_debut else ""
|
||||||
# Calcule durée en minutes
|
# Calcule durée en minutes
|
||||||
e_dict["descrheure"] = e.descr_heure()
|
e_dict["descrheure"] = e.descr_heure()
|
||||||
@ -582,7 +506,7 @@ def evaluation_enrich_dict(e: Evaluation, e_dict: dict):
|
|||||||
return e_dict
|
return e_dict
|
||||||
|
|
||||||
|
|
||||||
def check_and_convert_evaluation_args(data: dict, moduleimpl: "ModuleImpl"):
|
def check_convert_evaluation_args(moduleimpl: ModuleImpl, data: dict):
|
||||||
"""Check coefficient, dates and duration, raises exception if invalid.
|
"""Check coefficient, dates and duration, raises exception if invalid.
|
||||||
Convert date and time strings to date and time objects.
|
Convert date and time strings to date and time objects.
|
||||||
|
|
||||||
@ -597,7 +521,7 @@ def check_and_convert_evaluation_args(data: dict, moduleimpl: "ModuleImpl"):
|
|||||||
# --- evaluation_type
|
# --- evaluation_type
|
||||||
try:
|
try:
|
||||||
data["evaluation_type"] = int(data.get("evaluation_type", 0) or 0)
|
data["evaluation_type"] = int(data.get("evaluation_type", 0) or 0)
|
||||||
if not data["evaluation_type"] in Evaluation.VALID_EVALUATION_TYPES:
|
if not data["evaluation_type"] in VALID_EVALUATION_TYPES:
|
||||||
raise ScoValueError("invalid evaluation_type value")
|
raise ScoValueError("invalid evaluation_type value")
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise ScoValueError("invalid evaluation_type value") from exc
|
raise ScoValueError("invalid evaluation_type value") from exc
|
||||||
@ -622,7 +546,7 @@ def check_and_convert_evaluation_args(data: dict, moduleimpl: "ModuleImpl"):
|
|||||||
if coef < 0:
|
if coef < 0:
|
||||||
raise ScoValueError("invalid coefficient value (must be positive or null)")
|
raise ScoValueError("invalid coefficient value (must be positive or null)")
|
||||||
data["coefficient"] = coef
|
data["coefficient"] = coef
|
||||||
# --- date de l'évaluation dans le semestre ?
|
# --- date de l'évaluation
|
||||||
formsemestre = moduleimpl.formsemestre
|
formsemestre = moduleimpl.formsemestre
|
||||||
date_debut = data.get("date_debut", None)
|
date_debut = data.get("date_debut", None)
|
||||||
if date_debut:
|
if date_debut:
|
||||||
@ -637,7 +561,7 @@ def check_and_convert_evaluation_args(data: dict, moduleimpl: "ModuleImpl"):
|
|||||||
):
|
):
|
||||||
raise ScoValueError(
|
raise ScoValueError(
|
||||||
f"""La date de début de l'évaluation ({
|
f"""La date de début de l'évaluation ({
|
||||||
data["date_debut"].strftime(scu.DATE_FMT)
|
data["date_debut"].strftime("%d/%m/%Y")
|
||||||
}) n'est pas dans le semestre !""",
|
}) n'est pas dans le semestre !""",
|
||||||
dest_url="javascript:history.back();",
|
dest_url="javascript:history.back();",
|
||||||
)
|
)
|
||||||
@ -652,19 +576,27 @@ def check_and_convert_evaluation_args(data: dict, moduleimpl: "ModuleImpl"):
|
|||||||
):
|
):
|
||||||
raise ScoValueError(
|
raise ScoValueError(
|
||||||
f"""La date de fin de l'évaluation ({
|
f"""La date de fin de l'évaluation ({
|
||||||
data["date_fin"].strftime(scu.DATE_FMT)
|
data["date_fin"].strftime("%d/%m/%Y")
|
||||||
}) n'est pas dans le semestre !""",
|
}) n'est pas dans le semestre !""",
|
||||||
dest_url="javascript:history.back();",
|
dest_url="javascript:history.back();",
|
||||||
)
|
)
|
||||||
if date_debut and date_fin:
|
if date_debut and date_fin:
|
||||||
duration = data["date_fin"] - data["date_debut"]
|
duration = data["date_fin"] - data["date_debut"]
|
||||||
if duration.total_seconds() < 0 or duration > MAX_EVALUATION_DURATION:
|
if duration.total_seconds() < 0 or duration > MAX_EVALUATION_DURATION:
|
||||||
raise ScoValueError(
|
raise ScoValueError("Heures de l'évaluation incohérentes !")
|
||||||
"Heures de l'évaluation incohérentes !",
|
# # --- heures
|
||||||
dest_url="javascript:history.back();",
|
# heure_debut = data.get("heure_debut", None)
|
||||||
)
|
# if heure_debut and not isinstance(heure_debut, datetime.time):
|
||||||
if "blocked_until" in data:
|
# if date_format == "dmy":
|
||||||
data["blocked_until"] = data["blocked_until"] or None
|
# data["heure_debut"] = heure_to_time(heure_debut)
|
||||||
|
# else: # ISO
|
||||||
|
# data["heure_debut"] = datetime.time.fromisoformat(heure_debut)
|
||||||
|
# heure_fin = data.get("heure_fin", None)
|
||||||
|
# if heure_fin and not isinstance(heure_fin, datetime.time):
|
||||||
|
# if date_format == "dmy":
|
||||||
|
# data["heure_fin"] = heure_to_time(heure_fin)
|
||||||
|
# else: # ISO
|
||||||
|
# data["heure_fin"] = datetime.time.fromisoformat(heure_fin)
|
||||||
|
|
||||||
|
|
||||||
def heure_to_time(heure: str) -> datetime.time:
|
def heure_to_time(heure: str) -> datetime.time:
|
||||||
@ -674,6 +606,19 @@ def heure_to_time(heure: str) -> datetime.time:
|
|||||||
return datetime.time(int(h), int(m))
|
return datetime.time(int(h), int(m))
|
||||||
|
|
||||||
|
|
||||||
|
def _time_duration_HhM(heure_debut: str, heure_fin: str) -> int:
|
||||||
|
"""duree (nb entier de minutes) entre deux heures a notre format
|
||||||
|
ie 12h23
|
||||||
|
"""
|
||||||
|
if heure_debut and heure_fin:
|
||||||
|
h0, m0 = [int(x) for x in heure_debut.split("h")]
|
||||||
|
h1, m1 = [int(x) for x in heure_fin.split("h")]
|
||||||
|
d = (h1 - h0) * 60 + (m1 - m0)
|
||||||
|
return d
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _moduleimpl_evaluation_insert_before(
|
def _moduleimpl_evaluation_insert_before(
|
||||||
evaluations: list[Evaluation], next_eval: Evaluation
|
evaluations: list[Evaluation], next_eval: Evaluation
|
||||||
) -> int:
|
) -> int:
|
||||||
@ -694,6 +639,3 @@ def _moduleimpl_evaluation_insert_before(
|
|||||||
db.session.add(e)
|
db.session.add(e)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return n
|
return n
|
||||||
|
|
||||||
|
|
||||||
from app.models.moduleimpls import ModuleImpl, ModuleImplInscription
|
|
||||||
|
@ -12,12 +12,13 @@ from app import db
|
|||||||
from app import email
|
from app import email
|
||||||
from app import log
|
from app import log
|
||||||
from app.auth.models import User
|
from app.auth.models import User
|
||||||
from app.models import ScoDocModel, SHORT_STR_LEN
|
from app.models import SHORT_STR_LEN
|
||||||
|
from app.models.moduleimpls import ModuleImpl
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
|
|
||||||
|
|
||||||
class Scolog(ScoDocModel):
|
class Scolog(db.Model):
|
||||||
"""Log des actions (journal modif etudiants)"""
|
"""Log des actions (journal modif etudiants)"""
|
||||||
|
|
||||||
__tablename__ = "scolog"
|
__tablename__ = "scolog"
|
||||||
@ -27,15 +28,14 @@ class Scolog(ScoDocModel):
|
|||||||
method = db.Column(db.Text)
|
method = db.Column(db.Text)
|
||||||
msg = db.Column(db.Text)
|
msg = db.Column(db.Text)
|
||||||
etudid = db.Column(db.Integer) # sans contrainte pour garder logs après suppression
|
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
|
# zope_remote_addr suppressed
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def logdb(
|
def logdb(
|
||||||
cls, method: str = None, etudid: int = None, msg: str = None, commit=False
|
cls, method: str = None, etudid: int = None, msg: str = None, commit=False
|
||||||
):
|
):
|
||||||
"""Add entry in student's log (replacement for old scolog.logdb).
|
"""Add entry in student's log (replacement for old scolog.logdb)"""
|
||||||
Par défaut ne commite pas."""
|
|
||||||
entry = Scolog(
|
entry = Scolog(
|
||||||
method=method,
|
method=method,
|
||||||
msg=msg,
|
msg=msg,
|
||||||
@ -46,21 +46,6 @@ class Scolog(ScoDocModel):
|
|||||||
if commit:
|
if commit:
|
||||||
db.session.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):
|
class ScolarNews(db.Model):
|
||||||
"""Nouvelles pour page d'accueil"""
|
"""Nouvelles pour page d'accueil"""
|
||||||
@ -92,9 +77,7 @@ class ScolarNews(db.Model):
|
|||||||
date = db.Column(
|
date = db.Column(
|
||||||
db.DateTime(timezone=True), server_default=db.func.now(), index=True
|
db.DateTime(timezone=True), server_default=db.func.now(), index=True
|
||||||
)
|
)
|
||||||
authenticated_user = db.Column(
|
authenticated_user = db.Column(db.Text, index=True) # login, sans contrainte
|
||||||
db.Text, index=True
|
|
||||||
) # user_name login, sans contrainte
|
|
||||||
# type in 'INSCR', 'NOTES', 'FORM', 'SEM', 'MISC'
|
# type in 'INSCR', 'NOTES', 'FORM', 'SEM', 'MISC'
|
||||||
type = db.Column(db.String(SHORT_STR_LEN), index=True)
|
type = db.Column(db.String(SHORT_STR_LEN), index=True)
|
||||||
object = db.Column(
|
object = db.Column(
|
||||||
@ -150,7 +133,7 @@ class ScolarNews(db.Model):
|
|||||||
return query.order_by(cls.date.desc()).limit(n).all()
|
return query.order_by(cls.date.desc()).limit(n).all()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add(cls, typ, obj=None, text="", url=None, max_frequency=600, dept_id=None):
|
def add(cls, typ, obj=None, text="", url=None, max_frequency=600):
|
||||||
"""Enregistre une nouvelle
|
"""Enregistre une nouvelle
|
||||||
Si max_frequency, ne génère pas 2 nouvelles "identiques"
|
Si max_frequency, ne génère pas 2 nouvelles "identiques"
|
||||||
à moins de max_frequency secondes d'intervalle (10 minutes par défaut).
|
à moins de max_frequency secondes d'intervalle (10 minutes par défaut).
|
||||||
@ -158,11 +141,10 @@ class ScolarNews(db.Model):
|
|||||||
même (obj, typ, user).
|
même (obj, typ, user).
|
||||||
La nouvelle enregistrée est aussi envoyée par mail.
|
La nouvelle enregistrée est aussi envoyée par mail.
|
||||||
"""
|
"""
|
||||||
dept_id = dept_id if dept_id is not None else g.scodoc_dept_id
|
|
||||||
if max_frequency:
|
if max_frequency:
|
||||||
last_news = (
|
last_news = (
|
||||||
cls.query.filter_by(
|
cls.query.filter_by(
|
||||||
dept_id=dept_id,
|
dept_id=g.scodoc_dept_id,
|
||||||
authenticated_user=current_user.user_name,
|
authenticated_user=current_user.user_name,
|
||||||
type=typ,
|
type=typ,
|
||||||
object=obj,
|
object=obj,
|
||||||
@ -181,7 +163,7 @@ class ScolarNews(db.Model):
|
|||||||
return
|
return
|
||||||
|
|
||||||
news = ScolarNews(
|
news = ScolarNews(
|
||||||
dept_id=dept_id,
|
dept_id=g.scodoc_dept_id,
|
||||||
authenticated_user=current_user.user_name,
|
authenticated_user=current_user.user_name,
|
||||||
type=typ,
|
type=typ,
|
||||||
object=obj,
|
object=obj,
|
||||||
@ -198,7 +180,6 @@ class ScolarNews(db.Model):
|
|||||||
None si inexistant
|
None si inexistant
|
||||||
"""
|
"""
|
||||||
from app.models.formsemestre import FormSemestre
|
from app.models.formsemestre import FormSemestre
|
||||||
from app.models.moduleimpls import ModuleImpl
|
|
||||||
|
|
||||||
formsemestre_id = None
|
formsemestre_id = None
|
||||||
if self.type == self.NEWS_INSCR:
|
if self.type == self.NEWS_INSCR:
|
||||||
@ -250,9 +231,7 @@ class ScolarNews(db.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Transforme les URL en URL absolues
|
# Transforme les URL en URL absolues
|
||||||
base = url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)[
|
base = scu.ScoURL()
|
||||||
: -len("/index_html")
|
|
||||||
]
|
|
||||||
txt = re.sub('href=/.*?"', 'href="' + base + "/", txt)
|
txt = re.sub('href=/.*?"', 'href="' + base + "/", txt)
|
||||||
|
|
||||||
# Transforme les liens HTML en texte brut: '<a href="url">texte</a>' devient 'texte: url'
|
# Transforme les liens HTML en texte brut: '<a href="url">texte</a>' devient 'texte: url'
|
||||||
@ -269,12 +248,11 @@ class ScolarNews(db.Model):
|
|||||||
news_list = cls.last_news(n=n)
|
news_list = cls.last_news(n=n)
|
||||||
if not news_list:
|
if not news_list:
|
||||||
return ""
|
return ""
|
||||||
dept_news_url = url_for("scolar.dept_news", scodoc_dept=g.scodoc_dept)
|
|
||||||
H = [
|
H = [
|
||||||
f"""<div class="scobox news"><div class="scobox-title"><a href="{
|
f"""<div class="news"><span class="newstitle"><a href="{
|
||||||
dept_news_url
|
url_for("scolar.dept_news", scodoc_dept=g.scodoc_dept)
|
||||||
}">Dernières opérations</a>
|
}">Dernières opérations</a>
|
||||||
</div><ul class="newslist">"""
|
</span><ul class="newslist">"""
|
||||||
]
|
]
|
||||||
|
|
||||||
for news in news_list:
|
for news in news_list:
|
||||||
@ -282,22 +260,16 @@ class ScolarNews(db.Model):
|
|||||||
f"""<li class="newslist"><span class="newsdate">{news.formatted_date()}</span><span
|
f"""<li class="newslist"><span class="newsdate">{news.formatted_date()}</span><span
|
||||||
class="newstext">{news}</span></li>"""
|
class="newstext">{news}</span></li>"""
|
||||||
)
|
)
|
||||||
H.append(
|
|
||||||
f"""<li class="newslist">
|
|
||||||
<span class="newstext"><a href="{dept_news_url}" class="stdlink">...</a>
|
|
||||||
</span>
|
|
||||||
</li>"""
|
|
||||||
)
|
|
||||||
|
|
||||||
H.append("</ul></div>")
|
H.append("</ul>")
|
||||||
|
|
||||||
# Informations générales
|
# Informations générales
|
||||||
H.append(
|
H.append(
|
||||||
f"""<div>
|
f"""<div><a class="discretelink" href="{scu.SCO_ANNONCES_WEBSITE}">
|
||||||
Pour en savoir plus sur ScoDoc voir
|
Pour en savoir plus sur ScoDoc voir le site scodoc.org</a>.
|
||||||
<a class="stdlink" href="{scu.SCO_ANNONCES_WEBSITE}">scodoc.org</a>
|
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
H.append("</div>")
|
||||||
return "\n".join(H)
|
return "\n".join(H)
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
"""ScoDoc 9 models : Formations
|
"""ScoDoc 9 models : Formations
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import abort, g
|
|
||||||
from flask_sqlalchemy.query import Query
|
from flask_sqlalchemy.query import Query
|
||||||
|
|
||||||
import app
|
import app
|
||||||
from app import db
|
from app import db
|
||||||
from app.comp import df_cache
|
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 (
|
from app.models.but_refcomp import (
|
||||||
ApcAnneeParcours,
|
ApcAnneeParcours,
|
||||||
ApcCompetence,
|
ApcCompetence,
|
||||||
@ -23,7 +21,7 @@ from app.scodoc import sco_utils as scu
|
|||||||
from app.scodoc.codes_cursus import UE_STANDARD
|
from app.scodoc.codes_cursus import UE_STANDARD
|
||||||
|
|
||||||
|
|
||||||
class Formation(ScoDocModel):
|
class Formation(db.Model):
|
||||||
"""Programme pédagogique d'une formation"""
|
"""Programme pédagogique d'une formation"""
|
||||||
|
|
||||||
__tablename__ = "notes_formations"
|
__tablename__ = "notes_formations"
|
||||||
@ -66,21 +64,6 @@ class Formation(ScoDocModel):
|
|||||||
"titre complet pour affichage"
|
"titre complet pour affichage"
|
||||||
return f"""Formation {self.titre} ({self.acronyme}) [version {self.version}] code {self.formation_code}"""
|
return f"""Formation {self.titre} ({self.acronyme}) [version {self.version}] code {self.formation_code}"""
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_formation(cls, formation_id: int | str, dept_id: int = None) -> "Formation":
|
|
||||||
"""Formation ou 404, cherche uniquement dans le département spécifié
|
|
||||||
ou le courant (g.scodoc_dept)"""
|
|
||||||
if not isinstance(formation_id, int):
|
|
||||||
try:
|
|
||||||
formation_id = int(formation_id)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
abort(404, "formation_id invalide")
|
|
||||||
if g.scodoc_dept:
|
|
||||||
dept_id = dept_id if dept_id is not None else g.scodoc_dept_id
|
|
||||||
if dept_id is not None:
|
|
||||||
return cls.query.filter_by(id=formation_id, dept_id=dept_id).first_or_404()
|
|
||||||
return cls.query.filter_by(id=formation_id).first_or_404()
|
|
||||||
|
|
||||||
def to_dict(self, with_refcomp_attrs=False, with_departement=True):
|
def to_dict(self, with_refcomp_attrs=False, with_departement=True):
|
||||||
"""As a dict.
|
"""As a dict.
|
||||||
Si with_refcomp_attrs, ajoute attributs permettant de retrouver le ref. de comp.
|
Si with_refcomp_attrs, ajoute attributs permettant de retrouver le ref. de comp.
|
||||||
@ -297,7 +280,7 @@ class Formation(ScoDocModel):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
class Matiere(ScoDocModel):
|
class Matiere(db.Model):
|
||||||
"""Matières: regroupe les modules d'une UE
|
"""Matières: regroupe les modules d'une UE
|
||||||
La matière a peu d'utilité en dehors de la présentation des modules
|
La matière a peu d'utilité en dehors de la présentation des modules
|
||||||
d'une UE.
|
d'une UE.
|
||||||
@ -313,7 +296,6 @@ class Matiere(ScoDocModel):
|
|||||||
numero = db.Column(db.Integer, nullable=False, default=0) # ordre de présentation
|
numero = db.Column(db.Integer, nullable=False, default=0) # ordre de présentation
|
||||||
|
|
||||||
modules = db.relationship("Module", lazy="dynamic", backref="matiere")
|
modules = db.relationship("Module", lazy="dynamic", backref="matiere")
|
||||||
_sco_dept_relations = ("UniteEns", "Formation") # accès au dept_id
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"""<{self.__class__.__name__}(id={self.id}, ue_id={
|
return f"""<{self.__class__.__name__}(id={self.id}, ue_id={
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# -*- coding: UTF-8 -*
|
# -*- coding: UTF-8 -*
|
||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -10,22 +10,19 @@
|
|||||||
|
|
||||||
"""ScoDoc models: formsemestre
|
"""ScoDoc models: formsemestre
|
||||||
"""
|
"""
|
||||||
from collections import defaultdict
|
|
||||||
import datetime
|
import datetime
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from itertools import chain
|
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
|
||||||
from flask import abort, flash, g, url_for
|
from flask import flash, g, url_for
|
||||||
from sqlalchemy.sql import text
|
from sqlalchemy.sql import text
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
|
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app import db, log
|
from app import db, log
|
||||||
from app.auth.models import User
|
from app.auth.models import User
|
||||||
from app import models
|
|
||||||
from app.models import APO_CODE_STR_LEN, CODE_STR_LEN, SHORT_STR_LEN
|
from app.models import APO_CODE_STR_LEN, CODE_STR_LEN, SHORT_STR_LEN
|
||||||
from app.models.but_refcomp import (
|
from app.models.but_refcomp import (
|
||||||
ApcParcours,
|
ApcParcours,
|
||||||
@ -33,17 +30,11 @@ from app.models.but_refcomp import (
|
|||||||
parcours_formsemestre,
|
parcours_formsemestre,
|
||||||
)
|
)
|
||||||
from app.models.config import ScoDocSiteConfig
|
from app.models.config import ScoDocSiteConfig
|
||||||
from app.models.departements import Departement
|
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
from app.models.evaluations import Evaluation
|
from app.models.evaluations import Evaluation
|
||||||
from app.models.events import ScolarNews
|
|
||||||
from app.models.formations import Formation
|
from app.models.formations import Formation
|
||||||
from app.models.groups import GroupDescr, Partition
|
from app.models.groups import GroupDescr, Partition
|
||||||
from app.models.moduleimpls import (
|
from app.models.moduleimpls import ModuleImpl, ModuleImplInscription
|
||||||
ModuleImpl,
|
|
||||||
ModuleImplInscription,
|
|
||||||
notes_modules_enseignants,
|
|
||||||
)
|
|
||||||
from app.models.modules import Module
|
from app.models.modules import Module
|
||||||
from app.models.ues import UniteEns
|
from app.models.ues import UniteEns
|
||||||
from app.models.validations import ScolarFormSemestreValidation
|
from app.models.validations import ScolarFormSemestreValidation
|
||||||
@ -53,10 +44,12 @@ from app.scodoc.sco_permissions import Permission
|
|||||||
from app.scodoc.sco_utils import MONTH_NAMES_ABBREV, translate_assiduites_metric
|
from app.scodoc.sco_utils import MONTH_NAMES_ABBREV, translate_assiduites_metric
|
||||||
from app.scodoc.sco_vdi import ApoEtapeVDI
|
from app.scodoc.sco_vdi import ApoEtapeVDI
|
||||||
|
|
||||||
|
from app.scodoc.sco_utils import translate_assiduites_metric
|
||||||
|
|
||||||
GROUPS_AUTO_ASSIGNMENT_DATA_MAX = 1024 * 1024 # bytes
|
GROUPS_AUTO_ASSIGNMENT_DATA_MAX = 1024 * 1024 # bytes
|
||||||
|
|
||||||
|
|
||||||
class FormSemestre(models.ScoDocModel):
|
class FormSemestre(db.Model):
|
||||||
"""Mise en oeuvre d'un semestre de formation"""
|
"""Mise en oeuvre d'un semestre de formation"""
|
||||||
|
|
||||||
__tablename__ = "notes_formsemestre"
|
__tablename__ = "notes_formsemestre"
|
||||||
@ -70,7 +63,7 @@ class FormSemestre(models.ScoDocModel):
|
|||||||
semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1")
|
semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1")
|
||||||
titre = db.Column(db.Text(), nullable=False)
|
titre = db.Column(db.Text(), nullable=False)
|
||||||
date_debut = db.Column(db.Date(), nullable=False)
|
date_debut = db.Column(db.Date(), nullable=False)
|
||||||
date_fin = db.Column(db.Date(), nullable=False) # jour inclus
|
date_fin = db.Column(db.Date(), nullable=False)
|
||||||
edt_id: str | None = db.Column(db.Text(), index=True, nullable=True)
|
edt_id: str | None = db.Column(db.Text(), index=True, nullable=True)
|
||||||
"identifiant emplois du temps (unicité non imposée)"
|
"identifiant emplois du temps (unicité non imposée)"
|
||||||
etat = db.Column(db.Boolean(), nullable=False, default=True, server_default="true")
|
etat = db.Column(db.Boolean(), nullable=False, default=True, server_default="true")
|
||||||
@ -86,7 +79,7 @@ class FormSemestre(models.ScoDocModel):
|
|||||||
bul_hide_xml = db.Column(
|
bul_hide_xml = db.Column(
|
||||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||||
)
|
)
|
||||||
"ne publie pas le bulletin sur l'API"
|
"ne publie pas le bulletin XML ou JSON"
|
||||||
block_moyennes = db.Column(
|
block_moyennes = db.Column(
|
||||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||||
)
|
)
|
||||||
@ -95,10 +88,6 @@ class FormSemestre(models.ScoDocModel):
|
|||||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||||
)
|
)
|
||||||
"Si vrai, la moyenne générale indicative BUT n'est pas calculée"
|
"Si vrai, la moyenne générale indicative BUT n'est pas calculée"
|
||||||
mode_calcul_moyennes = db.Column(
|
|
||||||
db.Integer, nullable=False, default=0, server_default="0"
|
|
||||||
)
|
|
||||||
"pour usage futur"
|
|
||||||
gestion_semestrielle = db.Column(
|
gestion_semestrielle = db.Column(
|
||||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||||
)
|
)
|
||||||
@ -123,11 +112,9 @@ class FormSemestre(models.ScoDocModel):
|
|||||||
)
|
)
|
||||||
"autorise les enseignants à créer des évals dans leurs modimpls"
|
"autorise les enseignants à créer des évals dans leurs modimpls"
|
||||||
elt_sem_apo = db.Column(db.Text()) # peut être fort long !
|
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())
|
elt_annee_apo = db.Column(db.Text())
|
||||||
"code element annee Apogee, eg 'VRT1A' ou 'V2INLA,V2INCA,...'"
|
"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
|
# Data pour groups_auto_assignment
|
||||||
# (ce champ est utilisé uniquement via l'API par le front js)
|
# (ce champ est utilisé uniquement via l'API par le front js)
|
||||||
@ -193,15 +180,9 @@ class FormSemestre(models.ScoDocModel):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_formsemestre(
|
def get_formsemestre(
|
||||||
cls, formsemestre_id: int | str, dept_id: int = None
|
cls, formsemestre_id: int, dept_id: int = None
|
||||||
) -> "FormSemestre":
|
) -> "FormSemestre":
|
||||||
"""FormSemestre ou 404, cherche uniquement dans le département spécifié
|
""" "FormSemestre ou 404, cherche uniquement dans le département spécifié ou le courant"""
|
||||||
ou le courant (g.scodoc_dept)"""
|
|
||||||
if not isinstance(formsemestre_id, int):
|
|
||||||
try:
|
|
||||||
formsemestre_id = int(formsemestre_id)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
abort(404, "formsemestre_id invalide")
|
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
dept_id = dept_id if dept_id is not None else g.scodoc_dept_id
|
dept_id = dept_id if dept_id is not None else g.scodoc_dept_id
|
||||||
if dept_id is not None:
|
if dept_id is not None:
|
||||||
@ -210,72 +191,8 @@ class FormSemestre(models.ScoDocModel):
|
|||||||
).first_or_404()
|
).first_or_404()
|
||||||
return cls.query.filter_by(id=formsemestre_id).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:
|
def sort_key(self) -> tuple:
|
||||||
"""clé pour tris par ordre de date_debut, le plus ancien en tête
|
"""clé pour tris par ordre alphabétique
|
||||||
(pour avoir le plus récent d'abord, sort avec reverse=True)"""
|
(pour avoir le plus récent d'abord, sort avec reverse=True)"""
|
||||||
return (self.date_debut, self.semestre_id)
|
return (self.date_debut, self.semestre_id)
|
||||||
|
|
||||||
@ -291,12 +208,12 @@ class FormSemestre(models.ScoDocModel):
|
|||||||
d["formsemestre_id"] = self.id
|
d["formsemestre_id"] = self.id
|
||||||
d["titre_num"] = self.titre_num()
|
d["titre_num"] = self.titre_num()
|
||||||
if self.date_debut:
|
if self.date_debut:
|
||||||
d["date_debut"] = self.date_debut.strftime(scu.DATE_FMT)
|
d["date_debut"] = self.date_debut.strftime("%d/%m/%Y")
|
||||||
d["date_debut_iso"] = self.date_debut.isoformat()
|
d["date_debut_iso"] = self.date_debut.isoformat()
|
||||||
else:
|
else:
|
||||||
d["date_debut"] = d["date_debut_iso"] = ""
|
d["date_debut"] = d["date_debut_iso"] = ""
|
||||||
if self.date_fin:
|
if self.date_fin:
|
||||||
d["date_fin"] = self.date_fin.strftime(scu.DATE_FMT)
|
d["date_fin"] = self.date_fin.strftime("%d/%m/%Y")
|
||||||
d["date_fin_iso"] = self.date_fin.isoformat()
|
d["date_fin_iso"] = self.date_fin.isoformat()
|
||||||
else:
|
else:
|
||||||
d["date_fin"] = d["date_fin_iso"] = ""
|
d["date_fin"] = d["date_fin_iso"] = ""
|
||||||
@ -314,20 +231,19 @@ class FormSemestre(models.ScoDocModel):
|
|||||||
|
|
||||||
def to_dict_api(self):
|
def to_dict_api(self):
|
||||||
"""
|
"""
|
||||||
Un dict avec les informations sur le semestre destinées à l'api
|
Un dict avec les informations sur le semestre destiné à l'api
|
||||||
"""
|
"""
|
||||||
d = dict(self.__dict__)
|
d = dict(self.__dict__)
|
||||||
d.pop("_sa_instance_state", None)
|
d.pop("_sa_instance_state", None)
|
||||||
d.pop("groups_auto_assignment_data", None)
|
d.pop("groups_auto_assignment_data", None)
|
||||||
d["annee_scolaire"] = self.annee_scolaire()
|
d["annee_scolaire"] = self.annee_scolaire()
|
||||||
d["bul_hide_xml"] = self.bul_hide_xml
|
|
||||||
if self.date_debut:
|
if self.date_debut:
|
||||||
d["date_debut"] = self.date_debut.strftime(scu.DATE_FMT)
|
d["date_debut"] = self.date_debut.strftime("%d/%m/%Y")
|
||||||
d["date_debut_iso"] = self.date_debut.isoformat()
|
d["date_debut_iso"] = self.date_debut.isoformat()
|
||||||
else:
|
else:
|
||||||
d["date_debut"] = d["date_debut_iso"] = ""
|
d["date_debut"] = d["date_debut_iso"] = ""
|
||||||
if self.date_fin:
|
if self.date_fin:
|
||||||
d["date_fin"] = self.date_fin.strftime(scu.DATE_FMT)
|
d["date_fin"] = self.date_fin.strftime("%d/%m/%Y")
|
||||||
d["date_fin_iso"] = self.date_fin.isoformat()
|
d["date_fin_iso"] = self.date_fin.isoformat()
|
||||||
else:
|
else:
|
||||||
d["date_fin"] = d["date_fin_iso"] = ""
|
d["date_fin"] = d["date_fin_iso"] = ""
|
||||||
@ -353,15 +269,10 @@ class FormSemestre(models.ScoDocModel):
|
|||||||
return default_partition.groups.first()
|
return default_partition.groups.first()
|
||||||
raise ScoValueError("Le semestre n'a pas de groupe par défaut")
|
raise ScoValueError("Le semestre n'a pas de groupe par défaut")
|
||||||
|
|
||||||
def get_edt_ids(self) -> list[str]:
|
def get_edt_id(self) -> str:
|
||||||
"""Les ids pour l'emploi du temps: à défaut, les codes étape Apogée.
|
"l'id pour l'emploi du temps: à défaut, le 1er code étape Apogée"
|
||||||
Les edt_id de formsemestres ne sont pas normalisés afin de contrôler
|
|
||||||
précisément l'accès au fichier ics.
|
|
||||||
"""
|
|
||||||
return (
|
return (
|
||||||
scu.split_id(self.edt_id)
|
self.edt_id or "" or (self.etapes[0].etape_apo if len(self.etapes) else "")
|
||||||
or [e.etape_apo.strip() for e in self.etapes if e.etape_apo]
|
|
||||||
or []
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_infos_dict(self) -> dict:
|
def get_infos_dict(self) -> dict:
|
||||||
@ -466,80 +377,6 @@ class FormSemestre(models.ScoDocModel):
|
|||||||
_cache[key] = ues
|
_cache[key] = ues
|
||||||
return ues
|
return ues
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_user_formsemestres_annee_by_dept(
|
|
||||||
cls, user: User
|
|
||||||
) -> tuple[
|
|
||||||
defaultdict[int, list["FormSemestre"]], defaultdict[int, list[ModuleImpl]]
|
|
||||||
]:
|
|
||||||
"""Liste des formsemestres de l'année scolaire
|
|
||||||
dans lesquels user intervient (comme resp., resp. de module ou enseignant),
|
|
||||||
ainsi que la liste des modimpls concernés dans chaque formsemestre
|
|
||||||
Attention: les semestres et modimpls peuvent être de différents départements !
|
|
||||||
Résultat:
|
|
||||||
{ dept_id : [ formsemestre, ... ] },
|
|
||||||
{ formsemestre_id : [ modimpl, ... ]}
|
|
||||||
"""
|
|
||||||
debut_annee_scolaire = scu.date_debut_annee_scolaire()
|
|
||||||
fin_annee_scolaire = scu.date_fin_annee_scolaire()
|
|
||||||
|
|
||||||
query = FormSemestre.query.filter(
|
|
||||||
FormSemestre.date_fin >= debut_annee_scolaire,
|
|
||||||
FormSemestre.date_debut < fin_annee_scolaire,
|
|
||||||
)
|
|
||||||
# responsable ?
|
|
||||||
formsemestres_resp = query.join(notes_formsemestre_responsables).filter_by(
|
|
||||||
responsable_id=user.id
|
|
||||||
)
|
|
||||||
# Responsable d'un modimpl ?
|
|
||||||
modimpls_resp = (
|
|
||||||
ModuleImpl.query.filter_by(responsable_id=user.id)
|
|
||||||
.join(FormSemestre)
|
|
||||||
.filter(
|
|
||||||
FormSemestre.date_fin >= debut_annee_scolaire,
|
|
||||||
FormSemestre.date_debut < fin_annee_scolaire,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
# Enseignant dans un modimpl ?
|
|
||||||
modimpls_ens = (
|
|
||||||
ModuleImpl.query.join(notes_modules_enseignants)
|
|
||||||
.filter_by(ens_id=user.id)
|
|
||||||
.join(FormSemestre)
|
|
||||||
.filter(
|
|
||||||
FormSemestre.date_fin >= debut_annee_scolaire,
|
|
||||||
FormSemestre.date_debut < fin_annee_scolaire,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
# Liste les modimpls, uniques
|
|
||||||
modimpls = modimpls_resp.all()
|
|
||||||
ids = {modimpl.id for modimpl in modimpls}
|
|
||||||
for modimpl in modimpls_ens:
|
|
||||||
if modimpl.id not in ids:
|
|
||||||
modimpls.append(modimpl)
|
|
||||||
ids.add(modimpl.id)
|
|
||||||
# Liste les formsemestres et modimpls associés
|
|
||||||
modimpls_by_formsemestre = defaultdict(lambda: [])
|
|
||||||
formsemestres = formsemestres_resp.all()
|
|
||||||
ids = {formsemestre.id for formsemestre in formsemestres}
|
|
||||||
for modimpl in chain(modimpls_resp, modimpls_ens):
|
|
||||||
if modimpl.formsemestre_id not in ids:
|
|
||||||
formsemestres.append(modimpl.formsemestre)
|
|
||||||
ids.add(modimpl.formsemestre_id)
|
|
||||||
modimpls_by_formsemestre[modimpl.formsemestre_id].append(modimpl)
|
|
||||||
# Tris et organisation par département
|
|
||||||
formsemestres_by_dept = defaultdict(lambda: [])
|
|
||||||
formsemestres.sort(key=lambda x: (x.departement.acronym,) + x.sort_key())
|
|
||||||
for formsemestre in formsemestres:
|
|
||||||
formsemestres_by_dept[formsemestre.dept_id].append(formsemestre)
|
|
||||||
modimpls = modimpls_by_formsemestre[formsemestre.id]
|
|
||||||
if formsemestre.formation.is_apc():
|
|
||||||
key = lambda x: x.module.sort_key_apc()
|
|
||||||
else:
|
|
||||||
key = lambda x: x.module.sort_key()
|
|
||||||
modimpls.sort(key=key)
|
|
||||||
|
|
||||||
return formsemestres_by_dept, modimpls_by_formsemestre
|
|
||||||
|
|
||||||
def get_evaluations(self) -> list[Evaluation]:
|
def get_evaluations(self) -> list[Evaluation]:
|
||||||
"Liste de toutes les évaluations du semestre, triées par module/numero"
|
"Liste de toutes les évaluations du semestre, triées par module/numero"
|
||||||
return (
|
return (
|
||||||
@ -550,7 +387,7 @@ class FormSemestre(models.ScoDocModel):
|
|||||||
Module.numero,
|
Module.numero,
|
||||||
Module.code,
|
Module.code,
|
||||||
Evaluation.numero,
|
Evaluation.numero,
|
||||||
Evaluation.date_debut,
|
Evaluation.date_debut.desc(),
|
||||||
)
|
)
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
@ -560,7 +397,6 @@ class FormSemestre(models.ScoDocModel):
|
|||||||
"""Liste des modimpls du semestre (y compris bonus)
|
"""Liste des modimpls du semestre (y compris bonus)
|
||||||
- triée par type/numéro/code en APC
|
- triée par type/numéro/code en APC
|
||||||
- triée par numéros d'UE/matières/modules pour les formations standard.
|
- triée par numéros d'UE/matières/modules pour les formations standard.
|
||||||
Hors APC, élimine les modules de type ressources et SAEs.
|
|
||||||
"""
|
"""
|
||||||
modimpls = self.modimpls.all()
|
modimpls = self.modimpls.all()
|
||||||
if self.formation.is_apc():
|
if self.formation.is_apc():
|
||||||
@ -572,14 +408,6 @@ class FormSemestre(models.ScoDocModel):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
modimpls = [
|
|
||||||
mi
|
|
||||||
for mi in modimpls
|
|
||||||
if (
|
|
||||||
mi.module.module_type
|
|
||||||
not in (scu.ModuleType.RESSOURCE, scu.ModuleType.SAE)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
modimpls.sort(
|
modimpls.sort(
|
||||||
key=lambda m: (
|
key=lambda m: (
|
||||||
m.module.ue.numero or 0,
|
m.module.ue.numero or 0,
|
||||||
@ -677,41 +505,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:
|
def est_terminal(self) -> bool:
|
||||||
"Vrai si dernier semestre de son cursus (ou formation mono-semestre)"
|
"Vrai si dernier semestre de son cursus (ou formation mono-semestre)"
|
||||||
return (self.semestre_id < 0) or (
|
return (self.semestre_id < 0) or (
|
||||||
@ -726,7 +519,7 @@ class FormSemestre(models.ScoDocModel):
|
|||||||
mois_pivot_periode=scu.MONTH_DEBUT_PERIODE2,
|
mois_pivot_periode=scu.MONTH_DEBUT_PERIODE2,
|
||||||
jour_pivot_annee=1,
|
jour_pivot_annee=1,
|
||||||
jour_pivot_periode=1,
|
jour_pivot_periode=1,
|
||||||
) -> tuple[int, int]:
|
):
|
||||||
"""Calcule la session associée à un formsemestre commençant en date_debut
|
"""Calcule la session associée à un formsemestre commençant en date_debut
|
||||||
sous la forme (année, période)
|
sous la forme (année, période)
|
||||||
année: première année de l'année scolaire
|
année: première année de l'année scolaire
|
||||||
@ -776,27 +569,7 @@ class FormSemestre(models.ScoDocModel):
|
|||||||
mois_pivot_periode=ScoDocSiteConfig.get_month_debut_periode2(),
|
mois_pivot_periode=ScoDocSiteConfig.get_month_debut_periode2(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
def etapes_apo_vdi(self) -> list[ApoEtapeVDI]:
|
||||||
def get_dept_formsemestres_courants(
|
|
||||||
cls, dept: Departement, date_courante: datetime.datetime | None = None
|
|
||||||
) -> db.Query:
|
|
||||||
"""Liste (query) ordonnée des formsemestres courants, c'est
|
|
||||||
à dire contenant la date courant (si None, la date actuelle)"""
|
|
||||||
date_courante = date_courante or db.func.current_date()
|
|
||||||
# Les semestres en cours de ce département
|
|
||||||
formsemestres = FormSemestre.query.filter(
|
|
||||||
FormSemestre.dept_id == dept.id,
|
|
||||||
FormSemestre.date_debut <= date_courante,
|
|
||||||
FormSemestre.date_fin >= date_courante,
|
|
||||||
)
|
|
||||||
return formsemestres.order_by(
|
|
||||||
FormSemestre.date_debut.desc(),
|
|
||||||
FormSemestre.modalite,
|
|
||||||
FormSemestre.semestre_id,
|
|
||||||
FormSemestre.titre,
|
|
||||||
)
|
|
||||||
|
|
||||||
def etapes_apo_vdi(self) -> list["ApoEtapeVDI"]:
|
|
||||||
"Liste des vdis"
|
"Liste des vdis"
|
||||||
# was read_formsemestre_etapes
|
# was read_formsemestre_etapes
|
||||||
return [e.as_apovdi() for e in self.etapes if e.etape_apo]
|
return [e.as_apovdi() for e in self.etapes if e.etape_apo]
|
||||||
@ -809,9 +582,9 @@ class FormSemestre(models.ScoDocModel):
|
|||||||
return ""
|
return ""
|
||||||
return ", ".join(sorted([etape.etape_apo for etape in self.etapes if etape]))
|
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"
|
"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)
|
db.session.add(etape)
|
||||||
|
|
||||||
def regroupements_coherents_etud(self) -> list[tuple[UniteEns, UniteEns]]:
|
def regroupements_coherents_etud(self) -> list[tuple[UniteEns, UniteEns]]:
|
||||||
@ -845,11 +618,11 @@ class FormSemestre(models.ScoDocModel):
|
|||||||
else:
|
else:
|
||||||
return ", ".join([u.get_nomcomplet() for u in self.responsables])
|
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"
|
"True si l'user est l'un des responsables du semestre"
|
||||||
return user.id in [u.id for u in self.responsables]
|
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"
|
"Vrai si utilisateur (par def. current) est admin, chef dept ou responsable du semestre"
|
||||||
user = user or current_user
|
user = user or current_user
|
||||||
return user.has_permission(Permission.EditFormSemestre) or self.est_responsable(
|
return user.has_permission(Permission.EditFormSemestre) or self.est_responsable(
|
||||||
@ -858,7 +631,7 @@ class FormSemestre(models.ScoDocModel):
|
|||||||
|
|
||||||
def can_change_groups(self, user: User = None) -> bool:
|
def can_change_groups(self, user: User = None) -> bool:
|
||||||
"""Vrai si l'utilisateur (par def. current) peut changer les groupes dans
|
"""Vrai si l'utilisateur (par def. current) peut changer les groupes dans
|
||||||
ce semestre: vérifie permission et verrouillage (mais pas si la partition est éditable).
|
ce semestre: vérifie permission et verrouillage.
|
||||||
"""
|
"""
|
||||||
if not self.etat:
|
if not self.etat:
|
||||||
return False # semestre verrouillé
|
return False # semestre verrouillé
|
||||||
@ -867,7 +640,7 @@ class FormSemestre(models.ScoDocModel):
|
|||||||
return True # typiquement admin, chef dept
|
return True # typiquement admin, chef dept
|
||||||
return self.est_responsable(user)
|
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
|
"""Vrai si utilisateur (par def. current) peut saisir decision de jury
|
||||||
dans ce semestre: vérifie permission et verrouillage.
|
dans ce semestre: vérifie permission et verrouillage.
|
||||||
"""
|
"""
|
||||||
@ -978,9 +751,9 @@ class FormSemestre(models.ScoDocModel):
|
|||||||
descr_sem += " " + self.modalite
|
descr_sem += " " + self.modalite
|
||||||
return descr_sem
|
return descr_sem
|
||||||
|
|
||||||
def get_abs_count(self, etudid) -> tuple[int, int, int]:
|
def get_abs_count(self, etudid):
|
||||||
"""Les comptes d'absences de cet étudiant dans ce semestre:
|
"""Les comptes d'absences de cet étudiant dans ce semestre:
|
||||||
tuple (nb abs non just, nb abs justifiées, nb abs total)
|
tuple (nb abs, nb abs justifiées)
|
||||||
Utilise un cache.
|
Utilise un cache.
|
||||||
"""
|
"""
|
||||||
from app.scodoc import sco_assiduites
|
from app.scodoc import sco_assiduites
|
||||||
@ -995,12 +768,7 @@ class FormSemestre(models.ScoDocModel):
|
|||||||
|
|
||||||
def get_codes_apogee(self, category=None) -> set[str]:
|
def get_codes_apogee(self, category=None) -> set[str]:
|
||||||
"""Les codes Apogée (codés en base comme "VRT1,VRT2")
|
"""Les codes Apogée (codés en base comme "VRT1,VRT2")
|
||||||
category:
|
category: None: tous, "etapes": étapes associées, "sem: code semestre", "annee": code annuel
|
||||||
None: tous,
|
|
||||||
"etapes": étapes associées,
|
|
||||||
"sem: code semestre"
|
|
||||||
"annee": code annuel
|
|
||||||
"passage": code passage
|
|
||||||
"""
|
"""
|
||||||
codes = set()
|
codes = set()
|
||||||
if category is None or category == "etapes":
|
if category is None or category == "etapes":
|
||||||
@ -1009,8 +777,6 @@ class FormSemestre(models.ScoDocModel):
|
|||||||
codes |= {x.strip() for x in self.elt_sem_apo.split(",") if x}
|
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:
|
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}
|
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
|
return codes
|
||||||
|
|
||||||
def get_inscrits(self, include_demdef=False, order=False) -> list[Identite]:
|
def get_inscrits(self, include_demdef=False, order=False) -> list[Identite]:
|
||||||
@ -1045,16 +811,12 @@ class FormSemestre(models.ScoDocModel):
|
|||||||
partitions += [p for p in self.partitions if p.partition_name is None]
|
partitions += [p for p in self.partitions if p.partition_name is None]
|
||||||
return partitions
|
return partitions
|
||||||
|
|
||||||
def etudids_actifs(self) -> tuple[list[int], set[int]]:
|
@cached_property
|
||||||
"""Liste les etudids inscrits (incluant DEM et DEF),
|
def etudids_actifs(self) -> set:
|
||||||
qui sera l'index des dataframes de notes
|
"Set des etudids inscrits non démissionnaires et non défaillants"
|
||||||
et donne l'ensemble des inscrits non DEM ni DEF.
|
return {ins.etudid for ins in self.inscriptions if ins.etat == scu.INSCRIT}
|
||||||
"""
|
|
||||||
return [inscr.etudid for inscr in self.inscriptions], {
|
|
||||||
ins.etudid for ins in self.inscriptions if ins.etat == scu.INSCRIT
|
|
||||||
}
|
|
||||||
|
|
||||||
@property
|
@cached_property
|
||||||
def etuds_inscriptions(self) -> dict:
|
def etuds_inscriptions(self) -> dict:
|
||||||
"""Map { etudid : inscription } (incluant DEM et DEF)"""
|
"""Map { etudid : inscription } (incluant DEM et DEF)"""
|
||||||
return {ins.etud.id: ins for ins in self.inscriptions}
|
return {ins.etud.id: ins for ins in self.inscriptions}
|
||||||
@ -1278,33 +1040,6 @@ class FormSemestre(models.ScoDocModel):
|
|||||||
nb_recorded += 1
|
nb_recorded += 1
|
||||||
return nb_recorded
|
return nb_recorded
|
||||||
|
|
||||||
def change_formation(self, formation_dest: Formation):
|
|
||||||
"""Associe ce formsemestre à une autre formation.
|
|
||||||
Ce n'est possible que si la formation destination possède des modules de
|
|
||||||
même code que ceux utilisés dans la formation d'origine du formsemestre.
|
|
||||||
S'il manque un module, l'opération est annulée.
|
|
||||||
Commit (or rollback) session.
|
|
||||||
"""
|
|
||||||
ok = True
|
|
||||||
for mi in self.modimpls:
|
|
||||||
dest_modules = formation_dest.modules.filter_by(code=mi.module.code).all()
|
|
||||||
match len(dest_modules):
|
|
||||||
case 1:
|
|
||||||
mi.module = dest_modules[0]
|
|
||||||
db.session.add(mi)
|
|
||||||
case 0:
|
|
||||||
print(f"Argh ! no module found with code={mi.module.code}")
|
|
||||||
ok = False
|
|
||||||
case _:
|
|
||||||
print(f"Arg ! several modules found with code={mi.module.code}")
|
|
||||||
ok = False
|
|
||||||
|
|
||||||
if ok:
|
|
||||||
self.formation_id = formation_dest.id
|
|
||||||
db.session.commit()
|
|
||||||
else:
|
|
||||||
db.session.rollback()
|
|
||||||
|
|
||||||
|
|
||||||
# Association id des utilisateurs responsables (aka directeurs des etudes) du semestre
|
# Association id des utilisateurs responsables (aka directeurs des etudes) du semestre
|
||||||
notes_formsemestre_responsables = db.Table(
|
notes_formsemestre_responsables = db.Table(
|
||||||
@ -1334,18 +1069,10 @@ class FormSemestreEtape(db.Model):
|
|||||||
"Etape False if code empty"
|
"Etape False if code empty"
|
||||||
return self.etape_apo is not None and (len(self.etape_apo) > 0)
|
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):
|
def __repr__(self):
|
||||||
return f"<Etape {self.id} apo={self.etape_apo!r}>"
|
return f"<Etape {self.id} apo={self.etape_apo!r}>"
|
||||||
|
|
||||||
def __str__(self):
|
def as_apovdi(self) -> ApoEtapeVDI:
|
||||||
return self.etape_apo or ""
|
|
||||||
|
|
||||||
def as_apovdi(self) -> "ApoEtapeVDI":
|
|
||||||
return ApoEtapeVDI(self.etape_apo)
|
return ApoEtapeVDI(self.etape_apo)
|
||||||
|
|
||||||
|
|
||||||
@ -1498,9 +1225,8 @@ class FormSemestreInscription(db.Model):
|
|||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"""<{self.__class__.__name__} {self.id} etudid={self.etudid} sem={
|
return f"""<{self.__class__.__name__} {self.id} etudid={self.etudid} sem={
|
||||||
self.formsemestre_id} (S{self.formsemestre.semestre_id}) etat={self.etat} {
|
self.formsemestre_id} etat={self.etat} {
|
||||||
('parcours="'+str(self.parcour.code)+'"') if self.parcour else ''
|
('parcours='+str(self.parcour)) if self.parcour else ''}>"""
|
||||||
} {('etape="'+self.etape+'"') if self.etape else ''}>"""
|
|
||||||
|
|
||||||
|
|
||||||
class NotesSemSet(db.Model):
|
class NotesSemSet(db.Model):
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# -*- coding: UTF-8 -*
|
# -*- coding: UTF-8 -*
|
||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -11,15 +11,14 @@ from operator import attrgetter
|
|||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
from app import db, log
|
from app import db, log
|
||||||
from app.models import ScoDocModel, GROUPNAME_STR_LEN, SHORT_STR_LEN
|
from app.models import Scolog, GROUPNAME_STR_LEN, SHORT_STR_LEN
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
from app.models.events import Scolog
|
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||||
|
|
||||||
|
|
||||||
class Partition(ScoDocModel):
|
class Partition(db.Model):
|
||||||
"""Partition: découpage d'une promotion en groupes"""
|
"""Partition: découpage d'une promotion en groupes"""
|
||||||
|
|
||||||
__table_args__ = (db.UniqueConstraint("formsemestre_id", "partition_name"),)
|
__table_args__ = (db.UniqueConstraint("formsemestre_id", "partition_name"),)
|
||||||
@ -54,7 +53,6 @@ class Partition(ScoDocModel):
|
|||||||
cascade="all, delete-orphan",
|
cascade="all, delete-orphan",
|
||||||
order_by="GroupDescr.numero, GroupDescr.group_name",
|
order_by="GroupDescr.numero, GroupDescr.group_name",
|
||||||
)
|
)
|
||||||
_sco_dept_relations = ("FormSemestre",)
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super(Partition, self).__init__(**kwargs)
|
super(Partition, self).__init__(**kwargs)
|
||||||
@ -94,10 +92,6 @@ class Partition(ScoDocModel):
|
|||||||
):
|
):
|
||||||
group.remove_etud(etud)
|
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:
|
def is_parcours(self) -> bool:
|
||||||
"Vrai s'il s'agit de la partition de parcours"
|
"Vrai s'il s'agit de la partition de parcours"
|
||||||
return self.partition_name == scu.PARTITION_PARCOURS
|
return self.partition_name == scu.PARTITION_PARCOURS
|
||||||
@ -210,7 +204,7 @@ class Partition(ScoDocModel):
|
|||||||
return group
|
return group
|
||||||
|
|
||||||
|
|
||||||
class GroupDescr(ScoDocModel):
|
class GroupDescr(db.Model):
|
||||||
"""Description d'un groupe d'une partition"""
|
"""Description d'un groupe d'une partition"""
|
||||||
|
|
||||||
__tablename__ = "group_descr"
|
__tablename__ = "group_descr"
|
||||||
@ -226,11 +220,6 @@ class GroupDescr(ScoDocModel):
|
|||||||
numero = db.Column(db.Integer, nullable=False, default=0)
|
numero = db.Column(db.Integer, nullable=False, default=0)
|
||||||
"Numero = ordre de presentation"
|
"Numero = ordre de presentation"
|
||||||
|
|
||||||
_sco_dept_relations = (
|
|
||||||
"Partition",
|
|
||||||
"FormSemestre",
|
|
||||||
)
|
|
||||||
|
|
||||||
etuds = db.relationship(
|
etuds = db.relationship(
|
||||||
"Identite",
|
"Identite",
|
||||||
secondary="group_membership",
|
secondary="group_membership",
|
||||||
@ -252,20 +241,15 @@ class GroupDescr(ScoDocModel):
|
|||||||
|
|
||||||
def to_dict(self, with_partition=True) -> dict:
|
def to_dict(self, with_partition=True) -> dict:
|
||||||
"""as a dict, with or without partition"""
|
"""as a dict, with or without partition"""
|
||||||
if with_partition:
|
|
||||||
partition_dict = self.partition.to_dict(with_groups=False)
|
|
||||||
d = dict(self.__dict__)
|
d = dict(self.__dict__)
|
||||||
d.pop("_sa_instance_state", None)
|
d.pop("_sa_instance_state", None)
|
||||||
if with_partition:
|
if with_partition:
|
||||||
d["partition"] = partition_dict
|
d["partition"] = self.partition.to_dict(with_groups=False)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def get_edt_ids(self) -> list[str]:
|
def get_edt_id(self) -> str:
|
||||||
"les ids normalisés pour l'emploi du temps: à défaut, le nom scodoc du groupe"
|
"l'id pour l'emploi du temps: à défaut, le nom scodoc du groupe"
|
||||||
return [
|
return self.edt_id or self.group_name or ""
|
||||||
scu.normalize_edt_id(x)
|
|
||||||
for x in scu.split_id(self.edt_id) or [self.group_name] or []
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_nb_inscrits(self) -> int:
|
def get_nb_inscrits(self) -> int:
|
||||||
"""Nombre inscrits à ce group et au formsemestre.
|
"""Nombre inscrits à ce group et au formsemestre.
|
||||||
@ -298,18 +282,18 @@ class GroupDescr(ScoDocModel):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def set_name(self, group_name: str, dest_url: str = None):
|
def set_name(
|
||||||
|
self, group_name: str, edt_id: str | bool = False, dest_url: str = None
|
||||||
|
):
|
||||||
"""Set group name, and optionally edt_id.
|
"""Set group name, and optionally edt_id.
|
||||||
Check permission (partition must be groups_editable)
|
Check permission and invalidate caches. Commit session.
|
||||||
and invalidate caches. Commit session.
|
|
||||||
dest_url is used for error messages.
|
dest_url is used for error messages.
|
||||||
"""
|
"""
|
||||||
if not self.partition.formsemestre.can_change_groups():
|
if not self.partition.formsemestre.can_change_groups():
|
||||||
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
||||||
if self.group_name is None:
|
if self.group_name is None:
|
||||||
raise ValueError("can't set a name to default group")
|
raise ValueError("can't set a name to default group")
|
||||||
if not self.partition.groups_editable:
|
|
||||||
raise AccessDenied("Partition non éditable")
|
|
||||||
if group_name:
|
if group_name:
|
||||||
group_name = group_name.strip()
|
group_name = group_name.strip()
|
||||||
if not group_name:
|
if not group_name:
|
||||||
@ -322,32 +306,27 @@ class GroupDescr(ScoDocModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.group_name = group_name
|
self.group_name = group_name
|
||||||
|
if edt_id is not False:
|
||||||
|
if isinstance(edt_id, str):
|
||||||
|
edt_id = edt_id.strip() or None
|
||||||
|
self.edt_id = edt_id
|
||||||
db.session.add(self)
|
db.session.add(self)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
sco_cache.invalidate_formsemestre(
|
sco_cache.invalidate_formsemestre(
|
||||||
formsemestre_id=self.partition.formsemestre_id
|
formsemestre_id=self.partition.formsemestre_id
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_edt_id(self, edt_id: str):
|
|
||||||
"Set edt_id. Check permission. Commit session."
|
|
||||||
if not self.partition.formsemestre.can_change_groups():
|
|
||||||
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
|
||||||
if isinstance(edt_id, str):
|
|
||||||
edt_id = edt_id.strip() or None
|
|
||||||
self.edt_id = edt_id
|
|
||||||
db.session.add(self)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
def remove_etud(self, etud: "Identite"):
|
def remove_etud(self, etud: "Identite"):
|
||||||
"Enlève l'étudiant de ce groupe s'il en fait partie (ne fait rien sinon)"
|
"Enlève l'étudiant de ce groupe s'il en fait partie (ne fait rien sinon)"
|
||||||
if etud in self.etuds:
|
if etud in self.etuds:
|
||||||
self.etuds.remove(etud)
|
self.etuds.remove(etud)
|
||||||
|
db.session.commit()
|
||||||
Scolog.logdb(
|
Scolog.logdb(
|
||||||
method="group_remove_etud",
|
method="group_remove_etud",
|
||||||
etudid=etud.id,
|
etudid=etud.id,
|
||||||
msg=f"Retrait du groupe {self.group_name} de {self.partition.partition_name}",
|
msg=f"Retrait du groupe {self.group_name} de {self.partition.partition_name}",
|
||||||
|
commit=True,
|
||||||
)
|
)
|
||||||
db.session.commit()
|
|
||||||
# Update parcours
|
# Update parcours
|
||||||
if self.partition.partition_name == scu.PARTITION_PARCOURS:
|
if self.partition.partition_name == scu.PARTITION_PARCOURS:
|
||||||
self.partition.formsemestre.update_inscriptions_parcours_from_groups(
|
self.partition.formsemestre.update_inscriptions_parcours_from_groups(
|
||||||
|
@ -2,24 +2,20 @@
|
|||||||
"""ScoDoc models: moduleimpls
|
"""ScoDoc models: moduleimpls
|
||||||
"""
|
"""
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from flask import abort, g
|
|
||||||
from flask_login import current_user
|
|
||||||
from flask_sqlalchemy.query import Query
|
from flask_sqlalchemy.query import Query
|
||||||
|
|
||||||
import app
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.auth.models import User
|
from app.auth.models import User
|
||||||
from app.comp import df_cache
|
from app.comp import df_cache
|
||||||
from app.models import APO_CODE_STR_LEN, ScoDocModel
|
from app.models import APO_CODE_STR_LEN
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
from app.models.evaluations import Evaluation
|
|
||||||
from app.models.modules import Module
|
from app.models.modules import Module
|
||||||
from app.scodoc.sco_exceptions import AccessDenied, ScoLockedSemError
|
from app.scodoc.sco_exceptions import AccessDenied, ScoLockedSemError
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
class ModuleImpl(ScoDocModel):
|
class ModuleImpl(db.Model):
|
||||||
"""Mise en oeuvre d'un module pour une annee/semestre"""
|
"""Mise en oeuvre d'un module pour une annee/semestre"""
|
||||||
|
|
||||||
__tablename__ = "notes_moduleimpl"
|
__tablename__ = "notes_moduleimpl"
|
||||||
@ -38,29 +34,18 @@ class ModuleImpl(ScoDocModel):
|
|||||||
index=True,
|
index=True,
|
||||||
nullable=False,
|
nullable=False,
|
||||||
)
|
)
|
||||||
responsable_id = db.Column(
|
responsable_id = db.Column("responsable_id", db.Integer, db.ForeignKey("user.id"))
|
||||||
"responsable_id", db.Integer, db.ForeignKey("user.id", ondelete="SET NULL")
|
|
||||||
)
|
|
||||||
responsable = db.relationship("User", back_populates="modimpls")
|
|
||||||
# formule de calcul moyenne:
|
# formule de calcul moyenne:
|
||||||
computation_expr = db.Column(db.Text())
|
computation_expr = db.Column(db.Text())
|
||||||
|
|
||||||
evaluations = db.relationship(
|
evaluations = db.relationship("Evaluation", lazy="dynamic", backref="moduleimpl")
|
||||||
"Evaluation",
|
|
||||||
lazy="dynamic",
|
|
||||||
backref="moduleimpl",
|
|
||||||
order_by=(Evaluation.numero, Evaluation.date_debut),
|
|
||||||
)
|
|
||||||
"évaluations, triées par numéro et dates croissants, donc la plus ancienne d'abord."
|
|
||||||
enseignants = db.relationship(
|
enseignants = db.relationship(
|
||||||
"User",
|
"User",
|
||||||
secondary="notes_modules_enseignants",
|
secondary="notes_modules_enseignants",
|
||||||
lazy="dynamic",
|
lazy="dynamic",
|
||||||
backref="moduleimpl",
|
backref="moduleimpl",
|
||||||
|
viewonly=True,
|
||||||
)
|
)
|
||||||
"enseignants du module (sans le responsable)"
|
|
||||||
|
|
||||||
_sco_dept_relations = ("FormSemestre",) # accès au dept_id
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__name__} {self.id} module={repr(self.module)}>"
|
return f"<{self.__class__.__name__} {self.id} module={repr(self.module)}>"
|
||||||
@ -73,17 +58,16 @@ class ModuleImpl(ScoDocModel):
|
|||||||
return {x.strip() for x in self.code_apogee.split(",") if x}
|
return {x.strip() for x in self.code_apogee.split(",") if x}
|
||||||
return self.module.get_codes_apogee()
|
return self.module.get_codes_apogee()
|
||||||
|
|
||||||
def get_edt_ids(self) -> list[str]:
|
def get_edt_id(self) -> str:
|
||||||
"les ids pour l'emploi du temps: à défaut, les codes Apogée"
|
"l'id pour l'emploi du temps: à défaut, le 1er code Apogée"
|
||||||
return [
|
return (
|
||||||
scu.normalize_edt_id(x)
|
self.edt_id
|
||||||
for x in scu.split_id(self.edt_id) or scu.split_id(self.code_apogee)
|
or (self.code_apogee.split(",")[0] if self.code_apogee else "")
|
||||||
] or self.module.get_edt_ids()
|
or self.module.get_edt_id()
|
||||||
|
)
|
||||||
|
|
||||||
def get_evaluations_poids(self) -> pd.DataFrame:
|
def get_evaluations_poids(self) -> pd.DataFrame:
|
||||||
"""Les poids des évaluations vers les UEs (accès via cache redis).
|
"""Les poids des évaluations vers les UE (accès via cache)"""
|
||||||
Toutes les évaluations sont considérées (normales, bonus, rattr., etc.)
|
|
||||||
"""
|
|
||||||
evaluations_poids = df_cache.EvaluationsPoidsCache.get(self.id)
|
evaluations_poids = df_cache.EvaluationsPoidsCache.get(self.id)
|
||||||
if evaluations_poids is None:
|
if evaluations_poids is None:
|
||||||
from app.comp import moy_mod
|
from app.comp import moy_mod
|
||||||
@ -92,58 +76,24 @@ class ModuleImpl(ScoDocModel):
|
|||||||
df_cache.EvaluationsPoidsCache.set(self.id, evaluations_poids)
|
df_cache.EvaluationsPoidsCache.set(self.id, evaluations_poids)
|
||||||
return evaluations_poids
|
return evaluations_poids
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_modimpl(cls, moduleimpl_id: int | str, dept_id: int = None) -> "ModuleImpl":
|
|
||||||
"""ModuleImpl ou 404, cherche uniquement dans le département spécifié ou le courant."""
|
|
||||||
from app.models.formsemestre import FormSemestre
|
|
||||||
|
|
||||||
if not isinstance(moduleimpl_id, int):
|
|
||||||
try:
|
|
||||||
moduleimpl_id = int(moduleimpl_id)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
abort(404, "moduleimpl_id invalide")
|
|
||||||
if g.scodoc_dept:
|
|
||||||
dept_id = dept_id if dept_id is not None else g.scodoc_dept_id
|
|
||||||
query = cls.query.filter_by(id=moduleimpl_id)
|
|
||||||
if dept_id is not None:
|
|
||||||
query = query.join(FormSemestre).filter_by(dept_id=dept_id)
|
|
||||||
return query.first_or_404()
|
|
||||||
|
|
||||||
def invalidate_evaluations_poids(self):
|
def invalidate_evaluations_poids(self):
|
||||||
"""Invalide poids cachés"""
|
"""Invalide poids cachés"""
|
||||||
df_cache.EvaluationsPoidsCache.delete(self.id)
|
df_cache.EvaluationsPoidsCache.delete(self.id)
|
||||||
|
|
||||||
def check_apc_conformity(
|
def check_apc_conformity(self, res: "ResultatsSemestreBUT") -> bool:
|
||||||
self, res: "ResultatsSemestreBUT", evaluation_type=Evaluation.EVALUATION_NORMALE
|
"""true si les poids des évaluations du module permettent de satisfaire
|
||||||
) -> bool:
|
les coefficients du PN.
|
||||||
"""true si les poids des évaluations du type indiqué (normales par défaut)
|
|
||||||
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 (
|
if not self.module.formation.get_cursus().APC_SAE or (
|
||||||
self.module.module_type
|
self.module.module_type != scu.ModuleType.RESSOURCE
|
||||||
not in {scu.ModuleType.RESSOURCE, scu.ModuleType.SAE}
|
and self.module.module_type != scu.ModuleType.SAE
|
||||||
):
|
):
|
||||||
return True # Non BUT, toujours conforme
|
return True # Non BUT, toujours conforme
|
||||||
from app.comp import moy_mod
|
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(
|
return moy_mod.moduleimpl_is_conforme(
|
||||||
self,
|
self,
|
||||||
selected_evaluations_poids,
|
self.get_evaluations_poids(),
|
||||||
res.modimpl_coefs_df,
|
res.modimpl_coefs_df,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -213,7 +163,7 @@ class ModuleImpl(ScoDocModel):
|
|||||||
return allow_ens and user.id in (ens.id for ens in self.enseignants)
|
return allow_ens and user.id in (ens.id for ens in self.enseignants)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def can_change_responsable(self, user: User, raise_exc=False) -> bool:
|
def can_change_ens_by(self, user: User, raise_exc=False) -> bool:
|
||||||
"""Check if user can modify module resp.
|
"""Check if user can modify module resp.
|
||||||
If raise_exc, raises exception (AccessDenied or ScoLockedSemError) if not.
|
If raise_exc, raises exception (AccessDenied or ScoLockedSemError) if not.
|
||||||
= Admin, et dir des etud. (si option l'y autorise)
|
= Admin, et dir des etud. (si option l'y autorise)
|
||||||
@ -234,81 +184,21 @@ class ModuleImpl(ScoDocModel):
|
|||||||
raise AccessDenied(f"Modification impossible pour {user}")
|
raise AccessDenied(f"Modification impossible pour {user}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def can_change_ens(self, user: User | None = None, raise_exc=True) -> bool:
|
def est_inscrit(self, etud: Identite) -> bool:
|
||||||
"""check if user can modify ens list (raise exception if not)"
|
|
||||||
if user is None, current user.
|
|
||||||
"""
|
"""
|
||||||
user = current_user if user is None else user
|
Vérifie si l'étudiant est bien inscrit au moduleimpl
|
||||||
if not self.formsemestre.etat:
|
|
||||||
if raise_exc:
|
|
||||||
raise ScoLockedSemError("Modification impossible: semestre verrouille")
|
|
||||||
return False
|
|
||||||
# -- check access
|
|
||||||
# admin, resp. module ou resp. semestre
|
|
||||||
if (
|
|
||||||
user.id != self.responsable_id
|
|
||||||
and not user.has_permission(Permission.EditFormSemestre)
|
|
||||||
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 can_change_inscriptions(self, user: User | None = None, raise_exc=True) -> bool:
|
Retourne Vrai si c'est le cas, faux sinon
|
||||||
"""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):
|
is_module: int = (
|
||||||
"""
|
ModuleImplInscription.query.filter_by(
|
||||||
Vérifie si l'étudiant est bien inscrit au moduleimpl (même si DEM ou DEF au semestre).
|
etudid=etud.id, moduleimpl_id=self.id
|
||||||
(lent, pas de cache: pour un accès rapide, utiliser nt.modimpl_inscr_df).
|
).count()
|
||||||
Retourne ModuleImplInscription si inscrit au module, False sinon.
|
> 0
|
||||||
"""
|
|
||||||
# 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()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return inscription or False
|
return is_module
|
||||||
|
|
||||||
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)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Enseignants (chargés de TD ou TP) d'un moduleimpl
|
# Enseignants (chargés de TD ou TP) d'un moduleimpl
|
||||||
|
@ -1,24 +1,17 @@
|
|||||||
"""ScoDoc 9 models : Modules
|
"""ScoDoc 9 models : Modules
|
||||||
"""
|
"""
|
||||||
|
from flask import current_app
|
||||||
from flask import current_app, g
|
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app import models
|
|
||||||
from app.models import APO_CODE_STR_LEN
|
from app.models import APO_CODE_STR_LEN
|
||||||
from app.models.but_refcomp import (
|
from app.models.but_refcomp import ApcParcours, app_critiques_modules, parcours_modules
|
||||||
ApcParcours,
|
|
||||||
ApcReferentielCompetences,
|
|
||||||
app_critiques_modules,
|
|
||||||
parcours_modules,
|
|
||||||
)
|
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
from app.scodoc.codes_cursus import UE_SPORT
|
from app.scodoc.codes_cursus import UE_SPORT
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
from app.scodoc.sco_utils import ModuleType
|
from app.scodoc.sco_utils import ModuleType
|
||||||
|
|
||||||
|
|
||||||
class Module(models.ScoDocModel):
|
class Module(db.Model):
|
||||||
"""Module"""
|
"""Module"""
|
||||||
|
|
||||||
__tablename__ = "notes_modules"
|
__tablename__ = "notes_modules"
|
||||||
@ -29,7 +22,6 @@ class Module(models.ScoDocModel):
|
|||||||
abbrev = db.Column(db.Text()) # nom court
|
abbrev = db.Column(db.Text()) # nom court
|
||||||
# certains départements ont des codes infiniment longs: donc Text !
|
# certains départements ont des codes infiniment longs: donc Text !
|
||||||
code = db.Column(db.Text(), nullable=False)
|
code = db.Column(db.Text(), nullable=False)
|
||||||
"code module, chaine non nullable"
|
|
||||||
heures_cours = db.Column(db.Float)
|
heures_cours = db.Column(db.Float)
|
||||||
heures_td = db.Column(db.Float)
|
heures_td = db.Column(db.Float)
|
||||||
heures_tp = db.Column(db.Float)
|
heures_tp = db.Column(db.Float)
|
||||||
@ -75,8 +67,6 @@ class Module(models.ScoDocModel):
|
|||||||
backref=db.backref("modules", lazy=True),
|
backref=db.backref("modules", lazy=True),
|
||||||
)
|
)
|
||||||
|
|
||||||
_sco_dept_relations = "Formation" # accès au dept_id
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.ue_coefs = []
|
self.ue_coefs = []
|
||||||
super(Module, self).__init__(**kwargs)
|
super(Module, self).__init__(**kwargs)
|
||||||
@ -85,100 +75,6 @@ class Module(models.ScoDocModel):
|
|||||||
return f"""<Module{ModuleType(self.module_type or ModuleType.STANDARD).name
|
return f"""<Module{ModuleType(self.module_type or ModuleType.STANDARD).name
|
||||||
} id={self.id} code={self.code!r} semestre_id={self.semestre_id}>"""
|
} id={self.id} code={self.code!r} semestre_id={self.semestre_id}>"""
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def convert_dict_fields(cls, args: dict) -> dict:
|
|
||||||
"""Convert fields in the given dict. No other side effect.
|
|
||||||
returns: dict to store in model's db.
|
|
||||||
"""
|
|
||||||
# s'assure que ects etc est non ''
|
|
||||||
fs_empty_stored_as_nulls = {
|
|
||||||
"coefficient",
|
|
||||||
"ects",
|
|
||||||
"heures_cours",
|
|
||||||
"heures_td",
|
|
||||||
"heures_tp",
|
|
||||||
}
|
|
||||||
args_dict = {}
|
|
||||||
for key, value in args.items():
|
|
||||||
if hasattr(cls, key) and not isinstance(getattr(cls, key, None), property):
|
|
||||||
if key in fs_empty_stored_as_nulls and value == "":
|
|
||||||
value = None
|
|
||||||
args_dict[key] = value
|
|
||||||
|
|
||||||
return args_dict
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def filter_model_attributes(cls, args: 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
|
|
||||||
|
|
||||||
@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:
|
|
||||||
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)
|
|
||||||
)
|
|
||||||
if g.scodoc_dept:
|
|
||||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
|
||||||
parcour: ApcParcours = query.first()
|
|
||||||
if parcour is None:
|
|
||||||
raise ScoValueError("Parcours invalide")
|
|
||||||
self.parcours.append(parcour)
|
|
||||||
|
|
||||||
def clone(self):
|
def clone(self):
|
||||||
"""Create a new copy of this module."""
|
"""Create a new copy of this module."""
|
||||||
mod = Module(
|
mod = Module(
|
||||||
@ -210,29 +106,16 @@ class Module(models.ScoDocModel):
|
|||||||
mod.app_critiques.append(app_critique)
|
mod.app_critiques.append(app_critique)
|
||||||
return mod
|
return mod
|
||||||
|
|
||||||
def to_dict(
|
def to_dict(self, convert_objects=False, with_matiere=False, with_ue=False) -> dict:
|
||||||
self,
|
|
||||||
convert_objects=False,
|
|
||||||
with_matiere=False,
|
|
||||||
with_ue=False,
|
|
||||||
with_parcours_ids=False,
|
|
||||||
) -> dict:
|
|
||||||
"""If convert_objects, convert all attributes to native types
|
"""If convert_objects, convert all attributes to native types
|
||||||
(suitable jor json encoding).
|
(suitable jor json encoding).
|
||||||
If convert_objects and with_parcours_ids, give parcours as a list of id (API)
|
|
||||||
"""
|
"""
|
||||||
d = dict(self.__dict__)
|
d = dict(self.__dict__)
|
||||||
d.pop("_sa_instance_state", None)
|
d.pop("_sa_instance_state", None)
|
||||||
d.pop("formation", None)
|
|
||||||
if convert_objects:
|
if convert_objects:
|
||||||
if with_parcours_ids:
|
d["parcours"] = [p.to_dict() for p in self.parcours]
|
||||||
d["parcours"] = [p.id for p in self.parcours]
|
|
||||||
else:
|
|
||||||
d["parcours"] = [p.to_dict() for p in self.parcours]
|
|
||||||
d["ue_coefs"] = [
|
d["ue_coefs"] = [
|
||||||
c.to_dict(convert_objects=False)
|
c.to_dict(convert_objects=convert_objects) for c in self.ue_coefs
|
||||||
for c in self.ue_coefs
|
|
||||||
# note: don't convert_objects: we do wan't the details of the UEs here
|
|
||||||
]
|
]
|
||||||
d["app_critiques"] = {x.code: x.to_dict() for x in self.app_critiques}
|
d["app_critiques"] = {x.code: x.to_dict() for x in self.app_critiques}
|
||||||
if not with_matiere:
|
if not with_matiere:
|
||||||
@ -276,10 +159,6 @@ class Module(models.ScoDocModel):
|
|||||||
"Identifiant du module à afficher : abbrev ou titre ou code"
|
"Identifiant du module à afficher : abbrev ou titre ou code"
|
||||||
return self.abbrev or self.titre or self.code
|
return self.abbrev or self.titre or self.code
|
||||||
|
|
||||||
def sort_key(self) -> tuple:
|
|
||||||
"""Clé de tri pour formations classiques"""
|
|
||||||
return self.numero or 0, self.code
|
|
||||||
|
|
||||||
def sort_key_apc(self) -> tuple:
|
def sort_key_apc(self) -> tuple:
|
||||||
"""Clé de tri pour avoir
|
"""Clé de tri pour avoir
|
||||||
présentation par type (res, sae), parcours, type, numéro
|
présentation par type (res, sae), parcours, type, numéro
|
||||||
@ -400,33 +279,19 @@ class Module(models.ScoDocModel):
|
|||||||
# Liste seulement les coefs définis:
|
# Liste seulement les coefs définis:
|
||||||
return [(c.ue, c.coef) for c in self.get_ue_coefs_sorted()]
|
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]:
|
def get_codes_apogee(self) -> set[str]:
|
||||||
"""Les codes Apogée (codés en base comme "VRT1,VRT2")"""
|
"""Les codes Apogée (codés en base comme "VRT1,VRT2")"""
|
||||||
if self.code_apogee:
|
if self.code_apogee:
|
||||||
return {x.strip() for x in self.code_apogee.split(",") if x}
|
return {x.strip() for x in self.code_apogee.split(",") if x}
|
||||||
return set()
|
return set()
|
||||||
|
|
||||||
def get_edt_ids(self) -> list[str]:
|
def get_edt_id(self) -> str:
|
||||||
"les ids pour l'emploi du temps: à défaut, le 1er code Apogée"
|
"l'id pour l'emploi du temps: à défaut, le 1er code Apogée"
|
||||||
return [
|
return (
|
||||||
scu.normalize_edt_id(x)
|
self.edt_id
|
||||||
for x in scu.split_id(self.edt_id) or scu.split_id(self.code_apogee) or []
|
or (self.code_apogee.split(",")[0] if self.code_apogee else "")
|
||||||
]
|
or ""
|
||||||
|
)
|
||||||
|
|
||||||
def get_parcours(self) -> list[ApcParcours]:
|
def get_parcours(self) -> list[ApcParcours]:
|
||||||
"""Les parcours utilisant ce module.
|
"""Les parcours utilisant ce module.
|
||||||
@ -441,14 +306,6 @@ class Module(models.ScoDocModel):
|
|||||||
return []
|
return []
|
||||||
return self.parcours
|
return self.parcours
|
||||||
|
|
||||||
def add_tag(self, tag: "NotesTag"):
|
|
||||||
"""Add tag to module. Check if already has it."""
|
|
||||||
if tag.id in {t.id for t in self.tags}:
|
|
||||||
return
|
|
||||||
self.tags.append(tag)
|
|
||||||
db.session.add(self)
|
|
||||||
db.session.flush()
|
|
||||||
|
|
||||||
|
|
||||||
class ModuleUECoef(db.Model):
|
class ModuleUECoef(db.Model):
|
||||||
"""Coefficients des modules vers les UE (APC, BUT)
|
"""Coefficients des modules vers les UE (APC, BUT)
|
||||||
@ -511,19 +368,6 @@ class NotesTag(db.Model):
|
|||||||
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
|
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
|
||||||
title = db.Column(db.Text(), nullable=False)
|
title = db.Column(db.Text(), nullable=False)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_or_create(cls, title: str, dept_id: int | None = None) -> "NotesTag":
|
|
||||||
"""Get tag, or create it if it doesn't yet exists.
|
|
||||||
If dept_id unspecified, use current dept.
|
|
||||||
"""
|
|
||||||
dept_id = dept_id if dept_id is not None else g.scodoc_dept_id
|
|
||||||
tag = NotesTag.query.filter_by(dept_id=dept_id, title=title).first()
|
|
||||||
if tag is None:
|
|
||||||
tag = NotesTag(dept_id=dept_id, title=title)
|
|
||||||
db.session.add(tag)
|
|
||||||
db.session.flush()
|
|
||||||
return tag
|
|
||||||
|
|
||||||
|
|
||||||
# Association tag <-> module
|
# Association tag <-> module
|
||||||
notes_modules_tags = db.Table(
|
notes_modules_tags = db.Table(
|
||||||
|
@ -5,12 +5,11 @@
|
|||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from app import db
|
from app import db
|
||||||
from app import models
|
|
||||||
from app.scodoc import safehtml
|
from app.scodoc import safehtml
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
class BulAppreciations(models.ScoDocModel):
|
class BulAppreciations(db.Model):
|
||||||
"""Appréciations sur bulletins"""
|
"""Appréciations sur bulletins"""
|
||||||
|
|
||||||
__tablename__ = "notes_appreciations"
|
__tablename__ = "notes_appreciations"
|
||||||
@ -28,8 +27,6 @@ class BulAppreciations(models.ScoDocModel):
|
|||||||
author = db.Column(db.Text) # le pseudo (user_name), sans contrainte
|
author = db.Column(db.Text) # le pseudo (user_name), sans contrainte
|
||||||
comment = db.Column(db.Text) # texte libre
|
comment = db.Column(db.Text) # texte libre
|
||||||
|
|
||||||
_sco_dept_relations = ("Identite",) # accès au dept_id
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_appreciations_list(
|
def get_appreciations_list(
|
||||||
cls, formsemestre_id: int, etudid: int
|
cls, formsemestre_id: int, etudid: int
|
||||||
|
@ -3,10 +3,10 @@
|
|||||||
"""Model : preferences
|
"""Model : preferences
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from app import db, models
|
from app import db
|
||||||
|
|
||||||
|
|
||||||
class ScoPreference(models.ScoDocModel):
|
class ScoPreference(db.Model):
|
||||||
"""ScoDoc preferences (par département)"""
|
"""ScoDoc preferences (par département)"""
|
||||||
|
|
||||||
__tablename__ = "sco_prefs"
|
__tablename__ = "sco_prefs"
|
||||||
@ -19,8 +19,5 @@ class ScoPreference(models.ScoDocModel):
|
|||||||
value = db.Column(db.Text())
|
value = db.Column(db.Text())
|
||||||
formsemestre_id = db.Column(db.Integer, db.ForeignKey("notes_formsemestre.id"))
|
formsemestre_id = db.Column(db.Integer, db.ForeignKey("notes_formsemestre.id"))
|
||||||
|
|
||||||
_sco_dept_relations = ("FormSemestre",) # accès au dept_id
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"""<{self.__class__.__name__} {self.id} {self.departement.acronym
|
return f"<{self.__class__.__name__} {self.id} {self.departement.acronym} {self.name}={self.value}>"
|
||||||
} {self.name}={self.value}>"""
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user