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
|
||||
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
|
||||
|
||||
(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>
|
||||
|
||||
@ -23,7 +23,7 @@ Flask, SQLAlchemy, au lien de Python2/Zope dans les versions précédentes).
|
||||
|
||||
### Lignes de commandes
|
||||
|
||||
Voir [le guide de configuration](https://scodoc.org/GuideConfig).
|
||||
Voir [https://scodoc.org/GuideConfig](le guide de configuration).
|
||||
|
||||
## Organisation des fichiers
|
||||
|
||||
@ -41,41 +41,45 @@ Ils ne doivent pas être modifiés à la main, sauf certains fichiers de configu
|
||||
Le répertoire `/opt/scodoc-data` doit être régulièrement sauvegardé.
|
||||
|
||||
Principaux contenus:
|
||||
```
|
||||
/opt/scodoc-data
|
||||
/opt/scodoc-data/log # Fichiers de log ScoDoc
|
||||
/opt/scodoc-data/config # Fichiers de configuration
|
||||
.../config/logos # Logos de l'établissement
|
||||
.../config/depts # un fichier par département
|
||||
/opt/scodoc-data/photos # Photos des étudiants
|
||||
/opt/scodoc-data/archives # Archives: PV de jury, maquettes Apogée, fichiers étudiants
|
||||
```
|
||||
|
||||
/opt/scodoc-data
|
||||
/opt/scodoc-data/log # Fichiers de log ScoDoc
|
||||
/opt/scodoc-data/config # Fichiers de configuration
|
||||
.../config/logos # Logos de l'établissement
|
||||
.../config/depts # un fichier par département
|
||||
/opt/scodoc-data/photos # Photos des étudiants
|
||||
/opt/scodoc-data/archives # Archives: PV de jury, maquettes Apogée, fichiers étudiants
|
||||
|
||||
## Pour les développeurs
|
||||
|
||||
### Installation du code
|
||||
|
||||
Installer ScoDoc 9 normalement ([voir la doc](https://scodoc.org/GuideInstallDebian12)).
|
||||
Installer ScoDoc 9 normalement ([voir la doc](https://scodoc.org/GuideInstallDebian11)).
|
||||
|
||||
Puis remplacer `/opt/scodoc` par un clone du git.
|
||||
```bash
|
||||
sudo su
|
||||
mv /opt/scodoc /opt/off-scodoc # ou ce que vous voulez
|
||||
apt-get install git # si besoin
|
||||
git clone https://scodoc.org/git/ScoDoc/ScoDoc.git /opt/scodoc
|
||||
# (ou bien utiliser votre clone gitea si vous l'avez déjà créé !)
|
||||
|
||||
# Donner ce répertoire à l'utilisateur scodoc:
|
||||
chown -R scodoc:scodoc /opt/scodoc
|
||||
```
|
||||
sudo su
|
||||
mv /opt/scodoc /opt/off-scodoc # ou ce que vous voulez
|
||||
apt-get install git # si besoin
|
||||
cd /opt
|
||||
git clone https://scodoc.org/git/viennet/ScoDoc.git
|
||||
# (ou bien utiliser votre clone gitea si vous l'avez déjà créé !)
|
||||
|
||||
# Renommer le répertoire:
|
||||
mv ScoDoc scodoc
|
||||
|
||||
# Et donner ce répertoire à l'utilisateur scodoc:
|
||||
chown -R scodoc.scodoc /opt/scodoc
|
||||
|
||||
Il faut ensuite installer l'environnement et le fichier de configuration:
|
||||
```bash
|
||||
# Le plus simple est de piquer le virtualenv configuré par l'installeur:
|
||||
mv /opt/off-scodoc/venv /opt/scodoc
|
||||
```
|
||||
|
||||
# Le plus simple est de piquer le virtualenv configuré par l'installeur:
|
||||
mv /opt/off-scodoc/venv /opt/scodoc
|
||||
|
||||
Et la config:
|
||||
```bash
|
||||
ln -s /opt/scodoc-data/.env /opt/scodoc
|
||||
```
|
||||
|
||||
ln -s /opt/scodoc-data/.env /opt/scodoc
|
||||
|
||||
Cette dernière commande utilise le `.env` crée lors de l'install, ce qui
|
||||
n'est pas toujours le plus judicieux: vous pouvez modifier son contenu, par
|
||||
exemple pour travailler en mode "développement" avec `FLASK_ENV=development`.
|
||||
@ -84,11 +88,11 @@ exemple pour travailler en mode "développement" avec `FLASK_ENV=development`.
|
||||
|
||||
Les tests unitaires utilisent normalement la base postgresql `SCODOC_TEST`.
|
||||
Avant le premier lancement, créer cette base ainsi:
|
||||
```bash
|
||||
./tools/create_database.sh SCODOC_TEST
|
||||
export FLASK_ENV=test
|
||||
flask db upgrade
|
||||
```
|
||||
|
||||
./tools/create_database.sh SCODOC_TEST
|
||||
export FLASK_ENV=test
|
||||
flask db upgrade
|
||||
|
||||
Cette commande n'est nécessaire que la première fois (le contenu de la base
|
||||
est effacé au début de chaque test, mais son schéma reste) et aussi si des
|
||||
migrations (changements de schéma) ont eu lieu dans le code.
|
||||
@ -96,17 +100,17 @@ migrations (changements de schéma) ont eu lieu dans le code.
|
||||
Certains tests ont besoin d'un département déjà créé, qui n'est pas créé par les
|
||||
scripts de tests:
|
||||
Lancer au préalable:
|
||||
```bash
|
||||
flask delete-dept -fy TEST00 && flask create-dept TEST00
|
||||
```
|
||||
|
||||
flask delete-dept -fy TEST00 && flask create-dept TEST00
|
||||
|
||||
Puis dérouler les tests unitaires:
|
||||
```bash
|
||||
pytest tests/unit
|
||||
```
|
||||
|
||||
pytest tests/unit
|
||||
|
||||
Ou avec couverture (`pip install pytest-cov`)
|
||||
```bash
|
||||
pytest --cov=app --cov-report=term-missing --cov-branch tests/unit/*
|
||||
```
|
||||
|
||||
pytest --cov=app --cov-report=term-missing --cov-branch tests/unit/*
|
||||
|
||||
#### Utilisation des tests unitaires pour initialiser la base de dev
|
||||
|
||||
On peut aussi utiliser les tests unitaires pour mettre la base de données de
|
||||
@ -115,43 +119,43 @@ développement dans un état connu, par exemple pour éviter de recréer à la m
|
||||
|
||||
Il suffit de positionner une variable d'environnement indiquant la BD utilisée
|
||||
par les tests:
|
||||
```bash
|
||||
export SCODOC_TEST_DATABASE_URI=postgresql:///SCODOC_DEV
|
||||
```
|
||||
|
||||
export SCODOC_TEST_DATABASE_URI=postgresql:///SCODOC_DEV
|
||||
|
||||
(si elle n'existe pas, voir plus loin pour la créer) puis de les lancer
|
||||
normalement, par exemple:
|
||||
```bash
|
||||
pytest tests/unit/test_sco_basic.py
|
||||
```
|
||||
|
||||
pytest tests/unit/test_sco_basic.py
|
||||
|
||||
Il est en général nécessaire d'affecter ensuite un mot de passe à (au moins) un
|
||||
utilisateur:
|
||||
```bash
|
||||
flask user-password admin
|
||||
```
|
||||
|
||||
flask user-password admin
|
||||
|
||||
**Attention:** les tests unitaires **effacent** complètement le contenu de la
|
||||
base de données (tous les départements, et les utilisateurs) avant de commencer !
|
||||
|
||||
#### Modification du schéma de la base
|
||||
|
||||
On utilise SQLAlchemy avec Alembic et Flask-Migrate.
|
||||
```bash
|
||||
flask db migrate -m "message explicatif....."
|
||||
flask db upgrade
|
||||
```
|
||||
|
||||
flask db migrate -m "message explicatif....."
|
||||
flask db upgrade
|
||||
|
||||
Ne pas oublier de d'ajouter le script de migration à git (`git add migrations/...`).
|
||||
|
||||
**Mémo**: séquence re-création d'une base (vérifiez votre `.env`
|
||||
ou variables d'environnement pour interroger la bonne base !).
|
||||
```bash
|
||||
dropdb SCODOC_DEV
|
||||
tools/create_database.sh SCODOC_DEV # créé base SQL
|
||||
flask db upgrade # créé les tables à partir des migrations
|
||||
flask sco-db-init # ajoute au besoin les constantes (fait en migration 0)
|
||||
|
||||
# puis imports:
|
||||
flask import-scodoc7-users
|
||||
flask import-scodoc7-dept STID SCOSTID
|
||||
```
|
||||
dropdb SCODOC_DEV
|
||||
tools/create_database.sh SCODOC_DEV # créé base SQL
|
||||
flask db upgrade # créé les tables à partir des migrations
|
||||
flask sco-db-init # ajoute au besoin les constantes (fait en migration 0)
|
||||
|
||||
# puis imports:
|
||||
flask import-scodoc7-users
|
||||
flask import-scodoc7-dept STID SCOSTID
|
||||
|
||||
Si la base utilisée pour les dev n'est plus en phase avec les scripts de
|
||||
migration, utiliser les commandes `flask db history`et `flask db stamp`pour se
|
||||
positionner à la bonne étape.
|
||||
@ -159,23 +163,23 @@ positionner à la bonne étape.
|
||||
### Profiling
|
||||
|
||||
Sur une machine de DEV, lancer
|
||||
```bash
|
||||
flask profile --host 0.0.0.0 --length 32 --profile-dir /opt/scodoc-data
|
||||
```
|
||||
|
||||
flask profile --host 0.0.0.0 --length 32 --profile-dir /opt/scodoc-data
|
||||
|
||||
le fichier `.prof` sera alors écrit dans `/opt/scodoc-data` (on peut aussi utiliser `/tmp`).
|
||||
|
||||
Pour la visualisation, [snakeviz](https://jiffyclub.github.io/snakeviz/) est bien:
|
||||
```bash
|
||||
pip install snakeviz
|
||||
```
|
||||
|
||||
pip install snakeviz
|
||||
|
||||
puis
|
||||
```bash
|
||||
snakeviz -s --hostname 0.0.0.0 -p 5555 /opt/scodoc-data/GET.ScoDoc......prof
|
||||
```
|
||||
|
||||
snakeviz -s --hostname 0.0.0.0 -p 5555 /opt/scodoc-data/GET.ScoDoc......prof
|
||||
|
||||
## Paquet Debian 12
|
||||
|
||||
Les scripts associés au paquet Debian (.deb) sont dans `tools/debian`. Le plus
|
||||
important est `postinst` qui se charge de configurer le système (install ou
|
||||
important est `postinst`qui se charge de configurer le système (install ou
|
||||
upgrade de scodoc9).
|
||||
|
||||
La préparation d'une release se fait à l'aide du script
|
||||
|
@ -19,7 +19,11 @@ from flask import current_app, g, request
|
||||
from flask import Flask
|
||||
from flask import abort, flash, has_request_context
|
||||
from flask import render_template
|
||||
|
||||
# from flask.json import JSONEncoder
|
||||
from flask.logging import default_handler
|
||||
|
||||
from flask_bootstrap import Bootstrap
|
||||
from flask_caching import Cache
|
||||
from flask_json import FlaskJSON, json_response
|
||||
from flask_login import LoginManager, current_user
|
||||
@ -30,7 +34,6 @@ from flask_sqlalchemy import SQLAlchemy
|
||||
from jinja2 import select_autoescape
|
||||
import sqlalchemy as sa
|
||||
import werkzeug.debug
|
||||
from wtforms.fields import HiddenField
|
||||
|
||||
from flask_cas import CAS
|
||||
|
||||
@ -56,6 +59,8 @@ login.login_view = "auth.login"
|
||||
login.login_message = "Identifiez-vous pour accéder à cette page."
|
||||
|
||||
mail = Mail()
|
||||
bootstrap = Bootstrap()
|
||||
# moment = Moment()
|
||||
|
||||
CACHE_TYPE = os.environ.get("CACHE_TYPE")
|
||||
cache = Cache(
|
||||
@ -86,9 +91,8 @@ def handle_invalid_csrf(exc):
|
||||
return render_template("error_csrf.j2", exc=exc), 404
|
||||
|
||||
|
||||
# def handle_pdf_format_error(exc):
|
||||
# return "ay ay ay"
|
||||
handle_pdf_format_error = handle_sco_value_error
|
||||
def handle_pdf_format_error(exc):
|
||||
return "ay ay ay"
|
||||
|
||||
|
||||
def internal_server_error(exc):
|
||||
@ -300,6 +304,7 @@ def create_app(config_class=DevConfig):
|
||||
login.init_app(app)
|
||||
mail.init_app(app)
|
||||
app.extensions["mail"].debug = 0 # disable copy of mails to stderr
|
||||
bootstrap.init_app(app)
|
||||
cache.init_app(app)
|
||||
sco_cache.CACHE = cache
|
||||
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_web_bp
|
||||
|
||||
# Jinja2 configuration
|
||||
# Enable autoescaping of all templates, including .j2
|
||||
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
|
||||
app.register_blueprint(scodoc_bp)
|
||||
@ -551,8 +549,8 @@ def truncate_database():
|
||||
CREATE OR REPLACE FUNCTION reset_sequences(username IN VARCHAR) RETURNS void AS $$
|
||||
DECLARE
|
||||
statements CURSOR FOR
|
||||
SELECT sequence_name
|
||||
FROM information_schema.sequences
|
||||
SELECT sequence_name
|
||||
FROM information_schema.sequences
|
||||
ORDER BY sequence_name ;
|
||||
BEGIN
|
||||
FOR stmt IN statements LOOP
|
||||
@ -637,12 +635,14 @@ def critical_error(msg):
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
log(f"\n*** CRITICAL ERROR: {msg}")
|
||||
subject = f"CRITICAL ERROR: {msg}".strip()[:68]
|
||||
send_scodoc_alarm(subject, msg)
|
||||
send_scodoc_alarm(f"CRITICAL ERROR: {msg}", msg)
|
||||
clear_scodoc_cache()
|
||||
raise ScoValueError(
|
||||
f"""
|
||||
Une erreur est survenue, veuillez ré-essayer.
|
||||
Une erreur est survenue.
|
||||
|
||||
Si le problème persiste, merci de contacter le support ScoDoc via
|
||||
{scu.SCO_DISCORD_ASSISTANCE}
|
||||
|
||||
{msg}
|
||||
"""
|
||||
|
@ -1,17 +1,11 @@
|
||||
"""api.__init__
|
||||
"""
|
||||
|
||||
from functools import wraps
|
||||
|
||||
from flask_json import as_json
|
||||
from flask import Blueprint
|
||||
from flask import current_app, g, request
|
||||
from flask_login import current_user
|
||||
from flask import request, g
|
||||
from app import db
|
||||
from app.decorators import permission_required
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoException
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
api_bp = Blueprint("api", __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
|
||||
|
||||
|
||||
def api_permission_required(permission):
|
||||
"""Ce décorateur fait la même chose que @permission_required
|
||||
mais enregistre dans l'attribut .scodoc_permission
|
||||
de la fonction la valeur de la permission.
|
||||
Cette valeur n'est utilisée que pour la génération automatique de la documentation.
|
||||
"""
|
||||
|
||||
def decorator(f):
|
||||
f.scodoc_permission = permission
|
||||
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
scodoc_dept = getattr(g, "scodoc_dept", None)
|
||||
if not current_user.has_permission(permission, scodoc_dept):
|
||||
return current_app.login_manager.unauthorized()
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
@api_bp.errorhandler(ScoException)
|
||||
@api_web_bp.errorhandler(ScoException)
|
||||
@api_bp.errorhandler(404)
|
||||
@ -76,35 +48,20 @@ def requested_format(default_format="json", allowed_formats=None):
|
||||
|
||||
|
||||
@as_json
|
||||
def get_model_api_object(
|
||||
model_cls: db.Model,
|
||||
model_id: int,
|
||||
join_cls: db.Model = None,
|
||||
restrict: bool | None = None,
|
||||
):
|
||||
def get_model_api_object(model_cls: db.Model, model_id: int, join_cls: db.Model = None):
|
||||
"""
|
||||
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
|
||||
|
||||
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)
|
||||
if g.scodoc_dept and join_cls is not None:
|
||||
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 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)
|
||||
return unique.to_dict(format_api=True)
|
||||
|
||||
|
||||
from app.api import tokens
|
||||
|
@ -1,34 +1,30 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
"""ScoDoc 9 API : Assiduités"""
|
||||
|
||||
"""ScoDoc 9 API : Assiduités
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from flask import g, request
|
||||
from flask_json import as_json
|
||||
from flask_login import current_user, login_required
|
||||
from flask_sqlalchemy.query import Query
|
||||
from sqlalchemy.orm.exc import ObjectDeletedError
|
||||
from werkzeug.exceptions import HTTPException
|
||||
|
||||
from app import db, log, set_sco_dept
|
||||
import app.scodoc.sco_assiduites as scass
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.api import api_bp as bp
|
||||
from app.api import api_web_bp, get_model_api_object, tools
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.decorators import scodoc
|
||||
from app.decorators import permission_required, scodoc
|
||||
from app.models import (
|
||||
Assiduite,
|
||||
Evaluation,
|
||||
FormSemestre,
|
||||
Identite,
|
||||
ModuleImpl,
|
||||
Scolog,
|
||||
)
|
||||
from flask_sqlalchemy.query import Query
|
||||
from app.models.assiduites import (
|
||||
get_assiduites_justif,
|
||||
get_justifs_from_date,
|
||||
@ -43,13 +39,10 @@ from app.scodoc.sco_utils import json_error
|
||||
@api_web_bp.route("/assiduite/<int:assiduite_id>")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def assiduite(assiduite_id: int = None):
|
||||
"""Retourne un objet assiduité à partir de son id
|
||||
|
||||
Exemple de résultat:
|
||||
|
||||
```json
|
||||
{
|
||||
"assiduite_id": 1,
|
||||
"etudid": 2,
|
||||
@ -58,17 +51,11 @@ def assiduite(assiduite_id: int = None):
|
||||
"date_fin": "2022-10-31T10:00+01:00",
|
||||
"etat": "retard",
|
||||
"desc": "une description",
|
||||
"user_id": 1 or null,
|
||||
"user_name" : login scodoc or null,
|
||||
"user_nom_complet": "Marie Dupont",
|
||||
"user_id: 1 or null,
|
||||
"user_name" : login scodoc or null
|
||||
"user_nom_complet": "Marie Dupont"
|
||||
"est_just": False or True,
|
||||
}
|
||||
```
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduite/1;
|
||||
|
||||
"""
|
||||
|
||||
return get_model_api_object(Assiduite, assiduite_id, Identite)
|
||||
@ -86,26 +73,18 @@ def assiduite(assiduite_id: int = None):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def assiduite_justificatifs(assiduite_id: int = None, long: bool = False):
|
||||
"""Retourne la liste des justificatifs qui justifient cette assiduité.
|
||||
"""Retourne la liste des justificatifs qui justifie cette assiduitée
|
||||
|
||||
Exemple de résultat:
|
||||
|
||||
```json
|
||||
[
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
...
|
||||
]
|
||||
```
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduite/1/justificatifs;
|
||||
/assiduite/1/justificatifs/long;
|
||||
|
||||
"""
|
||||
|
||||
return get_assiduites_justif(assiduite_id, long)
|
||||
return get_assiduites_justif(assiduite_id, True)
|
||||
|
||||
|
||||
# etudid
|
||||
@ -136,42 +115,52 @@ def assiduite_justificatifs(assiduite_id: int = None, long: bool = False):
|
||||
@scodoc
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
def assiduites_count(
|
||||
def count_assiduites(
|
||||
etudid: int = None, nip: str = None, ine: str = None, with_query: bool = False
|
||||
):
|
||||
"""
|
||||
Retourne le nombre d'assiduités d'un étudiant.
|
||||
Retourne le nombre d'assiduités d'un étudiant
|
||||
chemin : /assiduites/<int:etudid>/count
|
||||
|
||||
Un filtrage peut être donné avec une query
|
||||
chemin : /assiduites/<int:etudid>/count/query?
|
||||
|
||||
Les différents filtres :
|
||||
Type (type de comptage -> journee, demi, heure, nombre d'assiduite):
|
||||
query?type=(journee, demi, heure) -> une seule valeur parmis les trois
|
||||
ex: .../query?type=heure
|
||||
Comportement par défaut : compte le nombre d'assiduité enregistrée
|
||||
|
||||
Etat (etat de l'étudiant -> absent, present ou retard):
|
||||
query?etat=[- liste des états séparé par une virgule -]
|
||||
ex: .../query?etat=present,retard
|
||||
Date debut
|
||||
(date de début de l'assiduité, sont affichés les assiduités
|
||||
dont la date de début est supérieur ou égale à la valeur donnée):
|
||||
query?date_debut=[- date au format iso -]
|
||||
ex: query?date_debut=2022-11-03T08:00+01:00
|
||||
Date fin
|
||||
(date de fin de l'assiduité, sont affichés les assiduités
|
||||
dont la date de fin est inférieure ou égale à la valeur donnée):
|
||||
query?date_fin=[- date au format iso -]
|
||||
ex: query?date_fin=2022-11-03T10:00+01:00
|
||||
Moduleimpl_id (l'id du module concerné par l'assiduité):
|
||||
query?moduleimpl_id=[- int ou vide -]
|
||||
ex: query?moduleimpl_id=1234
|
||||
query?moduleimpl_od=
|
||||
Formsemstre_id (l'id du formsemestre concerné par l'assiduité)
|
||||
query?formsemestre_id=[int]
|
||||
ex query?formsemestre_id=3
|
||||
user_id (l'id de l'auteur de l'assiduité)
|
||||
query?user_id=[int]
|
||||
ex query?user_id=3
|
||||
est_just (si l'assiduité est justifié (fait aussi filtre par abs/retard))
|
||||
query?est_just=[bool]
|
||||
query?est_just=f
|
||||
query?est_just=t
|
||||
|
||||
QUERY
|
||||
-----
|
||||
user_id:<int:user_id>
|
||||
est_just:<bool:est_just>
|
||||
moduleimpl_id:<int:moduleimpl_id>
|
||||
date_debut:<string:date_debut_iso>
|
||||
date_fin:<string:date_fin_iso>
|
||||
etat:<array[string]:etat>
|
||||
formsemestre_id:<int:formsemestre_id>
|
||||
metric:<array[string]:metric>
|
||||
split:<bool:split>
|
||||
|
||||
PARAMS
|
||||
-----
|
||||
user_id:l'id de l'auteur de l'assiduité
|
||||
est_just:si l'assiduité est justifiée (fait aussi filtre abs/retard)
|
||||
moduleimpl_id:l'id du module concerné par l'assiduité
|
||||
date_debut:date de début de l'assiduité (supérieur ou égal)
|
||||
date_fin:date de fin de l'assiduité (inférieur ou égal)
|
||||
etat:etat de l'étudiant → absent, present ou retard
|
||||
formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité
|
||||
metric: la/les métriques de comptage (journee, demi, heure, compte)
|
||||
split: divise le comptage par état
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduites/1/count;
|
||||
/assiduites/1/count/query?etat=retard;
|
||||
/assiduites/1/count/query?split;
|
||||
/assiduites/1/count/query?etat=present,retard&metric=compte,heure;
|
||||
|
||||
"""
|
||||
|
||||
@ -183,7 +172,6 @@ def assiduites_count(
|
||||
404,
|
||||
message="étudiant inconnu",
|
||||
)
|
||||
set_sco_dept(etud.departement.acronym)
|
||||
|
||||
# Les filtres qui seront appliqués au comptage (type, date, etudid...)
|
||||
filtered: dict[str, object] = {}
|
||||
@ -228,35 +216,40 @@ def assiduites_count(
|
||||
def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False):
|
||||
"""
|
||||
Retourne toutes les assiduités d'un étudiant
|
||||
chemin : /assiduites/<int:etudid>
|
||||
|
||||
QUERY
|
||||
-----
|
||||
user_id:<int:user_id>
|
||||
est_just:<bool:est_just>
|
||||
moduleimpl_id:<int:moduleimpl_id>
|
||||
date_debut:<string:date_debut_iso>
|
||||
date_fin:<string:date_fin_iso>
|
||||
etat:<array[string]:etat>
|
||||
formsemestre_id:<int:formsemestre_id>
|
||||
with_justifs:<bool:with_justifs>
|
||||
Un filtrage peut être donné avec une query
|
||||
chemin : /assiduites/<int:etudid>/query?
|
||||
|
||||
PARAMS
|
||||
-----
|
||||
user_id:l'id de l'auteur de l'assiduité
|
||||
est_just:si l'assiduité est justifiée (fait aussi filtre abs/retard)
|
||||
moduleimpl_id:l'id du module concerné par l'assiduité
|
||||
date_debut:date de début de l'assiduité (supérieur ou égal)
|
||||
date_fin:date de fin de l'assiduité (inférieur ou égal)
|
||||
etat:etat de l'étudiant → absent, present ou retard
|
||||
formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité
|
||||
with_justif:ajoute les justificatifs liés à l'assiduité
|
||||
Les différents filtres :
|
||||
Etat (etat de l'étudiant -> absent, present ou retard):
|
||||
query?etat=[- liste des états séparé par une virgule -]
|
||||
ex: .../query?etat=present,retard
|
||||
Date debut
|
||||
(date de début de l'assiduité, sont affichés les assiduités
|
||||
dont la date de début est supérieur ou égale à la valeur donnée):
|
||||
query?date_debut=[- date au format iso -]
|
||||
ex: query?date_debut=2022-11-03T08:00+01:00
|
||||
Date fin
|
||||
(date de fin de l'assiduité, sont affichés les assiduités
|
||||
dont la date de fin est inférieure ou égale à la valeur donnée):
|
||||
query?date_fin=[- date au format iso -]
|
||||
ex: query?date_fin=2022-11-03T10:00+01:00
|
||||
Moduleimpl_id (l'id du module concerné par l'assiduité):
|
||||
query?moduleimpl_id=[- int ou vide -]
|
||||
ex: query?moduleimpl_id=1234
|
||||
query?moduleimpl_od=
|
||||
Formsemstre_id (l'id du formsemestre concerné par l'assiduité)
|
||||
query?formsemstre_id=[int]
|
||||
ex query?formsemestre_id=3
|
||||
user_id (l'id de l'auteur de l'assiduité)
|
||||
query?user_id=[int]
|
||||
ex query?user_id=3
|
||||
est_just (si l'assiduité est justifié (fait aussi filtre par abs/retard))
|
||||
query?est_just=[bool]
|
||||
query?est_just=f
|
||||
query?est_just=t
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduites/1;
|
||||
/assiduites/1/query?etat=retard;
|
||||
/assiduites/1/query?moduleimpl_id=1;
|
||||
/assiduites/1/query?with_justifs=;
|
||||
|
||||
"""
|
||||
|
||||
@ -268,7 +261,6 @@ def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False)
|
||||
404,
|
||||
message="étudiant inconnu",
|
||||
)
|
||||
|
||||
# Récupération des assiduités de l'étudiant
|
||||
assiduites_query: Query = etud.assiduites
|
||||
|
||||
@ -291,108 +283,6 @@ def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False)
|
||||
return data_set
|
||||
|
||||
|
||||
@bp.route("/assiduites/<int:etudid>/evaluations")
|
||||
@api_web_bp.route("/assiduites/<int:etudid>/evaluations")
|
||||
# etudid
|
||||
@bp.route("/assiduites/etudid/<int:etudid>/evaluations")
|
||||
@api_web_bp.route("/assiduites/etudid/<int:etudid>/evaluations")
|
||||
# ine
|
||||
@bp.route("/assiduites/ine/<ine>/evaluations")
|
||||
@api_web_bp.route("/assiduites/ine/<ine>/evaluations")
|
||||
# nip
|
||||
@bp.route("/assiduites/nip/<nip>/evaluations")
|
||||
@api_web_bp.route("/assiduites/nip/<nip>/evaluations")
|
||||
@login_required
|
||||
@scodoc
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
def assiduites_evaluations(etudid: int = None, nip=None, ine=None):
|
||||
"""
|
||||
Retourne la liste de toutes les évaluations de l'étudiant
|
||||
Pour chaque évaluation, retourne la liste des objets assiduités
|
||||
sur la plage de l'évaluation
|
||||
|
||||
Exemple de résultat:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"evaluation_id": 1234,
|
||||
"assiduites": [
|
||||
{
|
||||
"assiduite_id":1234,
|
||||
...
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduites/1/evaluations;
|
||||
|
||||
```
|
||||
|
||||
"""
|
||||
# Récupération de l'étudiant
|
||||
etud: Identite = tools.get_etud(etudid, nip, ine)
|
||||
if etud is None:
|
||||
return json_error(
|
||||
404,
|
||||
message="étudiant inconnu",
|
||||
)
|
||||
|
||||
# Récupération des évaluations et des assidiutés
|
||||
etud_evaluations_assiduites: list[dict] = scass.get_etud_evaluations_assiduites(
|
||||
etud
|
||||
)
|
||||
|
||||
return etud_evaluations_assiduites
|
||||
|
||||
|
||||
@api_web_bp.route("/evaluation/<int:evaluation_id>/assiduites")
|
||||
@bp.route("/evaluation/<int:evaluation_id>/assiduites")
|
||||
@login_required
|
||||
@scodoc
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
def evaluation_assiduites(evaluation_id):
|
||||
"""
|
||||
Retourne les objets assiduités de chaque étudiant sur la plage de l'évaluation
|
||||
|
||||
Exemple de résultat:
|
||||
|
||||
```json
|
||||
{
|
||||
"<etudid>" : [
|
||||
{
|
||||
"assiduite_id":1234,
|
||||
...
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
Évaluations
|
||||
"""
|
||||
# Récupération de l'évaluation
|
||||
try:
|
||||
evaluation: Evaluation = Evaluation.get_evaluation(evaluation_id)
|
||||
except HTTPException:
|
||||
return json_error(404, "L'évaluation n'existe pas")
|
||||
|
||||
evaluation_assiduites_par_etudid: dict[int, list[Assiduite]] = {}
|
||||
for assi in scass.get_evaluation_assiduites(evaluation):
|
||||
etudid: str = str(assi.etudid)
|
||||
etud_assiduites = evaluation_assiduites_par_etudid.get(etudid, [])
|
||||
etud_assiduites.append(assi.to_dict(format_api=True))
|
||||
evaluation_assiduites_par_etudid[etudid] = etud_assiduites
|
||||
|
||||
return evaluation_assiduites_par_etudid
|
||||
|
||||
|
||||
@bp.route("/assiduites/group/query", defaults={"with_query": True})
|
||||
@api_web_bp.route("/assiduites/group/query", defaults={"with_query": True})
|
||||
@login_required
|
||||
@ -404,34 +294,38 @@ def assiduites_group(with_query: bool = False):
|
||||
Retourne toutes les assiduités d'un groupe d'étudiants
|
||||
chemin : /assiduites/group/query?etudids=1,2,3
|
||||
|
||||
Un filtrage peut être donné avec une query
|
||||
chemin : /assiduites/group/query?etudids=1,2,3
|
||||
|
||||
QUERY
|
||||
-----
|
||||
user_id:<int:user_id>
|
||||
est_just:<bool:est_just>
|
||||
moduleimpl_id:<int:moduleimpl_id>
|
||||
date_debut:<string:date_debut_iso>
|
||||
date_fin:<string:date_fin_iso>
|
||||
etat:<array[string]:etat>
|
||||
etudids:<array[int]:etudids>
|
||||
formsemestre_id:<int:formsemestre_id>
|
||||
with_justif:<bool:with_justif>
|
||||
Les différents filtres :
|
||||
Etat (etat de l'étudiant -> absent, present ou retard):
|
||||
query?etat=[- liste des états séparé par une virgule -]
|
||||
ex: .../query?etat=present,retard
|
||||
Date debut
|
||||
(date de début de l'assiduité, sont affichés les assiduités
|
||||
dont la date de début est supérieur ou égale à la valeur donnée):
|
||||
query?date_debut=[- date au format iso -]
|
||||
ex: query?date_debut=2022-11-03T08:00+01:00
|
||||
Date fin
|
||||
(date de fin de l'assiduité, sont affichés les assiduités
|
||||
dont la date de fin est inférieure ou égale à la valeur donnée):
|
||||
query?date_fin=[- date au format iso -]
|
||||
ex: query?date_fin=2022-11-03T10:00+01:00
|
||||
Moduleimpl_id (l'id du module concerné par l'assiduité):
|
||||
query?moduleimpl_id=[- int ou vide -]
|
||||
ex: query?moduleimpl_id=1234
|
||||
query?moduleimpl_od=
|
||||
Formsemstre_id (l'id du formsemestre concerné par l'assiduité)
|
||||
query?formsemstre_id=[int]
|
||||
ex query?formsemestre_id=3
|
||||
user_id (l'id de l'auteur de l'assiduité)
|
||||
query?user_id=[int]
|
||||
ex query?user_id=3
|
||||
est_just (si l'assiduité est justifié (fait aussi filtre par abs/retard))
|
||||
query?est_just=[bool]
|
||||
query?est_just=f
|
||||
query?est_just=t
|
||||
|
||||
PARAMS
|
||||
-----
|
||||
user_id:l'id de l'auteur de l'assiduité
|
||||
est_just:si l'assiduité est justifiée (fait aussi filtre abs/retard)
|
||||
moduleimpl_id:l'id du module concerné par l'assiduité
|
||||
date_debut:date de début de l'assiduité (supérieur ou égal)
|
||||
date_fin:date de fin de l'assiduité (inférieur ou égal)
|
||||
etat:etat de l'étudiant → absent, present ou retard
|
||||
etudids:liste des ids des étudiants concernés par la recherche
|
||||
formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité
|
||||
with_justifs:ajoute les justificatifs liés à l'assiduité
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduites/group/query?etudids=1,2,3;
|
||||
|
||||
"""
|
||||
|
||||
@ -441,7 +335,7 @@ def assiduites_group(with_query: bool = False):
|
||||
try:
|
||||
etuds = [int(etu) for etu in etuds]
|
||||
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
|
||||
query = Identite.query.filter(Identite.id.in_(etuds))
|
||||
@ -491,34 +385,7 @@ def assiduites_group(with_query: bool = False):
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
|
||||
"""Retourne toutes les assiduités du formsemestre
|
||||
|
||||
QUERY
|
||||
-----
|
||||
user_id:<int:user_id>
|
||||
est_just:<bool:est_just>
|
||||
moduleimpl_id:<int:moduleimpl_id>
|
||||
date_debut:<string:date_debut_iso>
|
||||
date_fin:<string:date_fin_iso>
|
||||
etat:<array[string]:etat>
|
||||
|
||||
PARAMS
|
||||
-----
|
||||
user_id:l'id de l'auteur de l'assiduité
|
||||
est_just:si l'assiduité est justifiée (fait aussi filtre abs/retard)
|
||||
moduleimpl_id:l'id du module concerné par l'assiduité
|
||||
date_debut:date de début de l'assiduité (supérieur ou égal)
|
||||
date_fin:date de fin de l'assiduité (inférieur ou égal)
|
||||
etat:etat de l'étudiant → absent, present ou retard
|
||||
formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduites/formsemestre/1;
|
||||
/assiduites/formsemestre/1/query?etat=retard;
|
||||
/assiduites/formsemestre/1/query?moduleimpl_id=1;
|
||||
|
||||
"""
|
||||
"""Retourne toutes les assiduités du formsemestre"""
|
||||
|
||||
# Récupération du formsemestre à partir du formsemestre_id
|
||||
formsemestre: FormSemestre = None
|
||||
@ -565,42 +432,10 @@ def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
|
||||
@scodoc
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
def assiduites_formsemestre_count(
|
||||
def count_assiduites_formsemestre(
|
||||
formsemestre_id: int = None, with_query: bool = False
|
||||
):
|
||||
"""Comptage des assiduités du formsemestre
|
||||
|
||||
QUERY
|
||||
-----
|
||||
user_id:<int:user_id>
|
||||
est_just:<bool:est_just>
|
||||
moduleimpl_id:<int:moduleimpl_id>
|
||||
date_debut:<string:date_debut_iso>
|
||||
date_fin:<string:date_fin_iso>
|
||||
etat:<array[string]:etat>
|
||||
formsemestre_id:<int:formsemestre_id>
|
||||
metric:<array[string]:metric>
|
||||
split:<bool:split>
|
||||
|
||||
PARAMS
|
||||
-----
|
||||
user_id:l'id de l'auteur de l'assiduité
|
||||
est_just:si l'assiduité est justifiée (fait aussi filtre abs/retard)
|
||||
moduleimpl_id:l'id du module concerné par l'assiduité
|
||||
date_debut:date de début de l'assiduité (supérieur ou égal)
|
||||
date_fin:date de fin de l'assiduité (inférieur ou égal)
|
||||
etat:etat de l'étudiant → absent, present ou retard
|
||||
formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité
|
||||
metric: la/les métriques de comptage (journee, demi, heure, compte)
|
||||
split: divise le comptage par état
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduites/formsemestre/1/count;
|
||||
/assiduites/formsemestre/1/count/query?etat=retard;
|
||||
/assiduites/formsemestre/1/count/query?etat=present,retard&metric=compte,heure;
|
||||
|
||||
"""
|
||||
"""Comptage des assiduités du formsemestre"""
|
||||
|
||||
# Récupération du formsemestre à partir du formsemestre_id
|
||||
formsemestre: FormSemestre = None
|
||||
@ -609,8 +444,6 @@ def assiduites_formsemestre_count(
|
||||
if formsemestre is None:
|
||||
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
|
||||
etuds = formsemestre.etuds.all()
|
||||
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):
|
||||
"""
|
||||
Enregistrement d'assiduités pour un étudiant (etudid)
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
La requête doit avoir un content type "application/json":
|
||||
[
|
||||
{
|
||||
"date_debut": str,
|
||||
@ -670,12 +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
|
||||
@ -729,10 +553,7 @@ def assiduite_create(etudid: int = None, nip=None, ine=None):
|
||||
def assiduites_create():
|
||||
"""
|
||||
Création d'une assiduité ou plusieurs assiduites
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
La requête doit avoir un content type "application/json":
|
||||
[
|
||||
{
|
||||
"date_debut": str,
|
||||
@ -745,17 +566,12 @@ def assiduites_create():
|
||||
"date_fin": str,
|
||||
"etat": str,
|
||||
"etudid":int,
|
||||
|
||||
"moduleimpl_id": int,
|
||||
"desc":str,
|
||||
}
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduites/create;[{""etudid"":1,""date_debut"": ""2023-10-26T08:00"",""date_fin"": ""2023-10-26T10:00"",""etat"": ""absent""}]
|
||||
/assiduites/create;[{""etudid"":-1,""date_debut"": ""2023-10-26T08:00"",""date_fin"": ""2023-10-26T10:00"",""etat"": ""absent""}]
|
||||
|
||||
"""
|
||||
|
||||
@ -789,9 +605,9 @@ def _create_one(
|
||||
etud: Identite,
|
||||
) -> 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.
|
||||
|
||||
@ -926,18 +742,13 @@ def assiduite_delete():
|
||||
"""
|
||||
Suppression d'une assiduité à partir de son id
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
Forme des données envoyées :
|
||||
|
||||
[
|
||||
<assiduite_id:int>,
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduite/delete;[2,2,3]
|
||||
|
||||
"""
|
||||
# Récupération des ids envoyés dans la liste
|
||||
@ -1012,33 +823,25 @@ def _delete_one(assiduite_id: int) -> tuple[int, str]:
|
||||
def assiduite_edit(assiduite_id: int):
|
||||
"""
|
||||
Edition d'une assiduité à partir de son id
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
La requête doit avoir un content type "application/json":
|
||||
{
|
||||
"etat"?: str,
|
||||
"moduleimpl_id"?: int
|
||||
"desc"?: str
|
||||
"est_just"?: bool
|
||||
}
|
||||
```
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduite/1/edit;{""etat"":""absent""}
|
||||
/assiduite/1/edit;{""moduleimpl_id"":2}
|
||||
/assiduite/1/edit;{""etat"": ""retard"",""moduleimpl_id"":3}
|
||||
|
||||
"""
|
||||
|
||||
# Récupération de l'assiduité à modifier
|
||||
assiduite_unique: Assiduite = Assiduite.query.filter_by(id=assiduite_id).first()
|
||||
if assiduite_unique is None:
|
||||
return json_error(404, "Assiduité non existante")
|
||||
assiduite_unique: Assiduite = Assiduite.query.filter_by(
|
||||
id=assiduite_id
|
||||
).first_or_404()
|
||||
# Récupération des valeurs à modifier
|
||||
data = request.get_json(force=True)
|
||||
|
||||
# Préparation du retour
|
||||
errors: list[str] = []
|
||||
|
||||
# Code 200 si modification réussie
|
||||
# Code 404 si raté + message d'erreur
|
||||
code, obj = _edit_one(assiduite_unique, data)
|
||||
@ -1054,10 +857,7 @@ def assiduite_edit(assiduite_id: int):
|
||||
msg=f"assiduite: modif {assiduite_unique}",
|
||||
)
|
||||
db.session.commit()
|
||||
try:
|
||||
scass.simple_invalidate_cache(assiduite_unique.to_dict())
|
||||
except ObjectDeletedError:
|
||||
return json_error(404, "Assiduité supprimée / inexistante")
|
||||
scass.simple_invalidate_cache(assiduite_unique.to_dict())
|
||||
|
||||
return {"OK": True}
|
||||
|
||||
@ -1071,10 +871,7 @@ def assiduite_edit(assiduite_id: int):
|
||||
def assiduites_edit():
|
||||
"""
|
||||
Edition de plusieurs assiduités
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
La requête doit avoir un content type "application/json":
|
||||
[
|
||||
{
|
||||
"assiduite_id" : int,
|
||||
@ -1084,13 +881,6 @@ def assiduites_edit():
|
||||
"est_just"?: bool
|
||||
}
|
||||
]
|
||||
```
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduites/edit;[{""etat"":""absent"",""assiduite_id"":1}]
|
||||
/assiduites/edit;[{""moduleimpl_id"":2,""assiduite_id"":1}]
|
||||
/assiduites/edit;[{""etat"": ""retard"",""moduleimpl_id"":3,""assiduite_id"":1}]
|
||||
|
||||
"""
|
||||
edit_list: list[object] = request.get_json(force=True)
|
||||
|
||||
@ -1198,7 +988,9 @@ def _edit_one(assiduite_unique: Assiduite, data: dict) -> tuple[int, str]:
|
||||
if moduleimpl is None:
|
||||
errors.append("param 'moduleimpl_id': invalide")
|
||||
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")
|
||||
else:
|
||||
# Mise à jour du moduleimpl
|
||||
@ -1214,9 +1006,7 @@ def _edit_one(assiduite_unique: Assiduite, data: dict) -> tuple[int, str]:
|
||||
if formsemestre:
|
||||
force = scu.is_assiduites_module_forced(formsemestre_id=formsemestre.id)
|
||||
else:
|
||||
force = scu.is_assiduites_module_forced(
|
||||
dept_id=assiduite_unique.etudiant.dept_id
|
||||
)
|
||||
force = scu.is_assiduites_module_forced(dept_id=etud.dept_id)
|
||||
|
||||
external_data = (
|
||||
external_data
|
||||
@ -1224,9 +1014,7 @@ def _edit_one(assiduite_unique: Assiduite, data: dict) -> tuple[int, str]:
|
||||
else assiduite_unique.external_data
|
||||
)
|
||||
|
||||
if force and not (
|
||||
external_data is not None and external_data.get("module", False) != ""
|
||||
):
|
||||
if force and not external_data.get("module", False):
|
||||
errors.append(
|
||||
"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()
|
||||
|
||||
assiduites_query: Query = assiduites_query.filter(
|
||||
Assiduite.date_debut >= scu.date_debut_annee_scolaire(annee),
|
||||
Assiduite.date_fin <= scu.date_fin_annee_scolaire(annee),
|
||||
Assiduite.date_debut >= scu.date_debut_anne_scolaire(annee),
|
||||
Assiduite.date_fin <= scu.date_fin_anne_scolaire(annee),
|
||||
)
|
||||
|
||||
return assiduites_query
|
||||
|
@ -1,16 +1,11 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
API : billets d'absences
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
Billets d'absence
|
||||
|
||||
"""
|
||||
|
||||
from flask import g, request
|
||||
@ -34,7 +29,7 @@ from app.scodoc.sco_permissions import Permission
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def billets_absence_etudiant(etudid: int):
|
||||
"""Liste des billets d'absence pour cet étudiant."""
|
||||
"""Liste des billets d'absence pour cet étudiant"""
|
||||
billets = sco_abs_billets.query_billets_etud(etudid)
|
||||
return [billet.to_dict() for billet in billets]
|
||||
|
||||
@ -46,20 +41,7 @@ def billets_absence_etudiant(etudid: int):
|
||||
@permission_required(Permission.AbsAddBillet)
|
||||
@as_json
|
||||
def billets_absence_create():
|
||||
"""Ajout d'un billet d'absence. Renvoie le billet créé en json.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
{
|
||||
"etudid" : int,
|
||||
"abs_begin" : date_iso,
|
||||
"abs_end" : date_iso,
|
||||
"description" : string,
|
||||
"justified" : bool
|
||||
}
|
||||
```
|
||||
"""
|
||||
"""Ajout d'un billet d'absence"""
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
etudid = data.get("etudid")
|
||||
abs_begin = data.get("abs_begin")
|
||||
|
@ -1,19 +1,14 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
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).
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
Département
|
||||
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
@ -21,15 +16,26 @@ from flask import request
|
||||
from flask_json import as_json
|
||||
from flask_login import login_required
|
||||
|
||||
from app import db, log
|
||||
import app
|
||||
from app import db
|
||||
from app.api import api_bp as bp, API_CLIENT_ERROR
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.decorators import scodoc
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import Departement, FormSemestre
|
||||
from app.models import departements
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_utils import json_error
|
||||
|
||||
|
||||
def get_departement(dept_ident: str) -> Departement:
|
||||
"Le departement, par id ou acronyme. Erreur 404 si pas trouvé."
|
||||
try:
|
||||
dept_id = int(dept_ident)
|
||||
except ValueError:
|
||||
dept_id = None
|
||||
if dept_id is None:
|
||||
return Departement.query.filter_by(acronym=dept_ident).first_or_404()
|
||||
return Departement.query.get_or_404(dept_id)
|
||||
|
||||
|
||||
@bp.route("/departements")
|
||||
@ -38,7 +44,7 @@ from app.scodoc.sco_utils import json_error
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def departements_list():
|
||||
"""Liste tous les départements."""
|
||||
"""Liste les départements"""
|
||||
return [dept.to_dict(with_dept_name=True) for dept in Departement.query]
|
||||
|
||||
|
||||
@ -48,7 +54,7 @@ def departements_list():
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def departements_ids():
|
||||
"""Liste des ids de tous les départements."""
|
||||
"""Liste des ids de départements"""
|
||||
return [dept.id for dept in Departement.query]
|
||||
|
||||
|
||||
@ -57,12 +63,11 @@ def departements_ids():
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def departement_by_acronym(acronym: str):
|
||||
def departement(acronym: str):
|
||||
"""
|
||||
Info sur un département. Accès par acronyme.
|
||||
|
||||
Exemple de résultat :
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"acronym": "TAPI",
|
||||
@ -71,7 +76,6 @@ def departement_by_acronym(acronym: str):
|
||||
"visible": true,
|
||||
"date_creation": "Fri, 15 Apr 2022 12:19:28 GMT"
|
||||
}
|
||||
```
|
||||
"""
|
||||
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||
return dept.to_dict(with_dept_name=True)
|
||||
@ -98,15 +102,11 @@ def departement_by_id(dept_id: int):
|
||||
def departement_create():
|
||||
"""
|
||||
Création d'un département.
|
||||
Le content type doit être `application/json`.
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
The request content type should be "application/json":
|
||||
{
|
||||
"acronym": str,
|
||||
"visible": bool,
|
||||
"visible":bool,
|
||||
}
|
||||
```
|
||||
"""
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
acronym = str(data.get("acronym", ""))
|
||||
@ -117,9 +117,6 @@ def departement_create():
|
||||
dept = departements.create_dept(acronym, visible=visible)
|
||||
except ScoValueError as exc:
|
||||
return json_error(500, exc.args[0] if exc.args else "")
|
||||
|
||||
log(f"departement_create {dept.acronym}")
|
||||
|
||||
return dept.to_dict()
|
||||
|
||||
|
||||
@ -130,12 +127,10 @@ def departement_create():
|
||||
@as_json
|
||||
def departement_edit(acronym):
|
||||
"""
|
||||
Édition d'un département: seul le champ `visible` peut être modifié.
|
||||
|
||||
DATA
|
||||
----
|
||||
Edition d'un département: seul visible peut être modifié
|
||||
The request content type should be "application/json":
|
||||
{
|
||||
"visible": bool,
|
||||
"visible":bool,
|
||||
}
|
||||
"""
|
||||
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||
@ -147,7 +142,6 @@ def departement_edit(acronym):
|
||||
dept.visible = visible
|
||||
db.session.add(dept)
|
||||
db.session.commit()
|
||||
log(f"departement_edit {dept.acronym}")
|
||||
return dept.to_dict()
|
||||
|
||||
|
||||
@ -157,13 +151,11 @@ def departement_edit(acronym):
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
def departement_delete(acronym):
|
||||
"""
|
||||
Suppression d'un département identifié par son acronyme.
|
||||
Suppression d'un département.
|
||||
"""
|
||||
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||
acronym = dept.acronym
|
||||
db.session.delete(dept)
|
||||
db.session.commit()
|
||||
log(f"departement_delete {acronym}")
|
||||
return {"OK": True}
|
||||
|
||||
|
||||
@ -172,16 +164,13 @@ def departement_delete(acronym):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def departement_etudiants(acronym: str):
|
||||
def dept_etudiants(acronym: str):
|
||||
"""
|
||||
Retourne la liste des étudiants d'un département.
|
||||
Retourne la liste des étudiants d'un département
|
||||
|
||||
PARAMS
|
||||
------
|
||||
acronym : l'acronyme d'un département
|
||||
acronym: l'acronyme d'un département
|
||||
|
||||
Exemple de résultat :
|
||||
```json
|
||||
[
|
||||
{
|
||||
"civilite": "M",
|
||||
@ -196,7 +185,6 @@ def departement_etudiants(acronym: str):
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
"""
|
||||
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||
return [etud.to_dict_short() for etud in dept.etudiants]
|
||||
@ -207,7 +195,7 @@ def departement_etudiants(acronym: str):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def departement_etudiants_by_id(dept_id: int):
|
||||
def dept_etudiants_by_id(dept_id: int):
|
||||
"""
|
||||
Retourne la liste des étudiants d'un département d'id donné.
|
||||
"""
|
||||
@ -220,8 +208,8 @@ def departement_etudiants_by_id(dept_id: int):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def departement_formsemestres_ids(acronym: str):
|
||||
"""Liste des ids de tous les formsemestres du département."""
|
||||
def dept_formsemestres_ids(acronym: str):
|
||||
"""liste des ids formsemestre du département"""
|
||||
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||
return [formsemestre.id for formsemestre in dept.formsemestres]
|
||||
|
||||
@ -231,39 +219,100 @@ def departement_formsemestres_ids(acronym: str):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def departement_formsemestres_ids_by_id(dept_id: int):
|
||||
"""Liste des ids de tous les formsemestres du département."""
|
||||
def dept_formsemestres_ids_by_id(dept_id: int):
|
||||
"""liste des ids formsemestre du département"""
|
||||
dept = Departement.query.get_or_404(dept_id)
|
||||
return [formsemestre.id for formsemestre in dept.formsemestres]
|
||||
|
||||
|
||||
@bp.route("/departement/<string:acronym>/formsemestres_courants")
|
||||
@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")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@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)
|
||||
contenant la date courante, ou à défaut celle indiquée en argument
|
||||
(au format ISO).
|
||||
|
||||
QUERY
|
||||
-----
|
||||
date_courante:<string:date_courante>
|
||||
|
||||
Liste des semestres actifs d'un département d'id donné
|
||||
"""
|
||||
dept = (
|
||||
Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||
if acronym
|
||||
else Departement.query.get_or_404(dept_id)
|
||||
)
|
||||
# Le département, spécifié par un id ou un acronyme
|
||||
dept = Departement.query.get_or_404(dept_id)
|
||||
date_courante = request.args.get("date_courante")
|
||||
date_courante = datetime.fromisoformat(date_courante) if date_courante else None
|
||||
return [
|
||||
formsemestre.to_dict_api()
|
||||
for formsemestre in FormSemestre.get_dept_formsemestres_courants(
|
||||
dept, 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]
|
||||
|
@ -1,20 +1,16 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
API : accès aux étudiants
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
Étudiants
|
||||
"""
|
||||
from datetime import datetime
|
||||
from operator import attrgetter
|
||||
|
||||
from flask import g, request, Response
|
||||
from flask import g, request
|
||||
from flask_json import as_json
|
||||
from flask_login import current_user
|
||||
from flask_login import login_required
|
||||
@ -22,28 +18,24 @@ from sqlalchemy import desc, func, or_
|
||||
from sqlalchemy.dialects.postgresql import VARCHAR
|
||||
|
||||
import app
|
||||
from app import db, log
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app.api import tools
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.but import bulletin_but_court
|
||||
from app.decorators import scodoc
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import (
|
||||
Admission,
|
||||
Departement,
|
||||
EtudAnnotation,
|
||||
FormSemestreInscription,
|
||||
FormSemestre,
|
||||
Identite,
|
||||
ScolarNews,
|
||||
)
|
||||
from app.scodoc import sco_bulletins
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc import sco_photos
|
||||
from app.scodoc.sco_utils import json_error, suppress_accents
|
||||
|
||||
import app.scodoc.sco_photos as sco_photos
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
# Un exemple:
|
||||
@ -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/long", defaults={"long": True})
|
||||
@api_web_bp.route("/etudiants/courants", defaults={"long": False})
|
||||
@ -93,20 +59,14 @@ def _get_etud_by_code(
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def etudiants_courants(long: bool = False):
|
||||
def etudiants_courants(long=False):
|
||||
"""
|
||||
La liste des étudiants des semestres "courants".
|
||||
Considère tous les départements dans lesquels l'utilisateur a la
|
||||
permission `ScoView` (donc tous si le dépt. du rôle est `None`),
|
||||
et les formsemestres contenant la date courante,
|
||||
ou à défaut celle indiquée en argument (au format ISO).
|
||||
|
||||
QUERY
|
||||
-----
|
||||
date_courante:<string:date_courante>
|
||||
La liste des étudiants des semestres "courants" (tous départements)
|
||||
(date du jour comprise dans la période couverte par le sem.)
|
||||
dans lesquels l'utilisateur a la permission ScoView
|
||||
(donc tous si le dept du rôle est None).
|
||||
|
||||
Exemple de résultat :
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1234,
|
||||
@ -119,7 +79,6 @@ def etudiants_courants(long: bool = False):
|
||||
}
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
En format "long": voir documentation.
|
||||
|
||||
@ -142,10 +101,7 @@ def etudiants_courants(long: bool = False):
|
||||
or_(Departement.acronym == acronym for acronym in allowed_depts)
|
||||
)
|
||||
if long:
|
||||
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
||||
data = [
|
||||
etud.to_dict_api(restrict=restrict, with_annotations=True) for etud in etuds
|
||||
]
|
||||
data = [etud.to_dict_api() for etud in etuds]
|
||||
else:
|
||||
data = [etud.to_dict_short() for etud in etuds]
|
||||
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é.
|
||||
|
||||
PARAMS
|
||||
------
|
||||
etudid : l'etudid de l'étudiant
|
||||
nip : le code nip de l'étudiant
|
||||
ine : le code ine de l'étudiant
|
||||
|
||||
`etudid` est unique dans la base (tous départements).
|
||||
Les codes INE et NIP sont uniques au sein d'un département.
|
||||
Si plusieurs objets ont le même code, on ramène le plus récemment inscrit.
|
||||
"""
|
||||
@ -182,8 +135,8 @@ def etudiant(etudid: int = None, nip: str = None, ine: str = None):
|
||||
404,
|
||||
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")
|
||||
@ -195,18 +148,11 @@ def etudiant(etudid: int = None, nip: str = None, ine: str = None):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def etudiant_get_photo_image(etudid: int = None, nip: str = None, ine: str = None):
|
||||
def get_photo_image(etudid: int = None, nip: str = None, ine: str = None):
|
||||
"""
|
||||
Retourne la photo de l'étudiant ou un placeholder si non existant.
|
||||
Le paramètre `size` peut prendre la valeur `small` (taille réduite, hauteur
|
||||
environ 90 pixels) ou `orig` (défaut, image de la taille originale).
|
||||
Retourne la photo de l'étudiant
|
||||
correspondant ou un placeholder si non existant.
|
||||
|
||||
QUERY
|
||||
-----
|
||||
size:<string:size>
|
||||
|
||||
PARAMS
|
||||
------
|
||||
etudid : l'etudid de l'étudiant
|
||||
nip : le code nip de l'étudiant
|
||||
ine : le code ine de l'étudiant
|
||||
@ -236,7 +182,7 @@ def etudiant_get_photo_image(etudid: int = None, nip: str = None, ine: str = Non
|
||||
@scodoc
|
||||
@permission_required(Permission.EtudChangeAdr)
|
||||
@as_json
|
||||
def etudiant_set_photo_image(etudid: int = None):
|
||||
def set_photo_image(etudid: int = None):
|
||||
"""Enregistre la photo de l'étudiant."""
|
||||
allowed_depts = current_user.get_depts_with_permission(Permission.EtudChangeAdr)
|
||||
query = Identite.query.filter_by(id=etudid)
|
||||
@ -279,12 +225,9 @@ def etudiant_set_photo_image(etudid: int = None):
|
||||
@as_json
|
||||
def etudiants(etudid: int = None, nip: str = None, ine: str = None):
|
||||
"""
|
||||
Info sur le ou les étudiants correspondants.
|
||||
|
||||
Comme `/etudiant` mais renvoie toujours une liste.
|
||||
|
||||
Info sur le ou les étudiants correspondant. Comme /etudiant mais renvoie
|
||||
toujours une liste.
|
||||
Si non trouvé, liste vide, pas d'erreur.
|
||||
|
||||
Dans 99% des cas, la liste contient un seul étudiant, mais si l'étudiant a
|
||||
été inscrit dans plusieurs départements, on a plusieurs objets (1 par dept.).
|
||||
"""
|
||||
@ -305,10 +248,7 @@ def etudiants(etudid: int = None, nip: str = None, ine: str = None):
|
||||
query = query.join(Departement).filter(
|
||||
or_(Departement.acronym == acronym for acronym in allowed_depts)
|
||||
)
|
||||
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
||||
return [
|
||||
etud.to_dict_api(restrict=restrict, with_annotations=True) for etud in query
|
||||
]
|
||||
return [etud.to_dict_api() for etud in query]
|
||||
|
||||
|
||||
@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)
|
||||
@as_json
|
||||
def etudiants_by_name(start: str = "", min_len=3, limit=32):
|
||||
"""Liste des étudiants dont le nom débute par `start`.
|
||||
|
||||
Si `start` fait moins de `min_len=3` caractères, liste vide.
|
||||
"""Liste des étudiants dont le nom débute par start.
|
||||
Si start fait moins de min_len=3 caractères, liste vide.
|
||||
La casse et les accents sont ignorés.
|
||||
"""
|
||||
if len(start) < min_len:
|
||||
@ -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)
|
||||
# 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(restrict=restrict)
|
||||
for etud in sorted(etuds, key=attrgetter("sort_key"))
|
||||
]
|
||||
return [etud.to_dict_api() for etud in sorted(etuds, key=attrgetter("sort_key"))]
|
||||
|
||||
|
||||
@bp.route("/etudiant/etudid/<int:etudid>/formsemestres")
|
||||
@ -354,13 +289,13 @@ def etudiants_by_name(start: str = "", min_len=3, limit=32):
|
||||
@as_json
|
||||
def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None):
|
||||
"""
|
||||
Liste des formsemestres qu'un étudiant a suivi, triés par ordre chronologique.
|
||||
Liste des semestres qu'un étudiant a suivi, triés par ordre chronologique.
|
||||
Accès par etudid, nip ou ine.
|
||||
|
||||
Attention, si accès via NIP ou INE, les formsemestres peuvent être de départements
|
||||
Attention, si accès via NIP ou INE, les semestres peuvent être de départements
|
||||
différents (si l'étudiant a changé de département). L'id du département est `dept_id`.
|
||||
|
||||
Si accès par département, ne retourne que les formsemestres suivis dans le département.
|
||||
Si accès par département, ne retourne que les formsemestre suivis dans le département.
|
||||
"""
|
||||
if etudid is not None:
|
||||
q_etud = Identite.query.filter_by(id=etudid)
|
||||
@ -421,43 +356,48 @@ def bulletin(
|
||||
code_type: str = "etudid",
|
||||
code: str = None,
|
||||
formsemestre_id: int = None,
|
||||
version: str = "selectedevals",
|
||||
version: str = "long",
|
||||
pdf: bool = False,
|
||||
with_img_signatures_pdf: bool = True,
|
||||
):
|
||||
"""
|
||||
Retourne le bulletin d'un étudiant dans un formsemestre.
|
||||
|
||||
PARAMS
|
||||
------
|
||||
formsemestre_id : l'id d'un formsemestre
|
||||
code_type : "etudid", "nip" ou "ine"
|
||||
code : valeur du code INE, NIP ou etudid, selon code_type.
|
||||
version : type de bulletin (par défaut, "selectedevals"): short, long, selectedevals, butcourt
|
||||
version : type de bulletin (par défaut, "long"): short, long, selectedevals, butcourt
|
||||
pdf : si spécifié, bulletin au format PDF (et non JSON).
|
||||
|
||||
Exemple de résultat : voir https://scodoc.org/ScoDoc9API/#bulletin
|
||||
"""
|
||||
if version == "pdf":
|
||||
version = "long"
|
||||
pdf = True
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
if version not in (
|
||||
scu.BULLETINS_VERSIONS_BUT
|
||||
if formsemestre.formation.is_apc()
|
||||
else scu.BULLETINS_VERSIONS
|
||||
):
|
||||
if version not in scu.BULLETINS_VERSIONS_BUT:
|
||||
return json_error(404, "version invalide")
|
||||
if formsemestre.bul_hide_xml and pdf:
|
||||
return json_error(403, "bulletin non disponible")
|
||||
# note: la version json est réduite si bul_hide_xml
|
||||
# return f"{code_type}={code}, version={version}, pdf={pdf}"
|
||||
formsemestre = FormSemestre.query.filter_by(id=formsemestre_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:
|
||||
return json_error(404, "formsemestre inexistant")
|
||||
app.set_sco_dept(dept.acronym)
|
||||
|
||||
ok, etud = _get_etud_by_code(code_type, code, dept)
|
||||
if not ok:
|
||||
return etud # json error
|
||||
if code_type == "nip":
|
||||
query = Identite.query.filter_by(code_nip=code, dept_id=dept.id)
|
||||
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 pdf:
|
||||
@ -478,9 +418,9 @@ def bulletin(
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/groups")
|
||||
@api_web_bp.route(
|
||||
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/groups"
|
||||
@bp.route(
|
||||
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/groups",
|
||||
methods=["GET"],
|
||||
)
|
||||
@scodoc
|
||||
@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é
|
||||
|
||||
PARAMS
|
||||
------
|
||||
formsemestre_id : l'id d'un formsemestre
|
||||
etudid : l'etudid d'un étudiant
|
||||
|
||||
Exemple de résultat :
|
||||
```json
|
||||
[
|
||||
{
|
||||
"partition_id": 1,
|
||||
@ -520,8 +457,8 @@ def etudiant_groups(formsemestre_id: int, etudid: int = None):
|
||||
"group_name": "A"
|
||||
}
|
||||
]
|
||||
```
|
||||
"""
|
||||
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
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)
|
||||
|
||||
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
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
ScoDoc 9 API : accès aux évaluations
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
Évaluations
|
||||
"""
|
||||
from flask import g, request
|
||||
from flask_json import as_json
|
||||
@ -18,8 +14,7 @@ from flask_login import current_user, login_required
|
||||
import app
|
||||
from app import log, db
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.decorators import scodoc
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import Evaluation, ModuleImpl, FormSemestre
|
||||
from app.scodoc import sco_evaluation_db, sco_saisie_notes
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||
@ -36,28 +31,24 @@ import app.scodoc.sco_utils as scu
|
||||
def get_evaluation(evaluation_id: int):
|
||||
"""Description d'une évaluation.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
{
|
||||
'coefficient': 1.0,
|
||||
'date_debut': '2016-01-04T08:30:00',
|
||||
'date_fin': '2016-01-04T12:30:00',
|
||||
'description': 'TP Température',
|
||||
'evaluation_type': 0,
|
||||
'id': 15797,
|
||||
'moduleimpl_id': 1234,
|
||||
'note_max': 20.0,
|
||||
'numero': 3,
|
||||
'poids': {
|
||||
'UE1.1': 1.0,
|
||||
'UE1.2': 1.0,
|
||||
'UE1.3': 1.0
|
||||
},
|
||||
'publish_incomplete': False,
|
||||
'visibulletin': True
|
||||
}
|
||||
```
|
||||
'coefficient': 1.0,
|
||||
'date_debut': '2016-01-04T08:30:00',
|
||||
'date_fin': '2016-01-04T12:30:00',
|
||||
'description': 'TP NI9219 Température',
|
||||
'evaluation_type': 0,
|
||||
'id': 15797,
|
||||
'moduleimpl_id': 1234,
|
||||
'note_max': 20.0,
|
||||
'numero': 3,
|
||||
'poids': {
|
||||
'UE1.1': 1.0,
|
||||
'UE1.2': 1.0,
|
||||
'UE1.3': 1.0
|
||||
},
|
||||
'publish_incomplete': False,
|
||||
'visibulletin': True
|
||||
}
|
||||
"""
|
||||
query = Evaluation.query.filter_by(id=evaluation_id)
|
||||
if g.scodoc_dept:
|
||||
@ -76,18 +67,22 @@ def get_evaluation(evaluation_id: int):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@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
|
||||
|
||||
Exemple de résultat : voir `/evaluation`.
|
||||
Exemple de résultat : voir /evaluation
|
||||
"""
|
||||
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
|
||||
return [evaluation.to_dict_api() for evaluation in modimpl.evaluations]
|
||||
query = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id)
|
||||
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")
|
||||
@ -98,36 +93,30 @@ def moduleimpl_evaluations(moduleimpl_id: int):
|
||||
@as_json
|
||||
def evaluation_notes(evaluation_id: int):
|
||||
"""
|
||||
Retourne la liste des notes de l'évaluation.
|
||||
Retourne la liste des notes de l'évaluation
|
||||
|
||||
PARAMS
|
||||
------
|
||||
evaluation_id : l'id de l'évaluation
|
||||
|
||||
Exemple de résultat :
|
||||
```json
|
||||
{
|
||||
"11": {
|
||||
{
|
||||
"11": {
|
||||
"etudid": 11,
|
||||
"evaluation_id": 1,
|
||||
"value": 15.0,
|
||||
"note_max" : 20.0,
|
||||
"comment": "",
|
||||
"date": "2024-07-19T19:08:44+02:00",
|
||||
"date": "Wed, 20 Apr 2022 06:49:05 GMT",
|
||||
"uid": 2
|
||||
},
|
||||
"12": {
|
||||
},
|
||||
"12": {
|
||||
"etudid": 12,
|
||||
"evaluation_id": 1,
|
||||
"value": "ABS",
|
||||
"note_max" : 20.0,
|
||||
"value": 12.0,
|
||||
"comment": "",
|
||||
"date": "2024-07-19T19:08:44+02:00",
|
||||
"date": "Wed, 20 Apr 2022 06:49:06 GMT",
|
||||
"uid": 2
|
||||
},
|
||||
...
|
||||
}
|
||||
```
|
||||
},
|
||||
...
|
||||
}
|
||||
"""
|
||||
query = Evaluation.query.filter_by(id=evaluation_id)
|
||||
if g.scodoc_dept:
|
||||
@ -159,20 +148,15 @@ def evaluation_notes(evaluation_id: int):
|
||||
@scodoc
|
||||
@permission_required(Permission.EnsView)
|
||||
@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.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
The request content type should be "application/json",
|
||||
and contains:
|
||||
{
|
||||
'notes' : [ [etudid, value], ... ],
|
||||
'comment' : optional string
|
||||
}
|
||||
```
|
||||
|
||||
Résultat:
|
||||
|
||||
Result:
|
||||
- nb_changed: nombre de notes changées
|
||||
- nb_suppress: nombre de notes effacées
|
||||
- etudids_with_decision: liste des etudiants dont la note a changé
|
||||
@ -207,9 +191,8 @@ def evaluation_set_notes(evaluation_id: int): # evaluation-notes-set
|
||||
@as_json
|
||||
def evaluation_create(moduleimpl_id: int):
|
||||
"""Création d'une évaluation.
|
||||
|
||||
DATA
|
||||
----
|
||||
The request content type should be "application/json",
|
||||
and contains:
|
||||
{
|
||||
"description" : str,
|
||||
"evaluation_type" : int, // {0,1,2} default 0 (normale)
|
||||
@ -222,8 +205,7 @@ def evaluation_create(moduleimpl_id: int):
|
||||
"coefficient" : float, // si non spécifié, 1.0
|
||||
"poids" : { ue_id : poids } // optionnel
|
||||
}
|
||||
|
||||
Résultat: l'évaluation créée.
|
||||
Result: l'évaluation créée.
|
||||
"""
|
||||
moduleimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id)
|
||||
if not moduleimpl.can_edit_evaluation(current_user):
|
||||
@ -273,7 +255,7 @@ def evaluation_create(moduleimpl_id: int):
|
||||
@as_json
|
||||
def evaluation_delete(evaluation_id: int):
|
||||
"""Suppression d'une évaluation.
|
||||
Efface aussi toutes ses notes.
|
||||
Efface aussi toutes ses notes
|
||||
"""
|
||||
query = Evaluation.query.filter_by(id=evaluation_id)
|
||||
if g.scodoc_dept:
|
||||
|
@ -1,15 +1,11 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
ScoDoc 9 API : accès aux formations
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
Formations
|
||||
"""
|
||||
|
||||
from flask import flash, g, request
|
||||
@ -19,15 +15,12 @@ from flask_login import login_required
|
||||
import app
|
||||
from app import db, log
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.decorators import scodoc
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import (
|
||||
ApcNiveau,
|
||||
ApcParcours,
|
||||
Formation,
|
||||
Module,
|
||||
UniteEns,
|
||||
)
|
||||
from app.scodoc import sco_formations
|
||||
@ -42,8 +35,7 @@ from app.scodoc.sco_permissions import Permission
|
||||
@as_json
|
||||
def formations():
|
||||
"""
|
||||
Retourne la liste de toutes les formations (tous départements,
|
||||
sauf si route départementale).
|
||||
Retourne la liste de toutes les formations (tous départements)
|
||||
"""
|
||||
query = Formation.query
|
||||
if g.scodoc_dept:
|
||||
@ -60,10 +52,9 @@ def formations():
|
||||
@as_json
|
||||
def formations_ids():
|
||||
"""
|
||||
Retourne la liste de toutes les id de formations
|
||||
(tous départements, ou du département indiqué dans la route)
|
||||
Retourne la liste de toutes les id de formations (tous départements)
|
||||
|
||||
Exemple de résultat : `[ 17, 99, 32 ]`.
|
||||
Exemple de résultat : [ 17, 99, 32 ]
|
||||
"""
|
||||
query = Formation.query
|
||||
if g.scodoc_dept:
|
||||
@ -79,26 +70,24 @@ def formations_ids():
|
||||
@as_json
|
||||
def formation_by_id(formation_id: int):
|
||||
"""
|
||||
La formation d'id donné.
|
||||
La formation d'id donné
|
||||
|
||||
formation_id : l'id d'une formation
|
||||
|
||||
Exemple de résultat :
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"acronyme": "BUT R&T",
|
||||
"titre_officiel": "Bachelor technologique réseaux et télécommunications",
|
||||
"formation_code": "V1RET",
|
||||
"code_specialite": null,
|
||||
"dept_id": 1,
|
||||
"titre": "BUT R&T",
|
||||
"version": 1,
|
||||
"type_parcours": 700,
|
||||
"referentiel_competence_id": null,
|
||||
"formation_id": 1
|
||||
}
|
||||
```
|
||||
{
|
||||
"id": 1,
|
||||
"acronyme": "BUT R&T",
|
||||
"titre_officiel": "Bachelor technologique réseaux et télécommunications",
|
||||
"formation_code": "V1RET",
|
||||
"code_specialite": null,
|
||||
"dept_id": 1,
|
||||
"titre": "BUT R&T",
|
||||
"version": 1,
|
||||
"type_parcours": 700,
|
||||
"referentiel_competence_id": null,
|
||||
"formation_id": 1
|
||||
}
|
||||
"""
|
||||
query = Formation.query.filter_by(id=formation_id)
|
||||
if g.scodoc_dept:
|
||||
@ -130,102 +119,97 @@ def formation_export_by_formation_id(formation_id: int, export_ids=False):
|
||||
"""
|
||||
Retourne la formation, avec UE, matières, modules
|
||||
|
||||
PARAMS
|
||||
------
|
||||
formation_id : l'id d'une formation
|
||||
export_with_ids : si présent, exporte aussi les ids des objets ScoDoc de la formation.
|
||||
export_ids : True ou False, si l'on veut ou non exporter les ids
|
||||
|
||||
Exemple de résultat :
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"acronyme": "BUT R&T",
|
||||
"titre_officiel": "Bachelor technologique r\u00e9seaux et t\u00e9l\u00e9communications",
|
||||
"formation_code": "V1RET",
|
||||
"code_specialite": null,
|
||||
"dept_id": 1,
|
||||
"titre": "BUT R&T",
|
||||
"version": 1,
|
||||
"type_parcours": 700,
|
||||
"referentiel_competence_id": null,
|
||||
"formation_id": 1,
|
||||
"ue": [
|
||||
{
|
||||
"acronyme": "RT1.1",
|
||||
"numero": 1,
|
||||
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
|
||||
"type": 0,
|
||||
"ue_code": "UCOD11",
|
||||
"ects": 12.0,
|
||||
"is_external": false,
|
||||
"code_apogee": "",
|
||||
"coefficient": 0.0,
|
||||
"semestre_idx": 1,
|
||||
"color": "#B80004",
|
||||
"reference": 1,
|
||||
"matiere": [
|
||||
"id": 1,
|
||||
"acronyme": "BUT R&T",
|
||||
"titre_officiel": "Bachelor technologique r\u00e9seaux et t\u00e9l\u00e9communications",
|
||||
"formation_code": "V1RET",
|
||||
"code_specialite": null,
|
||||
"dept_id": 1,
|
||||
"titre": "BUT R&T",
|
||||
"version": 1,
|
||||
"type_parcours": 700,
|
||||
"referentiel_competence_id": null,
|
||||
"formation_id": 1,
|
||||
"ue": [
|
||||
{
|
||||
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
|
||||
"numero": 1,
|
||||
"module": [
|
||||
"acronyme": "RT1.1",
|
||||
"numero": 1,
|
||||
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
|
||||
"type": 0,
|
||||
"ue_code": "UCOD11",
|
||||
"ects": 12.0,
|
||||
"is_external": false,
|
||||
"code_apogee": "",
|
||||
"coefficient": 0.0,
|
||||
"semestre_idx": 1,
|
||||
"color": "#B80004",
|
||||
"reference": 1,
|
||||
"matiere": [
|
||||
{
|
||||
"titre": "Initiation aux r\u00e9seaux informatiques",
|
||||
"abbrev": "Init aux r\u00e9seaux informatiques",
|
||||
"code": "R101",
|
||||
"heures_cours": 0.0,
|
||||
"heures_td": 0.0,
|
||||
"heures_tp": 0.0,
|
||||
"coefficient": 1.0,
|
||||
"ects": "",
|
||||
"semestre_id": 1,
|
||||
"numero": 10,
|
||||
"code_apogee": "",
|
||||
"module_type": 2,
|
||||
"coefficients": [
|
||||
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
|
||||
"numero": 1,
|
||||
"module": [
|
||||
{
|
||||
"ue_reference": "1",
|
||||
"coef": "12.0"
|
||||
"titre": "Initiation aux r\u00e9seaux informatiques",
|
||||
"abbrev": "Init aux r\u00e9seaux informatiques",
|
||||
"code": "R101",
|
||||
"heures_cours": 0.0,
|
||||
"heures_td": 0.0,
|
||||
"heures_tp": 0.0,
|
||||
"coefficient": 1.0,
|
||||
"ects": "",
|
||||
"semestre_id": 1,
|
||||
"numero": 10,
|
||||
"code_apogee": "",
|
||||
"module_type": 2,
|
||||
"coefficients": [
|
||||
{
|
||||
"ue_reference": "1",
|
||||
"coef": "12.0"
|
||||
},
|
||||
{
|
||||
"ue_reference": "2",
|
||||
"coef": "4.0"
|
||||
},
|
||||
{
|
||||
"ue_reference": "3",
|
||||
"coef": "4.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ue_reference": "2",
|
||||
"coef": "4.0"
|
||||
"titre": "Se sensibiliser \u00e0 l'hygi\u00e8ne informatique...",
|
||||
"abbrev": "Hygi\u00e8ne informatique",
|
||||
"code": "SAE11",
|
||||
"heures_cours": 0.0,
|
||||
"heures_td": 0.0,
|
||||
"heures_tp": 0.0,
|
||||
"coefficient": 1.0,
|
||||
"ects": "",
|
||||
"semestre_id": 1,
|
||||
"numero": 10,
|
||||
"code_apogee": "",
|
||||
"module_type": 3,
|
||||
"coefficients": [
|
||||
{
|
||||
"ue_reference": "1",
|
||||
"coef": "16.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ue_reference": "3",
|
||||
"coef": "4.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"titre": "Se sensibiliser \u00e0 l'hygi\u00e8ne informatique...",
|
||||
"abbrev": "Hygi\u00e8ne informatique",
|
||||
"code": "SAE11",
|
||||
"heures_cours": 0.0,
|
||||
"heures_td": 0.0,
|
||||
"heures_tp": 0.0,
|
||||
"coefficient": 1.0,
|
||||
"ects": "",
|
||||
"semestre_id": 1,
|
||||
"numero": 10,
|
||||
"code_apogee": "",
|
||||
"module_type": 3,
|
||||
"coefficients": [
|
||||
{
|
||||
"ue_reference": "1",
|
||||
"coef": "16.0"
|
||||
}
|
||||
]
|
||||
...
|
||||
]
|
||||
},
|
||||
...
|
||||
]
|
||||
]
|
||||
},
|
||||
...
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
]
|
||||
}
|
||||
"""
|
||||
query = Formation.query.filter_by(id=formation_id)
|
||||
if g.scodoc_dept:
|
||||
@ -248,8 +232,11 @@ def formation_export_by_formation_id(formation_id: int, export_ids=False):
|
||||
@as_json
|
||||
def referentiel_competences(formation_id: int):
|
||||
"""
|
||||
Retourne le référentiel de compétences de la formation
|
||||
ou null si pas de référentiel associé.
|
||||
Retourne le référentiel de compétences
|
||||
|
||||
formation_id : l'id d'une formation
|
||||
|
||||
return null si pas de référentiel associé.
|
||||
"""
|
||||
query = Formation.query.filter_by(id=formation_id)
|
||||
if g.scodoc_dept:
|
||||
@ -260,22 +247,16 @@ def referentiel_competences(formation_id: int):
|
||||
return formation.referentiel_competence.to_dict()
|
||||
|
||||
|
||||
@bp.route("/formation/ue/<int:ue_id>/set_parcours", methods=["POST"])
|
||||
@api_web_bp.route("/formation/ue/<int:ue_id>/set_parcours", methods=["POST"])
|
||||
@bp.route("/set_ue_parcours/<int:ue_id>", methods=["POST"])
|
||||
@api_web_bp.route("/set_ue_parcours/<int:ue_id>", methods=["POST"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.EditFormation)
|
||||
@as_json
|
||||
def ue_set_parcours(ue_id: int):
|
||||
def set_ue_parcours(ue_id: int):
|
||||
"""Associe UE et parcours BUT.
|
||||
|
||||
La liste des ids de parcours est passée en argument JSON.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
[ parcour_id1, parcour_id2, ... ]
|
||||
```
|
||||
JSON arg: [parcour_id1, parcour_id2, ...]
|
||||
"""
|
||||
query = UniteEns.query.filter_by(id=ue_id)
|
||||
if g.scodoc_dept:
|
||||
@ -288,7 +269,7 @@ def ue_set_parcours(ue_id: int):
|
||||
parcours = [
|
||||
ApcParcours.query.get_or_404(int(parcour_id)) for parcour_id in parcours_ids
|
||||
]
|
||||
log(f"ue_set_parcours: ue_id={ue.id} parcours_ids={parcours_ids}")
|
||||
log(f"set_ue_parcours: ue_id={ue.id} parcours_ids={parcours_ids}")
|
||||
ok, error_message = ue.set_parcours(parcours)
|
||||
if not ok:
|
||||
return json_error(404, error_message)
|
||||
@ -296,19 +277,19 @@ def ue_set_parcours(ue_id: int):
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/formation/ue/<int:ue_id>/assoc_niveau/<int:niveau_id>",
|
||||
"/assoc_ue_niveau/<int:ue_id>/<int:niveau_id>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formation/ue/<int:ue_id>/assoc_niveau/<int:niveau_id>",
|
||||
"/assoc_ue_niveau/<int:ue_id>/<int:niveau_id>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.EditFormation)
|
||||
@as_json
|
||||
def ue_assoc_niveau(ue_id: int, niveau_id: int):
|
||||
"""Associe l'UE au niveau de compétence."""
|
||||
def assoc_ue_niveau(ue_id: int, niveau_id: int):
|
||||
"""Associe l'UE au niveau de compétence"""
|
||||
query = UniteEns.query.filter_by(id=ue_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
@ -325,278 +306,30 @@ def ue_assoc_niveau(ue_id: int, niveau_id: int):
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/formation/ue/<int:ue_id>/desassoc_niveau",
|
||||
"/desassoc_ue_niveau/<int:ue_id>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formation/ue/<int:ue_id>/desassoc_niveau",
|
||||
"/desassoc_ue_niveau/<int:ue_id>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.EditFormation)
|
||||
@as_json
|
||||
def ue_desassoc_niveau(ue_id: int):
|
||||
def desassoc_ue_niveau(ue_id: int):
|
||||
"""Désassocie cette UE de son niveau de compétence
|
||||
(si elle n'est pas associée, ne fait rien).
|
||||
(si elle n'est pas associée, ne fait rien)
|
||||
"""
|
||||
query = UniteEns.query.filter_by(id=ue_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
ue: UniteEns = query.first_or_404()
|
||||
ok, error_message = ue.set_niveau_competence(None)
|
||||
if not ok:
|
||||
if g.scodoc_dept: # "usage web"
|
||||
flash(error_message, "error")
|
||||
return json_error(404, error_message)
|
||||
if g.scodoc_dept: # "usage web"
|
||||
ue.niveau_competence = None
|
||||
db.session.add(ue)
|
||||
db.session.commit()
|
||||
log(f"desassoc_ue_niveau: {ue}")
|
||||
if g.scodoc_dept:
|
||||
# "usage web"
|
||||
flash(f"UE {ue.acronyme} dé-associée")
|
||||
return {"status": 0}
|
||||
|
||||
|
||||
@bp.route("/formation/ue/<int:ue_id>", methods=["GET"])
|
||||
@api_web_bp.route("/formation/ue/<int:ue_id>", methods=["GET"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def get_ue(ue_id: int):
|
||||
"""Renvoie l'UE."""
|
||||
query = UniteEns.query.filter_by(id=ue_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
ue: UniteEns = query.first_or_404()
|
||||
return ue.to_dict(convert_objects=True)
|
||||
|
||||
|
||||
@bp.route("/formation/module/<int:module_id>", methods=["GET"])
|
||||
@api_web_bp.route("/formation/module/<int:module_id>", methods=["GET"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def formation_module_get(module_id: int):
|
||||
"""Renvoie le module."""
|
||||
query = Module.query.filter_by(id=module_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
module: Module = query.first_or_404()
|
||||
return module.to_dict(convert_objects=True)
|
||||
|
||||
|
||||
@bp.route("/formation/ue/set_code_apogee", methods=["POST"])
|
||||
@api_web_bp.route("/formation/ue/set_code_apogee", methods=["POST"])
|
||||
@bp.route(
|
||||
"/formation/ue/<int:ue_id>/set_code_apogee/<string:code_apogee>", methods=["POST"]
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formation/ue/<int:ue_id>/set_code_apogee/<string:code_apogee>", methods=["POST"]
|
||||
)
|
||||
@bp.route(
|
||||
"/formation/ue/<int:ue_id>/set_code_apogee",
|
||||
defaults={"code_apogee": ""},
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formation/ue/<int:ue_id>/set_code_apogee",
|
||||
defaults={"code_apogee": ""},
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.EditFormation)
|
||||
def ue_set_code_apogee(ue_id: int | None = None, code_apogee: str = ""):
|
||||
"""Change le code Apogée de l'UE.
|
||||
|
||||
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
|
||||
par des virgules.
|
||||
|
||||
Ce changement peut être fait sur formation verrouillée.
|
||||
|
||||
Si `ue_id` n'est pas spécifié, utilise l'argument oid du POST.
|
||||
Si `code_apogee` n'est pas spécifié ou vide,
|
||||
utilise l'argument value du POST.
|
||||
|
||||
Le retour est une chaîne (le code enregistré), pas du json.
|
||||
"""
|
||||
if ue_id is None:
|
||||
ue_id = request.form.get("oid")
|
||||
if ue_id is None:
|
||||
return json_error(404, "argument oid manquant")
|
||||
if not code_apogee:
|
||||
code_apogee = request.form.get("value", "")
|
||||
query = UniteEns.query.filter_by(id=ue_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
ue: UniteEns = query.first_or_404()
|
||||
|
||||
code_apogee = code_apogee.strip("-_ \t")[:APO_CODE_STR_LEN] # tronque
|
||||
|
||||
log(f"API ue_set_code_apogee: ue_id={ue.id} code_apogee={code_apogee}")
|
||||
|
||||
ue.code_apogee = code_apogee
|
||||
db.session.add(ue)
|
||||
db.session.commit()
|
||||
return code_apogee or ""
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/formation/ue/<int:ue_id>/set_code_apogee_rcue/<string:code_apogee>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formation/ue/<int:ue_id>/set_code_apogee_rcue/<string:code_apogee>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@bp.route(
|
||||
"/formation/ue/<int:ue_id>/set_code_apogee_rcue",
|
||||
defaults={"code_apogee": ""},
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formation/ue/<int:ue_id>/set_code_apogee_rcue",
|
||||
defaults={"code_apogee": ""},
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.EditFormation)
|
||||
def ue_set_code_apogee_rcue(ue_id: int, code_apogee: str = ""):
|
||||
"""Change le code Apogée du RCUE de l'UE.
|
||||
|
||||
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
|
||||
par des virgules.
|
||||
|
||||
Ce changement peut être fait sur formation verrouillée.
|
||||
|
||||
Si code_apogee n'est pas spécifié ou vide,
|
||||
utilise l'argument value du POST (utilisé par `jinplace.js`)
|
||||
|
||||
Le retour est une chaîne (le code enregistré), pas du json.
|
||||
"""
|
||||
if not code_apogee:
|
||||
code_apogee = request.form.get("value", "")
|
||||
query = UniteEns.query.filter_by(id=ue_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
ue: UniteEns = query.first_or_404()
|
||||
|
||||
code_apogee = code_apogee.strip("-_ \t")[:APO_CODE_STR_LEN] # tronque
|
||||
|
||||
log(f"API ue_set_code_apogee_rcue: ue_id={ue.id} code_apogee={code_apogee}")
|
||||
|
||||
ue.code_apogee_rcue = code_apogee
|
||||
db.session.add(ue)
|
||||
db.session.commit()
|
||||
return code_apogee or ""
|
||||
|
||||
|
||||
@bp.route("/formation/module/set_code_apogee", methods=["POST"])
|
||||
@api_web_bp.route("/formation/module/set_code_apogee", methods=["POST"])
|
||||
@bp.route(
|
||||
"/formation/module/<int:module_id>/set_code_apogee/<string:code_apogee>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formation/module/<int:module_id>/set_code_apogee/<string:code_apogee>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@bp.route(
|
||||
"/formation/module/<int:module_id>/set_code_apogee",
|
||||
defaults={"code_apogee": ""},
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formation/module/<int:module_id>/set_code_apogee",
|
||||
defaults={"code_apogee": ""},
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.EditFormation)
|
||||
def formation_module_set_code_apogee(
|
||||
module_id: int | None = None, code_apogee: str = ""
|
||||
):
|
||||
"""Change le code Apogée du module.
|
||||
|
||||
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
|
||||
par des virgules.
|
||||
|
||||
Ce changement peut être fait sur formation verrouillée.
|
||||
|
||||
Si `module_id` n'est pas spécifié, utilise l'argument `oid` du POST.
|
||||
Si `code_apogee` n'est pas spécifié ou vide,
|
||||
utilise l'argument value du POST (utilisé par jinplace.js)
|
||||
|
||||
Le retour est une chaîne (le code enregistré), pas du json.
|
||||
"""
|
||||
if module_id is None:
|
||||
module_id = request.form.get("oid")
|
||||
if module_id is None:
|
||||
return json_error(404, "argument oid manquant")
|
||||
if not code_apogee:
|
||||
code_apogee = request.form.get("value", "")
|
||||
query = Module.query.filter_by(id=module_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
module: Module = query.first_or_404()
|
||||
|
||||
code_apogee = code_apogee.strip("-_ \t")[:APO_CODE_STR_LEN] # tronque
|
||||
|
||||
log(
|
||||
f"API formation_module_set_code_apogee: module_id={module.id} code_apogee={code_apogee}"
|
||||
)
|
||||
|
||||
module.code_apogee = code_apogee
|
||||
db.session.add(module)
|
||||
db.session.commit()
|
||||
return code_apogee or ""
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/formation/module/<int:module_id>/edit",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formation/module/<int:module_id>/edit",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.EditFormation)
|
||||
@as_json
|
||||
def formation_module_edit(module_id: int):
|
||||
"""Édition d'un module. Renvoie le module en json."""
|
||||
query = Module.query.filter_by(id=module_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
module: Module = query.first_or_404()
|
||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
||||
module.from_dict(args)
|
||||
db.session.commit()
|
||||
db.session.refresh(module)
|
||||
log(f"API module_edit: module_id={module.id} args={args}")
|
||||
r = module.to_dict(convert_objects=True, with_parcours_ids=True)
|
||||
return r
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/formation/ue/<int:ue_id>/edit",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formation/ue/<int:ue_id>/edit",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.EditFormation)
|
||||
@as_json
|
||||
def ue_edit(ue_id: int):
|
||||
"""Édition d'une UE. Renvoie l'UE en json."""
|
||||
ue = UniteEns.get_ue(ue_id)
|
||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
||||
ue.from_dict(args)
|
||||
db.session.commit()
|
||||
db.session.refresh(ue)
|
||||
log(f"API ue_edit: ue_id={ue.id} args={args}")
|
||||
r = ue.to_dict(convert_objects=True)
|
||||
return r
|
||||
|
@ -1,29 +1,22 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
ScoDoc 9 API : accès aux formsemestres
|
||||
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
FormSemestre
|
||||
|
||||
"""
|
||||
from operator import attrgetter, itemgetter
|
||||
|
||||
from flask import g, make_response, request
|
||||
from flask_json import as_json
|
||||
from flask_login import current_user, login_required
|
||||
import sqlalchemy as sa
|
||||
from flask_login import login_required
|
||||
|
||||
import app
|
||||
from app import db, log
|
||||
from app import db
|
||||
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.decorators import scodoc
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.comp import res_sem
|
||||
from app.comp.moy_mod import ModuleImplResults
|
||||
@ -45,7 +38,7 @@ from app.scodoc import sco_groups
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
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>")
|
||||
@ -61,37 +54,35 @@ def formsemestre_infos(formsemestre_id: int):
|
||||
formsemestre_id : l'id du formsemestre
|
||||
|
||||
Exemple de résultat :
|
||||
```json
|
||||
{
|
||||
"block_moyennes": false,
|
||||
"bul_bgcolor": "white",
|
||||
"bul_hide_xml": false,
|
||||
"date_debut_iso": "2021-09-01",
|
||||
"date_debut": "01/09/2021",
|
||||
"date_fin_iso": "2022-08-31",
|
||||
"date_fin": "31/08/2022",
|
||||
"dept_id": 1,
|
||||
"elt_annee_apo": null,
|
||||
"elt_passage_apo" : null,
|
||||
"elt_sem_apo": null,
|
||||
"ens_can_edit_eval": false,
|
||||
"etat": true,
|
||||
"formation_id": 1,
|
||||
"formsemestre_id": 1,
|
||||
"gestion_compensation": false,
|
||||
"gestion_semestrielle": false,
|
||||
"id": 1,
|
||||
"modalite": "FI",
|
||||
"resp_can_change_ens": true,
|
||||
"resp_can_edit": false,
|
||||
"responsables": [1, 99], // uids
|
||||
"scodoc7_id": null,
|
||||
"semestre_id": 1,
|
||||
"titre_formation" : "BUT GEA",
|
||||
"titre_num": "BUT GEA semestre 1",
|
||||
"titre": "BUT GEA",
|
||||
}
|
||||
```
|
||||
{
|
||||
"block_moyennes": false,
|
||||
"bul_bgcolor": "white",
|
||||
"bul_hide_xml": false,
|
||||
"date_debut_iso": "2021-09-01",
|
||||
"date_debut": "01/09/2021",
|
||||
"date_fin_iso": "2022-08-31",
|
||||
"date_fin": "31/08/2022",
|
||||
"dept_id": 1,
|
||||
"elt_annee_apo": null,
|
||||
"elt_sem_apo": null,
|
||||
"ens_can_edit_eval": false,
|
||||
"etat": true,
|
||||
"formation_id": 1,
|
||||
"formsemestre_id": 1,
|
||||
"gestion_compensation": false,
|
||||
"gestion_semestrielle": false,
|
||||
"id": 1,
|
||||
"modalite": "FI",
|
||||
"resp_can_change_ens": true,
|
||||
"resp_can_edit": false,
|
||||
"responsables": [1, 99], // uids
|
||||
"scodoc7_id": null,
|
||||
"semestre_id": 1,
|
||||
"titre_formation" : "BUT GEA",
|
||||
"titre_num": "BUT GEA semestre 1",
|
||||
"titre": "BUT GEA",
|
||||
}
|
||||
|
||||
"""
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
@ -108,28 +99,15 @@ def formsemestre_infos(formsemestre_id: int):
|
||||
@as_json
|
||||
def formsemestres_query():
|
||||
"""
|
||||
Retourne les formsemestres filtrés par étape Apogée ou année scolaire
|
||||
ou département (acronyme ou id) ou état ou code étudiant.
|
||||
Retourne les formsemestres filtrés par
|
||||
étape Apogée ou année scolaire ou département (acronyme ou id) ou état ou code étudiant
|
||||
|
||||
PARAMS
|
||||
------
|
||||
etape_apo : un code étape apogée
|
||||
annee_scolaire : année de début de l'année scolaire
|
||||
dept_acronym : acronyme du département (eg "RT")
|
||||
dept_id : id du département
|
||||
ine ou nip: code d'un étudiant: ramène alors tous les semestres auxquels il est inscrit.
|
||||
etat: 0 si verrouillé, 1 sinon
|
||||
|
||||
QUERY
|
||||
-----
|
||||
etape_apo:<string:etape_apo>
|
||||
annee_scolaire:<string:annee_scolaire>
|
||||
dept_acronym:<string:dept_acronym>
|
||||
dept_id:<int:dept_id>
|
||||
etat:<int:etat>
|
||||
nip:<string:nip>
|
||||
ine:<string:ine>
|
||||
|
||||
"""
|
||||
etape_apo = request.args.get("etape_apo")
|
||||
annee_scolaire = request.args.get("annee_scolaire")
|
||||
@ -146,8 +124,8 @@ def formsemestres_query():
|
||||
annee_scolaire_int = int(annee_scolaire)
|
||||
except ValueError:
|
||||
return json_error(API_CLIENT_ERROR, "invalid annee_scolaire: not int")
|
||||
debut_annee = scu.date_debut_annee_scolaire(annee_scolaire_int)
|
||||
fin_annee = scu.date_fin_annee_scolaire(annee_scolaire_int)
|
||||
debut_annee = scu.date_debut_anne_scolaire(annee_scolaire_int)
|
||||
fin_annee = scu.date_fin_anne_scolaire(annee_scolaire_int)
|
||||
formsemestres = formsemestres.filter(
|
||||
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/<string:version>")
|
||||
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
|
||||
@ -418,12 +181,9 @@ def formsemestre_set_elt_passage_apo():
|
||||
@as_json
|
||||
def bulletins(formsemestre_id: int, version: str = "long"):
|
||||
"""
|
||||
Retourne les bulletins d'un formsemestre.
|
||||
Retourne les bulletins d'un formsemestre donné
|
||||
|
||||
PARAMS
|
||||
------
|
||||
formsemestre_id : int
|
||||
version : string ("long", "short", "selectedevals")
|
||||
formsemestre_id : l'id d'un formesemestre
|
||||
|
||||
Exemple de résultat : liste, voir https://scodoc.org/ScoDoc9API/#bulletin
|
||||
"""
|
||||
@ -455,67 +215,66 @@ def formsemestre_programme(formsemestre_id: int):
|
||||
"""
|
||||
Retourne la liste des UEs, ressources et SAEs d'un semestre
|
||||
|
||||
formsemestre_id : l'id d'un formsemestre
|
||||
|
||||
Exemple de résultat :
|
||||
```json
|
||||
{
|
||||
"ues": [
|
||||
{
|
||||
"type": 0,
|
||||
"formation_id": 1,
|
||||
"ue_code": "UCOD11",
|
||||
"id": 1,
|
||||
"ects": 12.0,
|
||||
"acronyme": "RT1.1",
|
||||
"is_external": false,
|
||||
"numero": 1,
|
||||
"code_apogee": "",
|
||||
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
|
||||
"coefficient": 0.0,
|
||||
"semestre_idx": 1,
|
||||
"color": "#B80004",
|
||||
"ue_id": 1
|
||||
},
|
||||
...
|
||||
],
|
||||
"ressources": [
|
||||
{
|
||||
"ens": [ 10, 18 ],
|
||||
"formsemestre_id": 1,
|
||||
"id": 15,
|
||||
"module": {
|
||||
"abbrev": "Programmer",
|
||||
"code": "SAE15",
|
||||
"code_apogee": "V7GOP",
|
||||
"coefficient": 1.0,
|
||||
"formation_id": 1,
|
||||
"heures_cours": 0.0,
|
||||
"heures_td": 0.0,
|
||||
"heures_tp": 0.0,
|
||||
"id": 15,
|
||||
"matiere_id": 3,
|
||||
"module_id": 15,
|
||||
"module_type": 3,
|
||||
"numero": 50,
|
||||
"semestre_id": 1,
|
||||
"titre": "Programmer en Python",
|
||||
"ue_id": 3
|
||||
},
|
||||
"module_id": 15,
|
||||
"moduleimpl_id": 15,
|
||||
"responsable_id": 2
|
||||
},
|
||||
...
|
||||
],
|
||||
"saes": [
|
||||
{
|
||||
"ues": [
|
||||
{
|
||||
"type": 0,
|
||||
"formation_id": 1,
|
||||
"ue_code": "UCOD11",
|
||||
"id": 1,
|
||||
"ects": 12.0,
|
||||
"acronyme": "RT1.1",
|
||||
"is_external": false,
|
||||
"numero": 1,
|
||||
"code_apogee": "",
|
||||
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
|
||||
"coefficient": 0.0,
|
||||
"semestre_idx": 1,
|
||||
"color": "#B80004",
|
||||
"ue_id": 1
|
||||
},
|
||||
...
|
||||
},
|
||||
...
|
||||
],
|
||||
"modules" : [ ... les modules qui ne sont ni des SAEs ni des ressources ... ]
|
||||
}
|
||||
```
|
||||
],
|
||||
"ressources": [
|
||||
{
|
||||
"ens": [ 10, 18 ],
|
||||
"formsemestre_id": 1,
|
||||
"id": 15,
|
||||
"module": {
|
||||
"abbrev": "Programmer",
|
||||
"code": "SAE15",
|
||||
"code_apogee": "V7GOP",
|
||||
"coefficient": 1.0,
|
||||
"formation_id": 1,
|
||||
"heures_cours": 0.0,
|
||||
"heures_td": 0.0,
|
||||
"heures_tp": 0.0,
|
||||
"id": 15,
|
||||
"matiere_id": 3,
|
||||
"module_id": 15,
|
||||
"module_type": 3,
|
||||
"numero": 50,
|
||||
"semestre_id": 1,
|
||||
"titre": "Programmer en Python",
|
||||
"ue_id": 3
|
||||
},
|
||||
"module_id": 15,
|
||||
"moduleimpl_id": 15,
|
||||
"responsable_id": 2
|
||||
},
|
||||
...
|
||||
],
|
||||
"saes": [
|
||||
{
|
||||
...
|
||||
},
|
||||
...
|
||||
],
|
||||
"modules" : [ ... les modules qui ne sont ni des SAEs ni des ressources ... ]
|
||||
}
|
||||
"""
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
@ -579,16 +338,7 @@ def formsemestre_programme(formsemestre_id: int):
|
||||
def formsemestre_etudiants(
|
||||
formsemestre_id: int, with_query: bool = False, long: bool = False
|
||||
):
|
||||
"""Étudiants d'un formsemestre.
|
||||
|
||||
Si l'état est spécifié, ne renvoie que les inscrits (`I`), les
|
||||
démissionnaires (`D`) ou les défaillants (`DEF`)
|
||||
|
||||
QUERY
|
||||
-----
|
||||
etat:<string:etat>
|
||||
|
||||
"""
|
||||
"""Étudiants d'un formsemestre."""
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
@ -610,8 +360,7 @@ def formsemestre_etudiants(
|
||||
inscriptions = formsemestre.inscriptions
|
||||
|
||||
if long:
|
||||
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
||||
etuds = [ins.etud.to_dict_api(restrict=restrict) for ins in inscriptions]
|
||||
etuds = [ins.etud.to_dict_api() for ins in inscriptions]
|
||||
else:
|
||||
etuds = [ins.etud.to_dict_short() for ins in inscriptions]
|
||||
# Ajout des groupes de chaque étudiants
|
||||
@ -630,13 +379,13 @@ def formsemestre_etudiants(
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def formsemestre_etat_evaluations(formsemestre_id: int):
|
||||
def etat_evals(formsemestre_id: int):
|
||||
"""
|
||||
Informations sur l'état des évaluations d'un formsemestre.
|
||||
|
||||
Exemple de résultat :
|
||||
formsemestre_id : l'id d'un semestre
|
||||
|
||||
```json
|
||||
Exemple de résultat :
|
||||
[
|
||||
{
|
||||
"id": 1, // moduleimpl_id
|
||||
@ -664,9 +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)
|
||||
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:
|
||||
modimpl_results: ModuleImplResults = nt.modimpls_results[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 = []
|
||||
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
|
||||
|
||||
eval_dict["saisie_notes"] = {
|
||||
"datetime_debut": (
|
||||
date_debut.isoformat() if date_debut is not None else None
|
||||
),
|
||||
"datetime_debut": date_debut.isoformat()
|
||||
if date_debut is not None
|
||||
else None,
|
||||
"datetime_fin": date_fin.isoformat() if date_fin is not None else None,
|
||||
"datetime_mediane": (
|
||||
date_mediane.isoformat() if date_mediane is not None else None
|
||||
),
|
||||
"datetime_mediane": date_mediane.isoformat()
|
||||
if date_mediane is not None
|
||||
else None,
|
||||
}
|
||||
|
||||
list_eval.append(eval_dict)
|
||||
@ -739,16 +490,8 @@ def formsemestre_etat_evaluations(formsemestre_id: int):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def formsemestre_resultat(formsemestre_id: int):
|
||||
"""Tableau récapitulatif des résultats.
|
||||
|
||||
"""Tableau récapitulatif des résultats
|
||||
Pour chaque étudiant, son état, ses groupes, ses moyennes d'UE et de modules.
|
||||
|
||||
Si `format=raw`, ne converti pas les valeurs.
|
||||
|
||||
QUERY
|
||||
-----
|
||||
format:<string:format>
|
||||
|
||||
"""
|
||||
format_spec = request.args.get("format", None)
|
||||
if format_spec is not None and format_spec != "raw":
|
||||
@ -761,41 +504,27 @@ def formsemestre_resultat(formsemestre_id: int):
|
||||
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
||||
app.set_sco_dept(formsemestre.departement.acronym)
|
||||
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(
|
||||
res,
|
||||
convert_values=convert_values,
|
||||
include_evaluations=False,
|
||||
mode_jury=False,
|
||||
row_class=RowRecapAPI,
|
||||
res, convert_values=convert_values, include_evaluations=False, mode_jury=False
|
||||
)
|
||||
|
||||
# Supprime les champs inutiles (mise en forme)
|
||||
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
|
||||
|
||||
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/groups_get_auto_assignment")
|
||||
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/groups_get_auto_assignment")
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/get_groups_auto_assignment")
|
||||
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/get_groups_auto_assignment")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def groups_get_auto_assignment(formsemestre_id: int):
|
||||
"""Rend les données stockées par `groups_save_auto_assignment`."""
|
||||
def get_groups_auto_assignment(formsemestre_id: int):
|
||||
"""rend les données"""
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
@ -806,27 +535,22 @@ def groups_get_auto_assignment(formsemestre_id: int):
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/formsemestre/<int:formsemestre_id>/groups_save_auto_assignment", methods=["POST"]
|
||||
"/formsemestre/<int:formsemestre_id>/save_groups_auto_assignment", methods=["POST"]
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formsemestre/<int:formsemestre_id>/groups_save_auto_assignment", methods=["POST"]
|
||||
"/formsemestre/<int:formsemestre_id>/save_groups_auto_assignment", methods=["POST"]
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def groups_save_auto_assignment(formsemestre_id: int):
|
||||
"""Enregistre les données, associées à ce formsemestre.
|
||||
Usage réservé aux fonctions de gestion des groupes, ne pas utiliser ailleurs.
|
||||
"""
|
||||
def save_groups_auto_assignment(formsemestre_id: int):
|
||||
"""enregistre les données"""
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
||||
|
||||
if not formsemestre.can_change_groups():
|
||||
return json_error(403, "non autorisé (can_change_groups)")
|
||||
|
||||
if len(request.data) > GROUPS_AUTO_ASSIGNMENT_DATA_MAX:
|
||||
return json_error(413, "data too large")
|
||||
formsemestre.groups_auto_assignment_data = request.data
|
||||
@ -841,23 +565,9 @@ def groups_save_auto_assignment(formsemestre_id: int):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def formsemestre_edt(formsemestre_id: int):
|
||||
"""L'emploi du temps du semestre.
|
||||
|
||||
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.
|
||||
"""
|
||||
"""l'emploi du temps du semestre"""
|
||||
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)
|
||||
group_ids = request.args.getlist("group_ids", int)
|
||||
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
|
||||
)
|
||||
return sco_edt_cal.formsemestre_edt_dict(formsemestre)
|
||||
|
@ -1,16 +1,11 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
ScoDoc 9 API : jury WIP à compléter avec enregistrement décisions.
|
||||
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
Jury
|
||||
ScoDoc 9 API : jury WIP à compléter avec enregistrement décisions
|
||||
"""
|
||||
|
||||
import datetime
|
||||
@ -22,8 +17,7 @@ from flask_login import current_user, login_required
|
||||
import app
|
||||
from app import db, log
|
||||
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR, tools
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.decorators import scodoc
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.scodoc.sco_exceptions import ScoException
|
||||
from app.but import jury_but_results
|
||||
from app.models import (
|
||||
@ -38,7 +32,6 @@ from app.models import (
|
||||
ScolarNews,
|
||||
Scolog,
|
||||
UniteEns,
|
||||
ValidationDUT120,
|
||||
)
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_cache
|
||||
@ -56,11 +49,6 @@ def decisions_jury(formsemestre_id: int):
|
||||
"""Décisions du jury des étudiants du formsemestre."""
|
||||
# APC, pair:
|
||||
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
|
||||
if formsemestre is None:
|
||||
return json_error(
|
||||
404,
|
||||
message="formsemestre inconnu",
|
||||
)
|
||||
if formsemestre.formation.is_apc():
|
||||
app.set_sco_dept(formsemestre.departement.acronym)
|
||||
rows = jury_but_results.get_jury_but_results(formsemestre)
|
||||
@ -69,16 +57,16 @@ def decisions_jury(formsemestre_id: int):
|
||||
raise ScoException("non implemente")
|
||||
|
||||
|
||||
def _news_delete_jury_etud(etud: Identite, detail: str = ""):
|
||||
def _news_delete_jury_etud(etud: Identite):
|
||||
"génère news sur effacement décision"
|
||||
# n'utilise pas g.scodoc_dept, pas toujours dispo en mode API
|
||||
url = url_for(
|
||||
"scolar.fiche_etud", scodoc_dept=etud.departement.acronym, etudid=etud.id
|
||||
"scolar.ficheEtud", scodoc_dept=etud.departement.acronym, etudid=etud.id
|
||||
)
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_JURY,
|
||||
obj=etud.id,
|
||||
text=f"""Suppression décision jury {detail} pour <a href="{url}">{etud.nomprenom}</a>""",
|
||||
text=f"""Suppression décision jury pour <a href="{url}">{etud.nomprenom}</a>""",
|
||||
url=url,
|
||||
)
|
||||
|
||||
@ -96,7 +84,7 @@ def _news_delete_jury_etud(etud: Identite, detail: str = ""):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def validation_ue_delete(etudid: int, validation_id: int):
|
||||
"Efface cette validation d'UE."
|
||||
"Efface cette validation"
|
||||
return _validation_ue_delete(etudid, validation_id)
|
||||
|
||||
|
||||
@ -113,7 +101,7 @@ def validation_ue_delete(etudid: int, validation_id: int):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def validation_formsemestre_delete(etudid: int, validation_id: int):
|
||||
"Efface cette validation de semestre."
|
||||
"Efface cette validation"
|
||||
# c'est la même chose (formations classiques)
|
||||
return _validation_ue_delete(etudid, validation_id)
|
||||
|
||||
@ -165,7 +153,7 @@ def _validation_ue_delete(etudid: int, validation_id: int):
|
||||
@permission_required(Permission.EtudInscrit)
|
||||
@as_json
|
||||
def autorisation_inscription_delete(etudid: int, validation_id: int):
|
||||
"Efface cette autorisation d'inscription."
|
||||
"Efface cette validation"
|
||||
etud = tools.get_etud(etudid)
|
||||
if etud is None:
|
||||
return "étudiant inconnu", 404
|
||||
@ -194,12 +182,8 @@ def autorisation_inscription_delete(etudid: int, validation_id: int):
|
||||
@as_json
|
||||
def validation_rcue_record(etudid: int):
|
||||
"""Enregistre une validation de RCUE.
|
||||
|
||||
Si une validation existe déjà pour ce RCUE, la remplace.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
The request content type should be "application/json":
|
||||
{
|
||||
"code" : str,
|
||||
"ue1_id" : int,
|
||||
@ -209,7 +193,6 @@ def validation_rcue_record(etudid: int):
|
||||
"date" : date_iso, // si non spécifié, now()
|
||||
"parcours_id" :int,
|
||||
}
|
||||
```
|
||||
"""
|
||||
etud = tools.get_etud(etudid)
|
||||
if etud is None:
|
||||
@ -301,12 +284,13 @@ def validation_rcue_record(etudid: int):
|
||||
db.session.add(validation)
|
||||
# invalider bulletins (les autres résultats ne dépendent pas des RCUEs):
|
||||
sco_cache.invalidate_formsemestre_etud(etud)
|
||||
db.session.commit()
|
||||
Scolog.logdb(
|
||||
method="validation_rcue_record",
|
||||
etudid=etudid,
|
||||
msg=f"Enregistrement {validation}",
|
||||
commit=True,
|
||||
)
|
||||
db.session.commit()
|
||||
log(f"{operation} {validation}")
|
||||
return validation.to_dict()
|
||||
|
||||
@ -324,18 +308,18 @@ def validation_rcue_record(etudid: int):
|
||||
@permission_required(Permission.EtudInscrit)
|
||||
@as_json
|
||||
def validation_rcue_delete(etudid: int, validation_id: int):
|
||||
"Efface cette validation de RCUE."
|
||||
"Efface cette validation"
|
||||
etud = tools.get_etud(etudid)
|
||||
if etud is None:
|
||||
return "étudiant inconnu", 404
|
||||
validation = ApcValidationRCUE.query.filter_by(
|
||||
id=validation_id, etudid=etudid
|
||||
).first_or_404()
|
||||
log(f"delete validation_ue_delete: etuid={etudid} {validation}")
|
||||
log(f"validation_ue_delete: etuid={etudid} {validation}")
|
||||
db.session.delete(validation)
|
||||
sco_cache.invalidate_formsemestre_etud(etud)
|
||||
db.session.commit()
|
||||
_news_delete_jury_etud(etud, detail="UE")
|
||||
_news_delete_jury_etud(etud)
|
||||
return "ok"
|
||||
|
||||
|
||||
@ -352,45 +336,16 @@ def validation_rcue_delete(etudid: int, validation_id: int):
|
||||
@permission_required(Permission.EtudInscrit)
|
||||
@as_json
|
||||
def validation_annee_but_delete(etudid: int, validation_id: int):
|
||||
"Efface cette validation d'année BUT."
|
||||
"Efface cette validation"
|
||||
etud = tools.get_etud(etudid)
|
||||
if etud is None:
|
||||
return "étudiant inconnu", 404
|
||||
validation = ApcValidationAnnee.query.filter_by(
|
||||
id=validation_id, etudid=etudid
|
||||
).first_or_404()
|
||||
ordre = validation.ordre
|
||||
log(f"delete validation_annee_but: etuid={etudid} {validation}")
|
||||
log(f"validation_annee_but: etuid={etudid} {validation}")
|
||||
db.session.delete(validation)
|
||||
sco_cache.invalidate_formsemestre_etud(etud)
|
||||
db.session.commit()
|
||||
_news_delete_jury_etud(etud, detail=f"année BUT{ordre}")
|
||||
return "ok"
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_dut120/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_dut120/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.EtudInscrit)
|
||||
@as_json
|
||||
def validation_dut120_delete(etudid: int, validation_id: int):
|
||||
"Efface cette validation de DUT120."
|
||||
etud = tools.get_etud(etudid)
|
||||
if etud is None:
|
||||
return "étudiant inconnu", 404
|
||||
validation = ValidationDUT120.query.filter_by(
|
||||
id=validation_id, etudid=etudid
|
||||
).first_or_404()
|
||||
log(f"delete validation_dut120: etuid={etudid} {validation}")
|
||||
db.session.delete(validation)
|
||||
sco_cache.invalidate_formsemestre_etud(etud)
|
||||
db.session.commit()
|
||||
_news_delete_jury_etud(etud, detail="diplôme DUT120")
|
||||
_news_delete_jury_etud(etud)
|
||||
return "ok"
|
||||
|
@ -3,26 +3,31 @@
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
"""ScoDoc 9 API : Justificatifs"""
|
||||
|
||||
"""ScoDoc 9 API : Justificatifs
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from flask_json import as_json
|
||||
from flask import g, request
|
||||
from flask_login import login_required, current_user
|
||||
from flask_sqlalchemy.query import Query
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
import app.scodoc.sco_assiduites as scass
|
||||
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_web_bp
|
||||
from app.api import get_model_api_object, tools
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.decorators import scodoc
|
||||
from app.models import Identite, Justificatif, Departement, FormSemestre, Scolog
|
||||
from app.decorators import permission_required, scodoc
|
||||
from app.models import (
|
||||
Identite,
|
||||
Justificatif,
|
||||
Departement,
|
||||
FormSemestre,
|
||||
FormSemestreInscription,
|
||||
)
|
||||
from app.models.assiduites import (
|
||||
compute_assiduites_justified,
|
||||
get_formsemestre_from_data,
|
||||
)
|
||||
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
|
||||
@ -38,11 +43,9 @@ from app.scodoc.sco_groups import get_group_members
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def justificatif(justif_id: int = None):
|
||||
"""Retourne un objet justificatif à partir de son id.
|
||||
"""Retourne un objet justificatif à partir de son id
|
||||
|
||||
Exemple de résultat:
|
||||
|
||||
```json
|
||||
{
|
||||
"justif_id": 1,
|
||||
"etudid": 2,
|
||||
@ -50,24 +53,14 @@ def justificatif(justif_id: int = None):
|
||||
"date_fin": "2022-10-31T10:00+01:00",
|
||||
"etat": "valide",
|
||||
"fichier": "archive_id",
|
||||
"raison": "une raison", // VIDE si pas le droit
|
||||
"raison": "une raison",
|
||||
"entry_date": "2022-10-31T08:00+01:00",
|
||||
"user_id": 1 or null,
|
||||
}
|
||||
```
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/justificatif/1;
|
||||
|
||||
"""
|
||||
|
||||
return get_model_api_object(
|
||||
Justificatif,
|
||||
justif_id,
|
||||
Identite,
|
||||
restrict=not current_user.has_permission(Permission.AbsJustifView),
|
||||
)
|
||||
return get_model_api_object(Justificatif, justif_id, Identite)
|
||||
|
||||
|
||||
# etudid
|
||||
@ -99,32 +92,28 @@ def justificatif(justif_id: int = None):
|
||||
def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = False):
|
||||
"""
|
||||
Retourne toutes les assiduités d'un étudiant
|
||||
chemin : /justificatifs/<int:etudid>
|
||||
|
||||
QUERY
|
||||
-----
|
||||
user_id:<int:user_id>
|
||||
date_debut:<string:date_debut_iso>
|
||||
date_fin:<string:date_fin_iso>
|
||||
etat:<array[string]:etat>
|
||||
order:<bool:order>
|
||||
courant:<bool:courant>
|
||||
group_id:<int:group_id>
|
||||
|
||||
PARAMS
|
||||
-----
|
||||
user_id:l'id de l'auteur du justificatif
|
||||
date_debut:date de début du justificatif (supérieur ou égal)
|
||||
date_fin:date de fin du justificatif (inférieur ou égal)
|
||||
etat:etat du justificatif → valide, non_valide, attente, modifie
|
||||
order:retourne les justificatifs dans l'ordre décroissant (non vide = True)
|
||||
courant:retourne les justificatifs de l'année courante (bool : v/t ou f)
|
||||
group_id:<int:group_id>
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/justificatifs/1;
|
||||
/justificatifs/1/query?etat=attente;
|
||||
Un filtrage peut être donné avec une query
|
||||
chemin : /justificatifs/<int:etudid>/query?
|
||||
|
||||
Les différents filtres :
|
||||
Etat (etat du justificatif -> validé, non validé, modifé, en attente):
|
||||
query?etat=[- liste des états séparé par une virgule -]
|
||||
ex: .../query?etat=validé,modifié
|
||||
Date debut
|
||||
(date de début du justificatif, sont affichés les justificatifs
|
||||
dont la date de début est supérieur ou égale à la valeur donnée):
|
||||
query?date_debut=[- date au format iso -]
|
||||
ex: query?date_debut=2022-11-03T08:00+01:00
|
||||
Date fin
|
||||
(date de fin du justificatif, sont affichés les justificatifs
|
||||
dont la date de fin est inférieure ou égale à la valeur donnée):
|
||||
query?date_fin=[- date au format iso -]
|
||||
ex: query?date_fin=2022-11-03T10:00+01:00
|
||||
user_id (l'id de l'auteur du justificatif)
|
||||
query?user_id=[int]
|
||||
ex query?user_id=3
|
||||
"""
|
||||
# Récupération de l'étudiant
|
||||
etud: Identite = tools.get_etud(etudid, nip, ine)
|
||||
@ -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
|
||||
data_set: list[dict] = []
|
||||
restrict = not current_user.has_permission(Permission.AbsJustifView)
|
||||
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)
|
||||
|
||||
return data_set
|
||||
@ -163,41 +151,10 @@ def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = Fal
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
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
|
||||
dept: Departement = Departement.query.get(dept_id)
|
||||
if dept is None:
|
||||
return json_error(404, "Assiduité non existante")
|
||||
dept: Departement = Departement.query.get_or_404(dept_id)
|
||||
etuds: list[int] = [etud.id for etud in dept.etudiants]
|
||||
|
||||
# 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)
|
||||
|
||||
# Mise en forme des données et retour JSON
|
||||
restrict = not current_user.has_permission(Permission.AbsJustifView)
|
||||
data_set: list[dict] = []
|
||||
for just in justificatifs_query:
|
||||
data_set.append(_set_sems(just, restrict=restrict))
|
||||
data_set.append(_set_sems(just))
|
||||
|
||||
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
|
||||
|
||||
@ -231,7 +187,7 @@ def _set_sems(justi: Justificatif, restrict: bool) -> dict:
|
||||
dict: La représentation de l'assiduité 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é
|
||||
formsemestre: FormSemestre = get_formsemestre_from_data(justi.to_dict())
|
||||
@ -263,34 +219,7 @@ def _set_sems(justi: Justificatif, restrict: bool) -> dict:
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False):
|
||||
"""Retourne tous les justificatifs du formsemestre
|
||||
|
||||
QUERY
|
||||
-----
|
||||
user_id:<int:user_id>
|
||||
est_just:<bool:est_just>
|
||||
date_debut:<string:date_debut_iso>
|
||||
date_fin:<string:date_fin_iso>
|
||||
etat:<array[string]:etat>
|
||||
order:<bool:order>
|
||||
courant:<bool:courant>
|
||||
group_id:<int:group_id>
|
||||
|
||||
PARAMS
|
||||
-----
|
||||
user_id:l'id de l'auteur du justificatif
|
||||
date_debut:date de début du justificatif (supérieur ou égal)
|
||||
date_fin:date de fin du justificatif (inférieur ou égal)
|
||||
etat:etat du justificatif → valide, non_valide, attente, modifie
|
||||
order:retourne les justificatifs dans l'ordre décroissant (non vide = True)
|
||||
courant:retourne les justificatifs de l'année courante (bool : v/t ou f)
|
||||
group_id:<int:group_id>
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/justificatifs/formsemestre/1;
|
||||
|
||||
"""
|
||||
"""Retourne tous les justificatifs du formsemestre"""
|
||||
|
||||
# Récupération du formsemestre
|
||||
formsemestre: FormSemestre = None
|
||||
@ -312,10 +241,9 @@ def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False):
|
||||
justificatifs_query: Query = _filter_manager(request, justificatifs_query)
|
||||
|
||||
# Retour des justificatifs en JSON
|
||||
restrict = not current_user.has_permission(Permission.AbsJustifView)
|
||||
data_set: list[dict] = []
|
||||
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)
|
||||
|
||||
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):
|
||||
"""
|
||||
Création d'un justificatif pour l'étudiant (etudid)
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
La requête doit avoir un content type "application/json":
|
||||
[
|
||||
{
|
||||
"date_debut": str,
|
||||
@ -356,10 +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,
|
||||
message="étudiant inconnu",
|
||||
)
|
||||
set_sco_dept(etud.departement.acronym)
|
||||
|
||||
# Récupération des justificatifs à créer
|
||||
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] = []
|
||||
success: list[dict] = []
|
||||
justifs: list[Justificatif] = []
|
||||
|
||||
# énumération des justificatifs
|
||||
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})
|
||||
else:
|
||||
success.append({"indice": i, "message": obj})
|
||||
justi.justifier_assiduites()
|
||||
justifs.append(justi)
|
||||
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}
|
||||
|
||||
|
||||
@ -413,10 +336,6 @@ def _create_one(
|
||||
errors.append("param 'etat': invalide")
|
||||
|
||||
etat: scu.EtatJustificatif = scu.EtatJustificatif.get(etat)
|
||||
if etat != scu.EtatJustificatif.ATTENTE and not current_user.has_permission(
|
||||
Permission.JustifValidate
|
||||
):
|
||||
errors.append("param 'etat': non autorisé (Permission.JustifValidate)")
|
||||
|
||||
# cas 2 : date_debut
|
||||
date_debut: str = data.get("date_debut", None)
|
||||
@ -455,7 +374,7 @@ def _create_one(
|
||||
date_debut=deb,
|
||||
date_fin=fin,
|
||||
etat=etat,
|
||||
etudiant=etud,
|
||||
etud=etud,
|
||||
raison=raison,
|
||||
user_id=current_user.id,
|
||||
external_data=external_data,
|
||||
@ -490,27 +409,20 @@ def _create_one(
|
||||
def justif_edit(justif_id: int):
|
||||
"""
|
||||
Edition d'un justificatif à partir de son id
|
||||
La requête doit avoir un content type "application/json":
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
{
|
||||
"etat"?: str,
|
||||
"raison"?: str
|
||||
"date_debut"?: str
|
||||
"date_fin"?: str
|
||||
}
|
||||
```
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/justificatif/1/edit;{""etat"":""valide""}
|
||||
/justificatif/1/edit;{""raison"":""MEDIC""}
|
||||
|
||||
"""
|
||||
|
||||
# Récupération du justificatif à modifier
|
||||
justificatif_unique = Justificatif.get_justificatif(justif_id)
|
||||
justificatif_unique: Query = Justificatif.query.filter_by(
|
||||
id=justif_id
|
||||
).first_or_404()
|
||||
|
||||
errors: list[str] = []
|
||||
data = request.get_json(force=True)
|
||||
@ -525,10 +437,7 @@ def justif_edit(justif_id: int):
|
||||
if etat is None:
|
||||
errors.append("param 'etat': invalide")
|
||||
else:
|
||||
if current_user.has_permission(Permission.JustifValidate):
|
||||
justificatif_unique.etat = etat
|
||||
else:
|
||||
errors.append("param 'etat': non autorisé (Permission.JustifValidate)")
|
||||
justificatif_unique.etat = etat
|
||||
|
||||
# Cas 2 : raison
|
||||
raison: str = data.get("raison", False)
|
||||
@ -579,13 +488,7 @@ def justif_edit(justif_id: int):
|
||||
return json_error(404, err)
|
||||
|
||||
# Mise à jour du justificatif
|
||||
justificatif_unique.dejustifier_assiduites()
|
||||
db.session.add(justificatif_unique)
|
||||
Scolog.logdb(
|
||||
method="edit_justificatif",
|
||||
etudid=justificatif_unique.etudiant.id,
|
||||
msg=f"justificatif modif: {justificatif_unique}",
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
# Génération du dictionnaire de retour
|
||||
@ -595,7 +498,11 @@ def justif_edit(justif_id: int):
|
||||
retour = {
|
||||
"couverture": {
|
||||
"avant": avant_ids,
|
||||
"apres": justificatif_unique.justifier_assiduites(),
|
||||
"après": compute_assiduites_justified(
|
||||
justificatif_unique.etudid,
|
||||
[justificatif_unique],
|
||||
True,
|
||||
),
|
||||
}
|
||||
}
|
||||
# Invalide le cache
|
||||
@ -613,18 +520,13 @@ def justif_delete():
|
||||
"""
|
||||
Suppression d'un justificatif à partir de son id
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
Forme des données envoyées :
|
||||
|
||||
[
|
||||
<justif_id:int>,
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/justificatif/delete;[2, 2, 3]
|
||||
|
||||
"""
|
||||
|
||||
@ -660,10 +562,12 @@ def _delete_one(justif_id: int) -> tuple[int, str]:
|
||||
message : OK si réussi, message d'erreur sinon
|
||||
"""
|
||||
# Récupération du justificatif à supprimer
|
||||
try:
|
||||
justificatif_unique = Justificatif.get_justificatif(justif_id)
|
||||
except NotFound:
|
||||
justificatif_unique: Justificatif = Justificatif.query.filter_by(
|
||||
id=justif_id
|
||||
).first()
|
||||
if justificatif_unique is None:
|
||||
return (404, "Justificatif non existant")
|
||||
|
||||
# Récupération de l'archive du justificatif
|
||||
archive_name: str = justificatif_unique.fichier
|
||||
|
||||
@ -677,15 +581,14 @@ def _delete_one(justif_id: int) -> tuple[int, str]:
|
||||
|
||||
# On invalide le cache
|
||||
scass.simple_invalidate_cache(justificatif_unique.to_dict())
|
||||
# On actualise les assiduités justifiées de l'étudiant concerné
|
||||
justificatif_unique.dejustifier_assiduites()
|
||||
Scolog.logdb(
|
||||
method="justificatif/delete",
|
||||
etudid=justificatif_unique.etudiant.id,
|
||||
msg="suppression justificatif",
|
||||
)
|
||||
# On supprime le justificatif
|
||||
db.session.delete(justificatif_unique)
|
||||
# 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")
|
||||
|
||||
@ -700,8 +603,6 @@ def _delete_one(justif_id: int) -> tuple[int, str]:
|
||||
def justif_import(justif_id: int = None):
|
||||
"""
|
||||
Importation d'un fichier (création d'archive)
|
||||
|
||||
> Procédure d'importation de fichier : [importer un justificatif](FichiersJustificatifs.md#importer-un-fichier)
|
||||
"""
|
||||
|
||||
# On vérifie qu'un fichier a bien été envoyé
|
||||
@ -712,7 +613,10 @@ def justif_import(justif_id: int = None):
|
||||
return json_error(404, "Il n'y a pas de fichier joint")
|
||||
|
||||
# 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
|
||||
archive_name: str = justificatif_unique.fichier
|
||||
@ -737,34 +641,26 @@ def justif_import(justif_id: int = None):
|
||||
db.session.commit()
|
||||
|
||||
return {"filename": fname}
|
||||
except ScoValueError as exc:
|
||||
except ScoValueError as err:
|
||||
# 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"])
|
||||
@api_web_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("/justificatif/<int:justif_id>/export/<filename>", methods=["POST"])
|
||||
@scodoc
|
||||
@login_required
|
||||
@permission_required(Permission.ScoView)
|
||||
def justif_export(justif_id: int | None = None, filename: str | None = None):
|
||||
@permission_required(Permission.AbsChange)
|
||||
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é
|
||||
justificatif_unique = Justificatif.get_justificatif(justif_id)
|
||||
|
||||
# Vérification des permissions
|
||||
if not (
|
||||
current_user.has_permission(Permission.AbsJustifView)
|
||||
or justificatif_unique.user_id == current_user.id
|
||||
):
|
||||
return json_error(401, "non autorisé à voir ce fichier")
|
||||
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 concernée
|
||||
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):
|
||||
"""
|
||||
Supression d'un fichier ou d'une archive
|
||||
|
||||
> Procédure de suppression de fichier : [supprimer un justificatif](FichiersJustificatifs.md#supprimer-un-fichier)
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
{
|
||||
"remove": <"all"/"list">,
|
||||
"remove": <"all"/"list">
|
||||
|
||||
"filenames"?: [
|
||||
<filename:str>,
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
"""
|
||||
|
||||
# On récupère le dictionnaire
|
||||
data: dict = request.get_json(force=True)
|
||||
|
||||
# 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
|
||||
archive_name: str = justificatif_unique.fichier
|
||||
@ -871,15 +764,13 @@ def justif_remove(justif_id: int = None):
|
||||
def justif_list(justif_id: int = None):
|
||||
"""
|
||||
Liste les fichiers du justificatif
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/justificatif/1/list;
|
||||
|
||||
"""
|
||||
|
||||
# Récupération du justificatif concerné
|
||||
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
|
||||
archive_name: str = justificatif_unique.fichier
|
||||
@ -918,15 +809,13 @@ def justif_list(justif_id: int = None):
|
||||
def justif_justifies(justif_id: int = None):
|
||||
"""
|
||||
Liste assiduite_id justifiées par le justificatif
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/justificatif/1/justifies;
|
||||
|
||||
"""
|
||||
|
||||
# On récupère le justificatif concerné
|
||||
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
|
||||
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):
|
||||
"""
|
||||
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
|
||||
etat: str = requested.args.get("etat")
|
||||
@ -975,7 +863,7 @@ def _filter_manager(requested, justificatifs_query: Query):
|
||||
formsemestre: FormSemestre = None
|
||||
try:
|
||||
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, Justificatif, formsemestre
|
||||
)
|
||||
@ -994,8 +882,8 @@ def _filter_manager(requested, justificatifs_query: Query):
|
||||
annee: int = scu.annee_scolaire()
|
||||
|
||||
justificatifs_query: Query = justificatifs_query.filter(
|
||||
Justificatif.date_debut >= scu.date_debut_annee_scolaire(annee),
|
||||
Justificatif.date_fin <= scu.date_fin_annee_scolaire(annee),
|
||||
Justificatif.date_debut >= scu.date_debut_anne_scolaire(annee),
|
||||
Justificatif.date_fin <= scu.date_fin_anne_scolaire(annee),
|
||||
)
|
||||
|
||||
# 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:
|
||||
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
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# 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
|
||||
# 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 app.api import api_bp as bp
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.decorators import scodoc
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.models import Departement
|
||||
from app.scodoc.sco_logos import list_logos, find_logo
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_utils import json_error
|
||||
|
||||
|
||||
# Note: l'API logos n'est accessible qu'en mode global (avec jeton, sans dept)
|
||||
|
||||
@ -49,8 +47,8 @@ from app.scodoc.sco_utils import json_error
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def logo_list_globals():
|
||||
"""Liste des noms des logos définis pour le site ScoDoc."""
|
||||
def api_get_glob_logos():
|
||||
"""Liste tous les logos"""
|
||||
logos = list_logos()[None]
|
||||
return list(logos.keys())
|
||||
|
||||
@ -58,12 +56,7 @@ def logo_list_globals():
|
||||
@bp.route("/logo/<string:logoname>")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
def logo_get_global(logoname):
|
||||
"""Renvoie le logo global de nom donné.
|
||||
|
||||
L'image est au format png ou jpg; le format retourné dépend du format sous lequel
|
||||
l'image a été initialement enregistrée.
|
||||
"""
|
||||
def api_get_glob_logo(logoname):
|
||||
logo = find_logo(logoname=logoname)
|
||||
if logo is None:
|
||||
return json_error(404, message="logo not found")
|
||||
@ -84,10 +77,7 @@ def _core_get_logos(dept_id) -> list:
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def logo_get_local_by_acronym(departement):
|
||||
"""Liste des noms des logos définis pour le département
|
||||
désigné par son acronyme.
|
||||
"""
|
||||
def api_get_local_logos_by_acronym(departement):
|
||||
dept_id = Departement.from_acronym(departement).id
|
||||
return _core_get_logos(dept_id)
|
||||
|
||||
@ -96,10 +86,7 @@ def logo_get_local_by_acronym(departement):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def logo_get_local_by_id(dept_id):
|
||||
"""Liste des noms des logos définis pour le département
|
||||
désigné par son id.
|
||||
"""
|
||||
def api_get_local_logos_by_id(dept_id):
|
||||
return _core_get_logos(dept_id)
|
||||
|
||||
|
||||
@ -118,13 +105,7 @@ def _core_get_logo(dept_id, logoname) -> Response:
|
||||
@bp.route("/departement/<string:departement>/logo/<string:logoname>")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
def logo_get_local_dept_by_acronym(departement, logoname):
|
||||
"""Le logo: image (format png ou jpg).
|
||||
|
||||
**Exemple d'utilisation:**
|
||||
|
||||
* `/ScoDoc/api/departement/MMI/logo/header`
|
||||
"""
|
||||
def api_get_local_logo_dept_by_acronym(departement, logoname):
|
||||
dept_id = Departement.from_acronym(departement).id
|
||||
return _core_get_logo(dept_id, logoname)
|
||||
|
||||
@ -132,11 +113,5 @@ def logo_get_local_dept_by_acronym(departement, logoname):
|
||||
@bp.route("/departement/id/<int:dept_id>/logo/<string:logoname>")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
def logo_get_local_dept_by_id(dept_id, logoname):
|
||||
"""Le logo: image (format png ou jpg).
|
||||
|
||||
**Exemple d'utilisation:**
|
||||
|
||||
* `/ScoDoc/api/departement/id/3/logo/header`
|
||||
"""
|
||||
def api_get_local_logo_dept_by_id(dept_id, logoname):
|
||||
return _core_get_logo(dept_id, logoname)
|
||||
|
@ -1,26 +1,23 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
ScoDoc 9 API : accès aux moduleimpl
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
ModuleImpl
|
||||
"""
|
||||
|
||||
from flask import g
|
||||
from flask_json import as_json
|
||||
from flask_login import login_required
|
||||
|
||||
import app
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.decorators import scodoc
|
||||
from app.models import ModuleImpl
|
||||
from app.scodoc import sco_liste_notes
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import (
|
||||
FormSemestre,
|
||||
ModuleImpl,
|
||||
)
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
|
||||
@ -32,45 +29,43 @@ from app.scodoc.sco_permissions import Permission
|
||||
@as_json
|
||||
def moduleimpl(moduleimpl_id: int):
|
||||
"""
|
||||
Retourne le moduleimpl.
|
||||
Retourne un moduleimpl en fonction de son id
|
||||
|
||||
PARAMS
|
||||
------
|
||||
moduleimpl_id : l'id d'un moduleimpl
|
||||
|
||||
Exemple de résultat :
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"formsemestre_id": 1,
|
||||
"module_id": 1,
|
||||
"responsable_id": 2,
|
||||
"moduleimpl_id": 1,
|
||||
"ens": [],
|
||||
"module": {
|
||||
"heures_tp": 0,
|
||||
"code_apogee": "",
|
||||
"titre": "Initiation aux réseaux informatiques",
|
||||
"coefficient": 1,
|
||||
"module_type": 2,
|
||||
{
|
||||
"id": 1,
|
||||
"ects": null,
|
||||
"abbrev": "Init aux réseaux informatiques",
|
||||
"ue_id": 1,
|
||||
"code": "R101",
|
||||
"formation_id": 1,
|
||||
"heures_cours": 0,
|
||||
"matiere_id": 1,
|
||||
"heures_td": 0,
|
||||
"semestre_id": 1,
|
||||
"numero": 10,
|
||||
"module_id": 1
|
||||
"formsemestre_id": 1,
|
||||
"module_id": 1,
|
||||
"responsable_id": 2,
|
||||
"moduleimpl_id": 1,
|
||||
"ens": [],
|
||||
"module": {
|
||||
"heures_tp": 0,
|
||||
"code_apogee": "",
|
||||
"titre": "Initiation aux réseaux informatiques",
|
||||
"coefficient": 1,
|
||||
"module_type": 2,
|
||||
"id": 1,
|
||||
"ects": null,
|
||||
"abbrev": "Init aux réseaux informatiques",
|
||||
"ue_id": 1,
|
||||
"code": "R101",
|
||||
"formation_id": 1,
|
||||
"heures_cours": 0,
|
||||
"matiere_id": 1,
|
||||
"heures_td": 0,
|
||||
"semestre_id": 1,
|
||||
"numero": 10,
|
||||
"module_id": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
"""
|
||||
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
|
||||
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)
|
||||
|
||||
|
||||
@ -81,55 +76,19 @@ def moduleimpl(moduleimpl_id: int):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def moduleimpl_inscriptions(moduleimpl_id: int):
|
||||
"""Liste des inscriptions à ce moduleimpl.
|
||||
|
||||
"""Liste des inscriptions à ce moduleimpl
|
||||
Exemple de résultat :
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"etudid": 666,
|
||||
"moduleimpl_id": 1234,
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"etudid": 666,
|
||||
"moduleimpl_id": 1234,
|
||||
},
|
||||
...
|
||||
]
|
||||
"""
|
||||
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
|
||||
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]
|
||||
|
||||
|
||||
@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
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
ScoDoc 9 API : partitions
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
Groupes et Partitions
|
||||
|
||||
"""
|
||||
from operator import attrgetter
|
||||
|
||||
@ -23,8 +18,7 @@ from sqlalchemy.exc import IntegrityError
|
||||
import app
|
||||
from app import db, log
|
||||
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.decorators import scodoc
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.models import FormSemestre, FormSemestreInscription, Identite
|
||||
from app.models import GroupDescr, Partition, Scolog
|
||||
@ -46,8 +40,7 @@ def partition_info(partition_id: int):
|
||||
"""Info sur une partition.
|
||||
|
||||
Exemple de résultat :
|
||||
|
||||
```json
|
||||
```
|
||||
{
|
||||
'bul_show_rank': False,
|
||||
'formsemestre_id': 39,
|
||||
@ -77,11 +70,10 @@ def partition_info(partition_id: int):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def formsemestre_partitions(formsemestre_id: int):
|
||||
"""Liste de toutes les partitions d'un formsemestre.
|
||||
"""Liste de toutes les partitions d'un formsemestre
|
||||
|
||||
Exemple de résultat :
|
||||
formsemestre_id : l'id d'un formsemestre
|
||||
|
||||
```json
|
||||
{
|
||||
partition_id : {
|
||||
"bul_show_rank": False,
|
||||
@ -95,7 +87,7 @@ def formsemestre_partitions(formsemestre_id: int):
|
||||
},
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
"""
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
@ -115,18 +107,13 @@ def formsemestre_partitions(formsemestre_id: int):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def group_etudiants(group_id: int):
|
||||
def etud_in_group(group_id: int):
|
||||
"""
|
||||
Retourne la liste des étudiants dans un groupe
|
||||
(inscrits au groupe et inscrits au semestre).
|
||||
|
||||
PARAMS
|
||||
------
|
||||
group_id : l'id d'un groupe
|
||||
|
||||
Exemple de résultat :
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
'civilite': 'M',
|
||||
@ -139,7 +126,6 @@ def group_etudiants(group_id: int):
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
"""
|
||||
query = GroupDescr.query.filter_by(id=group_id)
|
||||
if g.scodoc_dept:
|
||||
@ -164,14 +150,8 @@ def group_etudiants(group_id: int):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def group_etudiants_query(group_id: int):
|
||||
"""Étudiants du groupe, filtrés par état (aucun, `I`, `D`, `DEF`)
|
||||
|
||||
QUERY
|
||||
-----
|
||||
etat : string
|
||||
|
||||
"""
|
||||
def etud_in_group_query(group_id: int):
|
||||
"""Étudiants du groupe, filtrés par état"""
|
||||
etat = request.args.get("etat")
|
||||
if etat not in {None, scu.INSCRIT, scu.DEMISSION, scu.DEF}:
|
||||
return json_error(API_CLIENT_ERROR, "etat: valeur invalide")
|
||||
@ -198,8 +178,8 @@ def group_etudiants_query(group_id: int):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def group_set_etudiant(group_id: int, etudid: int):
|
||||
"""Affecte l'étudiant au groupe indiqué."""
|
||||
def set_etud_group(etudid: int, group_id: int):
|
||||
"""Affecte l'étudiant au groupe indiqué"""
|
||||
etud = Identite.query.get_or_404(etudid)
|
||||
query = GroupDescr.query.filter_by(id=group_id)
|
||||
if g.scodoc_dept:
|
||||
@ -261,8 +241,7 @@ def group_remove_etud(group_id: int, etudid: int):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def partition_remove_etud(partition_id: int, etudid: int):
|
||||
"""Enlève l'étudiant de tous les groupes de cette partition.
|
||||
|
||||
"""Enlève l'étudiant de tous les groupes de cette partition
|
||||
(NB: en principe, un étudiant ne doit être que dans 0 ou 1 groupe d'une partition)
|
||||
"""
|
||||
etud = Identite.query.get_or_404(etudid)
|
||||
@ -307,15 +286,12 @@ def partition_remove_etud(partition_id: int, etudid: int):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def group_create(partition_id: int): # partition-group-create
|
||||
"""Création d'un groupe dans une partition.
|
||||
"""Création d'un groupe dans une partition
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
The request content type should be "application/json":
|
||||
{
|
||||
"group_name" : nom_du_groupe,
|
||||
}
|
||||
```
|
||||
"""
|
||||
query = Partition.query.filter_by(id=partition_id)
|
||||
if g.scodoc_dept:
|
||||
@ -327,26 +303,15 @@ def group_create(partition_id: int): # partition-group-create
|
||||
return json_error(403, "partition non editable")
|
||||
if not partition.formsemestre.can_change_groups():
|
||||
return json_error(401, "opération non autorisée")
|
||||
|
||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
||||
group_name = args.get("group_name")
|
||||
if not isinstance(group_name, str):
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
group_name = data.get("group_name")
|
||||
if group_name is None:
|
||||
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, args["group_name"]):
|
||||
if not GroupDescr.check_name(partition, group_name):
|
||||
return json_error(API_CLIENT_ERROR, "invalid group_name")
|
||||
group_name = group_name.strip()
|
||||
|
||||
# le numero est optionnel
|
||||
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")
|
||||
group = GroupDescr(group_name=group_name, partition_id=partition_id)
|
||||
db.session.add(group)
|
||||
db.session.commit()
|
||||
log(f"created group {group}")
|
||||
@ -362,7 +327,7 @@ def group_create(partition_id: int): # partition-group-create
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def group_delete(group_id: int):
|
||||
"""Suppression d'un groupe."""
|
||||
"""Suppression d'un groupe"""
|
||||
query = GroupDescr.query.filter_by(id=group_id)
|
||||
if g.scodoc_dept:
|
||||
query = (
|
||||
@ -391,7 +356,7 @@ def group_delete(group_id: int):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def group_edit(group_id: int):
|
||||
"""Édition d'un groupe."""
|
||||
"""Edit a group"""
|
||||
query = GroupDescr.query.filter_by(id=group_id)
|
||||
if g.scodoc_dept:
|
||||
query = (
|
||||
@ -404,54 +369,21 @@ def group_edit(group_id: int):
|
||||
return json_error(403, "partition non editable")
|
||||
if not group.partition.formsemestre.can_change_groups():
|
||||
return json_error(401, "opération non autorisée")
|
||||
|
||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
||||
if "group_name" in args:
|
||||
if not isinstance(args["group_name"], str):
|
||||
return json_error(API_CLIENT_ERROR, "invalid data format for group_name")
|
||||
args["group_name"] = args["group_name"].strip() if args["group_name"] else ""
|
||||
if not GroupDescr.check_name(
|
||||
group.partition, args["group_name"], existing=True
|
||||
):
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
group_name = data.get("group_name")
|
||||
if group_name is not None:
|
||||
group_name = group_name.strip()
|
||||
if not GroupDescr.check_name(group.partition, group_name, existing=True):
|
||||
return json_error(API_CLIENT_ERROR, "invalid group_name")
|
||||
|
||||
group.from_dict(args)
|
||||
db.session.add(group)
|
||||
db.session.commit()
|
||||
log(f"modified {group}")
|
||||
|
||||
group.group_name = group_name
|
||||
db.session.add(group)
|
||||
db.session.commit()
|
||||
log(f"modified {group}")
|
||||
app.set_sco_dept(group.partition.formsemestre.departement.acronym)
|
||||
sco_cache.invalidate_formsemestre(group.partition.formsemestre_id)
|
||||
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"])
|
||||
@api_web_bp.route(
|
||||
"/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)
|
||||
@as_json
|
||||
def partition_create(formsemestre_id: int):
|
||||
"""Création d'une partition dans un semestre.
|
||||
"""Création d'une partition dans un semestre
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
The request content type should be "application/json":
|
||||
{
|
||||
"partition_name": str,
|
||||
"numero": int,
|
||||
"bul_show_rank": bool,
|
||||
"show_in_lists": bool,
|
||||
"groups_editable": bool
|
||||
"numero":int,
|
||||
"bul_show_rank":bool,
|
||||
"show_in_lists":bool,
|
||||
"groups_editable":bool
|
||||
}
|
||||
```
|
||||
"""
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
@ -528,14 +457,9 @@ def partition_create(formsemestre_id: int):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def formsemestre_set_partitions_order(formsemestre_id: int):
|
||||
"""Modifie l'ordre des partitions du formsemestre.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
[ partition_id1, partition_id2, ... ]
|
||||
```
|
||||
def formsemestre_order_partitions(formsemestre_id: int):
|
||||
"""Modifie l'ordre des partitions du formsemestre
|
||||
JSON args: [partition_id1, partition_id2, ...]
|
||||
"""
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
@ -546,7 +470,7 @@ def formsemestre_set_partitions_order(formsemestre_id: int):
|
||||
if not formsemestre.can_change_groups():
|
||||
return json_error(401, "opération non autorisée")
|
||||
partition_ids = request.get_json(force=True) # may raise 400 Bad Request
|
||||
if not isinstance(partition_ids, list) and not all(
|
||||
if not isinstance(partition_ids, int) and not all(
|
||||
isinstance(x, int) for x in partition_ids
|
||||
):
|
||||
return json_error(
|
||||
@ -560,7 +484,6 @@ def formsemestre_set_partitions_order(formsemestre_id: int):
|
||||
db.session.commit()
|
||||
app.set_sco_dept(formsemestre.departement.acronym)
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id)
|
||||
log(f"formsemestre_set_partitions_order({partition_ids})")
|
||||
return [
|
||||
partition.to_dict()
|
||||
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)
|
||||
@as_json
|
||||
def partition_order_groups(partition_id: int):
|
||||
"""Modifie l'ordre des groupes de la partition.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
[ group_id1, group_id2, ... ]
|
||||
```
|
||||
"""Modifie l'ordre des groupes de la partition
|
||||
JSON args: [group_id1, group_id2, ...]
|
||||
"""
|
||||
query = Partition.query.filter_by(id=partition_id)
|
||||
if g.scodoc_dept:
|
||||
@ -592,7 +510,7 @@ def partition_order_groups(partition_id: int):
|
||||
if not partition.formsemestre.can_change_groups():
|
||||
return json_error(401, "opération non autorisée")
|
||||
group_ids = request.get_json(force=True) # may raise 400 Bad Request
|
||||
if not isinstance(group_ids, list) and not all(
|
||||
if not isinstance(group_ids, int) and not all(
|
||||
isinstance(x, int) for x in group_ids
|
||||
):
|
||||
return json_error(
|
||||
@ -617,13 +535,10 @@ def partition_order_groups(partition_id: int):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def partition_edit(partition_id: int):
|
||||
"""Modification d'une partition dans un semestre.
|
||||
"""Modification d'une partition dans un semestre
|
||||
|
||||
Tous les champs sont optionnels.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
The request content type should be "application/json"
|
||||
All fields are optional:
|
||||
{
|
||||
"partition_name": str,
|
||||
"numero":int,
|
||||
@ -631,7 +546,6 @@ def partition_edit(partition_id: int):
|
||||
"show_in_lists":bool,
|
||||
"groups_editable":bool
|
||||
}
|
||||
```
|
||||
"""
|
||||
query = Partition.query.filter_by(id=partition_id)
|
||||
if g.scodoc_dept:
|
||||
@ -695,9 +609,9 @@ def partition_edit(partition_id: int):
|
||||
def partition_delete(partition_id: int):
|
||||
"""Suppression d'une partition (et de tous ses groupes).
|
||||
|
||||
* Note 1: La partition par défaut (tous les étudiants du sem.) ne peut
|
||||
Note 1: La partition par défaut (tous les étudiants du sem.) ne peut
|
||||
pas être supprimée.
|
||||
* Note 2: Si la partition de parcours est supprimée, les étudiants
|
||||
Note 2: Si la partition de parcours est supprimée, les étudiants
|
||||
sont désinscrits des parcours.
|
||||
"""
|
||||
query = Partition.query.filter_by(id=partition_id)
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -3,18 +3,12 @@ from app import db, log
|
||||
from app.api import api_bp as bp
|
||||
from app.auth.logic import basic_auth, token_auth
|
||||
|
||||
"""
|
||||
CATEGORY
|
||||
--------
|
||||
Authentification API
|
||||
"""
|
||||
|
||||
|
||||
@bp.route("/tokens", methods=["POST"])
|
||||
@basic_auth.login_required
|
||||
@as_json
|
||||
def token_get():
|
||||
"Renvoie un jeton jwt pour l'utilisateur courant."
|
||||
def get_token():
|
||||
"renvoie un jeton jwt pour l'utilisateur courant"
|
||||
token = basic_auth.current_user().get_token()
|
||||
log(f"API: giving token to {basic_auth.current_user()}")
|
||||
db.session.commit()
|
||||
@ -23,8 +17,8 @@ def token_get():
|
||||
|
||||
@bp.route("/tokens", methods=["DELETE"])
|
||||
@token_auth.login_required
|
||||
def token_revoke():
|
||||
"Révoque le jeton de l'utilisateur courant."
|
||||
def revoke_token():
|
||||
"révoque le jeton de l'utilisateur courant"
|
||||
user = token_auth.current_user()
|
||||
user.revoke_token()
|
||||
db.session.commit()
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
"""ScoDoc 9 API : outils
|
||||
|
184
app/api/users.py
184
app/api/users.py
@ -1,31 +1,28 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
ScoDoc 9 API : accès aux utilisateurs
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
Utilisateurs
|
||||
"""
|
||||
|
||||
|
||||
from flask import g, request
|
||||
from flask_json import as_json
|
||||
from flask_login import current_user, login_required
|
||||
|
||||
from app import db, log
|
||||
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.auth.models import User, Role, UserRole
|
||||
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.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
@bp.route("/user/<int:uid>")
|
||||
@ -36,7 +33,7 @@ from app.scodoc.sco_utils import json_error
|
||||
@as_json
|
||||
def user_info(uid: int):
|
||||
"""
|
||||
Info sur un compte utilisateur ScoDoc.
|
||||
Info sur un compte utilisateur scodoc
|
||||
"""
|
||||
user: User = db.session.get(User, uid)
|
||||
if user is None:
|
||||
@ -57,22 +54,11 @@ def user_info(uid: int):
|
||||
@as_json
|
||||
def users_info_query():
|
||||
"""Utilisateurs, filtrés par dept, active ou début nom
|
||||
|
||||
Exemple:
|
||||
```
|
||||
/users/query?departement=dept_acronym&active=1&starts_with=<string:nom>
|
||||
```
|
||||
|
||||
Seuls les utilisateurs "accessibles" (selon les permissions) sont retournés.
|
||||
Si accès via API web, le département de l'URL est ignoré, seules
|
||||
les permissions de l'utilisateur sont prises en compte.
|
||||
|
||||
QUERY
|
||||
-----
|
||||
active: bool
|
||||
departement: string
|
||||
starts_with: string
|
||||
|
||||
"""
|
||||
query = User.query
|
||||
active = request.args.get("active")
|
||||
@ -99,20 +85,6 @@ def users_info_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"])
|
||||
@api_web_bp.route("/user/create", methods=["POST"])
|
||||
@login_required
|
||||
@ -121,28 +93,23 @@ def _is_allowed_user_edit(args: dict) -> tuple[bool, str]:
|
||||
@as_json
|
||||
def user_create():
|
||||
"""Création d'un utilisateur
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
The request content type should be "application/json":
|
||||
{
|
||||
"active":bool (default True),
|
||||
"user_name": str,
|
||||
"dept": str or null,
|
||||
"nom": str,
|
||||
"prenom": str,
|
||||
"user_name": str,
|
||||
...
|
||||
"active":bool (default True)
|
||||
}
|
||||
```
|
||||
"""
|
||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
||||
user_name = args.get("user_name")
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
user_name = data.get("user_name")
|
||||
if not user_name:
|
||||
return json_error(404, "empty user_name")
|
||||
user = User.query.filter_by(user_name=user_name).first()
|
||||
if user:
|
||||
return json_error(404, f"user_create: user {user} already exists\n")
|
||||
dept = args.get("dept")
|
||||
dept = data.get("dept")
|
||||
if dept == "@all":
|
||||
dept = None
|
||||
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
|
||||
):
|
||||
return json_error(404, "user_create: departement inexistant")
|
||||
args["dept"] = dept
|
||||
ok, msg = _is_allowed_user_edit(args)
|
||||
if not ok:
|
||||
return json_error(403, f"user_create: {msg}")
|
||||
user = User(user_name=user_name)
|
||||
user.from_dict(args, new_user=True)
|
||||
nom = data.get("nom")
|
||||
prenom = data.get("prenom")
|
||||
active = scu.to_bool(data.get("active", True))
|
||||
user = User(user_name=user_name, active=active, dept=dept, nom=nom, prenom=prenom)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return user.to_dict()
|
||||
@ -170,24 +135,20 @@ def user_create():
|
||||
@permission_required(Permission.UsersAdmin)
|
||||
@as_json
|
||||
def user_edit(uid: int):
|
||||
"""Modification d'un utilisateur.
|
||||
|
||||
"""Modification d'un utilisateur
|
||||
Champs modifiables:
|
||||
```json
|
||||
{
|
||||
"dept": str or null,
|
||||
"nom": str,
|
||||
"prenom": str,
|
||||
"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)
|
||||
# L'utilisateur doit avoir le droit dans le département de départ et celui d'arrivée
|
||||
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 == "@all":
|
||||
dest_dept = None
|
||||
@ -203,11 +164,10 @@ def user_edit(uid: int):
|
||||
return json_error(404, "user_edit: departement inexistant")
|
||||
user.dept = dest_dept
|
||||
|
||||
ok, msg = _is_allowed_user_edit(args)
|
||||
if not ok:
|
||||
return json_error(403, f"user_edit: {msg}")
|
||||
user.nom = data.get("nom", user.nom)
|
||||
user.prenom = data.get("prenom", user.prenom)
|
||||
user.active = scu.to_bool(data.get("active", user.active))
|
||||
|
||||
user.from_dict(args)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return user.to_dict()
|
||||
@ -220,15 +180,11 @@ def user_edit(uid: int):
|
||||
@permission_required(Permission.UsersAdmin)
|
||||
@as_json
|
||||
def user_password(uid: int):
|
||||
"""Modification du mot de passe d'un utilisateur.
|
||||
|
||||
"""Modification du mot de passe d'un utilisateur
|
||||
Champs modifiables:
|
||||
```json
|
||||
{
|
||||
"password": str
|
||||
}
|
||||
```.
|
||||
|
||||
Si le mot de passe ne convient pas, erreur 400.
|
||||
"""
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
@ -262,7 +218,7 @@ def user_password(uid: int):
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def user_role_add(uid: int, role_name: str, dept: str = None):
|
||||
"""Ajoute un rôle à l'utilisateur dans le département donné."""
|
||||
"""Add a role in the given dept to the user"""
|
||||
user: User = User.query.get_or_404(uid)
|
||||
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
||||
if dept is not None: # check
|
||||
@ -291,7 +247,7 @@ def user_role_add(uid: int, role_name: str, dept: str = None):
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def user_role_remove(uid: int, role_name: str, dept: str = None):
|
||||
"""Retire le rôle (dans le département donné) à cet utilisateur."""
|
||||
"""Remove the role (in the given dept) from the user"""
|
||||
user: User = User.query.get_or_404(uid)
|
||||
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
||||
if dept is not None: # check
|
||||
@ -317,8 +273,8 @@ def user_role_remove(uid: int, role_name: str, dept: str = None):
|
||||
@scodoc
|
||||
@permission_required(Permission.UsersView)
|
||||
@as_json
|
||||
def permissions_list():
|
||||
"""Liste des noms de permissions définies."""
|
||||
def list_permissions():
|
||||
"""Liste des noms de permissions définies"""
|
||||
return list(Permission.permission_by_name.keys())
|
||||
|
||||
|
||||
@ -328,7 +284,7 @@ def permissions_list():
|
||||
@scodoc
|
||||
@permission_required(Permission.UsersView)
|
||||
@as_json
|
||||
def role_get(role_name: str):
|
||||
def list_role(role_name: str):
|
||||
"""Un rôle"""
|
||||
return Role.query.filter_by(name=role_name).first_or_404().to_dict()
|
||||
|
||||
@ -339,8 +295,8 @@ def role_get(role_name: str):
|
||||
@scodoc
|
||||
@permission_required(Permission.UsersView)
|
||||
@as_json
|
||||
def roles_list():
|
||||
"""Tous les rôles définis."""
|
||||
def list_roles():
|
||||
"""Tous les rôles définis"""
|
||||
return [role.to_dict() for role in Role.query]
|
||||
|
||||
|
||||
@ -357,7 +313,7 @@ def roles_list():
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def role_permission_add(role_name: str, perm_name: str):
|
||||
"""Ajoute une permission à un rôle."""
|
||||
"""Add permission to role"""
|
||||
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
||||
permission = Permission.get_by_name(perm_name)
|
||||
if permission is None:
|
||||
@ -382,7 +338,7 @@ def role_permission_add(role_name: str, perm_name: str):
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def role_permission_remove(role_name: str, perm_name: str):
|
||||
"""Retire une permission d'un rôle."""
|
||||
"""Remove permission from role"""
|
||||
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
||||
permission = Permission.get_by_name(perm_name)
|
||||
if permission is None:
|
||||
@ -401,15 +357,10 @@ def role_permission_remove(role_name: str, perm_name: str):
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def role_create(role_name: str):
|
||||
"""Création d'un nouveau rôle avec les permissions données.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
"""Create a new role with permissions.
|
||||
{
|
||||
"permissions" : [ 'ScoView', ... ]
|
||||
}
|
||||
```
|
||||
"""
|
||||
role: Role = Role.query.filter_by(name=role_name).first()
|
||||
if role:
|
||||
@ -434,16 +385,11 @@ def role_create(role_name: str):
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def role_edit(role_name: str):
|
||||
"""Édition d'un rôle. On peut spécifier un nom et/ou des permissions.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
"""Edit a role. On peut spécifier un nom et/ou des permissions.
|
||||
{
|
||||
"name" : name
|
||||
"permissions" : [ 'ScoView', ... ]
|
||||
}
|
||||
```
|
||||
"""
|
||||
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
@ -471,68 +417,8 @@ def role_edit(role_name: str):
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def role_delete(role_name: str):
|
||||
"""Suprression d'un rôle."""
|
||||
"""Delete a role"""
|
||||
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
||||
db.session.delete(role)
|
||||
db.session.commit()
|
||||
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:
|
||||
current_app.logger.info(f"CAS: login {user.user_name}")
|
||||
if login_user(user):
|
||||
flask.session["scodoc_cas_login_date"] = (
|
||||
datetime.datetime.now().isoformat()
|
||||
)
|
||||
flask.session[
|
||||
"scodoc_cas_login_date"
|
||||
] = datetime.datetime.now().isoformat()
|
||||
user.cas_last_login = datetime.datetime.utcnow()
|
||||
if flask.session.get("CAS_EDT_ID"):
|
||||
# essaie de récupérer l'edt_id s'il est présent
|
||||
@ -45,10 +45,8 @@ def after_cas_login():
|
||||
# via l'expression `cas_edt_id_from_xml_regexp`
|
||||
# voir flask_cas.routing
|
||||
edt_id = flask.session.get("CAS_EDT_ID")
|
||||
current_app.logger.info(
|
||||
f"""after_cas_login: storing edt_id for {
|
||||
user.user_name}: '{edt_id}'"""
|
||||
)
|
||||
current_app.logger.info(f"""after_cas_login: storing edt_id for {
|
||||
user.user_name}: '{edt_id}'""")
|
||||
user.edt_id = edt_id
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
@ -57,17 +55,12 @@ def after_cas_login():
|
||||
current_app.logger.info(
|
||||
f"CAS login denied for {user.user_name} (not allowed to use CAS)"
|
||||
)
|
||||
else: # pas d'utilisateur ScoDoc ou bien compte inactif
|
||||
else:
|
||||
current_app.logger.info(
|
||||
f"""CAS login denied for {
|
||||
user.user_name if user else ""
|
||||
} cas_id={cas_id} (unknown or inactive)"""
|
||||
)
|
||||
if ScoDocSiteConfig.is_cas_forced():
|
||||
# Dans ce cas, pas de redirect vers la page de login pour éviter de boucler
|
||||
raise ScoValueError(
|
||||
"compte ScoDoc inexistant ou inactif pour cet utilisateur CAS"
|
||||
)
|
||||
else:
|
||||
current_app.logger.info(
|
||||
f"""CAS attribute '{ScoDocSiteConfig.get("cas_attribute_id")}' not found !
|
||||
|
@ -12,30 +12,23 @@ from typing import Optional
|
||||
|
||||
import cracklib # pylint: disable=import-error
|
||||
|
||||
import flask
|
||||
from flask import current_app, g
|
||||
from flask_login import UserMixin, AnonymousUserMixin
|
||||
from sqlalchemy.exc import (
|
||||
IntegrityError,
|
||||
DataError,
|
||||
DatabaseError,
|
||||
OperationalError,
|
||||
ProgrammingError,
|
||||
StatementError,
|
||||
InterfaceError,
|
||||
)
|
||||
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
import jwt
|
||||
|
||||
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.config import ScoDocSiteConfig
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_roles_default import SCO_ROLES_DEFAULTS
|
||||
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@\\\-_\.]+$")
|
||||
|
||||
@ -57,17 +50,16 @@ def is_valid_password(cleartxt) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def is_valid_user_name(user_name: str) -> bool:
|
||||
"Check that user_name (aka login) is valid"
|
||||
def invalid_user_name(user_name: str) -> bool:
|
||||
"Check that user_name (aka login) is invalid"
|
||||
return (
|
||||
user_name
|
||||
and (len(user_name) >= 2)
|
||||
and (len(user_name) < USERNAME_STR_LEN)
|
||||
and VALID_LOGIN_EXP.match(user_name)
|
||||
(len(user_name) < 2)
|
||||
or (len(user_name) >= USERNAME_STR_LEN)
|
||||
or not VALID_LOGIN_EXP.match(user_name)
|
||||
)
|
||||
|
||||
|
||||
class User(UserMixin, ScoDocModel):
|
||||
class User(UserMixin, db.Model):
|
||||
"""ScoDoc users, handled by Flask / SQLAlchemy"""
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@ -99,7 +91,7 @@ class User(UserMixin, ScoDocModel):
|
||||
"""date du dernier login via CAS"""
|
||||
edt_id = db.Column(db.Text(), index=True, nullable=True)
|
||||
"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))
|
||||
last_seen = 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_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)
|
||||
Permission = Permission
|
||||
|
||||
@ -126,17 +116,12 @@ class User(UserMixin, ScoDocModel):
|
||||
)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"user_name:str is mandatory"
|
||||
self.roles = []
|
||||
self.user_roles = []
|
||||
# check login:
|
||||
if not "user_name" in kwargs:
|
||||
raise ValueError("missing user_name argument")
|
||||
if not is_valid_user_name(kwargs["user_name"]):
|
||||
if kwargs.get("user_name") and invalid_user_name(kwargs["user_name"]):
|
||||
raise ValueError(f"invalid user_name: {kwargs['user_name']}")
|
||||
kwargs["nom"] = kwargs.get("nom", "") or ""
|
||||
kwargs["prenom"] = kwargs.get("prenom", "") or ""
|
||||
super().__init__(**kwargs)
|
||||
super(User, self).__init__(**kwargs)
|
||||
# Ajoute roles:
|
||||
if (
|
||||
not self.roles
|
||||
@ -245,44 +230,33 @@ class User(UserMixin, ScoDocModel):
|
||||
return None
|
||||
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):
|
||||
"""l'utilisateur comme un dict, avec des champs supplémentaires"""
|
||||
data = {
|
||||
"date_expiration": (
|
||||
self.date_expiration.isoformat() + "Z" if self.date_expiration else None
|
||||
),
|
||||
"date_modif_passwd": (
|
||||
self.date_modif_passwd.isoformat() + "Z"
|
||||
if self.date_modif_passwd
|
||||
else None
|
||||
),
|
||||
"date_created": (
|
||||
self.date_created.isoformat() + "Z" if self.date_created else None
|
||||
),
|
||||
"date_expiration": self.date_expiration.isoformat() + "Z"
|
||||
if self.date_expiration
|
||||
else None,
|
||||
"date_modif_passwd": self.date_modif_passwd.isoformat() + "Z"
|
||||
if self.date_modif_passwd
|
||||
else None,
|
||||
"date_created": self.date_created.isoformat() + "Z"
|
||||
if self.date_created
|
||||
else None,
|
||||
"dept": self.dept,
|
||||
"id": self.id,
|
||||
"active": self.active,
|
||||
"cas_id": self.cas_id,
|
||||
"cas_allow_login": self.cas_allow_login,
|
||||
"cas_allow_scodoc_login": self.cas_allow_scodoc_login,
|
||||
"cas_last_login": (
|
||||
self.cas_last_login.isoformat() + "Z" if self.cas_last_login else None
|
||||
),
|
||||
"edt_id": self.edt_id,
|
||||
"cas_last_login": self.cas_last_login.isoformat() + "Z"
|
||||
if self.cas_last_login
|
||||
else None,
|
||||
"status_txt": "actif" if self.active else "fermé",
|
||||
"last_seen": self.last_seen.isoformat() + "Z" if self.last_seen else None,
|
||||
"nom": self.nom or "",
|
||||
"prenom": self.prenom or "",
|
||||
"nom": (self.nom or ""), # sco8
|
||||
"prenom": (self.prenom or ""), # sco8
|
||||
"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:
|
||||
"nom_fmt": self.get_nom_fmt(),
|
||||
"prenom_fmt": self.get_prenom_fmt(),
|
||||
@ -296,55 +270,37 @@ class User(UserMixin, ScoDocModel):
|
||||
data["email_institutionnel"] = self.email_institutionnel or ""
|
||||
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):
|
||||
"""Set users' attributes from given dict values.
|
||||
- roles_string : roles, encoded like "Ens_RT, Secr_CJ"
|
||||
- date_expiration is a dateime object.
|
||||
Does not check permissions here.
|
||||
Roles must be encoded as "roles_string", like "Ens_RT, Secr_CJ"
|
||||
"""
|
||||
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 "user_name" in data:
|
||||
# never change name of existing users
|
||||
# (see change_user_name method to do that)
|
||||
if not is_valid_user_name(data["user_name"]):
|
||||
raise ValueError(f"invalid user_name: {data['user_name']}")
|
||||
self.user_name = data["user_name"]
|
||||
if "password" in data:
|
||||
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, ..."
|
||||
if "roles_string" in data:
|
||||
self.user_roles = []
|
||||
@ -353,13 +309,11 @@ class User(UserMixin, ScoDocModel):
|
||||
role, dept = UserRole.role_dept_from_string(r_d)
|
||||
self.add_role(role, dept)
|
||||
|
||||
super().from_dict(data, excluded={"user_name", "roles_string", "roles"})
|
||||
|
||||
# Set cas_id using regexp if configured:
|
||||
exp = ScoDocSiteConfig.get("cas_uid_from_mail_regexp")
|
||||
if exp and 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
|
||||
|
||||
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
|
||||
e.g. Dupont Pierre (dupont)
|
||||
"""
|
||||
nom = scu.format_nom(self.nom) if self.nom else self.user_name.upper()
|
||||
return f"{nom} {scu.format_prenom(self.prenom)} ({self.user_name})"
|
||||
nom = sco_etud.format_nom(self.nom) if self.nom else self.user_name.upper()
|
||||
return f"{nom} {sco_etud.format_prenom(self.prenom)} ({self.user_name})"
|
||||
|
||||
@staticmethod
|
||||
def get_user_from_nomplogin(nomplogin: str) -> Optional["User"]:
|
||||
"""Returns User instance from the string "Dupont Pierre (dupont)"
|
||||
def get_user_id_from_nomplogin(nomplogin: str) -> Optional[int]:
|
||||
"""Returns id from the string "Dupont Pierre (dupont)"
|
||||
or None if user does not exist
|
||||
"""
|
||||
match = re.match(r".*\((.*)\)", nomplogin.strip())
|
||||
@ -500,96 +454,38 @@ class User(UserMixin, ScoDocModel):
|
||||
user_name = match.group(1)
|
||||
u = User.query.filter_by(user_name=user_name).first()
|
||||
if u:
|
||||
return u
|
||||
return u.id
|
||||
return None
|
||||
|
||||
def get_nom_fmt(self):
|
||||
"""Nom formaté: "Martin" """
|
||||
if self.nom:
|
||||
return scu.format_nom(self.nom, uppercase=False)
|
||||
return sco_etud.format_nom(self.nom, uppercase=False)
|
||||
else:
|
||||
return self.user_name
|
||||
|
||||
def get_prenom_fmt(self):
|
||||
"""Prénom formaté (minuscule capitalisées)"""
|
||||
return scu.format_prenom(self.prenom)
|
||||
return sco_etud.format_prenom(self.prenom)
|
||||
|
||||
def get_nomprenom(self):
|
||||
"""Nom capitalisé suivi de l'initiale du prénom:
|
||||
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()
|
||||
|
||||
def get_prenomnom(self):
|
||||
"""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()
|
||||
|
||||
def get_nomcomplet(self):
|
||||
"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)
|
||||
|
||||
def change_user_name(self, new_user_name: str):
|
||||
"""Modify user name, update all relevant tables.
|
||||
commit session.
|
||||
"""
|
||||
# Safety check
|
||||
new_user_name = new_user_name.strip()
|
||||
if (
|
||||
not is_valid_user_name(new_user_name)
|
||||
or User.query.filter_by(user_name=new_user_name).count() > 0
|
||||
):
|
||||
raise ValueError("invalid user_name")
|
||||
# Le user_name est utilisé dans d'autres tables (sans être une clé)
|
||||
# BulAppreciations.author
|
||||
# EntrepriseHistorique.authenticated_user
|
||||
# EtudAnnotation.author
|
||||
# ScolarNews.authenticated_user
|
||||
# Scolog.authenticated_user
|
||||
from app.models import (
|
||||
BulAppreciations,
|
||||
EtudAnnotation,
|
||||
ScolarNews,
|
||||
Scolog,
|
||||
)
|
||||
from app.entreprises.models import EntrepriseHistorique
|
||||
|
||||
try:
|
||||
# Update all instances of EtudAnnotation
|
||||
db.session.query(BulAppreciations).filter(
|
||||
BulAppreciations.author == self.user_name
|
||||
).update({BulAppreciations.author: new_user_name})
|
||||
db.session.query(EntrepriseHistorique).filter(
|
||||
EntrepriseHistorique.authenticated_user == self.user_name
|
||||
).update({EntrepriseHistorique.authenticated_user: new_user_name})
|
||||
db.session.query(EtudAnnotation).filter(
|
||||
EtudAnnotation.author == self.user_name
|
||||
).update({EtudAnnotation.author: new_user_name})
|
||||
db.session.query(ScolarNews).filter(
|
||||
ScolarNews.authenticated_user == self.user_name
|
||||
).update({ScolarNews.authenticated_user: new_user_name})
|
||||
db.session.query(Scolog).filter(
|
||||
Scolog.authenticated_user == self.user_name
|
||||
).update({Scolog.authenticated_user: new_user_name})
|
||||
# And update ourself:
|
||||
self.user_name = new_user_name
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
except (
|
||||
IntegrityError,
|
||||
DataError,
|
||||
DatabaseError,
|
||||
OperationalError,
|
||||
ProgrammingError,
|
||||
StatementError,
|
||||
InterfaceError,
|
||||
) as exc:
|
||||
db.session.rollback()
|
||||
raise exc
|
||||
|
||||
|
||||
class AnonymousUser(AnonymousUserMixin):
|
||||
"Notre utilisateur anonyme"
|
||||
@ -671,19 +567,8 @@ class Role(db.Model):
|
||||
"""Create default roles if missing, then, if reset_permissions,
|
||||
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"
|
||||
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()
|
||||
if role is None:
|
||||
role = Role(name=role_name)
|
||||
|
@ -18,7 +18,7 @@ from app.auth.forms import (
|
||||
ResetPasswordRequestForm,
|
||||
UserCreationForm,
|
||||
)
|
||||
from app.auth.models import Role, User, is_valid_user_name
|
||||
from app.auth.models import Role, User, invalid_user_name
|
||||
from app.auth.email import send_password_reset_email
|
||||
from app.decorators import admin_required
|
||||
from app.forms.generic import SimpleConfirmationForm
|
||||
@ -35,12 +35,10 @@ def _login_form():
|
||||
form = LoginForm()
|
||||
if form.validate_on_submit():
|
||||
# note: ceci est la première requête SQL déclenchée par un utilisateur arrivant
|
||||
user = (
|
||||
User.query.filter_by(user_name=form.user_name.data).first()
|
||||
if is_valid_user_name(form.user_name.data)
|
||||
else None
|
||||
)
|
||||
|
||||
if invalid_user_name(form.user_name.data):
|
||||
user = None
|
||||
else:
|
||||
user = User.query.filter_by(user_name=form.user_name.data).first()
|
||||
if user is None or not user.check_password(form.password.data):
|
||||
current_app.logger.info("login: invalid (%s)", form.user_name.data)
|
||||
flash(_("Nom ou mot de passe invalide"))
|
||||
@ -56,7 +54,6 @@ def _login_form():
|
||||
title=_("Sign In"),
|
||||
form=form,
|
||||
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"),
|
||||
form=form,
|
||||
)
|
||||
|
||||
return
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -21,9 +21,9 @@ def form_ue_choix_parcours(ue: UniteEns) -> str:
|
||||
return ""
|
||||
ref_comp = ue.formation.referentiel_competence
|
||||
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><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)
|
||||
}">associer un référentiel de compétence</a>
|
||||
</div>
|
||||
@ -31,36 +31,27 @@ def form_ue_choix_parcours(ue: UniteEns) -> str:
|
||||
|
||||
H = [
|
||||
"""
|
||||
<div class="scobox ue_advanced">
|
||||
<div class="scobox-title">Parcours du BUT</div>
|
||||
<div class="ue_advanced">
|
||||
<h3>Parcours du BUT</h3>
|
||||
"""
|
||||
]
|
||||
# Choix des parcours
|
||||
ue_pids = [p.id for p in ue.parcours]
|
||||
H.append(
|
||||
"""
|
||||
<div class="help">
|
||||
Cocher tous les parcours dans lesquels cette UE est utilisée,
|
||||
même si vous n'offrez pas ce parcours dans votre département.
|
||||
Sans quoi, les UEs de Tronc Commun ne seront pas reconnues.
|
||||
Ne cocher aucun parcours est équivalent à tous les cocher.
|
||||
</div>
|
||||
<form id="choix_parcours" style="margin-top: 12px;">
|
||||
"""
|
||||
)
|
||||
H.append("""<form id="choix_parcours">""")
|
||||
|
||||
ects_differents = {
|
||||
ue.get_ects(parcour, only_parcours=True) for parcour in ref_comp.parcours
|
||||
} != {None}
|
||||
for parcour in ref_comp.parcours:
|
||||
ects_parcour = ue.get_ects(parcour)
|
||||
ects_parcour_txt = (
|
||||
f" ({ue.get_ects(parcour):.3g} ects)" if ects_differents else ""
|
||||
)
|
||||
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 ""}
|
||||
onclick="set_ue_parcour(this);"
|
||||
data-setter="{url_for("apiweb.ue_set_parcours",
|
||||
data-setter="{url_for("apiweb.set_ue_parcours",
|
||||
scodoc_dept=g.scodoc_dept, ue_id=ue.id)}"
|
||||
>{parcour.code}{ects_parcour_txt}</label>"""
|
||||
)
|
||||
@ -71,7 +62,7 @@ def form_ue_choix_parcours(ue: UniteEns) -> str:
|
||||
<ul>
|
||||
<li>
|
||||
<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)
|
||||
}">définir des ECTS différents dans chaque parcours</a>
|
||||
</li>
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -9,14 +9,12 @@
|
||||
|
||||
import collections
|
||||
import datetime
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from flask import g, has_request_context, url_for
|
||||
|
||||
from app import db
|
||||
from app.comp.moy_mod import ModuleImplResults
|
||||
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.ues import UniteEns
|
||||
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
|
||||
"moyenne": None,
|
||||
# Le bonus sport appliqué sur cette UE
|
||||
"bonus": (
|
||||
fmt_note(res.bonus_ues[ue.id][etud.id])
|
||||
if res.bonus_ues is not None and ue.id in res.bonus_ues
|
||||
else fmt_note(0.0)
|
||||
),
|
||||
"bonus": fmt_note(res.bonus_ues[ue.id][etud.id])
|
||||
if res.bonus_ues is not None and ue.id in res.bonus_ues
|
||||
else fmt_note(0.0),
|
||||
"malus": fmt_note(res.malus[ue.id][etud.id]),
|
||||
"capitalise": None, # "AAAA-MM-JJ" TODO #sco93
|
||||
"ressources": self.etud_ue_mod_results(etud, ue, res.ressources),
|
||||
@ -185,16 +181,14 @@ class BulletinBUT:
|
||||
"is_external": ue_capitalisee.is_external,
|
||||
"date_capitalisation": ue_capitalisee.event_date,
|
||||
"formsemestre_id": ue_capitalisee.formsemestre_id,
|
||||
"bul_orig_url": (
|
||||
url_for(
|
||||
"notes.formsemestre_bulletinetud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=etud.id,
|
||||
formsemestre_id=ue_capitalisee.formsemestre_id,
|
||||
)
|
||||
if ue_capitalisee.formsemestre_id
|
||||
else None
|
||||
),
|
||||
"bul_orig_url": url_for(
|
||||
"notes.formsemestre_bulletinetud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=etud.id,
|
||||
formsemestre_id=ue_capitalisee.formsemestre_id,
|
||||
)
|
||||
if ue_capitalisee.formsemestre_id
|
||||
else None,
|
||||
"ressources": {}, # sans détail en BUT
|
||||
"saes": {},
|
||||
}
|
||||
@ -231,17 +225,15 @@ class BulletinBUT:
|
||||
if res.modimpl_inscr_df[modimpl.id][etud.id]: # si inscrit
|
||||
d[modimpl.module.code] = {
|
||||
"id": modimpl.id,
|
||||
"titre": modimpl.module.titre_str(),
|
||||
"titre": modimpl.module.titre,
|
||||
"code_apogee": modimpl.module.code_apogee,
|
||||
"url": (
|
||||
url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=modimpl.id,
|
||||
)
|
||||
if has_request_context()
|
||||
else "na"
|
||||
),
|
||||
"url": url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=modimpl.id,
|
||||
)
|
||||
if has_request_context()
|
||||
else "na",
|
||||
"moyenne": {
|
||||
# # moyenne indicative de module: moyenne des UE,
|
||||
# # ignorant celles sans notes (nan)
|
||||
@ -250,115 +242,68 @@ class BulletinBUT:
|
||||
# "max": fmt_note(moyennes_etuds.max()),
|
||||
# "moy": fmt_note(moyennes_etuds.mean()),
|
||||
},
|
||||
"evaluations": (
|
||||
self.etud_list_modimpl_evaluations(
|
||||
etud, modimpl, modimpl_results, version
|
||||
"evaluations": [
|
||||
self.etud_eval_results(etud, e)
|
||||
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
|
||||
|
||||
def etud_list_modimpl_evaluations(
|
||||
self,
|
||||
etud: Identite,
|
||||
modimpl: ModuleImpl,
|
||||
modimpl_results: ModuleImplResults,
|
||||
version: str,
|
||||
) -> list[dict]:
|
||||
"""Liste des résultats aux évaluations de ce modimpl à montrer pour cet étudiant"""
|
||||
evaluation: Evaluation
|
||||
eval_results = []
|
||||
for evaluation in modimpl.evaluations:
|
||||
if (
|
||||
(evaluation.visibulletin or version == "long")
|
||||
and (evaluation.id in modimpl_results.evaluations_etat)
|
||||
and (
|
||||
modimpl_results.evaluations_etat[evaluation.id].is_complete
|
||||
or self.prefs["bul_show_all_evals"]
|
||||
)
|
||||
):
|
||||
eval_notes = self.res.modimpls_results[modimpl.id].evals_notes[
|
||||
evaluation.id
|
||||
]
|
||||
|
||||
if (evaluation.evaluation_type == Evaluation.EVALUATION_NORMALE) or (
|
||||
not np.isnan(eval_notes[etud.id])
|
||||
):
|
||||
eval_results.append(
|
||||
self.etud_eval_results(etud, evaluation, eval_notes)
|
||||
)
|
||||
return eval_results
|
||||
|
||||
def etud_eval_results(
|
||||
self, etud: Identite, evaluation: Evaluation, eval_notes: pd.DataFrame
|
||||
) -> dict:
|
||||
def etud_eval_results(self, etud, e: Evaluation) -> dict:
|
||||
"dict resultats d'un étudiant à une évaluation"
|
||||
# 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()
|
||||
modimpls_evals_poids = self.res.modimpls_evals_poids[evaluation.moduleimpl_id]
|
||||
modimpls_evals_poids = self.res.modimpls_evals_poids[e.moduleimpl_id]
|
||||
try:
|
||||
etud_ues_ids = self.res.etud_ues_ids(etud.id)
|
||||
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
|
||||
if (ue.type != UE_SPORT) and (ue.id in etud_ues_ids)
|
||||
}
|
||||
except KeyError:
|
||||
poids = collections.defaultdict(lambda: 0.0)
|
||||
d = {
|
||||
"id": evaluation.id,
|
||||
"coef": (
|
||||
fmt_note(evaluation.coefficient)
|
||||
if evaluation.evaluation_type == Evaluation.EVALUATION_NORMALE
|
||||
else None
|
||||
),
|
||||
"date_debut": (
|
||||
evaluation.date_debut.isoformat() if evaluation.date_debut else None
|
||||
),
|
||||
"date_fin": (
|
||||
evaluation.date_fin.isoformat() if evaluation.date_fin else None
|
||||
),
|
||||
"description": evaluation.description,
|
||||
"evaluation_type": evaluation.evaluation_type,
|
||||
"note": (
|
||||
{
|
||||
"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 {}
|
||||
),
|
||||
"id": e.id,
|
||||
"coef": fmt_note(e.coefficient)
|
||||
if e.evaluation_type == scu.EVALUATION_NORMALE
|
||||
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,
|
||||
"description": e.description,
|
||||
"evaluation_type": e.evaluation_type,
|
||||
"note": {
|
||||
"value": fmt_note(
|
||||
eval_notes[etud.id],
|
||||
note_max=e.note_max,
|
||||
),
|
||||
"min": fmt_note(notes_ok.min(), note_max=e.note_max),
|
||||
"max": fmt_note(notes_ok.max(), note_max=e.note_max),
|
||||
"moy": fmt_note(notes_ok.mean(), note_max=e.note_max),
|
||||
},
|
||||
"poids": poids,
|
||||
"url": (
|
||||
url_for(
|
||||
"notes.evaluation_listenotes",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
evaluation_id=evaluation.id,
|
||||
)
|
||||
if has_request_context()
|
||||
else "na"
|
||||
),
|
||||
"url": url_for(
|
||||
"notes.evaluation_listenotes",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
evaluation_id=e.id,
|
||||
)
|
||||
if has_request_context()
|
||||
else "na",
|
||||
# deprecated (supprimer avant #sco9.7)
|
||||
"date": (
|
||||
evaluation.date_debut.isoformat() if evaluation.date_debut else None
|
||||
),
|
||||
"heure_debut": (
|
||||
evaluation.date_debut.time().isoformat("minutes")
|
||||
if evaluation.date_debut
|
||||
else None
|
||||
),
|
||||
"heure_fin": (
|
||||
evaluation.date_fin.time().isoformat("minutes")
|
||||
if evaluation.date_fin
|
||||
else None
|
||||
),
|
||||
"date": e.date_debut.isoformat() if e.date_debut else None,
|
||||
"heure_debut": e.date_debut.time().isoformat("minutes")
|
||||
if e.date_debut
|
||||
else None,
|
||||
"heure_fin": e.date_fin.time().isoformat("minutes") if e.date_fin else None,
|
||||
}
|
||||
return d
|
||||
|
||||
@ -398,18 +343,25 @@ class BulletinBUT:
|
||||
"short" : ne descend pas plus bas que les modules.
|
||||
|
||||
- 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:
|
||||
raise ScoValueError("bulletin_etud: version de bulletin demandée invalide")
|
||||
if version not in scu.BULLETINS_VERSIONS:
|
||||
raise ScoValueError("version de bulletin demandée invalide")
|
||||
res = self.res
|
||||
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 = {
|
||||
"version": "0",
|
||||
"type": "BUT",
|
||||
"date": datetime.datetime.utcnow().isoformat() + "Z",
|
||||
"publie": not formsemestre.bul_hide_xml,
|
||||
"etat_inscription": etud.inscription_etat(formsemestre.id),
|
||||
"etudiant": etud.to_dict_bul(),
|
||||
"formation": {
|
||||
"id": formsemestre.formation.id,
|
||||
@ -418,21 +370,15 @@ class BulletinBUT:
|
||||
"titre": formsemestre.formation.titre,
|
||||
},
|
||||
"formsemestre_id": formsemestre.id,
|
||||
"etat_inscription": etat_inscription,
|
||||
"options": sco_preferences.bulletin_option_affichage(
|
||||
formsemestre, self.prefs
|
||||
),
|
||||
}
|
||||
published = (not formsemestre.bul_hide_xml) or force_publishing
|
||||
if not published or d["etat_inscription"] is False:
|
||||
if not published:
|
||||
return d
|
||||
|
||||
nb_inscrits = self.res.get_inscriptions_counts()[scu.INSCRIT]
|
||||
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)
|
||||
nbabs, nbabsjust = formsemestre.get_abs_count(etud.id)
|
||||
etud_groups = sco_groups.get_etud_formsemestre_groups(
|
||||
etud, formsemestre, only_to_show=True
|
||||
)
|
||||
@ -447,7 +393,7 @@ class BulletinBUT:
|
||||
}
|
||||
if self.prefs["bul_show_abs"]:
|
||||
semestre_infos["absences"] = {
|
||||
"injustifie": nbabsnj,
|
||||
"injustifie": nbabs - nbabsjust,
|
||||
"total": nbabs,
|
||||
"metrique": {
|
||||
"H.": "Heure(s)",
|
||||
@ -464,7 +410,7 @@ class BulletinBUT:
|
||||
semestre_infos.update(
|
||||
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
|
||||
semestre_infos["notes"] = {
|
||||
"value": fmt_note(res.etud_moy_gen[etud.id]),
|
||||
@ -553,8 +499,10 @@ class BulletinBUT:
|
||||
d["etud"]["etat_civil"] = etud.etat_civil
|
||||
d.update(self.res.sem)
|
||||
etud_etat = self.res.get_etud_etat(etud.id)
|
||||
d["filigranne"] = sco_bulletins_pdf.get_filigranne_apc(
|
||||
etud_etat, self.prefs, etud.id, res=self.res
|
||||
d["filigranne"] = sco_bulletins_pdf.get_filigranne(
|
||||
etud_etat,
|
||||
self.prefs,
|
||||
decision_sem=d["semestre"].get("decision"),
|
||||
)
|
||||
if etud_etat == scu.DEMISSION:
|
||||
d["demission"] = "(Démission)"
|
||||
@ -564,7 +512,7 @@ class BulletinBUT:
|
||||
d["demission"] = ""
|
||||
|
||||
# --- 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
|
||||
infos, _ = sco_bulletins.etud_descr_situation_semestre(
|
||||
@ -576,11 +524,12 @@ class BulletinBUT:
|
||||
show_uevalid=self.prefs["bul_show_uevalid"],
|
||||
show_mention=self.prefs["bul_show_mention"],
|
||||
)
|
||||
|
||||
d.update(infos)
|
||||
# --- Rangs
|
||||
d["rang_nt"] = (
|
||||
f"{d['semestre']['rang']['value']} / {d['semestre']['rang']['total']}"
|
||||
)
|
||||
d[
|
||||
"rang_nt"
|
||||
] = f"{d['semestre']['rang']['value']} / {d['semestre']['rang']['total']}"
|
||||
d["rang_txt"] = "Rang " + d["rang_nt"]
|
||||
|
||||
d.update(sco_bulletins.make_context_dict(self.res.formsemestre, d["etud"]))
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# 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é)
|
||||
- `cursus: EtudCursusBUT`: infos sur le cursus BUT (niveaux validés etc)
|
||||
- `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
|
||||
- `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_logos import find_logo
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_pv_lettres_inviduelles import add_dut120_infos
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.views import notes_bp as bp
|
||||
from app.views import ScoData
|
||||
@ -66,48 +65,11 @@ def bulletin_but(formsemestre_id: int, etudid: int = None, fmt="html"):
|
||||
)
|
||||
if not formsemestre.formation.is_apc():
|
||||
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)
|
||||
if fmt == "pdf":
|
||||
bul: dict = bulletins_sem.bulletin_etud_complet(etud)
|
||||
filigranne = bul["filigranne"]
|
||||
else: # la même chose avec un peu moins d'infos
|
||||
bul: dict = bulletins_sem.bulletin_etud(etud, force_publishing=True)
|
||||
filigranne = ""
|
||||
decision_ues = (
|
||||
{x["acronyme"]: x for x in bul["semestre"]["decision_ue"]}
|
||||
if "semestre" in bul and "decision_ue" in bul["semestre"]
|
||||
@ -119,14 +81,6 @@ def _build_bulletin_but_infos(
|
||||
refcomp = formsemestre.formation.referentiel_competence
|
||||
if refcomp is None:
|
||||
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(
|
||||
refcomp, etud
|
||||
)
|
||||
@ -141,7 +95,6 @@ def _build_bulletin_but_infos(
|
||||
"decision_ues": decision_ues,
|
||||
"ects_total": ects_total,
|
||||
"etud": etud,
|
||||
"filigranne": filigranne,
|
||||
"formsemestre": formsemestre,
|
||||
"logo": logo,
|
||||
"prefs": bulletins_sem.prefs,
|
||||
@ -153,5 +106,16 @@ def _build_bulletin_but_infos(
|
||||
if ue.type == UE_STANDARD and ue.acronyme in ue_acronyms
|
||||
],
|
||||
}
|
||||
add_dut120_infos(formsemestre, etud.id, args)
|
||||
return args
|
||||
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,
|
||||
)
|
||||
|
@ -1,12 +1,12 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""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.
|
||||
|
||||
"""
|
||||
@ -31,37 +31,28 @@ from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
|
||||
from app.scodoc.sco_logos import Logo
|
||||
from app.scodoc.sco_pdf import PDFLOCK, SU
|
||||
from app.scodoc.sco_preferences import SemPreferences
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
def make_bulletin_but_court_pdf(
|
||||
args: dict,
|
||||
stand_alone: bool = True,
|
||||
bul: dict = None,
|
||||
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:
|
||||
"""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,
|
||||
"""
|
||||
"génère le bulletin court BUT en pdf"
|
||||
# A priori ce verrou n'est plus nécessaire avec Flask (multi-process)
|
||||
# mais...
|
||||
try:
|
||||
PDFLOCK.acquire()
|
||||
bul_generator = BulletinGeneratorBUTCourt(**args)
|
||||
bul_pdf = bul_generator.generate(fmt="pdf", stand_alone=stand_alone)
|
||||
bul_generator = BulletinGeneratorBUTCourt(**locals())
|
||||
bul_pdf = bul_generator.generate(fmt="pdf")
|
||||
finally:
|
||||
PDFLOCK.release()
|
||||
return bul_pdf
|
||||
@ -88,7 +79,6 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
||||
decision_ues: dict = None,
|
||||
ects_total: float = 0.0,
|
||||
etud: Identite = None,
|
||||
filigranne="",
|
||||
formsemestre: FormSemestre = None,
|
||||
logo: Logo = None,
|
||||
prefs: SemPreferences = None,
|
||||
@ -97,10 +87,8 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
||||
tuple[int, str], ScolarFormSemestreValidation
|
||||
] = None,
|
||||
ues_acronyms: list[str] = None,
|
||||
diplome_dut120: bool = False,
|
||||
diplome_dut120_descr: str = "",
|
||||
):
|
||||
super().__init__(bul, authuser=current_user, filigranne=filigranne)
|
||||
super().__init__(bul, authuser=current_user)
|
||||
self.bul = bul
|
||||
self.cursus = cursus
|
||||
self.decision_ues = decision_ues
|
||||
@ -112,8 +100,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
||||
self.title = title
|
||||
self.ue_validation_by_niveau = ue_validation_by_niveau
|
||||
self.ues_acronyms = ues_acronyms # sans UEs sport
|
||||
self.diplome_dut120 = diplome_dut120
|
||||
self.diplome_dut120_descr = diplome_dut120_descr
|
||||
|
||||
self.nb_ues = len(self.ues_acronyms)
|
||||
# Styles PDF
|
||||
self.style_base = styles.ParagraphStyle("style_base")
|
||||
@ -198,7 +185,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
||||
"""Génère la partie "titre" du bulletin de notes.
|
||||
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)
|
||||
|
||||
def bul_part_below(self, fmt="pdf") -> list:
|
||||
@ -246,17 +233,13 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
||||
)
|
||||
table_abs_ues.hAlign = "RIGHT"
|
||||
# Ligne (en bas) avec table cursus et boite jury
|
||||
# table_content = [self.table_cursus_but()]
|
||||
# if self.prefs["bul_show_decision"]:
|
||||
# table_content.append([Spacer(1, 8 * mm), self.boite_decisions_jury()])
|
||||
table_content = [self.table_cursus_but()]
|
||||
table_content.append(
|
||||
[Spacer(1, 8 * mm), self.boite_decisions_jury()]
|
||||
if self.prefs["bul_show_decision"]
|
||||
else []
|
||||
)
|
||||
table_cursus_jury = Table(
|
||||
[table_content],
|
||||
[
|
||||
[
|
||||
self.table_cursus_but(),
|
||||
[Spacer(1, 8 * mm), self.boite_decisions_jury()],
|
||||
]
|
||||
],
|
||||
colWidths=(self.width_page_avail - 84 * mm, 84 * mm),
|
||||
style=style_table_2cols,
|
||||
)
|
||||
@ -351,11 +334,9 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
||||
for mod in self.bul[mod_type]:
|
||||
row = [mod, bul[mod_type][mod]["titre"]]
|
||||
row += [
|
||||
(
|
||||
bul["ues"][ue][mod_type][mod]["moyenne"]
|
||||
if mod in bul["ues"][ue][mod_type]
|
||||
else ""
|
||||
)
|
||||
bul["ues"][ue][mod_type][mod]["moyenne"]
|
||||
if mod in bul["ues"][ue][mod_type]
|
||||
else ""
|
||||
for ue in self.ues_acronyms
|
||||
]
|
||||
rows.append(row)
|
||||
@ -416,8 +397,6 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
||||
|
||||
def boite_identite(self) -> list:
|
||||
"Les informations sur l'identité et l'inscription de l'étudiant"
|
||||
parcour = self.formsemestre.etuds_inscriptions[self.etud.id].parcour
|
||||
|
||||
return [
|
||||
Paragraph(
|
||||
SU(f"""{self.etud.nomprenom}"""),
|
||||
@ -428,8 +407,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
||||
f"""
|
||||
<b>{self.bul["demission"]}</b><br/>
|
||||
Formation: {self.formsemestre.titre_num()}<br/>
|
||||
{'Parcours ' + parcour.code + '<br/>' if parcour else ''}
|
||||
Année universitaire: {self.formsemestre.annee_scolaire_str()}<br/>
|
||||
Année scolaire: {self.formsemestre.annee_scolaire_str()}<br/>
|
||||
"""
|
||||
),
|
||||
style=self.style_base,
|
||||
@ -530,17 +508,14 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
||||
def boite_decisions_jury(self):
|
||||
"""La boite en bas à droite avec jury"""
|
||||
txt = f"""ECTS acquis en BUT : <b>{self.ects_total:g}</b><br/>"""
|
||||
|
||||
if self.bul["semestre"].get("decision_annee", None):
|
||||
txt += f"""
|
||||
Décision année BUT{self.bul["semestre"]["decision_annee"]["ordre"]}
|
||||
Décision saisie le {
|
||||
datetime.datetime.fromisoformat(self.bul["semestre"]["decision_annee"]["date"]).strftime("%d/%m/%Y")
|
||||
}, année BUT{self.bul["semestre"]["decision_annee"]["ordre"]}
|
||||
<b>{self.bul["semestre"]["decision_annee"]["code"]}</b>.
|
||||
<br/>
|
||||
{self.bul.get("diplomation", "")}
|
||||
"""
|
||||
if self.diplome_dut120_descr:
|
||||
txt += f"""<br/>{self.diplome_dut120_descr}."""
|
||||
|
||||
if self.bul["semestre"].get("autorisation_inscription", None):
|
||||
txt += (
|
||||
"<br/>Autorisé à s'inscrire en <b>"
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
La génération du bulletin PDF suit le chemin suivant:
|
||||
|
||||
- vue formsemestre_bulletinetud -> sco_bulletins.formsemestre_bulletinetud
|
||||
|
||||
|
||||
bul_dict = bulletin_but.BulletinBUT(formsemestre).bulletin_etud_complet(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.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 import gen_tables
|
||||
from app.scodoc.codes_cursus import UE_SPORT
|
||||
@ -73,7 +73,6 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
||||
html_class="notes_bulletin",
|
||||
html_class_ignore_default=True,
|
||||
html_with_td_classes=True,
|
||||
table_id="bul-table",
|
||||
)
|
||||
table_objects = table.gen(fmt=fmt)
|
||||
objects += table_objects
|
||||
@ -270,7 +269,7 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
||||
date_capitalisation = ue.get("date_capitalisation")
|
||||
if date_capitalisation:
|
||||
fields_bmr.append(
|
||||
f"""Capitalisée le {date_capitalisation.strftime(scu.DATE_FMT)}"""
|
||||
f"""Capitalisée le {date_capitalisation.strftime("%d/%m/%Y")}"""
|
||||
)
|
||||
t = {
|
||||
"titre": " - ".join(fields_bmr),
|
||||
@ -423,22 +422,16 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
||||
def evaluations_rows(self, rows, evaluations: list[dict], ue_acros=()):
|
||||
"lignes des évaluations"
|
||||
for e in evaluations:
|
||||
coef = (
|
||||
e["coef"]
|
||||
if e["evaluation_type"] == Evaluation.EVALUATION_NORMALE
|
||||
else "*"
|
||||
)
|
||||
note_value = e["note"].get("value", "")
|
||||
coef = e["coef"] if e["evaluation_type"] == scu.EVALUATION_NORMALE else "*"
|
||||
t = {
|
||||
"titre": f"{e['description'] or ''}",
|
||||
"moyenne": note_value,
|
||||
"_moyenne_pdf": Paragraph(f"""<para align=right>{note_value}</para>"""),
|
||||
"moyenne": e["note"]["value"],
|
||||
"_moyenne_pdf": Paragraph(
|
||||
f"""<para align=right>{e["note"]["value"]}</para>"""
|
||||
),
|
||||
"coef": coef,
|
||||
"_coef_pdf": Paragraph(
|
||||
f"""<para align=right fontSize={self.small_fontsize}><i>{
|
||||
coef if e["evaluation_type"] != Evaluation.EVALUATION_BONUS
|
||||
else "bonus"
|
||||
}</i></para>"""
|
||||
f"<para align=right fontSize={self.small_fontsize}><i>{coef}</i></para>"
|
||||
),
|
||||
"_pdf_style": [
|
||||
(
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# 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
|
||||
# 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.ElementTree import Element
|
||||
|
||||
from app import db, log
|
||||
from app import log
|
||||
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.notesdb as ndb
|
||||
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_preferences
|
||||
from app.scodoc import sco_xml
|
||||
@ -199,12 +202,12 @@ def bulletin_but_xml_compat(
|
||||
if e.visibulletin or version == "long":
|
||||
x_eval = Element(
|
||||
"evaluation",
|
||||
date_debut=(
|
||||
e.date_debut.isoformat() if e.date_debut else ""
|
||||
),
|
||||
date_fin=(
|
||||
e.date_fin.isoformat() if e.date_fin else ""
|
||||
),
|
||||
date_debut=e.date_debut.isoformat()
|
||||
if e.date_debut
|
||||
else "",
|
||||
date_fin=e.date_fin.isoformat()
|
||||
if e.date_debut
|
||||
else "",
|
||||
coefficient=str(e.coefficient),
|
||||
# pas les poids en XML compat
|
||||
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:
|
||||
note_max_origin=str(e.note_max),
|
||||
# --- deprecated
|
||||
jour=(
|
||||
e.date_debut.isoformat() if e.date_debut else ""
|
||||
),
|
||||
jour=e.date_debut.isoformat()
|
||||
if e.date_debut
|
||||
else "",
|
||||
heure_debut=e.heure_debut(),
|
||||
heure_fin=e.heure_fin(),
|
||||
)
|
||||
@ -241,7 +244,7 @@ def bulletin_but_xml_compat(
|
||||
|
||||
# --- Absences
|
||||
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)))
|
||||
|
||||
# -------- LA SUITE EST COPIEE SANS MODIF DE sco_bulletins_xml.py ---------
|
||||
@ -291,18 +294,17 @@ def bulletin_but_xml_compat(
|
||||
"decisions_ue"
|
||||
]: # 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():
|
||||
ue = db.session.get(UniteEns, ue_id)
|
||||
if ue:
|
||||
doc.append(
|
||||
Element(
|
||||
"decision_ue",
|
||||
ue_id=str(ue.id),
|
||||
numero=quote_xml_attr(ue.numero),
|
||||
acronyme=quote_xml_attr(ue.acronyme),
|
||||
titre=quote_xml_attr(ue.titre or ""),
|
||||
code=decision["decisions_ue"][ue_id]["code"],
|
||||
)
|
||||
ue = sco_edit_ue.ue_list({"ue_id": ue_id})[0]
|
||||
doc.append(
|
||||
Element(
|
||||
"decision_ue",
|
||||
ue_id=str(ue["ue_id"]),
|
||||
numero=quote_xml_attr(ue["numero"]),
|
||||
acronyme=quote_xml_attr(ue["acronyme"]),
|
||||
titre=quote_xml_attr(ue["titre"]),
|
||||
code=decision["decisions_ue"][ue_id]["code"],
|
||||
)
|
||||
)
|
||||
|
||||
for aut in decision["autorisations"]:
|
||||
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
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -14,39 +14,45 @@ Classe raccordant avec ScoDoc 7:
|
||||
|
||||
"""
|
||||
import collections
|
||||
from collections.abc import Iterable
|
||||
from operator import attrgetter
|
||||
|
||||
from flask import g, url_for
|
||||
from flask_sqlalchemy.query import Query
|
||||
|
||||
from app import db, log
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
|
||||
from app.models.but_refcomp import (
|
||||
ApcAnneeParcours,
|
||||
ApcCompetence,
|
||||
ApcNiveau,
|
||||
ApcParcours,
|
||||
ApcParcoursNiveauCompetence,
|
||||
ApcReferentielCompetences,
|
||||
)
|
||||
from app.models.ues import UEParcours
|
||||
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
||||
from app.models import Scolog, ScolarAutorisationInscription
|
||||
from app.models.but_validations import (
|
||||
ApcValidationAnnee,
|
||||
ApcValidationRCUE,
|
||||
)
|
||||
from app.models.etudiants import Identite
|
||||
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.validations import ScolarFormSemestreValidation
|
||||
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 import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
|
||||
|
||||
from app.scodoc import sco_cursus_dut
|
||||
|
||||
|
||||
class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
|
||||
"""Pour compat ScoDoc 7"""
|
||||
"""Pour compat ScoDoc 7: à revoir pour le BUT"""
|
||||
|
||||
def __init__(self, etud: Identite, formsemestre_id: int, res: ResultatsSemestreBUT):
|
||||
def __init__(self, etud: dict, formsemestre_id: int, res: ResultatsSemestreBUT):
|
||||
super().__init__(etud, formsemestre_id, res)
|
||||
# Ajustements pour le BUT
|
||||
self.can_compensate_with_prev = False # jamais de compensation à la mode DUT
|
||||
@ -56,22 +62,8 @@ class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
|
||||
return False
|
||||
|
||||
def parcours_validated(self):
|
||||
"True si le parcours (ici diplôme BUT) est validé"
|
||||
return but_parcours_validated(
|
||||
self.etud.id, self.cur_sem.formation.referentiel_competence_id
|
||||
)
|
||||
|
||||
|
||||
def but_parcours_validated(etudid: int, referentiel_competence_id: int) -> bool:
|
||||
"""Détermine si le parcours BUT est validé:
|
||||
ne regarde que si une validation BUT3 est enregistrée
|
||||
"""
|
||||
return any(
|
||||
sco_codes.code_annee_validant(v.code)
|
||||
for v in ApcValidationAnnee.query.filter_by(
|
||||
etudid=etudid, ordre=3, referentiel_competence_id=referentiel_competence_id
|
||||
)
|
||||
)
|
||||
"True si le parcours est validé"
|
||||
return False # XXX TODO
|
||||
|
||||
|
||||
class EtudCursusBUT:
|
||||
@ -127,15 +119,8 @@ class EtudCursusBUT:
|
||||
|
||||
self.validation_par_competence_et_annee = {}
|
||||
"""{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
|
||||
validation_rcue: ApcValidationRCUE
|
||||
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
||||
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:
|
||||
self.validation_par_competence_et_annee[niveau.competence.id] = {}
|
||||
previous_validation = self.validation_par_competence_et_annee.get(
|
||||
@ -209,10 +194,6 @@ class EtudCursusBUT:
|
||||
# slow, utile pour affichage fiche
|
||||
return annee in [n.annee for n in self.competences[competence_id].niveaux]
|
||||
|
||||
def get_ects_acquis(self) -> int:
|
||||
"Nombre d'ECTS validés par etud dans le BUT de ce référentiel"
|
||||
return but_ects_valides(self.etud, self.formation.referentiel_competence.id)
|
||||
|
||||
def load_validation_by_niveau(self) -> dict[int, list[ApcValidationRCUE]]:
|
||||
"""Cherche les validations de jury enregistrées pour chaque niveau
|
||||
Résultat: { niveau_id : [ ApcValidationRCUE ] }
|
||||
@ -307,136 +288,104 @@ class FormSemestreCursusBUT:
|
||||
)
|
||||
return niveaux_by_annee
|
||||
|
||||
# def get_etud_validation_par_competence_et_annee(self, etud: Identite):
|
||||
# """{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
|
||||
# validation_par_competence_et_annee = {}
|
||||
# for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
||||
# # On s'assurer qu'elle concerne notre cursus !
|
||||
# ue = validation_rcue.ue2
|
||||
# if ue.id not in self.ue_ids:
|
||||
# if (
|
||||
# ue.formation.referentiel_competences_id
|
||||
# == self.referentiel_competences_id
|
||||
# ):
|
||||
# self.ue_ids = ue.id
|
||||
# else:
|
||||
# continue # skip this validation
|
||||
# niveau = validation_rcue.niveau()
|
||||
# if not niveau.competence.id in validation_par_competence_et_annee:
|
||||
# validation_par_competence_et_annee[niveau.competence.id] = {}
|
||||
# previous_validation = validation_par_competence_et_annee.get(
|
||||
# niveau.competence.id
|
||||
# ).get(validation_rcue.annee())
|
||||
# # prend la "meilleure" validation
|
||||
# if (not previous_validation) or (
|
||||
# sco_codes.BUT_CODES_ORDER[validation_rcue.code]
|
||||
# > sco_codes.BUT_CODES_ORDER[previous_validation["code"]]
|
||||
# ):
|
||||
# self.validation_par_competence_et_annee[niveau.competence.id][
|
||||
# niveau.annee
|
||||
# ] = validation_rcue
|
||||
# return validation_par_competence_et_annee
|
||||
def get_etud_validation_par_competence_et_annee(self, etud: Identite):
|
||||
"""{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
|
||||
validation_par_competence_et_annee = {}
|
||||
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
||||
# On s'assurer qu'elle concerne notre cursus !
|
||||
ue = validation_rcue.ue2
|
||||
if ue.id not in self.ue_ids:
|
||||
if (
|
||||
ue.formation.referentiel_competences_id
|
||||
== self.referentiel_competences_id
|
||||
):
|
||||
self.ue_ids = ue.id
|
||||
else:
|
||||
continue # skip this validation
|
||||
niveau = validation_rcue.niveau()
|
||||
if not niveau.competence.id in validation_par_competence_et_annee:
|
||||
validation_par_competence_et_annee[niveau.competence.id] = {}
|
||||
previous_validation = validation_par_competence_et_annee.get(
|
||||
niveau.competence.id
|
||||
).get(validation_rcue.annee())
|
||||
# prend la "meilleure" validation
|
||||
if (not previous_validation) or (
|
||||
sco_codes.BUT_CODES_ORDER[validation_rcue.code]
|
||||
> sco_codes.BUT_CODES_ORDER[previous_validation["code"]]
|
||||
):
|
||||
self.validation_par_competence_et_annee[niveau.competence.id][
|
||||
niveau.annee
|
||||
] = validation_rcue
|
||||
return validation_par_competence_et_annee
|
||||
|
||||
# def list_etud_inscriptions(self, etud: Identite):
|
||||
# "Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
|
||||
# self.niveaux_by_annee = {}
|
||||
# "{ annee : liste des niveaux à valider }"
|
||||
# self.niveaux: dict[int, ApcNiveau] = {}
|
||||
# "cache les niveaux"
|
||||
# for annee in (1, 2, 3):
|
||||
# niveaux_d = formation.referentiel_competence.get_niveaux_by_parcours(
|
||||
# annee, [self.parcour] if self.parcour else None # XXX WIP
|
||||
# )[1]
|
||||
# # groupe les niveaux de tronc commun et ceux spécifiques au parcour
|
||||
# self.niveaux_by_annee[annee] = niveaux_d["TC"] + (
|
||||
# niveaux_d[self.parcour.id] if self.parcour else []
|
||||
# )
|
||||
# self.niveaux.update(
|
||||
# {niveau.id: niveau for niveau in self.niveaux_by_annee[annee]}
|
||||
# )
|
||||
def list_etud_inscriptions(self, etud: Identite):
|
||||
"Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
|
||||
self.niveaux_by_annee = {}
|
||||
"{ annee : liste des niveaux à valider }"
|
||||
self.niveaux: dict[int, ApcNiveau] = {}
|
||||
"cache les niveaux"
|
||||
for annee in (1, 2, 3):
|
||||
niveaux_d = formation.referentiel_competence.get_niveaux_by_parcours(
|
||||
annee, [self.parcour] if self.parcour else None # XXX WIP
|
||||
)[1]
|
||||
# groupe les niveaux de tronc commun et ceux spécifiques au parcour
|
||||
self.niveaux_by_annee[annee] = niveaux_d["TC"] + (
|
||||
niveaux_d[self.parcour.id] if self.parcour else []
|
||||
)
|
||||
self.niveaux.update(
|
||||
{niveau.id: niveau for niveau in self.niveaux_by_annee[annee]}
|
||||
)
|
||||
|
||||
# self.validation_par_competence_et_annee = {}
|
||||
# """{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
|
||||
# for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
||||
# niveau = validation_rcue.niveau()
|
||||
# if not niveau.competence.id in self.validation_par_competence_et_annee:
|
||||
# self.validation_par_competence_et_annee[niveau.competence.id] = {}
|
||||
# previous_validation = self.validation_par_competence_et_annee.get(
|
||||
# niveau.competence.id
|
||||
# ).get(validation_rcue.annee())
|
||||
# # prend la "meilleure" validation
|
||||
# if (not previous_validation) or (
|
||||
# sco_codes.BUT_CODES_ORDER[validation_rcue.code]
|
||||
# > sco_codes.BUT_CODES_ORDER[previous_validation["code"]]
|
||||
# ):
|
||||
# self.validation_par_competence_et_annee[niveau.competence.id][
|
||||
# niveau.annee
|
||||
# ] = validation_rcue
|
||||
self.validation_par_competence_et_annee = {}
|
||||
"""{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
|
||||
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
||||
niveau = validation_rcue.niveau()
|
||||
if not niveau.competence.id in self.validation_par_competence_et_annee:
|
||||
self.validation_par_competence_et_annee[niveau.competence.id] = {}
|
||||
previous_validation = self.validation_par_competence_et_annee.get(
|
||||
niveau.competence.id
|
||||
).get(validation_rcue.annee())
|
||||
# prend la "meilleure" validation
|
||||
if (not previous_validation) or (
|
||||
sco_codes.BUT_CODES_ORDER[validation_rcue.code]
|
||||
> sco_codes.BUT_CODES_ORDER[previous_validation["code"]]
|
||||
):
|
||||
self.validation_par_competence_et_annee[niveau.competence.id][
|
||||
niveau.annee
|
||||
] = validation_rcue
|
||||
|
||||
# self.competences = {
|
||||
# competence.id: competence
|
||||
# for competence in (
|
||||
# self.parcour.query_competences()
|
||||
# if self.parcour
|
||||
# else self.formation.referentiel_competence.get_competences_tronc_commun()
|
||||
# )
|
||||
# }
|
||||
# "cache { competence_id : competence }"
|
||||
self.competences = {
|
||||
competence.id: competence
|
||||
for competence in (
|
||||
self.parcour.query_competences()
|
||||
if self.parcour
|
||||
else self.formation.referentiel_competence.get_competences_tronc_commun()
|
||||
)
|
||||
}
|
||||
"cache { competence_id : competence }"
|
||||
|
||||
|
||||
def but_ects_valides(
|
||||
etud: Identite,
|
||||
referentiel_competence_id: int,
|
||||
annees_but: None | Iterable[str] = None,
|
||||
) -> int:
|
||||
def but_ects_valides(etud: Identite, referentiel_competence_id: int) -> float:
|
||||
"""Nombre d'ECTS validés par etud dans le BUT de référentiel indiqué.
|
||||
Ne prend que les UE associées à des niveaux de compétences,
|
||||
et ne les compte qu'une fois même en cas de redoublement avec re-validation.
|
||||
Si annees_but est spécifié, un iterable "BUT1, "BUT2" par exemple, ne prend que ces années.
|
||||
"""
|
||||
validations = but_validations_ues(etud, referentiel_competence_id, annees_but)
|
||||
ects_dict = {}
|
||||
for v in validations:
|
||||
key = (v.ue.semestre_idx, v.ue.niveau_competence.id)
|
||||
if v.code in CODES_UE_VALIDES:
|
||||
ects_dict[key] = v.ue.ects or 0.0
|
||||
|
||||
return int(sum(ects_dict.values())) if ects_dict else 0
|
||||
|
||||
|
||||
def but_validations_ues(
|
||||
etud: Identite,
|
||||
referentiel_competence_id: int,
|
||||
annees_but: None | Iterable[str] = None,
|
||||
) -> list[ScolarFormSemestreValidation]:
|
||||
"""Query les validations d'UEs pour cet étudiant
|
||||
dans des UEs appartenant à ce référentiel de compétence
|
||||
et en option pour les années BUT indiquées.
|
||||
annees_but : None (tout) ou liste [ "BUT1", ... ]
|
||||
"""
|
||||
validations = (
|
||||
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
|
||||
.filter(ScolarFormSemestreValidation.ue_id != None)
|
||||
.join(UniteEns)
|
||||
.join(ApcNiveau)
|
||||
)
|
||||
# restreint à certaines années (utile pour les ECTS du DUT120)
|
||||
if annees_but:
|
||||
validations = validations.filter(ApcNiveau.annee.in_(annees_but))
|
||||
# restreint au référentiel de compétence
|
||||
validations = validations.join(ApcCompetence).filter_by(
|
||||
referentiel_id=referentiel_competence_id
|
||||
.join(ApcCompetence)
|
||||
.filter_by(referentiel_id=referentiel_competence_id)
|
||||
)
|
||||
|
||||
# Tri (nb: fait en python pour gérer les validations externes qui n'ont pas de formsemestre)
|
||||
return sorted(
|
||||
validations,
|
||||
key=lambda v: (
|
||||
(v.formsemestre.semestre_id, v.ue.numero, v.ue.acronyme)
|
||||
if v.formsemestre
|
||||
else (v.ue.semestre_idx or -2, v.ue.numero, v.ue.acronyme)
|
||||
),
|
||||
)
|
||||
ects_dict = {}
|
||||
for v in validations:
|
||||
key = (v.ue.semestre_idx, v.ue.niveau_competence.id)
|
||||
if v.code in CODES_UE_VALIDES:
|
||||
ects_dict[key] = v.ue.ects
|
||||
|
||||
return sum(ects_dict.values()) if ects_dict else 0.0
|
||||
|
||||
|
||||
def etud_ues_de_but1_non_validees(
|
||||
@ -487,38 +436,15 @@ def formsemestre_warning_apc_setup(
|
||||
"""
|
||||
if not formsemestre.formation.is_apc():
|
||||
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:
|
||||
return f"""<div class="formsemestre_status_warning">
|
||||
La <a class="stdlink" href="{url_formation}">formation
|
||||
n'est pas associée à un référentiel de compétence.</a>
|
||||
La <a class="stdlink" href="{
|
||||
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>
|
||||
"""
|
||||
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
|
||||
H = []
|
||||
for parcour in formsemestre.parcours or [None]:
|
||||
annee = (formsemestre.semestre_id + 1) // 2
|
||||
niveaux_ids = {
|
||||
@ -543,8 +469,7 @@ def formsemestre_warning_apc_setup(
|
||||
if not H:
|
||||
return ""
|
||||
return f"""<div class="formsemestre_status_warning">
|
||||
Problème dans la
|
||||
<a class="stdlink" href="{url_formation}">configuration de la formation</a>:
|
||||
Problème dans la configuration de la formation:
|
||||
<ul>
|
||||
<li>{ '</li><li>'.join(H) }</li>
|
||||
</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(
|
||||
ues_possibles: list[UniteEns], niveau: ApcNiveau, sem_name: str = "S"
|
||||
) -> UniteEns:
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -10,11 +10,9 @@
|
||||
from flask_wtf import FlaskForm
|
||||
from flask_wtf.file import FileField, FileAllowed
|
||||
from wtforms import SelectField, SubmitField
|
||||
from wtforms.validators import DataRequired
|
||||
|
||||
|
||||
class FormationRefCompForm(FlaskForm):
|
||||
"Choix d'un référentiel"
|
||||
referentiel_competence = SelectField(
|
||||
"Choisir parmi les référentiels déjà chargés :"
|
||||
)
|
||||
@ -23,7 +21,6 @@ class FormationRefCompForm(FlaskForm):
|
||||
|
||||
|
||||
class RefCompLoadForm(FlaskForm):
|
||||
"Upload d'un référentiel"
|
||||
referentiel_standard = SelectField(
|
||||
"Choisir un référentiel de compétences officiel BUT"
|
||||
)
|
||||
@ -50,12 +47,3 @@ class RefCompLoadForm(FlaskForm):
|
||||
)
|
||||
return False
|
||||
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
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
from xml.etree import ElementTree
|
||||
@ -23,12 +23,9 @@ from app.models.but_refcomp import (
|
||||
from app.scodoc.sco_exceptions import ScoFormatError, ScoValueError
|
||||
|
||||
|
||||
def orebut_import_refcomp(
|
||||
xml_data: str, dept_id: int, orig_filename=None
|
||||
) -> ApcReferentielCompetences:
|
||||
def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None):
|
||||
"""Importation XML Orébut
|
||||
peut lever TypeError ou ScoFormatError
|
||||
L'objet créé est ajouté et commité.
|
||||
Résultat: instance de ApcReferentielCompetences
|
||||
"""
|
||||
# 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
|
||||
).count():
|
||||
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})
|
||||
Supprimez-le ou changez le nom du fichier."""
|
||||
)
|
||||
@ -44,7 +41,7 @@ def orebut_import_refcomp(
|
||||
try:
|
||||
root = ElementTree.XML(xml_data)
|
||||
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":
|
||||
raise ScoFormatError("élément racine 'referentiel_competence' manquant")
|
||||
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
|
||||
db.session.rollback()
|
||||
raise ScoValueError(
|
||||
f"""Un référentiel a déjà été chargé avec les mêmes compétences ! ({
|
||||
competence.attrib["id"]})
|
||||
f"""Un référentiel a déjà été chargé avec les mêmes compétences ! ({competence.attrib["id"]})
|
||||
"""
|
||||
) from exc
|
||||
ref.competences.append(c)
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -13,22 +13,22 @@ Utilisation:
|
||||
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.
|
||||
|
||||
on instancie des DecisionsProposees pour les
|
||||
on instancie des DecisionsProposees pour les
|
||||
différents éléments (UEs, RCUEs, Année, Diplôme)
|
||||
Cela donne
|
||||
Cela donne
|
||||
- les codes possibles (dans .codes)
|
||||
- le code actuel si une décision existe déjà (dans code_valide)
|
||||
- pour les UEs, le rcue s'il y en a un)
|
||||
|
||||
|
||||
2) Validation pour l'utilisateur (form)) => enregistrement code
|
||||
- on vérifie que le code soumis est bien dans les codes possibles
|
||||
- on enregistre la décision (dans ScolarFormSemestreValidation pour les UE,
|
||||
ApcValidationRCUE pour les RCUE, et ApcValidationAnnee pour les années)
|
||||
- Si RCUE validé, on déclenche d'éventuelles validations:
|
||||
("La validation des deux UE du niveau d'une compétence emporte la validation
|
||||
- Si RCUE validé, on déclenche d'éventuelles validations:
|
||||
("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.")
|
||||
|
||||
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`.
|
||||
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
|
||||
@ -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.
|
||||
Si l'utilisateur coche "décision manuelle", il peut alors choisir les codes RCUE et années.
|
||||
|
||||
La soumission du formulaire:
|
||||
- etud, formation
|
||||
La soumission du formulaire:
|
||||
- etud, formation
|
||||
- UEs: [(formsemestre, ue, code), ...]
|
||||
- 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.
|
||||
@ -64,7 +64,6 @@ import re
|
||||
|
||||
import numpy as np
|
||||
from flask import flash, g, url_for
|
||||
import sqlalchemy as sa
|
||||
|
||||
from app import db
|
||||
from app import log
|
||||
@ -78,14 +77,12 @@ from app.models.but_refcomp import (
|
||||
ApcNiveau,
|
||||
ApcParcours,
|
||||
)
|
||||
from app.models import Evaluation, ModuleImpl, Scolog, ScolarAutorisationInscription
|
||||
from app.models import Scolog, ScolarAutorisationInscription
|
||||
from app.models.but_validations import (
|
||||
ApcValidationAnnee,
|
||||
ApcValidationRCUE,
|
||||
ValidationDUT120,
|
||||
)
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.formations import Formation
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.models.ues import UniteEns
|
||||
from app.models.validations import ScolarFormSemestreValidation
|
||||
@ -93,7 +90,6 @@ from app.scodoc import sco_cache
|
||||
from app.scodoc import codes_cursus as sco_codes
|
||||
from app.scodoc.codes_cursus import (
|
||||
code_rcue_validant,
|
||||
code_ue_validant,
|
||||
BUT_CODES_ORDER,
|
||||
CODES_RCUE_VALIDES,
|
||||
CODES_UE_VALIDES,
|
||||
@ -121,7 +117,7 @@ class NoRCUEError(ScoValueError):
|
||||
{warning_impair}
|
||||
{warning_pair}
|
||||
<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))}
|
||||
</div>
|
||||
"""
|
||||
@ -264,11 +260,11 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
else []
|
||||
)
|
||||
# ---- Niveaux et RCUEs
|
||||
niveaux_by_parcours = formsemestre.formation.referentiel_competence.get_niveaux_by_parcours(
|
||||
self.annee_but, [self.parcour] if self.parcour else None
|
||||
)[
|
||||
1
|
||||
]
|
||||
niveaux_by_parcours = (
|
||||
formsemestre.formation.referentiel_competence.get_niveaux_by_parcours(
|
||||
self.annee_but, [self.parcour] if self.parcour else None
|
||||
)[1]
|
||||
)
|
||||
self.niveaux_competences = niveaux_by_parcours["TC"] + (
|
||||
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.
|
||||
"""
|
||||
self.rcue_by_niveau = self._compute_rcues_annee()
|
||||
"""RCUEs de l'année
|
||||
(peuvent être construits avec des UEs validées antérieurement: redoublants
|
||||
"""RCUEs de l'année
|
||||
(peuvent être construits avec des UEs validées antérieurement: redoublants
|
||||
avec UEs capitalisées, validation "antérieures")
|
||||
"""
|
||||
# ---- Décision année et autorisation
|
||||
@ -362,17 +358,13 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
# self.codes = [] # pas de décision annuelle sur semestres impairs
|
||||
elif self.inscription_etat != scu.INSCRIT:
|
||||
self.codes = [
|
||||
(
|
||||
sco_codes.DEM
|
||||
if self.inscription_etat == scu.DEMISSION
|
||||
else sco_codes.DEF
|
||||
),
|
||||
sco_codes.DEM
|
||||
if self.inscription_etat == scu.DEMISSION
|
||||
else sco_codes.DEF,
|
||||
# propose aussi d'autres codes, au cas où...
|
||||
(
|
||||
sco_codes.DEM
|
||||
if self.inscription_etat != scu.DEMISSION
|
||||
else sco_codes.DEF
|
||||
),
|
||||
sco_codes.DEM
|
||||
if self.inscription_etat != scu.DEMISSION
|
||||
else sco_codes.DEF,
|
||||
sco_codes.ABAN,
|
||||
sco_codes.ABL,
|
||||
sco_codes.EXCLU,
|
||||
@ -388,24 +380,14 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
sco_codes.ADJ,
|
||||
] + self.codes
|
||||
explanation += f" et {self.nb_rcues_under_8} < 8"
|
||||
else: # autres cas: non admis, non passage, non dem, pas la moitié des rcue:
|
||||
if formsemestre.semestre_id % 2 and self.formsemestre_pair is None:
|
||||
# Si jury sur un seul semestre impair, ne propose pas redoublement
|
||||
# et efface décision éventuellement existante
|
||||
codes = [None]
|
||||
else:
|
||||
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
|
||||
)
|
||||
else:
|
||||
self.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
|
||||
} 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:
|
||||
if self.code_valide == 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()
|
||||
if messages:
|
||||
self.explanation += (
|
||||
'<div class="warning warning-info">'
|
||||
+ '</div><div class="warning warning-info">'.join(messages)
|
||||
'<div class="warning">'
|
||||
+ '</div><div class="warning">'.join(messages)
|
||||
+ "</div>"
|
||||
)
|
||||
|
||||
# Présente les codes unifiés, avec le code proposé en tête et les autres triés
|
||||
codes_set = set(self.codes)
|
||||
codes_set.remove(self.codes[0])
|
||||
self.codes = [self.codes[0]] + sorted(x or "" for x in codes_set)
|
||||
self.codes = [self.codes[0]] + sorted(self.codes[1:])
|
||||
|
||||
def passage_de_droit_en_but3(self) -> tuple[bool, str]:
|
||||
"""Vérifie si les conditions supplémentaires de passage BUT2 vers BUT3 sont satisfaites"""
|
||||
@ -536,21 +514,19 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
"""Les deux formsemestres auquel est inscrit l'étudiant (ni DEM ni DEF)
|
||||
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 paire, S_pair est l'origine, et S_impair l'antérieur
|
||||
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
|
||||
return None, None
|
||||
|
||||
if formsemestre.semestre_id % 2:
|
||||
idx_autre = formsemestre.semestre_id + 1 # impair, autre = suivant
|
||||
idx_autre = formsemestre.semestre_id + 1
|
||||
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:
|
||||
autre_formsemestre = None
|
||||
@ -563,8 +539,6 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
inscr.formsemestre.formation.referentiel_competence
|
||||
== formsemestre.formation.referentiel_competence
|
||||
)
|
||||
# Non bloqué
|
||||
and not inscr.formsemestre.block_moyennes
|
||||
# L'autre semestre
|
||||
and (inscr.formsemestre.semestre_id == idx_autre)
|
||||
# de la même année scolaire
|
||||
@ -607,9 +581,11 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
# Ordonne par numéro d'UE
|
||||
niv_rcue = sorted(
|
||||
self.rcue_by_niveau.items(),
|
||||
key=lambda x: (
|
||||
x[1].ue_1.numero if x[1].ue_1 else x[1].ue_2.numero if x[1].ue_2 else 0
|
||||
),
|
||||
key=lambda x: x[1].ue_1.numero
|
||||
if x[1].ue_1
|
||||
else x[1].ue_2.numero
|
||||
if x[1].ue_2
|
||||
else 0,
|
||||
)
|
||||
return {
|
||||
niveau_id: DecisionsProposeesRCUE(self, rcue, self.inscription_etat)
|
||||
@ -634,7 +610,6 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
def next_semestre_ids(self, code: str) -> set[int]:
|
||||
"""Les indices des semestres dans lequels l'étudiant est autorisé
|
||||
à 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
|
||||
# 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,
|
||||
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")
|
||||
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:
|
||||
"""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 le code est None, efface le code déjà enregistré.
|
||||
Si mark_recorded est vrai, positionne self.recorded
|
||||
"""
|
||||
if self.inscription_etat != scu.INSCRIT:
|
||||
@ -761,22 +733,20 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
self.validation.date = datetime.now()
|
||||
|
||||
db.session.add(self.validation)
|
||||
db.session.commit()
|
||||
log(f"Recording {self}: {code}")
|
||||
Scolog.logdb(
|
||||
method="jury_but",
|
||||
etudid=self.etud.id,
|
||||
msg=f"Validation année BUT{self.annee_but}: {code}",
|
||||
)
|
||||
db.session.commit()
|
||||
if mark_recorded:
|
||||
self.recorded = True
|
||||
self.invalidate_formsemestre_cache()
|
||||
return True
|
||||
|
||||
def record_autorisation_inscription(self, code: str):
|
||||
"""Autorisation d'inscription dans semestre suivant.
|
||||
code: code jury sur année BUT
|
||||
"""
|
||||
"""Autorisation d'inscription dans semestre suivant"""
|
||||
if self.autorisations_recorded:
|
||||
return
|
||||
if self.inscription_etat != scu.INSCRIT:
|
||||
@ -804,33 +774,16 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
if self.formsemestre_pair is not None:
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre_pair.id)
|
||||
|
||||
def _get_current_res(self) -> ResultatsSemestreBUT:
|
||||
"Les res. du semestre d'origine du deca"
|
||||
return (
|
||||
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.res_pair
|
||||
if self.formsemestre_pair
|
||||
and (self.formsemestre.id == self.formsemestre_pair.id)
|
||||
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()
|
||||
|
||||
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:
|
||||
"""Enregistre les codes qui n'ont pas été spécifiés par le formulaire,
|
||||
et sont donc en mode "automatique".
|
||||
@ -843,15 +796,9 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
Return: True si au moins un code modifié et enregistré.
|
||||
"""
|
||||
modif = False
|
||||
if only_validantes:
|
||||
if self.has_notes_en_attente():
|
||||
# notes en attente dans formsemestre origine
|
||||
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
|
||||
# Vérification notes en attente dans formsemestre origine
|
||||
if only_validantes and self.has_notes_en_attente():
|
||||
return False
|
||||
|
||||
# Toujours valider dans l'ordre UE, RCUE, Année
|
||||
annee_scolaire = self.formsemestre.annee_scolaire()
|
||||
@ -893,7 +840,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
not only_validantes
|
||||
) or code in sco_codes.CODES_ANNEE_BUT_VALIDES_DE_DROIT:
|
||||
modif |= self.record(code)
|
||||
self.record_autorisation_inscription(code)
|
||||
self.record_autorisation_inscription(code)
|
||||
return modif
|
||||
|
||||
def erase(self, only_one_sem=False):
|
||||
@ -904,8 +851,6 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
Si only_one_sem, n'efface que les décisions UE et les
|
||||
autorisations de passage du semestre d'origine du deca.
|
||||
|
||||
Efface les validations de DUT120 issues du semestre d'origine du deca.
|
||||
|
||||
Dans tous les cas, efface les validations de l'année en cours.
|
||||
(commite la session.)
|
||||
"""
|
||||
@ -955,17 +900,6 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
msg=f"Validation année BUT{self.annee_but}: effacée",
|
||||
)
|
||||
|
||||
# Efface les validations de DUT120 issues du semestre d'origine du deca.
|
||||
for validation in ValidationDUT120.query.filter_by(
|
||||
etudid=self.etud.id, formsemestre_id=self.formsemestre.id
|
||||
):
|
||||
db.session.delete(validation)
|
||||
Scolog.logdb(
|
||||
"jury_but",
|
||||
etudid=self.etud.id,
|
||||
msg="Validation DUT120 effacée",
|
||||
)
|
||||
|
||||
# Efface éventuelles validations de semestre
|
||||
# (en principe inutilisées en BUT)
|
||||
# et autres UEs (en cas de changement d'architecture de formation depuis le jury ?)
|
||||
@ -1007,36 +941,6 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
pour PV jurys
|
||||
"""
|
||||
validations = []
|
||||
# Validations antérieures émises par ce formsemestre
|
||||
for res in (self.res_impair, self.res_pair):
|
||||
if res:
|
||||
validations_anterieures = (
|
||||
ScolarFormSemestreValidation.query.filter_by(
|
||||
etudid=self.etud.id, formsemestre_id=res.formsemestre.id
|
||||
)
|
||||
.filter(
|
||||
ScolarFormSemestreValidation.semestre_id
|
||||
!= res.formsemestre.semestre_id
|
||||
)
|
||||
.join(UniteEns)
|
||||
.join(Formation)
|
||||
.filter_by(formation_code=res.formsemestre.formation.formation_code)
|
||||
.order_by(
|
||||
sa.desc(UniteEns.semestre_idx),
|
||||
UniteEns.acronyme,
|
||||
sa.desc(ScolarFormSemestreValidation.event_date),
|
||||
)
|
||||
.all()
|
||||
)
|
||||
if validations_anterieures:
|
||||
validations.append(
|
||||
", ".join(
|
||||
v.ue.acronyme
|
||||
for v in validations_anterieures
|
||||
if v and v.ue and code_ue_validant(v.code)
|
||||
)
|
||||
)
|
||||
# Validations des UEs des deux semestres de l'année
|
||||
for res in (self.res_impair, self.res_pair):
|
||||
if res:
|
||||
dec_ues = [
|
||||
@ -1045,10 +949,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
if ue.type == UE_STANDARD and ue.id in self.decisions_ues
|
||||
]
|
||||
valids = [dec_ue.descr_validation() for dec_ue in dec_ues]
|
||||
# présentation de la liste des UEs:
|
||||
if valids:
|
||||
validations.append(", ".join(v for v in valids if v))
|
||||
|
||||
validations.append(", ".join(v for v in valids if v))
|
||||
return line_sep.join(validations)
|
||||
|
||||
def descr_pb_coherence(self) -> list[str]:
|
||||
@ -1068,28 +969,24 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
if dec_ue.code_valide not in CODES_UE_VALIDES:
|
||||
if (
|
||||
dec_ue.ue_status
|
||||
and dec_ue.ue_status["is_capitalized"]
|
||||
and dec_ue.ue_status["was_capitalized"]
|
||||
):
|
||||
messages.append(
|
||||
f"Information: l'UE {ue.acronyme} capitalisée est utilisée pour un RCUE cette année"
|
||||
)
|
||||
else:
|
||||
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:
|
||||
messages.append(
|
||||
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
|
||||
|
||||
def valide_diplome(self) -> bool:
|
||||
"Vrai si l'étudiant a validé son diplôme (décision enregistrée)"
|
||||
return self.annee_but == 3 and sco_codes.code_annee_validant(self.code_valide)
|
||||
"Vrai si l'étudiant à validé son diplôme"
|
||||
return False # TODO XXX
|
||||
|
||||
|
||||
def list_ue_parcour_etud(
|
||||
@ -1228,12 +1125,13 @@ class DecisionsProposeesRCUE(DecisionsProposees):
|
||||
code=code,
|
||||
)
|
||||
db.session.add(self.validation)
|
||||
db.session.commit()
|
||||
Scolog.logdb(
|
||||
method="jury_but",
|
||||
etudid=self.etud.id,
|
||||
msg=f"Validation {self.rcue}: {code}",
|
||||
commit=True,
|
||||
)
|
||||
db.session.commit()
|
||||
log(f"rcue.record {self}: {code}")
|
||||
|
||||
# Modifie au besoin les codes d'UE
|
||||
@ -1570,11 +1468,9 @@ class DecisionsProposeesUE(DecisionsProposees):
|
||||
self.validation = None # cache toute validation
|
||||
self.explanation = "non inscrit (dem. ou déf.)"
|
||||
self.codes = [
|
||||
(
|
||||
sco_codes.DEM
|
||||
if res.get_etud_etat(etud.id) == scu.DEMISSION
|
||||
else sco_codes.DEF
|
||||
)
|
||||
sco_codes.DEM
|
||||
if res.get_etud_etat(etud.id) == scu.DEMISSION
|
||||
else sco_codes.DEF
|
||||
]
|
||||
return
|
||||
|
||||
@ -1588,7 +1484,7 @@ class DecisionsProposeesUE(DecisionsProposees):
|
||||
|
||||
def __repr__(self) -> str:
|
||||
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):
|
||||
"""Calcul des .codes attribuables et de l'explanation associée"""
|
||||
@ -1646,12 +1542,13 @@ class DecisionsProposeesUE(DecisionsProposees):
|
||||
moy_ue=self.moy_ue,
|
||||
)
|
||||
db.session.add(self.validation)
|
||||
db.session.commit()
|
||||
Scolog.logdb(
|
||||
method="jury_but",
|
||||
etudid=self.etud.id,
|
||||
msg=f"Validation UE {self.ue.id} {self.ue.acronyme}({self.moy_ue}): {code}",
|
||||
commit=True,
|
||||
)
|
||||
db.session.commit()
|
||||
log(f"DecisionsProposeesUE: recording {self.validation}")
|
||||
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre.id)
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -8,12 +8,11 @@
|
||||
"""
|
||||
from flask import g, request, url_for
|
||||
|
||||
from openpyxl.styles import Alignment
|
||||
from openpyxl.styles import Font, Border, Side, Alignment, PatternFill
|
||||
|
||||
from app import log
|
||||
from app.but import jury_but
|
||||
from app.but.cursus_but import but_ects_valides
|
||||
from app.models.but_validations import ValidationDUT120
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
@ -56,21 +55,11 @@ def pvjury_page_but(formsemestre_id: int, fmt="html"):
|
||||
else:
|
||||
line_sep = "\n"
|
||||
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
|
||||
xls_style_base = sco_excel.excel_make_style()
|
||||
xls_style_base["alignment"] = Alignment(wrapText=True, vertical="top")
|
||||
|
||||
tab = GenTable(
|
||||
base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}",
|
||||
caption=title,
|
||||
@ -80,7 +69,7 @@ def pvjury_page_but(formsemestre_id: int, fmt="html"):
|
||||
html_title=f"""<div style="margin-bottom: 8px;"><span
|
||||
style="font-size: 120%; font-weight: bold;">{title}</span>
|
||||
<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")}"
|
||||
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 !"
|
||||
)
|
||||
titles = {
|
||||
"nom_pv": "Code" if anonymous else "Nom",
|
||||
"nom": "Code" if anonymous else "Nom",
|
||||
"cursus": "Cursus",
|
||||
"ects": "ECTS",
|
||||
"ues": "UE validées",
|
||||
@ -155,64 +144,33 @@ def pvjury_table_but(
|
||||
except ScoValueError:
|
||||
deca = None
|
||||
|
||||
ects_but_valides = but_ects_valides(etud, referentiel_competence_id)
|
||||
has_diplome = deca.valide_diplome() if deca else False
|
||||
diplome_lst = ["ADM"] if has_diplome else []
|
||||
validation_dut120 = ValidationDUT120.query.filter_by(
|
||||
etudid=etudid, formsemestre_id=formsemestre.id
|
||||
).first()
|
||||
if validation_dut120:
|
||||
diplome_lst.append("Diplôme de DUT validé.")
|
||||
diplome_str = ". ".join(diplome_lst)
|
||||
row = {
|
||||
"nom_pv": (
|
||||
etud.code_ine or etud.code_nip or etud.id
|
||||
if anonymous # Mode anonyme: affiche INE ou sinon NIP, ou id
|
||||
else etud.etat_civil_pv(
|
||||
line_sep=line_sep, with_paragraph=with_paragraph_nom
|
||||
)
|
||||
"nom": etud.code_ine or etud.code_nip or etud.id
|
||||
if anonymous # Mode anonyme: affiche INE ou sinon NIP, ou id
|
||||
else etud.etat_civil_pv(
|
||||
line_sep=line_sep, with_paragraph=with_paragraph_nom
|
||||
),
|
||||
"_nom_pv_order": etud.sort_key,
|
||||
"_nom_pv_target_attrs": f'class="etudinfo" id="{etud.id}"',
|
||||
"_nom_pv_td_attrs": f'id="{etud.id}" class="etudinfo"',
|
||||
"_nom_pv_target": url_for(
|
||||
"scolar.fiche_etud",
|
||||
"_nom_order": etud.sort_key,
|
||||
"_nom_target_attrs": f'class="etudinfo" id="{etud.id}"',
|
||||
"_nom_td_attrs": f'id="{etud.id}" class="etudinfo"',
|
||||
"_nom_target": url_for(
|
||||
"scolar.ficheEtud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=etud.id,
|
||||
),
|
||||
"cursus": _descr_cursus_but(etud),
|
||||
"ects": (
|
||||
f"""{deca.ects_annee():g}<br><br>Tot. {ects_but_valides:g}"""
|
||||
if deca
|
||||
else ""
|
||||
),
|
||||
"_ects_xls": deca.ects_annee() if deca else "",
|
||||
"ects_but": ects_but_valides,
|
||||
"ects": f"""{deca.ects_annee():g}<br><br>Tot. {but_ects_valides(etud, referentiel_competence_id ):g}""",
|
||||
"ues": deca.descr_ues_validation(line_sep=line_sep) if deca else "-",
|
||||
"niveaux": (
|
||||
deca.descr_niveaux_validation(line_sep=line_sep) if deca else "-"
|
||||
),
|
||||
"niveaux": deca.descr_niveaux_validation(line_sep=line_sep)
|
||||
if deca
|
||||
else "-",
|
||||
"decision_but": deca.code_valide if deca else "",
|
||||
"devenir": (
|
||||
"Diplôme obtenu"
|
||||
if has_diplome
|
||||
else (
|
||||
", ".join([f"S{i}" for i in deca.get_autorisations_passage()])
|
||||
if deca
|
||||
else ""
|
||||
)
|
||||
),
|
||||
"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,
|
||||
"devenir": ", ".join([f"S{i}" for i in deca.get_autorisations_passage()])
|
||||
if deca
|
||||
else "",
|
||||
}
|
||||
if (deca and deca.valide_diplome()) or not only_diplome:
|
||||
if deca.valide_diplome() or not only_diplome:
|
||||
rows.append(row)
|
||||
|
||||
rows.sort(key=lambda x: x["_nom_pv_order"])
|
||||
rows.sort(key=lambda x: x["_nom_order"])
|
||||
return rows, titles
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -9,15 +9,15 @@
|
||||
from flask import g, url_for
|
||||
|
||||
from app import db
|
||||
from app.but import jury_but, jury_dut120
|
||||
from app.models import Identite, FormSemestre, ScolarNews, ValidationDUT120
|
||||
from app.but import jury_but
|
||||
from app.models import Identite, FormSemestre, ScolarNews
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
|
||||
def formsemestre_validation_auto_but(
|
||||
formsemestre: FormSemestre, only_adm: bool = True, dry_run=False, with_dut120=True
|
||||
) -> tuple[int, list[jury_but.DecisionsProposeesAnnee]]:
|
||||
formsemestre: FormSemestre, only_adm: bool = True
|
||||
) -> int:
|
||||
"""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
|
||||
@ -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
|
||||
(mode à n'utiliser que pour les tests unitaires vérifiant la saisie des jurys)
|
||||
|
||||
Enregistre aussi le DUT120.
|
||||
|
||||
Returns:
|
||||
- En mode normal, (nombre d'étudiants pour lesquels on a enregistré au moins un code, []])
|
||||
- En mode dry_run, (0, list[DecisionsProposeesAnnee])
|
||||
Returns: nombre d'étudiants pour lesquels on a enregistré au moins un code.
|
||||
"""
|
||||
if not formsemestre.formation.is_apc():
|
||||
raise ScoValueError("fonction réservée aux formations BUT")
|
||||
nb_etud_modif = 0
|
||||
decas = []
|
||||
with sco_cache.DeferredSemCacheManager():
|
||||
for etudid in formsemestre.etuds_inscriptions:
|
||||
etud = Identite.get_etud(etudid)
|
||||
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
|
||||
if not dry_run:
|
||||
modified = deca.record_all(only_validantes=only_adm)
|
||||
modified |= validation_dut120_auto(etud, formsemestre)
|
||||
if modified:
|
||||
nb_etud_modif += 1
|
||||
else:
|
||||
decas.append(deca)
|
||||
nb_etud_modif += deca.record_all(only_validantes=only_adm)
|
||||
|
||||
db.session.commit()
|
||||
ScolarNews.add(
|
||||
@ -60,29 +49,4 @@ def formsemestre_validation_auto_but(
|
||||
formsemestre_id=formsemestre.id,
|
||||
),
|
||||
)
|
||||
return nb_etud_modif, decas
|
||||
|
||||
|
||||
def validation_dut120_auto(etud: Identite, formsemestre: FormSemestre) -> bool:
|
||||
"""Si l'étudiant n'a pas déjà validé son DUT120 dans cette spécialité
|
||||
et qu'il satisfait les confitions, l'enregistre.
|
||||
Returns True si nouvelle décision enregistrée.
|
||||
"""
|
||||
refcomp = formsemestre.formation.referentiel_competence
|
||||
if not refcomp:
|
||||
raise ScoValueError("formation non associée à un référentiel de compétences")
|
||||
validation = ValidationDUT120.query.filter_by(
|
||||
etudid=etud.id, referentiel_competence_id=refcomp.id
|
||||
).first()
|
||||
if validation:
|
||||
return False # déjà enregistré
|
||||
if jury_dut120.etud_valide_dut120(etud, refcomp.id):
|
||||
new_validation = ValidationDUT120(
|
||||
etudid=etud.id,
|
||||
referentiel_competence_id=refcomp.id,
|
||||
formsemestre_id=formsemestre.id, # Replace with appropriate value
|
||||
)
|
||||
db.session.add(new_validation)
|
||||
db.session.commit()
|
||||
return True
|
||||
return False # ne peut pas valider
|
||||
return nb_etud_modif
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -21,6 +21,8 @@ from app.but.jury_but import (
|
||||
DecisionsProposeesRCUE,
|
||||
DecisionsProposeesUE,
|
||||
)
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.models import (
|
||||
ApcNiveau,
|
||||
FormSemestre,
|
||||
@ -31,8 +33,11 @@ from app.models import (
|
||||
ScolarFormSemestreValidation,
|
||||
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.sco_exceptions import ScoValueError
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
@ -71,9 +76,9 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
|
||||
f"""
|
||||
<div class="titre_niveaux">
|
||||
<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={
|
||||
url_for("notes.validation_rcues", scodoc_dept=g.scodoc_dept,
|
||||
url_for("notes.validation_rcues", scodoc_dept=g.scodoc_dept,
|
||||
etudid=deca.etud.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()
|
||||
if formsemestre_2 else ""}</span>
|
||||
</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():
|
||||
@ -104,32 +109,23 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
|
||||
</div>"""
|
||||
)
|
||||
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)
|
||||
# tuples (UniteEns, read_only, dispense)
|
||||
ues_ro_dispense = [
|
||||
# Les UEs à afficher,
|
||||
# qui
|
||||
ues_ro = [
|
||||
(
|
||||
ue_impair,
|
||||
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,
|
||||
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:
|
||||
if reverse_semestre:
|
||||
ues_ro_dispense[0], ues_ro_dispense[1] = (
|
||||
ues_ro_dispense[1],
|
||||
ues_ro_dispense[0],
|
||||
)
|
||||
ues_ro[0], ues_ro[1] = ues_ro[1], ues_ro[0]
|
||||
# Colonnes d'UE:
|
||||
for ue, ue_read_only, ue_dispense in ues_ro_dispense:
|
||||
for ue, ue_read_only in ues_ro:
|
||||
if ue:
|
||||
H.append(
|
||||
_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,
|
||||
annee_prec=ue_read_only,
|
||||
niveau_id=ue.niveau_competence.id,
|
||||
ue_dispense=ue_dispense,
|
||||
)
|
||||
)
|
||||
else:
|
||||
@ -177,7 +172,7 @@ def _gen_but_select(
|
||||
]
|
||||
)
|
||||
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_recorded="{code_valide or ''}"
|
||||
onchange="change_menu_code(this);"
|
||||
@ -193,30 +188,21 @@ def _gen_but_niveau_ue(
|
||||
disabled: bool = False,
|
||||
annee_prec: bool = False,
|
||||
niveau_id: int = None,
|
||||
ue_dispense: bool = False,
|
||||
) -> str:
|
||||
if dec_ue.ue_status and dec_ue.ue_status["is_capitalized"]:
|
||||
moy_ue_str = f"""<span class="ue_cap">{
|
||||
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">
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
<div>
|
||||
{ etat_en_cours }
|
||||
<div>UE 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>
|
||||
"""
|
||||
@ -228,7 +214,7 @@ def _gen_but_niveau_ue(
|
||||
<div>
|
||||
<b>UE {ue.acronyme} antérieure </b>
|
||||
<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>
|
||||
</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>"""
|
||||
if dec_ue.code_valide:
|
||||
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
|
||||
else ""
|
||||
)
|
||||
@ -256,13 +244,7 @@ def _gen_but_niveau_ue(
|
||||
</div>
|
||||
"""
|
||||
else:
|
||||
if dec_ue.ue_status and dec_ue.ue_status["was_capitalized"]:
|
||||
scoplement = """<div class="scoplement">
|
||||
UE déjà capitalisée avec résultat moins favorable.
|
||||
</div>
|
||||
"""
|
||||
else:
|
||||
scoplement = ""
|
||||
scoplement = ""
|
||||
|
||||
ue_class = "" # 'recorded' if dec_ue.code_valide is not None else ''
|
||||
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}
|
||||
{'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>{moy_ue_str}</div>
|
||||
{scoplement}
|
||||
</div>
|
||||
<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.code_valide,
|
||||
disabled=disabled,
|
||||
klass=f"code_ue ue_rcue_{niveau_id}" if not disabled else ""
|
||||
)
|
||||
}</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:
|
||||
"""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
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -10,25 +10,23 @@ Non spécifique au BUT.
|
||||
"""
|
||||
|
||||
from flask import render_template
|
||||
from flask_login import current_user
|
||||
import sqlalchemy as sa
|
||||
|
||||
from app.models import (
|
||||
ApcValidationAnnee,
|
||||
ApcValidationRCUE,
|
||||
Identite,
|
||||
UniteEns,
|
||||
ScolarAutorisationInscription,
|
||||
ScolarFormSemestreValidation,
|
||||
UniteEns,
|
||||
ValidationDUT120,
|
||||
)
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.views import ScoData
|
||||
|
||||
|
||||
def jury_delete_manual(etud: Identite):
|
||||
"""Vue présentant *toutes* les décisions de jury concernant cet étudiant
|
||||
et permettant (si permission) de les supprimer une à une.
|
||||
"""Vue (réservée au chef de dept.)
|
||||
présentant *toutes* les décisions de jury concernant cet étudiant
|
||||
et permettant de les supprimer une à une.
|
||||
"""
|
||||
sem_vals = ScolarFormSemestreValidation.query.filter_by(
|
||||
etudid=etud.id, ue_id=None
|
||||
@ -62,12 +60,8 @@ def jury_delete_manual(etud: Identite):
|
||||
sem_vals=sem_vals,
|
||||
ue_vals=ue_vals,
|
||||
autorisations=autorisations,
|
||||
dut120_vals=ValidationDUT120.query.filter_by(etudid=etud.id).order_by(
|
||||
ValidationDUT120.date
|
||||
),
|
||||
rcue_vals=rcue_vals,
|
||||
annee_but_vals=annee_but_vals,
|
||||
sco=ScoData(),
|
||||
title=f"Toutes les décisions de jury enregistrées pour {etud.html_link_fiche()}",
|
||||
read_only=not current_user.has_permission(Permission.EtudInscrit),
|
||||
)
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -75,7 +75,7 @@ class RegroupementCoherentUE:
|
||||
else None
|
||||
)
|
||||
|
||||
# Autres validations pour les UEs paire/impaire
|
||||
# Autres validations pour l'UE paire
|
||||
self.validation_ue_best_pair = best_autre_ue_validation(
|
||||
etud.id,
|
||||
niveau.id,
|
||||
@ -101,24 +101,14 @@ class RegroupementCoherentUE:
|
||||
"résultats formsemestre de l'UE si elle est courante, None sinon"
|
||||
self.ue_status_impair = None
|
||||
if self.ue_cur_impair:
|
||||
# UE courante
|
||||
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.ue_1 = self.ue_cur_impair
|
||||
self.res_impair = res_impair
|
||||
self.ue_status_impair = ue_status
|
||||
elif self.validation_ue_best_impair:
|
||||
# UE capitalisée
|
||||
self.moy_ue_1 = self.validation_ue_best_impair.moy_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:
|
||||
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
|
||||
|
@ -1,13 +1,16 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""Jury édition manuelle des décisions RCUE antérieures
|
||||
"""Jury édition manuelle des décisions (correction d'erreurs, parcours hors normes)
|
||||
|
||||
Non spécifique au BUT.
|
||||
"""
|
||||
|
||||
from flask import render_template
|
||||
import sqlalchemy as sa
|
||||
|
||||
from app import log
|
||||
from app.but import cursus_but
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -30,9 +30,7 @@ class StatsMoyenne:
|
||||
self.max = np.nanmax(vals)
|
||||
self.size = len(vals)
|
||||
self.nb_vals = self.size - np.count_nonzero(np.isnan(vals))
|
||||
except (
|
||||
TypeError
|
||||
): # que des NaN dans un array d'objets, ou ce genre de choses exotiques...
|
||||
except 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
|
||||
|
||||
def to_dict(self):
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -667,12 +667,10 @@ class BonusCalais(BonusSportAdditif):
|
||||
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 :
|
||||
<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><b>en DUT</b> à la moyenne générale du semestre déjà obtenue par l'étudiant;
|
||||
</li>
|
||||
<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><b>en BUT et LP</b> à la moyenne des UE dont l'acronyme fini par <b>BS</b>
|
||||
(ex : UE2.1BS, UE32BS)
|
||||
</li>
|
||||
</ul>
|
||||
"""
|
||||
@ -694,17 +692,12 @@ class BonusCalais(BonusSportAdditif):
|
||||
else:
|
||||
self.classic_use_bonus_ues = True # pour les LP
|
||||
super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
|
||||
if (
|
||||
self.formsemestre.annee_scolaire() < 2023
|
||||
or not self.formsemestre.formation.is_apc()
|
||||
):
|
||||
# LP et anciens semestres: ne s'applique qu'aux UE dont l'acronyme termine par BS
|
||||
ues = self.formsemestre.get_ues(with_sport=False)
|
||||
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
|
||||
ues = self.formsemestre.get_ues(with_sport=False)
|
||||
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):
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# 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
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@ -27,7 +27,6 @@
|
||||
|
||||
"""caches pour tables APC
|
||||
"""
|
||||
from flask import g
|
||||
|
||||
from app.scodoc import sco_cache
|
||||
|
||||
@ -48,27 +47,3 @@ class EvaluationsPoidsCache(sco_cache.ScoDocCache):
|
||||
"""
|
||||
|
||||
prefix = "EPC"
|
||||
|
||||
@classmethod
|
||||
def invalidate_all(cls):
|
||||
"delete all cached evaluations poids (in current dept)"
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
|
||||
moduleimpl_ids = [
|
||||
mi.id
|
||||
for mi in ModuleImpl.query.join(FormSemestre).filter_by(
|
||||
dept_id=g.scodoc_dept_id
|
||||
)
|
||||
]
|
||||
cls.delete_many(moduleimpl_ids)
|
||||
|
||||
@classmethod
|
||||
def invalidate_sem(cls, formsemestre_id):
|
||||
"delete cached evaluations poids for this formsemestre from cache"
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
|
||||
moduleimpl_ids = [
|
||||
mi.id for mi in ModuleImpl.query.filter_by(formsemestre_id=formsemestre_id)
|
||||
]
|
||||
cls.delete_many(moduleimpl_ids)
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -23,7 +23,6 @@ from app.models import (
|
||||
)
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
class ValidationsSemestre(ResultatsCache):
|
||||
@ -39,7 +38,7 @@ class ValidationsSemestre(ResultatsCache):
|
||||
super().__init__(formsemestre, sco_cache.ValidationsSemestreCache)
|
||||
|
||||
self.decisions_jury = {}
|
||||
"""Décisions prises dans ce semestre:
|
||||
"""Décisions prises dans ce semestre:
|
||||
{ etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }}"""
|
||||
self.decisions_jury_ues = {}
|
||||
"""Décisions sur des UEs dans ce semestre:
|
||||
@ -85,7 +84,7 @@ class ValidationsSemestre(ResultatsCache):
|
||||
"code": decision.code,
|
||||
"assidu": decision.assidu,
|
||||
"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
|
||||
|
||||
@ -108,7 +107,7 @@ class ValidationsSemestre(ResultatsCache):
|
||||
decisions_jury_ues[decision.etudid][decision.ue.id] = {
|
||||
"code": decision.code,
|
||||
"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
|
||||
@ -146,11 +145,11 @@ def formsemestre_get_ue_capitalisees(formsemestre: FormSemestre) -> pd.DataFrame
|
||||
query = sa.text(
|
||||
"""
|
||||
SELECT DISTINCT SFV.*, ue.ue_code
|
||||
FROM
|
||||
notes_ue ue,
|
||||
FROM
|
||||
notes_ue ue,
|
||||
notes_formations nf,
|
||||
notes_formations nf2,
|
||||
scolar_formsemestre_validation SFV,
|
||||
notes_formations nf2,
|
||||
scolar_formsemestre_validation SFV,
|
||||
notes_formsemestre sem,
|
||||
notes_formsemestre_inscription ins
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# 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
|
||||
# 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
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
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_utils as scu
|
||||
from app.scodoc.codes_cursus import UE_SPORT
|
||||
from app.scodoc.sco_exceptions import ScoBugCatcher
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
|
||||
@ -54,7 +56,6 @@ class EvaluationEtat:
|
||||
|
||||
evaluation_id: int
|
||||
nb_attente: int
|
||||
nb_notes: int # nb notes d'étudiants inscrits au semestre et au modimpl
|
||||
is_complete: bool
|
||||
|
||||
def to_dict(self):
|
||||
@ -71,15 +72,7 @@ class ModuleImplResults:
|
||||
les caches sont gérés par ResultatsSemestre.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
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.
|
||||
"""
|
||||
def __init__(self, moduleimpl: ModuleImpl):
|
||||
self.moduleimpl_id = moduleimpl.id
|
||||
self.module_id = moduleimpl.module.id
|
||||
self.etudids = None
|
||||
@ -112,23 +105,14 @@ class ModuleImplResults:
|
||||
"""
|
||||
self.evals_etudids_sans_note = {}
|
||||
"""dict: evaluation_id : set des etudids non notés dans cette eval, sans les démissions."""
|
||||
self.evals_type = {}
|
||||
"""Type de chaque eval { evaluation.id : evaluation.evaluation_type }"""
|
||||
self.load_notes(etudids, etudids_actifs)
|
||||
self.load_notes()
|
||||
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"""
|
||||
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"""
|
||||
|
||||
def load_notes(
|
||||
self, etudids: list[int], etudids_actifs: set[int]
|
||||
): # ré-écriture de df_load_modimpl_notes
|
||||
def load_notes(self): # ré-écriture de df_load_modimpl_notes
|
||||
"""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
|
||||
colonnes: le nom de la colonne est l'evaluation_id (int)
|
||||
index (lignes): etudid (int)
|
||||
@ -151,12 +135,12 @@ class ModuleImplResults:
|
||||
qui ont des notes ATT.
|
||||
"""
|
||||
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":
|
||||
# on prend les inscrits au module ET au semestre (donc sans démissionnaires)
|
||||
inscrits_module = {ins.etud.id for ins in moduleimpl.inscriptions}.intersection(
|
||||
etudids_actifs
|
||||
moduleimpl.formsemestre.etudids_actifs
|
||||
)
|
||||
self.nb_inscrits_module = len(inscrits_module)
|
||||
|
||||
@ -164,24 +148,19 @@ class ModuleImplResults:
|
||||
evals_notes = pd.DataFrame(index=self.etudids, dtype=float)
|
||||
self.evaluations_completes = []
|
||||
self.evaluations_completes_dict = {}
|
||||
self.etudids_attente = set() # empty
|
||||
self.evals_type = {}
|
||||
evaluation: Evaluation
|
||||
for evaluation in moduleimpl.evaluations:
|
||||
self.evals_type[evaluation.id] = evaluation.evaluation_type
|
||||
eval_df = self._load_evaluation_notes(evaluation)
|
||||
# is_complete ssi
|
||||
# tous les inscrits (non dem) au module ont une note
|
||||
# ou évaluation déclarée "à prise en compte immédiate"
|
||||
# ou rattrapage, 2eme session, bonus
|
||||
# ET pas bloquée par date (is_blocked)
|
||||
is_blocked = evaluation.is_blocked()
|
||||
# is_complete ssi tous les inscrits (non dem) au semestre ont une note
|
||||
# ou évaluation déclarée "à prise en compte immédiate"
|
||||
# Les évaluations de rattrapage et 2eme session sont toujours complètes
|
||||
|
||||
etudids_sans_note = inscrits_module - set(eval_df.index) # sans les dem.
|
||||
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 (not etudids_sans_note)
|
||||
) and not is_blocked
|
||||
)
|
||||
self.evaluations_completes.append(is_complete)
|
||||
self.evaluations_completes_dict[evaluation.id] = is_complete
|
||||
self.evals_etudids_sans_note[evaluation.id] = etudids_sans_note
|
||||
@ -189,39 +168,25 @@ class ModuleImplResults:
|
||||
# NULL en base => ABS (= -999)
|
||||
eval_df.fillna(scu.NOTES_ABSENCE, inplace=True)
|
||||
# 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):
|
||||
evals_notes = evals_notes.merge(
|
||||
eval_df, how="left", left_index=True, right_index=True
|
||||
)
|
||||
# Notes en attente: (ne prend en compte que les inscrits, non démissionnaires)
|
||||
eval_notes_inscr = evals_notes[str(evaluation.id)][list(inscrits_module)]
|
||||
# Nombre de notes (non vides, incluant ATT etc) des inscrits:
|
||||
nb_notes = eval_notes_inscr.notna().sum()
|
||||
|
||||
if is_blocked:
|
||||
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:
|
||||
eval_etudids_attente = set(
|
||||
eval_notes_inscr.iloc[
|
||||
(eval_notes_inscr == scu.NOTES_ATTENTE).to_numpy()
|
||||
].index
|
||||
)
|
||||
self.etudids_attente |= eval_etudids_attente
|
||||
self.evaluations_etat[evaluation.id] = EvaluationEtat(
|
||||
evaluation_id=evaluation.id,
|
||||
nb_attente=len(eval_etudids_attente),
|
||||
nb_notes=int(nb_notes),
|
||||
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)
|
||||
|
||||
# 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)])
|
||||
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.
|
||||
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)
|
||||
"""
|
||||
return (
|
||||
np.array(
|
||||
[
|
||||
(
|
||||
e.coefficient
|
||||
if e.evaluation_type == Evaluation.EVALUATION_NORMALE
|
||||
else 0.0
|
||||
)
|
||||
for e in modimpl.evaluations
|
||||
e.coefficient
|
||||
if e.evaluation_type == scu.EVALUATION_NORMALE
|
||||
else 0.0
|
||||
for e in moduleimpl.evaluations
|
||||
],
|
||||
dtype=float,
|
||||
)
|
||||
* self.evaluations_completes
|
||||
).reshape(-1, 1)
|
||||
|
||||
def get_evaluations_special_coefs(
|
||||
self, modimpl: ModuleImpl, evaluation_type=Evaluation.EVALUATION_SESSION2
|
||||
) -> np.array:
|
||||
"""Coefficients des évaluations de session 2 ou rattrapage.
|
||||
Les évals de session 2 et rattrapage sont réputées "complètes": elles sont toujours
|
||||
prises en compte mais seules les notes numériques et ABS sont utilisées.
|
||||
Résultat: 2d-array of floats, shape (nb_evals, 1)
|
||||
"""
|
||||
return (
|
||||
np.array(
|
||||
[
|
||||
(e.coefficient if e.evaluation_type == evaluation_type else 0.0)
|
||||
for e in modimpl.evaluations
|
||||
],
|
||||
dtype=float,
|
||||
)
|
||||
).reshape(-1, 1)
|
||||
|
||||
# was _list_notes_evals_titles
|
||||
def get_evaluations_completes(self, moduleimpl: ModuleImpl) -> list[Evaluation]:
|
||||
"Liste des évaluations complètes"
|
||||
@ -309,7 +266,7 @@ class ModuleImplResults:
|
||||
) / [e.note_max / 20.0 for e in moduleimpl.evaluations]
|
||||
|
||||
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 }
|
||||
avec les valeurs float, ou "ABS" ou EXC
|
||||
"""
|
||||
@ -318,55 +275,44 @@ class ModuleImplResults:
|
||||
for (etudid, x) in self.evals_notes[evaluation_id].items()
|
||||
}
|
||||
|
||||
def get_evaluations_rattrapage(self, moduleimpl: ModuleImpl) -> list[Evaluation]:
|
||||
"""Les évaluations de rattrapage de ce module.
|
||||
def get_evaluation_rattrapage(self, moduleimpl: ModuleImpl):
|
||||
"""L'évaluation de rattrapage de ce module, ou None s'il n'en a pas.
|
||||
Rattrapage: la moyenne du module est la meilleure note entre moyenne
|
||||
des autres évals et la moyenne des notes de rattrapage.
|
||||
des autres évals et la note eval rattrapage.
|
||||
"""
|
||||
return [
|
||||
eval_list = [
|
||||
e
|
||||
for e in moduleimpl.evaluations
|
||||
if e.evaluation_type == Evaluation.EVALUATION_RATTRAPAGE
|
||||
if e.evaluation_type == scu.EVALUATION_RATTRAPAGE
|
||||
]
|
||||
if eval_list:
|
||||
return eval_list[0]
|
||||
return None
|
||||
|
||||
def get_evaluations_session2(self, moduleimpl: ModuleImpl) -> list[Evaluation]:
|
||||
"""Les évaluations de deuxième session de ce module, ou None s'il n'en a pas.
|
||||
La moyenne des notes de Session 2 remplace la note de moyenne des autres évals.
|
||||
def get_evaluation_session2(self, moduleimpl: ModuleImpl):
|
||||
"""L'évaluation de deuxième session de ce module, ou None s'il n'en a pas.
|
||||
Session 2: remplace la note de moyenne des autres évals.
|
||||
"""
|
||||
return [
|
||||
eval_list = [
|
||||
e
|
||||
for e in moduleimpl.evaluations
|
||||
if e.evaluation_type == Evaluation.EVALUATION_SESSION2
|
||||
]
|
||||
|
||||
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 e.evaluation_type == scu.EVALUATION_SESSION2
|
||||
]
|
||||
if eval_list:
|
||||
return eval_list[0]
|
||||
return None
|
||||
|
||||
|
||||
class ModuleImplResultsAPC(ModuleImplResults):
|
||||
"Calcul des moyennes de modules à la mode BUT"
|
||||
|
||||
def compute_module_moy(
|
||||
self, evals_poids_df: pd.DataFrame, modimpl_coefs_df: pd.DataFrame
|
||||
self,
|
||||
evals_poids_df: pd.DataFrame,
|
||||
) -> pd.DataFrame:
|
||||
"""Calcule les moyennes des étudiants dans ce module
|
||||
|
||||
Argument:
|
||||
evals_poids: DataFrame, colonnes: UEs, lignes: EVALs
|
||||
modimpl_coefs_df: DataFrame, colonnes: modimpl_id, lignes: ue_id
|
||||
Argument: evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs
|
||||
|
||||
Résultat: DataFrame, colonnes UE, lignes etud
|
||||
= la note de l'étudiant dans chaque UE pour ce module.
|
||||
@ -387,7 +333,6 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
||||
return pd.DataFrame(index=[], columns=evals_poids_df.columns)
|
||||
if nb_ues == 0:
|
||||
return pd.DataFrame(index=self.evals_notes.index, columns=[])
|
||||
# coefs des évals complètes normales (pas rattr., session 2 ni bonus):
|
||||
evals_coefs = self.get_evaluations_coefs(modimpl)
|
||||
evals_poids = evals_poids_df.values * evals_coefs
|
||||
# -> evals_poids shape : (nb_evals, nb_ues)
|
||||
@ -401,7 +346,7 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
||||
# et dans dans evals_poids_etuds
|
||||
# (rappel: la comparaison est toujours false face à un NaN)
|
||||
# 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(
|
||||
np.stack([self.evals_notes.values] * nb_ues, axis=2) > scu.NOTES_NEUTRALISE,
|
||||
poids_stacked,
|
||||
@ -409,61 +354,51 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
||||
)
|
||||
# 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 shape: nb_etuds, nb_evals, nb_ues
|
||||
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
|
||||
etuds_moy_module = np.sum(
|
||||
evals_poids_etuds * evals_notes_stacked, 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)
|
||||
evals_rat = self.get_evaluations_rattrapage(modimpl)
|
||||
if evals_session2:
|
||||
# Session2 : quand elle existe, remplace la note de module
|
||||
# Calcul moyenne notes session2 et remplace (si la note session 2 existe)
|
||||
etuds_moy_module_s2 = self._compute_moy_special(
|
||||
modimpl,
|
||||
evals_notes_stacked,
|
||||
evals_poids_df,
|
||||
Evaluation.EVALUATION_SESSION2,
|
||||
)
|
||||
|
||||
# Vrai si toutes les UEs avec coef non nul ont bien une note de session 2 calculée:
|
||||
mod_coefs = modimpl_coefs_df[modimpl.id]
|
||||
etuds_use_session2 = np.all(
|
||||
np.isfinite(etuds_moy_module_s2[:, mod_coefs != 0]), axis=1
|
||||
)
|
||||
# Session2 : quand elle existe, remplace la note de module
|
||||
eval_session2 = self.get_evaluation_session2(modimpl)
|
||||
if eval_session2:
|
||||
notes_session2 = self.evals_notes[eval_session2.id].values
|
||||
# n'utilise que les notes valides (pas ATT, EXC, ABS, NaN)
|
||||
etuds_use_session2 = notes_session2 > scu.NOTES_ABSENCE
|
||||
etuds_moy_module = np.where(
|
||||
etuds_use_session2[:, np.newaxis],
|
||||
etuds_moy_module_s2,
|
||||
np.tile(
|
||||
(notes_session2 / (eval_session2.note_max / 20.0))[:, np.newaxis],
|
||||
nb_ues,
|
||||
),
|
||||
etuds_moy_module,
|
||||
)
|
||||
self.etuds_use_session2 = pd.Series(
|
||||
etuds_use_session2, index=self.evals_notes.index
|
||||
)
|
||||
elif evals_rat:
|
||||
etuds_moy_module_rat = self._compute_moy_special(
|
||||
modimpl,
|
||||
evals_notes_stacked,
|
||||
evals_poids_df,
|
||||
Evaluation.EVALUATION_RATTRAPAGE,
|
||||
)
|
||||
etuds_ue_use_rattrapage = (
|
||||
etuds_moy_module_rat > etuds_moy_module
|
||||
) # etud x UE
|
||||
etuds_moy_module = np.where(
|
||||
etuds_ue_use_rattrapage, etuds_moy_module_rat, etuds_moy_module
|
||||
)
|
||||
self.etuds_use_rattrapage = pd.Series(
|
||||
np.any(etuds_ue_use_rattrapage, axis=1), index=self.evals_notes.index
|
||||
)
|
||||
# Application des évaluations bonus:
|
||||
etuds_moy_module = self.apply_bonus(
|
||||
etuds_moy_module,
|
||||
modimpl,
|
||||
evals_poids_df,
|
||||
evals_notes_stacked,
|
||||
)
|
||||
else:
|
||||
# Rattrapage: remplace la note de module ssi elle est supérieure
|
||||
eval_rat = self.get_evaluation_rattrapage(modimpl)
|
||||
if eval_rat:
|
||||
notes_rat = self.evals_notes[eval_rat.id].values
|
||||
# remplace les notes invalides (ATT, EXC...) par des NaN
|
||||
notes_rat = np.where(
|
||||
notes_rat > scu.NOTES_ABSENCE,
|
||||
notes_rat / (eval_rat.note_max / 20.0),
|
||||
np.nan,
|
||||
)
|
||||
# "Étend" le rattrapage sur les UE: la note de rattrapage est la même
|
||||
# pour toutes les UE mais ne remplace que là où elle est supérieure
|
||||
notes_rat_ues = np.stack([notes_rat] * nb_ues, axis=1)
|
||||
# prend le max
|
||||
etuds_use_rattrapage = notes_rat_ues > etuds_moy_module
|
||||
etuds_moy_module = np.where(
|
||||
etuds_use_rattrapage, notes_rat_ues, etuds_moy_module
|
||||
)
|
||||
# Serie indiquant que l'étudiant utilise une note de rattrapage sur l'une des UE:
|
||||
self.etuds_use_rattrapage = pd.Series(
|
||||
etuds_use_rattrapage.any(axis=1), index=self.evals_notes.index
|
||||
)
|
||||
self.etuds_moy_module = pd.DataFrame(
|
||||
etuds_moy_module,
|
||||
index=self.evals_notes.index,
|
||||
@ -471,58 +406,6 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
||||
)
|
||||
return self.etuds_moy_module
|
||||
|
||||
def _compute_moy_special(
|
||||
self,
|
||||
modimpl: ModuleImpl,
|
||||
evals_notes_stacked: np.array,
|
||||
evals_poids_df: pd.DataFrame,
|
||||
evaluation_type: int,
|
||||
) -> np.array:
|
||||
"""Calcul moyenne APC sur évals rattrapage ou session2"""
|
||||
nb_etuds = self.evals_notes.shape[0]
|
||||
nb_ues = evals_poids_df.shape[1]
|
||||
evals_coefs_s2 = self.get_evaluations_special_coefs(
|
||||
modimpl, evaluation_type=evaluation_type
|
||||
)
|
||||
evals_poids_s2 = evals_poids_df.values * evals_coefs_s2
|
||||
poids_stacked_s2 = np.stack(
|
||||
[evals_poids_s2] * nb_etuds
|
||||
) # nb_etuds, nb_evals, nb_ues
|
||||
evals_poids_etuds_s2 = np.where(
|
||||
np.stack([self.evals_notes.values] * nb_ues, axis=2) > scu.NOTES_NEUTRALISE,
|
||||
poids_stacked_s2,
|
||||
0,
|
||||
)
|
||||
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
|
||||
etuds_moy_module_s2 = np.sum(
|
||||
evals_poids_etuds_s2 * evals_notes_stacked, axis=1
|
||||
) / np.sum(evals_poids_etuds_s2, axis=1)
|
||||
return etuds_moy_module_s2
|
||||
|
||||
def apply_bonus(
|
||||
self,
|
||||
etuds_moy_module: pd.DataFrame,
|
||||
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]:
|
||||
"""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)
|
||||
"""
|
||||
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)
|
||||
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)
|
||||
if (
|
||||
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):
|
||||
try:
|
||||
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...
|
||||
|
||||
# Initialise poids non enregistrés:
|
||||
@ -571,7 +455,6 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
|
||||
return evals_poids, ues
|
||||
|
||||
|
||||
# appelé par ModuleImpl.check_apc_conformity()
|
||||
def moduleimpl_is_conforme(
|
||||
moduleimpl, evals_poids: pd.DataFrame, modimpl_coefs_df: pd.DataFrame
|
||||
) -> bool:
|
||||
@ -593,12 +476,12 @@ def moduleimpl_is_conforme(
|
||||
if len(modimpl_coefs_df) != nb_ues:
|
||||
# il arrive (#bug) que le cache ne soit pas à jour...
|
||||
sco_cache.invalidate_formsemestre()
|
||||
return app.critical_error("moduleimpl_is_conforme: err 1")
|
||||
raise ScoBugCatcher("moduleimpl_is_conforme: nb ue incoherent")
|
||||
|
||||
if moduleimpl.id not in modimpl_coefs_df:
|
||||
# soupçon de bug cache coef ?
|
||||
sco_cache.invalidate_formsemestre()
|
||||
return app.critical_error("moduleimpl_is_conforme: err 2")
|
||||
raise ScoBugCatcher("Erreur 454 - merci de ré-essayer")
|
||||
|
||||
module_evals_poids = evals_poids.transpose().sum(axis=1) != 0
|
||||
return all((modimpl_coefs_df[moduleimpl.id] != 0).eq(module_evals_poids))
|
||||
@ -640,87 +523,42 @@ class ModuleImplResultsClassic(ModuleImplResults):
|
||||
evals_coefs_etuds * evals_notes_20, axis=1
|
||||
) / np.sum(evals_coefs_etuds, axis=1)
|
||||
|
||||
evals_session2 = self.get_evaluations_session2(modimpl)
|
||||
evals_rat = self.get_evaluations_rattrapage(modimpl)
|
||||
if evals_session2:
|
||||
# Session2 : quand elle existe, remplace la note de module
|
||||
# Calcule la moyenne des évaluations de session2
|
||||
etuds_moy_module_s2 = self._compute_moy_special(
|
||||
modimpl, evals_notes_20, Evaluation.EVALUATION_SESSION2
|
||||
)
|
||||
etuds_use_session2 = np.isfinite(etuds_moy_module_s2)
|
||||
# Session2 : quand elle existe, remplace la note de module
|
||||
eval_session2 = self.get_evaluation_session2(modimpl)
|
||||
if eval_session2:
|
||||
notes_session2 = self.evals_notes[eval_session2.id].values
|
||||
# n'utilise que les notes valides (pas ATT, EXC, ABS, NaN)
|
||||
etuds_use_session2 = notes_session2 > scu.NOTES_ABSENCE
|
||||
etuds_moy_module = np.where(
|
||||
etuds_use_session2,
|
||||
etuds_moy_module_s2,
|
||||
notes_session2 / (eval_session2.note_max / 20.0),
|
||||
etuds_moy_module,
|
||||
)
|
||||
self.etuds_use_session2 = pd.Series(
|
||||
etuds_use_session2, index=self.evals_notes.index
|
||||
)
|
||||
elif evals_rat:
|
||||
else:
|
||||
# Rattrapage: remplace la note de module ssi elle est supérieure
|
||||
# Calcule la moyenne des évaluations de rattrapage
|
||||
etuds_moy_module_rat = self._compute_moy_special(
|
||||
modimpl, evals_notes_20, Evaluation.EVALUATION_RATTRAPAGE
|
||||
)
|
||||
etuds_use_rattrapage = etuds_moy_module_rat > etuds_moy_module
|
||||
etuds_moy_module = np.where(
|
||||
etuds_use_rattrapage, etuds_moy_module_rat, etuds_moy_module
|
||||
)
|
||||
self.etuds_use_rattrapage = pd.Series(
|
||||
etuds_use_rattrapage, index=self.evals_notes.index
|
||||
)
|
||||
|
||||
# Application des évaluations bonus:
|
||||
etuds_moy_module = self.apply_bonus(
|
||||
etuds_moy_module,
|
||||
modimpl,
|
||||
evals_notes_20,
|
||||
)
|
||||
eval_rat = self.get_evaluation_rattrapage(modimpl)
|
||||
if eval_rat:
|
||||
notes_rat = self.evals_notes[eval_rat.id].values
|
||||
# remplace les notes invalides (ATT, EXC...) par des NaN
|
||||
notes_rat = np.where(
|
||||
notes_rat > scu.NOTES_ABSENCE,
|
||||
notes_rat / (eval_rat.note_max / 20.0),
|
||||
np.nan,
|
||||
)
|
||||
# prend le max
|
||||
etuds_use_rattrapage = notes_rat > etuds_moy_module
|
||||
etuds_moy_module = np.where(
|
||||
etuds_use_rattrapage, notes_rat, etuds_moy_module
|
||||
)
|
||||
self.etuds_use_rattrapage = pd.Series(
|
||||
etuds_use_rattrapage, index=self.evals_notes.index
|
||||
)
|
||||
self.etuds_moy_module = pd.Series(
|
||||
etuds_moy_module,
|
||||
index=self.evals_notes.index,
|
||||
)
|
||||
|
||||
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
|
||||
#
|
||||
# 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
|
||||
# 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(
|
||||
Markup(
|
||||
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>)"""
|
||||
)
|
||||
)
|
||||
@ -100,7 +100,7 @@ def compute_sem_moys_apc_using_ects(
|
||||
|
||||
|
||||
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.
|
||||
|
||||
Result: couple (tuple)
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# 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
|
||||
# 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
|
||||
# sur toutes les UE)
|
||||
default_poids = {
|
||||
mod.id: (
|
||||
1.0
|
||||
if (mod.module_type == ModuleType.STANDARD) and (mod.ue.type == UE_SPORT)
|
||||
else 0.0
|
||||
)
|
||||
mod.id: 1.0
|
||||
if (mod.module_type == ModuleType.STANDARD) and (mod.ue.type == UE_SPORT)
|
||||
else 0.0
|
||||
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
|
||||
# sur toutes les UE)
|
||||
default_poids = {
|
||||
modimpl.id: (
|
||||
1.0
|
||||
if (modimpl.module.module_type == ModuleType.STANDARD)
|
||||
and (modimpl.module.ue.type == UE_SPORT)
|
||||
else 0.0
|
||||
)
|
||||
modimpl.id: 1.0
|
||||
if (modimpl.module.module_type == ModuleType.STANDARD)
|
||||
and (modimpl.module.ue.type == UE_SPORT)
|
||||
else 0.0
|
||||
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)
|
||||
|
||||
|
||||
def notes_sem_load_cube(
|
||||
formsemestre: FormSemestre, modimpl_coefs_df: pd.DataFrame
|
||||
) -> tuple:
|
||||
def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
|
||||
"""Construit le "cube" (tenseur) des notes du semestre.
|
||||
Charge toutes les notes (sql), calcule les moyennes des modules
|
||||
et assemble le cube.
|
||||
@ -206,11 +200,10 @@ def notes_sem_load_cube(
|
||||
modimpls_results = {}
|
||||
modimpls_evals_poids = {}
|
||||
modimpls_notes = []
|
||||
etudids, etudids_actifs = formsemestre.etudids_actifs()
|
||||
for modimpl in formsemestre.modimpls_sorted:
|
||||
mod_results = moy_mod.ModuleImplResultsAPC(modimpl, etudids, etudids_actifs)
|
||||
evals_poids = modimpl.get_evaluations_poids()
|
||||
etuds_moy_module = mod_results.compute_module_moy(evals_poids, modimpl_coefs_df)
|
||||
mod_results = moy_mod.ModuleImplResultsAPC(modimpl)
|
||||
evals_poids, _ = moy_mod.load_evaluations_poids(modimpl.id)
|
||||
etuds_moy_module = mod_results.compute_module_moy(evals_poids)
|
||||
modimpls_results[modimpl.id] = mod_results
|
||||
modimpls_evals_poids[modimpl.id] = evals_poids
|
||||
modimpls_notes.append(etuds_moy_module)
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -59,17 +59,16 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
|
||||
def compute(self):
|
||||
"Charge les notes et inscriptions et calcule les moyennes d'UE et gen."
|
||||
self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs(
|
||||
self.formsemestre, modimpls=self.formsemestre.modimpls_sorted
|
||||
)
|
||||
(
|
||||
self.sem_cube,
|
||||
self.modimpls_evals_poids,
|
||||
self.modimpls_results,
|
||||
) = moy_ue.notes_sem_load_cube(self.formsemestre, self.modimpl_coefs_df)
|
||||
) = moy_ue.notes_sem_load_cube(self.formsemestre)
|
||||
self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre)
|
||||
self.ues_inscr_parcours_df = self.load_ues_inscr_parcours()
|
||||
|
||||
self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs(
|
||||
self.formsemestre, modimpls=self.formsemestre.modimpls_sorted
|
||||
)
|
||||
# l'idx de la colonne du mod modimpl.id est
|
||||
# modimpl_coefs_df.columns.get_loc(modimpl.id)
|
||||
# idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id)
|
||||
@ -274,7 +273,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
return s.index[s.notna()]
|
||||
|
||||
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.
|
||||
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.
|
||||
@ -308,7 +307,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
|
||||
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
|
||||
émanant de ce formsemestre pour cet étudiant.
|
||||
prend aussi en compte les autorisations de passage.
|
||||
@ -319,12 +318,9 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
or ApcValidationAnnee.query.filter_by(
|
||||
formsemestre_id=self.formsemestre.id, etudid=etudid
|
||||
).count()
|
||||
or (
|
||||
include_rcues
|
||||
and ApcValidationRCUE.query.filter_by(
|
||||
formsemestre_id=self.formsemestre.id, etudid=etudid
|
||||
).count()
|
||||
)
|
||||
or ApcValidationRCUE.query.filter_by(
|
||||
formsemestre_id=self.formsemestre.id, etudid=etudid
|
||||
).count()
|
||||
)
|
||||
|
||||
def get_validations_annee(self) -> dict[int, ApcValidationAnnee]:
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -234,7 +234,7 @@ class ResultatsSemestreClassic(NotesTableCompat):
|
||||
raise ScoValueError(
|
||||
f"""<div class="scovalueerror"><p>Coefficient de l'UE capitalisée {ue.acronyme}
|
||||
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>
|
||||
<p>Il faut <a href="{
|
||||
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>
|
||||
</div>
|
||||
""",
|
||||
safe=True,
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
@ -257,9 +256,8 @@ def notes_sem_load_matrix(formsemestre: FormSemestre) -> tuple[np.ndarray, dict]
|
||||
"""
|
||||
modimpls_results = {}
|
||||
modimpls_notes = []
|
||||
etudids, etudids_actifs = formsemestre.etudids_actifs()
|
||||
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()
|
||||
modimpls_results[modimpl.id] = mod_results
|
||||
modimpls_notes.append(etuds_moy_module)
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -9,13 +9,12 @@
|
||||
|
||||
from collections import Counter, defaultdict
|
||||
from collections.abc import Generator
|
||||
import datetime
|
||||
from functools import cached_property
|
||||
from operator import attrgetter
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import sqlalchemy as sa
|
||||
|
||||
from flask import g, url_for
|
||||
|
||||
from app import db
|
||||
@ -23,19 +22,14 @@ from app.comp import res_sem
|
||||
from app.comp.res_cache import ResultatsCache
|
||||
from app.comp.jury import ValidationsSemestre
|
||||
from app.comp.moy_mod import ModuleImplResults
|
||||
from app.models import (
|
||||
Evaluation,
|
||||
FormSemestre,
|
||||
FormSemestreUECoef,
|
||||
Identite,
|
||||
ModuleImpl,
|
||||
ModuleImplInscription,
|
||||
ScolarAutorisationInscription,
|
||||
UniteEns,
|
||||
)
|
||||
from app.models import FormSemestre, FormSemestreUECoef
|
||||
from app.models import Identite
|
||||
from app.models import ModuleImpl, ModuleImplInscription
|
||||
from app.models import ScolarAutorisationInscription
|
||||
from app.models.ues import UniteEns
|
||||
from app.scodoc.sco_cache import ResultatsSemestreCache
|
||||
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
|
||||
|
||||
|
||||
@ -150,7 +144,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
def etud_ects_tot_sem(self, etudid: int) -> float:
|
||||
"""Le total des ECTS associées à ce semestre (que l'étudiant peut ou non valider)"""
|
||||
etud_ues = self.etud_ues(etudid)
|
||||
return sum([ue.ects or 0.0 for ue in etud_ues]) if etud_ues else 0.0
|
||||
return sum([ue.ects or 0 for ue in etud_ues]) if etud_ues else 0.0
|
||||
|
||||
def modimpl_notes(self, modimpl_id: int, ue_id: int) -> np.ndarray:
|
||||
"""Les notes moyennes des étudiants du sem. à ce modimpl dans cette ue.
|
||||
@ -198,86 +192,16 @@ class ResultatsSemestre(ResultatsCache):
|
||||
*[mr.etudids_attente for mr in self.modimpls_results.values()]
|
||||
)
|
||||
|
||||
# Etat des évaluations
|
||||
def get_evaluation_etat(self, evaluation: Evaluation) -> dict:
|
||||
"""État d'une évaluation
|
||||
{
|
||||
"coefficient" : float, # 0 si None
|
||||
"description" : str, # de l'évaluation, "" si None
|
||||
"etat" {
|
||||
"blocked" : bool, # vrai si prise en compte bloquée
|
||||
"evalcomplete" : bool,
|
||||
"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"]
|
||||
#
|
||||
# # Etat des évaluations
|
||||
# # (se substitue à do_evaluation_etat, sans les moyennes par groupes)
|
||||
# def get_evaluations_etats(evaluation_id: int) -> dict:
|
||||
# """Renvoie dict avec les clés:
|
||||
# last_modif
|
||||
# nb_evals_completes
|
||||
# nb_evals_en_cours
|
||||
# nb_evals_vides
|
||||
# attente
|
||||
# """
|
||||
|
||||
# --- JURY...
|
||||
def get_formsemestre_validations(self) -> ValidationsSemestre:
|
||||
@ -436,28 +360,11 @@ class ResultatsSemestre(ResultatsCache):
|
||||
ue_cap_dict["compense_formsemestre_id"] = None
|
||||
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.
|
||||
Result: dict, ou None si l'UE n'existe pas ou 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
|
||||
}
|
||||
Result: dict, ou None si l'UE n'est pas dans ce semestre.
|
||||
"""
|
||||
ue: UniteEns = db.session.get(UniteEns, ue_id)
|
||||
if not ue:
|
||||
return None
|
||||
ue_dict = ue.to_dict()
|
||||
|
||||
if ue.type == UE_SPORT:
|
||||
@ -476,7 +383,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
"ects": 0.0,
|
||||
"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
|
||||
if not self.validations:
|
||||
self.validations = res_sem.load_formsemestre_validations(self.formsemestre)
|
||||
@ -518,8 +425,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
Corrigez ou faite corriger le programme
|
||||
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
|
||||
formation_id=ue_capitalized.formation_id)}">via cette page</a>.
|
||||
""",
|
||||
safe=True,
|
||||
"""
|
||||
)
|
||||
else:
|
||||
# Coefs de l'UE capitalisée en formation classique:
|
||||
@ -534,13 +440,11 @@ class ResultatsSemestre(ResultatsCache):
|
||||
"is_external": ue_cap["is_external"] if is_capitalized else ue.is_external,
|
||||
"coef_ue": coef_ue,
|
||||
"ects_pot": ue.ects or 0.0,
|
||||
"ects": (
|
||||
self.validations.decisions_jury_ues.get(etudid, {})
|
||||
.get(ue.id, {})
|
||||
.get("ects", 0.0)
|
||||
if self.validations.decisions_jury_ues
|
||||
else 0.0
|
||||
),
|
||||
"ects": self.validations.decisions_jury_ues.get(etudid, {})
|
||||
.get(ue.id, {})
|
||||
.get("ects", 0.0)
|
||||
if self.validations.decisions_jury_ues
|
||||
else 0.0,
|
||||
"ects_ue": ue.ects,
|
||||
"cur_moy_ue": cur_moy_ue,
|
||||
"moy": moy_ue,
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -58,6 +58,7 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
self.moy_moy = "NA"
|
||||
self.moy_gen_rangs_by_group = {} # { 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._modimpls_dict_by_ue = {} # local cache
|
||||
|
||||
@ -216,9 +217,9 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
# Rangs / UEs:
|
||||
for ue in ues:
|
||||
group_moys_ue = self.etud_moy_ue[ue.id][group_members]
|
||||
self.ue_rangs_by_group.setdefault(ue.id, {})[group.id] = (
|
||||
moy_sem.comp_ranks_series(group_moys_ue * mask_inscr)
|
||||
)
|
||||
self.ue_rangs_by_group.setdefault(ue.id, {})[
|
||||
group.id
|
||||
] = moy_sem.comp_ranks_series(group_moys_ue * mask_inscr)
|
||||
|
||||
def get_etud_rang(self, etudid: int) -> str:
|
||||
"""Le rang (classement) de l'étudiant dans le semestre.
|
||||
@ -289,10 +290,9 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
]
|
||||
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.
|
||||
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.
|
||||
"""
|
||||
return bool(
|
||||
@ -322,7 +322,7 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
validations = self.get_formsemestre_validations()
|
||||
return validations.decisions_jury_ues.get(etudid, None)
|
||||
|
||||
def get_etud_ects_valides(self, etudid: int, decisions_ues: dict = False) -> float:
|
||||
def get_etud_ects_valides(self, etudid: int, decisions_ues: dict = False) -> 0:
|
||||
"""Le total des ECTS validés (et enregistrés) par l'étudiant dans ce semestre.
|
||||
NB: avant jury, rien d'enregistré, donc zéro ECTS.
|
||||
Optimisation: si decisions_ues est passé, l'utilise, sinon appelle get_etud_decisions_ue()
|
||||
@ -331,7 +331,7 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
decisions_ues = self.get_etud_decisions_ue(etudid)
|
||||
if not decisions_ues:
|
||||
return 0.0
|
||||
return float(sum(d.get("ects", 0) for d in decisions_ues.values()))
|
||||
return sum([d.get("ects", 0.0) for d in decisions_ues.values()])
|
||||
|
||||
def get_etud_decision_sem(self, etudid: int) -> dict:
|
||||
"""Decision du jury semestre prise pour cet etudiant, ou None s'il n'y en pas eu.
|
||||
@ -407,7 +407,7 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
de ce module.
|
||||
É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)
|
||||
if not modimpl_results:
|
||||
return [] # safeguard
|
||||
@ -422,37 +422,30 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
)
|
||||
return evaluations
|
||||
|
||||
def get_evaluations_etats(self) -> dict[int, dict]:
|
||||
""" "état" de chaque évaluation du semestre
|
||||
{
|
||||
evaluation_id : {
|
||||
"evalcomplete" : bool,
|
||||
"last_modif" : datetime | None
|
||||
"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
|
||||
def get_evaluations_etats(self) -> list[dict]:
|
||||
"""Liste de toutes les évaluations du semestre
|
||||
[ {...evaluation et son etat...} ]"""
|
||||
# TODO: à moderniser (voir dans ResultatsSemestre)
|
||||
# utilisé par
|
||||
# do_evaluation_etat_in_sem
|
||||
|
||||
# ancienne version < 2024-02-02
|
||||
# def get_mod_evaluation_etat_list(self, moduleimpl_id) -> list[dict]:
|
||||
# """Liste des états des évaluations de ce module
|
||||
# ordonnée selon (numero desc, date_debut desc)
|
||||
# """
|
||||
# # à moderniser: lent, recharge des données que l'on a déjà...
|
||||
# # remplacemé par ResultatsSemestre.get_mod_evaluation_etat_list
|
||||
# #
|
||||
# return [
|
||||
# e
|
||||
# for e in self.get_evaluations_etats()
|
||||
# if e["moduleimpl_id"] == moduleimpl_id
|
||||
# ]
|
||||
from app.scodoc import sco_evaluations
|
||||
|
||||
if not hasattr(self, "_evaluations_etats"):
|
||||
self._evaluations_etats = sco_evaluations.do_evaluation_list_in_sem(
|
||||
self.formsemestre.id
|
||||
)
|
||||
|
||||
return self._evaluations_etats
|
||||
|
||||
def get_mod_evaluation_etat_list(self, moduleimpl_id) -> list[dict]:
|
||||
"""Liste des états des évaluations de ce module"""
|
||||
# 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):
|
||||
"""Liste des modimpls du semestre ayant des notes en attente"""
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -84,9 +84,6 @@ def scodoc(func):
|
||||
|
||||
|
||||
def permission_required(permission):
|
||||
"""Vérifie les permissions"""
|
||||
|
||||
# Attention: l'API utilise api_permission_required
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
|
24
app/email.py
24
app/email.py
@ -1,17 +1,16 @@
|
||||
# -*- coding: UTF-8 -*
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
import datetime
|
||||
from threading import Thread
|
||||
|
||||
from flask import current_app, g
|
||||
from flask_mail import BadHeaderError, Message
|
||||
from flask_mail import Message
|
||||
|
||||
from app import log, mail
|
||||
from app import mail
|
||||
from app.models.departements import Departement
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
from app.scodoc import sco_preferences
|
||||
@ -20,15 +19,7 @@ from app.scodoc import sco_preferences
|
||||
def send_async_email(app, msg):
|
||||
"Send an email, async"
|
||||
with app.app_context():
|
||||
try:
|
||||
mail.send(msg)
|
||||
except BadHeaderError:
|
||||
log(
|
||||
f"""send_async_email: BadHeaderError
|
||||
msg={msg}
|
||||
"""
|
||||
)
|
||||
raise
|
||||
mail.send(msg)
|
||||
|
||||
|
||||
def send_email(
|
||||
@ -92,12 +83,9 @@ Adresses d'origine:
|
||||
\n\n"""
|
||||
+ 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(
|
||||
f"""[{formatted_time}] email sent to{
|
||||
f"""email sent to{
|
||||
' (mode test)' if email_test_mode_address else ''
|
||||
}: {msg.recipients}
|
||||
from sender {msg.sender}
|
||||
|
@ -6,7 +6,6 @@ from flask import Blueprint
|
||||
from app.scodoc import sco_etud
|
||||
from app.auth.models import User
|
||||
from app.models import Departement
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
bp = Blueprint("entreprises", __name__)
|
||||
|
||||
@ -16,12 +15,12 @@ SIRET_PROVISOIRE_START = "xx"
|
||||
|
||||
@bp.app_template_filter()
|
||||
def format_prenom(s):
|
||||
return scu.format_prenom(s)
|
||||
return sco_etud.format_prenom(s)
|
||||
|
||||
|
||||
@bp.app_template_filter()
|
||||
def format_nom(s):
|
||||
return scu.format_nom(s)
|
||||
return sco_etud.format_nom(s)
|
||||
|
||||
|
||||
@bp.app_template_filter()
|
||||
@ -59,4 +58,3 @@ def check_taxe_now(taxes):
|
||||
|
||||
|
||||
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
|
||||
#
|
||||
# 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
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# 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
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -151,7 +151,7 @@ class EntrepriseHistorique(db.Model):
|
||||
__tablename__ = "are_historique"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
authenticated_user = db.Column(db.Text) # user_name login sans contrainte
|
||||
authenticated_user = db.Column(db.Text)
|
||||
entreprise_id = db.Column(db.Integer)
|
||||
object = db.Column(db.Text)
|
||||
object_id = db.Column(db.Integer)
|
||||
|
@ -338,11 +338,9 @@ def add_entreprise():
|
||||
if form.validate_on_submit():
|
||||
entreprise = Entreprise(
|
||||
nom=form.nom_entreprise.data.strip(),
|
||||
siret=(
|
||||
form.siret.data.strip()
|
||||
if form.siret.data.strip()
|
||||
else f"{SIRET_PROVISOIRE_START}{datetime.now().strftime('%d%m%y%H%M%S')}"
|
||||
), # siret provisoire
|
||||
siret=form.siret.data.strip()
|
||||
if form.siret.data.strip()
|
||||
else f"{SIRET_PROVISOIRE_START}{datetime.now().strftime('%d%m%y%H%M%S')}", # siret provisoire
|
||||
siret_provisoire=False if form.siret.data.strip() else True,
|
||||
association=form.association.data,
|
||||
adresse=form.adresse.data.strip(),
|
||||
@ -354,7 +352,7 @@ def add_entreprise():
|
||||
db.session.add(entreprise)
|
||||
db.session.commit()
|
||||
db.session.refresh(entreprise)
|
||||
except Exception:
|
||||
except:
|
||||
db.session.rollback()
|
||||
flash("Une erreur est survenue veuillez réessayer.")
|
||||
return render_template(
|
||||
@ -806,9 +804,9 @@ def add_offre(entreprise_id):
|
||||
missions=form.missions.data.strip(),
|
||||
duree=form.duree.data.strip(),
|
||||
expiration_date=form.expiration_date.data,
|
||||
correspondant_id=(
|
||||
form.correspondant.data if form.correspondant.data != "" else None
|
||||
),
|
||||
correspondant_id=form.correspondant.data
|
||||
if form.correspondant.data != ""
|
||||
else None,
|
||||
)
|
||||
db.session.add(offre)
|
||||
db.session.commit()
|
||||
@ -1330,11 +1328,9 @@ def add_contact(entreprise_id):
|
||||
).first_or_404(description=f"entreprise {entreprise_id} inconnue")
|
||||
form = ContactCreationForm(
|
||||
date=f"{datetime.now().strftime('%Y-%m-%dT%H:%M')}",
|
||||
utilisateur=(
|
||||
f"{current_user.nom} {current_user.prenom} ({current_user.user_name})"
|
||||
if current_user.nom and current_user.prenom
|
||||
else ""
|
||||
),
|
||||
utilisateur=f"{current_user.nom} {current_user.prenom} ({current_user.user_name})"
|
||||
if current_user.nom and current_user.prenom
|
||||
else "",
|
||||
)
|
||||
if request.method == "POST" and form.cancel.data:
|
||||
return redirect(url_for("entreprises.contacts", entreprise_id=entreprise_id))
|
||||
@ -1500,9 +1496,9 @@ def add_stage_apprentissage(entreprise_id):
|
||||
date_debut=form.date_debut.data,
|
||||
date_fin=form.date_fin.data,
|
||||
formation_text=formation.formsemestre.titre if formation else None,
|
||||
formation_scodoc=(
|
||||
formation.formsemestre.formsemestre_id if formation else None
|
||||
),
|
||||
formation_scodoc=formation.formsemestre.formsemestre_id
|
||||
if formation
|
||||
else None,
|
||||
notes=form.notes.data.strip(),
|
||||
)
|
||||
db.session.add(stage_apprentissage)
|
||||
@ -1584,8 +1580,8 @@ def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
|
||||
)
|
||||
)
|
||||
elif request.method == "GET":
|
||||
form.etudiant.data = f"""{scu.format_nom(etudiant.nom)} {
|
||||
scu.format_prenom(etudiant.prenom)}"""
|
||||
form.etudiant.data = f"""{sco_etud.format_nom(etudiant.nom)} {
|
||||
sco_etud.format_prenom(etudiant.prenom)}"""
|
||||
form.etudid.data = etudiant.id
|
||||
form.type_offre.data = stage_apprentissage.type_offre
|
||||
form.date_debut.data = stage_apprentissage.date_debut
|
||||
@ -1703,7 +1699,7 @@ def json_etudiants():
|
||||
list = []
|
||||
for etudiant in etudiants:
|
||||
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:
|
||||
content = {
|
||||
"id": f"{etudiant.id}",
|
||||
@ -1806,7 +1802,7 @@ def import_donnees():
|
||||
db.session.add(entreprise)
|
||||
db.session.commit()
|
||||
db.session.refresh(entreprise)
|
||||
except Exception:
|
||||
except:
|
||||
db.session.rollback()
|
||||
flash("Une erreur est survenue veuillez réessayer.")
|
||||
return render_template(
|
||||
|
@ -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
|
||||
|
||||
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:
|
||||
ects = ue.get_ects(parcour, only_parcours=True)
|
||||
setattr(
|
||||
|
@ -4,7 +4,7 @@
|
||||
#
|
||||
# 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
|
||||
# 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],
|
||||
) -> FormSemestreChangeFormationForm:
|
||||
"Create our dynamical form"
|
||||
|
||||
# see https://wtforms.readthedocs.io/en/2.3.x/specific_problems/#dynamic-form-composition
|
||||
class F(FormSemestreChangeFormationForm):
|
||||
pass
|
||||
|
@ -4,7 +4,7 @@ Formulaire configuration des codes Apo et EDT des modimps d'un formsemestre
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
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
|
||||
|
||||
|
@ -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
|
||||
#
|
||||
# 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
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# 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
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@ -29,16 +29,56 @@
|
||||
Formulaire configuration Module Assiduités
|
||||
"""
|
||||
import datetime
|
||||
import re
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import DecimalField, SubmitField, ValidationError
|
||||
from wtforms.fields.simple import StringField
|
||||
from wtforms.validators import Optional, Length
|
||||
from wtforms.validators import Optional
|
||||
|
||||
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):
|
||||
"""Le tick_time doit être entre 0 et 60 minutes"""
|
||||
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}")
|
||||
|
||||
|
||||
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):
|
||||
"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)",
|
||||
places=0,
|
||||
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.""",
|
||||
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")
|
||||
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# 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
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@ -82,7 +82,7 @@ class ConfigCASForm(FlaskForm):
|
||||
|
||||
cas_attribute_id = StringField(
|
||||
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.""",
|
||||
)
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# 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
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# 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
|
||||
# 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()
|
||||
],
|
||||
)
|
||||
submit_bonus = SubmitField("Enregistrer ce bonus")
|
||||
submit_bonus = SubmitField("Valider")
|
||||
cancel_bonus = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
||||
|
||||
|
||||
class ScoDocConfigurationForm(FlaskForm):
|
||||
"Panneau de configuration avancée"
|
||||
disable_passerelle = BooleanField( # disable car par défaut activée
|
||||
"""cacher les fonctions liées à une passerelle de publication des résultats vers les étudiants ("œil"). N'affecte pas l'API, juste la présentation."""
|
||||
)
|
||||
enable_entreprises = BooleanField("activer le module <em>entreprises</em>")
|
||||
month_debut_annee_scolaire = SelectField(
|
||||
label="Mois de début des années scolaires",
|
||||
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.""",
|
||||
validators=[Optional(), Email()],
|
||||
)
|
||||
user_require_email_institutionnel = BooleanField(
|
||||
"imposer la saisie du mail institutionnel dans le formulaire de création utilisateur"
|
||||
)
|
||||
disable_bul_pdf = BooleanField(
|
||||
"interdire les exports des bulletins en PDF (déconseillé)"
|
||||
)
|
||||
submit_scodoc = SubmitField("Enregistrer ces paramètres")
|
||||
disable_bul_pdf = BooleanField("empêcher les exports des bulletins en PDF")
|
||||
submit_scodoc = SubmitField("Valider")
|
||||
cancel_scodoc = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
||||
|
||||
|
||||
@ -100,12 +93,10 @@ def configuration():
|
||||
form_scodoc = ScoDocConfigurationForm(
|
||||
data={
|
||||
"enable_entreprises": ScoDocSiteConfig.is_entreprises_enabled(),
|
||||
"disable_passerelle": ScoDocSiteConfig.is_passerelle_disabled(),
|
||||
"month_debut_annee_scolaire": ScoDocSiteConfig.get_month_debut_annee_scolaire(),
|
||||
"month_debut_periode2": ScoDocSiteConfig.get_month_debut_periode2(),
|
||||
"email_from_addr": ScoDocSiteConfig.get("email_from_addr"),
|
||||
"disable_bul_pdf": ScoDocSiteConfig.is_bul_pdf_disabled(),
|
||||
"user_require_email_institutionnel": ScoDocSiteConfig.is_user_require_email_institutionnel_enabled(),
|
||||
}
|
||||
)
|
||||
if request.method == "POST" and (
|
||||
@ -126,12 +117,12 @@ def configuration():
|
||||
flash("Fonction bonus inchangée.")
|
||||
return redirect(url_for("scodoc.index"))
|
||||
elif form_scodoc.submit_scodoc.data and form_scodoc.validate():
|
||||
if ScoDocSiteConfig.disable_passerelle(
|
||||
disabled=form_scodoc.data["disable_passerelle"]
|
||||
if ScoDocSiteConfig.enable_entreprises(
|
||||
enabled=form_scodoc.data["enable_entreprises"]
|
||||
):
|
||||
flash(
|
||||
"Fonction passerelle "
|
||||
+ ("cachées" if form_scodoc.data["disable_passerelle"] else "montrées")
|
||||
"Module entreprise "
|
||||
+ ("activé" if form_scodoc.data["enable_entreprises"] else "désactivé")
|
||||
)
|
||||
if ScoDocSiteConfig.set_month_debut_annee_scolaire(
|
||||
int(form_scodoc.data["month_debut_annee_scolaire"])
|
||||
@ -158,23 +149,10 @@ def configuration():
|
||||
"Exports PDF "
|
||||
+ ("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 render_template(
|
||||
"configuration.j2",
|
||||
is_entreprises_enabled=ScoDocSiteConfig.is_entreprises_enabled(),
|
||||
form_bonus=form_bonus,
|
||||
form_scodoc=form_scodoc,
|
||||
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
|
||||
#
|
||||
# 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
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# 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
|
||||
# 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
|
||||
"""
|
||||
|
||||
from flask import abort, g
|
||||
import sqlalchemy
|
||||
import app
|
||||
from app import db
|
||||
|
||||
CODE_STR_LEN = 16 # chaine pour les codes
|
||||
@ -25,17 +23,8 @@ convention = {
|
||||
metadata_obj = sqlalchemy.MetaData(naming_convention=convention)
|
||||
|
||||
|
||||
class ScoDocModel(db.Model):
|
||||
"""Superclass for our models. Add some 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
|
||||
class ScoDocModel:
|
||||
"Mixin class for our models. Add somme useful methods for editing, cloning, etc."
|
||||
|
||||
def clone(self, not_copying=()):
|
||||
"""Clone, not copying the given attrs
|
||||
@ -51,28 +40,21 @@ class ScoDocModel(db.Model):
|
||||
return copy
|
||||
|
||||
@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.
|
||||
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))
|
||||
if args:
|
||||
obj = cls(**args)
|
||||
else:
|
||||
obj = cls()
|
||||
else:
|
||||
obj = cls()
|
||||
args = cls.convert_dict_fields(cls.filter_model_attributes(data))
|
||||
obj = cls(**args)
|
||||
db.session.add(obj)
|
||||
return obj
|
||||
|
||||
@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 'id' to excluded."""
|
||||
excluded = excluded or set()
|
||||
excluded.add("id") # always exclude id
|
||||
By default, excluded == { 'id' }"""
|
||||
excluded = {"id"} if excluded is None else set()
|
||||
# Les attributs du modèle qui sont des variables: (élimine les __ et les alias comme adm_id)
|
||||
my_attributes = [
|
||||
a
|
||||
@ -88,7 +70,7 @@ class ScoDocModel(db.Model):
|
||||
|
||||
@classmethod
|
||||
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.
|
||||
args: dict with args in application.
|
||||
returns: dict to store in model's db.
|
||||
@ -96,63 +78,13 @@ class ScoDocModel(db.Model):
|
||||
# virtual, by default, do nothing
|
||||
return args
|
||||
|
||||
def from_dict(self, args: dict, excluded: set[str] | None = None) -> bool:
|
||||
"""Update object's fields given in dict. Add to session but don't commit.
|
||||
True if modification.
|
||||
"""
|
||||
args_dict = self.convert_dict_fields(
|
||||
self.filter_model_attributes(args, excluded=excluded)
|
||||
)
|
||||
modified = False
|
||||
def from_dict(self, args: dict):
|
||||
"Update object's fields given in dict. Add to session but don't commit."
|
||||
args_dict = self.convert_dict_fields(self.filter_model_attributes(args))
|
||||
for key, value in args_dict.items():
|
||||
if hasattr(self, key) and value != getattr(self, key):
|
||||
if hasattr(self, key):
|
||||
setattr(self, key, value)
|
||||
modified = True
|
||||
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
|
||||
@ -198,6 +130,7 @@ from app.models.notes import (
|
||||
NotesNotesLog,
|
||||
)
|
||||
from app.models.validations import (
|
||||
ScolarEvent,
|
||||
ScolarFormSemestreValidation,
|
||||
ScolarAutorisationInscription,
|
||||
)
|
||||
@ -211,13 +144,8 @@ from app.models.but_refcomp import (
|
||||
ApcReferentielCompetences,
|
||||
ApcSituationPro,
|
||||
)
|
||||
from app.models.but_validations import (
|
||||
ApcValidationAnnee,
|
||||
ApcValidationRCUE,
|
||||
ValidationDUT120,
|
||||
)
|
||||
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
||||
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
|
||||
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):
|
||||
"""LEGACY
|
||||
Ce modèle n'est PLUS UTILISE depuis ScoDoc 9.6 et remplacé par assiduité.
|
||||
une absence (sur une demi-journée)
|
||||
"""
|
||||
"""une absence (sur une demi-journée)"""
|
||||
|
||||
__tablename__ = "absences"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
@ -1,38 +1,25 @@
|
||||
# -*- coding: UTF-8 -*
|
||||
"""Gestion de l'assiduité (assiduités + justificatifs)"""
|
||||
|
||||
"""Gestion de l'assiduité (assiduités + justificatifs)
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from flask_login import current_user
|
||||
from flask_sqlalchemy.query import Query
|
||||
|
||||
from app import db, log, g, set_sco_dept
|
||||
from app.models import (
|
||||
ModuleImpl,
|
||||
Module,
|
||||
Scolog,
|
||||
FormSemestre,
|
||||
FormSemestreInscription,
|
||||
ScoDocModel,
|
||||
)
|
||||
from app import db, log
|
||||
from app.models import ModuleImpl, Scolog, FormSemestre, FormSemestreInscription
|
||||
from app.models.etudiants import Identite
|
||||
from app.auth.models import User
|
||||
from app.scodoc import sco_abs_notification
|
||||
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_utils import (
|
||||
EtatAssiduite,
|
||||
EtatJustificatif,
|
||||
localize_datetime,
|
||||
is_assiduites_module_forced,
|
||||
NonWorkDays,
|
||||
)
|
||||
|
||||
from flask_sqlalchemy.query import Query
|
||||
|
||||
class Assiduite(ScoDocModel):
|
||||
|
||||
class Assiduite(db.Model):
|
||||
"""
|
||||
Représente une assiduité:
|
||||
- une plage horaire lié à un état et un étudiant
|
||||
@ -90,14 +77,10 @@ class Assiduite(ScoDocModel):
|
||||
lazy="select",
|
||||
)
|
||||
|
||||
# Argument "restrict" obligatoire car on override la fonction "to_dict" de ScoDocModel
|
||||
# pylint: disable-next=unused-argument
|
||||
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.
|
||||
"""
|
||||
def to_dict(self, format_api=True) -> dict:
|
||||
"""Retourne la représentation json de l'assiduité"""
|
||||
etat = self.etat
|
||||
user: User | None = None
|
||||
user: User = None
|
||||
if format_api:
|
||||
# format api utilise les noms "present,absent,retard" au lieu des int
|
||||
etat = EtatAssiduite.inverse().get(self.etat).name
|
||||
@ -115,9 +98,9 @@ class Assiduite(ScoDocModel):
|
||||
"entry_date": self.entry_date,
|
||||
"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_nom_complet": (
|
||||
None if user is None else user.get_nomcomplet()
|
||||
), # "Marie Dupont"
|
||||
"user_nom_complet": None
|
||||
if user is None
|
||||
else user.get_nomcomplet(), # "Marie Dupont"
|
||||
"est_just": self.est_just,
|
||||
"external_data": self.external_data,
|
||||
}
|
||||
@ -152,56 +135,16 @@ class Assiduite(ScoDocModel):
|
||||
external_data: dict = None,
|
||||
notify_mail=False,
|
||||
) -> "Assiduite":
|
||||
"""Créer une nouvelle assiduité pour l'étudiant.
|
||||
Les datetime doivent être en timezone serveur.
|
||||
Raises ScoValueError en cas de conflit ou erreur.
|
||||
"""
|
||||
"""Créer une nouvelle assiduité pour l'étudiant"""
|
||||
if date_debut.tzinfo is None:
|
||||
log(
|
||||
f"Warning: create_assiduite: date_debut without timezone ({date_debut})"
|
||||
)
|
||||
if date_fin.tzinfo is None:
|
||||
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
|
||||
assiduites: Query = etud.assiduites
|
||||
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(
|
||||
"Duplication: la période rentre en conflit avec une plage enregistrée"
|
||||
)
|
||||
@ -251,8 +194,7 @@ class Assiduite(ScoDocModel):
|
||||
user_id=user_id,
|
||||
)
|
||||
db.session.add(nouv_assiduite)
|
||||
db.session.flush()
|
||||
log(f"create_assiduite: {etud.id} id={nouv_assiduite.id} {nouv_assiduite}")
|
||||
log(f"create_assiduite: {etud.id} {nouv_assiduite}")
|
||||
Scolog.logdb(
|
||||
method="create_assiduite",
|
||||
etudid=etud.id,
|
||||
@ -262,166 +204,8 @@ class Assiduite(ScoDocModel):
|
||||
sco_abs_notification.abs_notify(etud.id, nouv_assiduite.date_debut)
|
||||
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:
|
||||
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):
|
||||
class Justificatif(db.Model):
|
||||
"""
|
||||
Représente un justificatif:
|
||||
- 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())
|
||||
"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(
|
||||
db.Integer,
|
||||
@ -473,35 +255,23 @@ class Justificatif(ScoDocModel):
|
||||
etudiant = db.relationship(
|
||||
"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)
|
||||
|
||||
@classmethod
|
||||
def get_justificatif(cls, justif_id: int) -> "Justificatif":
|
||||
"""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
|
||||
"""
|
||||
def to_dict(self, format_api: bool = False) -> dict:
|
||||
"""transformation de l'objet en dictionnaire sérialisable"""
|
||||
|
||||
etat = self.etat
|
||||
user: User = self.user if self.user_id is not None else None
|
||||
username = self.user_id
|
||||
|
||||
if format_api:
|
||||
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 = {
|
||||
"justif_id": self.justif_id,
|
||||
@ -510,49 +280,30 @@ class Justificatif(ScoDocModel):
|
||||
"date_debut": self.date_debut,
|
||||
"date_fin": self.date_fin,
|
||||
"etat": etat,
|
||||
"raison": None if restrict else self.raison,
|
||||
"fichier": None if restrict else self.fichier,
|
||||
"raison": self.raison,
|
||||
"fichier": self.fichier,
|
||||
"entry_date": self.entry_date,
|
||||
"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_nom_complet": None if user is None else user.get_nomcomplet(),
|
||||
"external_data": None if restrict else self.external_data,
|
||||
"user_id": username,
|
||||
"external_data": self.external_data,
|
||||
}
|
||||
return data
|
||||
|
||||
def __repr__(self) -> str:
|
||||
def __str__(self) -> str:
|
||||
"chaine pour journaux et debug (lisible par humain français)"
|
||||
try:
|
||||
etat_str = EtatJustificatif(self.etat).name
|
||||
except ValueError:
|
||||
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_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
|
||||
def create_justificatif(
|
||||
cls,
|
||||
etudiant: Identite,
|
||||
# On a besoin des arguments mais on utilise "locals" pour les récupérer
|
||||
# pylint: disable=unused-argument
|
||||
etud: Identite,
|
||||
date_debut: datetime,
|
||||
date_fin: datetime,
|
||||
etat: EtatJustificatif,
|
||||
@ -561,163 +312,38 @@ class Justificatif(ScoDocModel):
|
||||
user_id: int = None,
|
||||
external_data: dict = None,
|
||||
) -> "Justificatif":
|
||||
"""Créer un nouveau justificatif pour l'étudiant.
|
||||
Raises ScoValueError si paramètres incorrects.
|
||||
"""
|
||||
nouv_justificatif = cls.create_from_dict(locals())
|
||||
log(f"create_justificatif: etudid={etudiant.id} {nouv_justificatif}")
|
||||
"""Créer un nouveau justificatif pour l'étudiant"""
|
||||
nouv_justificatif = Justificatif(
|
||||
date_debut=date_debut,
|
||||
date_fin=date_fin,
|
||||
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(
|
||||
method="create_justificatif",
|
||||
etudid=etudiant.id,
|
||||
etudid=etud.id,
|
||||
msg=f"justificatif: {nouv_justificatif}",
|
||||
)
|
||||
db.session.commit()
|
||||
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(
|
||||
date_debut: datetime,
|
||||
date_fin: datetime,
|
||||
collection: Query,
|
||||
collection_cls: Assiduite | Justificatif,
|
||||
obj_id: int = -1,
|
||||
) -> bool:
|
||||
"""
|
||||
Vérifie si une date n'entre pas en collision
|
||||
avec les justificatifs ou assiduites déjà présentes
|
||||
|
||||
On peut donner un objet_id pour exclure un objet de la vérification
|
||||
(utile pour les modifications)
|
||||
"""
|
||||
|
||||
# On s'assure que les dates soient avec TimeZone
|
||||
@ -725,14 +351,74 @@ def is_period_conflicting(
|
||||
date_fin = localize_datetime(date_fin)
|
||||
|
||||
count: int = collection.filter(
|
||||
collection_cls.date_debut < date_fin,
|
||||
collection_cls.date_fin > date_debut,
|
||||
collection_cls.id != obj_id,
|
||||
collection_cls.date_debut < date_fin, collection_cls.date_fin > date_debut
|
||||
).count()
|
||||
|
||||
return count > 0
|
||||
|
||||
|
||||
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]:
|
||||
"""
|
||||
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
|
||||
|
||||
Returns:
|
||||
list[int | dict]: La liste des justificatifs (par défaut uniquement
|
||||
les identifiants, sinon les dict si long est vrai)
|
||||
list[int | dict]: La liste des justificatifs (par défaut uniquement les identifiants, sinon les Dict si long est vrai)
|
||||
"""
|
||||
assi: Assiduite = Assiduite.query.get_or_404(assiduite_id)
|
||||
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.
|
||||
|
||||
Returns:
|
||||
list[int | dict]: La liste des justificatifs (par défaut uniquement
|
||||
les identifiants, sinon les dict si long est vrai)
|
||||
list[int | dict]: La liste des justificatifs (par défaut uniquement les identifiants, sinon les Dict si long est vrai)
|
||||
"""
|
||||
# On récupère les justificatifs d'un étudiant couvrant la période donnée
|
||||
justifs: Query = Justificatif.query.filter(
|
||||
@ -788,20 +472,16 @@ def get_justifs_from_date(
|
||||
if valid:
|
||||
justifs = justifs.filter(Justificatif.etat == EtatJustificatif.VALIDE)
|
||||
|
||||
# On renvoie la liste des id des justificatifs si long est Faux,
|
||||
# sinon on renvoie les dicts des justificatifs
|
||||
if long:
|
||||
return [j.to_dict(True) for j in justifs]
|
||||
return [j.justif_id for j in justifs]
|
||||
# On renvoie la liste des id des justificatifs si long est Faux, sinon on renvoie les dicts des justificatifs
|
||||
return [j.justif_id if not long else j.to_dict(True) for j in justifs]
|
||||
|
||||
|
||||
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
|
||||
Si l'étudiant est inscrit à plusieurs formsemestre, prend le premier.
|
||||
|
||||
Args:
|
||||
data (dict[str, datetime | int]): Une représentation simplifiée d'une
|
||||
assiduité ou d'un justificatif
|
||||
data (dict[str, datetime | int]): Une réprésentation simplifiée d'une assiduité ou d'un justificatif
|
||||
|
||||
data = {
|
||||
"etudid" : int,
|
||||
@ -824,29 +504,3 @@ def get_formsemestre_from_data(data: dict[str, datetime | int]) -> FormSemestre:
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
|
||||
def has_assiduites_disable_pref(formsemestre: FormSemestre) -> str | bool:
|
||||
"""
|
||||
Vérifie si le semestre possède la préférence "assiduites_disable"
|
||||
et renvoie le message d'erreur associé.
|
||||
|
||||
La préférence est un text field. Il est considéré comme vide si :
|
||||
- la chaine de caractère est vide
|
||||
- si elle n'est composée que de caractères d'espacement (espace, tabulation, retour à la ligne)
|
||||
|
||||
|
||||
Si la chaine est vide, la fonction renvoie False
|
||||
"""
|
||||
|
||||
# Si pas de formsemestre, on ne peut pas vérifier la préférence
|
||||
# On considère que la préférence n'est pas activée
|
||||
if formsemestre is None:
|
||||
return False
|
||||
|
||||
pref: str = (
|
||||
sco_preferences.get_preference("assiduites_disable", formsemestre.id) or ""
|
||||
)
|
||||
pref = pref.strip()
|
||||
|
||||
return pref if pref else False
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
"""ScoDoc 9 models : Référentiel Compétence BUT 2021
|
||||
@ -8,19 +8,16 @@
|
||||
from datetime import datetime
|
||||
import functools
|
||||
from operator import attrgetter
|
||||
import yaml
|
||||
|
||||
from flask import g
|
||||
from flask_sqlalchemy.query import Query
|
||||
from sqlalchemy.orm import class_mapper
|
||||
import sqlalchemy
|
||||
|
||||
from app import db, log
|
||||
from app import db
|
||||
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
|
||||
|
||||
REFCOMP_EQUIVALENCE_FILENAME = "ressources/referentiels/equivalences.yaml"
|
||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences
|
||||
|
||||
|
||||
# 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):
|
||||
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:
|
||||
"La version, normalement sous forme de date iso yyy-mm-dd"
|
||||
if not self.version_orebut:
|
||||
@ -132,11 +124,9 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
||||
"type_departement": self.type_departement,
|
||||
"type_titre": self.type_titre,
|
||||
"version_orebut": self.version_orebut,
|
||||
"scodoc_date_loaded": (
|
||||
self.scodoc_date_loaded.isoformat() + "Z"
|
||||
if self.scodoc_date_loaded
|
||||
else ""
|
||||
),
|
||||
"scodoc_date_loaded": self.scodoc_date_loaded.isoformat() + "Z"
|
||||
if self.scodoc_date_loaded
|
||||
else "",
|
||||
"scodoc_orig_filename": self.scodoc_orig_filename,
|
||||
"competences": {
|
||||
x.titre: x.to_dict(with_app_critiques=with_app_critiques)
|
||||
@ -244,100 +234,6 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
||||
|
||||
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):
|
||||
"Compétence"
|
||||
@ -478,11 +374,9 @@ class ApcNiveau(db.Model, XMLModel):
|
||||
"libelle": self.libelle,
|
||||
"annee": self.annee,
|
||||
"ordre": self.ordre,
|
||||
"app_critiques": (
|
||||
{x.code: x.to_dict() for x in self.app_critiques}
|
||||
if with_app_critiques
|
||||
else {}
|
||||
),
|
||||
"app_critiques": {x.code: x.to_dict() for x in self.app_critiques}
|
||||
if with_app_critiques
|
||||
else {},
|
||||
}
|
||||
|
||||
def to_dict_bul(self):
|
||||
@ -570,9 +464,9 @@ class ApcNiveau(db.Model, XMLModel):
|
||||
return []
|
||||
|
||||
if competence is None:
|
||||
parcour_niveaux: list[ApcParcoursNiveauCompetence] = (
|
||||
annee_parcour.niveaux_competences
|
||||
)
|
||||
parcour_niveaux: list[
|
||||
ApcParcoursNiveauCompetence
|
||||
] = annee_parcour.niveaux_competences
|
||||
niveaux: list[ApcNiveau] = [
|
||||
pn.competence.niveaux.filter_by(ordre=pn.niveau).first()
|
||||
for pn in parcour_niveaux
|
||||
@ -604,7 +498,6 @@ app_critiques_modules = db.Table(
|
||||
db.ForeignKey("apc_app_critique.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
),
|
||||
db.UniqueConstraint("module_id", "app_crit_id", name="uix_module_id_app_crit_id"),
|
||||
)
|
||||
|
||||
|
||||
|
@ -2,21 +2,17 @@
|
||||
|
||||
"""Décisions de jury (validations) des RCUE et années du BUT
|
||||
"""
|
||||
from collections import defaultdict
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from app import db
|
||||
from app.models import CODE_STR_LEN, ScoDocModel
|
||||
from app.models import CODE_STR_LEN
|
||||
from app.models.but_refcomp import ApcNiveau
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.formations import Formation
|
||||
from app.models.formsemestre import FormSemestre
|
||||
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
|
||||
|
||||
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)
|
||||
parcour = db.relationship("ApcParcours")
|
||||
|
||||
_sco_dept_relations = ("Identite",) # pour accéder au département
|
||||
|
||||
def __repr__(self):
|
||||
return f"""<{self.__class__.__name__} {self.id} {self.etud} {
|
||||
self.ue1}/{self.ue2}:{self.code!r}>"""
|
||||
|
||||
def __str__(self):
|
||||
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:
|
||||
"description en HTML"
|
||||
return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}:
|
||||
<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:
|
||||
"""l'année BUT concernée: "BUT1", "BUT2" ou "BUT3" """
|
||||
niveau = self.niveau()
|
||||
return niveau.annee if niveau else None
|
||||
|
||||
def niveau(self) -> ApcNiveau | None:
|
||||
def niveau(self) -> ApcNiveau:
|
||||
"""Le niveau de compétence associé à cet RCUE."""
|
||||
# Par convention, il est donné par la seconde UE
|
||||
# à défaut (si l'UE a été désacciée entre temps), la première
|
||||
# et à défaut, renvoie None
|
||||
return self.ue2.niveau_competence or self.ue1.niveau_competence
|
||||
return self.ue2.niveau_competence
|
||||
|
||||
def to_dict(self):
|
||||
"as a dict"
|
||||
@ -118,14 +111,8 @@ class ApcValidationRCUE(ScoDocModel):
|
||||
"formsemestre_id": self.formsemestre_id,
|
||||
}
|
||||
|
||||
def get_codes_apogee(self) -> set[str]:
|
||||
"""Les codes Apogée associés à cette validation RCUE.
|
||||
Prend les codes des deux UEs
|
||||
"""
|
||||
return self.ue1.get_codes_apogee_rcue() | self.ue2.get_codes_apogee_rcue()
|
||||
|
||||
|
||||
class ApcValidationAnnee(ScoDocModel):
|
||||
class ApcValidationAnnee(db.Model):
|
||||
"""Validation des années du BUT"""
|
||||
|
||||
__tablename__ = "apc_validation_annee"
|
||||
@ -156,8 +143,6 @@ class ApcValidationAnnee(ScoDocModel):
|
||||
etud = db.relationship("Identite", backref="apc_validations_annees")
|
||||
formsemestre = db.relationship("FormSemestre", backref="apc_validations_annees")
|
||||
|
||||
_sco_dept_relations = ("Identite",) # pour accéder au département
|
||||
|
||||
def __repr__(self):
|
||||
return f"""<{self.__class__.__name__} {self.id} {self.etud
|
||||
} BUT{self.ordre}/{self.annee_scolaire}:{self.code!r}>"""
|
||||
@ -177,7 +162,7 @@ class ApcValidationAnnee(ScoDocModel):
|
||||
def html(self) -> str:
|
||||
"Affichage html"
|
||||
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
|
||||
else "(sans date)"
|
||||
)
|
||||
@ -215,9 +200,17 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
|
||||
.order_by(UniteEns.numero, UniteEns.acronyme)
|
||||
)
|
||||
decisions["decision_rcue"] = [v.to_dict_bul() for v in validations_rcues]
|
||||
titres_rcues = _build_decisions_rcue_list(decisions["decision_rcue"])
|
||||
titres_rcues = []
|
||||
for dec_rcue in decisions["decision_rcue"]:
|
||||
niveau = dec_rcue["niveau"]
|
||||
if niveau is None:
|
||||
titres_rcues.append(f"""pas de compétence: code {dec_rcue["code"]}""")
|
||||
else:
|
||||
titres_rcues.append(
|
||||
f"""{niveau["competence"]["titre"]} {niveau["ordre"]}: {
|
||||
dec_rcue["code"]}"""
|
||||
)
|
||||
decisions["descr_decisions_rcue"] = ", ".join(titres_rcues)
|
||||
decisions["descr_decisions_rcue_list"] = titres_rcues
|
||||
decisions["descr_decisions_niveaux"] = (
|
||||
"Niveaux de compétences: " + decisions["descr_decisions_rcue"]
|
||||
)
|
||||
@ -226,127 +219,15 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
|
||||
decisions["descr_decisions_rcue"] = ""
|
||||
decisions["descr_decisions_niveaux"] = ""
|
||||
# --- 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
|
||||
validation = ApcValidationAnnee.query.filter_by(
|
||||
etudid=etud.id,
|
||||
annee_scolaire=formsemestre.annee_scolaire(),
|
||||
ordre=annee_but,
|
||||
referentiel_competence_id=formsemestre.formation.referentiel_competence_id,
|
||||
).first()
|
||||
if validation:
|
||||
decisions["decision_annee"] = validation.to_dict_bul()
|
||||
else:
|
||||
decisions["decision_annee"] = None
|
||||
annee_but = (formsemestre.semestre_id + 1) // 2
|
||||
validation = ApcValidationAnnee.query.filter_by(
|
||||
etudid=etud.id,
|
||||
annee_scolaire=formsemestre.annee_scolaire(),
|
||||
ordre=annee_but,
|
||||
referentiel_competence_id=formsemestre.formation.referentiel_competence_id,
|
||||
).first()
|
||||
if validation:
|
||||
decisions["decision_annee"] = validation.to_dict_bul()
|
||||
else:
|
||||
decisions["decision_annee"] = None
|
||||
return decisions
|
||||
|
||||
|
||||
def _build_decisions_rcue_list(decisions_rcue: dict) -> list[str]:
|
||||
"""Formatte liste des décisions niveaux de compétences / RCUE pour
|
||||
lettres individuelles.
|
||||
Le résulat est trié par compétence et donne pour chaque niveau avec validation:
|
||||
[ 'Administrer: niveau 1 ADM, niveau 2 ADJ', ... ]
|
||||
"""
|
||||
# Construit { id_competence : validation }
|
||||
# où validation est {'code': 'CMP', 'niveau': {'annee': 'BUT3', 'competence': {}, ... }
|
||||
validation_by_competence = defaultdict(list)
|
||||
for validation in decisions_rcue:
|
||||
if validation:
|
||||
# Attention, certaines validations de RCUE peuvent ne plus être associées
|
||||
# à un niveau de compétence si l'UE a été déassociée (ce qui ne devrait pas être fait)
|
||||
competence_id = (
|
||||
(validation.get("niveau") or {}).get("competence") or {}
|
||||
).get("id_orebut")
|
||||
validation_by_competence[competence_id].append(validation)
|
||||
# Tri des listes de validation par numéro de compétence
|
||||
validations_niveaux = sorted(
|
||||
validation_by_competence.values(),
|
||||
key=lambda v: (
|
||||
((v[0].get("niveau") or {}).get("competence") or {}).get("numero", 0)
|
||||
if v
|
||||
else -1
|
||||
),
|
||||
)
|
||||
titres_rcues = []
|
||||
empty = {} # pour syntaxe f-string
|
||||
for validations in validations_niveaux:
|
||||
if validations:
|
||||
v = validations[0]
|
||||
titre_competence = ((v.get("niveau") or {}).get("competence", {})).get(
|
||||
"titre", "sans titre ! A vérifier !"
|
||||
)
|
||||
titres_rcues.append(
|
||||
f"""{titre_competence} : """
|
||||
+ ", ".join(
|
||||
[
|
||||
f"niveau {((v.get('niveau') or empty).get('ordre') or '?')} {v.get('code', '?')}"
|
||||
for v in validations
|
||||
]
|
||||
)
|
||||
)
|
||||
return titres_rcues
|
||||
|
||||
|
||||
class ValidationDUT120(ScoDocModel):
|
||||
"""Validations du DUT 120
|
||||
Ce diplôme est attribué sur demande aux étudiants de BUT ayant acquis les 120 ECTS
|
||||
de BUT 1 et BUT 2.
|
||||
"""
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
etudid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||
index=True,
|
||||
nullable=False,
|
||||
)
|
||||
formsemestre_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_formsemestre.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
"""le semestre origine, dans la plupart des cas le S4 (le diplôme DUT120
|
||||
apparaîtra sur les PV de ce formsemestre)"""
|
||||
referentiel_competence_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_referentiel_competences.id"), nullable=False
|
||||
) # pas de cascade, on ne doit pas supprimer un référentiel utilisé
|
||||
"""Identifie la spécialité de DUT décernée"""
|
||||
date = db.Column(
|
||||
db.DateTime(timezone=True), server_default=db.func.now(), nullable=False
|
||||
)
|
||||
"""Date de délivrance"""
|
||||
|
||||
etud = db.relationship("Identite", backref="validations_dut120")
|
||||
formsemestre = db.relationship("FormSemestre", backref="validations_dut120")
|
||||
|
||||
_sco_dept_relations = ("Identite",) # pour accéder au département
|
||||
|
||||
def __repr__(self):
|
||||
return f"""<ValidationDUT120 {self.etud}>"""
|
||||
|
||||
def html(self) -> str:
|
||||
"Affichage html"
|
||||
date_str = (
|
||||
f"""le {self.date.strftime(scu.DATEATIME_FMT)}"""
|
||||
if self.date
|
||||
else "(sans date)"
|
||||
)
|
||||
link = (
|
||||
self.formsemestre.html_link_status(
|
||||
label=f"{self.formsemestre.titre_formation(with_sem_idx=1)}",
|
||||
title=self.formsemestre.titre_annee(),
|
||||
)
|
||||
if self.formsemestre
|
||||
else "externe/antérieure"
|
||||
)
|
||||
specialite = (
|
||||
self.formsemestre.formation.referentiel_competence.get_title()
|
||||
if self.formsemestre.formation.referentiel_competence
|
||||
else "(désassociée!)"
|
||||
)
|
||||
return f"""Diplôme de <b>DUT en 120 ECTS du {specialite}</b> émis par
|
||||
{link}
|
||||
{date_str}
|
||||
"""
|
||||
|
@ -92,11 +92,9 @@ class ScoDocSiteConfig(db.Model):
|
||||
"INSTITUTION_CITY": str,
|
||||
"DEFAULT_PDF_FOOTER_TEMPLATE": str,
|
||||
"enable_entreprises": bool,
|
||||
"disable_passerelle": bool, # remplace pref. bul_display_publication
|
||||
"month_debut_annee_scolaire": int,
|
||||
"month_debut_periode2": int,
|
||||
"disable_bul_pdf": bool,
|
||||
"user_require_email_institutionnel": bool,
|
||||
# CAS
|
||||
"cas_enable": bool,
|
||||
"cas_server": str,
|
||||
@ -233,52 +231,49 @@ class ScoDocSiteConfig(db.Model):
|
||||
cfg = ScoDocSiteConfig.query.filter_by(name="cas_enable").first()
|
||||
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
|
||||
def is_entreprises_enabled(cls) -> bool:
|
||||
"""True si on doit activer le module entreprise"""
|
||||
cfg = ScoDocSiteConfig.query.filter_by(name="enable_entreprises").first()
|
||||
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
|
||||
def is_bul_pdf_disabled(cls) -> bool:
|
||||
"""True si on interdit les exports PDF des bulletins"""
|
||||
"""True si on interdit les exports PDF des bulltins"""
|
||||
cfg = ScoDocSiteConfig.query.filter_by(name="disable_bul_pdf").first()
|
||||
return cfg is not None and cfg.value
|
||||
|
||||
@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."""
|
||||
return cls.set("enable_entreprises", "on" if enabled else "")
|
||||
|
||||
@classmethod
|
||||
def disable_passerelle(cls, disabled: bool = True) -> bool:
|
||||
"""Désactive (ou active) les fonctions liées à la présence d'une passerelle. True si changement."""
|
||||
return cls.set("disable_passerelle", "on" if disabled else "")
|
||||
if enabled != ScoDocSiteConfig.is_entreprises_enabled():
|
||||
cfg = ScoDocSiteConfig.query.filter_by(name="enable_entreprises").first()
|
||||
if cfg is None:
|
||||
cfg = ScoDocSiteConfig(
|
||||
name="enable_entreprises", value="on" if enabled else ""
|
||||
)
|
||||
else:
|
||||
cfg.value = "on" if enabled else ""
|
||||
db.session.add(cfg)
|
||||
db.session.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def disable_bul_pdf(cls, enabled=True) -> bool:
|
||||
"""Interdit (ou autorise) les exports PDF. True si changement."""
|
||||
return cls.set("disable_bul_pdf", "on" if enabled else "")
|
||||
"""Interedit (ou autorise) les exports PDF. True si changement."""
|
||||
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
|
||||
def get(cls, name: str, default: str = "") -> str:
|
||||
@ -297,10 +292,9 @@ class ScoDocSiteConfig(db.Model):
|
||||
if cfg is None:
|
||||
cfg = ScoDocSiteConfig(name=name, value=value_str)
|
||||
else:
|
||||
cfg.value = value_str
|
||||
cfg.value = str(value or "")
|
||||
current_app.logger.info(
|
||||
f"""ScoDocSiteConfig: recording {cfg.name}='{cfg.value[:32]}{
|
||||
'...' if len(cfg.value)>32 else ''}'"""
|
||||
f"""ScoDocSiteConfig: recording {cfg.name}='{cfg.value[:32]}...'"""
|
||||
)
|
||||
db.session.add(cfg)
|
||||
db.session.commit()
|
||||
@ -309,7 +303,7 @@ class ScoDocSiteConfig(db.Model):
|
||||
|
||||
@classmethod
|
||||
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()
|
||||
if (cfg is None) or cfg.value is None:
|
||||
return default
|
||||
@ -323,7 +317,7 @@ class ScoDocSiteConfig(db.Model):
|
||||
default=None,
|
||||
range_values: tuple = (),
|
||||
) -> bool:
|
||||
"""Set champ integer. True si changement."""
|
||||
"""Set champs integer. True si changement."""
|
||||
if value != cls._get_int_field(name, default=default):
|
||||
if not isinstance(value, int) or (
|
||||
range_values and (value < range_values[0]) or (value > range_values[1])
|
||||
|
@ -52,17 +52,6 @@ class Departement(db.Model):
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__}(id={self.id}, acronym='{self.acronym}')>"
|
||||
|
||||
@classmethod
|
||||
def get_departement(cls, dept_ident: str | int) -> "Departement":
|
||||
"Le département, par id ou acronyme. Erreur 404 si pas trouvé."
|
||||
try:
|
||||
dept_id = int(dept_ident)
|
||||
except ValueError:
|
||||
dept_id = None
|
||||
if dept_id is None:
|
||||
return cls.query.filter_by(acronym=dept_ident).first_or_404()
|
||||
return cls.query.get_or_404(dept_id)
|
||||
|
||||
def to_dict(self, with_dept_name=True, with_dept_preferences=False):
|
||||
data = {
|
||||
"id": self.id,
|
||||
|
@ -15,15 +15,14 @@ from sqlalchemy import desc, text
|
||||
|
||||
from app import db, log
|
||||
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.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
|
||||
|
||||
|
||||
class Identite(models.ScoDocModel):
|
||||
class Identite(db.Model, models.ScoDocModel):
|
||||
"""étudiant"""
|
||||
|
||||
__tablename__ = "identite"
|
||||
@ -101,12 +100,7 @@ class Identite(models.ScoDocModel):
|
||||
adresses = db.relationship(
|
||||
"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")
|
||||
#
|
||||
dispense_ues = db.relationship(
|
||||
@ -124,9 +118,6 @@ class Identite(models.ScoDocModel):
|
||||
"Justificatif", back_populates="etudiant", lazy="dynamic", cascade="all, delete"
|
||||
)
|
||||
|
||||
# Champs "protégés" par ViewEtudData (RGPD)
|
||||
protected_attrs = {"boursier", "nationalite"}
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
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:
|
||||
"lien vers la fiche"
|
||||
return (
|
||||
f"""<a class="etudlink" href="{self.url_fiche()}">{self.nom_prenom()}</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
|
||||
)
|
||||
return f"""<a class="stdlink" href="{
|
||||
url_for("scolar.ficheEtud", scodoc_dept=self.departement.acronym, etudid=self.id)
|
||||
}">{self.nomprenom}</a>"""
|
||||
|
||||
@classmethod
|
||||
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()
|
||||
|
||||
@classmethod
|
||||
def get_etud(cls, etudid: int, accept_none=False) -> "Identite":
|
||||
"""Etudiant ou 404 (ou None si accept_none),
|
||||
cherche uniquement dans le département courant.
|
||||
Si accept_none, return None si l'id est invalide ou ne correspond
|
||||
pas à un étudiant.
|
||||
"""
|
||||
if not isinstance(etudid, int):
|
||||
try:
|
||||
etudid = int(etudid)
|
||||
except (TypeError, ValueError):
|
||||
if accept_none:
|
||||
return None
|
||||
abort(404, "etudid invalide")
|
||||
|
||||
query = (
|
||||
cls.query.filter_by(id=etudid, dept_id=g.scodoc_dept_id)
|
||||
if g.scodoc_dept
|
||||
else cls.query.filter_by(id=etudid)
|
||||
)
|
||||
if accept_none:
|
||||
return query.first()
|
||||
return query.first_or_404()
|
||||
def get_etud(cls, etudid: int) -> "Identite":
|
||||
"""Etudiant ou 404, cherche uniquement dans le département courant"""
|
||||
if g.scodoc_dept:
|
||||
return cls.query.filter_by(
|
||||
id=etudid, dept_id=g.scodoc_dept_id
|
||||
).first_or_404()
|
||||
return cls.query.filter_by(id=etudid).first_or_404()
|
||||
|
||||
@classmethod
|
||||
def create_etud(cls, **args) -> "Identite":
|
||||
@ -230,48 +200,19 @@ class Identite(models.ScoDocModel):
|
||||
return cls.create_from_dict(args)
|
||||
|
||||
@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.
|
||||
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)
|
||||
"""
|
||||
if not "dept_id" in args:
|
||||
if "dept" in args:
|
||||
departement = Departement.query.filter_by(acronym=args["dept"]).first()
|
||||
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: Identite = super(cls, cls).create_from_dict(data)
|
||||
if (data.get("admission_id", None) is None) and (
|
||||
data.get("admission", None) is None
|
||||
):
|
||||
etud.admission = Admission()
|
||||
etud.adresses.append(Adresse(typeadresse="domicile"))
|
||||
db.session.flush()
|
||||
|
||||
event = ScolarEvent(etud=etud, event_type="CREATION")
|
||||
db.session.add(event)
|
||||
log(f"Identite.create {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
|
||||
def civilite_str(self) -> str:
|
||||
"""returns civilité usuelle: 'M.' ou 'Mme' ou '' (pour le genre neutre,
|
||||
@ -314,24 +255,18 @@ class Identite(models.ScoDocModel):
|
||||
else:
|
||||
return self.nom
|
||||
|
||||
@property
|
||||
@cached_property
|
||||
def nomprenom(self, reverse=False) -> str:
|
||||
"""DEPRECATED: préférer nom_prenom()
|
||||
Civilité/prénom/nom pour affichages: "M. Pierre Dupont"
|
||||
"""Civilité/nom/prenom pour affichages: "M. Pierre Dupont"
|
||||
Si reverse, "Dupont Pierre", sans civilité.
|
||||
Prend l'identité courante et non celle de l'état civil si elles diffèrent.
|
||||
"""
|
||||
nom = self.nom_usuel or self.nom
|
||||
prenom = self.prenom_str
|
||||
if reverse:
|
||||
return f"{nom} {prenom}".strip()
|
||||
return f"{self.civilite_str} {prenom} {nom}".strip()
|
||||
|
||||
def nom_prenom(self) -> str:
|
||||
"""Civilite NOM Prénom
|
||||
Prend l'identité courante et non celle de l'état civil si elles diffèrent.
|
||||
"""
|
||||
return f"{self.civilite_str} {(self.nom_usuel or self.nom).upper()} {self.prenom_str}"
|
||||
fields = (nom, prenom)
|
||||
else:
|
||||
fields = (self.civilite_str, prenom, nom)
|
||||
return " ".join([x for x in fields if x])
|
||||
|
||||
@property
|
||||
def prenom_str(self):
|
||||
@ -347,10 +282,12 @@ class Identite(models.ScoDocModel):
|
||||
|
||||
@property
|
||||
def etat_civil(self) -> str:
|
||||
"M. PRÉNOM NOM, utilisant les données état civil si présentes, usuelles sinon."
|
||||
return f"""{self.civilite_etat_civil_str} {
|
||||
self.prenom_etat_civil or self.prenom or ''} {
|
||||
self.nom or ''}""".strip()
|
||||
"M. Prénom NOM, utilisant les données état civil si présentes, usuelles sinon."
|
||||
if self.prenom_etat_civil:
|
||||
civ = {"M": "M.", "F": "Mme", "X": ""}[self.civilite_etat_civil]
|
||||
return f"{civ} {self.prenom_etat_civil} {self.nom}"
|
||||
else:
|
||||
return self.nomprenom
|
||||
|
||||
@property
|
||||
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()}."
|
||||
|
||||
@cached_property
|
||||
def sort_key(self) -> str:
|
||||
def sort_key(self) -> tuple:
|
||||
"clé pour tris par ordre alphabétique"
|
||||
# Note: scodoc7 utilisait sco_etud.etud_sort_key, à mettre à jour
|
||||
# si on modifie cette méthode.
|
||||
return scu.sanitize_string(
|
||||
(self.nom_usuel or self.nom or "") + ";" + (self.prenom or ""),
|
||||
remove_spaces=False,
|
||||
).lower()
|
||||
return (
|
||||
scu.sanitize_string(
|
||||
self.nom_usuel or self.nom or "", remove_spaces=False
|
||||
).lower(),
|
||||
scu.sanitize_string(self.prenom or "", remove_spaces=False).lower(),
|
||||
)
|
||||
|
||||
def get_first_email(self, field="email") -> str:
|
||||
"Le mail associé à la première adresse de l'étudiant, ou None"
|
||||
return getattr(self.adresses[0], field) if self.adresses.count() > 0 else None
|
||||
|
||||
def get_formsemestres(self, recent_first=True) -> list:
|
||||
def get_formsemestres(self) -> list:
|
||||
"""Liste des formsemestres dans lesquels l'étudiant est (a été) inscrit,
|
||||
triée par date_debut, le plus récent d'abord (comme "sems" de scodoc7)
|
||||
(si recent_first=False, le plus ancien en tête)
|
||||
triée par date_debut
|
||||
"""
|
||||
return sorted(
|
||||
[ins.formsemestre for ins in self.formsemestre_inscriptions],
|
||||
key=attrgetter("date_debut"),
|
||||
reverse=recent_first,
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
def get_modimpls_by_formsemestre(
|
||||
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
|
||||
def convert_dict_fields(cls, args: dict) -> dict:
|
||||
"""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.
|
||||
"""
|
||||
# Les champs qui sont toujours stockés en majuscules:
|
||||
@ -451,6 +341,8 @@ class Identite(models.ScoDocModel):
|
||||
"code_ine",
|
||||
}
|
||||
args_dict = {}
|
||||
if not "dept_id" in args:
|
||||
args["dept_id"] = g.scodoc_dept_id
|
||||
for key, value in args.items():
|
||||
if hasattr(cls, key) and not isinstance(getattr(cls, key, None), property):
|
||||
# compat scodoc7 (mauvaise idée de l'époque)
|
||||
@ -463,14 +355,14 @@ class Identite(models.ScoDocModel):
|
||||
elif key == "civilite_etat_civil":
|
||||
value = input_civilite_etat_civil(value)
|
||||
elif key == "boursier":
|
||||
value = scu.to_bool(value)
|
||||
value = bool(value)
|
||||
elif key == "date_naissance":
|
||||
value = ndb.DateDMYtoISO(value)
|
||||
args_dict[key] = value
|
||||
return args_dict
|
||||
|
||||
def to_dict_short(self) -> dict:
|
||||
"""Les champs essentiels (aucune donnée perso protégée)"""
|
||||
"""Les champs essentiels"""
|
||||
return {
|
||||
"id": self.id,
|
||||
"civilite": self.civilite,
|
||||
@ -485,11 +377,9 @@ class Identite(models.ScoDocModel):
|
||||
"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,
|
||||
compatible ScoDoc7 mais sans infos admission.
|
||||
Si restrict, cache les infos "personnelles" si pas permission ViewEtudData
|
||||
Si with_inscriptions, inclut les champs "inscription"
|
||||
compatible ScoDoc7 mais sans infos admission
|
||||
"""
|
||||
e_dict = self.__dict__.copy() # dict(self.__dict__)
|
||||
e_dict.pop("_sa_instance_state", None)
|
||||
@ -500,9 +390,7 @@ class Identite(models.ScoDocModel):
|
||||
e_dict["nomprenom"] = self.nomprenom
|
||||
adresse = self.adresses.first()
|
||||
if adresse:
|
||||
e_dict.update(adresse.to_dict(restrict=restrict))
|
||||
if with_inscriptions:
|
||||
e_dict.update(self.inscription_descr())
|
||||
e_dict.update(adresse.to_dict())
|
||||
return {k: v or "" for k, v in e_dict.items()} # convert_null_outputs_to_empty
|
||||
|
||||
def to_dict_bul(self, include_urls=True):
|
||||
@ -517,11 +405,9 @@ class Identite(models.ScoDocModel):
|
||||
"civilite": self.civilite,
|
||||
"code_ine": self.code_ine or "",
|
||||
"code_nip": self.code_nip or "",
|
||||
"date_naissance": (
|
||||
self.date_naissance.strftime(scu.DATE_FMT)
|
||||
if self.date_naissance
|
||||
else ""
|
||||
),
|
||||
"date_naissance": self.date_naissance.strftime("%d/%m/%Y")
|
||||
if self.date_naissance
|
||||
else "",
|
||||
"dept_acronym": self.departement.acronym,
|
||||
"dept_id": self.dept_id,
|
||||
"dept_naissance": self.dept_naissance or "",
|
||||
@ -539,7 +425,7 @@ class Identite(models.ScoDocModel):
|
||||
if include_urls and has_request_context():
|
||||
# test request context so we can use this func in tests under the flask shell
|
||||
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)
|
||||
adresse = self.adresses.first()
|
||||
@ -548,37 +434,22 @@ class Identite(models.ScoDocModel):
|
||||
d["id"] = self.id # a été écrasé par l'id de adresse
|
||||
return d
|
||||
|
||||
def to_dict_api(self, restrict=False, with_annotations=False) -> dict:
|
||||
"""Représentation dictionnaire pour export API, avec adresses et admission.
|
||||
Si restrict, supprime les infos "personnelles" (boursier)
|
||||
"""
|
||||
def to_dict_api(self) -> dict:
|
||||
"""Représentation dictionnaire pour export API, avec adresses et admission."""
|
||||
e = dict(self.__dict__)
|
||||
e.pop("_sa_instance_state", None)
|
||||
admission = self.admission
|
||||
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.pop("departement", None)
|
||||
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
|
||||
|
||||
def inscriptions(self) -> list["FormSemestreInscription"]:
|
||||
"Liste des inscriptions à des formsemestres, triée, la plus récente en tête"
|
||||
from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
||||
|
||||
return (
|
||||
FormSemestreInscription.query.join(FormSemestreInscription.formsemestre)
|
||||
.filter(
|
||||
@ -588,7 +459,7 @@ class Identite(models.ScoDocModel):
|
||||
.all()
|
||||
)
|
||||
|
||||
def inscription_courante(self) -> "FormSemestreInscription | None":
|
||||
def inscription_courante(self):
|
||||
"""La première inscription à un formsemestre _actuellement_ en cours.
|
||||
None s'il n'y en a pas (ou plus, ou pas encore).
|
||||
"""
|
||||
@ -604,6 +475,8 @@ class Identite(models.ScoDocModel):
|
||||
(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).
|
||||
"""
|
||||
from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
||||
|
||||
return (
|
||||
FormSemestreInscription.query.join(FormSemestreInscription.formsemestre)
|
||||
.filter(
|
||||
@ -626,9 +499,7 @@ class Identite(models.ScoDocModel):
|
||||
return r[0] if r else None
|
||||
|
||||
def inscription_descr(self) -> dict:
|
||||
"""Description de l'état d'inscription
|
||||
avec champs compatibles templates ScoDoc7
|
||||
"""
|
||||
"""Description de l'état d'inscription"""
|
||||
inscription_courante = self.inscription_courante()
|
||||
if inscription_courante:
|
||||
titre_sem = inscription_courante.formsemestre.titre_mois()
|
||||
@ -639,7 +510,7 @@ class Identite(models.ScoDocModel):
|
||||
else:
|
||||
inscr_txt = "Inscrit en"
|
||||
|
||||
result = {
|
||||
return {
|
||||
"etat_in_cursem": inscription_courante.etat,
|
||||
"inscription_courante": inscription_courante,
|
||||
"inscription": titre_sem,
|
||||
@ -662,20 +533,15 @@ class Identite(models.ScoDocModel):
|
||||
inscription = "ancien"
|
||||
situation = "ancien élève"
|
||||
else:
|
||||
inscription = "non inscrit"
|
||||
inscription = ("non inscrit",)
|
||||
situation = inscription
|
||||
result = {
|
||||
return {
|
||||
"etat_in_cursem": "?",
|
||||
"inscription_courante": None,
|
||||
"inscription": inscription,
|
||||
"inscription_str": inscription,
|
||||
"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:
|
||||
"""État de l'inscription de cet étudiant au semestre:
|
||||
@ -772,7 +638,7 @@ class Identite(models.ScoDocModel):
|
||||
"""
|
||||
if with_paragraph:
|
||||
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 ""}"""
|
||||
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(
|
||||
etudid=None, code_nip=None, use_request=True, raise_exc=False, abort_404=True
|
||||
) -> dict:
|
||||
@ -928,7 +742,7 @@ def pivot_year(y) -> int:
|
||||
return y
|
||||
|
||||
|
||||
class Adresse(models.ScoDocModel):
|
||||
class Adresse(db.Model, models.ScoDocModel):
|
||||
"""Adresse d'un étudiant
|
||||
(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)
|
||||
|
||||
# Champs "protégés" par ViewEtudData (RGPD)
|
||||
protected_attrs = {
|
||||
"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)."""
|
||||
def to_dict(self, convert_nulls_to_str=False):
|
||||
"""Représentation dictionnaire,"""
|
||||
e = dict(self.__dict__)
|
||||
e.pop("_sa_instance_state", None)
|
||||
if convert_nulls_to_str:
|
||||
e = {k: v or "" for k, v in e.items()}
|
||||
if restrict:
|
||||
e = {k: v for (k, v) in e.items() if k not in self.protected_attrs}
|
||||
return {k: e[k] or "" for k in e}
|
||||
return e
|
||||
|
||||
|
||||
class Admission(models.ScoDocModel):
|
||||
class Admission(db.Model, models.ScoDocModel):
|
||||
"""Informations liées à l'admission d'un étudiant"""
|
||||
|
||||
__tablename__ = "admissions"
|
||||
@ -1028,16 +829,12 @@ class Admission(models.ScoDocModel):
|
||||
# classement (1..Ngr) par le jury dans le groupe APB
|
||||
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:
|
||||
"Le bac. utiliser bac.abbrev() pour avoir une chaine de caractères."
|
||||
return Baccalaureat(self.bac, specialite=self.specialite)
|
||||
|
||||
def to_dict(self, no_nulls=False, restrict=False):
|
||||
"""Représentation dictionnaire. Si restrict, filtre les champs protégés (RGPD)."""
|
||||
def to_dict(self, no_nulls=False):
|
||||
"""Représentation dictionnaire,"""
|
||||
d = dict(self.__dict__)
|
||||
d.pop("_sa_instance_state", None)
|
||||
if no_nulls:
|
||||
@ -1052,8 +849,6 @@ class Admission(models.ScoDocModel):
|
||||
d[key] = 0
|
||||
elif isinstance(col_type, sqlalchemy.Boolean):
|
||||
d[key] = False
|
||||
if restrict:
|
||||
d = {k: v for (k, v) in d.items() if k in self.not_protected_attrs}
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
@ -1080,9 +875,8 @@ class Admission(models.ScoDocModel):
|
||||
return args_dict
|
||||
|
||||
|
||||
class ItemSuivi(models.ScoDocModel):
|
||||
"""Suivi scolarité / débouchés"""
|
||||
|
||||
# Suivi scolarité / débouchés
|
||||
class ItemSuivi(db.Model):
|
||||
__tablename__ = "itemsuivi"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@ -1094,8 +888,6 @@ class ItemSuivi(models.ScoDocModel):
|
||||
item_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
situation = db.Column(db.Text)
|
||||
|
||||
_sco_dept_relations = ("Identite",) # accès au dept_id
|
||||
|
||||
|
||||
class ItemSuiviTag(db.Model):
|
||||
__tablename__ = "itemsuivi_tags"
|
||||
@ -1117,25 +909,13 @@ itemsuivi_tags_assoc = db.Table(
|
||||
)
|
||||
|
||||
|
||||
class EtudAnnotation(models.ScoDocModel):
|
||||
class EtudAnnotation(db.Model):
|
||||
"""Annotation sur un étudiant"""
|
||||
|
||||
__tablename__ = "etud_annotations"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
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
|
||||
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
|
||||
from operator import attrgetter
|
||||
|
||||
from flask import abort, g, url_for
|
||||
from flask import g, url_for
|
||||
from flask_login import current_user
|
||||
import sqlalchemy as sa
|
||||
|
||||
from app import db, log
|
||||
from app import models
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.events import ScolarNews
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
from app.models.notes import NotesNotes
|
||||
from app.models.ues import UniteEns
|
||||
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||
@ -24,8 +25,10 @@ MAX_EVALUATION_DURATION = datetime.timedelta(days=365)
|
||||
NOON = datetime.time(12, 00)
|
||||
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, ...)"""
|
||||
|
||||
__tablename__ = "notes_evaluation"
|
||||
@ -37,9 +40,9 @@ class Evaluation(models.ScoDocModel):
|
||||
)
|
||||
date_debut = 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)
|
||||
note_max = db.Column(db.Float, nullable=False)
|
||||
coefficient = db.Column(db.Float, nullable=False)
|
||||
description = db.Column(db.Text)
|
||||
note_max = db.Column(db.Float)
|
||||
coefficient = db.Column(db.Float)
|
||||
visibulletin = db.Column(
|
||||
db.Boolean, nullable=False, default=True, server_default="true"
|
||||
)
|
||||
@ -47,41 +50,15 @@ class Evaluation(models.ScoDocModel):
|
||||
publish_incomplete = db.Column(
|
||||
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(
|
||||
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
|
||||
# est la plus ancienne eval):
|
||||
numero = db.Column(db.Integer, nullable=False, default=0)
|
||||
ues = db.relationship("UniteEns", secondary="evaluation_ue_poids", viewonly=True)
|
||||
|
||||
_sco_dept_relations = ("ModuleImpl", "FormSemestre") # accès au dept_id
|
||||
|
||||
EVALUATION_NORMALE = 0 # valeurs stockées en base, ne pas changer !
|
||||
EVALUATION_RATTRAPAGE = 1
|
||||
EVALUATION_SESSION2 = 2
|
||||
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):
|
||||
return f"""<Evaluation {self.id} {
|
||||
self.date_debut.isoformat() if self.date_debut else ''} "{
|
||||
@ -90,22 +67,20 @@ class Evaluation(models.ScoDocModel):
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
moduleimpl: "ModuleImpl" = None,
|
||||
moduleimpl: ModuleImpl = None,
|
||||
date_debut: datetime.datetime = None,
|
||||
date_fin: datetime.datetime = None,
|
||||
description=None,
|
||||
note_max=None,
|
||||
blocked_until=None,
|
||||
coefficient=None,
|
||||
visibulletin=None,
|
||||
publish_incomplete=None,
|
||||
evaluation_type=None,
|
||||
numero=None,
|
||||
**kw, # ceci pour absorber les éventuel arguments excedentaires
|
||||
) -> "Evaluation":
|
||||
):
|
||||
"""Create an evaluation. Check permission and all arguments.
|
||||
Ne crée pas les poids vers les UEs.
|
||||
Add to session, do not commit.
|
||||
"""
|
||||
if not moduleimpl.can_edit_evaluation(current_user):
|
||||
raise AccessDenied(
|
||||
@ -114,15 +89,13 @@ class Evaluation(models.ScoDocModel):
|
||||
args = locals()
|
||||
del args["cls"]
|
||||
del args["kw"]
|
||||
check_and_convert_evaluation_args(args, moduleimpl)
|
||||
check_convert_evaluation_args(moduleimpl, args)
|
||||
# Check numeros
|
||||
Evaluation.moduleimpl_evaluation_renumber(moduleimpl, only_if_unumbered=True)
|
||||
if not "numero" in args or args["numero"] is None:
|
||||
args["numero"] = cls.get_new_numero(moduleimpl, args["date_debut"])
|
||||
#
|
||||
evaluation = Evaluation(**args)
|
||||
db.session.add(evaluation)
|
||||
db.session.flush()
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id=moduleimpl.formsemestre_id)
|
||||
url = url_for(
|
||||
"notes.moduleimpl_status",
|
||||
@ -141,7 +114,7 @@ class Evaluation(models.ScoDocModel):
|
||||
|
||||
@classmethod
|
||||
def get_new_numero(
|
||||
cls, moduleimpl: "ModuleImpl", date_debut: datetime.datetime
|
||||
cls, moduleimpl: ModuleImpl, date_debut: datetime.datetime
|
||||
) -> int:
|
||||
"""Get a new numero for an evaluation in this moduleimpl
|
||||
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)"
|
||||
from app.scodoc import sco_evaluation_db
|
||||
|
||||
modimpl: "ModuleImpl" = self.moduleimpl
|
||||
modimpl: ModuleImpl = self.moduleimpl
|
||||
if not modimpl.can_edit_evaluation(current_user):
|
||||
raise AccessDenied(
|
||||
f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
|
||||
@ -213,24 +186,18 @@ class Evaluation(models.ScoDocModel):
|
||||
# ScoDoc7 output_formators
|
||||
e_dict["evaluation_id"] = self.id
|
||||
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["poids"] = self.get_ue_poids_dict() # { ue_id : poids }
|
||||
|
||||
# Deprecated
|
||||
e_dict["jour"] = (
|
||||
self.date_debut.strftime(scu.DATE_FMT) if self.date_debut else ""
|
||||
)
|
||||
e_dict["jour"] = self.date_debut.strftime("%d/%m/%Y") if self.date_debut else ""
|
||||
|
||||
return evaluation_enrich_dict(self, e_dict)
|
||||
|
||||
def to_dict_api(self) -> dict:
|
||||
"Représentation dict pour API JSON"
|
||||
return {
|
||||
"blocked": self.is_blocked(),
|
||||
"blocked_until": (
|
||||
self.blocked_until.isoformat() if self.blocked_until else ""
|
||||
),
|
||||
"coefficient": self.coefficient,
|
||||
"date_debut": self.date_debut.isoformat() if self.date_debut else "",
|
||||
"date_fin": self.date_fin.isoformat() if self.date_fin else "",
|
||||
@ -245,9 +212,9 @@ class Evaluation(models.ScoDocModel):
|
||||
"visibulletin": self.visibulletin,
|
||||
# Deprecated (supprimer avant #sco9.7)
|
||||
"date": self.date_debut.date().isoformat() if self.date_debut else "",
|
||||
"heure_debut": (
|
||||
self.date_debut.time().isoformat() if self.date_debut else ""
|
||||
),
|
||||
"heure_debut": self.date_debut.time().isoformat()
|
||||
if self.date_debut
|
||||
else "",
|
||||
"heure_fin": self.date_fin.time().isoformat() if self.date_fin else "",
|
||||
}
|
||||
|
||||
@ -267,28 +234,14 @@ class Evaluation(models.ScoDocModel):
|
||||
|
||||
return e_dict
|
||||
|
||||
@classmethod
|
||||
def get_evaluation(
|
||||
cls, evaluation_id: int | str, dept_id: int = None, accept_none=False
|
||||
) -> "Evaluation":
|
||||
"""Evaluation ou 404, cherche uniquement dans le département spécifié ou le courant.
|
||||
Si accept_none, return None si l'id est invalide ou n'existe pas.
|
||||
"""
|
||||
from app.models import FormSemestre
|
||||
|
||||
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()
|
||||
def from_dict(self, data):
|
||||
"""Set evaluation attributes from given dict values."""
|
||||
check_convert_evaluation_args(self.moduleimpl, data)
|
||||
if data.get("numero") is None:
|
||||
data["numero"] = Evaluation.get_max_numero(self.moduleimpl.id) + 1
|
||||
for k in self.__dict__.keys():
|
||||
if k != "_sa_instance_state" and k != "id" and k in data:
|
||||
setattr(self, k, data[k])
|
||||
|
||||
@classmethod
|
||||
def get_max_numero(cls, moduleimpl_id: int) -> int:
|
||||
@ -304,7 +257,7 @@ class Evaluation(models.ScoDocModel):
|
||||
|
||||
@classmethod
|
||||
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)
|
||||
Needed because previous versions of ScoDoc did not have eval numeros
|
||||
@ -314,9 +267,7 @@ class Evaluation(models.ScoDocModel):
|
||||
evaluations = moduleimpl.evaluations.order_by(
|
||||
Evaluation.date_debut, Evaluation.numero
|
||||
).all()
|
||||
numeros_distincts = {e.numero for e in evaluations if e.numero is not None}
|
||||
# pas de None, pas de dupliqués
|
||||
all_numbered = len(numeros_distincts) == len(evaluations)
|
||||
all_numbered = all(e.numero is not None for e in evaluations)
|
||||
if all_numbered and only_if_unumbered:
|
||||
return # all ok
|
||||
|
||||
@ -327,15 +278,14 @@ class Evaluation(models.ScoDocModel):
|
||||
db.session.add(e)
|
||||
i += 1
|
||||
db.session.commit()
|
||||
sco_cache.invalidate_formsemestre(moduleimpl.formsemestre_id)
|
||||
|
||||
def descr_heure(self) -> str:
|
||||
"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):
|
||||
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:
|
||||
return f"""de {self.date_debut.strftime(scu.TIME_FMT)
|
||||
} à {self.date_fin.strftime(scu.TIME_FMT)}"""
|
||||
return f"""de {self.date_debut.strftime("%Hh%M")
|
||||
} à {self.date_fin.strftime("%Hh%M")}"""
|
||||
else:
|
||||
return ""
|
||||
|
||||
@ -362,15 +312,13 @@ class Evaluation(models.ScoDocModel):
|
||||
|
||||
def _h(dt: datetime.datetime) -> str:
|
||||
if dt.minute:
|
||||
return dt.strftime(scu.TIME_FMT)
|
||||
return dt.strftime("%Hh%M")
|
||||
return f"{dt.hour}h"
|
||||
|
||||
if self.date_fin is None:
|
||||
return f"le {self.date_debut.strftime('%d/%m/%Y')} à {_h(self.date_debut)}"
|
||||
if self.date_debut.date() == self.date_fin.date(): # même jour
|
||||
if self.date_debut.time() == self.date_fin.time():
|
||||
if self.date_fin.time() == datetime.time(0, 0):
|
||||
return f"le {self.date_debut.strftime('%d/%m/%Y')}" # sans heure
|
||||
return (
|
||||
f"le {self.date_debut.strftime('%d/%m/%Y')} à {_h(self.date_debut)}"
|
||||
)
|
||||
@ -390,6 +338,19 @@ class Evaluation(models.ScoDocModel):
|
||||
Chaine vide si non renseignée."""
|
||||
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:
|
||||
"Evaluation commençant le matin (faux si pas de date)"
|
||||
if not self.date_debut:
|
||||
@ -402,14 +363,6 @@ class Evaluation(models.ScoDocModel):
|
||||
return False
|
||||
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:
|
||||
"""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.
|
||||
@ -434,28 +387,22 @@ class Evaluation(models.ScoDocModel):
|
||||
return modified
|
||||
|
||||
def set_ue_poids(self, ue, poids: float) -> None:
|
||||
"""Set poids évaluation vers cette UE. Commit."""
|
||||
"""Set poids évaluation vers cette UE"""
|
||||
self.update_ue_poids_dict({ue.id: poids})
|
||||
|
||||
def set_ue_poids_dict(self, ue_poids_dict: dict) -> None:
|
||||
"""set poids vers les UE (remplace existants)
|
||||
ue_poids_dict = { ue_id : poids }
|
||||
Commit session.
|
||||
"""
|
||||
from app.models.ues import UniteEns
|
||||
|
||||
L = []
|
||||
for ue_id, poids in ue_poids_dict.items():
|
||||
ue = db.session.get(UniteEns, ue_id)
|
||||
if ue is None:
|
||||
raise ScoValueError("poids vers une UE inexistante")
|
||||
ue_poids = EvaluationUEPoids(evaluation=self, ue=ue, poids=poids)
|
||||
db.session.add(ue_poids)
|
||||
L.append(ue_poids)
|
||||
|
||||
db.session.add(ue_poids)
|
||||
self.ue_poids = L # backref # pylint:disable=attribute-defined-outside-init
|
||||
|
||||
db.session.commit()
|
||||
self.moduleimpl.invalidate_evaluations_poids() # inval cache
|
||||
|
||||
def update_ue_poids_dict(self, ue_poids_dict: dict) -> None:
|
||||
@ -480,8 +427,8 @@ class Evaluation(models.ScoDocModel):
|
||||
|
||||
def get_ue_poids_str(self) -> str:
|
||||
"""string describing poids, for excel cells and pdfs
|
||||
Note: les poids nuls ou non initialisés (poids par défaut),
|
||||
ne sont pas affichés.
|
||||
Note: si les poids ne sont pas initialisés (poids par défaut),
|
||||
ils ne sont pas affichés.
|
||||
"""
|
||||
# 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
|
||||
@ -492,7 +439,7 @@ class Evaluation(models.ScoDocModel):
|
||||
for p in sorted(
|
||||
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()
|
||||
|
||||
@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):
|
||||
"""Poids des évaluations (BUT)
|
||||
@ -550,7 +474,7 @@ class EvaluationUEPoids(db.Model):
|
||||
backref=db.backref("ue_poids", cascade="all, delete-orphan"),
|
||||
)
|
||||
ue = db.relationship(
|
||||
"UniteEns",
|
||||
UniteEns,
|
||||
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):
|
||||
"""add or convert some fields in an evaluation dict"""
|
||||
# For ScoDoc7 compat
|
||||
e_dict["heure_debut"] = e.date_debut.strftime(scu.TIME_FMT) if e.date_debut else ""
|
||||
e_dict["heure_fin"] = e.date_fin.strftime(scu.TIME_FMT) if e.date_fin else ""
|
||||
e_dict["heure_debut"] = e.date_debut.strftime("%Hh%M") if e.date_debut 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 ""
|
||||
# Calcule durée en minutes
|
||||
e_dict["descrheure"] = e.descr_heure()
|
||||
@ -582,7 +506,7 @@ def evaluation_enrich_dict(e: Evaluation, e_dict: 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.
|
||||
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
|
||||
try:
|
||||
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")
|
||||
except ValueError as 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:
|
||||
raise ScoValueError("invalid coefficient value (must be positive or null)")
|
||||
data["coefficient"] = coef
|
||||
# --- date de l'évaluation dans le semestre ?
|
||||
# --- date de l'évaluation
|
||||
formsemestre = moduleimpl.formsemestre
|
||||
date_debut = data.get("date_debut", None)
|
||||
if date_debut:
|
||||
@ -637,7 +561,7 @@ def check_and_convert_evaluation_args(data: dict, moduleimpl: "ModuleImpl"):
|
||||
):
|
||||
raise ScoValueError(
|
||||
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 !""",
|
||||
dest_url="javascript:history.back();",
|
||||
)
|
||||
@ -652,19 +576,27 @@ def check_and_convert_evaluation_args(data: dict, moduleimpl: "ModuleImpl"):
|
||||
):
|
||||
raise ScoValueError(
|
||||
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 !""",
|
||||
dest_url="javascript:history.back();",
|
||||
)
|
||||
if date_debut and date_fin:
|
||||
duration = data["date_fin"] - data["date_debut"]
|
||||
if duration.total_seconds() < 0 or duration > MAX_EVALUATION_DURATION:
|
||||
raise ScoValueError(
|
||||
"Heures de l'évaluation incohérentes !",
|
||||
dest_url="javascript:history.back();",
|
||||
)
|
||||
if "blocked_until" in data:
|
||||
data["blocked_until"] = data["blocked_until"] or None
|
||||
raise ScoValueError("Heures de l'évaluation incohérentes !")
|
||||
# # --- heures
|
||||
# heure_debut = data.get("heure_debut", None)
|
||||
# if heure_debut and not isinstance(heure_debut, datetime.time):
|
||||
# if date_format == "dmy":
|
||||
# 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:
|
||||
@ -674,6 +606,19 @@ def heure_to_time(heure: str) -> datetime.time:
|
||||
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(
|
||||
evaluations: list[Evaluation], next_eval: Evaluation
|
||||
) -> int:
|
||||
@ -694,6 +639,3 @@ def _moduleimpl_evaluation_insert_before(
|
||||
db.session.add(e)
|
||||
db.session.commit()
|
||||
return n
|
||||
|
||||
|
||||
from app.models.moduleimpls import ModuleImpl, ModuleImplInscription
|
||||
|
@ -12,12 +12,13 @@ from app import db
|
||||
from app import email
|
||||
from app import log
|
||||
from app.auth.models import User
|
||||
from app.models import ScoDocModel, SHORT_STR_LEN
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc import sco_preferences
|
||||
|
||||
|
||||
class Scolog(ScoDocModel):
|
||||
class Scolog(db.Model):
|
||||
"""Log des actions (journal modif etudiants)"""
|
||||
|
||||
__tablename__ = "scolog"
|
||||
@ -27,15 +28,14 @@ class Scolog(ScoDocModel):
|
||||
method = db.Column(db.Text)
|
||||
msg = db.Column(db.Text)
|
||||
etudid = db.Column(db.Integer) # sans contrainte pour garder logs après suppression
|
||||
authenticated_user = db.Column(db.Text) # user_name login, sans contrainte
|
||||
authenticated_user = db.Column(db.Text) # login, sans contrainte
|
||||
# zope_remote_addr suppressed
|
||||
|
||||
@classmethod
|
||||
def logdb(
|
||||
cls, method: str = None, etudid: int = None, msg: str = None, commit=False
|
||||
):
|
||||
"""Add entry in student's log (replacement for old scolog.logdb).
|
||||
Par défaut ne commite pas."""
|
||||
"""Add entry in student's log (replacement for old scolog.logdb)"""
|
||||
entry = Scolog(
|
||||
method=method,
|
||||
msg=msg,
|
||||
@ -46,21 +46,6 @@ class Scolog(ScoDocModel):
|
||||
if commit:
|
||||
db.session.commit()
|
||||
|
||||
def to_dict(self, convert_date=False) -> dict:
|
||||
"convert to dict"
|
||||
return {
|
||||
"etudid": self.etudid,
|
||||
"date": (
|
||||
(self.date.strftime(scu.DATETIME_FMT) if convert_date else self.date)
|
||||
if self.date
|
||||
else ""
|
||||
),
|
||||
"_date_order": self.date.isoformat() if self.date else "",
|
||||
"authenticated_user": self.authenticated_user or "",
|
||||
"msg": self.msg or "",
|
||||
"method": self.method or "",
|
||||
}
|
||||
|
||||
|
||||
class ScolarNews(db.Model):
|
||||
"""Nouvelles pour page d'accueil"""
|
||||
@ -92,9 +77,7 @@ class ScolarNews(db.Model):
|
||||
date = db.Column(
|
||||
db.DateTime(timezone=True), server_default=db.func.now(), index=True
|
||||
)
|
||||
authenticated_user = db.Column(
|
||||
db.Text, index=True
|
||||
) # user_name login, sans contrainte
|
||||
authenticated_user = db.Column(db.Text, index=True) # login, sans contrainte
|
||||
# type in 'INSCR', 'NOTES', 'FORM', 'SEM', 'MISC'
|
||||
type = db.Column(db.String(SHORT_STR_LEN), index=True)
|
||||
object = db.Column(
|
||||
@ -150,7 +133,7 @@ class ScolarNews(db.Model):
|
||||
return query.order_by(cls.date.desc()).limit(n).all()
|
||||
|
||||
@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
|
||||
Si max_frequency, ne génère pas 2 nouvelles "identiques"
|
||||
à 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).
|
||||
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:
|
||||
last_news = (
|
||||
cls.query.filter_by(
|
||||
dept_id=dept_id,
|
||||
dept_id=g.scodoc_dept_id,
|
||||
authenticated_user=current_user.user_name,
|
||||
type=typ,
|
||||
object=obj,
|
||||
@ -181,7 +163,7 @@ class ScolarNews(db.Model):
|
||||
return
|
||||
|
||||
news = ScolarNews(
|
||||
dept_id=dept_id,
|
||||
dept_id=g.scodoc_dept_id,
|
||||
authenticated_user=current_user.user_name,
|
||||
type=typ,
|
||||
object=obj,
|
||||
@ -198,7 +180,6 @@ class ScolarNews(db.Model):
|
||||
None si inexistant
|
||||
"""
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
|
||||
formsemestre_id = None
|
||||
if self.type == self.NEWS_INSCR:
|
||||
@ -250,9 +231,7 @@ class ScolarNews(db.Model):
|
||||
)
|
||||
|
||||
# Transforme les URL en URL absolues
|
||||
base = url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)[
|
||||
: -len("/index_html")
|
||||
]
|
||||
base = scu.ScoURL()
|
||||
txt = re.sub('href=/.*?"', 'href="' + base + "/", txt)
|
||||
|
||||
# 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)
|
||||
if not news_list:
|
||||
return ""
|
||||
dept_news_url = url_for("scolar.dept_news", scodoc_dept=g.scodoc_dept)
|
||||
H = [
|
||||
f"""<div class="scobox news"><div class="scobox-title"><a href="{
|
||||
dept_news_url
|
||||
f"""<div class="news"><span class="newstitle"><a href="{
|
||||
url_for("scolar.dept_news", scodoc_dept=g.scodoc_dept)
|
||||
}">Dernières opérations</a>
|
||||
</div><ul class="newslist">"""
|
||||
</span><ul class="newslist">"""
|
||||
]
|
||||
|
||||
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
|
||||
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
|
||||
H.append(
|
||||
f"""<div>
|
||||
Pour en savoir plus sur ScoDoc voir
|
||||
<a class="stdlink" href="{scu.SCO_ANNONCES_WEBSITE}">scodoc.org</a>
|
||||
f"""<div><a class="discretelink" href="{scu.SCO_ANNONCES_WEBSITE}">
|
||||
Pour en savoir plus sur ScoDoc voir le site scodoc.org</a>.
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
|
||||
H.append("</div>")
|
||||
return "\n".join(H)
|
||||
|
@ -1,13 +1,11 @@
|
||||
"""ScoDoc 9 models : Formations
|
||||
"""
|
||||
|
||||
from flask import abort, g
|
||||
from flask_sqlalchemy.query import Query
|
||||
|
||||
import app
|
||||
from app import db
|
||||
from app.comp import df_cache
|
||||
from app.models import ScoDocModel, SHORT_STR_LEN
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models.but_refcomp import (
|
||||
ApcAnneeParcours,
|
||||
ApcCompetence,
|
||||
@ -23,7 +21,7 @@ from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.codes_cursus import UE_STANDARD
|
||||
|
||||
|
||||
class Formation(ScoDocModel):
|
||||
class Formation(db.Model):
|
||||
"""Programme pédagogique d'une formation"""
|
||||
|
||||
__tablename__ = "notes_formations"
|
||||
@ -66,21 +64,6 @@ class Formation(ScoDocModel):
|
||||
"titre complet pour affichage"
|
||||
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):
|
||||
"""As a dict.
|
||||
Si with_refcomp_attrs, ajoute attributs permettant de retrouver le ref. de comp.
|
||||
@ -297,7 +280,7 @@ class Formation(ScoDocModel):
|
||||
db.session.commit()
|
||||
|
||||
|
||||
class Matiere(ScoDocModel):
|
||||
class Matiere(db.Model):
|
||||
"""Matières: regroupe les modules d'une UE
|
||||
La matière a peu d'utilité en dehors de la présentation des modules
|
||||
d'une UE.
|
||||
@ -313,7 +296,6 @@ class Matiere(ScoDocModel):
|
||||
numero = db.Column(db.Integer, nullable=False, default=0) # ordre de présentation
|
||||
|
||||
modules = db.relationship("Module", lazy="dynamic", backref="matiere")
|
||||
_sco_dept_relations = ("UniteEns", "Formation") # accès au dept_id
|
||||
|
||||
def __repr__(self):
|
||||
return f"""<{self.__class__.__name__}(id={self.id}, ue_id={
|
||||
|
@ -1,7 +1,7 @@
|
||||
# -*- coding: UTF-8 -*
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -10,22 +10,19 @@
|
||||
|
||||
"""ScoDoc models: formsemestre
|
||||
"""
|
||||
from collections import defaultdict
|
||||
import datetime
|
||||
from functools import cached_property
|
||||
from itertools import chain
|
||||
from operator import attrgetter
|
||||
|
||||
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 import func
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import db, log
|
||||
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.but_refcomp import (
|
||||
ApcParcours,
|
||||
@ -33,17 +30,11 @@ from app.models.but_refcomp import (
|
||||
parcours_formsemestre,
|
||||
)
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
from app.models.departements import Departement
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.evaluations import Evaluation
|
||||
from app.models.events import ScolarNews
|
||||
from app.models.formations import Formation
|
||||
from app.models.groups import GroupDescr, Partition
|
||||
from app.models.moduleimpls import (
|
||||
ModuleImpl,
|
||||
ModuleImplInscription,
|
||||
notes_modules_enseignants,
|
||||
)
|
||||
from app.models.moduleimpls import ModuleImpl, ModuleImplInscription
|
||||
from app.models.modules import Module
|
||||
from app.models.ues import UniteEns
|
||||
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_vdi import ApoEtapeVDI
|
||||
|
||||
from app.scodoc.sco_utils import translate_assiduites_metric
|
||||
|
||||
GROUPS_AUTO_ASSIGNMENT_DATA_MAX = 1024 * 1024 # bytes
|
||||
|
||||
|
||||
class FormSemestre(models.ScoDocModel):
|
||||
class FormSemestre(db.Model):
|
||||
"""Mise en oeuvre d'un semestre de formation"""
|
||||
|
||||
__tablename__ = "notes_formsemestre"
|
||||
@ -70,7 +63,7 @@ class FormSemestre(models.ScoDocModel):
|
||||
semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1")
|
||||
titre = db.Column(db.Text(), 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)
|
||||
"identifiant emplois du temps (unicité non imposée)"
|
||||
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(
|
||||
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(
|
||||
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"
|
||||
)
|
||||
"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(
|
||||
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"
|
||||
elt_sem_apo = db.Column(db.Text()) # peut être fort long !
|
||||
"code element semestre Apogée, eg 'VRTW1' ou 'V2INCS4,V2INLS4,...'"
|
||||
"code element semestre Apogee, eg 'VRTW1' ou 'V2INCS4,V2INLS4,...'"
|
||||
elt_annee_apo = db.Column(db.Text())
|
||||
"code element annee Apogee, eg 'VRT1A' ou 'V2INLA,V2INCA,...'"
|
||||
elt_passage_apo = db.Column(db.Text())
|
||||
"code element passage Apogée"
|
||||
|
||||
# Data pour groups_auto_assignment
|
||||
# (ce champ est utilisé uniquement via l'API par le front js)
|
||||
@ -193,15 +180,9 @@ class FormSemestre(models.ScoDocModel):
|
||||
|
||||
@classmethod
|
||||
def get_formsemestre(
|
||||
cls, formsemestre_id: int | str, dept_id: int = None
|
||||
cls, formsemestre_id: int, dept_id: int = None
|
||||
) -> "FormSemestre":
|
||||
"""FormSemestre ou 404, cherche uniquement dans le département spécifié
|
||||
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")
|
||||
""" "FormSemestre ou 404, cherche uniquement dans le département spécifié ou le courant"""
|
||||
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:
|
||||
@ -210,72 +191,8 @@ class FormSemestre(models.ScoDocModel):
|
||||
).first_or_404()
|
||||
return cls.query.filter_by(id=formsemestre_id).first_or_404()
|
||||
|
||||
@classmethod
|
||||
def create_formsemestre(cls, args: dict, silent=False) -> "FormSemestre":
|
||||
"""Création d'un formsemestre, avec toutes les valeurs par défaut
|
||||
et notification (sauf si silent).
|
||||
Crée la partition par défaut.
|
||||
"""
|
||||
# was sco_formsemestre.do_formsemestre_create
|
||||
if "dept_id" not in args:
|
||||
args["dept_id"] = g.scodoc_dept_id
|
||||
formsemestre: "FormSemestre" = cls.create_from_dict(args)
|
||||
db.session.flush()
|
||||
for etape in args["etapes"]:
|
||||
formsemestre.add_etape(etape)
|
||||
db.session.commit()
|
||||
for u in args["responsables"]:
|
||||
formsemestre.responsables.append(u)
|
||||
# create default partition
|
||||
partition = Partition(
|
||||
formsemestre=formsemestre, partition_name=None, numero=1000000
|
||||
)
|
||||
db.session.add(partition)
|
||||
partition.create_group(default=True)
|
||||
db.session.commit()
|
||||
|
||||
if not silent:
|
||||
url = url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=formsemestre.departement.acronym,
|
||||
formsemestre_id=formsemestre.id,
|
||||
)
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_SEM,
|
||||
text=f"""Création du semestre <a href="{url}">{formsemestre.titre}</a>""",
|
||||
url=url,
|
||||
max_frequency=0,
|
||||
)
|
||||
|
||||
return formsemestre
|
||||
|
||||
@classmethod
|
||||
def convert_dict_fields(cls, args: dict) -> dict:
|
||||
"""Convert fields in the given dict.
|
||||
args: dict with args in application.
|
||||
returns: dict to store in model's db.
|
||||
"""
|
||||
if "date_debut" in args:
|
||||
args["date_debut"] = scu.convert_fr_date(args["date_debut"])
|
||||
if "date_fin" in args:
|
||||
args["date_fin"] = scu.convert_fr_date(args["date_fin"])
|
||||
if "etat" in args:
|
||||
args["etat"] = bool(args["etat"])
|
||||
if "bul_bgcolor" in args:
|
||||
args["bul_bgcolor"] = args.get("bul_bgcolor") or "white"
|
||||
if "titre" in args:
|
||||
args["titre"] = args.get("titre") or "sans titre"
|
||||
return args
|
||||
|
||||
@classmethod
|
||||
def filter_model_attributes(cls, data: dict, excluded: set[str] = None) -> dict:
|
||||
"""Returns a copy of dict with only the keys belonging to the Model and not in excluded.
|
||||
Add 'etapes' to excluded."""
|
||||
# on ne peut pas affecter directement etapes
|
||||
return super().filter_model_attributes(data, (excluded or set()) | {"etapes"})
|
||||
|
||||
def sort_key(self) -> tuple:
|
||||
"""clé pour tris par ordre de date_debut, le plus ancien en tête
|
||||
"""clé pour tris par ordre alphabétique
|
||||
(pour avoir le plus récent d'abord, sort avec reverse=True)"""
|
||||
return (self.date_debut, self.semestre_id)
|
||||
|
||||
@ -291,12 +208,12 @@ class FormSemestre(models.ScoDocModel):
|
||||
d["formsemestre_id"] = self.id
|
||||
d["titre_num"] = self.titre_num()
|
||||
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()
|
||||
else:
|
||||
d["date_debut"] = d["date_debut_iso"] = ""
|
||||
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()
|
||||
else:
|
||||
d["date_fin"] = d["date_fin_iso"] = ""
|
||||
@ -314,20 +231,19 @@ class FormSemestre(models.ScoDocModel):
|
||||
|
||||
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.pop("_sa_instance_state", None)
|
||||
d.pop("groups_auto_assignment_data", None)
|
||||
d["annee_scolaire"] = self.annee_scolaire()
|
||||
d["bul_hide_xml"] = self.bul_hide_xml
|
||||
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()
|
||||
else:
|
||||
d["date_debut"] = d["date_debut_iso"] = ""
|
||||
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()
|
||||
else:
|
||||
d["date_fin"] = d["date_fin_iso"] = ""
|
||||
@ -353,15 +269,10 @@ class FormSemestre(models.ScoDocModel):
|
||||
return default_partition.groups.first()
|
||||
raise ScoValueError("Le semestre n'a pas de groupe par défaut")
|
||||
|
||||
def get_edt_ids(self) -> list[str]:
|
||||
"""Les ids pour l'emploi du temps: à défaut, les codes é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.
|
||||
"""
|
||||
def get_edt_id(self) -> str:
|
||||
"l'id pour l'emploi du temps: à défaut, le 1er code étape Apogée"
|
||||
return (
|
||||
scu.split_id(self.edt_id)
|
||||
or [e.etape_apo.strip() for e in self.etapes if e.etape_apo]
|
||||
or []
|
||||
self.edt_id or "" or (self.etapes[0].etape_apo if len(self.etapes) else "")
|
||||
)
|
||||
|
||||
def get_infos_dict(self) -> dict:
|
||||
@ -466,80 +377,6 @@ class FormSemestre(models.ScoDocModel):
|
||||
_cache[key] = 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]:
|
||||
"Liste de toutes les évaluations du semestre, triées par module/numero"
|
||||
return (
|
||||
@ -550,7 +387,7 @@ class FormSemestre(models.ScoDocModel):
|
||||
Module.numero,
|
||||
Module.code,
|
||||
Evaluation.numero,
|
||||
Evaluation.date_debut,
|
||||
Evaluation.date_debut.desc(),
|
||||
)
|
||||
.all()
|
||||
)
|
||||
@ -560,7 +397,6 @@ class FormSemestre(models.ScoDocModel):
|
||||
"""Liste des modimpls du semestre (y compris bonus)
|
||||
- triée par type/numéro/code en APC
|
||||
- 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()
|
||||
if self.formation.is_apc():
|
||||
@ -572,14 +408,6 @@ class FormSemestre(models.ScoDocModel):
|
||||
)
|
||||
)
|
||||
else:
|
||||
modimpls = [
|
||||
mi
|
||||
for mi in modimpls
|
||||
if (
|
||||
mi.module.module_type
|
||||
not in (scu.ModuleType.RESSOURCE, scu.ModuleType.SAE)
|
||||
)
|
||||
]
|
||||
modimpls.sort(
|
||||
key=lambda m: (
|
||||
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:
|
||||
"Vrai si dernier semestre de son cursus (ou formation mono-semestre)"
|
||||
return (self.semestre_id < 0) or (
|
||||
@ -726,7 +519,7 @@ class FormSemestre(models.ScoDocModel):
|
||||
mois_pivot_periode=scu.MONTH_DEBUT_PERIODE2,
|
||||
jour_pivot_annee=1,
|
||||
jour_pivot_periode=1,
|
||||
) -> tuple[int, int]:
|
||||
):
|
||||
"""Calcule la session associée à un formsemestre commençant en date_debut
|
||||
sous la forme (année, période)
|
||||
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(),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
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"]:
|
||||
def etapes_apo_vdi(self) -> list[ApoEtapeVDI]:
|
||||
"Liste des vdis"
|
||||
# was read_formsemestre_etapes
|
||||
return [e.as_apovdi() for e in self.etapes if e.etape_apo]
|
||||
@ -809,9 +582,9 @@ class FormSemestre(models.ScoDocModel):
|
||||
return ""
|
||||
return ", ".join(sorted([etape.etape_apo for etape in self.etapes if etape]))
|
||||
|
||||
def add_etape(self, etape_apo: str | ApoEtapeVDI):
|
||||
def add_etape(self, etape_apo: str):
|
||||
"Ajoute une étape"
|
||||
etape = FormSemestreEtape(formsemestre_id=self.id, etape_apo=str(etape_apo))
|
||||
etape = FormSemestreEtape(formsemestre_id=self.id, etape_apo=etape_apo)
|
||||
db.session.add(etape)
|
||||
|
||||
def regroupements_coherents_etud(self) -> list[tuple[UniteEns, UniteEns]]:
|
||||
@ -845,11 +618,11 @@ class FormSemestre(models.ScoDocModel):
|
||||
else:
|
||||
return ", ".join([u.get_nomcomplet() for u in self.responsables])
|
||||
|
||||
def est_responsable(self, user: User) -> bool:
|
||||
def est_responsable(self, user: User):
|
||||
"True si l'user est l'un des responsables du semestre"
|
||||
return user.id in [u.id for u in self.responsables]
|
||||
|
||||
def est_chef_or_diretud(self, user: User | None = None) -> bool:
|
||||
def est_chef_or_diretud(self, user: User = None):
|
||||
"Vrai si utilisateur (par def. current) est admin, chef dept ou responsable du semestre"
|
||||
user = user or current_user
|
||||
return user.has_permission(Permission.EditFormSemestre) or self.est_responsable(
|
||||
@ -858,7 +631,7 @@ class FormSemestre(models.ScoDocModel):
|
||||
|
||||
def can_change_groups(self, user: User = None) -> bool:
|
||||
"""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:
|
||||
return False # semestre verrouillé
|
||||
@ -867,7 +640,7 @@ class FormSemestre(models.ScoDocModel):
|
||||
return True # typiquement admin, chef dept
|
||||
return self.est_responsable(user)
|
||||
|
||||
def can_edit_jury(self, user: User | None = None):
|
||||
def can_edit_jury(self, user: User = None):
|
||||
"""Vrai si utilisateur (par def. current) peut saisir decision de jury
|
||||
dans ce semestre: vérifie permission et verrouillage.
|
||||
"""
|
||||
@ -978,9 +751,9 @@ class FormSemestre(models.ScoDocModel):
|
||||
descr_sem += " " + self.modalite
|
||||
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:
|
||||
tuple (nb abs non just, nb abs justifiées, nb abs total)
|
||||
tuple (nb abs, nb abs justifiées)
|
||||
Utilise un cache.
|
||||
"""
|
||||
from app.scodoc import sco_assiduites
|
||||
@ -995,12 +768,7 @@ class FormSemestre(models.ScoDocModel):
|
||||
|
||||
def get_codes_apogee(self, category=None) -> set[str]:
|
||||
"""Les codes Apogée (codés en base comme "VRT1,VRT2")
|
||||
category:
|
||||
None: tous,
|
||||
"etapes": étapes associées,
|
||||
"sem: code semestre"
|
||||
"annee": code annuel
|
||||
"passage": code passage
|
||||
category: None: tous, "etapes": étapes associées, "sem: code semestre", "annee": code annuel
|
||||
"""
|
||||
codes = set()
|
||||
if category is None or category == "etapes":
|
||||
@ -1009,8 +777,6 @@ class FormSemestre(models.ScoDocModel):
|
||||
codes |= {x.strip() for x in self.elt_sem_apo.split(",") if x}
|
||||
if (category is None or category == "annee") and self.elt_annee_apo:
|
||||
codes |= {x.strip() for x in self.elt_annee_apo.split(",") if x}
|
||||
if (category is None or category == "passage") and self.elt_passage_apo:
|
||||
codes |= {x.strip() for x in self.elt_passage_apo.split(",") if x}
|
||||
return codes
|
||||
|
||||
def get_inscrits(self, include_demdef=False, order=False) -> list[Identite]:
|
||||
@ -1045,16 +811,12 @@ class FormSemestre(models.ScoDocModel):
|
||||
partitions += [p for p in self.partitions if p.partition_name is None]
|
||||
return partitions
|
||||
|
||||
def etudids_actifs(self) -> tuple[list[int], set[int]]:
|
||||
"""Liste les etudids inscrits (incluant DEM et DEF),
|
||||
qui sera l'index des dataframes de notes
|
||||
et donne l'ensemble des inscrits non DEM ni DEF.
|
||||
"""
|
||||
return [inscr.etudid for inscr in self.inscriptions], {
|
||||
ins.etudid for ins in self.inscriptions if ins.etat == scu.INSCRIT
|
||||
}
|
||||
@cached_property
|
||||
def etudids_actifs(self) -> set:
|
||||
"Set des etudids inscrits non démissionnaires et non défaillants"
|
||||
return {ins.etudid for ins in self.inscriptions if ins.etat == scu.INSCRIT}
|
||||
|
||||
@property
|
||||
@cached_property
|
||||
def etuds_inscriptions(self) -> dict:
|
||||
"""Map { etudid : inscription } (incluant DEM et DEF)"""
|
||||
return {ins.etud.id: ins for ins in self.inscriptions}
|
||||
@ -1278,33 +1040,6 @@ class FormSemestre(models.ScoDocModel):
|
||||
nb_recorded += 1
|
||||
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
|
||||
notes_formsemestre_responsables = db.Table(
|
||||
@ -1334,18 +1069,10 @@ class FormSemestreEtape(db.Model):
|
||||
"Etape False if code empty"
|
||||
return self.etape_apo is not None and (len(self.etape_apo) > 0)
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, ApoEtapeVDI):
|
||||
return self.as_apovdi() == other
|
||||
return str(self) == str(other)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Etape {self.id} apo={self.etape_apo!r}>"
|
||||
|
||||
def __str__(self):
|
||||
return self.etape_apo or ""
|
||||
|
||||
def as_apovdi(self) -> "ApoEtapeVDI":
|
||||
def as_apovdi(self) -> ApoEtapeVDI:
|
||||
return ApoEtapeVDI(self.etape_apo)
|
||||
|
||||
|
||||
@ -1498,9 +1225,8 @@ class FormSemestreInscription(db.Model):
|
||||
|
||||
def __repr__(self):
|
||||
return f"""<{self.__class__.__name__} {self.id} etudid={self.etudid} sem={
|
||||
self.formsemestre_id} (S{self.formsemestre.semestre_id}) etat={self.etat} {
|
||||
('parcours="'+str(self.parcour.code)+'"') if self.parcour else ''
|
||||
} {('etape="'+self.etape+'"') if self.etape else ''}>"""
|
||||
self.formsemestre_id} etat={self.etat} {
|
||||
('parcours='+str(self.parcour)) if self.parcour else ''}>"""
|
||||
|
||||
|
||||
class NotesSemSet(db.Model):
|
||||
|
@ -1,7 +1,7 @@
|
||||
# -*- coding: UTF-8 -*
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -11,15 +11,14 @@ from operator import attrgetter
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
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.events import Scolog
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||
|
||||
|
||||
class Partition(ScoDocModel):
|
||||
class Partition(db.Model):
|
||||
"""Partition: découpage d'une promotion en groupes"""
|
||||
|
||||
__table_args__ = (db.UniqueConstraint("formsemestre_id", "partition_name"),)
|
||||
@ -54,7 +53,6 @@ class Partition(ScoDocModel):
|
||||
cascade="all, delete-orphan",
|
||||
order_by="GroupDescr.numero, GroupDescr.group_name",
|
||||
)
|
||||
_sco_dept_relations = ("FormSemestre",)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(Partition, self).__init__(**kwargs)
|
||||
@ -94,10 +92,6 @@ class Partition(ScoDocModel):
|
||||
):
|
||||
group.remove_etud(etud)
|
||||
|
||||
def is_default(self) -> bool:
|
||||
"vrai si partition par défault (tous les étudiants)"
|
||||
return not self.partition_name
|
||||
|
||||
def is_parcours(self) -> bool:
|
||||
"Vrai s'il s'agit de la partition de parcours"
|
||||
return self.partition_name == scu.PARTITION_PARCOURS
|
||||
@ -210,7 +204,7 @@ class Partition(ScoDocModel):
|
||||
return group
|
||||
|
||||
|
||||
class GroupDescr(ScoDocModel):
|
||||
class GroupDescr(db.Model):
|
||||
"""Description d'un groupe d'une partition"""
|
||||
|
||||
__tablename__ = "group_descr"
|
||||
@ -226,11 +220,6 @@ class GroupDescr(ScoDocModel):
|
||||
numero = db.Column(db.Integer, nullable=False, default=0)
|
||||
"Numero = ordre de presentation"
|
||||
|
||||
_sco_dept_relations = (
|
||||
"Partition",
|
||||
"FormSemestre",
|
||||
)
|
||||
|
||||
etuds = db.relationship(
|
||||
"Identite",
|
||||
secondary="group_membership",
|
||||
@ -252,20 +241,15 @@ class GroupDescr(ScoDocModel):
|
||||
|
||||
def to_dict(self, with_partition=True) -> dict:
|
||||
"""as a dict, with or without partition"""
|
||||
if with_partition:
|
||||
partition_dict = self.partition.to_dict(with_groups=False)
|
||||
d = dict(self.__dict__)
|
||||
d.pop("_sa_instance_state", None)
|
||||
if with_partition:
|
||||
d["partition"] = partition_dict
|
||||
d["partition"] = self.partition.to_dict(with_groups=False)
|
||||
return d
|
||||
|
||||
def get_edt_ids(self) -> list[str]:
|
||||
"les ids normalisés pour l'emploi du temps: à défaut, le nom scodoc du groupe"
|
||||
return [
|
||||
scu.normalize_edt_id(x)
|
||||
for x in scu.split_id(self.edt_id) or [self.group_name] or []
|
||||
]
|
||||
def get_edt_id(self) -> str:
|
||||
"l'id pour l'emploi du temps: à défaut, le nom scodoc du groupe"
|
||||
return self.edt_id or self.group_name or ""
|
||||
|
||||
def get_nb_inscrits(self) -> int:
|
||||
"""Nombre inscrits à ce group et au formsemestre.
|
||||
@ -298,18 +282,18 @@ class GroupDescr(ScoDocModel):
|
||||
return False
|
||||
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.
|
||||
Check permission (partition must be groups_editable)
|
||||
and invalidate caches. Commit session.
|
||||
Check permission and invalidate caches. Commit session.
|
||||
dest_url is used for error messages.
|
||||
"""
|
||||
if not self.partition.formsemestre.can_change_groups():
|
||||
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
||||
if self.group_name is None:
|
||||
raise ValueError("can't set a name to default group")
|
||||
if not self.partition.groups_editable:
|
||||
raise AccessDenied("Partition non éditable")
|
||||
|
||||
if group_name:
|
||||
group_name = group_name.strip()
|
||||
if not group_name:
|
||||
@ -322,32 +306,27 @@ class GroupDescr(ScoDocModel):
|
||||
)
|
||||
|
||||
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.commit()
|
||||
sco_cache.invalidate_formsemestre(
|
||||
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"):
|
||||
"Enlève l'étudiant de ce groupe s'il en fait partie (ne fait rien sinon)"
|
||||
if etud in self.etuds:
|
||||
self.etuds.remove(etud)
|
||||
db.session.commit()
|
||||
Scolog.logdb(
|
||||
method="group_remove_etud",
|
||||
etudid=etud.id,
|
||||
msg=f"Retrait du groupe {self.group_name} de {self.partition.partition_name}",
|
||||
commit=True,
|
||||
)
|
||||
db.session.commit()
|
||||
# Update parcours
|
||||
if self.partition.partition_name == scu.PARTITION_PARCOURS:
|
||||
self.partition.formsemestre.update_inscriptions_parcours_from_groups(
|
||||
|
@ -2,24 +2,20 @@
|
||||
"""ScoDoc models: moduleimpls
|
||||
"""
|
||||
import pandas as pd
|
||||
from flask import abort, g
|
||||
from flask_login import current_user
|
||||
from flask_sqlalchemy.query import Query
|
||||
|
||||
import app
|
||||
from app import db
|
||||
from app.auth.models import User
|
||||
from app.comp import df_cache
|
||||
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.evaluations import Evaluation
|
||||
from app.models.modules import Module
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoLockedSemError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
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"""
|
||||
|
||||
__tablename__ = "notes_moduleimpl"
|
||||
@ -38,29 +34,18 @@ class ModuleImpl(ScoDocModel):
|
||||
index=True,
|
||||
nullable=False,
|
||||
)
|
||||
responsable_id = db.Column(
|
||||
"responsable_id", db.Integer, db.ForeignKey("user.id", ondelete="SET NULL")
|
||||
)
|
||||
responsable = db.relationship("User", back_populates="modimpls")
|
||||
responsable_id = db.Column("responsable_id", db.Integer, db.ForeignKey("user.id"))
|
||||
# formule de calcul moyenne:
|
||||
computation_expr = db.Column(db.Text())
|
||||
|
||||
evaluations = db.relationship(
|
||||
"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."
|
||||
evaluations = db.relationship("Evaluation", lazy="dynamic", backref="moduleimpl")
|
||||
enseignants = db.relationship(
|
||||
"User",
|
||||
secondary="notes_modules_enseignants",
|
||||
lazy="dynamic",
|
||||
backref="moduleimpl",
|
||||
viewonly=True,
|
||||
)
|
||||
"enseignants du module (sans le responsable)"
|
||||
|
||||
_sco_dept_relations = ("FormSemestre",) # accès au dept_id
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} {self.id} module={repr(self.module)}>"
|
||||
@ -73,17 +58,16 @@ class ModuleImpl(ScoDocModel):
|
||||
return {x.strip() for x in self.code_apogee.split(",") if x}
|
||||
return self.module.get_codes_apogee()
|
||||
|
||||
def get_edt_ids(self) -> list[str]:
|
||||
"les ids pour l'emploi du temps: à défaut, les codes Apogée"
|
||||
return [
|
||||
scu.normalize_edt_id(x)
|
||||
for x in scu.split_id(self.edt_id) or scu.split_id(self.code_apogee)
|
||||
] or self.module.get_edt_ids()
|
||||
def get_edt_id(self) -> str:
|
||||
"l'id pour l'emploi du temps: à défaut, le 1er code Apogée"
|
||||
return (
|
||||
self.edt_id
|
||||
or (self.code_apogee.split(",")[0] if self.code_apogee else "")
|
||||
or self.module.get_edt_id()
|
||||
)
|
||||
|
||||
def get_evaluations_poids(self) -> pd.DataFrame:
|
||||
"""Les poids des évaluations vers les UEs (accès via cache redis).
|
||||
Toutes les évaluations sont considérées (normales, bonus, rattr., etc.)
|
||||
"""
|
||||
"""Les poids des évaluations vers les UE (accès via cache)"""
|
||||
evaluations_poids = df_cache.EvaluationsPoidsCache.get(self.id)
|
||||
if evaluations_poids is None:
|
||||
from app.comp import moy_mod
|
||||
@ -92,58 +76,24 @@ class ModuleImpl(ScoDocModel):
|
||||
df_cache.EvaluationsPoidsCache.set(self.id, 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):
|
||||
"""Invalide poids cachés"""
|
||||
df_cache.EvaluationsPoidsCache.delete(self.id)
|
||||
|
||||
def check_apc_conformity(
|
||||
self, res: "ResultatsSemestreBUT", evaluation_type=Evaluation.EVALUATION_NORMALE
|
||||
) -> bool:
|
||||
"""true si les poids des évaluations du type indiqué (normales par défaut)
|
||||
du module permettent de satisfaire les coefficients du PN.
|
||||
def check_apc_conformity(self, res: "ResultatsSemestreBUT") -> bool:
|
||||
"""true si les poids des évaluations du module permettent de satisfaire
|
||||
les coefficients du PN.
|
||||
"""
|
||||
# appelé par formsemestre_status, liste notes, et moduleimpl_status
|
||||
if not self.module.formation.get_cursus().APC_SAE or (
|
||||
self.module.module_type
|
||||
not in {scu.ModuleType.RESSOURCE, scu.ModuleType.SAE}
|
||||
self.module.module_type != scu.ModuleType.RESSOURCE
|
||||
and self.module.module_type != scu.ModuleType.SAE
|
||||
):
|
||||
return True # Non BUT, toujours conforme
|
||||
from app.comp import moy_mod
|
||||
|
||||
mod_results = res.modimpls_results.get(self.id)
|
||||
if mod_results is None:
|
||||
app.critical_error("check_apc_conformity: err 1")
|
||||
|
||||
selected_evaluations_ids = [
|
||||
eval_id
|
||||
for eval_id, eval_type in mod_results.evals_type.items()
|
||||
if eval_type == evaluation_type
|
||||
]
|
||||
if not selected_evaluations_ids:
|
||||
return True # conforme si pas d'évaluations
|
||||
selected_evaluations_poids = self.get_evaluations_poids().loc[
|
||||
selected_evaluations_ids
|
||||
]
|
||||
return moy_mod.moduleimpl_is_conforme(
|
||||
self,
|
||||
selected_evaluations_poids,
|
||||
self.get_evaluations_poids(),
|
||||
res.modimpl_coefs_df,
|
||||
)
|
||||
|
||||
@ -213,7 +163,7 @@ class ModuleImpl(ScoDocModel):
|
||||
return allow_ens and user.id in (ens.id for ens in self.enseignants)
|
||||
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.
|
||||
If raise_exc, raises exception (AccessDenied or ScoLockedSemError) if not.
|
||||
= Admin, et dir des etud. (si option l'y autorise)
|
||||
@ -234,81 +184,21 @@ class ModuleImpl(ScoDocModel):
|
||||
raise AccessDenied(f"Modification impossible pour {user}")
|
||||
return False
|
||||
|
||||
def can_change_ens(self, user: User | None = None, raise_exc=True) -> bool:
|
||||
"""check if user can modify ens list (raise exception if not)"
|
||||
if user is None, current user.
|
||||
def est_inscrit(self, etud: Identite) -> bool:
|
||||
"""
|
||||
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
|
||||
# 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
|
||||
Vérifie si l'étudiant est bien inscrit au moduleimpl
|
||||
|
||||
def can_change_inscriptions(self, user: User | None = None, raise_exc=True) -> bool:
|
||||
"""check si user peut inscrire/désinsincrire des étudiants à ce module.
|
||||
Autorise ScoEtudInscrit ou responsables semestre.
|
||||
Retourne Vrai si c'est le cas, faux sinon
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Vérifie si l'étudiant est bien inscrit au moduleimpl (même si DEM ou DEF au semestre).
|
||||
(lent, pas de cache: pour un accès rapide, utiliser nt.modimpl_inscr_df).
|
||||
Retourne ModuleImplInscription si inscrit au module, False sinon.
|
||||
"""
|
||||
# vérifie inscrit au moduleimpl ET au formsemestre
|
||||
from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
||||
|
||||
inscription = (
|
||||
ModuleImplInscription.query.filter_by(etudid=etud.id, moduleimpl_id=self.id)
|
||||
.join(ModuleImpl)
|
||||
.join(FormSemestre)
|
||||
.join(FormSemestreInscription)
|
||||
.filter_by(etudid=etud.id)
|
||||
.first()
|
||||
is_module: int = (
|
||||
ModuleImplInscription.query.filter_by(
|
||||
etudid=etud.id, moduleimpl_id=self.id
|
||||
).count()
|
||||
> 0
|
||||
)
|
||||
|
||||
return inscription or False
|
||||
|
||||
def query_inscriptions(self) -> Query:
|
||||
"""Query ModuleImplInscription: inscrits au moduleimpl et au formsemestre
|
||||
(pas de cache: pour un accès rapide, utiliser nt.modimpl_inscr_df).
|
||||
"""
|
||||
from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
||||
|
||||
return (
|
||||
ModuleImplInscription.query.filter_by(moduleimpl_id=self.id)
|
||||
.join(ModuleImpl)
|
||||
.join(FormSemestre)
|
||||
.join(FormSemestreInscription)
|
||||
.filter_by(etudid=ModuleImplInscription.etudid)
|
||||
)
|
||||
return is_module
|
||||
|
||||
|
||||
# Enseignants (chargés de TD ou TP) d'un moduleimpl
|
||||
|
@ -1,24 +1,17 @@
|
||||
"""ScoDoc 9 models : Modules
|
||||
"""
|
||||
|
||||
from flask import current_app, g
|
||||
from flask import current_app
|
||||
|
||||
from app import db
|
||||
from app import models
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models.but_refcomp import (
|
||||
ApcParcours,
|
||||
ApcReferentielCompetences,
|
||||
app_critiques_modules,
|
||||
parcours_modules,
|
||||
)
|
||||
from app.models.but_refcomp import ApcParcours, app_critiques_modules, parcours_modules
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.codes_cursus import UE_SPORT
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
|
||||
class Module(models.ScoDocModel):
|
||||
class Module(db.Model):
|
||||
"""Module"""
|
||||
|
||||
__tablename__ = "notes_modules"
|
||||
@ -29,7 +22,6 @@ class Module(models.ScoDocModel):
|
||||
abbrev = db.Column(db.Text()) # nom court
|
||||
# certains départements ont des codes infiniment longs: donc Text !
|
||||
code = db.Column(db.Text(), nullable=False)
|
||||
"code module, chaine non nullable"
|
||||
heures_cours = db.Column(db.Float)
|
||||
heures_td = db.Column(db.Float)
|
||||
heures_tp = db.Column(db.Float)
|
||||
@ -75,8 +67,6 @@ class Module(models.ScoDocModel):
|
||||
backref=db.backref("modules", lazy=True),
|
||||
)
|
||||
|
||||
_sco_dept_relations = "Formation" # accès au dept_id
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.ue_coefs = []
|
||||
super(Module, self).__init__(**kwargs)
|
||||
@ -85,100 +75,6 @@ class Module(models.ScoDocModel):
|
||||
return f"""<Module{ModuleType(self.module_type or ModuleType.STANDARD).name
|
||||
} 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):
|
||||
"""Create a new copy of this module."""
|
||||
mod = Module(
|
||||
@ -210,29 +106,16 @@ class Module(models.ScoDocModel):
|
||||
mod.app_critiques.append(app_critique)
|
||||
return mod
|
||||
|
||||
def to_dict(
|
||||
self,
|
||||
convert_objects=False,
|
||||
with_matiere=False,
|
||||
with_ue=False,
|
||||
with_parcours_ids=False,
|
||||
) -> dict:
|
||||
def to_dict(self, convert_objects=False, with_matiere=False, with_ue=False) -> dict:
|
||||
"""If convert_objects, convert all attributes to native types
|
||||
(suitable jor json encoding).
|
||||
If convert_objects and with_parcours_ids, give parcours as a list of id (API)
|
||||
"""
|
||||
d = dict(self.__dict__)
|
||||
d.pop("_sa_instance_state", None)
|
||||
d.pop("formation", None)
|
||||
if convert_objects:
|
||||
if with_parcours_ids:
|
||||
d["parcours"] = [p.id for p in self.parcours]
|
||||
else:
|
||||
d["parcours"] = [p.to_dict() for p in self.parcours]
|
||||
d["parcours"] = [p.to_dict() for p in self.parcours]
|
||||
d["ue_coefs"] = [
|
||||
c.to_dict(convert_objects=False)
|
||||
for c in self.ue_coefs
|
||||
# note: don't convert_objects: we do wan't the details of the UEs here
|
||||
c.to_dict(convert_objects=convert_objects) for c in self.ue_coefs
|
||||
]
|
||||
d["app_critiques"] = {x.code: x.to_dict() for x in self.app_critiques}
|
||||
if not with_matiere:
|
||||
@ -276,10 +159,6 @@ class Module(models.ScoDocModel):
|
||||
"Identifiant du module à afficher : abbrev ou titre ou 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:
|
||||
"""Clé de tri pour avoir
|
||||
présentation par type (res, sae), parcours, type, numéro
|
||||
@ -400,33 +279,19 @@ class Module(models.ScoDocModel):
|
||||
# Liste seulement les coefs définis:
|
||||
return [(c.ue, c.coef) for c in self.get_ue_coefs_sorted()]
|
||||
|
||||
def get_ue_coefs_descr(self) -> str:
|
||||
"""Description des coefficients vers les UEs (APC)"""
|
||||
coefs_descr = ", ".join(
|
||||
[
|
||||
f"{ue.acronyme}: {co}"
|
||||
for ue, co in self.ue_coefs_list()
|
||||
if isinstance(co, float) and co > 0
|
||||
]
|
||||
)
|
||||
if coefs_descr:
|
||||
descr = "Coefs: " + coefs_descr
|
||||
else:
|
||||
descr = "(pas de coefficients) "
|
||||
return descr
|
||||
|
||||
def get_codes_apogee(self) -> set[str]:
|
||||
"""Les codes Apogée (codés en base comme "VRT1,VRT2")"""
|
||||
if self.code_apogee:
|
||||
return {x.strip() for x in self.code_apogee.split(",") if x}
|
||||
return set()
|
||||
|
||||
def get_edt_ids(self) -> list[str]:
|
||||
"les ids pour l'emploi du temps: à défaut, le 1er code Apogée"
|
||||
return [
|
||||
scu.normalize_edt_id(x)
|
||||
for x in scu.split_id(self.edt_id) or scu.split_id(self.code_apogee) or []
|
||||
]
|
||||
def get_edt_id(self) -> str:
|
||||
"l'id pour l'emploi du temps: à défaut, le 1er code Apogée"
|
||||
return (
|
||||
self.edt_id
|
||||
or (self.code_apogee.split(",")[0] if self.code_apogee else "")
|
||||
or ""
|
||||
)
|
||||
|
||||
def get_parcours(self) -> list[ApcParcours]:
|
||||
"""Les parcours utilisant ce module.
|
||||
@ -441,14 +306,6 @@ class Module(models.ScoDocModel):
|
||||
return []
|
||||
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):
|
||||
"""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)
|
||||
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
|
||||
notes_modules_tags = db.Table(
|
||||
|
@ -5,12 +5,11 @@
|
||||
|
||||
import sqlalchemy as sa
|
||||
from app import db
|
||||
from app import models
|
||||
from app.scodoc import safehtml
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
|
||||
class BulAppreciations(models.ScoDocModel):
|
||||
class BulAppreciations(db.Model):
|
||||
"""Appréciations sur bulletins"""
|
||||
|
||||
__tablename__ = "notes_appreciations"
|
||||
@ -28,8 +27,6 @@ class BulAppreciations(models.ScoDocModel):
|
||||
author = db.Column(db.Text) # le pseudo (user_name), sans contrainte
|
||||
comment = db.Column(db.Text) # texte libre
|
||||
|
||||
_sco_dept_relations = ("Identite",) # accès au dept_id
|
||||
|
||||
@classmethod
|
||||
def get_appreciations_list(
|
||||
cls, formsemestre_id: int, etudid: int
|
||||
|
@ -3,10 +3,10 @@
|
||||
"""Model : preferences
|
||||
"""
|
||||
|
||||
from app import db, models
|
||||
from app import db
|
||||
|
||||
|
||||
class ScoPreference(models.ScoDocModel):
|
||||
class ScoPreference(db.Model):
|
||||
"""ScoDoc preferences (par département)"""
|
||||
|
||||
__tablename__ = "sco_prefs"
|
||||
@ -19,8 +19,5 @@ class ScoPreference(models.ScoDocModel):
|
||||
value = db.Column(db.Text())
|
||||
formsemestre_id = db.Column(db.Integer, db.ForeignKey("notes_formsemestre.id"))
|
||||
|
||||
_sco_dept_relations = ("FormSemestre",) # accès au dept_id
|
||||
|
||||
def __repr__(self):
|
||||
return f"""<{self.__class__.__name__} {self.id} {self.departement.acronym
|
||||
} {self.name}={self.value}>"""
|
||||
return f"<{self.__class__.__name__} {self.id} {self.departement.acronym} {self.name}={self.value}>"
|
||||
|
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