forked from ScoDoc/ScoDoc
Compare commits
274 Commits
master
...
iziram-rev
Author | SHA1 | Date | |
---|---|---|---|
8ebe3fa6f3 | |||
|
21f57aab8f | ||
|
53c9658ce1 | ||
|
e18990d804 | ||
|
c11599b64f | ||
|
095eb6ce20 | ||
5a58346282 | |||
|
61d4186ad3 | ||
2fc978e515 | |||
|
4d72fec42d | ||
a63e14ce06 | |||
ba909d72f0 | |||
44a72c1ab9 | |||
|
2fd1b039f4 | ||
0f8998c891 | |||
be367de2a1 | |||
e81ad610b6 | |||
0f45101000 | |||
1224b46846 | |||
6b2ea5c5bc | |||
|
a7b856b1ec | ||
338c24a9c1 | |||
43849007fb | |||
|
547040bb93 | ||
|
8bc780f2cf | ||
86f5751e79 | |||
b160f64e4f | |||
ee2ac9d986 | |||
fc78484186 | |||
21b5474a6f | |||
3c1acc9c00 | |||
2548a97515 | |||
ce1cb7516b | |||
|
cf3258f5f9 | ||
3998b5a366 | |||
728010bf69 | |||
e9f23d8b3e | |||
f6d442beb4 | |||
b19c94a1f4 | |||
9fb70aef5d | |||
c8c05ecd77 | |||
efa8f617bb | |||
|
02ec55ca18 | ||
b30d5eb996 | |||
e33bc1e303 | |||
d2362c1080 | |||
6e4bf424e5 | |||
2170b06e33 | |||
696f4a5410 | |||
9880645c01 | |||
4d0ea06559 | |||
f46e3f6db5 | |||
4d66fb13ee | |||
138f9597f5 | |||
91e8c9185b | |||
f3b2c6d4fe | |||
7277c9f999 | |||
9a19919bae | |||
d97c0c08aa | |||
325978a175 | |||
135ca9fc1c | |||
a4072efe4c | |||
4430eb9a61 | |||
073c3c7c44 | |||
75b87b24de | |||
e0f6b022b1 | |||
98c6761f6a | |||
53514ef919 | |||
294ce1d708 | |||
cf63e1c038 | |||
584a7af2a1 | |||
635320fd62 | |||
6867974957 | |||
6ad415dfca | |||
2919ff517c | |||
89948db135 | |||
bdf90dfd69 | |||
0b9c9be222 | |||
b5cf210112 | |||
6833a28274 | |||
5753ac92f4 | |||
16cc35f63c | |||
5e0922a4bf | |||
10148bc7c0 | |||
556d8e7cbf | |||
c6b2af5635 | |||
cc0c544519 | |||
71116e6b39 | |||
3121a6d54c | |||
83afc1d6a0 | |||
5fc08b9716 | |||
e7559b7a78 | |||
d8a98b6e5b | |||
1287aecc4b | |||
549323e781 | |||
0ff5fa46d9 | |||
8d124eca3e | |||
6a7638d7ff | |||
452bbf2885 | |||
4915852d66 | |||
85f00c7cb6 | |||
b8b3fbb324 | |||
dd93d952d7 | |||
00c09b1eb8 | |||
0e628273cf | |||
664d5483fc | |||
b4eab5fcbc | |||
c551634417 | |||
efe8673e8a | |||
f17b10da3b | |||
cf18520e9c | |||
cda20c27b2 | |||
b7983a8d59 | |||
47b3eec14b | |||
9f6068caa2 | |||
04277d1f57 | |||
f9d15da553 | |||
a7126990f0 | |||
42d92cb998 | |||
2d3d7d49fc | |||
277e87add9 | |||
fffb07d612 | |||
afe9ae69a9 | |||
16f953caf6 | |||
|
f3b1b8a3cb | ||
7adc7d824b | |||
cf900d2027 | |||
1fa8375b11 | |||
bdefa111a7 | |||
c5b2df379e | |||
3460b217dd | |||
18aed44644 | |||
ec632dd43c | |||
acc1ecf906 | |||
9566551e7e | |||
7e1b0177f0 | |||
8e6dc37a87 | |||
a4840f494b | |||
a28f58a443 | |||
2a41cf972c | |||
e2ca9d417f | |||
|
f96571f520 | ||
|
4df1bdda8e | ||
6c88dfa722 | |||
|
b9f3db91d4 | ||
3e2631b94d | |||
9251810814 | |||
4c83c69f7c | |||
b09dc63fe3 | |||
075d864de3 | |||
6440ca4a1f | |||
7d2d19f3a8 | |||
882d131837 | |||
3012fc465d | |||
7069fb6e31 | |||
be2d7926bf | |||
bc6d9d5442 | |||
a0a6dbea00 | |||
872e741d9f | |||
5258a570a6 | |||
f0da8434a9 | |||
e995228ca7 | |||
e59fce5f6b | |||
0d9338dc0a | |||
f1fd4d98d7 | |||
c6e35dd4cd | |||
cb8d313dc7 | |||
3e3b09134d | |||
7b3c50620b | |||
63fb09348d | |||
7a9dc11af3 | |||
d178c636bf | |||
c6a06266fa | |||
e1504adc03 | |||
|
a9615bc077 | ||
|
3ff4abd19c | ||
0a1a847044 | |||
f5442b924f | |||
bec4cd7978 | |||
f63fa43862 | |||
ca20c303f0 | |||
014886c288 | |||
e2110f4abb | |||
ff12f4312e | |||
930a96b984 | |||
60fa12df81 | |||
0324771aa2 | |||
688fc5401f | |||
c1cbd6bce0 | |||
f2ffd69fe6 | |||
ba5b5cdb6f | |||
51b0ca088c | |||
9c618692d1 | |||
a0c33b3c19 | |||
ef1b28fe27 | |||
f246d9e82c | |||
cd36737460 | |||
acb8e6aab2 | |||
8ef19b14c7 | |||
7af381becc | |||
c9b4058717 | |||
42b03dbdfa | |||
a87dbd9927 | |||
ae9aad0619 | |||
e38d4bde81 | |||
25d1132a06 | |||
4c730a6302 | |||
287e4df74e | |||
77348c2cdf | |||
f67a11519e | |||
f9a9c2088d | |||
afbb1fb0e2 | |||
346701d91e | |||
f647ff1139 | |||
f5988b9e34 | |||
f318f35c1b | |||
4626cb9a3e | |||
0a58437fa9 | |||
c906cd7f16 | |||
ee86fba3d3 | |||
5018298d12 | |||
9d64caa749 | |||
6f257dc80d | |||
49d176c603 | |||
559b66de8b | |||
d7f1114a42 | |||
a2ea7d7a02 | |||
f6d8de5a20 | |||
2bf678ac50 | |||
|
63c0667694 | ||
|
3f7f4172b5 | ||
528d5c8863 | |||
7b28e0ba6b | |||
588f2f26eb | |||
4940decf57 | |||
d055c17c6b | |||
ea0a49d837 | |||
59a6ee3b3e | |||
111634db99 | |||
26dcc31ffb | |||
18f4b9cd42 | |||
dbc9aab7c3 | |||
|
9c50d03dd8 | ||
2d76cc0ad1 | |||
|
b7fb8879df | ||
3e0f43d5ea | |||
dcdd83d2e8 | |||
4d453d5d14 | |||
20b13b05cf | |||
a730bf759b | |||
|
a63349382e | ||
dab6bad08f | |||
e435dd10db | |||
95100ed429 | |||
155a093635 | |||
|
c85a51a8c5 | ||
d50107079b | |||
9535ff1e91 | |||
f4d8f4dded | |||
ba003d7c02 | |||
7ca3290357 | |||
7cb98e3f31 | |||
365e54f7e1 | |||
9da5506361 | |||
979359257b | |||
cc674b4e65 | |||
c103111aa1 | |||
b9d6688250 | |||
93e54982b6 | |||
eefdd5458e | |||
072d839b75 | |||
0de35b5400 | |||
cfcb100ab7 | |||
|
1c48758940 |
@ -1,4 +1,8 @@
|
||||
[[MESSAGES CONTROL]
|
||||
|
||||
[MASTER]
|
||||
load-plugins=pylint_flask_sqlalchemy,pylint_flask
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
# pylint and black disagree...
|
||||
disable=bad-continuation
|
||||
|
||||
|
41
README.md
41
README.md
@ -1,4 +1,3 @@
|
||||
i
|
||||
# ScoDoc - Gestion de la scolarité - Version ScoDoc 9
|
||||
|
||||
(c) Emmanuel Viennet 1999 - 2022 (voir LICENCE.txt).
|
||||
@ -9,29 +8,23 @@ Documentation utilisateur: <https://scodoc.org>
|
||||
|
||||
## Version ScoDoc 9
|
||||
|
||||
La version ScoDoc 9 est parue en septembre 2021.
|
||||
Elle représente une évolution majeure du projet, maintenant basé sur
|
||||
Flask (au lieu de Zope) et sur **python 3.9+**.
|
||||
La version ScoDoc 9 est parue en septembre 2021. Elle représente une évolution
|
||||
majeure du projet, maintenant basé sur Flask (au lieu de Zope) et sur **python
|
||||
3.9+**.
|
||||
|
||||
La version 9.0 s'efforce de reproduire presque à l'identique le fonctionnement
|
||||
de ScoDoc7, avec des composants logiciels différents (Debian 11, Python 3,
|
||||
Flask, SQLAlchemy, au lien de Python2/Zope dans les versions précédentes).
|
||||
|
||||
### État actuel (dec 22)
|
||||
|
||||
|
||||
### État actuel (26 jan 22)
|
||||
|
||||
- 9.1.5x (master) reproduit l'ensemble des fonctions de ScoDoc 7 (donc pas de BUT), sauf:
|
||||
- ancien module "Entreprises" (obsolète) et ajoute la gestion du BUT.
|
||||
|
||||
- 9.2 (branche dev92) est la version de développement.
|
||||
|
||||
- 9.4.x est en production
|
||||
- le prochain jalon est 9.5. Voir branches sur gitea.
|
||||
|
||||
### Lignes de commandes
|
||||
|
||||
Voir [https://scodoc.org/GuideConfig](le guide de configuration).
|
||||
|
||||
|
||||
## Organisation des fichiers
|
||||
|
||||
L'installation comporte les fichiers de l'application, sous `/opt/scodoc/`, et
|
||||
@ -40,6 +33,7 @@ les fichiers locaux (archives, photos, configurations, logs) sous
|
||||
postgresql et la configuration du système Linux.
|
||||
|
||||
### Fichiers locaux
|
||||
|
||||
Sous `/opt/scodoc-data`, fichiers et répertoires appartenant à l'utilisateur `scodoc`.
|
||||
Ils ne doivent pas être modifiés à la main, sauf certains fichiers de configuration sous
|
||||
`/opt/scodoc-data/config`.
|
||||
@ -107,7 +101,7 @@ Certains tests ont besoin d'un département déjà créé, qui n'est pas créé
|
||||
scripts de tests:
|
||||
Lancer au préalable:
|
||||
|
||||
flask delete-dept TEST00 && flask create-dept TEST00
|
||||
flask delete-dept -fy TEST00 && flask create-dept TEST00
|
||||
|
||||
Puis dérouler les tests unitaires:
|
||||
|
||||
@ -117,14 +111,14 @@ Ou avec couverture (`pip install pytest-cov`)
|
||||
|
||||
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 développement dans un état connu, par exemple pour éviter de
|
||||
recréer à la main étudiants et semestres quand on développe.
|
||||
|
||||
Il suffit de positionner une variable d'environnement indiquant la BD
|
||||
utilisée par les tests:
|
||||
On peut aussi utiliser les tests unitaires pour mettre la base de données de
|
||||
développement dans un état connu, par exemple pour éviter de recréer à la main
|
||||
étudiants et semestres quand on développe.
|
||||
|
||||
Il suffit de positionner une variable d'environnement indiquant la BD utilisée
|
||||
par les tests:
|
||||
|
||||
export SCODOC_TEST_DATABASE_URI=postgresql:///SCODOC_DEV
|
||||
|
||||
@ -133,8 +127,8 @@ normalement, par exemple:
|
||||
|
||||
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:
|
||||
Il est en général nécessaire d'affecter ensuite un mot de passe à (au moins) un
|
||||
utilisateur:
|
||||
|
||||
flask user-password admin
|
||||
|
||||
@ -182,8 +176,6 @@ puis
|
||||
|
||||
snakeviz -s --hostname 0.0.0.0 -p 5555 /opt/scodoc-data/GET.ScoDoc......prof
|
||||
|
||||
|
||||
|
||||
# Paquet Debian 11
|
||||
|
||||
Les scripts associés au paquet Debian (.deb) sont dans `tools/debian`. Le plus
|
||||
@ -192,4 +184,3 @@ upgrade de scodoc9).
|
||||
|
||||
La préparation d'une release se fait à l'aide du script
|
||||
`tools/build_release.sh`.
|
||||
|
||||
|
@ -26,11 +26,13 @@ from flask_mail import Mail
|
||||
from flask_bootstrap import Bootstrap
|
||||
from flask_moment import Moment
|
||||
from flask_caching import Cache
|
||||
from jinja2 import select_autoescape
|
||||
import sqlalchemy
|
||||
|
||||
from app.scodoc.sco_exceptions import (
|
||||
AccessDenied,
|
||||
ScoBugCatcher,
|
||||
ScoException,
|
||||
ScoGenError,
|
||||
ScoValueError,
|
||||
APIInvalidParams,
|
||||
@ -60,11 +62,11 @@ cache = Cache(
|
||||
|
||||
|
||||
def handle_sco_value_error(exc):
|
||||
return render_template("sco_value_error.html", exc=exc), 404
|
||||
return render_template("sco_value_error.j2", exc=exc), 404
|
||||
|
||||
|
||||
def handle_access_denied(exc):
|
||||
return render_template("error_access_denied.html", exc=exc), 403
|
||||
return render_template("error_access_denied.j2", exc=exc), 403
|
||||
|
||||
|
||||
def internal_server_error(exc):
|
||||
@ -74,7 +76,7 @@ def internal_server_error(exc):
|
||||
|
||||
return (
|
||||
render_template(
|
||||
"error_500.html",
|
||||
"error_500.j2",
|
||||
SCOVERSION=sco_version.SCOVERSION,
|
||||
date=datetime.datetime.now().isoformat(),
|
||||
exc=exc,
|
||||
@ -92,6 +94,9 @@ def handle_sco_bug(exc):
|
||||
"""Un bug, en général rare, sur lequel les dev cherchent des
|
||||
informations pour le corriger.
|
||||
"""
|
||||
if current_app.config["TESTING"] or current_app.config["DEBUG"]:
|
||||
raise ScoException # for development servers only
|
||||
else:
|
||||
Thread(
|
||||
target=_async_dump, args=(current_app._get_current_object(), request.url)
|
||||
).start()
|
||||
@ -142,7 +147,7 @@ def render_raw_html(template_filename: str, **args) -> str:
|
||||
|
||||
def postgresql_server_error(e):
|
||||
"""Erreur de connection au serveur postgresql (voir notesdb.open_db_connection)"""
|
||||
return render_raw_html("error_503.html", SCOVERSION=sco_version.SCOVERSION), 503
|
||||
return render_raw_html("error_503.j2", SCOVERSION=sco_version.SCOVERSION), 503
|
||||
|
||||
|
||||
class LogRequestFormatter(logging.Formatter):
|
||||
@ -271,6 +276,9 @@ def create_app(config_class=DevConfig):
|
||||
from app.api import api_bp
|
||||
from app.api import api_web_bp
|
||||
|
||||
# Enable autoescaping of all templates, including .j2
|
||||
app.jinja_env.autoescape = select_autoescape(default_for_string=True, default=True)
|
||||
|
||||
# https://scodoc.fr/ScoDoc
|
||||
app.register_blueprint(scodoc_bp)
|
||||
# https://scodoc.fr/ScoDoc/RT/Scolarite/...
|
||||
@ -435,8 +443,6 @@ def initialize_scodoc_database(erase=False, create_all=False):
|
||||
SQL tables and functions.
|
||||
If erase is True, _erase_ all database content.
|
||||
"""
|
||||
from app import models
|
||||
|
||||
# - ERASE (the truncation sql function has been defined above)
|
||||
if erase:
|
||||
truncate_database()
|
||||
@ -463,6 +469,26 @@ def truncate_database():
|
||||
except:
|
||||
db.session.rollback()
|
||||
raise
|
||||
# Remet les compteurs (séquences sql) à zéro
|
||||
db.session.execute(
|
||||
"""
|
||||
CREATE OR REPLACE FUNCTION reset_sequences(username IN VARCHAR) RETURNS void AS $$
|
||||
DECLARE
|
||||
statements CURSOR FOR
|
||||
SELECT sequence_name
|
||||
FROM information_schema.sequences
|
||||
ORDER BY sequence_name ;
|
||||
BEGIN
|
||||
FOR stmt IN statements LOOP
|
||||
EXECUTE 'ALTER SEQUENCE ' || quote_ident(stmt.sequence_name) || ' RESTART;';
|
||||
END LOOP;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
SELECT reset_sequences('scodoc');
|
||||
"""
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def clear_scodoc_cache():
|
||||
@ -480,12 +506,10 @@ def clear_scodoc_cache():
|
||||
|
||||
|
||||
# --------- Logging
|
||||
def log(msg: str, silent_test=True):
|
||||
def log(msg: str):
|
||||
"""log a message.
|
||||
If Flask app, use configured logger, else stderr.
|
||||
"""
|
||||
if silent_test and current_app and current_app.config["TESTING"]:
|
||||
return
|
||||
try:
|
||||
dept = getattr(g, "scodoc_dept", "")
|
||||
msg = f" ({dept}) {msg}"
|
||||
@ -530,3 +554,22 @@ def scodoc_flash_status_messages():
|
||||
f"Mode test: mails redirigés vers {email_test_mode_address}",
|
||||
category="warning",
|
||||
)
|
||||
|
||||
|
||||
def critical_error(msg):
|
||||
"""Handle a critical error: flush all caches, display message to the user"""
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
log(f"\n*** CRITICAL ERROR: {msg}")
|
||||
send_scodoc_alarm(f"CRITICAL ERROR: {msg}", msg)
|
||||
clear_scodoc_cache()
|
||||
raise ScoValueError(
|
||||
f"""
|
||||
Une erreur est survenue.
|
||||
|
||||
Si le problème persiste, merci de contacter le support ScoDoc via
|
||||
{scu.SCO_DISCORD_ASSISTANCE}
|
||||
|
||||
{msg}
|
||||
"""
|
||||
)
|
||||
|
@ -2,7 +2,8 @@
|
||||
"""
|
||||
|
||||
from flask import Blueprint
|
||||
from flask import request
|
||||
from flask import request, g, jsonify
|
||||
from app import db
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import ScoException
|
||||
|
||||
@ -31,9 +32,26 @@ def requested_format(default_format="json", allowed_formats=None):
|
||||
return 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, Formsemstre) -> join_cls
|
||||
|
||||
exemple d'utilisation : fonction "justificatif()" -> app/api/justificatifs.py
|
||||
"""
|
||||
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_or_404()
|
||||
|
||||
return jsonify(unique.to_dict(format_api=True))
|
||||
|
||||
|
||||
from app.api import tokens
|
||||
from app.api import (
|
||||
absences,
|
||||
assiduites,
|
||||
billets_absences,
|
||||
departements,
|
||||
etudiants,
|
||||
@ -41,6 +59,7 @@ from app.api import (
|
||||
formations,
|
||||
formsemestres,
|
||||
jury,
|
||||
justificatifs,
|
||||
logos,
|
||||
partitions,
|
||||
users,
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
"""ScoDoc 9 API : Absences
|
||||
|
575
app/api/assiduites.py
Normal file
575
app/api/assiduites.py
Normal file
@ -0,0 +1,575 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
"""ScoDoc 9 API : Assiduités
|
||||
"""
|
||||
from datetime import datetime
|
||||
from flask import g, jsonify, request
|
||||
from flask_login import login_required
|
||||
|
||||
import app.scodoc.sco_assiduites as scass
|
||||
import app.scodoc.sco_utils as scu
|
||||
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
|
||||
from app.decorators import permission_required, scodoc
|
||||
from app.models import Assiduite, FormSemestre, Identite, ModuleImpl
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_utils import json_error
|
||||
|
||||
|
||||
@bp.route("/assiduite/<int:assiduite_id>")
|
||||
@api_web_bp.route("/assiduite/<int:assiduite_id>")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def assiduite(assiduite_id: int = None):
|
||||
"""Retourne un objet assiduité à partir de son id
|
||||
|
||||
Exemple de résultat:
|
||||
{
|
||||
"assiduite_id": 1,
|
||||
"etudid": 2,
|
||||
"moduleimpl_id": 3,
|
||||
"date_debut": "2022-10-31T08:00+01:00",
|
||||
"date_fin": "2022-10-31T10:00+01:00",
|
||||
"etat": "retard",
|
||||
"desc": "une description",
|
||||
}
|
||||
"""
|
||||
|
||||
return get_model_api_object(Assiduite, assiduite_id, Identite)
|
||||
|
||||
|
||||
@bp.route("/assiduites/<int:etudid>/count", defaults={"with_query": False})
|
||||
@bp.route("/assiduites/<int:etudid>/count/query", defaults={"with_query": True})
|
||||
@api_web_bp.route("/assiduites/<int:etudid>/count", defaults={"with_query": False})
|
||||
@api_web_bp.route("/assiduites/<int:etudid>/count/query", defaults={"with_query": True})
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def count_assiduites(etudid: int = None, with_query: bool = False):
|
||||
"""
|
||||
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?formsemstre_id=[int]
|
||||
ex query?formsemestre_id=3
|
||||
|
||||
|
||||
"""
|
||||
query = Identite.query.filter_by(id=etudid)
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
|
||||
etud: Identite = query.first_or_404(etudid)
|
||||
filtered: dict[str, object] = {}
|
||||
metric: str = "all"
|
||||
|
||||
if with_query:
|
||||
metric, filtered = _count_manager(request)
|
||||
|
||||
return jsonify(
|
||||
scass.get_assiduites_stats(
|
||||
assiduites=etud.assiduites, metric=metric, filtered=filtered
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/assiduites/<int:etudid>", defaults={"with_query": False})
|
||||
@bp.route("/assiduites/<int:etudid>/query", defaults={"with_query": True})
|
||||
@api_web_bp.route("/assiduites/<int:etudid>", defaults={"with_query": False})
|
||||
@api_web_bp.route("/assiduites/<int:etudid>/query", defaults={"with_query": True})
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def assiduites(etudid: int = None, with_query: bool = False):
|
||||
"""
|
||||
Retourne toutes les assiduités d'un étudiant
|
||||
chemin : /assiduites/<int:etudid>
|
||||
|
||||
Un filtrage peut être donné avec une query
|
||||
chemin : /assiduites/<int:etudid>/query?
|
||||
|
||||
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
|
||||
|
||||
|
||||
"""
|
||||
|
||||
query = Identite.query.filter_by(id=etudid)
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
|
||||
etud: Identite = query.first_or_404(etudid)
|
||||
assiduites_query = etud.assiduites
|
||||
|
||||
if with_query:
|
||||
assiduites_query = _filter_manager(request, assiduites_query)
|
||||
|
||||
data_set: list[dict] = []
|
||||
for ass in assiduites_query.all():
|
||||
data = ass.to_dict(format_api=True)
|
||||
data_set.append(data)
|
||||
|
||||
return jsonify(data_set)
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/assiduites/formsemestre/<int:formsemestre_id>", defaults={"with_query": False}
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/assiduites/formsemestre/<int:formsemestre_id>", defaults={"with_query": False}
|
||||
)
|
||||
@bp.route(
|
||||
"/assiduites/formsemestre/<int:formsemestre_id>/query",
|
||||
defaults={"with_query": True},
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/assiduites/formsemestre/<int:formsemestre_id>/query",
|
||||
defaults={"with_query": True},
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
|
||||
formsemestre: FormSemestre = None
|
||||
formsemestre_id = int(formsemestre_id)
|
||||
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
|
||||
|
||||
if formsemestre is None:
|
||||
return json_error(404, "le paramètre 'formsemestre_id' n'existe pas")
|
||||
|
||||
assiduites_query = scass.filter_by_formsemestre(Assiduite.query, formsemestre)
|
||||
|
||||
if with_query:
|
||||
assiduites_query = _filter_manager(request, assiduites_query)
|
||||
|
||||
data_set: list[dict] = []
|
||||
for ass in assiduites_query.all():
|
||||
data = ass.to_dict(format_api=True)
|
||||
data_set.append(data)
|
||||
|
||||
return jsonify(data_set)
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/assiduites/formsemestre/<int:formsemestre_id>/count",
|
||||
defaults={"with_query": False},
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/assiduites/formsemestre/<int:formsemestre_id>/count",
|
||||
defaults={"with_query": False},
|
||||
)
|
||||
@bp.route(
|
||||
"/assiduites/formsemestre/<int:formsemestre_id>/count/query",
|
||||
defaults={"with_query": True},
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/assiduites/formsemestre/<int:formsemestre_id>/count/query",
|
||||
defaults={"with_query": True},
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def count_assiduites_formsemestre(
|
||||
formsemestre_id: int = None, with_query: bool = False
|
||||
):
|
||||
formsemestre: FormSemestre = None
|
||||
formsemestre_id = int(formsemestre_id)
|
||||
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
|
||||
|
||||
if formsemestre is None:
|
||||
return json_error(404, "le paramètre 'formsemestre_id' n'existe pas")
|
||||
|
||||
etuds = formsemestre.etuds.all()
|
||||
etuds_id = [etud.id for etud in etuds]
|
||||
|
||||
assiduites_query = Assiduite.query.filter(Assiduite.etudid.in_(etuds_id))
|
||||
assiduites_query = scass.filter_by_formsemestre(assiduites_query, formsemestre)
|
||||
metric: str = "all"
|
||||
filtered: dict = {}
|
||||
if with_query:
|
||||
metric, filtered = _count_manager(request)
|
||||
|
||||
return jsonify(scass.get_assiduites_stats(assiduites_query, metric, filtered))
|
||||
|
||||
|
||||
@bp.route("/assiduite/<int:etudid>/create", methods=["POST"])
|
||||
@api_web_bp.route("/assiduite/<int:etudid>/create", methods=["POST"])
|
||||
@scodoc
|
||||
@login_required
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def assiduite_create(etudid: int = None):
|
||||
"""
|
||||
Création d'une assiduité pour l'étudiant (etudid)
|
||||
La requête doit avoir un content type "application/json":
|
||||
[
|
||||
{
|
||||
"date_debut": str,
|
||||
"date_fin": str,
|
||||
"etat": str,
|
||||
},
|
||||
{
|
||||
"date_debut": str,
|
||||
"date_fin": str,
|
||||
"etat": str,
|
||||
"moduleimpl_id": int,
|
||||
"desc":str,
|
||||
}
|
||||
...
|
||||
]
|
||||
|
||||
"""
|
||||
etud: Identite = Identite.query.filter_by(id=etudid).first_or_404()
|
||||
|
||||
create_list: list[object] = request.get_json(force=True)
|
||||
|
||||
if not isinstance(create_list, list):
|
||||
return json_error(404, "Le contenu envoyé n'est pas une liste")
|
||||
|
||||
errors: dict[int, str] = {}
|
||||
success: dict[int, object] = {}
|
||||
for i, data in enumerate(create_list):
|
||||
code, obj = _create_singular(data, etud)
|
||||
if code == 404:
|
||||
errors[i] = obj
|
||||
else:
|
||||
success[i] = obj
|
||||
|
||||
return jsonify({"errors": errors, "success": success})
|
||||
|
||||
|
||||
def _create_singular(
|
||||
data: dict,
|
||||
etud: Identite,
|
||||
) -> tuple[int, object]:
|
||||
errors: list[str] = []
|
||||
|
||||
# -- vérifications de l'objet json --
|
||||
# cas 1 : ETAT
|
||||
etat = data.get("etat", None)
|
||||
if etat is None:
|
||||
errors.append("param 'etat': manquant")
|
||||
elif not scu.EtatAssiduite.contains(etat):
|
||||
errors.append("param 'etat': invalide")
|
||||
|
||||
etat = scu.EtatAssiduite.get(etat)
|
||||
|
||||
# cas 2 : date_debut
|
||||
date_debut = data.get("date_debut", None)
|
||||
if date_debut is None:
|
||||
errors.append("param 'date_debut': manquant")
|
||||
deb = scu.is_iso_formated(date_debut, convert=True)
|
||||
if deb is None:
|
||||
errors.append("param 'date_debut': format invalide")
|
||||
|
||||
# cas 3 : date_fin
|
||||
date_fin = data.get("date_fin", None)
|
||||
if date_fin is None:
|
||||
errors.append("param 'date_fin': manquant")
|
||||
fin = scu.is_iso_formated(date_fin, convert=True)
|
||||
if fin is None:
|
||||
errors.append("param 'date_fin': format invalide")
|
||||
|
||||
# cas 4 : moduleimpl_id
|
||||
|
||||
moduleimpl_id = data.get("moduleimpl_id", False)
|
||||
moduleimpl: ModuleImpl = None
|
||||
|
||||
if moduleimpl_id is not False:
|
||||
moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first()
|
||||
if moduleimpl is None:
|
||||
errors.append("param 'moduleimpl_id': invalide")
|
||||
|
||||
# cas 5 : desc
|
||||
|
||||
desc: str = data.get("desc", None)
|
||||
|
||||
if errors:
|
||||
err: str = ", ".join(errors)
|
||||
return (404, err)
|
||||
|
||||
# TOUT EST OK
|
||||
|
||||
try:
|
||||
nouv_assiduite: Assiduite = Assiduite.create_assiduite(
|
||||
date_debut=deb,
|
||||
date_fin=fin,
|
||||
etat=etat,
|
||||
etud=etud,
|
||||
moduleimpl=moduleimpl,
|
||||
description=desc,
|
||||
)
|
||||
|
||||
db.session.add(nouv_assiduite)
|
||||
db.session.commit()
|
||||
return (200, {"assiduite_id": nouv_assiduite.assiduite_id})
|
||||
except ScoValueError as excp:
|
||||
return (
|
||||
404,
|
||||
excp.args[0],
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/assiduite/delete", methods=["POST"])
|
||||
@api_web_bp.route("/assiduite/delete", methods=["POST"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def assiduite_cdelete():
|
||||
"""
|
||||
Suppression d'une assiduité à partir de son id
|
||||
|
||||
Forme des données envoyées :
|
||||
|
||||
[
|
||||
<assiduite_id:int>,
|
||||
...
|
||||
]
|
||||
|
||||
|
||||
"""
|
||||
assiduites_list: list[int] = request.get_json(force=True)
|
||||
if not isinstance(assiduites_list, list):
|
||||
return json_error(404, "Le contenu envoyé n'est pas une liste")
|
||||
|
||||
output = {"errors": {}, "success": {}}
|
||||
|
||||
for i, ass in enumerate(assiduites_list):
|
||||
code, msg = _delete_singular(ass, db)
|
||||
if code == 404:
|
||||
output["errors"][f"{i}"] = msg
|
||||
else:
|
||||
output["success"][f"{i}"] = {"OK": True}
|
||||
db.session.commit()
|
||||
return jsonify(output)
|
||||
|
||||
|
||||
def _delete_singular(assiduite_id: int, database):
|
||||
assiduite_unique: Assiduite = Assiduite.query.filter_by(id=assiduite_id).first()
|
||||
if assiduite_unique is None:
|
||||
return (404, "Assiduite non existante")
|
||||
database.session.delete(assiduite_unique)
|
||||
return (200, "OK")
|
||||
|
||||
|
||||
@bp.route("/assiduite/<int:assiduite_id>/edit", methods=["POST"])
|
||||
@api_web_bp.route("/assiduite/<int:assiduite_id>/edit", methods=["POST"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def assiduite_edit(assiduite_id: int):
|
||||
"""
|
||||
Edition d'une assiduité à partir de son id
|
||||
La requête doit avoir un content type "application/json":
|
||||
{
|
||||
"etat"?: str,
|
||||
"moduleimpl_id"?: int
|
||||
"desc"?: str
|
||||
}
|
||||
"""
|
||||
assiduite_unique: Assiduite = Assiduite.query.filter_by(
|
||||
id=assiduite_id
|
||||
).first_or_404()
|
||||
errors: list[str] = []
|
||||
data = request.get_json(force=True)
|
||||
|
||||
# Vérifications de data
|
||||
|
||||
# Cas 1 : Etat
|
||||
if data.get("etat") is not None:
|
||||
etat = scu.EtatAssiduite.get(data.get("etat"))
|
||||
if etat is None:
|
||||
errors.append("param 'etat': invalide")
|
||||
else:
|
||||
assiduite_unique.etat = etat
|
||||
|
||||
# Cas 2 : Moduleimpl_id
|
||||
moduleimpl_id = data.get("moduleimpl_id", False)
|
||||
moduleimpl: ModuleImpl = None
|
||||
|
||||
if moduleimpl_id is not False:
|
||||
if moduleimpl_id is not None:
|
||||
moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first()
|
||||
if moduleimpl is None:
|
||||
errors.append("param 'moduleimpl_id': invalide")
|
||||
else:
|
||||
if not moduleimpl.est_inscrit(
|
||||
Identite.query.filter_by(id=assiduite_unique.etudid).first()
|
||||
):
|
||||
errors.append("param 'moduleimpl_id': etud non inscrit")
|
||||
else:
|
||||
assiduite_unique.moduleimpl_id = moduleimpl_id
|
||||
else:
|
||||
assiduite_unique.moduleimpl_id = moduleimpl_id
|
||||
|
||||
# Cas 3 : desc
|
||||
desc = data.get("desc", False)
|
||||
if desc is not False:
|
||||
assiduite_unique.desc = desc
|
||||
|
||||
if errors:
|
||||
err: str = ", ".join(errors)
|
||||
return json_error(404, err)
|
||||
|
||||
db.session.add(assiduite_unique)
|
||||
db.session.commit()
|
||||
return jsonify({"OK": True})
|
||||
|
||||
|
||||
# -- Utils --
|
||||
|
||||
|
||||
def _count_manager(requested) -> tuple[str, dict]:
|
||||
"""
|
||||
Retourne la/les métriques à utiliser ainsi que le filtre donnés en query de la requête
|
||||
"""
|
||||
filtered: dict = {}
|
||||
# cas 1 : etat assiduite
|
||||
etat = requested.args.get("etat")
|
||||
if etat is not None:
|
||||
filtered["etat"] = etat
|
||||
|
||||
# cas 2 : date de début
|
||||
deb = requested.args.get("date_debut")
|
||||
deb: datetime = scu.is_iso_formated(deb, True)
|
||||
if deb is not None:
|
||||
filtered["date_debut"] = deb
|
||||
|
||||
# cas 3 : date de fin
|
||||
fin = requested.args.get("date_fin")
|
||||
fin = scu.is_iso_formated(fin, True)
|
||||
|
||||
if fin is not None:
|
||||
filtered["date_fin"] = fin
|
||||
|
||||
# cas 4 : moduleimpl_id
|
||||
module = requested.args.get("moduleimpl_id", False)
|
||||
try:
|
||||
if module is False:
|
||||
raise ValueError
|
||||
if module != "":
|
||||
module = int(module)
|
||||
else:
|
||||
module = None
|
||||
except ValueError:
|
||||
module = False
|
||||
|
||||
if module is not False:
|
||||
filtered["moduleimpl_id"] = module
|
||||
|
||||
# cas 5 : formsemestre_id
|
||||
formsemestre_id = requested.args.get("formsemestre_id")
|
||||
|
||||
if formsemestre_id is not None:
|
||||
formsemestre: FormSemestre = None
|
||||
formsemestre_id = int(formsemestre_id)
|
||||
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
|
||||
filtered["formsemestre"] = formsemestre
|
||||
|
||||
# cas 6 : type
|
||||
metric = requested.args.get("metric", "all")
|
||||
|
||||
return (metric, filtered)
|
||||
|
||||
|
||||
def _filter_manager(requested, assiduites_query: Assiduite):
|
||||
"""
|
||||
Retourne les assiduites entrées filtrées en fonction de la request
|
||||
"""
|
||||
# cas 1 : etat assiduite
|
||||
etat = requested.args.get("etat")
|
||||
if etat is not None:
|
||||
assiduites_query = scass.filter_assiduites_by_etat(assiduites_query, etat)
|
||||
|
||||
# cas 2 : date de début
|
||||
deb = requested.args.get("date_debut")
|
||||
deb: datetime = scu.is_iso_formated(deb, True)
|
||||
|
||||
# cas 3 : date de fin
|
||||
fin = requested.args.get("date_fin")
|
||||
fin = scu.is_iso_formated(fin, True)
|
||||
|
||||
if (deb, fin) != (None, None):
|
||||
assiduites_query: Assiduite = scass.filter_by_date(
|
||||
assiduites_query, Assiduite, deb, fin
|
||||
)
|
||||
|
||||
# cas 4 : moduleimpl_id
|
||||
module = requested.args.get("moduleimpl_id", False)
|
||||
try:
|
||||
if module is False:
|
||||
raise ValueError
|
||||
if module != "":
|
||||
module = int(module)
|
||||
else:
|
||||
module = None
|
||||
except ValueError:
|
||||
module = False
|
||||
|
||||
if module is not False:
|
||||
assiduites_query = scass.filter_by_module_impl(assiduites_query, module)
|
||||
|
||||
# cas 5 : formsemestre_id
|
||||
formsemestre_id = requested.args.get("formsemestre_id")
|
||||
|
||||
if formsemestre_id is not None:
|
||||
formsemestre: FormSemestre = None
|
||||
formsemestre_id = int(formsemestre_id)
|
||||
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
|
||||
assiduites_query = scass.filter_by_formsemestre(assiduites_query, formsemestre)
|
||||
|
||||
return assiduites_query
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -10,12 +10,13 @@
|
||||
Note: les routes /departement[s] sont publiées sur l'API (/ScoDoc/api/),
|
||||
mais évidemment pas sur l'API web (/ScoDoc/<dept>/api).
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from flask import jsonify, request
|
||||
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
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.decorators import scodoc, permission_required
|
||||
@ -42,7 +43,7 @@ def get_departement(dept_ident: str) -> Departement:
|
||||
@permission_required(Permission.ScoView)
|
||||
def departements_list():
|
||||
"""Liste les départements"""
|
||||
return jsonify([dept.to_dict() for dept in Departement.query])
|
||||
return jsonify([dept.to_dict(with_dept_name=True) for dept in Departement.query])
|
||||
|
||||
|
||||
@bp.route("/departements_ids")
|
||||
@ -66,13 +67,14 @@ def departement(acronym: str):
|
||||
{
|
||||
"id": 1,
|
||||
"acronym": "TAPI",
|
||||
"dept_name" : "TEST",
|
||||
"description": null,
|
||||
"visible": true,
|
||||
"date_creation": "Fri, 15 Apr 2022 12:19:28 GMT"
|
||||
}
|
||||
"""
|
||||
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||
return jsonify(dept.to_dict())
|
||||
return jsonify(dept.to_dict(with_dept_name=True))
|
||||
|
||||
|
||||
@bp.route("/departement/id/<int:dept_id>")
|
||||
@ -256,15 +258,18 @@ def dept_formsemestres_courants(acronym: str):
|
||||
]
|
||||
"""
|
||||
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 <= app.db.func.now(),
|
||||
FormSemestre.date_fin >= app.db.func.now(),
|
||||
FormSemestre.date_debut <= test_date,
|
||||
FormSemestre.date_fin >= test_date,
|
||||
)
|
||||
|
||||
return jsonify([d.to_dict(convert_objects=True) for d in formsemestres])
|
||||
return jsonify([d.to_dict_api() for d in formsemestres])
|
||||
|
||||
|
||||
@bp.route("/departement/id/<int:dept_id>/formsemestres_courants")
|
||||
@ -277,12 +282,16 @@ def dept_formsemestres_courants_by_id(dept_id: int):
|
||||
"""
|
||||
# Le département, spécifié par un id ou un acronyme
|
||||
dept = Departement.query.get_or_404(dept_id)
|
||||
|
||||
date_courante = request.args.get("date_courante")
|
||||
if date_courante:
|
||||
test_date = datetime.fromisoformat(date_courante)
|
||||
else:
|
||||
test_date = app.db.func.now()
|
||||
# Les semestres en cours de ce département
|
||||
formsemestres = FormSemestre.query.filter(
|
||||
FormSemestre.dept_id == dept.id,
|
||||
FormSemestre.date_debut <= app.db.func.now(),
|
||||
FormSemestre.date_fin >= app.db.func.now(),
|
||||
FormSemestre.date_debut <= test_date,
|
||||
FormSemestre.date_fin >= test_date,
|
||||
)
|
||||
|
||||
return jsonify([d.to_dict(convert_objects=True) for d in formsemestres])
|
||||
return jsonify([d.to_dict_api() for d in formsemestres])
|
||||
|
@ -1,14 +1,15 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
API : accès aux étudiants
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from flask import g, jsonify
|
||||
from flask import abort, g, jsonify, request
|
||||
from flask_login import current_user
|
||||
from flask_login import login_required
|
||||
from sqlalchemy import desc, or_
|
||||
@ -75,11 +76,16 @@ def etudiants_courants(long=False):
|
||||
|
||||
"""
|
||||
allowed_depts = current_user.get_depts_with_permission(Permission.ScoView)
|
||||
date_courante = request.args.get("date_courante")
|
||||
if date_courante:
|
||||
test_date = datetime.fromisoformat(date_courante)
|
||||
else:
|
||||
test_date = app.db.func.now()
|
||||
etuds = Identite.query.filter(
|
||||
Identite.id == FormSemestreInscription.etudid,
|
||||
FormSemestreInscription.formsemestre_id == FormSemestre.id,
|
||||
FormSemestre.date_debut <= app.db.func.now(),
|
||||
FormSemestre.date_fin >= app.db.func.now(),
|
||||
FormSemestre.date_debut <= test_date,
|
||||
FormSemestre.date_fin >= test_date,
|
||||
)
|
||||
if not None in allowed_depts:
|
||||
# restreint aux départements autorisés:
|
||||
@ -204,160 +210,75 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None)
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin",
|
||||
methods=["GET"],
|
||||
defaults={"version": "long", "pdf": False},
|
||||
"/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin",
|
||||
)
|
||||
@bp.route(
|
||||
"/etudiant/nip/<string:nip>/formsemestre/<int:formsemestre_id>/bulletin",
|
||||
methods=["GET"],
|
||||
defaults={"version": "long", "pdf": False},
|
||||
"/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin/<string:version>",
|
||||
)
|
||||
@bp.route(
|
||||
"/etudiant/ine/<string:ine>/formsemestre/<int:formsemestre_id>/bulletin",
|
||||
methods=["GET"],
|
||||
defaults={"version": "long", "pdf": False},
|
||||
)
|
||||
@bp.route(
|
||||
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/pdf",
|
||||
methods=["GET"],
|
||||
defaults={"version": "long", "pdf": True},
|
||||
)
|
||||
@bp.route(
|
||||
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/short",
|
||||
methods=["GET"],
|
||||
defaults={"version": "short", "pdf": False},
|
||||
)
|
||||
@bp.route(
|
||||
"/etudiant/nip/<string:nip>/formsemestre/<int:formsemestre_id>/bulletin/short",
|
||||
methods=["GET"],
|
||||
defaults={"version": "short", "pdf": False},
|
||||
)
|
||||
@bp.route(
|
||||
"/etudiant/ine/<string:ine>/formsemestre/<int:formsemestre_id>/bulletin/short",
|
||||
methods=["GET"],
|
||||
defaults={"version": "short", "pdf": False},
|
||||
)
|
||||
@bp.route(
|
||||
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/short/pdf",
|
||||
methods=["GET"],
|
||||
defaults={"version": "short", "pdf": True},
|
||||
)
|
||||
@bp.route(
|
||||
"/etudiant/nip/<string:nip>/formsemestre/<int:formsemestre_id>/bulletin/short/pdf",
|
||||
methods=["GET"],
|
||||
defaults={"version": "short", "pdf": True},
|
||||
)
|
||||
@bp.route(
|
||||
"/etudiant/ine/<string:ine>/formsemestre/<int:formsemestre_id>/bulletin/short/pdf",
|
||||
methods=["GET"],
|
||||
defaults={"version": "short", "pdf": True},
|
||||
)
|
||||
@bp.route(
|
||||
"/etudiant/nip/<string:nip>/formsemestre/<int:formsemestre_id>/bulletin/pdf",
|
||||
methods=["GET"],
|
||||
defaults={"version": "long", "pdf": True},
|
||||
)
|
||||
@bp.route(
|
||||
"/etudiant/ine/<string:ine>/formsemestre/<int:formsemestre_id>/bulletin/pdf",
|
||||
methods=["GET"],
|
||||
defaults={"version": "long", "pdf": True},
|
||||
"/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin/<string:version>/pdf",
|
||||
defaults={"pdf": True},
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin",
|
||||
methods=["GET"],
|
||||
defaults={"version": "long", "pdf": False},
|
||||
"/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin",
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/nip/<string:nip>/formsemestre/<int:formsemestre_id>/bulletin",
|
||||
methods=["GET"],
|
||||
defaults={"version": "long", "pdf": False},
|
||||
"/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin/<string:version>",
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/ine/<string:ine>/formsemestre/<int:formsemestre_id>/bulletin",
|
||||
methods=["GET"],
|
||||
defaults={"version": "long", "pdf": False},
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/pdf",
|
||||
methods=["GET"],
|
||||
defaults={"version": "long", "pdf": True},
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/short",
|
||||
methods=["GET"],
|
||||
defaults={"version": "short", "pdf": False},
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/nip/<string:nip>/formsemestre/<int:formsemestre_id>/bulletin/short",
|
||||
methods=["GET"],
|
||||
defaults={"version": "short", "pdf": False},
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/ine/<string:ine>/formsemestre/<int:formsemestre_id>/bulletin/short",
|
||||
methods=["GET"],
|
||||
defaults={"version": "short", "pdf": False},
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/short/pdf",
|
||||
methods=["GET"],
|
||||
defaults={"version": "short", "pdf": True},
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/nip/<string:nip>/formsemestre/<int:formsemestre_id>/bulletin/short/pdf",
|
||||
methods=["GET"],
|
||||
defaults={"version": "short", "pdf": True},
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/ine/<string:ine>/formsemestre/<int:formsemestre_id>/bulletin/short/pdf",
|
||||
methods=["GET"],
|
||||
defaults={"version": "short", "pdf": True},
|
||||
"/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin/<string:version>/pdf",
|
||||
defaults={"pdf": True},
|
||||
)
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def etudiant_bulletin_semestre(
|
||||
formsemestre_id,
|
||||
etudid: int = None,
|
||||
nip: str = None,
|
||||
ine: str = None,
|
||||
version="long",
|
||||
def bulletin(
|
||||
code_type: str = "etudid",
|
||||
code: str = None,
|
||||
formsemestre_id: int = None,
|
||||
version: str = "long",
|
||||
pdf: bool = False,
|
||||
):
|
||||
"""
|
||||
Retourne le bulletin d'un étudiant en fonction de son id et d'un semestre donné
|
||||
|
||||
formsemestre_id : l'id d'un formsemestre
|
||||
etudid : l'etudid d'un étudiant
|
||||
nip : le code nip d'un étudiant
|
||||
ine : le code ine d'un étudiant
|
||||
Exemple de résultat : voir https://scodoc.org/ScoDoc9API/#bulletin
|
||||
code_type : "etudid", "nip" ou "ine"
|
||||
code : valeur du code INE, NIP ou etudid, selon code_type.
|
||||
version : type de bulletin (par défaut, "long"): short, long, long_mat
|
||||
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
|
||||
# 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 non trouve")
|
||||
if etudid is not None:
|
||||
query = Identite.query.filter_by(id=etudid)
|
||||
elif nip is not None:
|
||||
query = Identite.query.filter_by(code_nip=nip, dept_id=dept.id)
|
||||
elif ine is not None:
|
||||
query = Identite.query.filter_by(code_ine=ine, dept_id=dept.id)
|
||||
else:
|
||||
return json_error(404, message="parametre manquant")
|
||||
app.set_sco_dept(dept.acronym)
|
||||
|
||||
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")
|
||||
|
||||
app.set_sco_dept(dept.acronym)
|
||||
|
||||
if pdf:
|
||||
pdf_response, _ = do_formsemestre_bulletinetud(
|
||||
formsemestre, etud.id, version=version, format="pdf"
|
||||
)
|
||||
return pdf_response
|
||||
|
||||
return sco_bulletins.get_formsemestre_bulletin_etud_json(
|
||||
formsemestre, etud, version=version
|
||||
)
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -22,6 +22,44 @@ from app.scodoc.sco_permissions import Permission
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
|
||||
@bp.route("/evaluation/<int:evaluation_id>")
|
||||
@api_web_bp.route("/evaluation/<int:evaluation_id>")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def evaluation(evaluation_id: int):
|
||||
"""Description d'une évaluation.
|
||||
|
||||
{
|
||||
'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,
|
||||
'visi_bulletin': True
|
||||
}
|
||||
"""
|
||||
query = Evaluation.query.filter_by(id=evaluation_id)
|
||||
if g.scodoc_dept:
|
||||
query = (
|
||||
query.join(ModuleImpl)
|
||||
.join(FormSemestre)
|
||||
.filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
e = query.first_or_404()
|
||||
return jsonify(e.to_dict_api())
|
||||
|
||||
|
||||
@bp.route("/moduleimpl/<int:moduleimpl_id>/evaluations")
|
||||
@api_web_bp.route("/moduleimpl/<int:moduleimpl_id>/evaluations")
|
||||
@login_required
|
||||
@ -33,39 +71,16 @@ def evaluations(moduleimpl_id: int):
|
||||
|
||||
moduleimpl_id : l'id d'un moduleimpl
|
||||
|
||||
Exemple de résultat :
|
||||
[
|
||||
{
|
||||
"moduleimpl_id": 1,
|
||||
"jour": "20/04/2022",
|
||||
"heure_debut": "08h00",
|
||||
"description": "eval1",
|
||||
"coefficient": 1.0,
|
||||
"publish_incomplete": false,
|
||||
"numero": 0,
|
||||
"id": 1,
|
||||
"heure_fin": "09h00",
|
||||
"note_max": 20.0,
|
||||
"visibulletin": true,
|
||||
"evaluation_type": 0,
|
||||
"evaluation_id": 1,
|
||||
"jouriso": "2022-04-20",
|
||||
"duree": "1h",
|
||||
"descrheure": " de 08h00 à 09h00",
|
||||
"matin": 1,
|
||||
"apresmidi": 0
|
||||
},
|
||||
...
|
||||
]
|
||||
Exemple de résultat : voir /evaluation
|
||||
"""
|
||||
query = Evaluation.query.filter_by(id=moduleimpl_id)
|
||||
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 jsonify([d.to_dict() for d in query])
|
||||
return jsonify([e.to_dict_api() for e in query])
|
||||
|
||||
|
||||
@bp.route("/evaluation/<int:evaluation_id>/notes")
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -22,6 +22,8 @@ from app.models import (
|
||||
Evaluation,
|
||||
FormSemestre,
|
||||
FormSemestreEtape,
|
||||
FormSemestreInscription,
|
||||
Identite,
|
||||
ModuleImpl,
|
||||
NotesNotes,
|
||||
)
|
||||
@ -95,11 +97,14 @@ def formsemestres_query():
|
||||
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.
|
||||
"""
|
||||
etape_apo = request.args.get("etape_apo")
|
||||
annee_scolaire = request.args.get("annee_scolaire")
|
||||
dept_acronym = request.args.get("dept_acronym")
|
||||
dept_id = request.args.get("dept_id")
|
||||
nip = request.args.get("nip")
|
||||
ine = request.args.get("ine")
|
||||
formsemestres = FormSemestre.query
|
||||
if g.scodoc_dept:
|
||||
formsemestres = formsemestres.filter_by(dept_id=g.scodoc_dept_id)
|
||||
@ -125,16 +130,30 @@ def formsemestres_query():
|
||||
formsemestres = formsemestres.join(FormSemestreEtape).filter(
|
||||
FormSemestreEtape.etape_apo == etape_apo
|
||||
)
|
||||
inscr_joined = False
|
||||
if nip is not None:
|
||||
formsemestres = (
|
||||
formsemestres.join(FormSemestreInscription)
|
||||
.join(Identite)
|
||||
.filter_by(code_nip=nip)
|
||||
)
|
||||
inscr_joined = True
|
||||
if ine is not None:
|
||||
if not inscr_joined:
|
||||
formsemestres = formsemestres.join(FormSemestreInscription).join(Identite)
|
||||
formsemestres = formsemestres.filter_by(code_ine=ine)
|
||||
|
||||
return jsonify([formsemestre.to_dict_api() for formsemestre in formsemestres])
|
||||
|
||||
|
||||
@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")
|
||||
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/bulletins/<string:version>")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def bulletins(formsemestre_id: int):
|
||||
def bulletins(formsemestre_id: int, version: str = "long"):
|
||||
"""
|
||||
Retourne les bulletins d'un formsemestre donné
|
||||
|
||||
@ -145,12 +164,16 @@ def bulletins(formsemestre_id: int):
|
||||
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)
|
||||
formsemestre: FormSemestre = query.first()
|
||||
if formsemestre is None:
|
||||
return json_error(404, "formsemestre non trouve")
|
||||
app.set_sco_dept(formsemestre.departement.acronym)
|
||||
|
||||
data = []
|
||||
for etu in formsemestre.etuds:
|
||||
bul_etu = get_formsemestre_bulletin_etud_json(formsemestre, etu)
|
||||
bul_etu = get_formsemestre_bulletin_etud_json(
|
||||
formsemestre, etu, version=version
|
||||
)
|
||||
data.append(bul_etu.json)
|
||||
|
||||
return jsonify(data)
|
||||
@ -381,7 +404,7 @@ def etat_evals(formsemestre_id: int):
|
||||
for evaluation_id in modimpl_results.evaluations_etat:
|
||||
eval_etat = modimpl_results.evaluations_etat[evaluation_id]
|
||||
evaluation = Evaluation.query.get_or_404(evaluation_id)
|
||||
eval_dict = evaluation.to_dict()
|
||||
eval_dict = evaluation.to_dict_api()
|
||||
eval_dict["etat"] = eval_etat.to_dict()
|
||||
|
||||
eval_dict["nb_inscrits"] = modimpl_results.nb_inscrits_module
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
580
app/api/justificatifs.py
Normal file
580
app/api/justificatifs.py
Normal file
@ -0,0 +1,580 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
"""ScoDoc 9 API : Assiduités
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from flask import g, jsonify, request
|
||||
from flask_login import login_required
|
||||
|
||||
import app.scodoc.sco_assiduites as scass
|
||||
import app.scodoc.sco_utils as scu
|
||||
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
|
||||
from app.decorators import permission_required, scodoc
|
||||
from app.models import Identite, Justificatif
|
||||
from app.models.assiduites import is_period_conflicting
|
||||
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.sco_utils import json_error
|
||||
|
||||
|
||||
# @bp.route("/justificatif/remove")
|
||||
# @api_web_bp.route("/justificatif/remove")
|
||||
# @scodoc
|
||||
# def justremove():
|
||||
# """ """
|
||||
# archiver: JustificatifArchiver = JustificatifArchiver()
|
||||
|
||||
# archiver.delete_justificatif(etudid=1, archive_id="2023-02-01-10-29-20")
|
||||
# return jsonify("done")
|
||||
|
||||
# Partie Modèle
|
||||
@bp.route("/justificatif/<int:justif_id>")
|
||||
@api_web_bp.route("/justificatif/<int:justif_id>")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def justificatif(justif_id: int = None):
|
||||
"""Retourne un objet justificatif à partir de son id
|
||||
|
||||
Exemple de résultat:
|
||||
{
|
||||
"justif_id": 1,
|
||||
"etudid": 2,
|
||||
"date_debut": "2022-10-31T08:00+01:00",
|
||||
"date_fin": "2022-10-31T10:00+01:00",
|
||||
"etat": "valide",
|
||||
"fichier": "archive_id",
|
||||
"raison": "une raison",
|
||||
"entry_date": "2022-10-31T08:00+01:00",
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
return get_model_api_object(Justificatif, justif_id, Identite)
|
||||
|
||||
|
||||
@bp.route("/justificatifs/<int:etudid>", defaults={"with_query": False})
|
||||
@bp.route("/justificatifs/<int:etudid>/query", defaults={"with_query": True})
|
||||
@api_web_bp.route("/justificatifs/<int:etudid>", defaults={"with_query": False})
|
||||
@api_web_bp.route("/justificatifs/<int:etudid>/query", defaults={"with_query": True})
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def justificatifs(etudid: int = None, with_query: bool = False):
|
||||
"""
|
||||
Retourne toutes les assiduités d'un étudiant
|
||||
chemin : /justificatifs/<int:etudid>
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
query = Identite.query.filter_by(id=etudid)
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
|
||||
etud: Identite = query.first_or_404(etudid)
|
||||
justificatifs_query = etud.justificatifs
|
||||
|
||||
if with_query:
|
||||
justificatifs_query = _filter_manager(request, justificatifs_query)
|
||||
|
||||
data_set: list[dict] = []
|
||||
for just in justificatifs_query.all():
|
||||
data = just.to_dict(format_api=True)
|
||||
data_set.append(data)
|
||||
|
||||
return jsonify(data_set)
|
||||
|
||||
|
||||
@bp.route("/justificatif/<int:etudid>/create", methods=["POST"])
|
||||
@api_web_bp.route("/justificatif/<int:etudid>/create", methods=["POST"])
|
||||
@scodoc
|
||||
@login_required
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def justif_create(etudid: int = None):
|
||||
"""
|
||||
Création d'un justificatif pour l'étudiant (etudid)
|
||||
La requête doit avoir un content type "application/json":
|
||||
[
|
||||
{
|
||||
"date_debut": str,
|
||||
"date_fin": str,
|
||||
"etat": str,
|
||||
},
|
||||
{
|
||||
"date_debut": str,
|
||||
"date_fin": str,
|
||||
"etat": str,
|
||||
"raison":str,
|
||||
}
|
||||
...
|
||||
]
|
||||
|
||||
"""
|
||||
etud: Identite = Identite.query.filter_by(id=etudid).first_or_404()
|
||||
|
||||
create_list: list[object] = request.get_json(force=True)
|
||||
|
||||
if not isinstance(create_list, list):
|
||||
return json_error(404, "Le contenu envoyé n'est pas une liste")
|
||||
|
||||
errors: dict[int, str] = {}
|
||||
success: dict[int, object] = {}
|
||||
for i, data in enumerate(create_list):
|
||||
code, obj = _create_singular(data, etud)
|
||||
if code == 404:
|
||||
errors[i] = obj
|
||||
else:
|
||||
success[i] = obj
|
||||
|
||||
return jsonify({"errors": errors, "success": success})
|
||||
|
||||
|
||||
def _create_singular(
|
||||
data: dict,
|
||||
etud: Identite,
|
||||
) -> tuple[int, object]:
|
||||
errors: list[str] = []
|
||||
|
||||
# -- vérifications de l'objet json --
|
||||
# cas 1 : ETAT
|
||||
etat = data.get("etat", None)
|
||||
if etat is None:
|
||||
errors.append("param 'etat': manquant")
|
||||
elif not scu.EtatJustificatif.contains(etat):
|
||||
errors.append("param 'etat': invalide")
|
||||
|
||||
etat = scu.EtatJustificatif.get(etat)
|
||||
|
||||
# cas 2 : date_debut
|
||||
date_debut = data.get("date_debut", None)
|
||||
if date_debut is None:
|
||||
errors.append("param 'date_debut': manquant")
|
||||
deb = scu.is_iso_formated(date_debut, convert=True)
|
||||
if deb is None:
|
||||
errors.append("param 'date_debut': format invalide")
|
||||
|
||||
# cas 3 : date_fin
|
||||
date_fin = data.get("date_fin", None)
|
||||
if date_fin is None:
|
||||
errors.append("param 'date_fin': manquant")
|
||||
fin = scu.is_iso_formated(date_fin, convert=True)
|
||||
if fin is None:
|
||||
errors.append("param 'date_fin': format invalide")
|
||||
|
||||
# cas 4 : raison
|
||||
|
||||
raison: str = data.get("raison", None)
|
||||
|
||||
if errors:
|
||||
err: str = ", ".join(errors)
|
||||
return (404, err)
|
||||
|
||||
# TOUT EST OK
|
||||
|
||||
try:
|
||||
nouv_justificatif: Justificatif = Justificatif.create_justificatif(
|
||||
date_debut=deb,
|
||||
date_fin=fin,
|
||||
etat=etat,
|
||||
etud=etud,
|
||||
raison=raison,
|
||||
)
|
||||
|
||||
db.session.add(nouv_justificatif)
|
||||
db.session.commit()
|
||||
return (200, {"justif_id": nouv_justificatif.id})
|
||||
except ScoValueError as excp:
|
||||
return (
|
||||
404,
|
||||
excp.args[0],
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/justificatif/<int:justif_id>/edit", methods=["POST"])
|
||||
@api_web_bp.route("/justificatif/<int:justif_id>/edit", methods=["POST"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def justif_edit(justif_id: int):
|
||||
"""
|
||||
Edition d'un justificatif à partir de son id
|
||||
La requête doit avoir un content type "application/json":
|
||||
|
||||
{
|
||||
"etat"?: str,
|
||||
"raison"?: str
|
||||
"date_debut"?: str
|
||||
"date_fin"?: str
|
||||
}
|
||||
"""
|
||||
justificatif_unique: Justificatif = Justificatif.query.filter_by(
|
||||
id=justif_id
|
||||
).first_or_404()
|
||||
errors: list[str] = []
|
||||
data = request.get_json(force=True)
|
||||
|
||||
# Vérifications de data
|
||||
|
||||
# Cas 1 : Etat
|
||||
if data.get("etat") is not None:
|
||||
etat = scu.EtatJustificatif.get(data.get("etat"))
|
||||
if etat is None:
|
||||
errors.append("param 'etat': invalide")
|
||||
else:
|
||||
justificatif_unique.etat = etat
|
||||
|
||||
# Cas 2 : raison
|
||||
raison = data.get("raison", False)
|
||||
if raison is not False:
|
||||
justificatif_unique.raison = raison
|
||||
|
||||
deb, fin = None, None
|
||||
|
||||
# cas 3 : date_debut
|
||||
date_debut = data.get("date_debut", False)
|
||||
if date_debut is not False:
|
||||
if date_debut is None:
|
||||
errors.append("param 'date_debut': manquant")
|
||||
deb = scu.is_iso_formated(date_debut, convert=True)
|
||||
if deb is None:
|
||||
errors.append("param 'date_debut': format invalide")
|
||||
|
||||
if justificatif_unique.date_fin >= deb:
|
||||
errors.append("param 'date_debut': date de début située après date de fin ")
|
||||
|
||||
# cas 4 : date_fin
|
||||
date_fin = data.get("date_fin", False)
|
||||
if date_fin is not False:
|
||||
if date_fin is None:
|
||||
errors.append("param 'date_fin': manquant")
|
||||
fin = scu.is_iso_formated(date_fin, convert=True)
|
||||
if fin is None:
|
||||
errors.append("param 'date_fin': format invalide")
|
||||
if justificatif_unique.date_debut <= fin:
|
||||
errors.append("param 'date_fin': date de fin située avant date de début ")
|
||||
|
||||
# Vérification du conflit d'horaire
|
||||
if (deb is not None) or (fin is not None):
|
||||
deb = deb if deb is not None else justificatif_unique.date_debut
|
||||
fin = fin if fin is not None else justificatif_unique.date_fin
|
||||
|
||||
justificatifs_list: list[Justificatif] = Justificatif.query.filter_by(
|
||||
etuid=justificatif_unique.etudid
|
||||
).all()
|
||||
|
||||
if is_period_conflicting(deb, fin, justificatifs_list):
|
||||
errors.append(
|
||||
"Modification de la plage horaire impossible: conflit avec les autres justificatifs"
|
||||
)
|
||||
justificatif_unique.date_debut = deb
|
||||
justificatif_unique.date_fin = fin
|
||||
|
||||
if errors:
|
||||
err: str = ", ".join(errors)
|
||||
return json_error(404, err)
|
||||
|
||||
db.session.add(justificatif_unique)
|
||||
db.session.commit()
|
||||
return jsonify({"OK": True})
|
||||
|
||||
|
||||
@bp.route("/justificatif/delete", methods=["POST"])
|
||||
@api_web_bp.route("/justificatif/delete", methods=["POST"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def justif_delete():
|
||||
"""
|
||||
Suppression d'un justificatif à partir de son id
|
||||
|
||||
Forme des données envoyées :
|
||||
|
||||
[
|
||||
<justif_id:int>,
|
||||
...
|
||||
]
|
||||
|
||||
|
||||
"""
|
||||
justificatifs_list: list[int] = request.get_json(force=True)
|
||||
if not isinstance(justificatifs_list, list):
|
||||
return json_error(404, "Le contenu envoyé n'est pas une liste")
|
||||
|
||||
output = {"errors": {}, "success": {}}
|
||||
|
||||
for i, ass in enumerate(justificatifs_list):
|
||||
code, msg = _delete_singular(ass, db)
|
||||
if code == 404:
|
||||
output["errors"][f"{i}"] = msg
|
||||
else:
|
||||
output["success"][f"{i}"] = {"OK": True}
|
||||
db.session.commit()
|
||||
return jsonify(output)
|
||||
|
||||
|
||||
def _delete_singular(justif_id: int, database):
|
||||
justificatif_unique: Justificatif = Justificatif.query.filter_by(
|
||||
id=justif_id
|
||||
).first()
|
||||
if justificatif_unique is None:
|
||||
return (404, "Justificatif non existant")
|
||||
|
||||
archive_name: str = justificatif_unique.fichier
|
||||
|
||||
if archive_name is not None:
|
||||
archiver: JustificatifArchiver = JustificatifArchiver()
|
||||
archiver.delete_justificatif(justificatif_unique.etudid, archive_name)
|
||||
|
||||
database.session.delete(justificatif_unique)
|
||||
return (200, "OK")
|
||||
|
||||
|
||||
# Partie archivage
|
||||
@bp.route("/justificatif/import/<int:justif_id>", methods=["POST"])
|
||||
@api_web_bp.route("/justificatif/import/<int:justif_id>", methods=["POST"])
|
||||
@scodoc
|
||||
@login_required
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def justif_import(justif_id: int = None):
|
||||
"""
|
||||
Importation d'un fichier (création d'archive)
|
||||
"""
|
||||
if len(request.files) == 0:
|
||||
return json_error(404, "Il n'y a pas de fichier joint")
|
||||
|
||||
file = list(request.files.values())[0]
|
||||
if file.filename == "":
|
||||
return json_error(404, "Il n'y a pas de fichier joint")
|
||||
|
||||
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()
|
||||
|
||||
archive_name: str = justificatif_unique.fichier
|
||||
|
||||
archiver: JustificatifArchiver = JustificatifArchiver()
|
||||
try:
|
||||
fname: str
|
||||
archive_name, fname = archiver.save_justificatif(
|
||||
etudid=justificatif_unique.etudid,
|
||||
filename=file.filename,
|
||||
data=file.stream.read(),
|
||||
archive_name=archive_name,
|
||||
)
|
||||
|
||||
justificatif_unique.fichier = archive_name
|
||||
|
||||
db.session.add(justificatif_unique)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"filename": fname})
|
||||
except ScoValueError as err:
|
||||
return json_error(404, err.args[0])
|
||||
|
||||
|
||||
@bp.route("/justificatif/export/<int:justif_id>/<filename>", methods=["POST"])
|
||||
@api_web_bp.route("/justificatif/export/<int:justif_id>/<filename>", methods=["POST"])
|
||||
@scodoc
|
||||
@login_required
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def justif_export(justif_id: int = None, filename: str = None):
|
||||
"""
|
||||
Retourne un fichier d'une archive d'un justificatif
|
||||
"""
|
||||
|
||||
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()
|
||||
|
||||
archive_name: str = justificatif_unique.fichier
|
||||
if archive_name is None:
|
||||
return json_error(404, "le justificatif ne possède pas de fichier")
|
||||
|
||||
archiver: JustificatifArchiver = JustificatifArchiver()
|
||||
|
||||
try:
|
||||
return archiver.get_justificatif_file(
|
||||
archive_name, justificatif_unique.etudid, filename
|
||||
)
|
||||
except ScoValueError as err:
|
||||
return json_error(404, err.args[0])
|
||||
|
||||
|
||||
@bp.route("/justificatif/remove/<int:justif_id>", methods=["POST"])
|
||||
@api_web_bp.route("/justificatif/remove/<int:justif_id>", methods=["POST"])
|
||||
@scodoc
|
||||
@login_required
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def justif_remove(justif_id: int = None):
|
||||
"""
|
||||
Supression d'un fichier ou d'une archive
|
||||
# TOTALK: Doc, expliquer les noms coté server
|
||||
{
|
||||
"remove": <"all"/"list">
|
||||
|
||||
"filenames"?: [
|
||||
<filename:str>,
|
||||
...
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
data: dict = request.get_json(force=True)
|
||||
|
||||
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()
|
||||
|
||||
archive_name: str = justificatif_unique.fichier
|
||||
if archive_name is None:
|
||||
return json_error(404, "le justificatif ne possède pas de fichier")
|
||||
|
||||
remove: str = data.get("remove")
|
||||
if remove is None or remove not in ("all", "list"):
|
||||
return json_error(404, "param 'remove': Valeur invalide")
|
||||
archiver: JustificatifArchiver = JustificatifArchiver()
|
||||
etudid: int = justificatif_unique.etudid
|
||||
try:
|
||||
if remove == "all":
|
||||
archiver.delete_justificatif(etudid=etudid, archive_name=archive_name)
|
||||
justificatif_unique.fichier = None
|
||||
db.session.add(justificatif_unique)
|
||||
db.session.commit()
|
||||
|
||||
else:
|
||||
for fname in data.get("filenames", []):
|
||||
archiver.delete_justificatif(
|
||||
etudid=etudid,
|
||||
archive_name=archive_name,
|
||||
filename=fname,
|
||||
)
|
||||
|
||||
if len(archiver.list_justificatifs(archive_name, etudid)) == 0:
|
||||
archiver.delete_justificatif(etudid, archive_name)
|
||||
justificatif_unique.fichier = None
|
||||
db.session.add(justificatif_unique)
|
||||
db.session.commit()
|
||||
|
||||
except ScoValueError as err:
|
||||
return json_error(404, err.args[0])
|
||||
|
||||
return jsonify({"response": "removed"})
|
||||
|
||||
|
||||
@bp.route("/justificatif/list/<int:justif_id>", methods=["GET"])
|
||||
@api_web_bp.route("/justificatif/list/<int:justif_id>", methods=["GET"])
|
||||
@scodoc
|
||||
@login_required
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def justif_list(justif_id: int = None):
|
||||
"""
|
||||
Liste les fichiers du justificatif
|
||||
"""
|
||||
|
||||
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()
|
||||
|
||||
archive_name: str = justificatif_unique.fichier
|
||||
|
||||
filenames: list[str] = []
|
||||
|
||||
archiver: JustificatifArchiver = JustificatifArchiver()
|
||||
if archive_name is not None:
|
||||
filenames = archiver.list_justificatifs(
|
||||
archive_name, justificatif_unique.etudid
|
||||
)
|
||||
|
||||
return jsonify(filenames)
|
||||
|
||||
|
||||
# Partie justification
|
||||
@bp.route("/justificatif/justified/<int:justif_id>", methods=["GET"])
|
||||
@api_web_bp.route("/justificatif/justified/<int:justif_id>", methods=["GET"])
|
||||
@scodoc
|
||||
@login_required
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def justif_justified(justif_id: int = None):
|
||||
"""
|
||||
Liste assiduite_id justifiées par le justificatif
|
||||
"""
|
||||
|
||||
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()
|
||||
|
||||
assiduites_list: list[int] = scass.justifies(justificatif_unique)
|
||||
|
||||
return jsonify(assiduites_list)
|
||||
|
||||
|
||||
# -- Utils --
|
||||
|
||||
|
||||
def _filter_manager(requested, justificatifs_query):
|
||||
"""
|
||||
Retourne les justificatifs entrés filtrés en fonction de la request
|
||||
"""
|
||||
# cas 1 : etat justificatif
|
||||
etat = requested.args.get("etat")
|
||||
if etat is not None:
|
||||
justificatifs_query = scass.filter_justificatifs_by_etat(
|
||||
justificatifs_query, etat
|
||||
)
|
||||
|
||||
# cas 2 : date de début
|
||||
deb = requested.args.get("date_debut")
|
||||
deb: datetime = scu.is_iso_formated(deb, True)
|
||||
|
||||
# cas 3 : date de fin
|
||||
fin = requested.args.get("date_fin")
|
||||
fin = scu.is_iso_formated(fin, True)
|
||||
|
||||
if (deb, fin) != (None, None):
|
||||
justificatifs_query: Justificatif = scass.filter_by_date(
|
||||
justificatifs_query, Justificatif, deb, fin
|
||||
)
|
||||
|
||||
return justificatifs_query
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 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,6 +48,7 @@ from app.scodoc.sco_permissions import Permission
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
def api_get_glob_logos():
|
||||
"""Liste tous les logos"""
|
||||
logos = list_logos()[None]
|
||||
return jsonify(list(logos.keys()))
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -19,6 +19,7 @@ from app.models import FormSemestre, FormSemestreInscription, Identite
|
||||
from app.models import GroupDescr, Partition
|
||||
from app.models.groups import group_membership
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
@ -170,24 +171,15 @@ def set_etud_group(etudid: int, group_id: int):
|
||||
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
group = query.first_or_404()
|
||||
if not group.partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
if etud.id not in {e.id for e in group.partition.formsemestre.etuds}:
|
||||
return json_error(404, "etud non inscrit au formsemestre du groupe")
|
||||
groups = (
|
||||
GroupDescr.query.filter_by(partition_id=group.partition.id)
|
||||
.join(group_membership)
|
||||
.filter_by(etudid=etudid)
|
||||
|
||||
sco_groups.change_etud_group_in_partition(
|
||||
etudid, group_id, group.partition.to_dict()
|
||||
)
|
||||
ok = False
|
||||
for other_group in groups:
|
||||
if other_group.id == group_id:
|
||||
ok = True
|
||||
else:
|
||||
other_group.etuds.remove(etud)
|
||||
if not ok:
|
||||
group.etuds.append(etud)
|
||||
log(f"set_etud_group({etud}, {group})")
|
||||
db.session.commit()
|
||||
sco_cache.invalidate_formsemestre(group.partition.formsemestre_id)
|
||||
|
||||
return jsonify({"group_id": group_id, "etudid": etudid})
|
||||
|
||||
|
||||
@ -207,6 +199,8 @@ def group_remove_etud(group_id: int, etudid: int):
|
||||
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
group = query.first_or_404()
|
||||
if not group.partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
if etud in group.etuds:
|
||||
group.etuds.remove(etud)
|
||||
db.session.commit()
|
||||
@ -232,6 +226,8 @@ def partition_remove_etud(partition_id: int, etudid: int):
|
||||
if g.scodoc_dept:
|
||||
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
partition = query.first_or_404()
|
||||
if not partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
groups = (
|
||||
GroupDescr.query.filter_by(partition_id=partition_id)
|
||||
.join(group_membership)
|
||||
@ -262,8 +258,10 @@ def group_create(partition_id: int):
|
||||
if g.scodoc_dept:
|
||||
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
partition: Partition = query.first_or_404()
|
||||
if not partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
if not partition.groups_editable:
|
||||
return json_error(404, "partition non editable")
|
||||
return json_error(403, "partition non editable")
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
group_name = data.get("group_name")
|
||||
if group_name is None:
|
||||
@ -294,8 +292,10 @@ def group_delete(group_id: int):
|
||||
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
group: GroupDescr = query.first_or_404()
|
||||
if not group.partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
if not group.partition.groups_editable:
|
||||
return json_error(404, "partition non editable")
|
||||
return json_error(403, "partition non editable")
|
||||
formsemestre_id = group.partition.formsemestre_id
|
||||
log(f"deleting {group}")
|
||||
db.session.delete(group)
|
||||
@ -318,8 +318,10 @@ def group_edit(group_id: int):
|
||||
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
group: GroupDescr = query.first_or_404()
|
||||
if not group.partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
if not group.partition.groups_editable:
|
||||
return json_error(404, "partition non editable")
|
||||
return json_error(403, "partition non editable")
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
group_name = data.get("group_name")
|
||||
if group_name is not None:
|
||||
@ -358,6 +360,8 @@ def partition_create(formsemestre_id: int):
|
||||
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.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
partition_name = data.get("partition_name")
|
||||
if partition_name is None:
|
||||
@ -406,6 +410,8 @@ def formsemestre_order_partitions(formsemestre_id: int):
|
||||
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.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
partition_ids = request.get_json(force=True) # may raise 400 Bad Request
|
||||
if not isinstance(partition_ids, int) and not all(
|
||||
isinstance(x, int) for x in partition_ids
|
||||
@ -443,6 +449,8 @@ def partition_order_groups(partition_id: int):
|
||||
if g.scodoc_dept:
|
||||
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
partition: Partition = query.first_or_404()
|
||||
if not partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
group_ids = request.get_json(force=True) # may raise 400 Bad Request
|
||||
if not isinstance(group_ids, int) and not all(
|
||||
isinstance(x, int) for x in group_ids
|
||||
@ -484,6 +492,8 @@ def partition_edit(partition_id: int):
|
||||
if g.scodoc_dept:
|
||||
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
partition: Partition = query.first_or_404()
|
||||
if not partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
modified = False
|
||||
partition_name = data.get("partition_name")
|
||||
@ -542,6 +552,8 @@ def partition_delete(partition_id: int):
|
||||
if g.scodoc_dept:
|
||||
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
partition: Partition = query.first_or_404()
|
||||
if not partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
if not partition.partition_name:
|
||||
return json_error(404, "ne peut pas supprimer la partition par défaut")
|
||||
is_parcours = partition.is_parcours()
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
"""ScoDoc 9 API : outils
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -11,5 +11,5 @@ def send_password_reset_email(user):
|
||||
sender=current_app.config["SCODOC_MAIL_FROM"],
|
||||
recipients=[user.email],
|
||||
text_body=render_template("email/reset_password.txt", user=user, token=token),
|
||||
html_body=render_template("email/reset_password.html", user=user, token=token),
|
||||
html_body=render_template("email/reset_password.j2", user=user, token=token),
|
||||
)
|
||||
|
@ -42,7 +42,7 @@ def login():
|
||||
return form.redirect("scodoc.index")
|
||||
message = request.args.get("message", "")
|
||||
return render_template(
|
||||
"auth/login.html", title=_("Sign In"), form=form, message=message
|
||||
"auth/login.j2", title=_("Sign In"), form=form, message=message
|
||||
)
|
||||
|
||||
|
||||
@ -65,9 +65,7 @@ def create_user():
|
||||
db.session.commit()
|
||||
flash(f"Utilisateur {user.user_name} créé")
|
||||
return redirect(url_for("scodoc.index"))
|
||||
return render_template(
|
||||
"auth/register.html", title="Création utilisateur", form=form
|
||||
)
|
||||
return render_template("auth/register.j2", title="Création utilisateur", form=form)
|
||||
|
||||
|
||||
@bp.route("/reset_password_request", methods=["GET", "POST"])
|
||||
@ -98,7 +96,7 @@ def reset_password_request():
|
||||
)
|
||||
return redirect(url_for("auth.login"))
|
||||
return render_template(
|
||||
"auth/reset_password_request.html", title=_("Reset Password"), form=form
|
||||
"auth/reset_password_request.j2", title=_("Reset Password"), form=form
|
||||
)
|
||||
|
||||
|
||||
@ -116,7 +114,7 @@ def reset_password(token):
|
||||
db.session.commit()
|
||||
flash(_("Votre mot de passe a été changé."))
|
||||
return redirect(url_for("auth.login"))
|
||||
return render_template("auth/reset_password.html", form=form, user=user)
|
||||
return render_template("auth/reset_password.j2", form=form, user=user)
|
||||
|
||||
|
||||
@bp.route("/reset_standard_roles_permissions", methods=["GET", "POST"])
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -8,14 +8,14 @@
|
||||
Edition associations UE <-> Ref. Compétence
|
||||
"""
|
||||
from flask import g, url_for
|
||||
from app import db, log
|
||||
from app.models import Formation, UniteEns
|
||||
from app.models.but_refcomp import ApcNiveau
|
||||
from app.models import ApcReferentielCompetences, Formation, UniteEns
|
||||
from app.scodoc import sco_codes_parcours
|
||||
|
||||
|
||||
def form_ue_choix_niveau(formation: Formation, ue: UniteEns) -> str:
|
||||
"""Form. HTML pour associer une UE à un niveau de compétence"""
|
||||
def form_ue_choix_niveau(ue: UniteEns) -> str:
|
||||
"""Form. HTML pour associer une UE à un niveau de compétence.
|
||||
Le menu select lui meême est vide et rempli en JS par appel à get_ue_niveaux_options_html
|
||||
"""
|
||||
if ue.type != sco_codes_parcours.UE_STANDARD:
|
||||
return ""
|
||||
ref_comp = ue.formation.referentiel_competence
|
||||
@ -27,11 +27,70 @@ def form_ue_choix_niveau(formation: Formation, ue: UniteEns) -> str:
|
||||
}">associer un référentiel de compétence</a>
|
||||
</div>
|
||||
</div>"""
|
||||
annee = 1 if ue.semestre_idx is None else (ue.semestre_idx + 1) // 2 # 1, 2, 3
|
||||
niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(annee)
|
||||
# Les parcours:
|
||||
parcours_options = []
|
||||
for parcour in ref_comp.parcours:
|
||||
parcours_options.append(
|
||||
f"""<option value="{parcour.id}" {
|
||||
'selected' if ue.parcour == parcour else ''}
|
||||
>{parcour.libelle} ({parcour.code})
|
||||
</option>"""
|
||||
)
|
||||
|
||||
newline = "\n"
|
||||
return f"""
|
||||
<div class="ue_choix_niveau">
|
||||
<form class="form_ue_choix_niveau">
|
||||
<div class="cont_ue_choix_niveau">
|
||||
<div>
|
||||
<b>Parcours :</b>
|
||||
<select class="select_parcour"
|
||||
onchange="set_ue_parcour(this);"
|
||||
data-ue_id="{ue.id}"
|
||||
data-setter="{
|
||||
url_for( "notes.set_ue_parcours", scodoc_dept=g.scodoc_dept)
|
||||
}">
|
||||
<option value="" {
|
||||
'selected' if ue.parcour is None else ''
|
||||
}>Tous</option>
|
||||
{newline.join(parcours_options)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<b>Niveau de compétence :</b>
|
||||
<select class="select_niveau_ue"
|
||||
onchange="set_ue_niveau_competence(this);"
|
||||
data-ue_id="{ue.id}"
|
||||
data-setter="{
|
||||
url_for( "notes.set_ue_niveau_competence", scodoc_dept=g.scodoc_dept)
|
||||
}">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def get_ue_niveaux_options_html(ue: UniteEns) -> str:
|
||||
"""fragment html avec les options du menu de sélection du
|
||||
niveau de compétences associé à une UE.
|
||||
|
||||
Si l'UE n'a pas de parcours associé: présente les niveaux
|
||||
de tous les parcours.
|
||||
Si l'UE a un parcours: seulement les niveaux de ce parcours.
|
||||
"""
|
||||
ref_comp: ApcReferentielCompetences = ue.formation.referentiel_competence
|
||||
if ref_comp is None:
|
||||
return ""
|
||||
# Les niveaux:
|
||||
annee = ue.annee() # 1, 2, 3
|
||||
parcours, niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(
|
||||
annee, parcour=ue.parcour
|
||||
)
|
||||
|
||||
# Les niveaux déjà associés à d'autres UE du même semestre
|
||||
autres_ues = formation.ues.filter_by(semestre_idx=ue.semestre_idx)
|
||||
autres_ues = ue.formation.ues.filter_by(semestre_idx=ue.semestre_idx)
|
||||
niveaux_autres_ues = {
|
||||
oue.niveau_competence_id for oue in autres_ues if oue.id != ue.id
|
||||
}
|
||||
@ -39,18 +98,14 @@ def form_ue_choix_niveau(formation: Formation, ue: UniteEns) -> str:
|
||||
if niveaux_by_parcours["TC"]: # TC pour Tronc Commun
|
||||
options.append("""<optgroup label="Tronc commun">""")
|
||||
for n in niveaux_by_parcours["TC"]:
|
||||
if n.id in niveaux_autres_ues:
|
||||
disabled = "disabled"
|
||||
else:
|
||||
disabled = ""
|
||||
options.append(
|
||||
f"""<option value="{n.id}" {'selected'
|
||||
if ue.niveau_competence == n else ''}
|
||||
{disabled}>{n.annee} {n.competence.titre_long}
|
||||
f"""<option value="{n.id}" {
|
||||
'selected' if ue.niveau_competence == n else ''}
|
||||
>{n.annee} {n.competence.titre_long}
|
||||
niveau {n.ordre}</option>"""
|
||||
)
|
||||
options.append("""</optgroup>""")
|
||||
for parcour in ref_comp.parcours:
|
||||
for parcour in parcours:
|
||||
if len(niveaux_by_parcours[parcour.id]):
|
||||
options.append(f"""<optgroup label="Parcours {parcour.libelle}">""")
|
||||
for n in niveaux_by_parcours[parcour.id]:
|
||||
@ -65,46 +120,7 @@ def form_ue_choix_niveau(formation: Formation, ue: UniteEns) -> str:
|
||||
niveau {n.ordre}</option>"""
|
||||
)
|
||||
options.append("""</optgroup>""")
|
||||
options_str = "\n".join(options)
|
||||
return f"""
|
||||
<div class="ue_choix_niveau">
|
||||
<form class="form_ue_choix_niveau">
|
||||
<b>Niveau de compétence associé:</b>
|
||||
<select onchange="set_ue_niveau_competence(this);"
|
||||
data-ue_id="{ue.id}"
|
||||
data-setter="{
|
||||
url_for( "notes.set_ue_niveau_competence", scodoc_dept=g.scodoc_dept)
|
||||
}">
|
||||
<option value="" {'selected' if ue.niveau_competence is None else ''}>aucun</option>
|
||||
{options_str}
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def set_ue_niveau_competence(ue_id: int, niveau_id: int):
|
||||
"""Associe le niveau et l'UE"""
|
||||
ue = UniteEns.query.get_or_404(ue_id)
|
||||
|
||||
autres_ues = ue.formation.ues.filter_by(semestre_idx=ue.semestre_idx)
|
||||
niveaux_autres_ues = {
|
||||
oue.niveau_competence_id for oue in autres_ues if oue.id != ue.id
|
||||
}
|
||||
if niveau_id in niveaux_autres_ues:
|
||||
log(
|
||||
f"set_ue_niveau_competence: denying association of {ue} to already associated {niveau_id}"
|
||||
return (
|
||||
f"""<option value="" {'selected' if ue.niveau_competence is None else ''}>aucun</option>"""
|
||||
+ "\n".join(options)
|
||||
)
|
||||
return "", 409 # conflict
|
||||
if niveau_id == "":
|
||||
niveau = ""
|
||||
# suppression de l'association
|
||||
ue.niveau_competence = None
|
||||
else:
|
||||
niveau = ApcNiveau.query.get_or_404(niveau_id)
|
||||
ue.niveau_competence = niveau
|
||||
db.session.add(ue)
|
||||
db.session.commit()
|
||||
log(f"set_ue_niveau_competence( {ue}, {niveau} )")
|
||||
|
||||
return "", 204
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -80,6 +80,9 @@ class BulletinBUT:
|
||||
"""
|
||||
res = self.res
|
||||
|
||||
if (etud.id, ue.id) in self.res.dispense_ues:
|
||||
return {}
|
||||
|
||||
if ue.type == UE_SPORT:
|
||||
modimpls_spo = [
|
||||
modimpl
|
||||
@ -239,6 +242,7 @@ class BulletinBUT:
|
||||
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"]
|
||||
@ -256,10 +260,11 @@ class BulletinBUT:
|
||||
notes_ok = eval_notes.where(eval_notes > scu.NOTES_ABSENCE).dropna()
|
||||
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][e.id]
|
||||
for ue in self.res.ues
|
||||
if ue.type != UE_SPORT
|
||||
if (ue.type != UE_SPORT) and (ue.id in etud_ues_ids)
|
||||
}
|
||||
except KeyError:
|
||||
poids = collections.defaultdict(lambda: 0.0)
|
||||
@ -356,7 +361,7 @@ class BulletinBUT:
|
||||
"formsemestre_id": formsemestre.id,
|
||||
"etat_inscription": etat_inscription,
|
||||
"options": sco_preferences.bulletin_option_affichage(
|
||||
formsemestre.id, self.prefs
|
||||
formsemestre, self.prefs
|
||||
),
|
||||
}
|
||||
if not published:
|
||||
@ -460,6 +465,7 @@ class BulletinBUT:
|
||||
"ressources": {},
|
||||
"saes": {},
|
||||
"ues": {},
|
||||
"ues_capitalisees": {},
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 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 - 2022 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,13 +43,13 @@ from app.but import bulletin_but
|
||||
from app.models import FormSemestre, Identite
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc import sco_abs
|
||||
from app.scodoc import sco_codes_parcours
|
||||
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
|
||||
from app.scodoc.sco_xml import quote_xml_attr
|
||||
|
||||
|
||||
def bulletin_but_xml_compat(
|
||||
@ -108,13 +108,13 @@ def bulletin_but_xml_compat(
|
||||
etudid=str(etudid),
|
||||
code_nip=etud.code_nip or "",
|
||||
code_ine=etud.code_ine or "",
|
||||
nom=scu.quote_xml_attr(etud.nom),
|
||||
prenom=scu.quote_xml_attr(etud.prenom),
|
||||
civilite=scu.quote_xml_attr(etud.civilite_str),
|
||||
sexe=scu.quote_xml_attr(etud.civilite_str), # compat
|
||||
photo_url=scu.quote_xml_attr(sco_photos.get_etud_photo_url(etud.id)),
|
||||
email=scu.quote_xml_attr(etud.get_first_email() or ""),
|
||||
emailperso=scu.quote_xml_attr(etud.get_first_email("emailperso") or ""),
|
||||
nom=quote_xml_attr(etud.nom),
|
||||
prenom=quote_xml_attr(etud.prenom),
|
||||
civilite=quote_xml_attr(etud.civilite_str),
|
||||
sexe=quote_xml_attr(etud.civilite_str), # compat
|
||||
photo_url=quote_xml_attr(sco_photos.get_etud_photo_url(etud.id)),
|
||||
email=quote_xml_attr(etud.get_first_email() or ""),
|
||||
emailperso=quote_xml_attr(etud.get_first_email("emailperso") or ""),
|
||||
)
|
||||
)
|
||||
# Disponible pour publication ?
|
||||
@ -153,10 +153,10 @@ def bulletin_but_xml_compat(
|
||||
x_ue = Element(
|
||||
"ue",
|
||||
id=str(ue.id),
|
||||
numero=scu.quote_xml_attr(ue.numero),
|
||||
acronyme=scu.quote_xml_attr(ue.acronyme or ""),
|
||||
titre=scu.quote_xml_attr(ue.titre or ""),
|
||||
code_apogee=scu.quote_xml_attr(ue.code_apogee or ""),
|
||||
numero=quote_xml_attr(ue.numero),
|
||||
acronyme=quote_xml_attr(ue.acronyme or ""),
|
||||
titre=quote_xml_attr(ue.titre or ""),
|
||||
code_apogee=quote_xml_attr(ue.code_apogee or ""),
|
||||
)
|
||||
doc.append(x_ue)
|
||||
if ue.type != sco_codes_parcours.UE_SPORT:
|
||||
@ -192,11 +192,9 @@ def bulletin_but_xml_compat(
|
||||
code=str(modimpl.module.code or ""),
|
||||
coefficient=str(coef),
|
||||
numero=str(modimpl.module.numero or 0),
|
||||
titre=scu.quote_xml_attr(modimpl.module.titre or ""),
|
||||
abbrev=scu.quote_xml_attr(modimpl.module.abbrev or ""),
|
||||
code_apogee=scu.quote_xml_attr(
|
||||
modimpl.module.code_apogee or ""
|
||||
),
|
||||
titre=quote_xml_attr(modimpl.module.titre or ""),
|
||||
abbrev=quote_xml_attr(modimpl.module.abbrev or ""),
|
||||
code_apogee=quote_xml_attr(modimpl.module.code_apogee or ""),
|
||||
)
|
||||
# XXX TODO rangs et effectifs
|
||||
# --- notes de chaque eval:
|
||||
@ -215,7 +213,7 @@ def bulletin_but_xml_compat(
|
||||
coefficient=str(e.coefficient),
|
||||
# pas les poids en XML compat
|
||||
evaluation_type=str(e.evaluation_type),
|
||||
description=scu.quote_xml_attr(e.description),
|
||||
description=quote_xml_attr(e.description),
|
||||
# notes envoyées sur 20, ceci juste pour garder trace:
|
||||
note_max_origin=str(e.note_max),
|
||||
)
|
||||
@ -262,7 +260,7 @@ def bulletin_but_xml_compat(
|
||||
),
|
||||
)
|
||||
x_situation = Element("situation")
|
||||
x_situation.text = scu.quote_xml_attr(infos["situation"])
|
||||
x_situation.text = quote_xml_attr(infos["situation"])
|
||||
doc.append(x_situation)
|
||||
if dpv:
|
||||
decision = dpv["decisions"][0]
|
||||
@ -297,9 +295,9 @@ def bulletin_but_xml_compat(
|
||||
Element(
|
||||
"decision_ue",
|
||||
ue_id=str(ue["ue_id"]),
|
||||
numero=scu.quote_xml_attr(ue["numero"]),
|
||||
acronyme=scu.quote_xml_attr(ue["acronyme"]),
|
||||
titre=scu.quote_xml_attr(ue["titre"]),
|
||||
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"],
|
||||
)
|
||||
)
|
||||
@ -322,7 +320,7 @@ def bulletin_but_xml_compat(
|
||||
"appreciation",
|
||||
date=ndb.DateDMYtoISO(appr["date"]),
|
||||
)
|
||||
x_appr.text = scu.quote_xml_attr(appr["comment"])
|
||||
x_appr.text = quote_xml_attr(appr["comment"])
|
||||
doc.append(x_appr)
|
||||
|
||||
if is_appending:
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -13,7 +13,7 @@ Classe raccordant avec ScoDoc 7:
|
||||
avec la même interface.
|
||||
|
||||
"""
|
||||
|
||||
import collections
|
||||
from typing import Union
|
||||
|
||||
from flask import g, url_for
|
||||
@ -47,12 +47,14 @@ from app.models.validations import ScolarFormSemestreValidation
|
||||
from app.scodoc import sco_codes_parcours as sco_codes
|
||||
from app.scodoc.sco_codes_parcours import RED, UE_STANDARD
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import ScoException, ScoValueError
|
||||
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: à revoir pour le BUT"""
|
||||
|
||||
def __init__(self, etud: dict, formsemestre_id: int, res: ResultatsSemestreBUT):
|
||||
super().__init__(etud, formsemestre_id, res)
|
||||
# Ajustements pour le BUT
|
||||
@ -65,3 +67,117 @@ class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
|
||||
def parcours_validated(self):
|
||||
"True si le parcours est validé"
|
||||
return False # XXX TODO
|
||||
|
||||
|
||||
class EtudCursusBUT:
|
||||
"""L'état de l'étudiant dans son cursus BUT
|
||||
Liste des niveaux validés/à valider
|
||||
"""
|
||||
|
||||
def __init__(self, etud: Identite, formation: Formation):
|
||||
"""formation indique la spécialité préparée"""
|
||||
# Vérifie que l'étudiant est bien inscrit à un sem. de cette formation
|
||||
if formation.id not in (
|
||||
ins.formsemestre.formation.id for ins in etud.formsemestre_inscriptions
|
||||
):
|
||||
raise ScoValueError(
|
||||
f"{etud.nomprenom} non inscrit dans {formation.titre} v{formation.version}"
|
||||
)
|
||||
if not formation.referentiel_competence:
|
||||
raise ScoNoReferentielCompetences(formation=formation)
|
||||
#
|
||||
self.etud = etud
|
||||
self.formation = formation
|
||||
self.inscriptions = sorted(
|
||||
[
|
||||
ins
|
||||
for ins in etud.formsemestre_inscriptions
|
||||
if ins.formsemestre.formation.referentiel_competence
|
||||
and (
|
||||
ins.formsemestre.formation.referentiel_competence.id
|
||||
== formation.referentiel_competence.id
|
||||
)
|
||||
],
|
||||
key=lambda s: (s.formsemestre.semestre_id, s.formsemestre.date_debut),
|
||||
)
|
||||
"Liste des inscriptions aux sem. de la formation, triées par indice et chronologie"
|
||||
self.parcour: ApcParcours = self.inscriptions[-1].parcour
|
||||
"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
|
||||
)[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]}
|
||||
)
|
||||
# Probablement inutile:
|
||||
# # Cherche les validations de jury enregistrées pour chaque niveau
|
||||
# self.validations_by_niveau = collections.defaultdict(lambda: [])
|
||||
# " { niveau_id : [ ApcValidationRCUE ] }"
|
||||
# for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
||||
# self.validations_by_niveau[validation_rcue.niveau().id].append(
|
||||
# validation_rcue
|
||||
# )
|
||||
# self.validation_by_niveau = {
|
||||
# niveau_id: sorted(
|
||||
# validations, key=lambda v: sco_codes.BUT_CODES_ORDERED[v.code]
|
||||
# )[0]
|
||||
# for niveau_id, validations in self.validations_by_niveau.items()
|
||||
# }
|
||||
# "{ niveau_id : meilleure validation pour ce niveau }"
|
||||
|
||||
self.validation_par_competence_et_annee = {}
|
||||
"{ competence_id : { 'BUT1' : validation_rcue, ... } }"
|
||||
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_ORDERED[validation_rcue.code]
|
||||
> sco_codes.BUT_CODES_ORDERED[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 }"
|
||||
|
||||
def to_dict(self):
|
||||
"""
|
||||
{
|
||||
competence_id : {
|
||||
annee : meilleure_validation
|
||||
}
|
||||
}
|
||||
"""
|
||||
return {
|
||||
competence.id: {
|
||||
annee: {
|
||||
self.validation_par_competence_et_annee.get(competence.id, {}).get(
|
||||
annee
|
||||
)
|
||||
}
|
||||
for annee in ("BUT1", "BUT2", "BUT3")
|
||||
}
|
||||
for competence in self.competences.values()
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
"""
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from flask_wtf.file import FileField, FileAllowed, FileRequired
|
||||
from flask_wtf.file import FileField, FileAllowed
|
||||
from wtforms import SelectField, SubmitField
|
||||
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
from xml.etree import ElementTree
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -64,7 +64,7 @@ import re
|
||||
from typing import Union
|
||||
|
||||
import numpy as np
|
||||
from flask import g, url_for
|
||||
from flask import flash, g, url_for
|
||||
|
||||
from app import db
|
||||
from app import log
|
||||
@ -91,9 +91,15 @@ from app.models.ues import UniteEns
|
||||
from app.models.validations import ScolarFormSemestreValidation
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_codes_parcours as sco_codes
|
||||
from app.scodoc.sco_codes_parcours import RED, UE_STANDARD
|
||||
from app.scodoc.sco_codes_parcours import (
|
||||
BUT_CODES_ORDERED,
|
||||
CODES_RCUE_VALIDES,
|
||||
CODES_UE_VALIDES,
|
||||
RED,
|
||||
UE_STANDARD,
|
||||
)
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import ScoException, ScoValueError
|
||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
|
||||
|
||||
|
||||
class NoRCUEError(ScoValueError):
|
||||
@ -126,7 +132,7 @@ class DecisionsProposees:
|
||||
"""Une décision de jury proposé, constituée d'une liste de codes et d'une explication.
|
||||
Super-classe, spécialisée pour les UE, les RCUE, les années et le diplôme.
|
||||
|
||||
validation : None ou une instance de d'une classe avec un champ code
|
||||
validation : None ou une instance d'une classe avec un champ code
|
||||
ApcValidationRCUE, ApcValidationAnnee ou ScolarFormSemestreValidation
|
||||
"""
|
||||
|
||||
@ -170,7 +176,7 @@ class DecisionsProposees:
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"""<{self.__class__.__name__} valid={self.code_valide
|
||||
} codes={self.codes} explanation={self.explanation}"""
|
||||
} codes={self.codes} explanation={self.explanation}>"""
|
||||
|
||||
|
||||
class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
@ -204,7 +210,12 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
etud: Identite,
|
||||
formsemestre: FormSemestre,
|
||||
):
|
||||
assert formsemestre.formation.is_apc()
|
||||
if formsemestre.formation.referentiel_competence is None:
|
||||
raise ScoNoReferentielCompetences(formation=formsemestre.formation)
|
||||
super().__init__(etud=etud)
|
||||
self.formsemestre = formsemestre
|
||||
"le formsemestre utilisé pour construire ce deca"
|
||||
self.formsemestre_id = formsemestre.id
|
||||
"l'id du formsemestre utilisé pour construire ce deca"
|
||||
formsemestre_impair, formsemestre_pair = self.comp_formsemestres(formsemestre)
|
||||
@ -219,19 +230,34 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
)
|
||||
)
|
||||
)
|
||||
# Si les années scolaires sont distinctes, on est "à cheval"
|
||||
self.a_cheval = (
|
||||
formsemestre_impair
|
||||
and formsemestre_pair
|
||||
and formsemestre_impair.annee_scolaire()
|
||||
!= formsemestre_pair.annee_scolaire()
|
||||
)
|
||||
"vrai si on groupe deux semestres d'années scolaires différentes"
|
||||
# Si on part d'un semestre IMPAIR, il n'y aura pas de décision année proposée
|
||||
# (mais on pourra évidemment valider des UE et même des RCUE)
|
||||
self.jury_annuel: bool = formsemestre.semestre_id in (2, 4, 6)
|
||||
"vrai si jury de fin d'année scolaire (propose code annuel)"
|
||||
|
||||
self.formsemestre_impair = formsemestre_impair
|
||||
"le 1er semestre de l'année scolaire considérée (S1, S3, S5)"
|
||||
"le 1er semestre du groupement (S1, S3, S5)"
|
||||
self.formsemestre_pair = formsemestre_pair
|
||||
"le second formsemestre de la même année scolaire (S2, S4, S6)"
|
||||
"le second formsemestre (S2, S4, S6), de la même année scolaire ou d'une précédente"
|
||||
formsemestre_last = formsemestre_pair or formsemestre_impair
|
||||
"le formsemestre le plus avancé dans cette année"
|
||||
"le formsemestre le plus avancé (en indice de semestre) dans le groupement"
|
||||
|
||||
self.annee_but = (formsemestre_last.semestre_id + 1) // 2
|
||||
"le rang de l'année dans le BUT: 1, 2, 3"
|
||||
assert self.annee_but in (1, 2, 3)
|
||||
self.rcues_annee = []
|
||||
"RCUEs de l'année"
|
||||
"""RCUEs de l'année
|
||||
(peuvent concerner l'année scolaire antérieur pour les redoublants
|
||||
avec UE capitalisées)
|
||||
"""
|
||||
self.inscription_etat = etud.inscription_etat(formsemestre_last.id)
|
||||
"état de l'inscription dans le semestre le plus avancé (pair si année complète)"
|
||||
self.inscription_etat_pair = (
|
||||
@ -293,10 +319,18 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
if self.formsemestre_impair
|
||||
else self.formsemestre_pair.formation
|
||||
)
|
||||
self.niveaux_competences = ApcNiveau.niveaux_annee_de_parcours(
|
||||
self.parcour, self.annee_but, formation.referentiel_competence
|
||||
).all() # non triés
|
||||
"liste des niveaux de compétences associés à cette année"
|
||||
(
|
||||
parcours,
|
||||
niveaux_by_parcours,
|
||||
) = formation.referentiel_competence.get_niveaux_by_parcours(
|
||||
self.annee_but, self.parcour
|
||||
)
|
||||
self.niveaux_competences = niveaux_by_parcours["TC"] + (
|
||||
niveaux_by_parcours[self.parcour.id] if self.parcour else []
|
||||
)
|
||||
"""liste non triée des niveaux de compétences associés à cette année pour cet étudiant.
|
||||
= niveaux du tronc commun + niveau du parcours de l'étudiant.
|
||||
"""
|
||||
self.decisions_rcue_by_niveau = self.compute_decisions_niveaux()
|
||||
"les décisions rcue associées aux niveau_id"
|
||||
self.dec_rcue_by_ue = self._dec_rcue_by_ue()
|
||||
@ -322,8 +356,9 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
)
|
||||
"vrai si l'année est réussie, tous niveaux validables ou validés par le jury"
|
||||
self.valide_moitie_rcue = self.nb_validables > (self.nb_competences // 2)
|
||||
# Peut passer si plus de la moitié validables et tous > 8
|
||||
"Vrai si plus de la moitié des RCUE validables"
|
||||
self.passage_de_droit = self.valide_moitie_rcue and (self.nb_rcues_under_8 == 0)
|
||||
"Vrai si peut passer dans l'année BUT suivante: plus de la moitié validables et tous > 8"
|
||||
# XXX TODO ajouter condition pour passage en S5
|
||||
|
||||
# Enfin calcule les codes des UE:
|
||||
@ -331,12 +366,13 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
dec_ue.compute_codes()
|
||||
|
||||
# Reste à attribuer ADM, ADJ, PASD, PAS1NCI, RED, NAR
|
||||
expl_rcues = (
|
||||
f"{self.nb_validables} niveau validable(s) sur {self.nb_competences}"
|
||||
)
|
||||
plural = self.nb_validables > 1
|
||||
expl_rcues = f"""{self.nb_validables} niveau{"x" if plural else ""} validable{
|
||||
"s" if plural else ""} sur {self.nb_competences}"""
|
||||
if self.admis:
|
||||
self.codes = [sco_codes.ADM] + self.codes
|
||||
self.explanation = expl_rcues
|
||||
# elif not self.jury_annuel:
|
||||
# self.codes = [] # pas de décision annuelle sur semestres impairs
|
||||
elif self.inscription_etat != scu.INSCRIT:
|
||||
self.codes = [
|
||||
sco_codes.DEM
|
||||
@ -350,9 +386,9 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
sco_codes.ABL,
|
||||
sco_codes.EXCLU,
|
||||
]
|
||||
expl_rcues = ""
|
||||
elif self.passage_de_droit:
|
||||
self.codes = [sco_codes.PASD, sco_codes.ADJ] + self.codes
|
||||
self.explanation = expl_rcues
|
||||
elif self.valide_moitie_rcue: # mais au moins 1 rcue insuffisante
|
||||
self.codes = [
|
||||
sco_codes.RED,
|
||||
@ -360,7 +396,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
sco_codes.PAS1NCI,
|
||||
sco_codes.ADJ,
|
||||
] + self.codes
|
||||
self.explanation = expl_rcues + f" et {self.nb_rcues_under_8} < 8"
|
||||
expl_rcues += f" et {self.nb_rcues_under_8} < 8"
|
||||
else:
|
||||
self.codes = [
|
||||
sco_codes.RED,
|
||||
@ -369,50 +405,57 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
sco_codes.ADJ,
|
||||
sco_codes.PASD, # voir #488 (discutable, conventions locales)
|
||||
] + self.codes
|
||||
self.explanation = (
|
||||
expl_rcues
|
||||
+ f""" et {self.nb_rcues_under_8}
|
||||
niveau{'x' if self.nb_rcues_under_8 > 1 else ''} < 8"""
|
||||
)
|
||||
expl_rcues += f""" et {self.nb_rcues_under_8} niveau{'x' if self.nb_rcues_under_8 > 1 else ''} < 8"""
|
||||
|
||||
# Si l'un des semestres est extérieur, propose ADM
|
||||
if (
|
||||
self.formsemestre_impair and self.formsemestre_impair.modalite == "EXT"
|
||||
) or (self.formsemestre_pair and self.formsemestre_pair.modalite == "EXT"):
|
||||
self.codes.insert(0, sco_codes.ADM)
|
||||
|
||||
self.explanation = f"<div>{expl_rcues}</div>"
|
||||
messages = self.descr_pb_coherence()
|
||||
if messages:
|
||||
self.explanation += (
|
||||
'<div class="warning">'
|
||||
+ '</div><div class="warning">'.join(messages)
|
||||
+ "</div>"
|
||||
)
|
||||
#
|
||||
|
||||
def infos(self) -> str:
|
||||
"informations, for debugging purpose"
|
||||
return f"""<b>DecisionsProposeesAnnee</b>
|
||||
"""informations, for debugging purpose."""
|
||||
text = f"""<b>DecisionsProposeesAnnee</b>
|
||||
<ul>
|
||||
<li>Etudiant: <a href="{url_for("scolar.ficheEtud",
|
||||
scodoc_dept=g.scodoc_dept, etudid=self.etud.id)
|
||||
}">{self.etud.nomprenom}</a>
|
||||
</li>
|
||||
<li>formsemestre_impair: <a href="{url_for("notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=self.formsemestre_impair.id)
|
||||
}">{html.escape(str(self.formsemestre_impair))}</a>
|
||||
"""
|
||||
for formsemestre, title in (
|
||||
(self.formsemestre_impair, "formsemestre_impair"),
|
||||
(self.formsemestre_pair, "formsemestre_pair"),
|
||||
):
|
||||
text += f"<li>{title}:"
|
||||
if formsemestre is not None:
|
||||
text += f"""
|
||||
<a href="{url_for("notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
|
||||
}">{html.escape(str(formsemestre))}</a>
|
||||
<ul>
|
||||
<li>Formation: <a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
|
||||
semestre_idx=self.formsemestre_impair.semestre_id,
|
||||
formation_id=self.formsemestre_impair.formation.id)}">
|
||||
{self.formsemestre_impair.formation.to_html()} ({self.formsemestre_impair.formation.id})</a>
|
||||
<li>Formation: <a href="{url_for('notes.ue_table',
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
semestre_idx=formsemestre.semestre_id,
|
||||
formation_id=formsemestre.formation.id)}">
|
||||
{formsemestre.formation.to_html()} ({
|
||||
formsemestre.formation.id})</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>formsemestre_pair: <a href="{url_for("notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=self.formsemestre_pair.id)
|
||||
}">{html.escape(str(self.formsemestre_pair))}</a>
|
||||
<ul>
|
||||
<li>Formation: <a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
|
||||
semestre_idx=self.formsemestre_pair.semestre_id,
|
||||
formation_id=self.formsemestre_pair.formation.id)}">
|
||||
{self.formsemestre_pair.formation.to_html()} ({self.formsemestre_pair.formation.id})</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
"""
|
||||
else:
|
||||
text += " aucun."
|
||||
text += "</li>"
|
||||
|
||||
text += f"""
|
||||
<li>RCUEs: {html.escape(str(self.rcues_annee))}</li>
|
||||
<li>nb_competences: {getattr(self, "nb_competences", "-")}</li>
|
||||
<li>nb_validables: {getattr(self, "nb_validables", "-")}</li>
|
||||
@ -420,6 +463,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
<li>explanation: {self.explanation}</li>
|
||||
</ul>
|
||||
"""
|
||||
return text
|
||||
|
||||
def annee_scolaire(self) -> int:
|
||||
"L'année de début de l'année scolaire"
|
||||
@ -434,14 +478,19 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
def comp_formsemestres(
|
||||
self, formsemestre: FormSemestre
|
||||
) -> tuple[FormSemestre, FormSemestre]:
|
||||
"""les deux formsemestres de l'année scolaire à laquelle appartient formsemestre."""
|
||||
"""Les deux formsemestres du niveau auquel appartient formsemestre.
|
||||
Complète le niveau avec le formsemestre antérieur le plus récent.
|
||||
L'"autre" formsemestre peut ainsi appartenir à l'année scolaire
|
||||
antérieure (redoublants).
|
||||
-> S_impair, S_pair
|
||||
"""
|
||||
if not formsemestre.formation.is_apc(): # garde fou
|
||||
return None, None
|
||||
if formsemestre.semestre_id % 2 == 0:
|
||||
other_semestre_id = formsemestre.semestre_id - 1
|
||||
else:
|
||||
other_semestre_id = formsemestre.semestre_id + 1
|
||||
annee_scolaire = formsemestre.annee_scolaire()
|
||||
|
||||
other_formsemestre = None
|
||||
for inscr in self.etud.formsemestre_inscriptions:
|
||||
if (
|
||||
@ -452,8 +501,13 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
)
|
||||
# L'autre semestre
|
||||
and (inscr.formsemestre.semestre_id == other_semestre_id)
|
||||
# de la même année scolaire:
|
||||
and (inscr.formsemestre.annee_scolaire() == annee_scolaire)
|
||||
# Antérieur
|
||||
and inscr.formsemestre.date_debut < formsemestre.date_debut
|
||||
# Et plus le récent possible
|
||||
and (
|
||||
(other_formsemestre is None)
|
||||
or (other_formsemestre.date_debut < inscr.formsemestre.date_debut)
|
||||
)
|
||||
):
|
||||
other_formsemestre = inscr.formsemestre
|
||||
if formsemestre.semestre_id % 2 == 0:
|
||||
@ -462,7 +516,8 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
|
||||
def compute_ues_annee(self) -> list[list[UniteEns], list[UniteEns]]:
|
||||
"""UEs à valider cette année pour cet étudiant, selon son parcours.
|
||||
Ramène [ listes des UE du semestre impair, liste des UE du semestre pair ].
|
||||
Affecte self.parcour suivant l'inscription de l'étudiant et
|
||||
ramène [ listes des UE du semestre impair, liste des UE du semestre pair ].
|
||||
"""
|
||||
ues_sems = []
|
||||
for (formsemestre, res) in (
|
||||
@ -497,9 +552,8 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
|
||||
def compute_rcues_annee(self) -> list[RegroupementCoherentUE]:
|
||||
"""Liste des regroupements d'UE à considérer cette année.
|
||||
Pour le moment on ne considère pas de RCUE à cheval sur plusieurs années (redoublants).
|
||||
On peut avoir un RCUE à cheval sur plusieurs années (redoublants avec UE capitalisées).
|
||||
Si on n'a pas les deux semestres, aucun RCUE.
|
||||
Raises ScoValueError s'il y a des UE sans RCUE.
|
||||
"""
|
||||
if self.formsemestre_pair is None or self.formsemestre_impair is None:
|
||||
return []
|
||||
@ -508,30 +562,47 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
for ue_pair in self.ues_pair:
|
||||
rcue = None
|
||||
for ue_impair in self.ues_impair:
|
||||
if self.a_cheval:
|
||||
# l'UE paire DOIT être capitalisée pour être utilisée
|
||||
if (
|
||||
self.decisions_ues[ue_pair.id].code_valide
|
||||
not in CODES_UE_VALIDES
|
||||
):
|
||||
continue # ignore cette UE antérieure non capitalisée
|
||||
# et l'UE impaire doit être actuellement meilleure que
|
||||
# celle éventuellement capitalisée
|
||||
if (
|
||||
self.decisions_ues[ue_impair.id].ue_status
|
||||
and self.decisions_ues[ue_impair.id].ue_status["is_capitalized"]
|
||||
):
|
||||
continue # ignore cette UE car capitalisée et actuelle moins bonne
|
||||
if ue_pair.niveau_competence_id == ue_impair.niveau_competence_id:
|
||||
rcue = RegroupementCoherentUE(
|
||||
self.etud,
|
||||
self.formsemestre_impair,
|
||||
ue_impair,
|
||||
self.decisions_ues[ue_impair.id],
|
||||
self.formsemestre_pair,
|
||||
ue_pair,
|
||||
self.decisions_ues[ue_pair.id],
|
||||
self.inscription_etat,
|
||||
)
|
||||
ues_impair_sans_rcue.discard(ue_impair.id)
|
||||
break
|
||||
if rcue is None:
|
||||
raise NoRCUEError(deca=self, ue=ue_pair)
|
||||
# if rcue is None and not self.a_cheval:
|
||||
# raise NoRCUEError(deca=self, ue=ue_pair)
|
||||
if rcue is not None:
|
||||
rcues_annee.append(rcue)
|
||||
if len(ues_impair_sans_rcue) > 0:
|
||||
ue = UniteEns.query.get(ues_impair_sans_rcue.pop())
|
||||
raise NoRCUEError(deca=self, ue=ue)
|
||||
# Si jury annuel (pas à cheval), on doit avoir tous les RCUEs:
|
||||
# if len(ues_impair_sans_rcue) > 0 and not self.a_cheval:
|
||||
# ue = UniteEns.query.get(ues_impair_sans_rcue.pop())
|
||||
# raise NoRCUEError(deca=self, ue=ue)
|
||||
return rcues_annee
|
||||
|
||||
def compute_decisions_niveaux(self) -> dict[int, "DecisionsProposeesRCUE"]:
|
||||
"""Pour chaque niveau de compétence de cette année, construit
|
||||
le DecisionsProposeesRCUE,
|
||||
ou None s'il n'y en a pas
|
||||
le DecisionsProposeesRCUE, ou None s'il n'y en a pas
|
||||
(ne devrait pas arriver car compute_rcues_annee vérifie déjà cela).
|
||||
|
||||
Appelé à la construction du deca, donc avant décisions manuelles.
|
||||
Return: { niveau_id : DecisionsProposeesRCUE }
|
||||
"""
|
||||
# Retrouve le RCUE associé à chaque niveau
|
||||
@ -562,22 +633,42 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
d[dec_rcue.rcue.ue_2.id] = dec_rcue
|
||||
return d
|
||||
|
||||
def next_annee_semestre_id(self, code: str) -> int:
|
||||
"""L'indice du semestre dans lequel l'étudiant est autorisé à
|
||||
poursuivre l'année suivante. None si aucun."""
|
||||
if self.formsemestre_pair is None:
|
||||
return None # seulement sur année
|
||||
if code == RED:
|
||||
return self.formsemestre_pair.semestre_id - 1
|
||||
elif (
|
||||
code in sco_codes.BUT_CODES_PASSAGE
|
||||
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.
|
||||
"""
|
||||
ids = set()
|
||||
# La poursuite d'études dans un semestre pair d’une même année
|
||||
# est de droit pour tout étudiant:
|
||||
if (self.formsemestre.semestre_id % 2) and sco_codes.ParcoursBUT.NB_SEM:
|
||||
ids.add(self.formsemestre.semestre_id + 1)
|
||||
|
||||
# La poursuite d’études dans un semestre impair est possible si
|
||||
# et seulement si l’étudiant a obtenu :
|
||||
# - la moyenne à plus de la moitié des regroupements cohérents d’UE ;
|
||||
# - et une moyenne égale ou supérieure à 8 sur 20 à chaque RCUE.
|
||||
#
|
||||
# La condition a paru trop stricte à de nombreux collègues.
|
||||
# ScoDoc ne contraint donc pas à la respecter strictement.
|
||||
# Si le code est dans BUT_CODES_PASSAGE (ADM, ADJ, PASD, PAS1NCI, ATJ),
|
||||
# autorise à passer dans le semestre suivant
|
||||
if (
|
||||
self.jury_annuel
|
||||
and code in sco_codes.BUT_CODES_PASSAGE
|
||||
and self.formsemestre_pair.semestre_id < sco_codes.ParcoursBUT.NB_SEM
|
||||
):
|
||||
return self.formsemestre_pair.semestre_id + 1
|
||||
return None
|
||||
ids.add(self.formsemestre.semestre_id + 1)
|
||||
|
||||
if code == RED:
|
||||
ids.add(
|
||||
self.formsemestre.semestre_id - (self.formsemestre.semestre_id + 1) % 2
|
||||
)
|
||||
|
||||
return ids
|
||||
|
||||
def record_form(self, form: dict):
|
||||
"""Enregistre les codes de jury en base
|
||||
à partir d'un dict représentant le formulaire jury BUT:
|
||||
form dict:
|
||||
- 'code_ue_1896' : 'AJ' code pour l'UE id 1896
|
||||
- 'code_rcue_6" : 'ADM' code pour le RCUE du niveau 6
|
||||
@ -587,7 +678,9 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
et qu'il n'y en a pas déjà, enregistre ceux par défaut.
|
||||
"""
|
||||
log("jury_but.DecisionsProposeesAnnee.record_form")
|
||||
with sco_cache.DeferredSemCacheManager():
|
||||
code_annee = None
|
||||
codes_rcues = [] # [ (dec_rcue, code), ... ]
|
||||
codes_ues = [] # [ (dec_ue, code), ... ]
|
||||
for key in form:
|
||||
code = form[key]
|
||||
# Codes d'UE
|
||||
@ -597,7 +690,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
dec_ue = self.decisions_ues.get(ue_id)
|
||||
if not dec_ue:
|
||||
raise ScoValueError(f"UE invalide ue_id={ue_id}")
|
||||
dec_ue.record(code)
|
||||
codes_ues.append((dec_ue, code))
|
||||
else:
|
||||
# Codes de RCUE
|
||||
m = re.match(r"^code_rcue_(\d+)$", key)
|
||||
@ -606,31 +699,39 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
dec_rcue = self.decisions_rcue_by_niveau.get(niveau_id)
|
||||
if not dec_rcue:
|
||||
raise ScoValueError(f"RCUE invalide niveau_id={niveau_id}")
|
||||
dec_rcue.record(code)
|
||||
codes_rcues.append((dec_rcue, code))
|
||||
elif key == "code_annee":
|
||||
# Code annuel
|
||||
self.record(code)
|
||||
code_annee = code
|
||||
|
||||
with sco_cache.DeferredSemCacheManager():
|
||||
# Enregistre les codes, dans l'ordre UE, RCUE, Année
|
||||
for dec_ue, code in codes_ues:
|
||||
dec_ue.record(code)
|
||||
for dec_rcue, code in codes_rcues:
|
||||
dec_rcue.record(code)
|
||||
self.record(code_annee)
|
||||
self.record_all()
|
||||
|
||||
db.session.commit()
|
||||
|
||||
def record(self, code: str, no_overwrite=False):
|
||||
def record(self, code: str, no_overwrite=False) -> bool:
|
||||
"""Enregistre le code de l'année, et au besoin l'autorisation d'inscription.
|
||||
Si no_overwrite, ne fait rien si un code est déjà enregistré.
|
||||
Si l'étudiant est DEM ou DEF, ne fait rien.
|
||||
"""
|
||||
if self.inscription_etat != scu.INSCRIT:
|
||||
return
|
||||
return False
|
||||
if code and not code in self.codes:
|
||||
raise ScoValueError(
|
||||
f"code annee <tt>{html.escape(code)}</tt> invalide pour formsemestre {html.escape(self.formsemestre)}"
|
||||
)
|
||||
if code == self.code_valide or (self.code_valide is not None and no_overwrite):
|
||||
self.recorded = True
|
||||
return # no change
|
||||
|
||||
if code != self.code_valide and (self.code_valide is None or not no_overwrite):
|
||||
# Enregistrement du code annuel BUT
|
||||
if self.validation:
|
||||
db.session.delete(self.validation)
|
||||
db.session.flush()
|
||||
db.session.commit()
|
||||
if code is None:
|
||||
self.validation = None
|
||||
else:
|
||||
@ -641,31 +742,32 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
annee_scolaire=self.annee_scolaire(),
|
||||
code=code,
|
||||
)
|
||||
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.add(self.validation)
|
||||
|
||||
# --- Autorisation d'inscription dans semestre suivant ?
|
||||
if self.formsemestre_pair is not None:
|
||||
if code is None:
|
||||
ScolarAutorisationInscription.delete_autorisation_etud(
|
||||
etudid=self.etud.id,
|
||||
origin_formsemestre_id=self.formsemestre_pair.id,
|
||||
origin_formsemestre_id=self.formsemestre.id,
|
||||
)
|
||||
else:
|
||||
next_semestre_id = self.next_annee_semestre_id(code)
|
||||
if next_semestre_id is not None:
|
||||
for next_semestre_id in self.next_semestre_ids(code):
|
||||
ScolarAutorisationInscription.autorise_etud(
|
||||
self.etud.id,
|
||||
self.formsemestre_pair.formation.formation_code,
|
||||
self.formsemestre_pair.id,
|
||||
self.formsemestre.formation.formation_code,
|
||||
self.formsemestre.id,
|
||||
next_semestre_id,
|
||||
)
|
||||
|
||||
db.session.commit()
|
||||
self.recorded = True
|
||||
self.invalidate_formsemestre_cache()
|
||||
return True
|
||||
|
||||
def invalidate_formsemestre_cache(self):
|
||||
"invalide le résultats des deux formsemestres"
|
||||
@ -676,29 +778,72 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
if self.formsemestre_pair is not None:
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre_pair.id)
|
||||
|
||||
def record_all(self):
|
||||
def record_all(
|
||||
self, no_overwrite: bool = True, 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"
|
||||
et sont donc en mode "automatique".
|
||||
- Si "à cheval", ne modifie pas les codes UE de l'année scolaire précédente.
|
||||
- Pour les RCUE: n'enregistre que si la nouvelle décision est plus favorable que l'ancienne.
|
||||
|
||||
Si only_validantes, n'enregistre que des décisions "validantes" de droit: ADM ou CMP.
|
||||
|
||||
Return: True si au moins un code modifié et enregistré.
|
||||
"""
|
||||
decisions = (
|
||||
list(self.decisions_ues.values())
|
||||
+ list(self.decisions_rcue_by_niveau.values())
|
||||
+ [self]
|
||||
)
|
||||
for dec in decisions:
|
||||
if not dec.recorded:
|
||||
modif = False
|
||||
# Toujours valider dans l'ordre UE, RCUE, Année
|
||||
annee_scolaire = self.formsemestre.annee_scolaire()
|
||||
# UEs
|
||||
for dec_ue in self.decisions_ues.values():
|
||||
if (
|
||||
not dec_ue.recorded
|
||||
) and dec_ue.formsemestre.annee_scolaire() == annee_scolaire:
|
||||
# rappel: le code par défaut est en tête
|
||||
code = dec.codes[0] if dec.codes else None
|
||||
# s'il n'y a pas de code, efface
|
||||
dec.record(code, no_overwrite=True)
|
||||
code = dec_ue.codes[0] if dec_ue.codes else None
|
||||
if (not only_validantes) or code in sco_codes.CODES_UE_VALIDES_DE_DROIT:
|
||||
# enregistre le code jury seulement s'il n'y a pas déjà de code
|
||||
# (no_overwrite=True) sauf en mode test yaml
|
||||
modif |= dec_ue.record(code, no_overwrite=no_overwrite)
|
||||
# RCUE :
|
||||
for dec_rcue in self.decisions_rcue_by_niveau.values():
|
||||
code = dec_rcue.codes[0] if dec_rcue.codes else None
|
||||
if (
|
||||
(not dec_rcue.recorded)
|
||||
and ( # enregistre seulement si pas déjà validé "mieux"
|
||||
(not dec_rcue.validation)
|
||||
or BUT_CODES_ORDERED.get(dec_rcue.validation.code, 0)
|
||||
< BUT_CODES_ORDERED.get(code, 0)
|
||||
)
|
||||
and ( # décision validante de droit ?
|
||||
(
|
||||
(not only_validantes)
|
||||
or code in sco_codes.CODES_RCUE_VALIDES_DE_DROIT
|
||||
)
|
||||
)
|
||||
):
|
||||
modif |= dec_rcue.record(code, no_overwrite=no_overwrite)
|
||||
# Année:
|
||||
if not self.recorded:
|
||||
# rappel: le code par défaut est en tête
|
||||
code = self.codes[0] if self.codes else None
|
||||
# enregistre le code jury seulement s'il n'y a pas déjà de code
|
||||
# (no_overwrite=True) sauf en mode test yaml
|
||||
if (
|
||||
not only_validantes
|
||||
) or code in sco_codes.CODES_ANNEE_BUT_VALIDES_DE_DROIT:
|
||||
modif |= self.record(code, no_overwrite=no_overwrite)
|
||||
|
||||
return modif
|
||||
|
||||
def erase(self, only_one_sem=False):
|
||||
"""Efface les décisions de jury de cet étudiant
|
||||
pour cette année: décisions d'UE, de RCUE, d'année,
|
||||
et autorisations d'inscription émises.
|
||||
Efface même si étudiant DEM ou DEF.
|
||||
Si à cheval, n'efface que pour le semestre d'origine du deca.
|
||||
(commite la session.)
|
||||
"""
|
||||
if only_one_sem:
|
||||
if only_one_sem or self.a_cheval:
|
||||
# N'efface que les autorisations venant de ce semestre,
|
||||
# et les validations de ses UEs
|
||||
ScolarAutorisationInscription.delete_autorisation_etud(
|
||||
@ -727,22 +872,37 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
)
|
||||
for validation in validations:
|
||||
db.session.delete(validation)
|
||||
db.session.flush()
|
||||
Scolog.logdb(
|
||||
"jury_but",
|
||||
etudid=self.etud.id,
|
||||
msg=f"Validation année BUT{self.annee_but}: 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 ?)
|
||||
#
|
||||
for validation in ScolarFormSemestreValidation.query.filter_by(
|
||||
etudid=self.etud.id, formsemestre_id=self.formsemestre_id
|
||||
):
|
||||
db.session.delete(validation)
|
||||
|
||||
db.session.commit()
|
||||
self.invalidate_formsemestre_cache()
|
||||
|
||||
def get_autorisations_passage(self) -> list[int]:
|
||||
"""Les liste des indices de semestres auxquels on est autorisé à
|
||||
s'inscrire depuis cette année"""
|
||||
formsemestre = self.formsemestre_pair or self.formsemestre_impair
|
||||
if not formsemestre:
|
||||
return []
|
||||
return [
|
||||
"""Liste des indices de semestres auxquels on est autorisé à
|
||||
s'inscrire depuis le semestre courant.
|
||||
"""
|
||||
return sorted(
|
||||
[
|
||||
a.semestre_id
|
||||
for a in ScolarAutorisationInscription.query.filter_by(
|
||||
etudid=self.etud.id,
|
||||
origin_formsemestre_id=formsemestre.id,
|
||||
origin_formsemestre_id=self.formsemestre.id,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
def descr_niveaux_validation(self, line_sep: str = "\n") -> str:
|
||||
"""Description textuelle des niveaux validés (enregistrés)
|
||||
@ -770,11 +930,34 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
validations.append(", ".join(v for v in valids if v))
|
||||
return line_sep.join(validations)
|
||||
|
||||
def descr_pb_coherence(self) -> list[str]:
|
||||
"""Description d'éventuels problèmes de cohérence entre
|
||||
les décisions *enregistrées* d'UE et de RCUE.
|
||||
Note: en principe, la cohérence RCUE/UE est assurée au moment de
|
||||
l'enregistrement (record).
|
||||
Mais la base peut avoir été modifiée par d'autres voies.
|
||||
"""
|
||||
messages = []
|
||||
for dec_rcue in self.decisions_rcue_by_niveau.values():
|
||||
if dec_rcue.code_valide in CODES_RCUE_VALIDES:
|
||||
for ue in (dec_rcue.rcue.ue_1, dec_rcue.rcue.ue_2):
|
||||
dec_ue = self.decisions_ues.get(ue.id)
|
||||
if dec_ue:
|
||||
if dec_ue.code_valide not in CODES_UE_VALIDES:
|
||||
messages.append(
|
||||
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 (???)")
|
||||
return messages
|
||||
|
||||
|
||||
def list_ue_parcour_etud(
|
||||
formsemestre: FormSemestre, etud: Identite, res: ResultatsSemestreBUT
|
||||
) -> tuple[ApcParcours, list[UniteEns]]:
|
||||
"""Parcour dans lequel l'étudiant est inscrit, et liste des UEs pour ce semestre"""
|
||||
"""Parcour dans lequel l'étudiant est inscrit,
|
||||
et liste des UEs à valider pour ce semestre (sans les UE "dispensées")
|
||||
"""
|
||||
if res.etuds_parcour_id[etud.id] is None:
|
||||
parcour = None
|
||||
# pas de parcour: prend toutes les UEs (non bonus)
|
||||
@ -788,6 +971,7 @@ def list_ue_parcour_etud(
|
||||
.order_by(UniteEns.numero)
|
||||
.all()
|
||||
)
|
||||
ues = [ue for ue in ues if (etud.id, ue.id) not in res.dispense_ues]
|
||||
return parcour, ues
|
||||
|
||||
|
||||
@ -813,6 +997,7 @@ class DecisionsProposeesRCUE(DecisionsProposees):
|
||||
inscription_etat: str = scu.INSCRIT,
|
||||
):
|
||||
super().__init__(etud=dec_prop_annee.etud)
|
||||
self.deca = dec_prop_annee
|
||||
self.rcue = rcue
|
||||
if rcue is None: # RCUE non dispo, eg un seul semestre
|
||||
self.codes = []
|
||||
@ -844,30 +1029,48 @@ class DecisionsProposeesRCUE(DecisionsProposees):
|
||||
or dec_prop_annee.formsemestre_pair.modalite == "EXT"
|
||||
):
|
||||
self.codes.insert(0, sco_codes.ADM)
|
||||
# S'il y a une décision enregistrée: si elle est plus favorable que celle que l'on
|
||||
# proposerait, la place en tête.
|
||||
# Sinon, la place en seconde place
|
||||
if self.code_valide and self.code_valide != self.codes[0]:
|
||||
code_default = self.codes[0]
|
||||
if self.code_valide in self.codes:
|
||||
self.codes.remove(self.code_valide)
|
||||
if sco_codes.BUT_CODES_ORDERED.get(
|
||||
self.code_valide, 0
|
||||
) > sco_codes.BUT_CODES_ORDERED.get(code_default, 0):
|
||||
self.codes.insert(0, self.code_valide)
|
||||
else:
|
||||
self.codes.insert(1, self.code_valide)
|
||||
|
||||
def record(self, code: str, no_overwrite=False):
|
||||
"""Enregistre le code"""
|
||||
def __repr__(self) -> str:
|
||||
return f"""<{self.__class__.__name__} rcue={self.rcue} valid={self.code_valide
|
||||
} codes={self.codes} explanation={self.explanation}"""
|
||||
|
||||
def record(self, code: str, no_overwrite=False) -> bool:
|
||||
"""Enregistre le code RCUE.
|
||||
Note:
|
||||
- si le RCUE est ADJ, les UE non validées sont passées à ADJ
|
||||
XXX on pourra imposer ici d'autres règles de cohérence
|
||||
"""
|
||||
if self.rcue is None:
|
||||
return # pas de RCUE a enregistrer
|
||||
return False # pas de RCUE a enregistrer
|
||||
if self.inscription_etat != scu.INSCRIT:
|
||||
return
|
||||
return False
|
||||
if code and not code in self.codes:
|
||||
raise ScoValueError(
|
||||
f"code UE invalide pour ue_id={self.ue.id}: {html.escape(code)}"
|
||||
)
|
||||
if code == self.code_valide or (self.code_valide is not None and no_overwrite):
|
||||
self.recorded = True
|
||||
return # no change
|
||||
return False # no change
|
||||
parcours_id = self.parcour.id if self.parcour is not None else None
|
||||
if self.validation:
|
||||
db.session.delete(self.validation)
|
||||
db.session.flush()
|
||||
db.session.commit()
|
||||
if code is None:
|
||||
self.validation = None
|
||||
else:
|
||||
# log(
|
||||
# f"RCUE.record(etudid={self.etud.id}, ue1_id={self.rcue.ue_1.id}, ue2_id={self.rcue.ue_2.id}, code={code} )"
|
||||
# )
|
||||
self.validation = ApcValidationRCUE(
|
||||
etudid=self.etud.id,
|
||||
formsemestre_id=self.rcue.formsemestre_2.id,
|
||||
@ -876,12 +1079,31 @@ class DecisionsProposeesRCUE(DecisionsProposees):
|
||||
parcours_id=parcours_id,
|
||||
code=code,
|
||||
)
|
||||
db.session.add(self.validation)
|
||||
db.session.commit()
|
||||
Scolog.logdb(
|
||||
method="jury_but",
|
||||
etudid=self.etud.id,
|
||||
msg=f"Validation RCUE {repr(self.rcue)}",
|
||||
msg=f"Validation {self.rcue}: {code}",
|
||||
commit=True,
|
||||
)
|
||||
db.session.add(self.validation)
|
||||
log(f"rcue.record {self}: {code}")
|
||||
|
||||
# Modifie au besoin les codes d'UE
|
||||
if code == "ADJ":
|
||||
deca = self.deca
|
||||
for ue_id in (self.rcue.ue_1.id, self.rcue.ue_2.id):
|
||||
dec_ue = deca.decisions_ues.get(ue_id)
|
||||
if dec_ue and dec_ue.code_valide not in CODES_UE_VALIDES:
|
||||
log(f"rcue.record: force ADJR sur {dec_ue}")
|
||||
flash(
|
||||
f"""UEs du RCUE "{dec_ue.ue.niveau_competence.competence.titre}" passées en ADJR"""
|
||||
)
|
||||
dec_ue.record(sco_codes.ADJR)
|
||||
|
||||
# Valide les niveaux inférieurs de la compétence (code ADSUP)
|
||||
# TODO
|
||||
|
||||
if self.rcue.formsemestre_1 is not None:
|
||||
sco_cache.invalidate_formsemestre(
|
||||
formsemestre_id=self.rcue.formsemestre_1.id
|
||||
@ -890,13 +1112,16 @@ class DecisionsProposeesRCUE(DecisionsProposees):
|
||||
sco_cache.invalidate_formsemestre(
|
||||
formsemestre_id=self.rcue.formsemestre_2.id
|
||||
)
|
||||
self.code_valide = code # mise à jour état
|
||||
self.recorded = True
|
||||
return True
|
||||
|
||||
def erase(self):
|
||||
"""Efface la décision de jury de cet étudiant pour cet RCUE"""
|
||||
# par prudence, on requete toutes les validations, en cas de doublons
|
||||
validations = self.rcue.query_validations()
|
||||
for validation in validations:
|
||||
log(f"DecisionsProposeesRCUE: deleting {validation}")
|
||||
db.session.delete(validation)
|
||||
db.session.flush()
|
||||
|
||||
@ -928,7 +1153,7 @@ class DecisionsProposeesUE(DecisionsProposees):
|
||||
sinon si compensation dans RCUE: CMP
|
||||
sinon: ADJ, AJ
|
||||
|
||||
et proposer toujours: RAT, DEF, ABAN, DEM, UEBSL (codes_communs)
|
||||
et proposer toujours: RAT, DEF, ABAN, ADJR, DEM, UEBSL (codes_communs)
|
||||
"""
|
||||
|
||||
# Codes toujours proposés sauf si include_communs est faux:
|
||||
@ -936,6 +1161,7 @@ class DecisionsProposeesUE(DecisionsProposees):
|
||||
sco_codes.RAT,
|
||||
sco_codes.DEF,
|
||||
sco_codes.ABAN,
|
||||
sco_codes.ADJR,
|
||||
sco_codes.ATJ,
|
||||
sco_codes.DEM,
|
||||
sco_codes.UEBSL,
|
||||
@ -950,20 +1176,24 @@ class DecisionsProposeesUE(DecisionsProposees):
|
||||
):
|
||||
# Une UE peut être validée plusieurs fois en cas de redoublement (qu'elle soit capitalisée ou non)
|
||||
# mais ici on a restreint au formsemestre donc une seule (prend la première)
|
||||
self.validation = ScolarFormSemestreValidation.query.filter_by(
|
||||
validation = ScolarFormSemestreValidation.query.filter_by(
|
||||
etudid=etud.id, formsemestre_id=formsemestre.id, ue_id=ue.id
|
||||
).first()
|
||||
super().__init__(
|
||||
etud=etud,
|
||||
code_valide=self.validation.code if self.validation is not None else None,
|
||||
code_valide=validation.code if validation is not None else None,
|
||||
)
|
||||
# log(f"built {self}")
|
||||
self.validation = validation
|
||||
self.formsemestre = formsemestre
|
||||
self.ue: UniteEns = ue
|
||||
self.rcue: RegroupementCoherentUE = None
|
||||
"Le rcue auquel est rattaché cette UE, ou None"
|
||||
self.inscription_etat = inscription_etat
|
||||
"inscription: I, DEM, DEF dans le semestre de cette UE"
|
||||
self.moy_ue = np.NaN
|
||||
self.moy_ue_with_cap = np.NaN
|
||||
self.ue_status = {}
|
||||
|
||||
if ue.type == sco_codes.UE_SPORT:
|
||||
self.explanation = "UE bonus, pas de décision de jury"
|
||||
self.codes = [] # aucun code proposé
|
||||
@ -974,22 +1204,29 @@ class DecisionsProposeesUE(DecisionsProposees):
|
||||
self.codes = [
|
||||
sco_codes.DEM if inscription_etat == scu.DEMISSION else sco_codes.DEF
|
||||
]
|
||||
self.moy_ue = np.NaN
|
||||
return
|
||||
|
||||
# Moyenne de l'UE ?
|
||||
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
|
||||
# Safety checks:
|
||||
if not ue.id in res.etud_moy_ue:
|
||||
self.explanation = "UE sans résultat"
|
||||
return
|
||||
if not etud.id in res.etud_moy_ue[ue.id]:
|
||||
self.explanation = "Étudiant sans résultat dans cette UE"
|
||||
return
|
||||
self.moy_ue = res.etud_moy_ue[ue.id][etud.id]
|
||||
ue_status = res.get_etud_ue_status(etud.id, ue.id)
|
||||
self.moy_ue = ue_status["cur_moy_ue"]
|
||||
self.moy_ue_with_cap = ue_status["moy"]
|
||||
self.ue_status = ue_status
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"""<{self.__class__.__name__} ue={self.ue.acronyme} valid={self.code_valide
|
||||
} codes={self.codes} explanation={self.explanation}>"""
|
||||
|
||||
def set_rcue(self, rcue: RegroupementCoherentUE):
|
||||
"""Rattache cette UE à un RCUE. Cela peut modifier les codes
|
||||
proposés (si compensation)"""
|
||||
proposés par compute_codes() (si compensation)"""
|
||||
self.rcue = rcue
|
||||
|
||||
def compute_codes(self):
|
||||
@ -1000,7 +1237,7 @@ class DecisionsProposeesUE(DecisionsProposees):
|
||||
self.moy_ue > (sco_codes.ParcoursBUT.BARRE_MOY - sco_codes.NOTES_TOLERANCE)
|
||||
) or self.formsemestre.modalite == "EXT":
|
||||
self.codes.insert(0, sco_codes.ADM)
|
||||
self.explanation = (f"Moyenne >= {sco_codes.ParcoursBUT.BARRE_MOY}/20",)
|
||||
self.explanation = f"Moyenne >= {sco_codes.ParcoursBUT.BARRE_MOY}/20"
|
||||
elif self.rcue and self.rcue.est_compensable():
|
||||
self.codes.insert(0, sco_codes.CMP)
|
||||
self.explanation = "compensable dans le RCUE"
|
||||
@ -1009,9 +1246,10 @@ class DecisionsProposeesUE(DecisionsProposees):
|
||||
self.codes = [sco_codes.AJ, sco_codes.ADJ] + self.codes
|
||||
self.explanation = "notes insuffisantes"
|
||||
|
||||
def record(self, code: str, no_overwrite=False):
|
||||
def record(self, code: str, no_overwrite=False) -> bool:
|
||||
"""Enregistre le code jury pour cette UE.
|
||||
Si no_overwrite, n'enregistre pas s'il y a déjà un code.
|
||||
Return: True si code enregistré (modifié)
|
||||
"""
|
||||
if code and not code in self.codes:
|
||||
raise ScoValueError(
|
||||
@ -1019,10 +1257,16 @@ class DecisionsProposeesUE(DecisionsProposees):
|
||||
)
|
||||
if code == self.code_valide or (self.code_valide is not None and no_overwrite):
|
||||
self.recorded = True
|
||||
return # no change
|
||||
return False # no change
|
||||
self.erase()
|
||||
if code is None:
|
||||
self.validation = None
|
||||
Scolog.logdb(
|
||||
method="jury_but",
|
||||
etudid=self.etud.id,
|
||||
msg=f"Validation UE {self.ue.id} {self.ue.acronyme}: effacée",
|
||||
commit=True,
|
||||
)
|
||||
else:
|
||||
self.validation = ScolarFormSemestreValidation(
|
||||
etudid=self.etud.id,
|
||||
@ -1031,16 +1275,20 @@ class DecisionsProposeesUE(DecisionsProposees):
|
||||
code=code,
|
||||
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}",
|
||||
msg=f"Validation UE {self.ue.id} {self.ue.acronyme}({self.moy_ue}): {code}",
|
||||
commit=True,
|
||||
)
|
||||
db.session.add(self.validation)
|
||||
log(f"DecisionsProposeesUE: recording {self.validation}")
|
||||
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre.id)
|
||||
self.code_valide = code # mise à jour
|
||||
self.recorded = True
|
||||
return True
|
||||
|
||||
def erase(self):
|
||||
"""Efface la décision de jury de cet étudiant pour cette UE"""
|
||||
@ -1051,7 +1299,13 @@ class DecisionsProposeesUE(DecisionsProposees):
|
||||
for validation in validations:
|
||||
log(f"DecisionsProposeesUE: deleting {validation}")
|
||||
db.session.delete(validation)
|
||||
db.session.flush()
|
||||
Scolog.logdb(
|
||||
method="jury_but",
|
||||
etudid=self.etud.id,
|
||||
msg=f"Validation UE {validation.ue.id} {validation.ue.acronyme}: effacée",
|
||||
)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
def descr_validation(self) -> str:
|
||||
"""Description validation niveau enregistrée, pour PV jury.
|
||||
@ -1067,7 +1321,7 @@ class BUTCursusEtud: # WIP TODO
|
||||
|
||||
def __init__(self, formsemestre: FormSemestre, etud: Identite):
|
||||
if formsemestre.formation.referentiel_competence is None:
|
||||
raise ScoException("BUTCursusEtud: pas de référentiel de compétences")
|
||||
raise ScoNoReferentielCompetences(formation=formsemestre.formation)
|
||||
assert len(etud.formsemestre_inscriptions) > 0
|
||||
self.formsemestre = formsemestre
|
||||
self.etud = etud
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -1,12 +1,13 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""Jury BUT: table recap annuelle et liens saisie
|
||||
"""
|
||||
|
||||
import collections
|
||||
import time
|
||||
import numpy as np
|
||||
from flask import g, url_for
|
||||
@ -31,7 +32,7 @@ from app.scodoc.sco_codes_parcours import (
|
||||
from app.scodoc import sco_formsemestre_status
|
||||
from app.scodoc import sco_pvjury
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences
|
||||
|
||||
|
||||
def formsemestre_saisie_jury_but(
|
||||
@ -58,20 +59,13 @@ def formsemestre_saisie_jury_but(
|
||||
# DecisionsProposeesAnnee(etud, formsemestre2)
|
||||
# Pour le 1er etud, faire un check_ues_ready_jury(self) -> page d'erreur
|
||||
# -> rcue .ue_1, .ue_2 -> stroe moy ues, rcue.moy_rcue, etc
|
||||
if formsemestre2.semestre_id % 2 != 0:
|
||||
raise ScoValueError("Cette page ne fonctionne que sur les semestres pairs")
|
||||
# XXX if formsemestre2.semestre_id % 2 != 0:
|
||||
# raise ScoValueError("Cette page ne fonctionne que sur les semestres pairs")
|
||||
|
||||
if formsemestre2.formation.referentiel_competence is None:
|
||||
raise ScoValueError(
|
||||
"""
|
||||
<p>Pas de référentiel de compétences associé à la formation !</p>
|
||||
<p>Pour associer un référentiel, passer par le menu <b>Semestre /
|
||||
Voir la formation... </b> et suivre le lien <em>"associer à un référentiel
|
||||
de compétences"</em>
|
||||
"""
|
||||
)
|
||||
raise ScoNoReferentielCompetences(formation=formsemestre2.formation)
|
||||
|
||||
rows, titles, column_ids = get_jury_but_table(
|
||||
rows, titles, column_ids, jury_stats = get_jury_but_table(
|
||||
formsemestre2, read_only=read_only, mode=mode
|
||||
)
|
||||
if not rows:
|
||||
@ -153,6 +147,28 @@ def formsemestre_saisie_jury_but(
|
||||
f"""
|
||||
</div>
|
||||
|
||||
<div class="jury_stats">
|
||||
<div>Nb d'étudiants avec décision annuelle:
|
||||
{sum(jury_stats["codes_annuels"].values())} / {jury_stats["nb_etuds"]}
|
||||
</div>
|
||||
<div><b>Codes annuels octroyés:</b></div>
|
||||
<table class="jury_stats_codes">
|
||||
"""
|
||||
)
|
||||
for code in sorted(jury_stats["codes_annuels"].keys()):
|
||||
H.append(
|
||||
f"""<tr>
|
||||
<td>{code}</td>
|
||||
<td style="text-align:right">{jury_stats["codes_annuels"][code]}</td>
|
||||
<td style="text-align:right">{
|
||||
(100*jury_stats["codes_annuels"][code] / jury_stats["nb_etuds"]):2.1f}%
|
||||
</td>
|
||||
</tr>"""
|
||||
)
|
||||
H.append(
|
||||
f"""
|
||||
</table>
|
||||
</div>
|
||||
{html_sco_header.sco_footer()}
|
||||
"""
|
||||
)
|
||||
@ -262,12 +278,16 @@ class RowCollector:
|
||||
# --- Codes (seront cachés, mais exportés en excel)
|
||||
self.add_cell("etudid", "etudid", etud.id, "codes")
|
||||
self.add_cell("code_nip", "code_nip", etud.code_nip or "", "codes")
|
||||
# --- Identité étudiant (adapté de res_comon/get_table_recap, à factoriser XXX TODO)
|
||||
# --- Identité étudiant (adapté de res_common/get_table_recap, à factoriser XXX TODO)
|
||||
self.add_cell("civilite_str", "Civ.", etud.civilite_str, "identite_detail")
|
||||
self.add_cell("nom_disp", "Nom", etud.nom_disp(), "identite_detail")
|
||||
self["_nom_disp_order"] = etud.sort_key
|
||||
self.add_cell("prenom", "Prénom", etud.prenom, "identite_detail")
|
||||
self.add_cell("nom_short", "Nom", etud.nom_short, "identite_court")
|
||||
self["_nom_short_data"] = {
|
||||
"etudid": etud.id,
|
||||
"nomprenom": etud.nomprenom,
|
||||
}
|
||||
if with_links:
|
||||
self["_nom_short_order"] = etud.sort_key
|
||||
self["_nom_short_target"] = url_for(
|
||||
@ -352,10 +372,6 @@ class RowCollector:
|
||||
+ ((" " + scu.EMO_WARNING) if deca.nb_rcues_under_8 > 0 else ""),
|
||||
"col_rcue col_rcues_validables" + klass,
|
||||
)
|
||||
self["_rcues_validables_data"] = {
|
||||
"etudid": deca.etud.id,
|
||||
"nomprenom": deca.etud.nomprenom,
|
||||
}
|
||||
if len(deca.rcues_annee) > 0:
|
||||
# permet un tri par nb de niveaux validables + moyenne gen indicative S_pair
|
||||
if deca.res_pair and deca.etud.id in deca.res_pair.etud_moy_gen:
|
||||
@ -377,10 +393,17 @@ class RowCollector:
|
||||
|
||||
def get_jury_but_table(
|
||||
formsemestre2: FormSemestre, read_only: bool = False, mode="jury", with_links=True
|
||||
) -> tuple[list[dict], list[str], list[str]]:
|
||||
"""Construit la table des résultats annuels pour le jury BUT"""
|
||||
) -> tuple[list[dict], list[str], list[str], dict]:
|
||||
"""Construit la table des résultats annuels pour le jury BUT
|
||||
=> rows_dict, titles, column_ids, jury_stats
|
||||
où jury_stats est un dict donnant des comptages sur le jury.
|
||||
"""
|
||||
res2: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre2)
|
||||
titles = {} # column_id : title
|
||||
jury_stats = {
|
||||
"nb_etuds": len(formsemestre2.etuds_inscriptions),
|
||||
"codes_annuels": collections.Counter(),
|
||||
}
|
||||
column_classes = {}
|
||||
rows = []
|
||||
for etudid in formsemestre2.etuds_inscriptions:
|
||||
@ -417,6 +440,8 @@ def get_jury_but_table(
|
||||
f"""{deca.code_valide or ''}""",
|
||||
"col_code_annee",
|
||||
)
|
||||
if deca.code_valide:
|
||||
jury_stats["codes_annuels"][deca.code_valide] += 1
|
||||
# --- Le lien de saisie
|
||||
if mode != "recap" and with_links:
|
||||
row.add_cell(
|
||||
@ -439,11 +464,14 @@ def get_jury_but_table(
|
||||
rows.append(row)
|
||||
rows_dict = [row.get_row_dict() for row in rows]
|
||||
if len(rows_dict) > 0:
|
||||
res2.recap_add_partitions(rows_dict, titles, col_idx=row.last_etud_cell_idx + 1)
|
||||
col_idx = res2.recap_add_partitions(
|
||||
rows_dict, titles, col_idx=row.last_etud_cell_idx + 1
|
||||
)
|
||||
res2.recap_add_cursus(rows_dict, titles, col_idx=col_idx + 1)
|
||||
column_ids = [title for title in titles if not title.startswith("_")]
|
||||
column_ids.sort(key=lambda col_id: titles.get("_" + col_id + "_col_order", 1000))
|
||||
rows_dict.sort(key=lambda row: row["_nom_disp_order"])
|
||||
return rows_dict, titles, column_ids
|
||||
return rows_dict, titles, column_ids, jury_stats
|
||||
|
||||
|
||||
def get_jury_but_results(formsemestre: FormSemestre) -> list[dict]:
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -15,20 +15,32 @@ from app.scodoc import sco_cache
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
|
||||
def formsemestre_validation_auto_but(formsemestre: FormSemestre) -> int:
|
||||
"""Calcul automatique des décisions de jury sur une année BUT.
|
||||
Returns: nombre d'étudiants "admis"
|
||||
def formsemestre_validation_auto_but(
|
||||
formsemestre: FormSemestre, only_adm: bool = True, no_overwrite: 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
|
||||
si on a des RCUE "à cheval".
|
||||
- Ne modifie jamais de décisions déjà enregistrées (sauf si no_overwrite est faux,
|
||||
ce qui est utilisé pour certains tests unitaires).
|
||||
- Normalement, only_adm est True et on n'enregistre que les décisions validantes
|
||||
de droit: ADM ou CMP.
|
||||
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)
|
||||
|
||||
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_admis = 0
|
||||
nb_etud_modif = 0
|
||||
with sco_cache.DeferredSemCacheManager():
|
||||
for etudid in formsemestre.etuds_inscriptions:
|
||||
etud: Identite = Identite.query.get(etudid)
|
||||
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
|
||||
if deca.admis: # année réussie
|
||||
deca.record_all()
|
||||
nb_admis += 1
|
||||
nb_etud_modif += deca.record_all(
|
||||
no_overwrite=no_overwrite, only_validantes=only_adm
|
||||
)
|
||||
|
||||
db.session.commit()
|
||||
return nb_admis
|
||||
return nb_etud_modif
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -8,25 +8,34 @@
|
||||
"""
|
||||
|
||||
import re
|
||||
import numpy as np
|
||||
|
||||
import flask
|
||||
from flask import flash, url_for
|
||||
from flask import flash, render_template, url_for
|
||||
from flask import g, request
|
||||
|
||||
from app import db
|
||||
from app.but import jury_but
|
||||
from app.but.jury_but import DecisionsProposeesAnnee, DecisionsProposeesUE
|
||||
from app.but.jury_but import (
|
||||
DecisionsProposeesAnnee,
|
||||
DecisionsProposeesRCUE,
|
||||
DecisionsProposeesUE,
|
||||
)
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.models import (
|
||||
ApcNiveau,
|
||||
FormSemestre,
|
||||
FormSemestreInscription,
|
||||
Identite,
|
||||
UniteEns,
|
||||
ScolarAutorisationInscription,
|
||||
ScolarFormSemestreValidation,
|
||||
)
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
@ -35,14 +44,8 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
|
||||
Si pas read_only, menus sélection codes jury.
|
||||
"""
|
||||
H = []
|
||||
if deca.code_valide and not read_only:
|
||||
erase_span = f"""<a href="{
|
||||
url_for("notes.formsemestre_jury_but_erase",
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=deca.formsemestre_id,
|
||||
etudid=deca.etud.id)}" class="stdlink">effacer décisions</a>"""
|
||||
else:
|
||||
erase_span = ""
|
||||
|
||||
if deca.jury_annuel:
|
||||
H.append(
|
||||
f"""
|
||||
<div class="but_section_annee">
|
||||
@ -51,21 +54,40 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
|
||||
_gen_but_select("code_annee", deca.codes, deca.code_valide,
|
||||
disabled=True, klass="manual")
|
||||
}
|
||||
<span>({'non ' if deca.code_valide is None else ''}enregistrée)</span>
|
||||
<span>{erase_span}</span>
|
||||
<span>({deca.code_valide or 'non'} enregistrée)</span>
|
||||
</div>
|
||||
<div class="but_explanation">{deca.explanation}</div>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
|
||||
formsemestre_1 = deca.formsemestre_impair
|
||||
formsemestre_2 = deca.formsemestre_pair
|
||||
# Ordonne selon les dates des 2 semestres considérés (pour les redoublants à cheval):
|
||||
reverse_semestre = (
|
||||
deca.formsemestre_pair
|
||||
and deca.formsemestre_impair
|
||||
and deca.formsemestre_pair.date_debut < deca.formsemestre_impair.date_debut
|
||||
)
|
||||
if reverse_semestre:
|
||||
formsemestre_1, formsemestre_2 = formsemestre_2, formsemestre_1
|
||||
H.append(
|
||||
f"""
|
||||
<div><b>Niveaux de compétences et unités d'enseignement :</b></div>
|
||||
<div class="titre_niveaux">
|
||||
<b>Niveaux de compétences et unités d'enseignement du BUT{deca.annee_but}</b>
|
||||
</div>
|
||||
<div class="but_explanation">{deca.explanation}</div>
|
||||
<div class="but_annee">
|
||||
<div class="titre"></div>
|
||||
<div class="titre">S{1}</div>
|
||||
<div class="titre">S{2}</div>
|
||||
<div class="titre">{"S" +str(formsemestre_1.semestre_id)
|
||||
if formsemestre_1 else "-"}
|
||||
<span class="avertissement_redoublement">{formsemestre_1.annee_scolaire_str()
|
||||
if formsemestre_1 else ""}</span>
|
||||
</div>
|
||||
<div class="titre">{"S"+str(formsemestre_2.semestre_id)
|
||||
if formsemestre_2 else "-"}
|
||||
<span class="avertissement_redoublement">{formsemestre_2.annee_scolaire_str()
|
||||
if formsemestre_2 else ""}</span>
|
||||
</div>
|
||||
<div class="titre">RCUE</div>
|
||||
"""
|
||||
)
|
||||
@ -75,44 +97,52 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
|
||||
<div title="{niveau.competence.titre_long}">{niveau.competence.titre}</div>
|
||||
</div>"""
|
||||
)
|
||||
dec_rcue = deca.decisions_rcue_by_niveau.get(niveau.id)
|
||||
if dec_rcue is None:
|
||||
break
|
||||
# Semestre impair
|
||||
dec_rcue = deca.decisions_rcue_by_niveau.get(niveau.id) # peut être None
|
||||
ues = [
|
||||
ue
|
||||
for ue in deca.ues_impair
|
||||
if ue.niveau_competence and ue.niveau_competence.id == niveau.id
|
||||
]
|
||||
ue_impair = ues[0] if ues else None
|
||||
ues = [
|
||||
ue
|
||||
for ue in deca.ues_pair
|
||||
if ue.niveau_competence and ue.niveau_competence.id == niveau.id
|
||||
]
|
||||
ue_pair = ues[0] if ues else None
|
||||
# Les UEs à afficher,
|
||||
# qui seront toujours en readonly sur le formsemestre de l'année précédente du redoublant
|
||||
ues_ro = [
|
||||
(
|
||||
ue_impair,
|
||||
(deca.a_cheval and deca.formsemestre_id != deca.formsemestre_impair.id),
|
||||
),
|
||||
(
|
||||
ue_pair,
|
||||
deca.a_cheval and deca.formsemestre_id != deca.formsemestre_pair.id,
|
||||
),
|
||||
]
|
||||
# Ordonne selon les dates des 2 semestres considérés:
|
||||
if reverse_semestre:
|
||||
ues_ro[0], ues_ro[1] = ues_ro[1], ues_ro[0]
|
||||
# Colonnes d'UE:
|
||||
for ue, ue_read_only in ues_ro:
|
||||
if ue:
|
||||
H.append(
|
||||
_gen_but_niveau_ue(
|
||||
dec_rcue.rcue.ue_1,
|
||||
deca.decisions_ues[dec_rcue.rcue.ue_1.id].moy_ue,
|
||||
# dec_rcue.rcue.moy_ue_1,
|
||||
deca.decisions_ues[dec_rcue.rcue.ue_1.id],
|
||||
disabled=read_only,
|
||||
ue,
|
||||
deca.decisions_ues[ue.id],
|
||||
disabled=read_only or ue_read_only,
|
||||
annee_prec=ue_read_only,
|
||||
niveau_id=ue.niveau_competence.id,
|
||||
)
|
||||
)
|
||||
# Semestre pair
|
||||
H.append(
|
||||
_gen_but_niveau_ue(
|
||||
dec_rcue.rcue.ue_2,
|
||||
deca.decisions_ues[dec_rcue.rcue.ue_2.id].moy_ue,
|
||||
# dec_rcue.rcue.moy_ue_2,
|
||||
deca.decisions_ues[dec_rcue.rcue.ue_2.id],
|
||||
disabled=read_only,
|
||||
)
|
||||
)
|
||||
# RCUE
|
||||
H.append(
|
||||
f"""<div class="but_niveau_rcue
|
||||
{'recorded' if dec_rcue.code_valide is not None else ''}
|
||||
">
|
||||
<div class="but_note">{scu.fmt_note(dec_rcue.rcue.moy_rcue)}</div>
|
||||
<div class="but_code">{
|
||||
_gen_but_select("code_rcue_"+str(niveau.id),
|
||||
dec_rcue.codes,
|
||||
dec_rcue.code_valide,
|
||||
disabled=True, klass="manual"
|
||||
)
|
||||
}</div>
|
||||
</div>"""
|
||||
)
|
||||
else:
|
||||
H.append("""<div class="niveau_vide"></div>""")
|
||||
|
||||
# Colonne RCUE
|
||||
H.append(_gen_but_rcue(dec_rcue, niveau))
|
||||
|
||||
H.append("</div>") # but_annee
|
||||
return "\n".join(H)
|
||||
|
||||
@ -123,9 +153,12 @@ def _gen_but_select(
|
||||
code_valide: str,
|
||||
disabled: bool = False,
|
||||
klass: str = "",
|
||||
data: dict = {},
|
||||
) -> str:
|
||||
"Le menu html select avec les codes"
|
||||
h = "\n".join(
|
||||
# if disabled: # mauvaise idée car le disabled est traité en JS
|
||||
# return f"""<div class="but_code {klass}">{code_valide}</div>"""
|
||||
options_htm = "\n".join(
|
||||
[
|
||||
f"""<option value="{code}"
|
||||
{'selected' if code == code_valide else ''}
|
||||
@ -136,46 +169,139 @@ def _gen_but_select(
|
||||
)
|
||||
return f"""<select required name="{name}"
|
||||
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);"
|
||||
{"disabled" if disabled else ""}
|
||||
>{h}</select>
|
||||
{" ".join( f'data-{k}="{v}"' for (k,v) in data.items() )}
|
||||
>{options_htm}</select>
|
||||
"""
|
||||
|
||||
|
||||
def _gen_but_niveau_ue(
|
||||
ue: UniteEns, moy_ue: float, dec_ue: DecisionsProposeesUE, disabled=False
|
||||
):
|
||||
ue: UniteEns,
|
||||
dec_ue: DecisionsProposeesUE,
|
||||
disabled: bool = False,
|
||||
annee_prec: bool = False,
|
||||
niveau_id: int = None,
|
||||
) -> 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>"""
|
||||
scoplement = f"""<div class="scoplement">
|
||||
<div>
|
||||
<b>UE {ue.acronyme} capitalisée </b>
|
||||
<span>le {dec_ue.ue_status["event_date"].strftime("%d/%m/%Y")}
|
||||
</span>
|
||||
</div>
|
||||
<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>
|
||||
"""
|
||||
else:
|
||||
moy_ue_str = f"""<span>{scu.fmt_note(dec_ue.moy_ue)}</span>"""
|
||||
if dec_ue.code_valide:
|
||||
scoplement = f"""<div class="scoplement">
|
||||
<div>Code {dec_ue.code_valide} enregistré le {dec_ue.validation.event_date.strftime("%d/%m/%Y")}
|
||||
à {dec_ue.validation.event_date.strftime("%Hh%M")}
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
else:
|
||||
scoplement = ""
|
||||
|
||||
return f"""<div class="but_niveau_ue {
|
||||
'recorded' if dec_ue.code_valide is not None else ''}
|
||||
{'annee_prec' if annee_prec else ''}
|
||||
">
|
||||
<div title="{ue.titre}">{ue.acronyme}</div>
|
||||
<div class="but_note">{scu.fmt_note(moy_ue)}</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),
|
||||
dec_ue.codes,
|
||||
dec_ue.code_valide, disabled=disabled
|
||||
dec_ue.code_valide,
|
||||
disabled=disabled,
|
||||
klass=f"code_ue ue_rcue_{niveau_id}" if not disabled else ""
|
||||
)
|
||||
}</div>
|
||||
|
||||
</div>"""
|
||||
|
||||
|
||||
def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str:
|
||||
if dec_rcue is None:
|
||||
return """
|
||||
<div class="but_niveau_rcue niveau_vide with_scoplement">
|
||||
<div></div>
|
||||
<div class="scoplement">Pas de RCUE (UE non capitalisée ?)</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
scoplement = (
|
||||
f"""<div class="scoplement">{
|
||||
dec_rcue.validation.to_html()
|
||||
}</div>"""
|
||||
if dec_rcue.validation
|
||||
else ""
|
||||
)
|
||||
|
||||
# Déjà enregistré ?
|
||||
niveau_rcue_class = ""
|
||||
if dec_rcue.code_valide is not None and dec_rcue.codes:
|
||||
if dec_rcue.code_valide == dec_rcue.codes[0]:
|
||||
niveau_rcue_class = "recorded"
|
||||
else:
|
||||
niveau_rcue_class = "recorded_different"
|
||||
|
||||
return f"""
|
||||
<div class="but_niveau_rcue {niveau_rcue_class}
|
||||
">
|
||||
<div class="but_note with_scoplement">
|
||||
<div>{scu.fmt_note(dec_rcue.rcue.moy_rcue)}</div>
|
||||
{scoplement}
|
||||
</div>
|
||||
<div class="but_code">
|
||||
{_gen_but_select("code_rcue_"+str(niveau.id),
|
||||
dec_rcue.codes,
|
||||
dec_rcue.code_valide,
|
||||
disabled=True,
|
||||
klass="manual code_rcue",
|
||||
data = { "niveau_id" : str(niveau.id)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def jury_but_semestriel(
|
||||
formsemestre: FormSemestre, etud: Identite, read_only: bool
|
||||
formsemestre: FormSemestre,
|
||||
etud: Identite,
|
||||
read_only: bool,
|
||||
navigation_div: str = "",
|
||||
) -> str:
|
||||
"""Formulaire saisie décision d'UE d'un semestre BUT isolé (pas jury annuel)"""
|
||||
"""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_parcours().NB_SEM
|
||||
)
|
||||
est_autorise_a_passer = (formsemestre.semestre_id + 1) in (
|
||||
a.semestre_id
|
||||
for a in ScolarAutorisationInscription.query.filter_by(
|
||||
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
|
||||
@ -188,9 +314,9 @@ def jury_but_semestriel(
|
||||
for key in request.form:
|
||||
code = request.form[key]
|
||||
# Codes d'UE
|
||||
m = re.match(r"^code_ue_(\d+)$", key)
|
||||
if m:
|
||||
ue_id = int(m.group(1))
|
||||
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}")
|
||||
@ -199,7 +325,9 @@ def jury_but_semestriel(
|
||||
flash("codes enregistrés")
|
||||
if not semestre_terminal:
|
||||
if request.form.get("autorisation_passage"):
|
||||
if not est_autorise_a_passer:
|
||||
if not formsemestre.semestre_id + 1 in (
|
||||
a.semestre_id for a in autorisations_passage
|
||||
):
|
||||
ScolarAutorisationInscription.autorise_etud(
|
||||
etud.id,
|
||||
formsemestre.formation.formation_code,
|
||||
@ -208,7 +336,8 @@ def jury_but_semestriel(
|
||||
)
|
||||
db.session.commit()
|
||||
flash(
|
||||
f"autorisation de passage en S{formsemestre.semestre_id + 1} enregistrée"
|
||||
f"""autorisation de passage en S{formsemestre.semestre_id + 1
|
||||
} enregistrée"""
|
||||
)
|
||||
else:
|
||||
if est_autorise_a_passer:
|
||||
@ -237,7 +366,7 @@ def jury_but_semestriel(
|
||||
warning = ""
|
||||
H = [
|
||||
html_sco_header.sco_header(
|
||||
page_title="Validation BUT",
|
||||
page_title=f"Validation BUT S{formsemestre.semestre_id}",
|
||||
formsemestre_id=formsemestre.id,
|
||||
etudid=etud.id,
|
||||
cssstyles=("css/jury_but.css",),
|
||||
@ -258,27 +387,47 @@ def jury_but_semestriel(
|
||||
}">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a>
|
||||
</div>
|
||||
</div>
|
||||
<h3>Jury sur un semestre BUT isolé</h3>
|
||||
<h3>Jury sur un semestre BUT isolé (ne concerne que les UEs)</h3>
|
||||
{warning}
|
||||
</div>
|
||||
|
||||
<form method="POST">
|
||||
<form method="post" id="jury_but">
|
||||
""",
|
||||
]
|
||||
if (not read_only) and any([dec.code_valide for dec in decisions_ues.values()]):
|
||||
|
||||
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 décisions</a>"""
|
||||
etudid=etud.id, only_one_sem=1)
|
||||
}" class="stdlink">effacer les décisions enregistrées</a>"""
|
||||
else:
|
||||
erase_span = "aucune décision enregistrée pour ce semestre"
|
||||
erase_span = (
|
||||
"Cet étudiant n'a aucune décision enregistrée pour ce semestre."
|
||||
)
|
||||
|
||||
H.append(
|
||||
f"""
|
||||
<div class="but_section_annee">
|
||||
<span>{erase_span}</span>
|
||||
</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>
|
||||
@ -292,7 +441,6 @@ def jury_but_semestriel(
|
||||
H.append(
|
||||
_gen_but_niveau_ue(
|
||||
ue,
|
||||
dec_ue.moy_ue,
|
||||
dec_ue,
|
||||
disabled=read_only,
|
||||
)
|
||||
@ -303,11 +451,27 @@ def jury_but_semestriel(
|
||||
)
|
||||
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(
|
||||
"""<div class="but_explanation">
|
||||
Vous n'avez pas la permission de modifier ces décisions.
|
||||
Les champs entourés en vert sont enregistrés.</div>"""
|
||||
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_parcours().NB_SEM:
|
||||
@ -324,12 +488,26 @@ def jury_but_semestriel(
|
||||
else:
|
||||
H.append("""<div class="help">dernier semestre de la formation.</div>""")
|
||||
H.append(
|
||||
"""
|
||||
f"""
|
||||
<div class="but_buttons">
|
||||
<input type="submit" value="Enregistrer ces décisions">
|
||||
<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)
|
||||
|
||||
|
||||
@ -355,7 +533,6 @@ def infos_fiche_etud_html(etudid: int) -> str:
|
||||
# temporaire quick & dirty: affiche le dernier
|
||||
try:
|
||||
deca = DecisionsProposeesAnnee(etud, formsemestres_but[-1])
|
||||
if len(deca.rcues_annee) > 0:
|
||||
return f"""<div class="infos_but">
|
||||
{show_etud(deca, read_only=True)}
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -18,21 +18,11 @@ import pandas as pd
|
||||
|
||||
from flask import g
|
||||
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||
from app.scodoc.sco_codes_parcours import ParcoursDUT, ParcoursDUTMono
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
|
||||
def get_bonus_sport_class_from_name(dept_id):
|
||||
"""La classe de bonus sport pour le département indiqué.
|
||||
Note: en ScoDoc 9, le bonus sport est défini gloabelement et
|
||||
ne dépend donc pas du département.
|
||||
Résultat: une sous-classe de BonusSport
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class BonusSport:
|
||||
"""Calcul du bonus sport.
|
||||
|
||||
@ -65,7 +55,7 @@ class BonusSport:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
formsemestre: FormSemestre,
|
||||
formsemestre: "FormSemestre",
|
||||
sem_modimpl_moys: np.array,
|
||||
ues: list,
|
||||
modimpl_inscr_df: pd.DataFrame,
|
||||
@ -362,18 +352,37 @@ class BonusAisneStQuentin(BonusSportAdditif):
|
||||
|
||||
|
||||
class BonusAmiens(BonusSportAdditif):
|
||||
"""Bonus IUT Amiens pour les modules optionnels (sport, culture, ...).
|
||||
"""Bonus IUT Amiens pour les modules optionnels (sport, culture, ...)
|
||||
|
||||
Toute note non nulle, peu importe sa valeur, entraine un bonus de 0,1 point
|
||||
<p><b>À partir d'août 2022:</b></p>
|
||||
<p>
|
||||
Deux activités optionnelles sont possibles chaque semestre, et peuvent donner lieu à une bonification de 0,1 chacune sur la moyenne de chaque UE.
|
||||
</p><p>
|
||||
La note saisie peut valoir 0 (pas de bonus), 1 (bonus de 0,1 points) ou 2 (bonus de 0,2 points).
|
||||
</p>
|
||||
|
||||
<p><b>Avant juillet 2022:</b></p>
|
||||
<p>Toute note non nulle, peu importe sa valeur, entraine un bonus de 0,1 point
|
||||
sur toutes les moyennes d'UE.
|
||||
</p>
|
||||
"""
|
||||
|
||||
name = "bonus_amiens"
|
||||
displayed_name = "IUT d'Amiens"
|
||||
seuil_moy_gen = 0.0 # tous les points sont comptés
|
||||
proportion_point = 1e10
|
||||
bonus_max = 0.1
|
||||
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
|
||||
seuil_moy_gen = 0.0 # tous les points sont comptés
|
||||
|
||||
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||||
"""calcul du bonus, avec réglage différent suivant la date"""
|
||||
|
||||
if self.formsemestre.date_debut > datetime.date(2022, 8, 1):
|
||||
self.proportion_point = 0.1
|
||||
self.bonus_max = 0.2
|
||||
else: # anciens semestres
|
||||
self.proportion_point = 1e10
|
||||
self.bonus_max = 0.1
|
||||
|
||||
super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
|
||||
|
||||
|
||||
# Finalement ils n'en veulent pas.
|
||||
@ -421,6 +430,22 @@ class BonusAmiens(BonusSportAdditif):
|
||||
# )
|
||||
|
||||
|
||||
class BonusBesanconVesoul(BonusSportAdditif):
|
||||
"""Bonus IUT Besançon - Vesoul pour les UE libres
|
||||
|
||||
<p>Toute note non nulle, peu importe sa valeur, entraine un bonus de 0,2 point
|
||||
sur toutes les moyennes d'UE.
|
||||
</p>
|
||||
"""
|
||||
|
||||
name = "bonus_besancon_vesoul"
|
||||
displayed_name = "IUT de Besançon - Vesoul"
|
||||
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
|
||||
seuil_moy_gen = 0.0 # tous les points sont comptés
|
||||
proportion_point = 1e10 # infini
|
||||
bonus_max = 0.2
|
||||
|
||||
|
||||
class BonusBethune(BonusSportMultiplicatif):
|
||||
"""
|
||||
Calcul bonus modules optionnels (sport, culture), règle IUT de Béthune.
|
||||
@ -638,7 +663,10 @@ class BonusCalais(BonusSportAdditif):
|
||||
dans la limite de 10 points. 6% de ces points cumulés s'ajoutent :
|
||||
<ul>
|
||||
<li><b>en DUT</b> à la moyenne générale du semestre déjà obtenue par l'étudiant.
|
||||
<li><b>en BUT et LP</b> à la moyenne des UE dont l'acronyme fini par <b>BS</b> (ex : UE2.1BS, UE32BS)
|
||||
</li>
|
||||
<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>
|
||||
"""
|
||||
|
||||
@ -1199,7 +1227,7 @@ class BonusStDenis(BonusSportAdditif):
|
||||
bonus_max = 0.5
|
||||
|
||||
|
||||
class BonusStNazaire(BonusSportMultiplicatif):
|
||||
class BonusStNazaire(BonusSport):
|
||||
"""IUT de Saint-Nazaire
|
||||
|
||||
Trois bonifications sont possibles : sport, culture et engagement citoyen
|
||||
@ -1221,9 +1249,37 @@ class BonusStNazaire(BonusSportMultiplicatif):
|
||||
name = "bonus_iutSN"
|
||||
displayed_name = "IUT de Saint-Nazaire"
|
||||
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
|
||||
seuil_moy_gen = 0.0 # tous les points comptent
|
||||
amplitude = 0.01 / 4 # 4pt => 1%
|
||||
factor_max = 0.1 # 10% max
|
||||
# Modifié 2022-11-29: calculer chaque bonus
|
||||
# (de 1 à 3 modules) séparément.
|
||||
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||||
"""Calcul du bonus St Nazaire 2022
|
||||
sem_modimpl_moys_inscrits: les notes de sport
|
||||
En APC: ndarray (nb_etuds, nb_mod_sport, nb_ues_non_bonus)
|
||||
En classic: ndarray (nb_etuds, nb_mod_sport)
|
||||
"""
|
||||
if 0 in sem_modimpl_moys_inscrits.shape:
|
||||
# pas d'étudiants ou pas d'UE ou pas de module...
|
||||
return
|
||||
# Prend les 3 premiers bonus trouvés
|
||||
# ignore les coefficients
|
||||
bonus_mod_moys = sem_modimpl_moys_inscrits[:, :3]
|
||||
bonus_mod_moys = np.nan_to_num(bonus_mod_moys, copy=False)
|
||||
factor = bonus_mod_moys * self.amplitude
|
||||
# somme les bonus:
|
||||
factor = factor.sum(axis=1)
|
||||
# et limite à 10%:
|
||||
factor.clip(0.0, self.factor_max, out=factor)
|
||||
|
||||
# Applique aux moyennes d'UE
|
||||
if len(factor.shape) == 1: # classic
|
||||
factor = factor[:, np.newaxis]
|
||||
bonus = self.etud_moy_ue * factor
|
||||
self.bonus_ues = bonus # DataFrame
|
||||
|
||||
# Les bonus multiplicatifs ne s'appliquent pas à la moyenne générale
|
||||
self.bonus_moy_gen = None
|
||||
|
||||
|
||||
class BonusTarbes(BonusIUTRennes1):
|
||||
@ -1302,7 +1358,45 @@ class BonusIUTvannes(BonusSportAdditif):
|
||||
classic_use_bonus_ues = False # seulement sur moy gen.
|
||||
|
||||
|
||||
class BonusVilleAvray(BonusSport):
|
||||
class BonusValenciennes(BonusDirect):
|
||||
"""Article 7 des RCC de l’IUT de Valenciennes
|
||||
|
||||
<p>
|
||||
Une bonification maximale de 0.25 point (1/4 de point) peut être ajoutée
|
||||
à la moyenne de chaque Unité d’Enseignement pour :
|
||||
</p>
|
||||
<ul>
|
||||
<li>l'engagement citoyen ;</li>
|
||||
<li>la participation à un module de sport.</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
Une bonification accordée par la commission des sports de l’UPHF peut être attribuée
|
||||
aux sportifs de haut niveau. Cette bonification est appliquée à l’ensemble des
|
||||
Unités d’Enseignement. Ce bonus est :
|
||||
</p>
|
||||
<ul>
|
||||
<li> 0.5 pour la catégorie <em>or</em> (sportif inscrit sur liste ministérielle
|
||||
jeunesse et sport) ;
|
||||
</li>
|
||||
<li> 0.45 pour la catégorie <em>argent</em> (sportif en club professionnel) ;
|
||||
</li>
|
||||
<li> 0.40 pour le <em>bronze</em> (sportif de niveau départemental, régional ou national).
|
||||
</li>
|
||||
</ul>
|
||||
<p>Le cumul de bonifications est possible mais ne peut excéder 0.5 point (un demi-point).
|
||||
</p>
|
||||
<p><em>Dans ScoDoc, saisir directement la valeur désirée du bonus
|
||||
dans une évaluation notée sur 20.</em>
|
||||
</p>
|
||||
"""
|
||||
|
||||
name = "bonus_valenciennes"
|
||||
displayed_name = "IUT de Valenciennes"
|
||||
bonus_max = 0.5
|
||||
|
||||
|
||||
class BonusVilleAvray(BonusSportAdditif):
|
||||
"""Bonus modules optionnels (sport, culture), règle IUT Ville d'Avray.
|
||||
|
||||
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
|
||||
@ -1351,7 +1445,7 @@ class BonusIUTV(BonusSportAdditif):
|
||||
|
||||
name = "bonus_iutv"
|
||||
displayed_name = "IUT de Villetaneuse"
|
||||
pass # oui, c'est le bonus par défaut
|
||||
# c'est le bonus par défaut: aucune méthode à surcharger
|
||||
|
||||
|
||||
def get_bonus_class_dict(start=BonusSport, d=None):
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 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,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -122,6 +122,10 @@ def formsemestre_get_ue_capitalisees(formsemestre: FormSemestre) -> pd.DataFrame
|
||||
event_date :
|
||||
} ]
|
||||
"""
|
||||
|
||||
# Note: pour récupérer aussi les UE validées en CMp ou ADJ, changer une ligne
|
||||
# and ( SFV.code = 'ADM' or SFV.code = 'ADJ' or SFV.code = 'CMP' )
|
||||
|
||||
query = """
|
||||
SELECT DISTINCT SFV.*, ue.ue_code
|
||||
FROM
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 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 - 2022 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,14 +35,16 @@ moyenne générale d'une UE.
|
||||
"""
|
||||
import dataclasses
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
import app
|
||||
from app import db
|
||||
from app.models import ModuleImpl, Evaluation, EvaluationUEPoids
|
||||
from app.models import Evaluation, EvaluationUEPoids, ModuleImpl
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc.sco_exceptions import ScoBugCatcher
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
@ -217,12 +219,19 @@ class ModuleImplResults:
|
||||
]
|
||||
|
||||
def get_evaluations_coefs(self, moduleimpl: ModuleImpl) -> np.array:
|
||||
"""Coefficients des évaluations, met à zéro ceux des évals incomplètes.
|
||||
"""Coefficients des évaluations.
|
||||
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 for e in moduleimpl.evaluations],
|
||||
[
|
||||
e.coefficient
|
||||
if e.evaluation_type == scu.EVALUATION_NORMALE
|
||||
else 0.0
|
||||
for e in moduleimpl.evaluations
|
||||
],
|
||||
dtype=float,
|
||||
)
|
||||
* self.evaluations_completes
|
||||
@ -236,8 +245,8 @@ class ModuleImplResults:
|
||||
]
|
||||
|
||||
def get_eval_notes_sur_20(self, moduleimpl: ModuleImpl) -> np.array:
|
||||
"""Les notes des évaluations,
|
||||
remplace les ATT, EXC, ABS, NaN par zéro et mets les notes sur 20.
|
||||
"""Les notes de toutes les évaluations du module, complètes ou non.
|
||||
Remplace les ATT, EXC, ABS, NaN par zéro et mets les notes sur 20.
|
||||
Résultat: 2d array of floats, shape nb_etuds x nb_evaluations
|
||||
"""
|
||||
return np.where(
|
||||
@ -368,7 +377,7 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
||||
etuds_moy_module = np.where(
|
||||
etuds_use_rattrapage, notes_rat_ues, etuds_moy_module
|
||||
)
|
||||
# Serie indiquant que l'étudiant utilise une note de rattarage sur l'une des UE:
|
||||
# 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
|
||||
)
|
||||
@ -429,7 +438,7 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
|
||||
|
||||
|
||||
def moduleimpl_is_conforme(
|
||||
moduleimpl, evals_poids: pd.DataFrame, modules_coefficients: pd.DataFrame
|
||||
moduleimpl, evals_poids: pd.DataFrame, modimpl_coefs_df: pd.DataFrame
|
||||
) -> bool:
|
||||
"""Vérifie que les évaluations de ce moduleimpl sont bien conformes
|
||||
au PN.
|
||||
@ -438,7 +447,7 @@ def moduleimpl_is_conforme(
|
||||
|
||||
Arguments:
|
||||
evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs
|
||||
modules_coefficients: DataFrame, cols module_id, lignes UEs
|
||||
modimpl_coefs_df: DataFrame, cols: modimpl_id, lignes: UEs du formsemestre
|
||||
NB: les UEs dans evals_poids sont sans le bonus sport
|
||||
"""
|
||||
nb_evals, nb_ues = evals_poids.shape
|
||||
@ -446,18 +455,18 @@ def moduleimpl_is_conforme(
|
||||
return True # modules vides conformes
|
||||
if nb_ues == 0:
|
||||
return False # situation absurde (pas d'UE)
|
||||
if len(modules_coefficients) != nb_ues:
|
||||
if len(modimpl_coefs_df) != nb_ues:
|
||||
# il arrive (#bug) que le cache ne soit pas à jour...
|
||||
sco_cache.invalidate_formsemestre()
|
||||
raise ScoBugCatcher("moduleimpl_is_conforme: nb ue incoherent")
|
||||
|
||||
if moduleimpl.module_id not in modules_coefficients:
|
||||
if moduleimpl.id not in modimpl_coefs_df:
|
||||
# soupçon de bug cache coef ?
|
||||
sco_cache.invalidate_formsemestre()
|
||||
raise ScoBugCatcher("Erreur 454 - merci de ré-essayer")
|
||||
|
||||
module_evals_poids = evals_poids.transpose().sum(axis=1) != 0
|
||||
return all((modules_coefficients[moduleimpl.module_id] != 0).eq(module_evals_poids))
|
||||
return all((modimpl_coefs_df[moduleimpl.id] != 0).eq(module_evals_poids))
|
||||
|
||||
|
||||
class ModuleImplResultsClassic(ModuleImplResults):
|
||||
@ -476,7 +485,8 @@ class ModuleImplResultsClassic(ModuleImplResults):
|
||||
if nb_etuds == 0:
|
||||
return pd.Series()
|
||||
evals_coefs = self.get_evaluations_coefs(modimpl).reshape(-1)
|
||||
assert evals_coefs.shape == (nb_evals,)
|
||||
if evals_coefs.shape != (nb_evals,):
|
||||
app.critical_error("compute_module_moy: vals_coefs.shape != nb_evals")
|
||||
evals_notes_20 = self.get_eval_notes_sur_20(modimpl)
|
||||
# Les coefs des évals pour chaque étudiant: là où il a des notes
|
||||
# non neutralisées
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 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
|
||||
@ -52,13 +52,16 @@ def compute_sem_moys_apc_using_coefs(
|
||||
|
||||
|
||||
def compute_sem_moys_apc_using_ects(
|
||||
etud_moy_ue_df: pd.DataFrame, ects: list, formation_id=None, skip_empty_ues=False
|
||||
etud_moy_ue_df: pd.DataFrame,
|
||||
ects_df: pd.DataFrame,
|
||||
formation_id=None,
|
||||
skip_empty_ues=False,
|
||||
) -> pd.Series:
|
||||
"""Calcule les moyennes générales indicatives de tous les étudiants
|
||||
= moyenne des moyennes d'UE, pondérée par leurs ECTS.
|
||||
|
||||
etud_moy_ue_df: DataFrame, colonnes ue_id, lignes etudid
|
||||
ects: liste de floats ou None, 1 par UE
|
||||
ects: DataFrame, col. ue_id, lignes etudid, valeur float ou None
|
||||
|
||||
Si skip_empty_ues: ne compte pas les UE non notées.
|
||||
Sinon (par défaut), une UE non notée compte comme zéro.
|
||||
@ -68,11 +71,11 @@ def compute_sem_moys_apc_using_ects(
|
||||
try:
|
||||
if skip_empty_ues:
|
||||
# annule les coefs des UE sans notes (NaN)
|
||||
ects = np.where(etud_moy_ue_df.isna(), 0.0, np.array(ects, dtype=float))
|
||||
# ects est devenu nb_etuds x nb_ues
|
||||
moy_gen = (etud_moy_ue_df * ects).sum(axis=1) / ects.sum(axis=1)
|
||||
ects = np.where(etud_moy_ue_df.isna(), 0.0, ects_df.to_numpy())
|
||||
else:
|
||||
moy_gen = (etud_moy_ue_df * ects).sum(axis=1) / sum(ects)
|
||||
ects = ects_df.to_numpy()
|
||||
# ects est maintenant un array nb_etuds x nb_ues
|
||||
moy_gen = (etud_moy_ue_df * ects).sum(axis=1) / ects.sum(axis=1)
|
||||
except TypeError:
|
||||
if None in ects:
|
||||
formation = Formation.query.get(formation_id)
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 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
|
||||
@ -32,9 +32,14 @@ import pandas as pd
|
||||
|
||||
from app import db
|
||||
from app import models
|
||||
from app.models import UniteEns, Module, ModuleImpl, ModuleUECoef
|
||||
from app.models import (
|
||||
FormSemestre,
|
||||
Module,
|
||||
ModuleImpl,
|
||||
ModuleUECoef,
|
||||
UniteEns,
|
||||
)
|
||||
from app.comp import moy_mod
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||
@ -69,9 +74,7 @@ def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.Data
|
||||
& (UniteEns.type == sco_codes_parcours.UE_SPORT)
|
||||
)
|
||||
)
|
||||
.order_by(
|
||||
Module.semestre_id, Module.module_type.desc(), Module.numero, Module.code
|
||||
)
|
||||
.order_by(Module.semestre_id, Module.module_type, Module.numero, Module.code)
|
||||
)
|
||||
if semestre_idx is not None:
|
||||
ues = ues.filter_by(semestre_idx=semestre_idx)
|
||||
@ -140,7 +143,8 @@ def df_load_modimpl_coefs(
|
||||
mod_coef.ue_id
|
||||
] = mod_coef.coef
|
||||
except IndexError:
|
||||
# il peut y avoir en base des coefs sur des modules ou UE qui ont depuis été retirés de la formation
|
||||
# il peut y avoir en base des coefs sur des modules ou UE
|
||||
# qui ont depuis été retirés de la formation
|
||||
pass
|
||||
# Initialisation des poids non fixés:
|
||||
# 0 pour modules normaux, 1. pour bonus (car par défaut, on veut qu'un bonus agisse
|
||||
@ -199,7 +203,7 @@ def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
|
||||
modimpls_results[modimpl.id] = mod_results
|
||||
modimpls_evals_poids[modimpl.id] = evals_poids
|
||||
modimpls_notes.append(etuds_moy_module)
|
||||
if len(modimpls_notes):
|
||||
if len(modimpls_notes) > 0:
|
||||
cube = notes_sem_assemble_cube(modimpls_notes)
|
||||
else:
|
||||
nb_etuds = formsemestre.etuds.count()
|
||||
@ -215,10 +219,11 @@ def compute_ue_moys_apc(
|
||||
sem_cube: np.array,
|
||||
etuds: list,
|
||||
modimpls: list,
|
||||
ues: list,
|
||||
modimpl_inscr_df: pd.DataFrame,
|
||||
modimpl_coefs_df: pd.DataFrame,
|
||||
modimpl_mask: np.array,
|
||||
dispense_ues: set[tuple[int, int]],
|
||||
block: bool = False,
|
||||
) -> pd.DataFrame:
|
||||
"""Calcul de la moyenne d'UE en mode APC (BUT).
|
||||
La moyenne d'UE est un nombre (note/20), ou NaN si pas de notes disponibles
|
||||
@ -229,18 +234,17 @@ def compute_ue_moys_apc(
|
||||
etuds : liste des étudiants (dim. 0 du cube)
|
||||
modimpls : liste des module_impl (dim. 1 du cube)
|
||||
ues : liste des UE (dim. 2 du cube)
|
||||
modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl)
|
||||
modimpl_inscr_df: matrice d'inscription aux modules du semestre (etud x modimpl)
|
||||
modimpl_coefs_df: matrice coefficients (UE x modimpl), sans UEs bonus sport
|
||||
modimpl_mask: liste de booléens, indiquants le module doit être pris ou pas.
|
||||
(utilisé pour éliminer les bonus, et pourra servir à cacluler
|
||||
sur des sous-ensembles de modules)
|
||||
|
||||
block: si vrai, ne calcule rien et renvoie des NaNs
|
||||
Résultat: DataFrame columns UE (sans bonus), rows etudid
|
||||
"""
|
||||
nb_etuds, nb_modules, nb_ues_no_bonus = sem_cube.shape
|
||||
nb_ues_tot = len(ues)
|
||||
assert len(modimpls) == nb_modules
|
||||
if nb_modules == 0 or nb_etuds == 0 or nb_ues_no_bonus == 0:
|
||||
if block or nb_modules == 0 or nb_etuds == 0 or nb_ues_no_bonus == 0:
|
||||
return pd.DataFrame(
|
||||
index=modimpl_inscr_df.index, columns=modimpl_coefs_df.index
|
||||
)
|
||||
@ -277,11 +281,16 @@ def compute_ue_moys_apc(
|
||||
etud_moy_ue = np.sum(
|
||||
modimpl_coefs_etuds_no_nan * sem_cube_inscrits, axis=1
|
||||
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
|
||||
return pd.DataFrame(
|
||||
etud_moy_ue_df = pd.DataFrame(
|
||||
etud_moy_ue,
|
||||
index=modimpl_inscr_df.index, # les etudids
|
||||
columns=modimpl_coefs_df.index, # les UE sans les UE bonus sport
|
||||
)
|
||||
# Les "dispenses" sont très peu nombreuses et traitées en python:
|
||||
for dispense_ue in dispense_ues:
|
||||
etud_moy_ue_df[dispense_ue[1]][dispense_ue[0]] = 0.0
|
||||
|
||||
return etud_moy_ue_df
|
||||
|
||||
|
||||
def compute_ue_moys_classic(
|
||||
@ -291,6 +300,7 @@ def compute_ue_moys_classic(
|
||||
modimpl_inscr_df: pd.DataFrame,
|
||||
modimpl_coefs: np.array,
|
||||
modimpl_mask: np.array,
|
||||
block: bool = False,
|
||||
) -> tuple[pd.Series, pd.DataFrame, pd.DataFrame]:
|
||||
"""Calcul de la moyenne d'UE et de la moy. générale en mode classique (DUT, LMD, ...).
|
||||
|
||||
@ -312,6 +322,7 @@ def compute_ue_moys_classic(
|
||||
modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl)
|
||||
modimpl_coefs: vecteur des coefficients de modules
|
||||
modimpl_mask: masque des modimpls à prendre en compte
|
||||
block: si vrai, ne calcule rien et renvoie des NaNs
|
||||
|
||||
Résultat:
|
||||
- moyennes générales: pd.Series, index etudid
|
||||
@ -320,13 +331,14 @@ def compute_ue_moys_classic(
|
||||
les coefficients effectifs de chaque UE pour chaque étudiant
|
||||
(sommes de coefs de modules pris en compte)
|
||||
"""
|
||||
if (not len(modimpl_mask)) or (
|
||||
sem_matrix.shape[0] == 0
|
||||
if (
|
||||
block or (len(modimpl_mask) == 0) or (sem_matrix.shape[0] == 0)
|
||||
): # aucun module ou aucun étudiant
|
||||
# etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df
|
||||
val = np.nan if block else 0.0
|
||||
return (
|
||||
pd.Series(
|
||||
[0.0] * len(modimpl_inscr_df.index), index=modimpl_inscr_df.index
|
||||
[val] * len(modimpl_inscr_df.index), index=modimpl_inscr_df.index
|
||||
),
|
||||
pd.DataFrame(columns=[ue.id for ue in ues], index=modimpl_inscr_df.index),
|
||||
pd.DataFrame(columns=[ue.id for ue in ues], index=modimpl_inscr_df.index),
|
||||
@ -431,7 +443,7 @@ def compute_mat_moys_classic(
|
||||
Résultat:
|
||||
- moyennes: pd.Series, index etudid
|
||||
"""
|
||||
if (not len(modimpl_mask)) or (
|
||||
if (0 == len(modimpl_mask)) or (
|
||||
sem_matrix.shape[0] == 0
|
||||
): # aucun module ou aucun étudiant
|
||||
# etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df
|
||||
@ -462,6 +474,7 @@ def compute_mat_moys_classic(
|
||||
if modimpl_coefs_etuds_no_nan.dtype == object: # arrive sur des tableaux vides
|
||||
modimpl_coefs_etuds_no_nan = modimpl_coefs_etuds_no_nan.astype(np.float)
|
||||
|
||||
with np.errstate(invalid="ignore"): # il peut y avoir des NaN
|
||||
etud_moy_mat = (modimpl_coefs_etuds_no_nan * sem_matrix_inscrits).sum(
|
||||
axis=1
|
||||
) / modimpl_coefs_etuds_no_nan.sum(axis=1)
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -16,7 +16,7 @@ from app.comp.res_compat import NotesTableCompat
|
||||
from app.comp.bonus_spo import BonusSport
|
||||
from app.models import ScoDocSiteConfig
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
from app.models.ues import UniteEns
|
||||
from app.models.ues import DispenseUE, UniteEns
|
||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||
from app.scodoc import sco_preferences
|
||||
|
||||
@ -39,6 +39,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
"""ndarray (etuds x modimpl x ue)"""
|
||||
self.etuds_parcour_id = None
|
||||
"""Parcours de chaque étudiant { etudid : parcour_id }"""
|
||||
|
||||
if not self.load_cached():
|
||||
t0 = time.time()
|
||||
self.compute()
|
||||
@ -71,15 +72,18 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
modimpl.module.ue.type != UE_SPORT
|
||||
for modimpl in self.formsemestre.modimpls_sorted
|
||||
]
|
||||
|
||||
self.dispense_ues = DispenseUE.load_formsemestre_dispense_ues_set(
|
||||
self.formsemestre, self.modimpl_inscr_df.index, self.ues
|
||||
)
|
||||
self.etud_moy_ue = moy_ue.compute_ue_moys_apc(
|
||||
self.sem_cube,
|
||||
self.etuds,
|
||||
self.formsemestre.modimpls_sorted,
|
||||
self.ues,
|
||||
self.modimpl_inscr_df,
|
||||
self.modimpl_coefs_df,
|
||||
modimpls_mask,
|
||||
self.dispense_ues,
|
||||
block=self.formsemestre.block_moyennes,
|
||||
)
|
||||
# Les coefficients d'UE ne sont pas utilisés en APC
|
||||
self.etud_coef_ue_df = pd.DataFrame(
|
||||
@ -114,6 +118,10 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
|
||||
# Nanifie les moyennes d'UE hors parcours pour chaque étudiant
|
||||
self.etud_moy_ue *= self.ues_inscr_parcours_df
|
||||
# Les ects (utilisés comme coefs) sont nuls pour les UE hors parcours:
|
||||
ects = self.ues_inscr_parcours_df.fillna(0.0) * [
|
||||
ue.ects for ue in self.ues if ue.type != UE_SPORT
|
||||
]
|
||||
|
||||
# Moyenne générale indicative:
|
||||
# (note: le bonus sport a déjà été appliqué aux moyennes d'UE, et impacte
|
||||
@ -121,9 +129,14 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
# self.etud_moy_gen = moy_sem.compute_sem_moys_apc_using_coefs(
|
||||
# self.etud_moy_ue, self.modimpl_coefs_df
|
||||
# )
|
||||
if self.formsemestre.block_moyenne_generale or self.formsemestre.block_moyennes:
|
||||
self.etud_moy_gen = pd.Series(
|
||||
index=self.etud_moy_ue.index, dtype=float
|
||||
) # NaNs
|
||||
else:
|
||||
self.etud_moy_gen = moy_sem.compute_sem_moys_apc_using_ects(
|
||||
self.etud_moy_ue,
|
||||
[ue.ects for ue in self.ues if ue.type != UE_SPORT],
|
||||
ects,
|
||||
formation_id=self.formsemestre.formation_id,
|
||||
skip_empty_ues=sco_preferences.get_preference(
|
||||
"but_moy_skip_empty_ues", self.formsemestre.id
|
||||
@ -204,27 +217,33 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
}
|
||||
self.etuds_parcour_id = etuds_parcour_id
|
||||
ue_ids = [ue.id for ue in self.ues if ue.type != UE_SPORT]
|
||||
# matrice de 1, inscrits par défaut à toutes les UE:
|
||||
ues_inscr_parcours_df = pd.DataFrame(
|
||||
|
||||
if self.formsemestre.formation.referentiel_competence is None:
|
||||
return pd.DataFrame(
|
||||
1.0, index=etuds_parcour_id.keys(), columns=ue_ids, dtype=float
|
||||
)
|
||||
if self.formsemestre.formation.referentiel_competence is None:
|
||||
return ues_inscr_parcours_df
|
||||
|
||||
# matrice de NaN: inscrits par défaut à AUCUNE UE:
|
||||
ues_inscr_parcours_df = pd.DataFrame(
|
||||
np.nan, index=etuds_parcour_id.keys(), columns=ue_ids, dtype=float # XXX
|
||||
)
|
||||
# Construit pour chaque parcours du référentiel l'ensemble de ses UE
|
||||
# (considère aussi le cas des semestres sans parcours: None)
|
||||
ue_by_parcours = {} # parcours_id : {ue_id:0|1}
|
||||
for parcour in self.formsemestre.formation.referentiel_competence.parcours:
|
||||
ue_by_parcours[parcour.id] = {
|
||||
for (
|
||||
parcour
|
||||
) in self.formsemestre.formation.referentiel_competence.parcours.all() + [None]:
|
||||
ue_by_parcours[None if parcour is None else parcour.id] = {
|
||||
ue.id: 1.0
|
||||
for ue in self.formsemestre.formation.query_ues_parcour(
|
||||
parcour
|
||||
).filter_by(semestre_idx=self.formsemestre.semestre_id)
|
||||
}
|
||||
#
|
||||
for etudid in etuds_parcour_id:
|
||||
parcour = etuds_parcour_id[etudid]
|
||||
if parcour is not None:
|
||||
ues_inscr_parcours_df.loc[etudid] = ue_by_parcours[
|
||||
etuds_parcour_id[etudid]
|
||||
]
|
||||
parcour_id = etuds_parcour_id[etudid]
|
||||
if parcour_id in ue_by_parcours:
|
||||
if ue_by_parcours[parcour_id]:
|
||||
ues_inscr_parcours_df.loc[etudid] = ue_by_parcours[parcour_id]
|
||||
return ues_inscr_parcours_df
|
||||
|
||||
def etud_ues_ids(self, etudid: int) -> list[int]:
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -90,6 +90,7 @@ class ResultatsSemestreClassic(NotesTableCompat):
|
||||
self.modimpl_inscr_df,
|
||||
self.modimpl_coefs,
|
||||
modimpl_standards_mask,
|
||||
block=self.formsemestre.block_moyennes,
|
||||
)
|
||||
# --- Modules de MALUS sur les UEs et la moyenne générale
|
||||
self.malus = moy_ue.compute_malus(
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -48,12 +48,13 @@ class ResultatsSemestre(ResultatsCache):
|
||||
_cached_attrs = (
|
||||
"bonus",
|
||||
"bonus_ues",
|
||||
"dispense_ues",
|
||||
"etud_coef_ue_df",
|
||||
"etud_moy_gen_ranks",
|
||||
"etud_moy_gen",
|
||||
"etud_moy_ue",
|
||||
"modimpl_inscr_df",
|
||||
"modimpls_results",
|
||||
"etud_coef_ue_df",
|
||||
"moyennes_matieres",
|
||||
)
|
||||
|
||||
@ -66,6 +67,8 @@ class ResultatsSemestre(ResultatsCache):
|
||||
"Bonus sur moy. gen. Series de float, index etudid"
|
||||
self.bonus_ues: pd.DataFrame = None # virtuel
|
||||
"DataFrame de float, index etudid, columns: ue.id"
|
||||
self.dispense_ues: set[tuple[int, int]] = set()
|
||||
"""set des dispenses d'UE: (etudid, ue_id), en APC seulement."""
|
||||
# ResultatsSemestreBUT ou ResultatsSemestreClassic
|
||||
self.etud_moy_ue = {}
|
||||
"etud_moy_ue: DataFrame columns UE, rows etudid"
|
||||
@ -316,7 +319,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
"""L'état de l'UE pour cet étudiant.
|
||||
Result: dict, ou None si l'UE n'est pas dans ce semestre.
|
||||
"""
|
||||
ue = UniteEns.query.get(ue_id) # TODO cacher nos UEs ?
|
||||
ue = UniteEns.query.get(ue_id)
|
||||
if ue.type == UE_SPORT:
|
||||
return {
|
||||
"is_capitalized": False,
|
||||
@ -439,7 +442,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
allow_html=True,
|
||||
):
|
||||
"""Table récap. des résultats.
|
||||
allow_html: si vri, peut-mettre du HTML dans les valeurs
|
||||
allow_html: si vrai, peut mettre du HTML dans les valeurs
|
||||
|
||||
Result: tuple avec
|
||||
- rows: liste de dicts { column_id : value }
|
||||
@ -491,7 +494,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
classes: str = "",
|
||||
idx: int = 100,
|
||||
):
|
||||
"Add a row to our table. classes is a list of css class names"
|
||||
"Add a cell to our table. classes is a list of css class names"
|
||||
row[col_id] = content
|
||||
if classes:
|
||||
row[f"_{col_id}_class"] = classes + f" c{idx}"
|
||||
@ -516,6 +519,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
row, "code_nip", "code_nip", etud.code_nip or "", "codes", idx
|
||||
)
|
||||
# --- Rang
|
||||
if not self.formsemestre.block_moyenne_generale:
|
||||
idx = add_cell(
|
||||
row, "rang", "Rg", self.etud_moy_gen_ranks[etudid], "rang", idx
|
||||
)
|
||||
@ -539,12 +543,17 @@ class ResultatsSemestre(ResultatsCache):
|
||||
formsemestre_id=self.formsemestre.id,
|
||||
etudid=etudid,
|
||||
)
|
||||
row["_nom_short_data"] = {
|
||||
"etudid": etud.id,
|
||||
"nomprenom": etud.nomprenom,
|
||||
}
|
||||
row["_nom_short_target_attrs"] = f'class="etudinfo" id="{etudid}"'
|
||||
row["_nom_disp_target"] = row["_nom_short_target"]
|
||||
row["_nom_disp_target_attrs"] = row["_nom_short_target_attrs"]
|
||||
|
||||
idx = 30 # début des colonnes de notes
|
||||
# --- Moyenne générale
|
||||
if not self.formsemestre.block_moyenne_generale:
|
||||
moy_gen = self.etud_moy_gen.get(etudid, False)
|
||||
note_class = ""
|
||||
if moy_gen is False:
|
||||
@ -564,7 +573,8 @@ class ResultatsSemestre(ResultatsCache):
|
||||
)
|
||||
# --- Moyenne d'UE
|
||||
nb_ues_validables, nb_ues_warning = 0, 0
|
||||
for ue in ues_sans_bonus:
|
||||
idx_ue_start = idx
|
||||
for idx_ue, ue in enumerate(ues_sans_bonus):
|
||||
ue_status = self.get_etud_ue_status(etudid, ue.id)
|
||||
if ue_status is not None:
|
||||
col_id = f"moy_ue_{ue.id}"
|
||||
@ -585,7 +595,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
ue.acronyme,
|
||||
fmt_note(val),
|
||||
"col_ue" + note_class,
|
||||
idx,
|
||||
idx_ue * 10000 + idx_ue_start,
|
||||
)
|
||||
titles_bot[
|
||||
f"_{col_id}_target_attrs"
|
||||
@ -606,7 +616,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
f"Bonus {ue.acronyme}",
|
||||
val_fmt_html if allow_html else val_fmt,
|
||||
"col_ue_bonus",
|
||||
idx,
|
||||
idx_ue * 10000 + idx_ue_start + 1,
|
||||
)
|
||||
row[f"_bonus_ue_{ue.id}_xls"] = val_fmt
|
||||
# Les moyennes des modules (ou ressources et SAÉs) dans cette UE
|
||||
@ -651,7 +661,11 @@ class ResultatsSemestre(ResultatsCache):
|
||||
val_fmt_html,
|
||||
# class col_res mod_ue_123
|
||||
f"col_{modimpl.module.type_abbrv()} mod_ue_{ue.id}",
|
||||
idx,
|
||||
idx_ue * 10000
|
||||
+ idx_ue_start
|
||||
+ 1
|
||||
+ (modimpl.module.module_type or 0) * 1000
|
||||
+ (modimpl.module.numero or 0),
|
||||
)
|
||||
row[f"_{col_id}_xls"] = val_fmt
|
||||
if modimpl.module.module_type == scu.ModuleType.MALUS:
|
||||
@ -701,7 +715,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
else:
|
||||
jury_code_sem = ""
|
||||
else:
|
||||
# formations classiqes: code semestre
|
||||
# formations classiques: code semestre
|
||||
dec_sem = self.validations.decisions_jury.get(etudid)
|
||||
jury_code_sem = dec_sem["code"] if dec_sem else ""
|
||||
idx = add_cell(
|
||||
@ -719,17 +733,22 @@ class ResultatsSemestre(ResultatsCache):
|
||||
f"""<a href="{url_for('notes.formsemestre_validation_etud_form',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=self.formsemestre.id, etudid=etudid
|
||||
)
|
||||
}">{"saisir" if not jury_code_sem else "modifier"} décision</a>""",
|
||||
}">{("saisir" if not jury_code_sem else "modifier")
|
||||
if self.formsemestre.etat else "voir"} décisions</a>""",
|
||||
"col_jury_link",
|
||||
idx,
|
||||
)
|
||||
rows.append(row)
|
||||
|
||||
self.recap_add_partitions(rows, titles)
|
||||
col_idx = self.recap_add_partitions(rows, titles)
|
||||
self.recap_add_cursus(rows, titles, col_idx=col_idx + 1)
|
||||
self._recap_add_admissions(rows, titles)
|
||||
|
||||
# tri par rang croissant
|
||||
if not self.formsemestre.block_moyenne_generale:
|
||||
rows.sort(key=lambda e: e["_rang_order"])
|
||||
else:
|
||||
rows.sort(key=lambda e: e["_ues_validables_order"], reverse=True)
|
||||
|
||||
# INFOS POUR FOOTER
|
||||
bottom_infos = self._recap_bottom_infos(ues_sans_bonus, modimpl_ids, fmt_note)
|
||||
@ -746,6 +765,20 @@ class ResultatsSemestre(ResultatsCache):
|
||||
for row in bottom_infos.values():
|
||||
row[c_class] = row.get(c_class, "") + " col_empty"
|
||||
|
||||
# Ligne avec la classe de chaque colonne
|
||||
# récupère le type à partir des classes css (hack...)
|
||||
row_class = {}
|
||||
for col_id in titles:
|
||||
klass = titles.get(f"_{col_id}_class")
|
||||
if klass:
|
||||
row_class[col_id] = " ".join(
|
||||
cls[4:] for cls in klass.split() if cls.startswith("col_")
|
||||
)
|
||||
# cette case (nb d'UE validables) a deux classes col_xxx, on en garde une seule:
|
||||
if "ues_validables" in row_class[col_id]:
|
||||
row_class[col_id] = "ues_validables"
|
||||
bottom_infos["type_col"] = row_class
|
||||
|
||||
# --- TABLE FOOTER: ECTS, moyennes, min, max...
|
||||
footer_rows = []
|
||||
for (bottom_line, row) in bottom_infos.items():
|
||||
@ -769,7 +802,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
return (rows, footer_rows, titles, column_ids)
|
||||
|
||||
def _recap_bottom_infos(self, ues, modimpl_ids: set, fmt_note) -> dict:
|
||||
"""Les informations à mettre en bas de la table: min, max, moy, ECTS"""
|
||||
"""Les informations à mettre en bas de la table: min, max, moy, ECTS, Apo"""
|
||||
row_min, row_max, row_moy, row_coef, row_ects, row_apo = (
|
||||
{"_tr_class": "bottom_info", "_title": "Min."},
|
||||
{"_tr_class": "bottom_info"},
|
||||
@ -829,7 +862,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
row_moy[f"_{colid}_class"] = "col_empty"
|
||||
row_apo[colid] = modimpl.module.code_apogee or ""
|
||||
|
||||
return { # { key : row } avec key = min, max, moy, coef
|
||||
return { # { key : row } avec key = min, max, moy, coef, ...
|
||||
"min": row_min,
|
||||
"max": row_max,
|
||||
"moy": row_moy,
|
||||
@ -877,7 +910,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
}
|
||||
first = True
|
||||
for i, cid in enumerate(fields):
|
||||
titles[f"_{cid}_col_order"] = 10000 + i # tout à droite
|
||||
titles[f"_{cid}_col_order"] = 100000 + i # tout à droite
|
||||
if first:
|
||||
titles[f"_{cid}_class"] = "admission admission_first"
|
||||
first = False
|
||||
@ -896,10 +929,29 @@ class ResultatsSemestre(ResultatsCache):
|
||||
else:
|
||||
row[f"_{cid}_class"] = "admission"
|
||||
|
||||
def recap_add_partitions(self, rows: list[dict], titles: dict, col_idx: int = None):
|
||||
def recap_add_cursus(self, rows: list[dict], titles: dict, col_idx: int = None):
|
||||
"""Ajoute colonne avec code cursus, eg 'S1 S2 S1'"""
|
||||
cid = "code_cursus"
|
||||
titles[cid] = "Cursus"
|
||||
titles[f"_{cid}_col_order"] = col_idx
|
||||
formation_code = self.formsemestre.formation.formation_code
|
||||
for row in rows:
|
||||
etud = Identite.query.get(row["etudid"])
|
||||
row[cid] = " ".join(
|
||||
[
|
||||
f"S{ins.formsemestre.semestre_id}"
|
||||
for ins in reversed(etud.inscriptions())
|
||||
if ins.formsemestre.formation.formation_code == formation_code
|
||||
]
|
||||
)
|
||||
|
||||
def recap_add_partitions(
|
||||
self, rows: list[dict], titles: dict, col_idx: int = None
|
||||
) -> int:
|
||||
"""Ajoute les colonnes indiquant les groupes
|
||||
rows est une liste de dict avec une clé "etudid"
|
||||
Les colonnes ont la classe css "partition"
|
||||
Renvoie l'indice de la dernière colonne utilisée
|
||||
"""
|
||||
partitions, partitions_etud_groups = sco_groups.get_formsemestre_groups(
|
||||
self.formsemestre.id
|
||||
@ -948,6 +1000,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
row[rg_cid] = rang.get(row["etudid"], "")
|
||||
|
||||
first_partition = False
|
||||
return col_order
|
||||
|
||||
def _recap_add_evaluations(
|
||||
self, rows: list[dict], titles: dict, bottom_infos: dict
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -25,7 +25,7 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
"""Implementation partielle de NotesTable
|
||||
|
||||
Les méthodes définies dans cette classe sont là
|
||||
pour conserver la compatibilité abvec les codes anciens et
|
||||
pour conserver la compatibilité avec les codes anciens et
|
||||
il n'est pas recommandé de les utiliser dans de nouveaux
|
||||
développements (API malcommode et peu efficace).
|
||||
"""
|
||||
@ -103,10 +103,9 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
"""Stats (moy/min/max) sur la moyenne générale"""
|
||||
return StatsMoyenne(self.etud_moy_gen)
|
||||
|
||||
def get_ues_stat_dict(
|
||||
self, filter_sport=False, check_apc_ects=True
|
||||
) -> list[dict]: # was get_ues()
|
||||
"""Liste des UEs, ordonnée par numero.
|
||||
def get_ues_stat_dict(self, filter_sport=False, check_apc_ects=True) -> list[dict]:
|
||||
"""Liste des UEs de toutes les UEs du semestre (tous parcours),
|
||||
ordonnée par numero.
|
||||
Si filter_sport, retire les UE de type SPORT.
|
||||
Résultat: liste de dicts { champs UE U stats moyenne UE }
|
||||
"""
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -16,6 +16,7 @@ import flask_login
|
||||
import app
|
||||
from app.auth.models import User
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
|
||||
class ZUser(object):
|
||||
@ -180,18 +181,23 @@ def scodoc7func(func):
|
||||
else:
|
||||
arg_names = argspec.args
|
||||
for arg_name in arg_names: # pour chaque arg de la fonction vue
|
||||
if arg_name == "REQUEST": # ne devrait plus arriver !
|
||||
# debug check, TODO remove after tests
|
||||
raise ValueError("invalid REQUEST parameter !")
|
||||
else:
|
||||
# peut produire une KeyError s'il manque un argument attendu:
|
||||
v = req_args[arg_name]
|
||||
# try to convert all arguments to INTEGERS
|
||||
# necessary for db ids and boolean values
|
||||
try:
|
||||
v = int(v)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
v = int(v) if v else v
|
||||
except (ValueError, TypeError) as exc:
|
||||
if arg_name in {
|
||||
"etudid",
|
||||
"formation_id",
|
||||
"formsemestre_id",
|
||||
"module_id",
|
||||
"moduleimpl_id",
|
||||
"partition_id",
|
||||
"ue_id",
|
||||
}:
|
||||
raise ScoValueError("page introuvable (id invalide)") from exc
|
||||
pos_arg_values.append(v)
|
||||
# current_app.logger.info("pos_arg_values=%s" % pos_arg_values)
|
||||
# current_app.logger.info("req_args=%s" % req_args)
|
||||
|
@ -1,7 +1,7 @@
|
||||
# -*- coding: UTF-8 -*
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 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 - 2022 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 - 2022 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 index():
|
||||
visible=True, association=True, siret_provisoire=True
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/entreprises.html",
|
||||
"entreprises/entreprises.j2",
|
||||
title="Entreprises",
|
||||
entreprises=entreprises,
|
||||
logs=logs,
|
||||
@ -109,7 +109,7 @@ def logs():
|
||||
EntrepriseHistorique.date.desc()
|
||||
).paginate(page=page, per_page=20)
|
||||
return render_template(
|
||||
"entreprises/logs.html",
|
||||
"entreprises/logs.j2",
|
||||
title="Logs",
|
||||
logs=logs,
|
||||
)
|
||||
@ -134,7 +134,7 @@ def correspondants():
|
||||
.all()
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/correspondants.html",
|
||||
"entreprises/correspondants.j2",
|
||||
title="Correspondants",
|
||||
correspondants=correspondants,
|
||||
logs=logs,
|
||||
@ -149,7 +149,7 @@ def validation():
|
||||
"""
|
||||
entreprises = Entreprise.query.filter_by(visible=False).all()
|
||||
return render_template(
|
||||
"entreprises/entreprises_validation.html",
|
||||
"entreprises/entreprises_validation.j2",
|
||||
title="Validation entreprises",
|
||||
entreprises=entreprises,
|
||||
)
|
||||
@ -167,7 +167,7 @@ def fiche_entreprise_validation(entreprise_id):
|
||||
description=f"fiche entreprise (validation) {entreprise_id} inconnue"
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/fiche_entreprise_validation.html",
|
||||
"entreprises/fiche_entreprise_validation.j2",
|
||||
title="Validation fiche entreprise",
|
||||
entreprise=entreprise,
|
||||
)
|
||||
@ -205,7 +205,7 @@ def validate_entreprise(entreprise_id):
|
||||
flash("L'entreprise a été validé et ajouté à la liste.")
|
||||
return redirect(url_for("entreprises.validation"))
|
||||
return render_template(
|
||||
"entreprises/form_validate_confirmation.html",
|
||||
"entreprises/form_validate_confirmation.j2",
|
||||
title="Validation entreprise",
|
||||
form=form,
|
||||
)
|
||||
@ -242,7 +242,7 @@ def delete_validation_entreprise(entreprise_id):
|
||||
flash("L'entreprise a été supprimé de la liste des entreprise à valider.")
|
||||
return redirect(url_for("entreprises.validation"))
|
||||
return render_template(
|
||||
"entreprises/form_confirmation.html",
|
||||
"entreprises/form_confirmation.j2",
|
||||
title="Supression entreprise",
|
||||
form=form,
|
||||
info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression",
|
||||
@ -282,7 +282,7 @@ def offres_recues():
|
||||
files.append(file)
|
||||
offres_recues_with_files.append([envoi_offre, offre, files, correspondant])
|
||||
return render_template(
|
||||
"entreprises/offres_recues.html",
|
||||
"entreprises/offres_recues.j2",
|
||||
title="Offres reçues",
|
||||
offres_recues=offres_recues_with_files,
|
||||
)
|
||||
@ -321,7 +321,7 @@ def preferences():
|
||||
form.mail_entreprise.data = EntreprisePreferences.get_email_notifications()
|
||||
form.check_siret.data = int(EntreprisePreferences.get_check_siret())
|
||||
return render_template(
|
||||
"entreprises/preferences.html",
|
||||
"entreprises/preferences.j2",
|
||||
title="Préférences",
|
||||
form=form,
|
||||
)
|
||||
@ -357,7 +357,7 @@ def add_entreprise():
|
||||
db.session.rollback()
|
||||
flash("Une erreur est survenue veuillez réessayer.")
|
||||
return render_template(
|
||||
"entreprises/form_ajout_entreprise.html",
|
||||
"entreprises/form_ajout_entreprise.j2",
|
||||
title="Ajout entreprise avec correspondant",
|
||||
form=form,
|
||||
)
|
||||
@ -408,7 +408,7 @@ def add_entreprise():
|
||||
flash("L'entreprise a été ajouté à la liste pour la validation.")
|
||||
return redirect(url_for("entreprises.index"))
|
||||
return render_template(
|
||||
"entreprises/form_ajout_entreprise.html",
|
||||
"entreprises/form_ajout_entreprise.j2",
|
||||
title="Ajout entreprise avec correspondant",
|
||||
form=form,
|
||||
)
|
||||
@ -446,7 +446,7 @@ def fiche_entreprise(entreprise_id):
|
||||
.all()
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/fiche_entreprise.html",
|
||||
"entreprises/fiche_entreprise.j2",
|
||||
title="Fiche entreprise",
|
||||
entreprise=entreprise,
|
||||
offres=offres_with_files,
|
||||
@ -472,7 +472,7 @@ def logs_entreprise(entreprise_id):
|
||||
.paginate(page=page, per_page=20)
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/logs_entreprise.html",
|
||||
"entreprises/logs_entreprise.j2",
|
||||
title="Logs",
|
||||
logs=logs,
|
||||
entreprise=entreprise,
|
||||
@ -490,7 +490,7 @@ def offres_expirees(entreprise_id):
|
||||
).first_or_404(description=f"fiche entreprise {entreprise_id} inconnue")
|
||||
offres_with_files = are.get_offres_expirees_with_files(entreprise.offres)
|
||||
return render_template(
|
||||
"entreprises/offres_expirees.html",
|
||||
"entreprises/offres_expirees.j2",
|
||||
title="Offres expirées",
|
||||
entreprise=entreprise,
|
||||
offres_expirees=offres_with_files,
|
||||
@ -574,7 +574,7 @@ def edit_entreprise(entreprise_id):
|
||||
form.pays.data = entreprise.pays
|
||||
form.association.data = entreprise.association
|
||||
return render_template(
|
||||
"entreprises/form_modification_entreprise.html",
|
||||
"entreprises/form_modification_entreprise.j2",
|
||||
title="Modification entreprise",
|
||||
form=form,
|
||||
)
|
||||
@ -610,7 +610,7 @@ def fiche_entreprise_desactiver(entreprise_id):
|
||||
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise.id)
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/form_confirmation.html",
|
||||
"entreprises/form_confirmation.j2",
|
||||
title="Désactiver entreprise",
|
||||
form=form,
|
||||
info_message="Cliquez sur le bouton Modifier pour confirmer la désactivation",
|
||||
@ -646,7 +646,7 @@ def fiche_entreprise_activer(entreprise_id):
|
||||
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise.id)
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/form_confirmation.html",
|
||||
"entreprises/form_confirmation.j2",
|
||||
title="Activer entreprise",
|
||||
form=form,
|
||||
info_message="Cliquez sur le bouton Modifier pour confirmer l'activaction",
|
||||
@ -692,7 +692,7 @@ def add_taxe_apprentissage(entreprise_id):
|
||||
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise.id)
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/form.html",
|
||||
"entreprises/form.j2",
|
||||
title="Ajout taxe apprentissage",
|
||||
form=form,
|
||||
)
|
||||
@ -735,7 +735,7 @@ def edit_taxe_apprentissage(entreprise_id, taxe_id):
|
||||
form.montant.data = taxe.montant
|
||||
form.notes.data = taxe.notes
|
||||
return render_template(
|
||||
"entreprises/form.html",
|
||||
"entreprises/form.j2",
|
||||
title="Modification taxe apprentissage",
|
||||
form=form,
|
||||
)
|
||||
@ -775,7 +775,7 @@ def delete_taxe_apprentissage(entreprise_id, taxe_id):
|
||||
url_for("entreprises.fiche_entreprise", entreprise_id=taxe.entreprise_id)
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/form_confirmation.html",
|
||||
"entreprises/form_confirmation.j2",
|
||||
title="Supprimer taxe apprentissage",
|
||||
form=form,
|
||||
info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression",
|
||||
@ -845,7 +845,7 @@ def add_offre(entreprise_id):
|
||||
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise.id)
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/form.html",
|
||||
"entreprises/form.j2",
|
||||
title="Ajout offre",
|
||||
form=form,
|
||||
)
|
||||
@ -921,7 +921,7 @@ def edit_offre(entreprise_id, offre_id):
|
||||
form.expiration_date.data = offre.expiration_date
|
||||
form.depts.data = offre_depts_list
|
||||
return render_template(
|
||||
"entreprises/form.html",
|
||||
"entreprises/form.j2",
|
||||
title="Modification offre",
|
||||
form=form,
|
||||
)
|
||||
@ -971,7 +971,7 @@ def delete_offre(entreprise_id, offre_id):
|
||||
url_for("entreprises.fiche_entreprise", entreprise_id=offre.entreprise_id)
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/form_confirmation.html",
|
||||
"entreprises/form_confirmation.j2",
|
||||
title="Supression offre",
|
||||
form=form,
|
||||
info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression",
|
||||
@ -1047,7 +1047,7 @@ def add_site(entreprise_id):
|
||||
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise.id)
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/form.html",
|
||||
"entreprises/form.j2",
|
||||
title="Ajout site",
|
||||
form=form,
|
||||
)
|
||||
@ -1098,7 +1098,7 @@ def edit_site(entreprise_id, site_id):
|
||||
form.ville.data = site.ville
|
||||
form.pays.data = site.pays
|
||||
return render_template(
|
||||
"entreprises/form.html",
|
||||
"entreprises/form.j2",
|
||||
title="Modification site",
|
||||
form=form,
|
||||
)
|
||||
@ -1154,7 +1154,7 @@ def add_correspondant(entreprise_id, site_id):
|
||||
url_for("entreprises.fiche_entreprise", entreprise_id=site.entreprise_id)
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/form_ajout_correspondants.html",
|
||||
"entreprises/form_ajout_correspondants.j2",
|
||||
title="Ajout correspondant",
|
||||
form=form,
|
||||
)
|
||||
@ -1234,7 +1234,7 @@ def edit_correspondant(entreprise_id, site_id, correspondant_id):
|
||||
form.origine.data = correspondant.origine
|
||||
form.notes.data = correspondant.notes
|
||||
return render_template(
|
||||
"entreprises/form.html",
|
||||
"entreprises/form.j2",
|
||||
title="Modification correspondant",
|
||||
form=form,
|
||||
)
|
||||
@ -1290,7 +1290,7 @@ def delete_correspondant(entreprise_id, site_id, correspondant_id):
|
||||
)
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/form_confirmation.html",
|
||||
"entreprises/form_confirmation.j2",
|
||||
title="Supression correspondant",
|
||||
form=form,
|
||||
info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression",
|
||||
@ -1308,7 +1308,7 @@ def contacts(entreprise_id):
|
||||
).first_or_404(description=f"entreprise {entreprise_id} inconnue")
|
||||
contacts = EntrepriseContact.query.filter_by(entreprise=entreprise.id).all()
|
||||
return render_template(
|
||||
"entreprises/contacts.html",
|
||||
"entreprises/contacts.j2",
|
||||
title="Liste des contacts",
|
||||
contacts=contacts,
|
||||
entreprise=entreprise,
|
||||
@ -1365,7 +1365,7 @@ def add_contact(entreprise_id):
|
||||
db.session.commit()
|
||||
return redirect(url_for("entreprises.contacts", entreprise_id=entreprise.id))
|
||||
return render_template(
|
||||
"entreprises/form.html",
|
||||
"entreprises/form.j2",
|
||||
title="Ajout contact",
|
||||
form=form,
|
||||
)
|
||||
@ -1421,7 +1421,7 @@ def edit_contact(entreprise_id, contact_id):
|
||||
)
|
||||
form.notes.data = contact.notes
|
||||
return render_template(
|
||||
"entreprises/form.html",
|
||||
"entreprises/form.j2",
|
||||
title="Modification contact",
|
||||
form=form,
|
||||
)
|
||||
@ -1459,7 +1459,7 @@ def delete_contact(entreprise_id, contact_id):
|
||||
url_for("entreprises.contacts", entreprise_id=contact.entreprise)
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/form_confirmation.html",
|
||||
"entreprises/form_confirmation.j2",
|
||||
title="Supression contact",
|
||||
form=form,
|
||||
info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression",
|
||||
@ -1525,7 +1525,7 @@ def add_stage_apprentissage(entreprise_id):
|
||||
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise.id)
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/form_ajout_stage_apprentissage.html",
|
||||
"entreprises/form_ajout_stage_apprentissage.j2",
|
||||
title="Ajout stage / apprentissage",
|
||||
form=form,
|
||||
)
|
||||
@ -1599,7 +1599,7 @@ def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
|
||||
form.date_fin.data = stage_apprentissage.date_fin
|
||||
form.notes.data = stage_apprentissage.notes
|
||||
return render_template(
|
||||
"entreprises/form_ajout_stage_apprentissage.html",
|
||||
"entreprises/form_ajout_stage_apprentissage.j2",
|
||||
title="Modification stage / apprentissage",
|
||||
form=form,
|
||||
)
|
||||
@ -1640,7 +1640,7 @@ def delete_stage_apprentissage(entreprise_id, stage_apprentissage_id):
|
||||
)
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/form_confirmation.html",
|
||||
"entreprises/form_confirmation.j2",
|
||||
title="Supression stage/apprentissage",
|
||||
form=form,
|
||||
info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression",
|
||||
@ -1690,7 +1690,7 @@ def envoyer_offre(entreprise_id, offre_id):
|
||||
url_for("entreprises.fiche_entreprise", entreprise_id=offre.entreprise_id)
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/form_envoi_offre.html",
|
||||
"entreprises/form_envoi_offre.j2",
|
||||
title="Envoyer une offre",
|
||||
form=form,
|
||||
)
|
||||
@ -1816,7 +1816,7 @@ def import_donnees():
|
||||
db.session.rollback()
|
||||
flash("Une erreur est survenue veuillez réessayer.")
|
||||
return render_template(
|
||||
"entreprises/import_donnees.html",
|
||||
"entreprises/import_donnees.j2",
|
||||
title="Importation données",
|
||||
form=form,
|
||||
)
|
||||
@ -1845,7 +1845,7 @@ def import_donnees():
|
||||
db.session.commit()
|
||||
flash(f"Importation réussie")
|
||||
return render_template(
|
||||
"entreprises/import_donnees.html",
|
||||
"entreprises/import_donnees.j2",
|
||||
title="Importation données",
|
||||
form=form,
|
||||
entreprises_import=entreprises_import,
|
||||
@ -1853,7 +1853,7 @@ def import_donnees():
|
||||
correspondants_import=correspondants,
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/import_donnees.html", title="Importation données", form=form
|
||||
"entreprises/import_donnees.j2", title="Importation données", form=form
|
||||
)
|
||||
|
||||
|
||||
@ -1927,7 +1927,7 @@ def add_offre_file(entreprise_id, offre_id):
|
||||
url_for("entreprises.fiche_entreprise", entreprise_id=offre.entreprise_id)
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/form.html",
|
||||
"entreprises/form.j2",
|
||||
title="Ajout fichier à une offre",
|
||||
form=form,
|
||||
)
|
||||
@ -1969,7 +1969,7 @@ def delete_offre_file(entreprise_id, offre_id, filedir):
|
||||
)
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/form_confirmation.html",
|
||||
"entreprises/form_confirmation.j2",
|
||||
title="Suppression fichier d'une offre",
|
||||
form=form,
|
||||
info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression",
|
||||
@ -1981,4 +1981,4 @@ def not_found_error_handler(e):
|
||||
"""
|
||||
Renvoie une page d'erreur pour l'erreur 404
|
||||
"""
|
||||
return render_template("entreprises/error.html", title="Erreur", e=e)
|
||||
return render_template("entreprises/error.j2", title="Erreur", e=e)
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# ScoDoc
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 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
|
||||
@ -63,6 +63,7 @@ class CodesDecisionsForm(FlaskForm):
|
||||
ABL = _build_code_field("ABL")
|
||||
ADC = _build_code_field("ADC")
|
||||
ADJ = _build_code_field("ADJ")
|
||||
ADJR = _build_code_field("ADJR")
|
||||
ADM = _build_code_field("ADM")
|
||||
AJ = _build_code_field("AJ")
|
||||
ATB = _build_code_field("ATB")
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# ScoDoc
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 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
|
||||
@ -171,7 +171,7 @@ class AddLogoForm(FlaskForm):
|
||||
|
||||
|
||||
class LogoForm(FlaskForm):
|
||||
"""Embed both presentation of a logo (cf. template file configuration.html)
|
||||
"""Embed both presentation of a logo (cf. template file configuration.j2)
|
||||
and all its data and UI action (change, delete)"""
|
||||
|
||||
dept_key = HiddenField()
|
||||
@ -434,7 +434,7 @@ def config_logos():
|
||||
scu.flash_errors(form)
|
||||
|
||||
return render_template(
|
||||
"config_logos.html",
|
||||
"config_logos.j2",
|
||||
scodoc_dept=None,
|
||||
title="Configuration ScoDoc",
|
||||
form=form,
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# ScoDoc
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 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
|
||||
@ -54,6 +54,22 @@ class BonusConfigurationForm(FlaskForm):
|
||||
class ScoDocConfigurationForm(FlaskForm):
|
||||
"Panneau de configuration avancée"
|
||||
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.
|
||||
S'applique à tous les départements.""",
|
||||
choices=[
|
||||
(i, name.capitalize()) for (i, name) in enumerate(scu.MONTH_NAMES, start=1)
|
||||
],
|
||||
)
|
||||
month_debut_periode2 = SelectField(
|
||||
label="Mois de début deuxième période de l'année",
|
||||
description="""Date pivot. En France métropolitaine, décembre.
|
||||
S'applique à tous les départements.""",
|
||||
choices=[
|
||||
(i, name.capitalize()) for (i, name) in enumerate(scu.MONTH_NAMES, start=1)
|
||||
],
|
||||
)
|
||||
submit_scodoc = SubmitField("Valider")
|
||||
cancel_scodoc = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
||||
|
||||
@ -67,7 +83,11 @@ def configuration():
|
||||
}
|
||||
)
|
||||
form_scodoc = ScoDocConfigurationForm(
|
||||
data={"enable_entreprises": ScoDocSiteConfig.is_entreprises_enabled()}
|
||||
data={
|
||||
"enable_entreprises": ScoDocSiteConfig.is_entreprises_enabled(),
|
||||
"month_debut_annee_scolaire": ScoDocSiteConfig.get_month_debut_annee_scolaire(),
|
||||
"month_debut_periode2": ScoDocSiteConfig.get_month_debut_periode2(),
|
||||
}
|
||||
)
|
||||
if request.method == "POST" and (
|
||||
form_bonus.cancel_bonus.data or form_scodoc.cancel_scodoc.data
|
||||
@ -94,10 +114,26 @@ def configuration():
|
||||
"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"])
|
||||
):
|
||||
flash(
|
||||
f"""Début des années scolaires fixé au mois de {
|
||||
scu.MONTH_NAMES[ScoDocSiteConfig.get_month_debut_annee_scolaire()-1]
|
||||
}"""
|
||||
)
|
||||
if ScoDocSiteConfig.set_month_debut_periode2(
|
||||
int(form_scodoc.data["month_debut_periode2"])
|
||||
):
|
||||
flash(
|
||||
f"""Début des années scolaires fixé au mois de {
|
||||
scu.MONTH_NAMES[ScoDocSiteConfig.get_month_debut_periode2()-1]
|
||||
}"""
|
||||
)
|
||||
return redirect(url_for("scodoc.index"))
|
||||
|
||||
return render_template(
|
||||
"configuration.html",
|
||||
"configuration.j2",
|
||||
form_bonus=form_bonus,
|
||||
form_scodoc=form_scodoc,
|
||||
scu=scu,
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# ScoDoc
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 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
|
||||
|
@ -36,7 +36,7 @@ from app.models.etudiants import (
|
||||
from app.models.events import Scolog, ScolarNews
|
||||
from app.models.formations import Formation, Matiere
|
||||
from app.models.modules import Module, ModuleUECoef, NotesTag, notes_modules_tags
|
||||
from app.models.ues import UniteEns
|
||||
from app.models.ues import DispenseUE, UniteEns
|
||||
from app.models.formsemestre import (
|
||||
FormSemestre,
|
||||
FormSemestreEtape,
|
||||
@ -72,12 +72,15 @@ from app.models.validations import (
|
||||
from app.models.preferences import ScoPreference
|
||||
|
||||
from app.models.but_refcomp import (
|
||||
ApcReferentielCompetences,
|
||||
ApcCompetence,
|
||||
ApcSituationPro,
|
||||
ApcAppCritique,
|
||||
ApcCompetence,
|
||||
ApcNiveau,
|
||||
ApcParcours,
|
||||
ApcReferentielCompetences,
|
||||
ApcSituationPro,
|
||||
)
|
||||
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
||||
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
|
||||
from app.models.assiduites import Assiduite, Justificatif
|
||||
|
@ -15,8 +15,10 @@ class Absence(db.Model):
|
||||
db.Integer, db.ForeignKey("identite.id", ondelete="CASCADE"), index=True
|
||||
)
|
||||
jour = db.Column(db.Date)
|
||||
# absent / justifié / absent+ justifié
|
||||
estabs = db.Column(db.Boolean())
|
||||
estjust = db.Column(db.Boolean())
|
||||
|
||||
matin = db.Column(db.Boolean())
|
||||
# motif de l'absence:
|
||||
description = db.Column(db.Text())
|
||||
|
277
app/models/assiduites.py
Normal file
277
app/models/assiduites.py
Normal file
@ -0,0 +1,277 @@
|
||||
# -*- coding: UTF-8 -*
|
||||
"""Gestion de l'assiduité (assiduités + justificatifs)
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from app import db
|
||||
from app.models import ModuleImpl
|
||||
from app.models.etudiants import Identite
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_utils import (
|
||||
EtatAssiduite,
|
||||
EtatJustificatif,
|
||||
is_period_overlapping,
|
||||
localize_datetime,
|
||||
)
|
||||
|
||||
|
||||
class Assiduite(db.Model):
|
||||
"""
|
||||
Représente une assiduité:
|
||||
- une plage horaire lié à un état et un étudiant
|
||||
- un module si spécifiée
|
||||
- une description si spécifiée
|
||||
"""
|
||||
|
||||
__tablename__ = "assiduites"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
assiduite_id = db.synonym("id")
|
||||
|
||||
date_debut = db.Column(
|
||||
db.DateTime(timezone=True), server_default=db.func.now(), nullable=False
|
||||
)
|
||||
date_fin = db.Column(
|
||||
db.DateTime(timezone=True), server_default=db.func.now(), nullable=False
|
||||
)
|
||||
|
||||
moduleimpl_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_moduleimpl.id", ondelete="SET NULL"),
|
||||
)
|
||||
etudid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||
index=True,
|
||||
nullable=False,
|
||||
)
|
||||
etat = db.Column(db.Integer, nullable=False)
|
||||
|
||||
desc = db.Column(db.Text)
|
||||
|
||||
entry_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
|
||||
def to_dict(self, format_api=True) -> dict:
|
||||
etat = self.etat
|
||||
|
||||
if format_api:
|
||||
etat = EtatAssiduite.inverse().get(self.etat).name
|
||||
data = {
|
||||
"assiduite_id": self.assiduite_id,
|
||||
"etudid": self.etudid,
|
||||
"moduleimpl_id": self.moduleimpl_id,
|
||||
"date_debut": self.date_debut,
|
||||
"date_fin": self.date_fin,
|
||||
"etat": etat,
|
||||
"desc": self.desc,
|
||||
"entry_date": self.entry_date,
|
||||
}
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def create_assiduite(
|
||||
cls,
|
||||
etud: Identite,
|
||||
date_debut: datetime,
|
||||
date_fin: datetime,
|
||||
etat: EtatAssiduite,
|
||||
moduleimpl: ModuleImpl = None,
|
||||
description: str = None,
|
||||
entry_date: datetime = None,
|
||||
) -> object or int:
|
||||
"""Créer une nouvelle assiduité pour l'étudiant"""
|
||||
# Vérification de non duplication des périodes
|
||||
assiduites: list[Assiduite] = etud.assiduites
|
||||
if is_period_conflicting(date_debut, date_fin, assiduites, Assiduite):
|
||||
raise ScoValueError(
|
||||
"Duplication des assiduités (la période rentrée rentre en conflit avec une assiduité enregistrée)"
|
||||
)
|
||||
if moduleimpl is not None:
|
||||
# Vérification de l'existence du module pour l'étudiant
|
||||
if moduleimpl.est_inscrit(etud):
|
||||
nouv_assiduite = Assiduite(
|
||||
date_debut=date_debut,
|
||||
date_fin=date_fin,
|
||||
etat=etat,
|
||||
etudiant=etud,
|
||||
moduleimpl_id=moduleimpl.id,
|
||||
desc=description,
|
||||
entry_date=entry_date,
|
||||
)
|
||||
else:
|
||||
raise ScoValueError("L'étudiant n'est pas inscrit au moduleimpl")
|
||||
else:
|
||||
nouv_assiduite = Assiduite(
|
||||
date_debut=date_debut,
|
||||
date_fin=date_fin,
|
||||
etat=etat,
|
||||
etudiant=etud,
|
||||
desc=description,
|
||||
entry_date=entry_date,
|
||||
)
|
||||
|
||||
return nouv_assiduite
|
||||
|
||||
@classmethod
|
||||
def fast_create_assiduite(
|
||||
cls,
|
||||
etudid: int,
|
||||
date_debut: datetime,
|
||||
date_fin: datetime,
|
||||
etat: EtatAssiduite,
|
||||
moduleimpl_id: int = None,
|
||||
description: str = None,
|
||||
entry_date: datetime = None,
|
||||
) -> object or int:
|
||||
"""Créer une nouvelle assiduité pour l'étudiant"""
|
||||
# Vérification de non duplication des périodes
|
||||
|
||||
nouv_assiduite = Assiduite(
|
||||
date_debut=date_debut,
|
||||
date_fin=date_fin,
|
||||
etat=etat,
|
||||
etudid=etudid,
|
||||
moduleimpl_id=moduleimpl_id,
|
||||
desc=description,
|
||||
entry_date=entry_date,
|
||||
)
|
||||
|
||||
return nouv_assiduite
|
||||
|
||||
|
||||
class Justificatif(db.Model):
|
||||
"""
|
||||
Représente un justificatif:
|
||||
- une plage horaire lié à un état et un étudiant
|
||||
- une raison si spécifiée
|
||||
- un fichier si spécifié
|
||||
"""
|
||||
|
||||
__tablename__ = "justificatifs"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
justif_id = db.synonym("id")
|
||||
|
||||
date_debut = db.Column(
|
||||
db.DateTime(timezone=True), server_default=db.func.now(), nullable=False
|
||||
)
|
||||
date_fin = db.Column(
|
||||
db.DateTime(timezone=True), server_default=db.func.now(), nullable=False
|
||||
)
|
||||
|
||||
etudid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||
index=True,
|
||||
nullable=False,
|
||||
)
|
||||
etat = db.Column(
|
||||
db.Integer,
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
entry_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
|
||||
raison = db.Column(db.Text())
|
||||
|
||||
# Archive_id -> sco_archives_justificatifs.py
|
||||
fichier = db.Column(db.Text())
|
||||
|
||||
def to_dict(self, format_api: bool = False) -> dict:
|
||||
"""transformation de l'objet en dictionnaire sérialisable"""
|
||||
|
||||
etat = self.etat
|
||||
|
||||
if format_api:
|
||||
etat = EtatJustificatif.inverse().get(self.etat).name
|
||||
|
||||
data = {
|
||||
"justif_id": self.justif_id,
|
||||
"etudid": self.etudid,
|
||||
"date_debut": self.date_debut,
|
||||
"date_fin": self.date_fin,
|
||||
"etat": etat,
|
||||
"raison": self.raison,
|
||||
"fichier": self.fichier,
|
||||
"entry_date": self.entry_date,
|
||||
}
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def create_justificatif(
|
||||
cls,
|
||||
etud: Identite,
|
||||
date_debut: datetime,
|
||||
date_fin: datetime,
|
||||
etat: EtatJustificatif,
|
||||
raison: str = None,
|
||||
entry_date: datetime = None,
|
||||
) -> object or int:
|
||||
"""Créer un nouveau justificatif pour l'étudiant"""
|
||||
# Vérification de non duplication des périodes
|
||||
justificatifs: list[Justificatif] = etud.justificatifs
|
||||
if is_period_conflicting(date_debut, date_fin, justificatifs, Justificatif):
|
||||
raise ScoValueError(
|
||||
"Duplication des justificatifs (la période rentrée rentre en conflit avec un justificatif enregistré)"
|
||||
)
|
||||
|
||||
nouv_justificatif = Justificatif(
|
||||
date_debut=date_debut,
|
||||
date_fin=date_fin,
|
||||
etat=etat,
|
||||
etudiant=etud,
|
||||
raison=raison,
|
||||
entry_date=entry_date,
|
||||
)
|
||||
|
||||
return nouv_justificatif
|
||||
|
||||
@classmethod
|
||||
def fast_create_justificatif(
|
||||
cls,
|
||||
etudid: int,
|
||||
date_debut: datetime,
|
||||
date_fin: datetime,
|
||||
etat: EtatJustificatif,
|
||||
raison: str = None,
|
||||
entry_date: datetime = None,
|
||||
) -> object or int:
|
||||
"""Créer un nouveau justificatif pour l'étudiant"""
|
||||
|
||||
nouv_justificatif = Justificatif(
|
||||
date_debut=date_debut,
|
||||
date_fin=date_fin,
|
||||
etat=etat,
|
||||
etudid=etudid,
|
||||
raison=raison,
|
||||
entry_date=entry_date,
|
||||
)
|
||||
|
||||
return nouv_justificatif
|
||||
|
||||
|
||||
def is_period_conflicting(
|
||||
date_debut: datetime,
|
||||
date_fin: datetime,
|
||||
collection: list[Assiduite or Justificatif],
|
||||
collection_cls: Assiduite or Justificatif,
|
||||
) -> bool:
|
||||
"""
|
||||
Vérifie si une date n'entre pas en collision
|
||||
avec les justificatifs ou assiduites déjà présentes
|
||||
"""
|
||||
|
||||
date_debut = localize_datetime(date_debut)
|
||||
date_fin = localize_datetime(date_fin)
|
||||
|
||||
if (
|
||||
collection.filter_by(date_debut=date_debut, date_fin=date_fin).first()
|
||||
is not None
|
||||
):
|
||||
return True
|
||||
|
||||
count: int = collection.filter(
|
||||
collection_cls.date_debut < date_fin, collection_cls.date_fin > date_debut
|
||||
).count()
|
||||
|
||||
return count > 0
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 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
|
||||
@ -14,7 +14,7 @@ import sqlalchemy
|
||||
from app import db
|
||||
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences
|
||||
|
||||
|
||||
# from https://stackoverflow.com/questions/2537471/method-of-iterating-over-sqlalchemy-models-defined-columns
|
||||
@ -54,13 +54,15 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
||||
"Référentiel de compétence d'une spécialité"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
|
||||
annexe = db.Column(db.Text())
|
||||
specialite = db.Column(db.Text())
|
||||
specialite_long = db.Column(db.Text())
|
||||
type_titre = db.Column(db.Text())
|
||||
type_structure = db.Column(db.Text())
|
||||
annexe = db.Column(db.Text()) # '1', '22', ...
|
||||
specialite = db.Column(db.Text()) # 'CJ', 'RT', 'INFO', ...
|
||||
specialite_long = db.Column(
|
||||
db.Text()
|
||||
) # 'Carrière Juridique', 'Réseaux et télécommunications', ...
|
||||
type_titre = db.Column(db.Text()) # 'B.U.T.'
|
||||
type_structure = db.Column(db.Text()) # 'type1', 'type2', ...
|
||||
type_departement = db.Column(db.Text()) # "secondaire", "tertiaire"
|
||||
version_orebut = db.Column(db.Text())
|
||||
version_orebut = db.Column(db.Text()) # '2021-12-11 00:00:00'
|
||||
_xml_attribs = { # Orébut xml attrib : attribute
|
||||
"type": "type_titre",
|
||||
"version": "version_orebut",
|
||||
@ -86,9 +88,16 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
||||
def __repr__(self):
|
||||
return f"<ApcReferentielCompetences {self.id} {self.specialite!r} {self.departement!r}>"
|
||||
|
||||
def to_dict(self):
|
||||
def get_version(self) -> str:
|
||||
"La version, normalement sous forme de date iso yyy-mm-dd"
|
||||
if not self.version_orebut:
|
||||
return ""
|
||||
return self.version_orebut.split()[0]
|
||||
|
||||
def to_dict(self, parcours: list["ApcParcours"] = None, with_app_critiques=True):
|
||||
"""Représentation complète du ref. de comp.
|
||||
comme un dict.
|
||||
Si parcours est une liste de parcours, restreint l'export aux parcours listés.
|
||||
"""
|
||||
return {
|
||||
"dept_id": self.dept_id,
|
||||
@ -103,29 +112,45 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
||||
if self.scodoc_date_loaded
|
||||
else "",
|
||||
"scodoc_orig_filename": self.scodoc_orig_filename,
|
||||
"competences": {x.titre: x.to_dict() for x in self.competences},
|
||||
"parcours": {x.code: x.to_dict() for x in self.parcours},
|
||||
"competences": {
|
||||
x.titre: x.to_dict(with_app_critiques=with_app_critiques)
|
||||
for x in self.competences
|
||||
},
|
||||
"parcours": {
|
||||
x.code: x.to_dict()
|
||||
for x in (self.parcours if parcours is None else parcours)
|
||||
},
|
||||
}
|
||||
|
||||
def get_niveaux_by_parcours(self, annee) -> dict:
|
||||
def get_niveaux_by_parcours(
|
||||
self, annee: int, parcour: "ApcParcours" = None
|
||||
) -> tuple[list["ApcParcours"], dict]:
|
||||
"""
|
||||
Construit la liste des niveaux de compétences pour chaque parcours
|
||||
de ce référentiel.
|
||||
de ce référentiel, ou seulement pour le parcours donné.
|
||||
|
||||
Les niveaux sont groupés par parcours, en isolant les niveaux de tronc commun.
|
||||
|
||||
Le tronc commun n'est pas identifié comme tel dans les référentiels Orébut:
|
||||
on cherche les niveaux qui sont présents dans tous les parcours et les range sous
|
||||
la clé "TC" (toujours présente mais éventuellement liste vide si pas de tronc commun).
|
||||
|
||||
résultat:
|
||||
Résultat: couple
|
||||
( [ ApcParcours ],
|
||||
{
|
||||
"TC" : [ ApcNiveau ],
|
||||
parcour.id : [ ApcNiveau ]
|
||||
}
|
||||
)
|
||||
"""
|
||||
parcours = self.parcours.order_by(ApcParcours.numero).all()
|
||||
parcours_ref = self.parcours.order_by(ApcParcours.numero).all()
|
||||
if parcour is None:
|
||||
parcours = parcours_ref
|
||||
else:
|
||||
parcours = [parcour]
|
||||
niveaux_by_parcours = {
|
||||
parcour.id: ApcNiveau.niveaux_annee_de_parcours(parcour, annee, self)
|
||||
for parcour in parcours
|
||||
for parcour in parcours_ref
|
||||
}
|
||||
# Cherche tronc commun
|
||||
if niveaux_by_parcours:
|
||||
@ -154,7 +179,28 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
||||
niveau for niveau in niveaux_parcours_1 if niveau.id in niveaux_ids_tc
|
||||
]
|
||||
niveaux_by_parcours_no_tc["TC"] = niveaux_tc
|
||||
return niveaux_by_parcours_no_tc
|
||||
return parcours, niveaux_by_parcours_no_tc
|
||||
|
||||
def get_competences_tronc_commun(self) -> list["ApcCompetence"]:
|
||||
"""Liste des compétences communes à tous les parcours du référentiel."""
|
||||
parcours = self.parcours.all()
|
||||
if not parcours:
|
||||
return []
|
||||
|
||||
ids = set.intersection(
|
||||
*[
|
||||
{competence.id for competence in parcour.query_competences()}
|
||||
for parcour in parcours
|
||||
]
|
||||
)
|
||||
return sorted(
|
||||
[
|
||||
competence
|
||||
for competence in parcours[0].query_competences()
|
||||
if competence.id in ids
|
||||
],
|
||||
key=lambda c: c.numero or 0,
|
||||
)
|
||||
|
||||
|
||||
class ApcCompetence(db.Model, XMLModel):
|
||||
@ -197,7 +243,7 @@ class ApcCompetence(db.Model, XMLModel):
|
||||
def __repr__(self):
|
||||
return f"<ApcCompetence {self.id} {self.titre!r}>"
|
||||
|
||||
def to_dict(self):
|
||||
def to_dict(self, with_app_critiques=True):
|
||||
"repr dict recursive sur situations, composantes, niveaux"
|
||||
return {
|
||||
"id_orebut": self.id_orebut,
|
||||
@ -209,7 +255,10 @@ class ApcCompetence(db.Model, XMLModel):
|
||||
"composantes_essentielles": [
|
||||
x.to_dict() for x in self.composantes_essentielles
|
||||
],
|
||||
"niveaux": {x.annee: x.to_dict() for x in self.niveaux},
|
||||
"niveaux": {
|
||||
x.annee: x.to_dict(with_app_critiques=with_app_critiques)
|
||||
for x in self.niveaux
|
||||
},
|
||||
}
|
||||
|
||||
def to_dict_bul(self) -> dict:
|
||||
@ -275,13 +324,15 @@ class ApcNiveau(db.Model, XMLModel):
|
||||
return f"""<{self.__class__.__name__} {self.id} ordre={self.ordre!r} annee={
|
||||
self.annee!r} {self.competence!r}>"""
|
||||
|
||||
def to_dict(self):
|
||||
"as a dict, recursif sur les AC"
|
||||
def to_dict(self, with_app_critiques=True):
|
||||
"as a dict, recursif (ou non) sur les AC"
|
||||
return {
|
||||
"libelle": self.libelle,
|
||||
"annee": self.annee,
|
||||
"ordre": self.ordre,
|
||||
"app_critiques": {x.code: x.to_dict() for x in self.app_critiques},
|
||||
"app_critiques": {x.code: x.to_dict() for x in self.app_critiques}
|
||||
if with_app_critiques
|
||||
else {},
|
||||
}
|
||||
|
||||
def to_dict_bul(self):
|
||||
@ -306,9 +357,8 @@ class ApcNiveau(db.Model, XMLModel):
|
||||
if annee not in {1, 2, 3}:
|
||||
raise ValueError("annee invalide pour un parcours BUT")
|
||||
if referentiel_competence is None:
|
||||
raise ScoValueError(
|
||||
"Pas de référentiel de compétences associé à la formation !"
|
||||
)
|
||||
raise ScoNoReferentielCompetences()
|
||||
|
||||
annee_formation = f"BUT{annee}"
|
||||
if parcour is None:
|
||||
return ApcNiveau.query.filter(
|
||||
@ -436,6 +486,7 @@ class ApcParcours(db.Model, XMLModel):
|
||||
lazy="dynamic",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
ues = db.relationship("UniteEns", back_populates="parcour")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__.__name__} {self.id} {self.code!r} ref={self.referentiel}>"
|
||||
@ -453,6 +504,14 @@ class ApcParcours(db.Model, XMLModel):
|
||||
d["annees"] = {x.ordre: x.to_dict() for x in self.annees}
|
||||
return d
|
||||
|
||||
def query_competences(self) -> flask_sqlalchemy.BaseQuery:
|
||||
"Les compétences associées à ce parcours"
|
||||
return (
|
||||
ApcCompetence.query.join(ApcParcoursNiveauCompetence, ApcAnneeParcours)
|
||||
.filter_by(parcours_id=self.id)
|
||||
.order_by(ApcCompetence.numero)
|
||||
)
|
||||
|
||||
|
||||
class ApcAnneeParcours(db.Model, XMLModel):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
@ -2,19 +2,17 @@
|
||||
|
||||
"""Décisions de jury (validations) des RCUE et années du BUT
|
||||
"""
|
||||
|
||||
import flask_sqlalchemy
|
||||
from sqlalchemy.sql import text
|
||||
from typing import Union
|
||||
|
||||
from app import db
|
||||
import flask_sqlalchemy
|
||||
|
||||
from app import db
|
||||
from app.models import CODE_STR_LEN
|
||||
from app.models.but_refcomp import ApcNiveau
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.ues import UniteEns
|
||||
from app.models.formations import Formation
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.models.ues import UniteEns
|
||||
from app.scodoc import sco_codes_parcours as sco_codes
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
@ -63,13 +61,32 @@ class ApcValidationRCUE(db.Model):
|
||||
self.ue1}/{self.ue2}:{self.code!r}>"""
|
||||
|
||||
def __str__(self):
|
||||
return f"""décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}: {self.code}"""
|
||||
return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}: {
|
||||
self.code} enregistrée le {self.date.strftime("%d/%m/%Y")}"""
|
||||
|
||||
def to_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("%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:
|
||||
"""Le niveau de compétence associé à cet RCUE."""
|
||||
# Par convention, il est donné par la seconde UE
|
||||
return self.ue2.niveau_competence
|
||||
|
||||
def to_dict(self):
|
||||
"as a dict"
|
||||
d = dict(self.__dict__)
|
||||
d.pop("_sa_instance_state", None)
|
||||
return d
|
||||
|
||||
def to_dict_bul(self) -> dict:
|
||||
"Export dict pour bulletins: le code et le niveau de compétence"
|
||||
niveau = self.niveau()
|
||||
@ -84,28 +101,24 @@ class RegroupementCoherentUE:
|
||||
"""Le regroupement cohérent d'UE, dans la terminologie du BUT, est le couple d'UEs
|
||||
de la même année (BUT1,2,3) liées au *même niveau de compétence*.
|
||||
|
||||
La moyenne (10/20) au RCU déclenche la compensation des UE.
|
||||
La moyenne (10/20) au RCUE déclenche la compensation des UE.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
etud: Identite,
|
||||
formsemestre_1: FormSemestre,
|
||||
ue_1: UniteEns,
|
||||
dec_ue_1: "DecisionsProposeesUE",
|
||||
formsemestre_2: FormSemestre,
|
||||
ue_2: UniteEns,
|
||||
dec_ue_2: "DecisionsProposeesUE",
|
||||
inscription_etat: str,
|
||||
):
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
|
||||
ue_1 = dec_ue_1.ue
|
||||
ue_2 = dec_ue_2.ue
|
||||
# Ordonne les UE dans le sens croissant (S1,S2) ou (S3,S4)...
|
||||
if formsemestre_1.semestre_id > formsemestre_2.semestre_id:
|
||||
(ue_1, formsemestre_1), (ue_2, formsemestre_2) = (
|
||||
(
|
||||
ue_2,
|
||||
formsemestre_2,
|
||||
),
|
||||
(ue_2, formsemestre_2),
|
||||
(ue_1, formsemestre_1),
|
||||
)
|
||||
assert formsemestre_1.semestre_id % 2 == 1
|
||||
@ -125,21 +138,12 @@ class RegroupementCoherentUE:
|
||||
self.moy_ue_1 = self.moy_ue_2 = "-"
|
||||
self.moy_ue_1_val = self.moy_ue_2_val = 0.0
|
||||
return
|
||||
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre_1)
|
||||
if ue_1.id in res.etud_moy_ue and etud.id in res.etud_moy_ue[ue_1.id]:
|
||||
self.moy_ue_1 = res.etud_moy_ue[ue_1.id][etud.id]
|
||||
self.moy_ue_1_val = self.moy_ue_1 # toujours float, peut être NaN
|
||||
else:
|
||||
self.moy_ue_1 = None
|
||||
self.moy_ue_1_val = 0.0
|
||||
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre_2)
|
||||
if ue_2.id in res.etud_moy_ue and etud.id in res.etud_moy_ue[ue_2.id]:
|
||||
self.moy_ue_2 = res.etud_moy_ue[ue_2.id][etud.id]
|
||||
self.moy_ue_2_val = self.moy_ue_2
|
||||
else:
|
||||
self.moy_ue_2 = None
|
||||
self.moy_ue_2_val = 0.0
|
||||
# Calcul de la moyenne au RCUE
|
||||
self.moy_ue_1 = dec_ue_1.moy_ue_with_cap
|
||||
self.moy_ue_1_val = self.moy_ue_1 if self.moy_ue_1 is not None else 0.0
|
||||
self.moy_ue_2 = dec_ue_2.moy_ue_with_cap
|
||||
self.moy_ue_2_val = self.moy_ue_2 if self.moy_ue_2 is not None else 0.0
|
||||
|
||||
# Calcul de la moyenne au RCUE (utilise les moy d'UE capitalisées)
|
||||
if (self.moy_ue_1 is not None) and (self.moy_ue_2 is not None):
|
||||
# Moyenne RCUE (les pondérations par défaut sont 1.)
|
||||
self.moy_rcue = (
|
||||
@ -149,7 +153,14 @@ class RegroupementCoherentUE:
|
||||
self.moy_rcue = None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__.__name__} {self.ue_1.acronyme}({self.moy_ue_1}) {self.ue_2.acronyme}({self.moy_ue_2})>"
|
||||
return f"""<{self.__class__.__name__} {
|
||||
self.ue_1.acronyme}({self.moy_ue_1}) {
|
||||
self.ue_2.acronyme}({self.moy_ue_2})>"""
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"""RCUE {
|
||||
self.ue_1.acronyme}({self.moy_ue_1}) + {
|
||||
self.ue_2.acronyme}({self.moy_ue_2})"""
|
||||
|
||||
def query_validations(
|
||||
self,
|
||||
@ -181,8 +192,9 @@ class RegroupementCoherentUE:
|
||||
return self.query_validations().count() > 0
|
||||
|
||||
def est_compensable(self):
|
||||
"""Vrai si ce RCUE est validable par compensation
|
||||
c'est à dire que sa moyenne est > 10 avec une UE < 10
|
||||
"""Vrai si ce RCUE est validable (uniquement) par compensation
|
||||
c'est à dire que sa moyenne est > 10 avec une UE < 10.
|
||||
Note: si ADM, est_compensable est faux.
|
||||
"""
|
||||
return (
|
||||
(self.moy_rcue is not None)
|
||||
@ -218,62 +230,62 @@ class RegroupementCoherentUE:
|
||||
|
||||
|
||||
# unused
|
||||
def find_rcues(
|
||||
formsemestre: FormSemestre, ue: UniteEns, etud: Identite, inscription_etat: str
|
||||
) -> list[RegroupementCoherentUE]:
|
||||
"""Les RCUE (niveau de compétence) à considérer pour cet étudiant dans
|
||||
ce semestre pour cette UE.
|
||||
# def find_rcues(
|
||||
# formsemestre: FormSemestre, ue: UniteEns, etud: Identite, inscription_etat: str
|
||||
# ) -> list[RegroupementCoherentUE]:
|
||||
# """Les RCUE (niveau de compétence) à considérer pour cet étudiant dans
|
||||
# ce semestre pour cette UE.
|
||||
|
||||
Cherche les UEs du même niveau de compétence auxquelles l'étudiant est inscrit.
|
||||
En cas de redoublement, il peut y en avoir plusieurs, donc plusieurs RCUEs.
|
||||
# Cherche les UEs du même niveau de compétence auxquelles l'étudiant est inscrit.
|
||||
# En cas de redoublement, il peut y en avoir plusieurs, donc plusieurs RCUEs.
|
||||
|
||||
Résultat: la liste peut être vide.
|
||||
"""
|
||||
if (ue.niveau_competence is None) or (ue.semestre_idx is None):
|
||||
return []
|
||||
# Résultat: la liste peut être vide.
|
||||
# """
|
||||
# if (ue.niveau_competence is None) or (ue.semestre_idx is None):
|
||||
# return []
|
||||
|
||||
if ue.semestre_idx % 2: # S1, S3, S5
|
||||
other_semestre_idx = ue.semestre_idx + 1
|
||||
else:
|
||||
other_semestre_idx = ue.semestre_idx - 1
|
||||
# if ue.semestre_idx % 2: # S1, S3, S5
|
||||
# other_semestre_idx = ue.semestre_idx + 1
|
||||
# else:
|
||||
# other_semestre_idx = ue.semestre_idx - 1
|
||||
|
||||
cursor = db.session.execute(
|
||||
text(
|
||||
"""SELECT
|
||||
ue.id, formsemestre.id
|
||||
FROM
|
||||
notes_ue ue,
|
||||
notes_formsemestre_inscription inscr,
|
||||
notes_formsemestre formsemestre
|
||||
# cursor = db.session.execute(
|
||||
# text(
|
||||
# """SELECT
|
||||
# ue.id, formsemestre.id
|
||||
# FROM
|
||||
# notes_ue ue,
|
||||
# notes_formsemestre_inscription inscr,
|
||||
# notes_formsemestre formsemestre
|
||||
|
||||
WHERE
|
||||
inscr.etudid = :etudid
|
||||
AND inscr.formsemestre_id = formsemestre.id
|
||||
# WHERE
|
||||
# inscr.etudid = :etudid
|
||||
# AND inscr.formsemestre_id = formsemestre.id
|
||||
|
||||
AND formsemestre.semestre_id = :other_semestre_idx
|
||||
AND ue.formation_id = formsemestre.formation_id
|
||||
AND ue.niveau_competence_id = :ue_niveau_competence_id
|
||||
AND ue.semestre_idx = :other_semestre_idx
|
||||
"""
|
||||
),
|
||||
{
|
||||
"etudid": etud.id,
|
||||
"other_semestre_idx": other_semestre_idx,
|
||||
"ue_niveau_competence_id": ue.niveau_competence_id,
|
||||
},
|
||||
)
|
||||
rcues = []
|
||||
for ue_id, formsemestre_id in cursor:
|
||||
other_ue = UniteEns.query.get(ue_id)
|
||||
other_formsemestre = FormSemestre.query.get(formsemestre_id)
|
||||
rcues.append(
|
||||
RegroupementCoherentUE(
|
||||
etud, formsemestre, ue, other_formsemestre, other_ue, inscription_etat
|
||||
)
|
||||
)
|
||||
# safety check: 1 seul niveau de comp. concerné:
|
||||
assert len({rcue.ue_1.niveau_competence_id for rcue in rcues}) == 1
|
||||
return rcues
|
||||
# AND formsemestre.semestre_id = :other_semestre_idx
|
||||
# AND ue.formation_id = formsemestre.formation_id
|
||||
# AND ue.niveau_competence_id = :ue_niveau_competence_id
|
||||
# AND ue.semestre_idx = :other_semestre_idx
|
||||
# """
|
||||
# ),
|
||||
# {
|
||||
# "etudid": etud.id,
|
||||
# "other_semestre_idx": other_semestre_idx,
|
||||
# "ue_niveau_competence_id": ue.niveau_competence_id,
|
||||
# },
|
||||
# )
|
||||
# rcues = []
|
||||
# for ue_id, formsemestre_id in cursor:
|
||||
# other_ue = UniteEns.query.get(ue_id)
|
||||
# other_formsemestre = FormSemestre.query.get(formsemestre_id)
|
||||
# rcues.append(
|
||||
# RegroupementCoherentUE(
|
||||
# etud, formsemestre, ue, other_formsemestre, other_ue, inscription_etat
|
||||
# )
|
||||
# )
|
||||
# # safety check: 1 seul niveau de comp. concerné:
|
||||
# assert len({rcue.ue_1.niveau_competence_id for rcue in rcues}) == 1
|
||||
# return rcues
|
||||
|
||||
|
||||
class ApcValidationAnnee(db.Model):
|
||||
@ -281,7 +293,7 @@ class ApcValidationAnnee(db.Model):
|
||||
|
||||
__tablename__ = "apc_validation_annee"
|
||||
# Assure unicité de la décision:
|
||||
__table_args__ = (db.UniqueConstraint("etudid", "annee_scolaire"),)
|
||||
__table_args__ = (db.UniqueConstraint("etudid", "annee_scolaire", "ordre"),)
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
etudid = db.Column(
|
||||
db.Integer,
|
||||
@ -303,7 +315,8 @@ class ApcValidationAnnee(db.Model):
|
||||
formsemestre = db.relationship("FormSemestre", backref="apc_validations_annees")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} {self.id} {self.etud} BUT{self.ordre}/{self.annee_scolaire}:{self.code!r}>"
|
||||
return f"""<{self.__class__.__name__} {self.id} {self.etud
|
||||
} BUT{self.ordre}/{self.annee_scolaire}:{self.code!r}>"""
|
||||
|
||||
def __str__(self):
|
||||
return f"""décision sur année BUT{self.ordre} {self.annee_scolaire} : {self.code}"""
|
||||
@ -340,7 +353,8 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
|
||||
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"]}"""
|
||||
f"""{niveau["competence"]["titre"]} {niveau["ordre"]}: {
|
||||
dec_rcue["code"]}"""
|
||||
)
|
||||
decisions["descr_decisions_rcue"] = ", ".join(titres_rcues)
|
||||
decisions["descr_decisions_niveaux"] = (
|
||||
|
@ -6,13 +6,14 @@
|
||||
from flask import flash
|
||||
from app import db, log
|
||||
from app.comp import bonus_spo
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
from app.scodoc.sco_codes_parcours import (
|
||||
ABAN,
|
||||
ABL,
|
||||
ADC,
|
||||
ADJ,
|
||||
ADJR,
|
||||
ADM,
|
||||
AJ,
|
||||
ATB,
|
||||
@ -34,6 +35,7 @@ CODES_SCODOC_TO_APO = {
|
||||
ABL: "ABL",
|
||||
ADC: "ADMC",
|
||||
ADJ: "ADM",
|
||||
ADJR: "ADM",
|
||||
ADM: "ADM",
|
||||
AJ: "AJ",
|
||||
ATB: "AJAC",
|
||||
@ -83,6 +85,8 @@ class ScoDocSiteConfig(db.Model):
|
||||
"INSTITUTION_CITY": str,
|
||||
"DEFAULT_PDF_FOOTER_TEMPLATE": str,
|
||||
"enable_entreprises": bool,
|
||||
"month_debut_annee_scolaire": int,
|
||||
"month_debut_periode2": int,
|
||||
}
|
||||
|
||||
def __init__(self, name, value):
|
||||
@ -223,3 +227,73 @@ class ScoDocSiteConfig(db.Model):
|
||||
db.session.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def _get_int_field(cls, name: str, default=None) -> int:
|
||||
"""Valeur d'un champs integer"""
|
||||
cfg = ScoDocSiteConfig.query.filter_by(name=name).first()
|
||||
if (cfg is None) or cfg.value is None:
|
||||
return default
|
||||
return int(cfg.value)
|
||||
|
||||
@classmethod
|
||||
def _set_int_field(
|
||||
cls,
|
||||
name: str,
|
||||
value: int,
|
||||
default=None,
|
||||
range_values: tuple = (),
|
||||
) -> bool:
|
||||
"""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])
|
||||
):
|
||||
raise ValueError("invalid value")
|
||||
cfg = ScoDocSiteConfig.query.filter_by(name=name).first()
|
||||
if cfg is None:
|
||||
cfg = ScoDocSiteConfig(name=name, value=str(value))
|
||||
else:
|
||||
cfg.value = str(value)
|
||||
db.session.add(cfg)
|
||||
db.session.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def get_month_debut_annee_scolaire(cls) -> int:
|
||||
"""Mois de début de l'année scolaire."""
|
||||
return cls._get_int_field(
|
||||
"month_debut_annee_scolaire", scu.MONTH_DEBUT_ANNEE_SCOLAIRE
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_month_debut_periode2(cls) -> int:
|
||||
"""Mois de début de l'année scolaire."""
|
||||
return cls._get_int_field("month_debut_periode2", scu.MONTH_DEBUT_PERIODE2)
|
||||
|
||||
@classmethod
|
||||
def set_month_debut_annee_scolaire(
|
||||
cls, month: int = scu.MONTH_DEBUT_ANNEE_SCOLAIRE
|
||||
) -> bool:
|
||||
"""Fixe le mois de début des années scolaires.
|
||||
True si changement.
|
||||
"""
|
||||
if cls._set_int_field(
|
||||
"month_debut_annee_scolaire", month, scu.MONTH_DEBUT_ANNEE_SCOLAIRE, (1, 12)
|
||||
):
|
||||
log(f"set_month_debut_annee_scolaire({month})")
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def set_month_debut_periode2(cls, month: int = scu.MONTH_DEBUT_PERIODE2) -> bool:
|
||||
"""Fixe le mois de début des années scolaires.
|
||||
True si changement.
|
||||
"""
|
||||
if cls._set_int_field(
|
||||
"month_debut_periode2", month, scu.MONTH_DEBUT_PERIODE2, (1, 12)
|
||||
):
|
||||
log(f"set_month_debut_periode2({month})")
|
||||
return True
|
||||
return False
|
||||
|
@ -2,10 +2,10 @@
|
||||
|
||||
"""ScoDoc models : departements
|
||||
"""
|
||||
from typing import Any
|
||||
|
||||
from app import db
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models.preferences import ScoPreference
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
|
||||
@ -39,7 +39,7 @@ class Departement(db.Model):
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__}(id={self.id}, acronym='{self.acronym}')>"
|
||||
|
||||
def to_dict(self):
|
||||
def to_dict(self, with_dept_name=True, with_dept_preferences=False):
|
||||
data = {
|
||||
"id": self.id,
|
||||
"acronym": self.acronym,
|
||||
@ -47,6 +47,17 @@ class Departement(db.Model):
|
||||
"visible": self.visible,
|
||||
"date_creation": self.date_creation,
|
||||
}
|
||||
if with_dept_name:
|
||||
pref = ScoPreference.query.filter_by(
|
||||
dept_id=self.id, name="DeptName"
|
||||
).first()
|
||||
data["dept_name"] = pref.value if pref else None
|
||||
# Ceci n'est pas encore utilisé, mais pourrait être publié
|
||||
# par l'API après nettoyage des préférences.
|
||||
if with_dept_preferences:
|
||||
data["preferences"] = {
|
||||
p.name: p.value for p in ScoPreference.query.filter_by(dept_id=self.id)
|
||||
}
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
|
@ -58,6 +58,16 @@ class Identite(db.Model):
|
||||
billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic")
|
||||
#
|
||||
admission = db.relationship("Admission", backref="identite", lazy="dynamic")
|
||||
dispense_ues = db.relationship(
|
||||
"DispenseUE",
|
||||
back_populates="etud",
|
||||
cascade="all, delete",
|
||||
passive_deletes=True,
|
||||
)
|
||||
|
||||
# Relations avec les assiduites et les justificatifs
|
||||
assiduites = db.relationship("Assiduite", backref="etudiant", lazy="dynamic")
|
||||
justificatifs = db.relationship("Justificatif", backref="etudiant", lazy="dynamic")
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
@ -73,6 +83,14 @@ class Identite(db.Model):
|
||||
args = make_etud_args(etudid=etudid, code_nip=code_nip)
|
||||
return Identite.query.filter_by(**args).first_or_404()
|
||||
|
||||
@classmethod
|
||||
def create_etud(cls, **args):
|
||||
"Crée un étudiant, avec admission et adresse vides."
|
||||
etud: Identite = cls(**args)
|
||||
etud.adresses.append(Adresse())
|
||||
etud.admission.append(Admission())
|
||||
return etud
|
||||
|
||||
@property
|
||||
def civilite_str(self):
|
||||
"""returns 'M.' ou 'Mme' ou '' (pour le genre neutre,
|
||||
|
@ -13,6 +13,8 @@ from app.models.ues import UniteEns
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
import app.scodoc.notesdb as ndb
|
||||
|
||||
DEFAULT_EVALUATION_TIME = datetime.time(8, 0)
|
||||
|
||||
|
||||
class Evaluation(db.Model):
|
||||
"""Evaluation (contrôle, examen, ...)"""
|
||||
@ -51,7 +53,7 @@ class Evaluation(db.Model):
|
||||
self.description[:16] if self.description else ''}">"""
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"Représentation dict, pour json"
|
||||
"Représentation dict (riche, compat ScoDoc 7)"
|
||||
e = dict(self.__dict__)
|
||||
e.pop("_sa_instance_state", None)
|
||||
# ScoDoc7 output_formators
|
||||
@ -71,6 +73,34 @@ class Evaluation(db.Model):
|
||||
e["poids"] = self.get_ue_poids_dict() # { ue_id : poids }
|
||||
return evaluation_enrich_dict(e)
|
||||
|
||||
def to_dict_api(self) -> dict:
|
||||
"Représentation dict pour API JSON"
|
||||
if self.jour is None:
|
||||
date_debut = None
|
||||
date_fin = None
|
||||
else:
|
||||
date_debut = datetime.datetime.combine(
|
||||
self.jour, self.heure_debut or datetime.time(0, 0)
|
||||
).isoformat()
|
||||
date_fin = datetime.datetime.combine(
|
||||
self.jour, self.heure_fin or datetime.time(0, 0)
|
||||
).isoformat()
|
||||
|
||||
return {
|
||||
"coefficient": self.coefficient,
|
||||
"date_debut": date_debut,
|
||||
"date_fin": date_fin,
|
||||
"description": self.description,
|
||||
"evaluation_type": self.evaluation_type,
|
||||
"id": self.id,
|
||||
"moduleimpl_id": self.moduleimpl_id,
|
||||
"note_max": self.note_max,
|
||||
"numero": self.numero,
|
||||
"poids": self.get_ue_poids_dict(),
|
||||
"publish_incomplete": self.publish_incomplete,
|
||||
"visi_bulletin": self.visibulletin,
|
||||
}
|
||||
|
||||
def from_dict(self, data):
|
||||
"""Set evaluation attributes from given dict values."""
|
||||
check_evaluation_args(data)
|
||||
@ -83,12 +113,24 @@ class Evaluation(db.Model):
|
||||
if self.heure_debut and (
|
||||
not self.heure_fin or self.heure_fin == self.heure_debut
|
||||
):
|
||||
return f"""à {self.heure_debut.strftime("%H:%M")}"""
|
||||
return f"""à {self.heure_debut.strftime("%Hh%M")}"""
|
||||
elif self.heure_debut and self.heure_fin:
|
||||
return f"""de {self.heure_debut.strftime("%H:%M")} à {self.heure_fin.strftime("%H:%M")}"""
|
||||
return f"""de {self.heure_debut.strftime("%Hh%M")} à {self.heure_fin.strftime("%Hh%M")}"""
|
||||
else:
|
||||
return ""
|
||||
|
||||
def descr_duree(self) -> str:
|
||||
"Description de la durée pour affichages"
|
||||
if self.heure_debut is None and self.heure_fin is None:
|
||||
return ""
|
||||
debut = self.heure_debut or DEFAULT_EVALUATION_TIME
|
||||
fin = self.heure_fin or DEFAULT_EVALUATION_TIME
|
||||
d = (fin.hour * 60 + fin.minute) - (debut.hour * 60 + debut.minute)
|
||||
duree = f"{d//60}h"
|
||||
if d % 60:
|
||||
duree += f"{d%60:02d}"
|
||||
return duree
|
||||
|
||||
def clone(self, not_copying=()):
|
||||
"""Clone, not copying the given attrs
|
||||
Attention: la copie n'a pas d'id avant le prochain commit
|
||||
@ -227,7 +269,7 @@ def evaluation_enrich_dict(e: dict):
|
||||
heure_fin_dt = e["heure_fin"] or datetime.time(8, 00)
|
||||
e["heure_debut"] = ndb.TimefromISO8601(e["heure_debut"])
|
||||
e["heure_fin"] = ndb.TimefromISO8601(e["heure_fin"])
|
||||
e["jouriso"] = ndb.DateDMYtoISO(e["jour"])
|
||||
e["jour_iso"] = ndb.DateDMYtoISO(e["jour"])
|
||||
heure_debut, heure_fin = e["heure_debut"], e["heure_fin"]
|
||||
d = ndb.TimeDuration(heure_debut, heure_fin)
|
||||
if d is not None:
|
||||
|
@ -36,6 +36,7 @@ class Formation(db.Model):
|
||||
titre = db.Column(db.Text(), nullable=False)
|
||||
titre_officiel = db.Column(db.Text(), nullable=False)
|
||||
version = db.Column(db.Integer, default=1, server_default="1")
|
||||
commentaire = db.Column(db.Text())
|
||||
formation_code = db.Column(
|
||||
db.String(SHORT_STR_LEN),
|
||||
server_default=db.text("notes_newid_fcod()"),
|
||||
@ -55,18 +56,21 @@ class Formation(db.Model):
|
||||
modules = db.relationship("Module", lazy="dynamic", backref="formation")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__}(id={self.id}, dept_id={self.dept_id}, acronyme='{self.acronyme!r}')>"
|
||||
return f"""<{self.__class__.__name__}(id={self.id}, dept_id={
|
||||
self.dept_id}, acronyme={self.acronyme!r}, version={self.version})>"""
|
||||
|
||||
def to_html(self) -> str:
|
||||
"titre complet pour affichage"
|
||||
return f"""Formation {self.titre} ({self.acronyme}) [version {self.version}] code {self.formation_code}"""
|
||||
|
||||
def to_dict(self, with_refcomp_attrs=False):
|
||||
""" "as a dict.
|
||||
"""As a dict.
|
||||
Si with_refcomp_attrs, ajoute attributs permettant de retrouver le ref. de comp.
|
||||
"""
|
||||
e = dict(self.__dict__)
|
||||
e.pop("_sa_instance_state", None)
|
||||
if "referentiel_competence" in e:
|
||||
e.pop("referentiel_competence")
|
||||
e["departement"] = self.departement.to_dict()
|
||||
e["formation_id"] = self.id # ScoDoc7 backward compat
|
||||
if with_refcomp_attrs and self.referentiel_competence:
|
||||
@ -201,12 +205,17 @@ class Formation(db.Model):
|
||||
|
||||
def query_ues_parcour(self, parcour: ApcParcours) -> flask_sqlalchemy.BaseQuery:
|
||||
"""Les UEs d'un parcours de la formation.
|
||||
Si parcour est None, les UE sans parcours.
|
||||
Exemple: pour avoir les UE du semestre 3, faire
|
||||
`formation.query_ues_parcour(parcour).filter_by(semestre_idx=3)`
|
||||
"""
|
||||
return UniteEns.query.filter_by(formation=self).filter(
|
||||
if parcour is None:
|
||||
return UniteEns.query.filter_by(
|
||||
formation=self, type=UE_STANDARD, parcour_id=None
|
||||
)
|
||||
return UniteEns.query.filter_by(formation=self, type=UE_STANDARD).filter(
|
||||
UniteEns.niveau_competence_id == ApcNiveau.id,
|
||||
UniteEns.type == UE_STANDARD,
|
||||
(UniteEns.parcour_id == parcour.id) | (UniteEns.parcour_id == None),
|
||||
ApcParcoursNiveauCompetence.competence_id == ApcNiveau.competence_id,
|
||||
ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id,
|
||||
ApcAnneeParcours.parcours_id == parcour.id,
|
||||
@ -233,6 +242,21 @@ class Formation(db.Model):
|
||||
.filter(ApcAnneeParcours.parcours_id == parcour.id)
|
||||
)
|
||||
|
||||
def refcomp_desassoc(self):
|
||||
"""Désassocie la formation de son ref. de compétence"""
|
||||
self.referentiel_competence = None
|
||||
db.session.add(self)
|
||||
# Niveaux des UE
|
||||
for ue in self.ues:
|
||||
ue.niveau_competence = None
|
||||
db.session.add(ue)
|
||||
# Parcours et AC des modules
|
||||
for mod in self.modules:
|
||||
mod.parcours = []
|
||||
mod.app_critiques = []
|
||||
db.session.add(mod)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
class Matiere(db.Model):
|
||||
"""Matières: regroupe les modules d'une UE
|
||||
|
@ -1,48 +1,47 @@
|
||||
# -*- coding: UTF-8 -*
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
# pylint génère trop de faux positifs avec les colonnes date:
|
||||
# pylint: disable=no-member,not-an-iterable
|
||||
|
||||
"""ScoDoc models: formsemestre
|
||||
"""
|
||||
import datetime
|
||||
from functools import cached_property
|
||||
|
||||
from flask import flash, g
|
||||
import flask_sqlalchemy
|
||||
from flask import flash, g
|
||||
from sqlalchemy import and_, or_
|
||||
from sqlalchemy.sql import text
|
||||
|
||||
from app import db
|
||||
from app import log
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models import CODE_STR_LEN
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import db, log
|
||||
from app.models import APO_CODE_STR_LEN, CODE_STR_LEN, SHORT_STR_LEN
|
||||
from app.models.but_refcomp import (
|
||||
ApcAnneeParcours,
|
||||
ApcNiveau,
|
||||
ApcParcours,
|
||||
ApcParcoursNiveauCompetence,
|
||||
ApcReferentielCompetences,
|
||||
parcours_formsemestre,
|
||||
)
|
||||
from app.models.groups import GroupDescr, Partition
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.models.but_refcomp import parcours_formsemestre
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.formations import Formation
|
||||
from app.models.modules import Module
|
||||
from app.models.groups import GroupDescr, Partition
|
||||
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
|
||||
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc.sco_vdi import ApoEtapeVDI
|
||||
from app.scodoc import sco_codes_parcours, sco_preferences
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_utils import MONTH_NAMES_ABBREV
|
||||
from app.scodoc.sco_vdi import ApoEtapeVDI
|
||||
|
||||
|
||||
class FormSemestre(db.Model):
|
||||
@ -57,51 +56,58 @@ class FormSemestre(db.Model):
|
||||
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
|
||||
formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id"))
|
||||
semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1")
|
||||
titre = db.Column(db.Text())
|
||||
date_debut = db.Column(db.Date())
|
||||
date_fin = db.Column(db.Date())
|
||||
etat = db.Column(
|
||||
db.Boolean(), nullable=False, default=True, server_default="true"
|
||||
) # False si verrouillé
|
||||
titre = db.Column(db.Text(), nullable=False)
|
||||
date_debut = db.Column(db.Date(), nullable=False)
|
||||
date_fin = db.Column(db.Date(), nullable=False)
|
||||
etat = db.Column(db.Boolean(), nullable=False, default=True, server_default="true")
|
||||
"False si verrouillé"
|
||||
modalite = db.Column(
|
||||
db.String(SHORT_STR_LEN), db.ForeignKey("notes_form_modalites.modalite")
|
||||
) # "FI", "FAP", "FC", ...
|
||||
# gestion compensation sem DUT:
|
||||
)
|
||||
"Modalité de formation: 'FI', 'FAP', 'FC', ..."
|
||||
gestion_compensation = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
)
|
||||
# ne publie pas le bulletin XML ou JSON:
|
||||
"gestion compensation sem DUT (inutilisé en APC)"
|
||||
bul_hide_xml = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
)
|
||||
# Bloque le calcul des moyennes (générale et d'UE)
|
||||
"ne publie pas le bulletin XML ou JSON"
|
||||
block_moyennes = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
)
|
||||
# semestres decales (pour gestion jurys):
|
||||
"Bloque le calcul des moyennes (générale et d'UE)"
|
||||
block_moyenne_generale = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
)
|
||||
"Si vrai, la moyenne générale indicative BUT n'est pas calculée"
|
||||
gestion_semestrielle = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
)
|
||||
# couleur fond bulletins HTML:
|
||||
"Semestres décalés (pour gestion jurys DUT, pas implémenté ou utile en BUT)"
|
||||
bul_bgcolor = db.Column(
|
||||
db.String(SHORT_STR_LEN), default="white", server_default="white"
|
||||
db.String(SHORT_STR_LEN),
|
||||
default="white",
|
||||
server_default="white",
|
||||
nullable=False,
|
||||
)
|
||||
# autorise resp. a modifier semestre:
|
||||
"couleur fond bulletins HTML"
|
||||
resp_can_edit = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
)
|
||||
# autorise resp. a modifier slt les enseignants:
|
||||
"autorise resp. à modifier le formsemestre"
|
||||
resp_can_change_ens = db.Column(
|
||||
db.Boolean(), nullable=False, default=True, server_default="true"
|
||||
)
|
||||
# autorise les ens a creer des evals:
|
||||
"autorise resp. a modifier slt les enseignants"
|
||||
ens_can_edit_eval = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="False"
|
||||
)
|
||||
# code element semestre Apogee, eg 'VRTW1' ou 'V2INCS4,V2INLS4,...'
|
||||
"autorise les enseignants à créer des évals dans leurs modimpls"
|
||||
elt_sem_apo = db.Column(db.Text()) # peut être fort long !
|
||||
# code element annee Apogee, eg 'VRT1A' ou 'V2INLA,V2INCA,...'
|
||||
"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,...'"
|
||||
|
||||
# Relations:
|
||||
etapes = db.relationship(
|
||||
@ -111,6 +117,7 @@ class FormSemestre(db.Model):
|
||||
"ModuleImpl",
|
||||
backref="formsemestre",
|
||||
lazy="dynamic",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
etuds = db.relationship(
|
||||
"Identite",
|
||||
@ -148,7 +155,12 @@ class FormSemestre(db.Model):
|
||||
self.modalite = FormationModalite.DEFAULT_MODALITE
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} {self.id} {self.titre_num()}>"
|
||||
return f"<{self.__class__.__name__} {self.id} {self.titre_annee()}>"
|
||||
|
||||
def sort_key(self) -> tuple:
|
||||
"""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)
|
||||
|
||||
def to_dict(self, convert_objects=False) -> dict:
|
||||
"""dict (compatible ScoDoc7).
|
||||
@ -173,7 +185,7 @@ class FormSemestre(db.Model):
|
||||
d["responsables"] = [u.id for u in self.responsables]
|
||||
d["titre_formation"] = self.titre_formation()
|
||||
if convert_objects:
|
||||
d["parcours"] = [p.to_dict() for p in self.parcours]
|
||||
d["parcours"] = [p.to_dict() for p in self.get_parcours_apc()]
|
||||
d["departement"] = self.departement.to_dict()
|
||||
d["formation"] = self.formation.to_dict()
|
||||
d["etape_apo"] = self.etapes_apo_str()
|
||||
@ -200,9 +212,10 @@ class FormSemestre(db.Model):
|
||||
d["etape_apo"] = self.etapes_apo_str()
|
||||
d["formsemestre_id"] = self.id
|
||||
d["formation"] = self.formation.to_dict()
|
||||
d["parcours"] = [p.to_dict() for p in self.parcours]
|
||||
d["parcours"] = [p.to_dict() for p in self.get_parcours_apc()]
|
||||
d["responsables"] = [u.id for u in self.responsables]
|
||||
d["titre_court"] = self.formation.acronyme
|
||||
d["titre_formation"] = self.titre_formation()
|
||||
d["titre_num"] = self.titre_num()
|
||||
d["session_id"] = self.session_id()
|
||||
return d
|
||||
@ -222,7 +235,8 @@ class FormSemestre(db.Model):
|
||||
d["mois_debut_ord"] = self.date_debut.month
|
||||
d["mois_fin_ord"] = self.date_fin.month
|
||||
# La période: considère comme "S1" (ou S3) les débuts en aout-sept-octobre
|
||||
# devrait sans doute pouvoir etre changé...
|
||||
# devrait sans doute pouvoir etre changé... XXX PIVOT
|
||||
d["periode"] = self.periode()
|
||||
if self.date_debut.month >= 8 and self.date_debut.month <= 10:
|
||||
d["periode"] = 1 # typiquement, début en septembre: S1, S3...
|
||||
else:
|
||||
@ -241,17 +255,36 @@ class FormSemestre(db.Model):
|
||||
d["etapes_apo_str"] = self.etapes_apo_str()
|
||||
return d
|
||||
|
||||
def get_parcours_apc(self) -> list[ApcParcours]:
|
||||
"""Liste des parcours proposés par ce semestre.
|
||||
Si aucun n'est coché et qu'il y a un référentiel, tous ceux du référentiel.
|
||||
"""
|
||||
r = self.parcours or (
|
||||
self.formation.referentiel_competence
|
||||
and self.formation.referentiel_competence.parcours
|
||||
)
|
||||
return r or []
|
||||
|
||||
def query_ues(self, with_sport=False) -> flask_sqlalchemy.BaseQuery:
|
||||
"""UE des modules de ce semestre, triées par numéro.
|
||||
- Formations classiques: les UEs auxquelles appartiennent
|
||||
les modules mis en place dans ce semestre.
|
||||
- Formations APC / BUT: les UEs de la formation qui ont
|
||||
le même numéro de semestre que ce formsemestre.
|
||||
- Formations APC / BUT: les UEs de la formation qui
|
||||
- ont le même numéro de semestre que ce formsemestre
|
||||
- sont associées à l'un des parcours de ce formsemestre (ou à aucun)
|
||||
|
||||
"""
|
||||
if self.formation.get_parcours().APC_SAE:
|
||||
sem_ues = UniteEns.query.filter_by(
|
||||
formation=self.formation, semestre_idx=self.semestre_id
|
||||
)
|
||||
if self.parcours:
|
||||
# Prend toutes les UE de l'un des parcours du sem., ou déclarées sans parcours
|
||||
sem_ues = sem_ues.filter(
|
||||
(UniteEns.parcour == None)
|
||||
| (UniteEns.parcour_id.in_([p.id for p in self.parcours]))
|
||||
)
|
||||
# si le sem. ne coche aucun parcours, prend toutes les UE
|
||||
else:
|
||||
sem_ues = db.session.query(UniteEns).filter(
|
||||
ModuleImpl.formsemestre_id == self.id,
|
||||
@ -263,8 +296,11 @@ class FormSemestre(db.Model):
|
||||
return sem_ues.order_by(UniteEns.numero)
|
||||
|
||||
def query_ues_parcours_etud(self, etudid: int) -> flask_sqlalchemy.BaseQuery:
|
||||
"""UE que suit l'étudiant dans ce semestre BUT
|
||||
"""XXX inutilisé à part pour un test unitaire => supprimer ?
|
||||
UEs que suit l'étudiant dans ce semestre BUT
|
||||
en fonction du parcours dans lequel il est inscrit.
|
||||
Si l'étudiant n'est inscrit à aucun parcours,
|
||||
renvoie uniquement les UEs de tronc commun (sans parcours).
|
||||
|
||||
Si voulez les UE d'un parcours, il est plus efficace de passer par
|
||||
`formation.query_ues_parcour(parcour)`.
|
||||
@ -275,7 +311,13 @@ class FormSemestre(db.Model):
|
||||
UniteEns.niveau_competence_id == ApcNiveau.id,
|
||||
ApcParcoursNiveauCompetence.competence_id == ApcNiveau.competence_id,
|
||||
ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id,
|
||||
or_(
|
||||
ApcAnneeParcours.parcours_id == FormSemestreInscription.parcour_id,
|
||||
and_(
|
||||
FormSemestreInscription.parcour_id.is_(None),
|
||||
UniteEns.parcour_id.is_(None),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@cached_property
|
||||
@ -288,7 +330,7 @@ class FormSemestre(db.Model):
|
||||
if self.formation.is_apc():
|
||||
modimpls.sort(
|
||||
key=lambda m: (
|
||||
m.module.module_type or 0,
|
||||
m.module.module_type or 0, # ressources (2) avant SAEs (3)
|
||||
m.module.numero or 0,
|
||||
m.module.code or 0,
|
||||
)
|
||||
@ -327,7 +369,7 @@ class FormSemestre(db.Model):
|
||||
return [ModuleImpl.query.get(modimpl_id) for modimpl_id in cursor]
|
||||
|
||||
def can_be_edited_by(self, user):
|
||||
"""Vrai si user peut modifier ce semestre"""
|
||||
"""Vrai si user peut modifier ce semestre (est chef ou l'un des responsables)"""
|
||||
if not user.has_permission(Permission.ScoImplement): # pas chef
|
||||
if not self.resp_can_edit or user.id not in [
|
||||
resp.id for resp in self.responsables
|
||||
@ -341,7 +383,7 @@ class FormSemestre(db.Model):
|
||||
(les dates de début et fin sont incluses)
|
||||
"""
|
||||
today = datetime.date.today()
|
||||
return (self.date_debut <= today) and (today <= self.date_fin)
|
||||
return self.date_debut <= today <= self.date_fin
|
||||
|
||||
def contient_periode(self, date_debut, date_fin) -> bool:
|
||||
"""Vrai si l'intervalle [date_debut, date_fin] est
|
||||
@ -354,29 +396,99 @@ class FormSemestre(db.Model):
|
||||
"""Test si sem est entièrement sur la même année scolaire.
|
||||
(ce n'est pas obligatoire mais si ce n'est pas le
|
||||
cas les exports Apogée risquent de mal fonctionner)
|
||||
Pivot au 1er août.
|
||||
Pivot au 1er août par défaut.
|
||||
"""
|
||||
if self.date_debut > self.date_fin:
|
||||
flash(f"Dates début/fin inversées pour le semestre {self.titre_annee()}")
|
||||
log(f"Warning: semestre {self.id} begins after ending !")
|
||||
annee_debut = self.date_debut.year
|
||||
if self.date_debut.month < 8: # août
|
||||
# considere que debut sur l'anne scolaire precedente
|
||||
month_debut_annee = ScoDocSiteConfig.get_month_debut_annee_scolaire()
|
||||
if self.date_debut.month < month_debut_annee:
|
||||
# début sur l'année scolaire précédente (juillet inclus par défaut)
|
||||
annee_debut -= 1
|
||||
annee_fin = self.date_fin.year
|
||||
if self.date_fin.month < 9:
|
||||
# 9 (sept) pour autoriser un début en sept et une fin en aout
|
||||
if self.date_fin.month < (month_debut_annee + 1):
|
||||
# 9 (sept) pour autoriser un début en sept et une fin en août
|
||||
annee_fin -= 1
|
||||
return annee_debut == annee_fin
|
||||
|
||||
def est_decale(self):
|
||||
"""Vrai si semestre "décalé"
|
||||
c'est à dire semestres impairs commençant entre janvier et juin
|
||||
et les pairs entre juillet et decembre
|
||||
c'est à dire semestres impairs commençant (par défaut)
|
||||
entre janvier et juin et les pairs entre juillet et décembre.
|
||||
"""
|
||||
if self.semestre_id <= 0:
|
||||
return False # formations sans semestres
|
||||
return (self.semestre_id % 2 and self.date_debut.month <= 6) or (
|
||||
not self.semestre_id % 2 and self.date_debut.month > 6
|
||||
return (
|
||||
# impair
|
||||
(
|
||||
self.semestre_id % 2
|
||||
and self.date_debut.month < scu.MONTH_DEBUT_ANNEE_SCOLAIRE
|
||||
)
|
||||
or
|
||||
# pair
|
||||
(
|
||||
(not self.semestre_id % 2)
|
||||
and self.date_debut.month >= scu.MONTH_DEBUT_ANNEE_SCOLAIRE
|
||||
)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def comp_periode(
|
||||
cls,
|
||||
date_debut: datetime,
|
||||
mois_pivot_annee=scu.MONTH_DEBUT_ANNEE_SCOLAIRE,
|
||||
mois_pivot_periode=scu.MONTH_DEBUT_PERIODE2,
|
||||
jour_pivot_annee=1,
|
||||
jour_pivot_periode=1,
|
||||
):
|
||||
"""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
|
||||
période = 1 (première période de l'année scolaire, souvent automne)
|
||||
ou 2 (deuxième période de l'année scolaire, souvent printemps)
|
||||
Les quatre derniers paramètres forment les dates pivots pour l'année
|
||||
(1er août par défaut) et pour la période (1er décembre par défaut).
|
||||
|
||||
Les calculs se font à partir de la date de début indiquée.
|
||||
Exemples dans tests/unit/test_periode
|
||||
|
||||
Implémentation:
|
||||
Cas à considérer pour le calcul de la période
|
||||
|
||||
pa < pp -----------------|-------------------|---------------->
|
||||
(A-1, P:2) pa (A, P:1) pp (A, P:2)
|
||||
pp < pa -----------------|-------------------|---------------->
|
||||
(A-1, P:1) pp (A-1, P:2) pa (A, P:1)
|
||||
"""
|
||||
pivot_annee = 100 * mois_pivot_annee + jour_pivot_annee
|
||||
pivot_periode = 100 * mois_pivot_periode + jour_pivot_periode
|
||||
pivot_sem = 100 * date_debut.month + date_debut.day
|
||||
if pivot_sem < pivot_annee:
|
||||
annee = date_debut.year - 1
|
||||
else:
|
||||
annee = date_debut.year
|
||||
if pivot_annee < pivot_periode:
|
||||
if pivot_sem < pivot_annee or pivot_sem >= pivot_periode:
|
||||
periode = 2
|
||||
else:
|
||||
periode = 1
|
||||
else:
|
||||
if pivot_sem < pivot_periode or pivot_sem >= pivot_annee:
|
||||
periode = 1
|
||||
else:
|
||||
periode = 2
|
||||
return annee, periode
|
||||
|
||||
def periode(self) -> int:
|
||||
"""La période:
|
||||
* 1 : première période: automne à Paris
|
||||
* 2 : deuxième période, printemps à Paris
|
||||
"""
|
||||
return FormSemestre.comp_periode(
|
||||
self.date_debut,
|
||||
mois_pivot_annee=ScoDocSiteConfig.get_month_debut_annee_scolaire(),
|
||||
mois_pivot_periode=ScoDocSiteConfig.get_month_debut_periode2(),
|
||||
)
|
||||
|
||||
def etapes_apo_vdi(self) -> list[ApoEtapeVDI]:
|
||||
@ -429,7 +541,7 @@ class FormSemestre(db.Model):
|
||||
|
||||
def annee_scolaire(self) -> int:
|
||||
"""L'année de début de l'année scolaire.
|
||||
Par exemple, 2022 si le semestre va de septebre 2022 à février 2023."""
|
||||
Par exemple, 2022 si le semestre va de septembre 2022 à février 2023."""
|
||||
return scu.annee_scolaire_debut(self.date_debut.year, self.date_debut.month)
|
||||
|
||||
def annee_scolaire_str(self):
|
||||
@ -479,7 +591,9 @@ class FormSemestre(db.Model):
|
||||
)
|
||||
|
||||
def titre_annee(self) -> str:
|
||||
""" """
|
||||
"""Le titre avec l'année
|
||||
'DUT Réseaux et Télécommunications semestre 3 FAP 2020-2021'
|
||||
"""
|
||||
titre_annee = (
|
||||
f"{self.titre_num()} {self.modalite or ''} {self.date_debut.year}"
|
||||
)
|
||||
@ -585,14 +699,43 @@ class FormSemestre(db.Model):
|
||||
db.session.add(partition)
|
||||
db.session.flush() # pour avoir un id
|
||||
flash("Partition Parcours créée.")
|
||||
elif partition.groups_editable:
|
||||
# Il ne faut jamais laisser éditer cette partition de parcours
|
||||
partition.groups_editable = False
|
||||
db.session.add(partition)
|
||||
|
||||
for parcour in self.parcours:
|
||||
for parcour in self.get_parcours_apc():
|
||||
if parcour.code:
|
||||
group = GroupDescr.query.filter_by(
|
||||
partition_id=partition.id, group_name=parcour.code
|
||||
).first()
|
||||
if not group:
|
||||
partition.groups.append(GroupDescr(group_name=parcour.code))
|
||||
db.session.flush()
|
||||
# S'il reste des groupes de parcours qui ne sont plus dans le semestre
|
||||
# - s'ils n'ont pas d'inscrits, supprime-les.
|
||||
# - s'ils ont des inscrits: avertissement
|
||||
for group in GroupDescr.query.filter_by(partition_id=partition.id):
|
||||
if group.group_name not in (p.code for p in self.get_parcours_apc()):
|
||||
if (
|
||||
len(
|
||||
[
|
||||
inscr
|
||||
for inscr in self.inscriptions
|
||||
if (inscr.parcour is not None)
|
||||
and inscr.parcour.code == group.group_name
|
||||
]
|
||||
)
|
||||
== 0
|
||||
):
|
||||
flash(f"Suppression du groupe de parcours vide {group.group_name}")
|
||||
db.session.delete(group)
|
||||
else:
|
||||
flash(
|
||||
f"""Attention: groupe de parcours {group.group_name} non vide:
|
||||
réaffectez ses étudiants dans des parcours du semestre"""
|
||||
)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
def update_inscriptions_parcours_from_groups(self) -> None:
|
||||
@ -653,7 +796,7 @@ class FormSemestre(db.Model):
|
||||
|
||||
def etud_validations_description_html(self, etudid: int) -> str:
|
||||
"""Description textuelle des validations de jury de cet étudiant dans ce semestre"""
|
||||
from app.models.but_validations import ApcValidationRCUE, ApcValidationAnnee
|
||||
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
||||
|
||||
vals_sem = ScolarFormSemestreValidation.query.filter_by(
|
||||
etudid=etudid, formsemestre_id=self.id, ue_id=None
|
||||
@ -914,8 +1057,8 @@ class NotesSemSet(db.Model):
|
||||
|
||||
title = db.Column(db.Text)
|
||||
annee_scolaire = db.Column(db.Integer, nullable=True, default=None)
|
||||
# periode: 0 (année), 1 (Simpair), 2 (Spair)
|
||||
sem_id = db.Column(db.Integer, nullable=True, default=None)
|
||||
sem_id = db.Column(db.Integer, nullable=False, default=0)
|
||||
"période: 0 (année), 1 (Simpair), 2 (Spair)"
|
||||
|
||||
|
||||
# Association: many to many
|
||||
|
@ -1,7 +1,7 @@
|
||||
# -*- coding: UTF-8 -*
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -87,6 +87,7 @@ class Partition(db.Model):
|
||||
def to_dict(self, with_groups=False) -> dict:
|
||||
"""as a dict, with or without groups"""
|
||||
d = dict(self.__dict__)
|
||||
d["partition_id"] = self.id
|
||||
d.pop("_sa_instance_state", None)
|
||||
d.pop("formsemestre", None)
|
||||
|
||||
|
@ -20,14 +20,12 @@ class ModuleImpl(db.Model):
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
moduleimpl_id = db.synonym("id")
|
||||
module_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_modules.id"),
|
||||
)
|
||||
module_id = db.Column(db.Integer, db.ForeignKey("notes_modules.id"), nullable=False)
|
||||
formsemestre_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_formsemestre.id"),
|
||||
index=True,
|
||||
nullable=False,
|
||||
)
|
||||
responsable_id = db.Column("responsable_id", db.Integer, db.ForeignKey("user.id"))
|
||||
# formule de calcul moyenne:
|
||||
@ -62,7 +60,7 @@ class ModuleImpl(db.Model):
|
||||
"""Invalide poids cachés"""
|
||||
df_cache.EvaluationsPoidsCache.delete(self.id)
|
||||
|
||||
def check_apc_conformity(self) -> bool:
|
||||
def check_apc_conformity(self, res: "ResultatsSemestreBUT") -> bool:
|
||||
"""true si les poids des évaluations du module permettent de satisfaire
|
||||
les coefficients du PN.
|
||||
"""
|
||||
@ -76,7 +74,7 @@ class ModuleImpl(db.Model):
|
||||
return moy_mod.moduleimpl_is_conforme(
|
||||
self,
|
||||
self.get_evaluations_poids(),
|
||||
self.module.formation.get_module_coefs(self.module.semestre_id),
|
||||
res.modimpl_coefs_df,
|
||||
)
|
||||
|
||||
def to_dict(self, convert_objects=False, with_module=True):
|
||||
@ -101,6 +99,22 @@ class ModuleImpl(db.Model):
|
||||
d.pop("module", None)
|
||||
return d
|
||||
|
||||
def est_inscrit(self, etud: Identite) -> bool:
|
||||
"""
|
||||
Vérifie si l'étudiant est bien inscrit au moduleimpl
|
||||
|
||||
Retourne Vrai si c'est le cas, faux sinon
|
||||
"""
|
||||
|
||||
is_module: int = (
|
||||
ModuleImplInscription.query.filter_by(
|
||||
etudid=etud.id, moduleimpl_id=self.id
|
||||
).count()
|
||||
> 0
|
||||
)
|
||||
|
||||
return is_module
|
||||
|
||||
|
||||
# Enseignants (chargés de TD ou TP) d'un moduleimpl
|
||||
notes_modules_enseignants = db.Table(
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
from app import db
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models.but_refcomp import 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.sco_codes_parcours import UE_SPORT
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
@ -37,7 +37,9 @@ class Module(db.Model):
|
||||
# Type: ModuleType.STANDARD, MALUS, RESSOURCE, SAE (enum)
|
||||
module_type = db.Column(db.Integer, nullable=False, default=0, server_default="0")
|
||||
# Relations:
|
||||
modimpls = db.relationship("ModuleImpl", backref="module", lazy="dynamic")
|
||||
modimpls = db.relationship(
|
||||
"ModuleImpl", backref="module", lazy="dynamic", cascade="all, delete-orphan"
|
||||
)
|
||||
ues_apc = db.relationship("UniteEns", secondary="module_ue_coef", viewonly=True)
|
||||
tags = db.relationship(
|
||||
"NotesTag",
|
||||
@ -66,7 +68,39 @@ class Module(db.Model):
|
||||
super(Module, self).__init__(**kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Module{ModuleType(self.module_type or ModuleType.STANDARD).name} id={self.id} code={self.code!r} semestre_id={self.semestre_id}>"
|
||||
return f"""<Module{ModuleType(self.module_type or ModuleType.STANDARD).name
|
||||
} id={self.id} code={self.code!r} semestre_id={self.semestre_id}>"""
|
||||
|
||||
def clone(self):
|
||||
"""Create a new copy of this module."""
|
||||
mod = Module(
|
||||
titre=self.titre,
|
||||
abbrev=self.abbrev,
|
||||
code=self.code + "-copie",
|
||||
heures_cours=self.heures_cours,
|
||||
heures_td=self.heures_td,
|
||||
heures_tp=self.heures_tp,
|
||||
coefficient=self.coefficient,
|
||||
ects=self.ects,
|
||||
ue_id=self.ue_id,
|
||||
matiere_id=self.matiere_id,
|
||||
formation_id=self.formation_id,
|
||||
semestre_id=self.semestre_id,
|
||||
numero=self.numero, # il est conseillé de renuméroter
|
||||
code_apogee="", # volontairement vide pour éviter les erreurs
|
||||
module_type=self.module_type,
|
||||
)
|
||||
|
||||
# Les tags:
|
||||
for tag in self.tags:
|
||||
mod.tags.append(tag)
|
||||
# Les parcours
|
||||
for parcour in self.parcours:
|
||||
mod.parcours.append(parcour)
|
||||
# Les AC
|
||||
for app_critique in self.app_critiques:
|
||||
mod.app_critiques.append(app_critique)
|
||||
return mod
|
||||
|
||||
def to_dict(self, convert_objects=False, with_matiere=False, with_ue=False) -> dict:
|
||||
"""If convert_objects, convert all attributes to native types
|
||||
@ -188,25 +222,31 @@ class Module(db.Model):
|
||||
# à redéfinir les relationships...
|
||||
return sorted(self.ue_coefs, key=lambda x: x.ue.numero)
|
||||
|
||||
def ue_coefs_list(self, include_zeros=True):
|
||||
def ue_coefs_list(
|
||||
self, include_zeros=True, ues: list["UniteEns"] = None
|
||||
) -> list[tuple["UniteEns", float]]:
|
||||
"""Liste des coefs vers les UE (pour les modules APC).
|
||||
Si include_zeros, liste aussi les UE sans coef (donc nul) de ce semestre,
|
||||
Si ues est spécifié, restreint aux UE indiquées.
|
||||
Sinon si include_zeros, liste aussi les UE sans coef (donc nul) de ce semestre,
|
||||
sauf UE bonus sport.
|
||||
Result: List of tuples [ (ue, coef) ]
|
||||
"""
|
||||
if not self.is_apc():
|
||||
return []
|
||||
if include_zeros:
|
||||
if include_zeros and ues is None:
|
||||
# Toutes les UE du même semestre:
|
||||
ues_semestre = (
|
||||
ues = (
|
||||
self.formation.ues.filter_by(semestre_idx=self.ue.semestre_idx)
|
||||
.filter(UniteEns.type != UE_SPORT)
|
||||
.order_by(UniteEns.numero)
|
||||
.all()
|
||||
)
|
||||
if not ues:
|
||||
return []
|
||||
if ues:
|
||||
coefs_dict = self.get_ue_coef_dict()
|
||||
coefs_list = []
|
||||
for ue in ues_semestre:
|
||||
for ue in ues:
|
||||
coefs_list.append((ue, coefs_dict.get(ue.id, 0.0)))
|
||||
return coefs_list
|
||||
# Liste seulement les coefs définis:
|
||||
@ -218,6 +258,19 @@ class Module(db.Model):
|
||||
return {x.strip() for x in self.code_apogee.split(",") if x}
|
||||
return set()
|
||||
|
||||
def get_parcours(self) -> list[ApcParcours]:
|
||||
"""Les parcours utilisant ce module.
|
||||
Si tous les parcours, liste vide (!).
|
||||
"""
|
||||
ref_comp = self.formation.referentiel_competence
|
||||
if not ref_comp:
|
||||
return []
|
||||
tous_parcours_ids = {p.id for p in ref_comp.parcours}
|
||||
parcours_ids = {p.id for p in self.parcours}
|
||||
if tous_parcours_ids == parcours_ids:
|
||||
return []
|
||||
return self.parcours
|
||||
|
||||
|
||||
class ModuleUECoef(db.Model):
|
||||
"""Coefficients des modules vers les UE (APC, BUT)
|
||||
|
@ -1,9 +1,14 @@
|
||||
"""ScoDoc 9 models : Unités d'Enseignement (UE)
|
||||
"""
|
||||
|
||||
from app import db
|
||||
import pandas as pd
|
||||
|
||||
from app import db, log
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models.but_refcomp import ApcNiveau, ApcParcours
|
||||
from app.models.modules import Module
|
||||
from app.scodoc.sco_exceptions import ScoFormationConflict
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
@ -49,9 +54,19 @@ class UniteEns(db.Model):
|
||||
niveau_competence_id = db.Column(db.Integer, db.ForeignKey("apc_niveau.id"))
|
||||
niveau_competence = db.relationship("ApcNiveau", back_populates="ues")
|
||||
|
||||
# Une ue appartient soit à tous les parcours (tronc commun), soit à un seul:
|
||||
parcour_id = db.Column(db.Integer, db.ForeignKey("apc_parcours.id"), index=True)
|
||||
parcour = db.relationship("ApcParcours", back_populates="ues")
|
||||
|
||||
# relations
|
||||
matieres = db.relationship("Matiere", lazy="dynamic", backref="ue")
|
||||
modules = db.relationship("Module", lazy="dynamic", backref="ue")
|
||||
dispense_ues = db.relationship(
|
||||
"DispenseUE",
|
||||
back_populates="ue",
|
||||
cascade="all, delete",
|
||||
passive_deletes=True,
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"""<{self.__class__.__name__}(id={self.id}, formation_id={
|
||||
@ -59,6 +74,28 @@ class UniteEns(db.Model):
|
||||
self.semestre_idx} {
|
||||
'EXTERNE' if self.is_external else ''})>"""
|
||||
|
||||
def clone(self):
|
||||
"""Create a new copy of this ue.
|
||||
Ne copie pas le code, ni le code Apogée, ni les liens au réf. de comp.
|
||||
(parcours et niveau).
|
||||
"""
|
||||
ue = UniteEns(
|
||||
formation_id=self.formation_id,
|
||||
acronyme=self.acronyme + "-copie",
|
||||
numero=self.numero,
|
||||
titre=self.titre,
|
||||
semestre_idx=self.semestre_idx,
|
||||
type=self.type,
|
||||
ue_code="", # ne duplique pas le code
|
||||
ects=self.ects,
|
||||
is_external=self.is_external,
|
||||
code_apogee="", # ne copie pas les codes Apo
|
||||
coefficient=self.coefficient,
|
||||
coef_rcue=self.coef_rcue,
|
||||
color=self.color,
|
||||
)
|
||||
return ue
|
||||
|
||||
def to_dict(self, convert_objects=False, with_module_ue_coefs=True):
|
||||
"""as a dict, with the same conversions as in ScoDoc7
|
||||
(except ECTS: keep None)
|
||||
@ -74,6 +111,7 @@ class UniteEns(db.Model):
|
||||
e["ects"] = e["ects"]
|
||||
e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0
|
||||
e["code_apogee"] = e["code_apogee"] or "" # pas de None
|
||||
e["parcour"] = self.parcour.to_dict() if self.parcour else None
|
||||
if with_module_ue_coefs:
|
||||
if convert_objects:
|
||||
e["module_ue_coefs"] = [
|
||||
@ -83,6 +121,12 @@ class UniteEns(db.Model):
|
||||
e.pop("module_ue_coefs", None)
|
||||
return e
|
||||
|
||||
def annee(self) -> int:
|
||||
"""L'année dans la formation (commence à 1).
|
||||
En APC seulement, en classic renvoie toujours 1.
|
||||
"""
|
||||
return 1 if self.semestre_idx is None else (self.semestre_idx - 1) // 2 + 1
|
||||
|
||||
def is_locked(self):
|
||||
"""True if UE should not be modified
|
||||
(contains modules used in a locked formsemestre)
|
||||
@ -135,3 +179,137 @@ class UniteEns(db.Model):
|
||||
if self.code_apogee:
|
||||
return {x.strip() for x in self.code_apogee.split(",") if x}
|
||||
return set()
|
||||
|
||||
def _check_apc_conflict(self, new_niveau_id: int, new_parcour_id: int):
|
||||
"raises ScoFormationConflict si (niveau, parcours) pas unique dans ce semestre"
|
||||
# Les UE du même semestre que nous:
|
||||
ues_sem = self.formation.ues.filter_by(semestre_idx=self.semestre_idx)
|
||||
if (new_niveau_id, new_parcour_id) in (
|
||||
(oue.niveau_competence_id, oue.parcour_id)
|
||||
for oue in ues_sem
|
||||
if oue.id != self.id
|
||||
):
|
||||
log(
|
||||
f"set_ue_niveau_competence: {self}: ({new_niveau_id}, {new_parcour_id}) déjà associé"
|
||||
)
|
||||
raise ScoFormationConflict()
|
||||
|
||||
def set_niveau_competence(self, niveau: ApcNiveau):
|
||||
"""Associe cette UE au niveau de compétence indiqué.
|
||||
Le niveau doit être dans le parcours de l'UE, s'il y en a un.
|
||||
Assure que ce soit la seule dans son parcours.
|
||||
Sinon, raises ScoFormationConflict.
|
||||
|
||||
Si niveau est None, désassocie.
|
||||
"""
|
||||
if niveau is not None:
|
||||
self._check_apc_conflict(niveau.id, self.parcour_id)
|
||||
# Le niveau est-il dans le parcours ? Sinon, erreur
|
||||
if self.parcour and niveau.id not in (
|
||||
n.id
|
||||
for n in niveau.niveaux_annee_de_parcours(
|
||||
self.parcour, self.annee(), self.formation.referentiel_competence
|
||||
)
|
||||
):
|
||||
log(
|
||||
f"set_niveau_competence: niveau {niveau} hors parcours {self.parcour}"
|
||||
)
|
||||
return
|
||||
|
||||
self.niveau_competence = niveau
|
||||
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
# Invalidation du cache
|
||||
self.formation.invalidate_cached_sems()
|
||||
log(f"ue.set_niveau_competence( {self}, {niveau} )")
|
||||
|
||||
def set_parcour(self, parcour: ApcParcours):
|
||||
"""Associe cette UE au parcours indiqué.
|
||||
Assure que ce soit la seule dans son parcours.
|
||||
Sinon, raises ScoFormationConflict.
|
||||
|
||||
Si niveau est None, désassocie.
|
||||
"""
|
||||
if (parcour is not None) and self.niveau_competence is not None:
|
||||
self._check_apc_conflict(self.niveau_competence.id, parcour.id)
|
||||
self.parcour = parcour
|
||||
# Le niveau est-il dans ce parcours ? Sinon, l'enlève
|
||||
if (
|
||||
parcour
|
||||
and self.niveau_competence
|
||||
and self.niveau_competence.id
|
||||
not in (
|
||||
n.id
|
||||
for n in self.niveau_competence.niveaux_annee_de_parcours(
|
||||
parcour, self.annee(), self.formation.referentiel_competence
|
||||
)
|
||||
)
|
||||
):
|
||||
self.niveau_competence = None
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
# Invalidation du cache
|
||||
self.formation.invalidate_cached_sems()
|
||||
log(f"ue.set_parcour( {self}, {parcour} )")
|
||||
|
||||
|
||||
class DispenseUE(db.Model):
|
||||
"""Dispense d'UE
|
||||
Utilisé en APC (BUT) pour indiquer les étudiants redoublants avec une UE capitalisée
|
||||
qu'ils ne refont pas.
|
||||
La dispense d'UE n'est PAS une validation:
|
||||
- elle n'est pas affectée par les décisions de jury (pas effacée)
|
||||
- elle est associée à un formsemestre
|
||||
- elle ne permet pas la délivrance d'ECTS ou du diplôme.
|
||||
|
||||
On utilise cette dispense et non une "inscription" par souci d'efficacité:
|
||||
en général, la grande majorité des étudiants suivront toutes les UEs de leur parcours,
|
||||
la dispense étant une exception.
|
||||
"""
|
||||
|
||||
__table_args__ = (db.UniqueConstraint("formsemestre_id", "ue_id", "etudid"),)
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
formsemestre_id = formsemestre_id = db.Column(
|
||||
db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, nullable=True
|
||||
)
|
||||
ue_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey(UniteEns.id, ondelete="CASCADE"),
|
||||
index=True,
|
||||
nullable=False,
|
||||
)
|
||||
ue = db.relationship("UniteEns", back_populates="dispense_ues")
|
||||
etudid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||
index=True,
|
||||
nullable=False,
|
||||
)
|
||||
etud = db.relationship("Identite", back_populates="dispense_ues")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"""<{self.__class__.__name__} {self.id} etud={
|
||||
repr(self.etud)} ue={repr(self.ue)}>"""
|
||||
|
||||
@classmethod
|
||||
def load_formsemestre_dispense_ues_set(
|
||||
cls, formsemestre: "FormSemestre", etudids: pd.Index, ues: list[UniteEns]
|
||||
) -> set[tuple[int, int]]:
|
||||
"""Construit l'ensemble des
|
||||
etudids = modimpl_inscr_df.index, # les etudids
|
||||
ue_ids : modimpl_coefs_df.index, # les UE du formsemestre sans les UE bonus sport
|
||||
|
||||
Résultat: set de (etudid, ue_id).
|
||||
"""
|
||||
# Prend toutes les dispenses obtenues par des étudiants de ce formsemestre,
|
||||
# puis filtre sur inscrits et ues
|
||||
ue_ids = {ue.id for ue in ues}
|
||||
dispense_ues = {
|
||||
(dispense_ue.etudid, dispense_ue.ue_id)
|
||||
for dispense_ue in DispenseUE.query.filter_by(
|
||||
formsemestre_id=formsemestre.id
|
||||
)
|
||||
if dispense_ue.etudid in etudids and dispense_ue.ue_id in ue_ids
|
||||
}
|
||||
return dispense_ues
|
||||
|
@ -4,6 +4,7 @@
|
||||
"""
|
||||
|
||||
from app import db
|
||||
from app import log
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models import CODE_STR_LEN
|
||||
from app.models.events import Scolog
|
||||
@ -58,7 +59,7 @@ class ScolarFormSemestreValidation(db.Model):
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}({self.formsemestre_id}, {self.etudid}, code={self.code}, ue={self.ue}, moy_ue={self.moy_ue})"
|
||||
return f"{self.__class__.__name__}(sem={self.formsemestre_id}, etuid={self.etudid}, code={self.code}, ue={self.ue}, moy_ue={self.moy_ue})"
|
||||
|
||||
def __str__(self):
|
||||
if self.ue_id:
|
||||
@ -93,6 +94,10 @@ class ScolarAutorisationInscription(db.Model):
|
||||
db.ForeignKey("notes_formsemestre.id"),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"""{self.__class__.__name__}(id={self.id}, etudid={
|
||||
self.etudid}, semestre_id={self.semestre_id})"""
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"as a dict"
|
||||
d = dict(self.__dict__)
|
||||
@ -116,7 +121,10 @@ class ScolarAutorisationInscription(db.Model):
|
||||
semestre_id=semestre_id,
|
||||
)
|
||||
db.session.add(autorisation)
|
||||
Scolog.logdb("autorise_etud", etudid=etudid, msg=f"passage vers S{semestre_id}")
|
||||
Scolog.logdb(
|
||||
"autorise_etud", etudid=etudid, msg=f"Passage vers S{semestre_id}: autorisé"
|
||||
)
|
||||
log(f"ScolarAutorisationInscription: recording {autorisation}")
|
||||
|
||||
@classmethod
|
||||
def delete_autorisation_etud(
|
||||
@ -130,10 +138,11 @@ class ScolarAutorisationInscription(db.Model):
|
||||
)
|
||||
for autorisation in autorisations:
|
||||
db.session.delete(autorisation)
|
||||
log(f"ScolarAutorisationInscription: deleting {autorisation}")
|
||||
Scolog.logdb(
|
||||
"autorise_etud",
|
||||
etudid=etudid,
|
||||
msg=f"annule passage vers S{autorisation.semestre_id}",
|
||||
msg=f"Passage vers S{autorisation.semestre_id}: effacé",
|
||||
)
|
||||
db.session.flush()
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 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 - 2022 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
|
||||
@ -459,8 +459,7 @@ class JuryPE(object):
|
||||
etud = self.get_cache_etudInfo_d_un_etudiant(etudid)
|
||||
(_, parcours) = sco_report.get_codeparcoursetud(etud)
|
||||
if (
|
||||
len(set(sco_codes_parcours.CODES_SEM_REO.keys()) & set(parcours.values()))
|
||||
> 0
|
||||
len(sco_codes_parcours.CODES_SEM_REO & set(parcours.values())) > 0
|
||||
): # Eliminé car NAR apparait dans le parcours
|
||||
reponse = True
|
||||
if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2:
|
||||
@ -563,9 +562,8 @@ class JuryPE(object):
|
||||
dec = nt.get_etud_decision_sem(
|
||||
etudid
|
||||
) # quelle est la décision du jury ?
|
||||
if dec and dec["code"] in list(
|
||||
sco_codes_parcours.CODES_SEM_VALIDES.keys()
|
||||
): # isinstance( sesMoyennes[i+1], float) and
|
||||
if dec and (dec["code"] in sco_codes_parcours.CODES_SEM_VALIDES):
|
||||
# isinstance( sesMoyennes[i+1], float) and
|
||||
# mT = sesMoyennes[i+1] # substitue la moyenne si le semestre suivant est "valide"
|
||||
leFid = sem["formsemestre_id"]
|
||||
else:
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 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 - 2022 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 - 2022 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 - 2022 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 - 2022 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
app/profiler.py
Normal file
43
app/profiler.py
Normal file
@ -0,0 +1,43 @@
|
||||
from time import time
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class Profiler:
|
||||
OUTPUT: str = "/tmp/scodoc.profiler.csv"
|
||||
|
||||
def __init__(self, tag: str) -> None:
|
||||
self.tag: str = tag
|
||||
self.start_time: time = None
|
||||
self.stop_time: time = None
|
||||
|
||||
def start(self):
|
||||
self.start_time = time()
|
||||
return self
|
||||
|
||||
def stop(self):
|
||||
self.stop_time = time()
|
||||
return self
|
||||
|
||||
def elapsed(self) -> float:
|
||||
return self.stop_time - self.start_time
|
||||
|
||||
def dates(self) -> tuple[datetime, datetime]:
|
||||
return datetime.fromtimestamp(self.start_time), datetime.fromtimestamp(
|
||||
self.stop_time
|
||||
)
|
||||
|
||||
def write(self):
|
||||
with open(Profiler.OUTPUT, "a") as file:
|
||||
dates: tuple = self.dates()
|
||||
date_str = (dates[0].isoformat(), dates[1].isoformat())
|
||||
file.write(f"\n{self.tag},{self.elapsed() : .2}")
|
||||
|
||||
@classmethod
|
||||
def write_in(cls, msg: str):
|
||||
with open(cls.OUTPUT, "a") as file:
|
||||
file.write(f"\n# {msg}")
|
||||
|
||||
@classmethod
|
||||
def clear(cls):
|
||||
with open(cls.OUTPUT, "w") as file:
|
||||
file.write("")
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 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 - 2022 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 - 2022 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
|
||||
@ -274,7 +274,7 @@ def sco_header(
|
||||
H.append("""<div id="gtrcontent">""")
|
||||
# En attendant le replacement complet de cette fonction,
|
||||
# inclusion ici des messages flask
|
||||
H.append(render_template("flashed_messages.html"))
|
||||
H.append(render_template("flashed_messages.j2"))
|
||||
#
|
||||
# Barre menu semestre:
|
||||
H.append(formsemestre_page_title(formsemestre_id))
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 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
|
||||
@ -101,7 +101,6 @@ def sidebar(etudid: int = None):
|
||||
etudid = request.form.get("etudid", None)
|
||||
|
||||
if etudid is not None:
|
||||
etudi = int(etudid)
|
||||
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
|
||||
params.update(etud)
|
||||
params["fiche_url"] = url_for(
|
||||
@ -167,6 +166,6 @@ def sidebar(etudid: int = None):
|
||||
def sidebar_dept():
|
||||
"""Partie supérieure de la marge de gauche"""
|
||||
return render_template(
|
||||
"sidebar_dept.html",
|
||||
"sidebar_dept.j2",
|
||||
prefs=sco_preferences.SemPreferences(),
|
||||
)
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 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
|
||||
@ -83,7 +83,7 @@ def histogram_notes(notes):
|
||||
return "\n".join(D)
|
||||
|
||||
|
||||
def make_menu(title, items, css_class="", alone=False):
|
||||
def make_menu(title, items, css_class="", alone=False) -> str:
|
||||
"""HTML snippet to render a simple drop down menu.
|
||||
items is a list of dicts:
|
||||
{ 'title' :
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 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 - 2022 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 - 2022 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 - 2022 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
|
||||
@ -42,6 +42,8 @@ from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
from app.scodoc import sco_preferences
|
||||
from app.models import Assiduite
|
||||
import app.scodoc.sco_assiduites as scass
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
# --- Misc tools.... ------------------
|
||||
@ -1052,6 +1054,36 @@ def get_abs_count_in_interval(etudid, date_debut_iso, date_fin_iso):
|
||||
return r
|
||||
|
||||
|
||||
def get_assiduites_count_in_interval(etudid, date_debut_iso, date_fin_iso):
|
||||
"""Les comptes d'absences de cet étudiant entre ces deux dates, incluses:
|
||||
tuple (nb abs, nb abs justifiées)
|
||||
Utilise un cache.
|
||||
"""
|
||||
key = str(etudid) + "_" + date_debut_iso + "_" + date_fin_iso
|
||||
r = sco_cache.AbsSemEtudCache.get(key)
|
||||
if not r:
|
||||
|
||||
date_debut: datetime.datetime = scu.is_iso_formated(date_debut_iso, True)
|
||||
date_fin: datetime.datetime = scu.is_iso_formated(date_debut_iso, True)
|
||||
|
||||
assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid)
|
||||
|
||||
assiduites = scass.filter_assiduites_by_date(assiduites, date_debut, sup=True)
|
||||
assiduites = scass.filter_assiduites_by_date(assiduites, date_fin, sup=False)
|
||||
|
||||
nb_abs = scass.get_count(assiduites)["demi"]
|
||||
nb_abs_just = count_abs_just(
|
||||
etudid=etudid,
|
||||
debut=date_debut_iso,
|
||||
fin=date_fin_iso,
|
||||
)
|
||||
r = (nb_abs, nb_abs_just)
|
||||
ans = sco_cache.AbsSemEtudCache.set(key, r)
|
||||
if not ans:
|
||||
log("warning: get_abs_count failed to cache")
|
||||
return r
|
||||
|
||||
|
||||
def invalidate_abs_count(etudid, sem):
|
||||
"""Invalidate (clear) cached counts"""
|
||||
date_debut = sem["date_debut_iso"]
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 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 - 2022 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 - 2022 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
|
||||
@ -685,7 +685,7 @@ def EtatAbsences():
|
||||
|
||||
</td></tr></table>
|
||||
</form>"""
|
||||
% (scu.AnneeScolaire(), datetime.datetime.now().strftime("%d/%m/%Y")),
|
||||
% (scu.annee_scolaire(), datetime.datetime.now().strftime("%d/%m/%Y")),
|
||||
html_sco_header.sco_footer(),
|
||||
]
|
||||
return "\n".join(H)
|
||||
@ -719,15 +719,27 @@ def formChoixSemestreGroupe(all=False):
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
def _convert_sco_year(year) -> int:
|
||||
try:
|
||||
year = int(year)
|
||||
if year > 1900 and year < 2999:
|
||||
return year
|
||||
except:
|
||||
raise ScoValueError("année scolaire invalide")
|
||||
|
||||
|
||||
def CalAbs(etudid, sco_year=None):
|
||||
"""Calendrier des absences d'un etudiant"""
|
||||
# crude portage from 1999 DTML
|
||||
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
|
||||
etudid = etud["etudid"]
|
||||
anneescolaire = int(scu.AnneeScolaire(sco_year))
|
||||
datedebut = str(anneescolaire) + "-08-01"
|
||||
datefin = str(anneescolaire + 1) + "-07-31"
|
||||
annee_courante = scu.AnneeScolaire()
|
||||
if sco_year:
|
||||
annee_scolaire = _convert_sco_year(sco_year)
|
||||
else:
|
||||
annee_scolaire = scu.annee_scolaire()
|
||||
datedebut = str(annee_scolaire) + "-08-01"
|
||||
datefin = str(annee_scolaire + 1) + "-07-31"
|
||||
annee_courante = scu.annee_scolaire()
|
||||
nbabs = sco_abs.count_abs(etudid=etudid, debut=datedebut, fin=datefin)
|
||||
nbabsjust = sco_abs.count_abs_just(etudid=etudid, debut=datedebut, fin=datefin)
|
||||
events = []
|
||||
@ -746,7 +758,7 @@ def CalAbs(etudid, sco_year=None):
|
||||
events.append(
|
||||
(str(a["jour"]), "X", "#8EA2C6", "", a["matin"], a["description"])
|
||||
)
|
||||
CalHTML = sco_abs.YearTable(anneescolaire, events=events, halfday=1)
|
||||
CalHTML = sco_abs.YearTable(annee_scolaire, events=events, halfday=1)
|
||||
|
||||
#
|
||||
H = [
|
||||
@ -777,12 +789,12 @@ def CalAbs(etudid, sco_year=None):
|
||||
CalHTML,
|
||||
"""<form method="GET" action="CalAbs" name="f">""",
|
||||
"""<input type="hidden" name="etudid" value="%s"/>""" % etudid,
|
||||
"""Année scolaire %s-%s""" % (anneescolaire, anneescolaire + 1),
|
||||
"""Année scolaire %s-%s""" % (annee_scolaire, annee_scolaire + 1),
|
||||
""" Changer année: <select name="sco_year" onchange="document.f.submit()">""",
|
||||
]
|
||||
for y in range(annee_courante, min(annee_courante - 6, anneescolaire - 6), -1):
|
||||
for y in range(annee_courante, min(annee_courante - 6, annee_scolaire - 6), -1):
|
||||
H.append("""<option value="%s" """ % y)
|
||||
if y == anneescolaire:
|
||||
if y == annee_scolaire:
|
||||
H.append("selected")
|
||||
H.append(""">%s</option>""" % y)
|
||||
H.append("""</select></form>""")
|
||||
@ -811,7 +823,11 @@ def ListeAbsEtud(
|
||||
"""
|
||||
# si absjust_only, table absjust seule (export xls ou pdf)
|
||||
absjust_only = scu.to_bool(absjust_only)
|
||||
datedebut = "%s-08-01" % scu.AnneeScolaire(sco_year=sco_year)
|
||||
if sco_year:
|
||||
annee_scolaire = _convert_sco_year(sco_year)
|
||||
else:
|
||||
annee_scolaire = scu.annee_scolaire()
|
||||
datedebut = f"{annee_scolaire}-{scu.MONTH_DEBUT_ANNEE_SCOLAIRE+1}-01"
|
||||
etudid = etudid or False
|
||||
etuds = sco_etud.get_etud_info(etudid=etudid, code_nip=code_nip, filled=True)
|
||||
if not etuds:
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 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,6 +43,7 @@ Pour chaque étudiant commun:
|
||||
comparer les résultats
|
||||
|
||||
"""
|
||||
from flask import g, url_for
|
||||
|
||||
from app import log
|
||||
from app.scodoc import sco_apogee_csv
|
||||
@ -72,11 +73,11 @@ def apo_compare_csv_form():
|
||||
"""
|
||||
<div class="apo_compare_csv_form_but">
|
||||
Fichier Apogée A:
|
||||
<input type="file" size="30" name="A_file"/>
|
||||
<input type="file" size="30" name="file_a"/>
|
||||
</div>
|
||||
<div class="apo_compare_csv_form_but">
|
||||
Fichier Apogée B:
|
||||
<input type="file" size="30" name="B_file"/>
|
||||
<input type="file" size="30" name="file_b"/>
|
||||
</div>
|
||||
<input type="checkbox" name="autodetect" checked/>autodétecter encodage</input>
|
||||
<div class="apo_compare_csv_form_submit">
|
||||
@ -88,17 +89,36 @@ def apo_compare_csv_form():
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
def apo_compare_csv(A_file, B_file, autodetect=True):
|
||||
def apo_compare_csv(file_a, file_b, autodetect=True):
|
||||
"""Page comparing 2 Apogee CSV files"""
|
||||
A = _load_apo_data(A_file, autodetect=autodetect)
|
||||
B = _load_apo_data(B_file, autodetect=autodetect)
|
||||
|
||||
try:
|
||||
apo_data_a = _load_apo_data(file_a, autodetect=autodetect)
|
||||
apo_data_b = _load_apo_data(file_b, autodetect=autodetect)
|
||||
except (UnicodeDecodeError, UnicodeEncodeError) as exc:
|
||||
dest_url = url_for("notes.semset_page", scodoc_dept=g.scodoc_dept)
|
||||
if autodetect:
|
||||
raise ScoValueError(
|
||||
"""
|
||||
Erreur: l'encodage de l'un des fichiers est mal détecté.
|
||||
Essayez sans auto-détection, ou vérifiez le codage et le contenu
|
||||
des fichiers.
|
||||
""",
|
||||
dest_url=dest_url,
|
||||
) from exc
|
||||
else:
|
||||
raise ScoValueError(
|
||||
f"""
|
||||
Erreur: l'encodage de l'un des fichiers est incorrect.
|
||||
Vérifiez qu'il est bien en {sco_apogee_csv.APO_INPUT_ENCODING}
|
||||
""",
|
||||
dest_url=dest_url,
|
||||
) from exc
|
||||
H = [
|
||||
html_sco_header.sco_header(page_title="Comparaison de fichiers Apogée"),
|
||||
"<h2>Comparaison de fichiers Apogée</h2>",
|
||||
_help_txt,
|
||||
'<div class="apo_compare_csv">',
|
||||
_apo_compare_csv(A, B),
|
||||
_apo_compare_csv(apo_data_a, apo_data_b),
|
||||
"</div>",
|
||||
"""<p><a href="apo_compare_csv_form" class="stdlink">Autre comparaison</a></p>""",
|
||||
html_sco_header.sco_footer(),
|
||||
@ -112,9 +132,9 @@ def _load_apo_data(csvfile, autodetect=True):
|
||||
if autodetect:
|
||||
data_b, message = sco_apogee_csv.fix_data_encoding(data_b)
|
||||
if message:
|
||||
log("apo_compare_csv: %s" % message)
|
||||
log(f"apo_compare_csv: {message}")
|
||||
if not data_b:
|
||||
raise ScoValueError("apo_compare_csv: no data")
|
||||
raise ScoValueError("fichier vide ? (apo_compare_csv: no data)")
|
||||
data = data_b.decode(sco_apogee_csv.APO_INPUT_ENCODING)
|
||||
apo_data = sco_apogee_csv.ApoData(data, orig_filename=csvfile.filename)
|
||||
return apo_data
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 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
|
||||
@ -155,28 +155,25 @@ def fix_data_encoding(
|
||||
text: bytes,
|
||||
default_source_encoding=APO_INPUT_ENCODING,
|
||||
dest_encoding=APO_INPUT_ENCODING,
|
||||
) -> bytes:
|
||||
) -> tuple[bytes, str]:
|
||||
"""Try to ensure that text is using dest_encoding
|
||||
returns converted text, and a message describing the conversion.
|
||||
|
||||
Raises UnicodeEncodeError en cas de problème, en général liée à
|
||||
une auto-détection errornée.
|
||||
"""
|
||||
message = ""
|
||||
detected_encoding = guess_data_encoding(text)
|
||||
if not detected_encoding:
|
||||
if default_source_encoding != dest_encoding:
|
||||
message = "converting from %s to %s" % (
|
||||
default_source_encoding,
|
||||
dest_encoding,
|
||||
)
|
||||
text = text.decode(default_source_encoding).encode(
|
||||
dest_encoding
|
||||
) # XXX #py3 #sco8 à tester
|
||||
message = f"converting from {default_source_encoding} to {dest_encoding}"
|
||||
text = text.decode(default_source_encoding).encode(dest_encoding)
|
||||
else:
|
||||
if detected_encoding != dest_encoding:
|
||||
message = "converting from detected %s to %s" % (
|
||||
detected_encoding,
|
||||
dest_encoding,
|
||||
message = (
|
||||
f"converting from detected {default_source_encoding} to {dest_encoding}"
|
||||
)
|
||||
text = text.decode(detected_encoding).encode(dest_encoding) # XXX
|
||||
text = text.decode(detected_encoding).encode(dest_encoding)
|
||||
return text, message
|
||||
|
||||
|
||||
@ -511,7 +508,7 @@ class ApoEtud(dict):
|
||||
# print 'comp_elt_annuel cur_sem=%s autre_sem=%s' % (cur_sem['formsemestre_id'], autre_sem['formsemestre_id'])
|
||||
if not cur_sem:
|
||||
# l'étudiant n'a pas de semestre courant ?!
|
||||
log("comp_elt_annuel: etudid %s has no cur_sem" % etudid)
|
||||
log(f"comp_elt_annuel: etudid {etudid} has no cur_sem")
|
||||
return VOID_APO_RES
|
||||
cur_formsemestre = FormSemestre.query.get_or_404(cur_sem["formsemestre_id"])
|
||||
cur_nt: NotesTableCompat = res_sem.load_formsemestre_results(cur_formsemestre)
|
||||
@ -586,7 +583,11 @@ class ApoEtud(dict):
|
||||
(sem["semestre_id"] == apo_data.cur_semestre_id)
|
||||
and (apo_data.etape in sem["etapes"])
|
||||
and (
|
||||
sco_formsemestre.sem_in_annee_scolaire(sem, apo_data.annee_scolaire)
|
||||
sco_formsemestre.sem_in_semestre_scolaire(
|
||||
sem,
|
||||
apo_data.annee_scolaire,
|
||||
0, # annee complete
|
||||
)
|
||||
)
|
||||
)
|
||||
]
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 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
|
||||
@ -68,7 +68,7 @@ from app import log
|
||||
from app.but import jury_but_pv
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import Departement, FormSemestre
|
||||
from app.models import FormSemestre
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||
from app.scodoc.sco_exceptions import (
|
||||
AccessDenied,
|
||||
@ -89,6 +89,11 @@ class BaseArchiver(object):
|
||||
self.archive_type = archive_type
|
||||
self.initialized = False
|
||||
self.root = None
|
||||
self.dept_id = None
|
||||
|
||||
def set_dept_id(self, dept_id: int):
|
||||
"set dept"
|
||||
self.dept_id = dept_id
|
||||
|
||||
def initialize(self):
|
||||
if self.initialized:
|
||||
@ -105,20 +110,21 @@ class BaseArchiver(object):
|
||||
try:
|
||||
scu.GSL.acquire()
|
||||
if not os.path.isdir(path):
|
||||
log("creating directory %s" % path)
|
||||
log(f"creating directory {path}")
|
||||
os.mkdir(path)
|
||||
finally:
|
||||
scu.GSL.release()
|
||||
self.initialized = True
|
||||
if self.dept_id is None:
|
||||
self.dept_id = getattr(g, "scodoc_dept_id")
|
||||
|
||||
def get_obj_dir(self, oid):
|
||||
def get_obj_dir(self, oid: int):
|
||||
"""
|
||||
:return: path to directory of archives for this object (eg formsemestre_id or etudid).
|
||||
If directory does not yet exist, create it.
|
||||
"""
|
||||
self.initialize()
|
||||
dept = Departement.query.filter_by(acronym=g.scodoc_dept).first()
|
||||
dept_dir = os.path.join(self.root, str(dept.id))
|
||||
dept_dir = os.path.join(self.root, str(self.dept_id))
|
||||
try:
|
||||
scu.GSL.acquire()
|
||||
if not os.path.isdir(dept_dir):
|
||||
@ -137,12 +143,11 @@ class BaseArchiver(object):
|
||||
:return: list of archive oids
|
||||
"""
|
||||
self.initialize()
|
||||
dept = Departement.query.filter_by(acronym=g.scodoc_dept).first()
|
||||
base = os.path.join(self.root, str(dept.id)) + os.path.sep
|
||||
base = os.path.join(self.root, str(self.dept_id)) + os.path.sep
|
||||
dirs = glob.glob(base + "*")
|
||||
return [os.path.split(x)[1] for x in dirs]
|
||||
|
||||
def list_obj_archives(self, oid):
|
||||
def list_obj_archives(self, oid: int):
|
||||
"""Returns
|
||||
:return: list of archive identifiers for this object (paths to non empty dirs)
|
||||
"""
|
||||
@ -157,7 +162,7 @@ class BaseArchiver(object):
|
||||
dirs.sort()
|
||||
return dirs
|
||||
|
||||
def delete_archive(self, archive_id):
|
||||
def delete_archive(self, archive_id: str):
|
||||
"""Delete (forever) this archive"""
|
||||
self.initialize()
|
||||
try:
|
||||
@ -166,7 +171,7 @@ class BaseArchiver(object):
|
||||
finally:
|
||||
scu.GSL.release()
|
||||
|
||||
def get_archive_date(self, archive_id):
|
||||
def get_archive_date(self, archive_id: str):
|
||||
"""Returns date (as a DateTime object) of an archive"""
|
||||
return datetime.datetime(
|
||||
*[int(x) for x in os.path.split(archive_id)[1].split("-")]
|
||||
@ -183,17 +188,17 @@ class BaseArchiver(object):
|
||||
files.sort()
|
||||
return [f for f in files if f and f[0] != "_"]
|
||||
|
||||
def get_archive_name(self, archive_id):
|
||||
def get_archive_name(self, archive_id: str):
|
||||
"""name identifying archive, to be used in web URLs"""
|
||||
return os.path.split(archive_id)[1]
|
||||
|
||||
def is_valid_archive_name(self, archive_name):
|
||||
def is_valid_archive_name(self, archive_name: str):
|
||||
"""check if name is valid."""
|
||||
return re.match(
|
||||
"^[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}$", archive_name
|
||||
)
|
||||
|
||||
def get_id_from_name(self, oid, archive_name):
|
||||
def get_id_from_name(self, oid, archive_name: str):
|
||||
"""returns archive id (check that name is valid)"""
|
||||
self.initialize()
|
||||
if not self.is_valid_archive_name(archive_name):
|
||||
@ -206,7 +211,7 @@ class BaseArchiver(object):
|
||||
raise ScoValueError(f"Archive {archive_name} introuvable")
|
||||
return archive_id
|
||||
|
||||
def get_archive_description(self, archive_id):
|
||||
def get_archive_description(self, archive_id: str) -> str:
|
||||
"""Return description of archive"""
|
||||
self.initialize()
|
||||
filename = os.path.join(archive_id, "_description.txt")
|
||||
@ -247,7 +252,7 @@ class BaseArchiver(object):
|
||||
data = data.encode(scu.SCO_ENCODING)
|
||||
self.initialize()
|
||||
filename = scu.sanitize_filename(filename)
|
||||
log("storing %s (%d bytes) in %s" % (filename, len(data), archive_id))
|
||||
log(f"storing {filename} ({len(data)} bytes) in {archive_id}")
|
||||
try:
|
||||
scu.GSL.acquire()
|
||||
fname = os.path.join(archive_id, filename)
|
||||
@ -261,16 +266,18 @@ class BaseArchiver(object):
|
||||
"""Retreive data"""
|
||||
self.initialize()
|
||||
if not scu.is_valid_filename(filename):
|
||||
log('Archiver.get: invalid filename "%s"' % filename)
|
||||
log(f"""Archiver.get: invalid filename '{filename}'""")
|
||||
raise ScoValueError("archive introuvable (déjà supprimée ?)")
|
||||
fname = os.path.join(archive_id, filename)
|
||||
log("reading archive file %s" % fname)
|
||||
log(f"reading archive file {fname}")
|
||||
with open(fname, "rb") as f:
|
||||
data = f.read()
|
||||
return data
|
||||
|
||||
def get_archived_file(self, oid, archive_name, filename):
|
||||
"""Recupere donnees du fichier indiqué et envoie au client"""
|
||||
"""Recupère les donnees du fichier indiqué et envoie au client.
|
||||
Returns: Response
|
||||
"""
|
||||
archive_id = self.get_id_from_name(oid, archive_name)
|
||||
data = self.get(archive_id, filename)
|
||||
mime = mimetypes.guess_type(filename)[0]
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 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
|
||||
@ -373,7 +373,7 @@ def etudarchive_import_files(
|
||||
filename_title="fichier_a_charger",
|
||||
)
|
||||
return render_template(
|
||||
"scolar/photos_import_files.html",
|
||||
"scolar/photos_import_files.j2",
|
||||
page_title="Téléchargement de fichiers associés aux étudiants",
|
||||
ignored_zipfiles=ignored_zipfiles,
|
||||
unmatched_files=unmatched_files,
|
||||
|
108
app/scodoc/sco_archives_justificatifs.py
Normal file
108
app/scodoc/sco_archives_justificatifs.py
Normal file
@ -0,0 +1,108 @@
|
||||
from app.scodoc.sco_archives import BaseArchiver
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.models import Identite, Departement
|
||||
from flask import g
|
||||
import os
|
||||
|
||||
|
||||
class JustificatifArchiver(BaseArchiver):
|
||||
"""
|
||||
|
||||
TOTALK:
|
||||
- oid -> etudid
|
||||
- archive_id -> date de création de l'archive (une archive par dépot de document)
|
||||
|
||||
justificatif
|
||||
└── <dept_id>
|
||||
└── <etudid/oid>
|
||||
└── <archive_id>
|
||||
├── [_description.txt]
|
||||
└── [<filename.ext>]
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
BaseArchiver.__init__(self, archive_type="justificatifs")
|
||||
|
||||
def save_justificatif(
|
||||
self,
|
||||
etudid: int,
|
||||
filename: str,
|
||||
data: bytes or str,
|
||||
archive_name: str = None,
|
||||
description: str = "",
|
||||
) -> str:
|
||||
"""
|
||||
Ajoute un fichier dans une archive "justificatif" pour l'etudid donné
|
||||
Retourne l'archive_name utilisé
|
||||
"""
|
||||
self._set_dept(etudid)
|
||||
if archive_name is None:
|
||||
archive_id: str = self.create_obj_archive(
|
||||
oid=etudid, description=description
|
||||
)
|
||||
else:
|
||||
archive_id: str = self.get_id_from_name(etudid, archive_name)
|
||||
|
||||
fname: str = self.store(archive_id, filename, data)
|
||||
|
||||
return self.get_archive_name(archive_id), fname
|
||||
|
||||
def delete_justificatif(self, etudid: int, archive_name: str, filename: str = None):
|
||||
"""
|
||||
Supprime une archive ou un fichier particulier de l'archivage de l'étudiant donné
|
||||
"""
|
||||
self._set_dept(etudid)
|
||||
if str(etudid) not in self.list_oids():
|
||||
raise ValueError(f"Aucune archive pour etudid[{etudid}]")
|
||||
|
||||
archive_id = self.get_id_from_name(etudid, archive_name)
|
||||
|
||||
if filename is not None:
|
||||
if filename not in self.list_archive(archive_id):
|
||||
raise ValueError(
|
||||
f"filename {filename} inconnu dans l'archive archive_id[{archive_id}] -> etudid[{etudid}]"
|
||||
)
|
||||
|
||||
path: str = os.path.join(self.get_obj_dir(etudid), archive_id, filename)
|
||||
|
||||
if os.path.isfile(path):
|
||||
os.remove(path)
|
||||
|
||||
else:
|
||||
self.delete_archive(
|
||||
os.path.join(
|
||||
self.get_obj_dir(etudid),
|
||||
archive_id,
|
||||
)
|
||||
)
|
||||
|
||||
def list_justificatifs(self, archive_name: str, etudid: int) -> list[str]:
|
||||
"""
|
||||
Retourne la liste des noms de fichiers dans l'archive donnée
|
||||
"""
|
||||
self._set_dept(etudid)
|
||||
filenames: list[str] = []
|
||||
archive_id = self.get_id_from_name(etudid, archive_name)
|
||||
|
||||
filenames = self.list_archive(archive_id)
|
||||
return filenames
|
||||
|
||||
def get_justificatif_file(self, archive_name: str, etudid: int, filename: str):
|
||||
"""
|
||||
Retourne une réponse de téléchargement de fichier si le fichier existe
|
||||
"""
|
||||
self._set_dept(etudid)
|
||||
archive_id: str = self.get_id_from_name(etudid, archive_name)
|
||||
if filename in self.list_archive(archive_id):
|
||||
return self.get_archived_file(etudid, archive_name, filename)
|
||||
raise ScoValueError(
|
||||
f"Fichier {filename} introuvable dans l'archive {archive_name}"
|
||||
)
|
||||
|
||||
def _set_dept(self, etudid: int):
|
||||
"""
|
||||
Mets à jour le dept_id de l'archiver en fonction du département de l'étudiant
|
||||
"""
|
||||
etud: Identite = Identite.query.filter_by(id=etudid).first()
|
||||
self.set_dept_id(etud.dept_id)
|
401
app/scodoc/sco_assiduites.py
Normal file
401
app/scodoc/sco_assiduites.py
Normal file
@ -0,0 +1,401 @@
|
||||
from datetime import date, datetime, time, timedelta
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.models.assiduites import Assiduite, Justificatif
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
||||
|
||||
|
||||
class CountCalculator:
|
||||
def __init__(
|
||||
self,
|
||||
morning: time = time(8, 0),
|
||||
noon: time = time(12, 0),
|
||||
after_noon: time = time(14, 00),
|
||||
evening: time = time(18, 0),
|
||||
skip_saturday: bool = True,
|
||||
) -> None:
|
||||
|
||||
self.morning: time = morning
|
||||
self.noon: time = noon
|
||||
self.after_noon: time = after_noon
|
||||
self.evening: time = evening
|
||||
self.skip_saturday: bool = skip_saturday
|
||||
|
||||
delta_total: timedelta = datetime.combine(date.min, evening) - datetime.combine(
|
||||
date.min, morning
|
||||
)
|
||||
delta_lunch: timedelta = datetime.combine(
|
||||
date.min, after_noon
|
||||
) - datetime.combine(date.min, noon)
|
||||
|
||||
self.hour_per_day: float = (delta_total - delta_lunch).total_seconds() / 3600
|
||||
|
||||
self.days: list[date] = []
|
||||
self.half_days: list[tuple[date, bool]] = [] # tuple -> (date, morning:bool)
|
||||
self.hours: float = 0.0
|
||||
|
||||
self.count: int = 0
|
||||
|
||||
def add_half_day(self, day: date, is_morning: bool = True):
|
||||
key: tuple[date, bool] = (day, is_morning)
|
||||
if key not in self.half_days:
|
||||
self.half_days.append(key)
|
||||
|
||||
def add_day(self, day: date):
|
||||
if day not in self.days:
|
||||
self.days.append(day)
|
||||
|
||||
def check_in_morning(self, period: tuple[datetime, datetime]) -> bool:
|
||||
|
||||
interval_morning: tuple[datetime, datetime] = (
|
||||
scu.localize_datetime(datetime.combine(period[0].date(), self.morning)),
|
||||
scu.localize_datetime(datetime.combine(period[0].date(), self.noon)),
|
||||
)
|
||||
|
||||
in_morning: bool = scu.is_period_overlapping(period, interval_morning)
|
||||
return in_morning
|
||||
|
||||
def check_in_evening(self, period: tuple[datetime, datetime]) -> bool:
|
||||
|
||||
interval_evening: tuple[datetime, datetime] = (
|
||||
scu.localize_datetime(datetime.combine(period[0].date(), self.after_noon)),
|
||||
scu.localize_datetime(datetime.combine(period[0].date(), self.evening)),
|
||||
)
|
||||
|
||||
in_evening: bool = scu.is_period_overlapping(period, interval_evening)
|
||||
|
||||
return in_evening
|
||||
|
||||
def compute_long_assiduite(self, assi: Assiduite):
|
||||
|
||||
pointer_date: date = assi.date_debut.date() + timedelta(days=1)
|
||||
start_hours: timedelta = assi.date_debut - scu.localize_datetime(
|
||||
datetime.combine(assi.date_debut, self.morning)
|
||||
)
|
||||
finish_hours: timedelta = assi.date_fin - scu.localize_datetime(
|
||||
datetime.combine(assi.date_fin, self.morning)
|
||||
)
|
||||
|
||||
self.add_day(assi.date_debut.date())
|
||||
self.add_day(assi.date_fin.date())
|
||||
|
||||
start_period: tuple[datetime, datetime] = (
|
||||
assi.date_debut,
|
||||
scu.localize_datetime(
|
||||
datetime.combine(assi.date_debut.date(), self.evening)
|
||||
),
|
||||
)
|
||||
|
||||
finish_period: tuple[datetime, datetime] = (
|
||||
scu.localize_datetime(datetime.combine(assi.date_fin.date(), self.morning)),
|
||||
assi.date_fin,
|
||||
)
|
||||
hours = 0.0
|
||||
for period in (start_period, finish_period):
|
||||
if self.check_in_evening(period):
|
||||
self.add_half_day(period[0].date(), False)
|
||||
if self.check_in_morning(period):
|
||||
self.add_half_day(period[0].date())
|
||||
|
||||
while pointer_date < assi.date_fin.date():
|
||||
if pointer_date.weekday() < (6 - self.skip_saturday):
|
||||
self.add_day(pointer_date)
|
||||
self.add_half_day(pointer_date)
|
||||
self.add_half_day(pointer_date, False)
|
||||
self.hours += self.hour_per_day
|
||||
hours += self.hour_per_day
|
||||
|
||||
pointer_date += timedelta(days=1)
|
||||
|
||||
self.hours += finish_hours.total_seconds() / 3600
|
||||
self.hours += self.hour_per_day - (start_hours.total_seconds() / 3600)
|
||||
|
||||
def compute_assiduites(self, assiduites: Assiduite):
|
||||
assi: Assiduite
|
||||
for assi in assiduites.all():
|
||||
self.count += 1
|
||||
delta: timedelta = assi.date_fin - assi.date_debut
|
||||
|
||||
if delta.days > 0:
|
||||
# raise Exception(self.hours)
|
||||
self.compute_long_assiduite(assi)
|
||||
|
||||
continue
|
||||
|
||||
period: tuple[datetime, datetime] = (assi.date_debut, assi.date_fin)
|
||||
deb_date: date = assi.date_debut.date()
|
||||
if self.check_in_morning(period):
|
||||
self.add_half_day(deb_date)
|
||||
if self.check_in_evening(period):
|
||||
self.add_half_day(deb_date, False)
|
||||
|
||||
self.add_day(deb_date)
|
||||
|
||||
self.hours += delta.total_seconds() / 3600
|
||||
|
||||
def to_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
"compte": self.count,
|
||||
"journee": len(self.days),
|
||||
"demi": len(self.half_days),
|
||||
"heure": round(self.hours, 2),
|
||||
}
|
||||
|
||||
|
||||
def get_assiduites_stats(
|
||||
assiduites: Assiduite, metric: str = "all", filtered: dict[str, object] = None
|
||||
) -> Assiduite:
|
||||
|
||||
if filtered is not None:
|
||||
deb, fin = None, None
|
||||
for key in filtered:
|
||||
if key == "etat":
|
||||
assiduites = filter_assiduites_by_etat(assiduites, filtered[key])
|
||||
elif key == "date_fin":
|
||||
fin = filtered[key]
|
||||
elif key == "date_debut":
|
||||
deb = filtered[key]
|
||||
elif key == "moduleimpl_id":
|
||||
assiduites = filter_by_module_impl(assiduites, filtered[key])
|
||||
elif key == "formsemestre":
|
||||
assiduites = filter_by_formsemestre(assiduites, filtered[key])
|
||||
if (deb, fin) != (None, None):
|
||||
assiduites = filter_by_date(assiduites, Assiduite, deb, fin)
|
||||
|
||||
calculator: CountCalculator = CountCalculator()
|
||||
calculator.compute_assiduites(assiduites)
|
||||
count: dict = calculator.to_dict()
|
||||
|
||||
metrics: list[str] = metric.split(",")
|
||||
|
||||
output: dict = {}
|
||||
|
||||
for key, val in count.items():
|
||||
if key in metrics:
|
||||
output[key] = val
|
||||
return output if output else count
|
||||
|
||||
|
||||
# def big_counter(
|
||||
# interval: tuple[datetime],
|
||||
# pref_time: time = time(12, 0),
|
||||
# ):
|
||||
# curr_date: datetime
|
||||
|
||||
# if interval[0].time() >= pref_time:
|
||||
# curr_date = scu.localize_datetime(
|
||||
# datetime.combine(interval[0].date(), pref_time)
|
||||
# )
|
||||
# else:
|
||||
# curr_date = scu.localize_datetime(
|
||||
# datetime.combine(interval[0].date(), time(0, 0))
|
||||
# )
|
||||
|
||||
# def next_(curr: datetime, journee):
|
||||
# if curr.time() != pref_time:
|
||||
# next_time = scu.localize_datetime(datetime.combine(curr.date(), pref_time))
|
||||
# else:
|
||||
# next_time = scu.localize_datetime(
|
||||
# datetime.combine(curr.date() + timedelta(days=1), time(0, 0))
|
||||
# )
|
||||
# journee += 1
|
||||
# return next_time, journee
|
||||
|
||||
# demi: int = 0
|
||||
# j: int = 0
|
||||
# while curr_date <= interval[1]:
|
||||
# next_time: datetime
|
||||
# next_time, j = next_(curr_date, j)
|
||||
# if scu.is_period_overlapping((curr_date, next_time), interval, True):
|
||||
# demi += 1
|
||||
# curr_date = next_time
|
||||
|
||||
# delta: timedelta = interval[1] - interval[0]
|
||||
# heures: float = delta.total_seconds() / 3600
|
||||
|
||||
# if delta.days >= 1:
|
||||
# heures -= delta.days * 16
|
||||
|
||||
# return (demi, j, heures)
|
||||
|
||||
|
||||
# def get_count(
|
||||
# assiduites: Assiduite, noon: time = time(hour=12)
|
||||
# ) -> dict[str, int or float]:
|
||||
# """Fonction permettant de compter les assiduites
|
||||
# -> seul "compte" est correcte lorsque les assiduites viennent de plusieurs étudiants
|
||||
# """
|
||||
# # TODO: Comptage demi journée / journée d'assiduité longue
|
||||
# output: dict[str, int or float] = {}
|
||||
# compte: int = assiduites.count()
|
||||
# heure: float = 0.0
|
||||
# journee: int = 0
|
||||
# demi: int = 0
|
||||
|
||||
# all_assiduites: list[Assiduite] = assiduites.order_by(Assiduite.date_debut).all()
|
||||
|
||||
# current_day: date = None
|
||||
# current_time: str = None
|
||||
|
||||
# midnight: time = time(hour=0)
|
||||
|
||||
# def time_check(dtime):
|
||||
# return midnight <= dtime.time() <= noon
|
||||
|
||||
# for ass in all_assiduites:
|
||||
# delta: timedelta = ass.date_fin - ass.date_debut
|
||||
|
||||
# if delta.days > 0:
|
||||
|
||||
# computed_values: tuple[int, int, float] = big_counter(
|
||||
# (ass.date_debut, ass.date_fin), noon
|
||||
# )
|
||||
|
||||
# demi += computed_values[0] - 1
|
||||
# journee += computed_values[1] - 1
|
||||
# heure += computed_values[2]
|
||||
|
||||
# current_day = ass.date_fin.date()
|
||||
# continue
|
||||
|
||||
# heure += delta.total_seconds() / 3600
|
||||
|
||||
# ass_time: str = time_check(ass.date_debut)
|
||||
|
||||
# if current_day != ass.date_debut.date():
|
||||
# current_day = ass.date_debut.date()
|
||||
# current_time = ass_time
|
||||
# demi += 1
|
||||
# journee += 1
|
||||
|
||||
# if current_time != ass_time:
|
||||
# current_time = ass_time
|
||||
# demi += 1
|
||||
|
||||
# heure = round(heure, 2)
|
||||
# return {"compte": compte, "journee": journee, "heure": heure, "demi": demi}
|
||||
|
||||
|
||||
def filter_assiduites_by_etat(assiduites: Assiduite, etat: str) -> Assiduite:
|
||||
"""
|
||||
Filtrage d'une collection d'assiduites en fonction de leur état
|
||||
"""
|
||||
etats: list[str] = list(etat.split(","))
|
||||
etats = [scu.EtatAssiduite.get(e, -1) for e in etats]
|
||||
return assiduites.filter(Assiduite.etat.in_(etats))
|
||||
|
||||
|
||||
def filter_by_date(
|
||||
collection: Assiduite or Justificatif,
|
||||
collection_cls: Assiduite or Justificatif,
|
||||
date_deb: datetime = None,
|
||||
date_fin: datetime = None,
|
||||
strict: bool = False,
|
||||
):
|
||||
"""
|
||||
Filtrage d'une collection d'assiduites en fonction d'une date
|
||||
"""
|
||||
if date_deb is None:
|
||||
date_deb = datetime.min
|
||||
if date_fin is None:
|
||||
date_fin = datetime.max
|
||||
|
||||
date_deb = scu.localize_datetime(date_deb)
|
||||
date_fin = scu.localize_datetime(date_fin)
|
||||
if not strict:
|
||||
return collection.filter(
|
||||
collection_cls.date_debut <= date_fin, collection_cls.date_fin >= date_deb
|
||||
)
|
||||
return collection.filter(
|
||||
collection_cls.date_debut < date_fin, collection_cls.date_fin > date_deb
|
||||
)
|
||||
|
||||
|
||||
def filter_justificatifs_by_etat(
|
||||
justificatifs: Justificatif, etat: str
|
||||
) -> Justificatif:
|
||||
"""
|
||||
Filtrage d'une collection de justificatifs en fonction de leur état
|
||||
"""
|
||||
etats: list[str] = list(etat.split(","))
|
||||
etats = [scu.EtatJustificatif.get(e, -1) for e in etats]
|
||||
return justificatifs.filter(Justificatif.etat.in_(etats))
|
||||
|
||||
|
||||
def filter_justificatifs_by_date(
|
||||
justificatifs: Justificatif, date_: datetime, sup: bool = True
|
||||
) -> Assiduite:
|
||||
"""
|
||||
Filtrage d'une collection d'assiduites en fonction d'une date
|
||||
|
||||
Sup == True -> les assiduites doivent débuter après 'date'\n
|
||||
Sup == False -> les assiduites doivent finir avant 'date'
|
||||
"""
|
||||
|
||||
if date_.tzinfo is None:
|
||||
first_justificatif: Justificatif = justificatifs.first()
|
||||
if first_justificatif is not None:
|
||||
date_: datetime = date_.replace(tzinfo=first_justificatif.date_debut.tzinfo)
|
||||
|
||||
if sup:
|
||||
return justificatifs.filter(Justificatif.date_debut >= date_)
|
||||
|
||||
return justificatifs.filter(Justificatif.date_fin <= date_)
|
||||
|
||||
|
||||
def filter_by_module_impl(
|
||||
assiduites: Assiduite, module_impl_id: int or None
|
||||
) -> Assiduite:
|
||||
"""
|
||||
Filtrage d'une collection d'assiduites en fonction de l'ID du module_impl
|
||||
"""
|
||||
return assiduites.filter(Assiduite.moduleimpl_id == module_impl_id)
|
||||
|
||||
|
||||
def filter_by_formsemestre(assiduites_query: Assiduite, formsemestre: FormSemestre):
|
||||
"""
|
||||
Filtrage d'une collection d'assiduites en fonction d'un formsemestre
|
||||
"""
|
||||
|
||||
if formsemestre is None:
|
||||
return assiduites_query.filter(False)
|
||||
|
||||
assiduites_query = (
|
||||
assiduites_query.join(Identite, Assiduite.etudid == Identite.id)
|
||||
.join(
|
||||
FormSemestreInscription,
|
||||
Identite.id == FormSemestreInscription.etudid,
|
||||
)
|
||||
.filter(FormSemestreInscription.formsemestre_id == formsemestre.id)
|
||||
)
|
||||
|
||||
assiduites_query = assiduites_query.filter(
|
||||
Assiduite.date_debut >= formsemestre.date_debut
|
||||
)
|
||||
return assiduites_query.filter(Assiduite.date_fin <= formsemestre.date_fin)
|
||||
|
||||
|
||||
def justifies(justi: Justificatif) -> list[int]:
|
||||
"""
|
||||
Retourne la liste des assiduite_id qui sont justifié par la justification
|
||||
Une assiduité est justifiée si elle est STRICTEMENT comprise dans la plage du justificatif
|
||||
et que l'état du justificatif est "validé"
|
||||
"""
|
||||
|
||||
justified: list[int] = []
|
||||
|
||||
if justi.etat != scu.EtatJustificatif.VALIDE:
|
||||
return justified
|
||||
|
||||
assiduites_query: Assiduite = Assiduite.query.join(
|
||||
Justificatif, Assiduite.etudid == Justificatif.etudid
|
||||
).filter(Assiduite.etat != scu.EtatAssiduite.PRESENT)
|
||||
|
||||
assiduites_query = filter_by_date(
|
||||
assiduites_query, Assiduite, justi.date_debut, justi.date_fin
|
||||
)
|
||||
|
||||
justified = [assi.id for assi in assiduites_query.all()]
|
||||
|
||||
return justified
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 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 - 2022 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
|
||||
@ -926,7 +926,7 @@ def formsemestre_bulletinetud(
|
||||
_formsemestre_bulletinetud_header_html(etud, formsemestre, format, version),
|
||||
bulletin,
|
||||
render_template(
|
||||
"bul_foot.html",
|
||||
"bul_foot.j2",
|
||||
appreciations=None, # déjà affichées
|
||||
css_class="bul_classic_foot",
|
||||
etud=etud,
|
||||
@ -990,6 +990,8 @@ def do_formsemestre_bulletinetud(
|
||||
version=version,
|
||||
)
|
||||
return bul, ""
|
||||
if version.endswith("_mat"):
|
||||
version = version[:-4] # enlève le "_mat"
|
||||
|
||||
if formsemestre.formation.is_apc():
|
||||
etudiant = Identite.query.get(etudid)
|
||||
@ -1082,7 +1084,7 @@ def mail_bulletin(formsemestre_id, infos, pdfdata, filename, recipient_addr):
|
||||
recipients = [recipient_addr]
|
||||
sender = sco_preferences.get_preference("email_from_addr", formsemestre_id)
|
||||
if copy_addr:
|
||||
bcc = copy_addr.strip()
|
||||
bcc = copy_addr.strip().split(",")
|
||||
else:
|
||||
bcc = ""
|
||||
|
||||
@ -1092,7 +1094,7 @@ def mail_bulletin(formsemestre_id, infos, pdfdata, filename, recipient_addr):
|
||||
subject,
|
||||
sender,
|
||||
recipients,
|
||||
bcc=[bcc],
|
||||
bcc=bcc,
|
||||
text_body=hea,
|
||||
attachments=[
|
||||
{"filename": filename, "mimetype": scu.PDF_MIMETYPE, "data": pdfdata}
|
||||
@ -1215,7 +1217,8 @@ def make_menu_autres_operations(
|
||||
"formsemestre_id": formsemestre.id,
|
||||
"etudid": etud.id,
|
||||
},
|
||||
"enabled": sco_permissions_check.can_validate_sem(formsemestre.id),
|
||||
"enabled": sco_permissions_check.can_validate_sem(formsemestre.id)
|
||||
and not formsemestre.formation.is_apc(),
|
||||
},
|
||||
{
|
||||
"title": "Entrer décisions jury",
|
||||
@ -1256,7 +1259,7 @@ def _formsemestre_bulletinetud_header_html(
|
||||
cssstyles=["css/radar_bulletin.css"],
|
||||
),
|
||||
render_template(
|
||||
"bul_head.html",
|
||||
"bul_head.j2",
|
||||
etud=etud,
|
||||
format=format,
|
||||
formsemestre=formsemestre,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user