forked from ScoDoc/ScoDoc
Compare commits
140 Commits
bac_a_sabl
...
master
Author | SHA1 | Date | |
---|---|---|---|
632e285d26 | |||
96531f839c | |||
1dfe754793 | |||
e0188ebc2d | |||
c79eb6410a | |||
92e75e11f2 | |||
0dfc27e072 | |||
c3cc316777 | |||
b04812f812 | |||
febc15c4c8 | |||
a58919d8b4 | |||
8be0ab0678 | |||
b913119b58 | |||
0573081711 | |||
f414ec1c0d | |||
1b297580c9 | |||
538a5427ba | |||
30666fc822 | |||
8614a29f9b | |||
5cdfb360fa | |||
f08a4130dd | |||
24bb78bdfd | |||
ea9c6a6ef2 | |||
8ee95cc2e5 | |||
c933f010d4 | |||
2c93c35aa7 | |||
417b3b8383 | |||
814a3802e9 | |||
1de265536e | |||
ea1e5cfb89 | |||
5c78c1cf9f | |||
9bfebfc8a2 | |||
2e5add1c48 | |||
52282c98cb | |||
dba48f32eb | |||
8b37f661d6 | |||
d88e41b83e | |||
41a791282a | |||
6c56b921e8 | |||
428a34e6ba | |||
c5a702e6d1 | |||
0824598aa4 | |||
90c1454b21 | |||
9b3df5febf | |||
a2c5be22cb | |||
cf0d3c06c4 | |||
e963ca52f5 | |||
2cc911eb0d | |||
84d1ed6c85 | |||
5f06b190a2 | |||
937a96d086 | |||
10de8c4cc2 | |||
da7f9a334f | |||
0dda9157eb | |||
90fd45a572 | |||
ad4e4e33ec | |||
87316f057e | |||
7a1dfcbb63 | |||
3325b41690 | |||
35fb269a41 | |||
b4c68cea10 | |||
77e4c4f726 | |||
61b46db4dd | |||
75d0170b4a | |||
ee95a6178a | |||
ebe7dd8f73 | |||
5d30b9233b | |||
4b49fd5ed9 | |||
7ed521e4f5 | |||
71ffb33175 | |||
0c9d202e09 | |||
e190756b98 | |||
37845750a6 | |||
70049da38f | |||
b2bd659c47 | |||
658fb3595d | |||
f9961498bf | |||
66983ff767 | |||
52db344926 | |||
87cc4c06d6 | |||
c9babcd8c2 | |||
d57b6638ea | |||
438caf1052 | |||
c45abc33cc | |||
cc39e4a862 | |||
b501233ba4 | |||
9067424f8f | |||
83218e39b6 | |||
544abba758 | |||
ff73ba8a5b | |||
d2fbbad84b | |||
7c9f07c36e | |||
c8a974d460 | |||
ccc589f4d5 | |||
f4277a1336 | |||
8156cce4be | |||
47cf5962f9 | |||
d1d83e0327 | |||
027224a7b3 | |||
91b86f30a5 | |||
b026349e74 | |||
fdfffb70be | |||
de23302b3e | |||
756c46df0b | |||
84d40091a8 | |||
f9b4539231 | |||
021b4ec5f8 | |||
e3b979fc10 | |||
44dffea8d2 | |||
008dd9b50e | |||
e46ae76399 | |||
73023b7806 | |||
c73581c52f | |||
c547990eef | |||
c6f3ad448a | |||
f4c776e9f3 | |||
cf876ca0d3 | |||
f979eb137c | |||
fda11298b4 | |||
0322603a22 | |||
fb4cabee3b | |||
bfa8cf1683 | |||
42b232dd59 | |||
a8ab0cb48c | |||
feb799cc20 | |||
bf738d1706 | |||
656bf4d25e | |||
8e1cb055f6 | |||
111c400333 | |||
9579cd73c2 | |||
c5f5cb7daa | |||
5f0ac236d7 | |||
63cf7dfc42 | |||
9a4f7abfa8 | |||
79d92dc9ac | |||
1693bb6c6c | |||
753578813e | |||
cf72686ce4 | |||
4a3910adcf | |||
e5a620a9ea |
4
app/__init__.py
Executable file → Normal file
4
app/__init__.py
Executable file → Normal file
@ -322,7 +322,6 @@ def create_app(config_class=DevConfig):
|
||||
from app.views import notes_bp
|
||||
from app.views import users_bp
|
||||
from app.views import absences_bp
|
||||
from app.views import assiduites_bp
|
||||
from app.api import api_bp
|
||||
from app.api import api_web_bp
|
||||
|
||||
@ -341,9 +340,6 @@ def create_app(config_class=DevConfig):
|
||||
app.register_blueprint(
|
||||
absences_bp, url_prefix="/ScoDoc/<scodoc_dept>/Scolarite/Absences"
|
||||
)
|
||||
app.register_blueprint(
|
||||
assiduites_bp, url_prefix="/ScoDoc/<scodoc_dept>/Scolarite/Assiduites"
|
||||
)
|
||||
app.register_blueprint(api_bp, url_prefix="/ScoDoc/api")
|
||||
app.register_blueprint(api_web_bp, url_prefix="/ScoDoc/<scodoc_dept>/api")
|
||||
|
||||
|
@ -2,8 +2,7 @@
|
||||
"""
|
||||
|
||||
from flask import Blueprint
|
||||
from flask import request, g, jsonify
|
||||
from app import db
|
||||
from flask import request
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import ScoException
|
||||
|
||||
@ -35,26 +34,9 @@ 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,
|
||||
@ -62,7 +44,6 @@ from app.api import (
|
||||
formations,
|
||||
formsemestres,
|
||||
jury,
|
||||
justificatifs,
|
||||
logos,
|
||||
partitions,
|
||||
semset,
|
||||
|
@ -8,6 +8,7 @@
|
||||
|
||||
from flask_json import as_json
|
||||
|
||||
from app import db
|
||||
from app.api import api_bp as bp, API_CLIENT_ERROR
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.decorators import scodoc, permission_required
|
||||
@ -51,7 +52,7 @@ def absences(etudid: int = None):
|
||||
}
|
||||
]
|
||||
"""
|
||||
etud = Identite.query.get(etudid)
|
||||
etud = db.session.get(Identite, etudid)
|
||||
if etud is None:
|
||||
return json_error(404, message="etudiant inexistant")
|
||||
# Absences de l'étudiant
|
||||
@ -96,7 +97,7 @@ def absences_just(etudid: int = None):
|
||||
}
|
||||
]
|
||||
"""
|
||||
etud = Identite.query.get(etudid)
|
||||
etud = db.session.get(Identite, etudid)
|
||||
if etud is None:
|
||||
return json_error(404, message="etudiant inexistant")
|
||||
|
||||
|
@ -1,868 +0,0 @@
|
||||
##############################################################################
|
||||
# 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, current_user
|
||||
|
||||
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",
|
||||
"user_id: 1 or null,
|
||||
"est_just": False or True,
|
||||
}
|
||||
"""
|
||||
|
||||
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?formsemestre_id=[int]
|
||||
ex query?formsemestre_id=3
|
||||
user_id (l'id de l'auteur de l'assiduité)
|
||||
query?user_id=[int]
|
||||
ex query?user_id=3
|
||||
est_just (si l'assiduité est justifié (fait aussi filtre par abs/retard))
|
||||
query?est_just=[bool]
|
||||
query?est_just=f
|
||||
query?est_just=t
|
||||
|
||||
|
||||
|
||||
"""
|
||||
query = 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
|
||||
user_id (l'id de l'auteur de l'assiduité)
|
||||
query?user_id=[int]
|
||||
ex query?user_id=3
|
||||
est_just (si l'assiduité est justifié (fait aussi filtre par abs/retard))
|
||||
query?est_just=[bool]
|
||||
query?est_just=f
|
||||
query?est_just=t
|
||||
|
||||
|
||||
"""
|
||||
|
||||
query = 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/group/query", defaults={"with_query": True})
|
||||
@api_web_bp.route("/assiduites/group/query", defaults={"with_query": True})
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def assiduites_group(with_query: bool = False):
|
||||
"""
|
||||
Retourne toutes les assiduités d'un groupe d'étudiants
|
||||
chemin : /assiduites/group/query?etudids=1,2,3
|
||||
|
||||
Un filtrage peut être donné avec une query
|
||||
chemin : /assiduites/group/query?etudids=1,2,3
|
||||
|
||||
Les différents filtres :
|
||||
Etat (etat de l'étudiant -> absent, present ou retard):
|
||||
query?etat=[- liste des états séparé par une virgule -]
|
||||
ex: .../query?etat=present,retard
|
||||
Date debut
|
||||
(date de début de l'assiduité, sont affichés les assiduités
|
||||
dont la date de début est supérieur ou égale à la valeur donnée):
|
||||
query?date_debut=[- date au format iso -]
|
||||
ex: query?date_debut=2022-11-03T08:00+01:00
|
||||
Date fin
|
||||
(date de fin de l'assiduité, sont affichés les assiduités
|
||||
dont la date de fin est inférieure ou égale à la valeur donnée):
|
||||
query?date_fin=[- date au format iso -]
|
||||
ex: query?date_fin=2022-11-03T10:00+01:00
|
||||
Moduleimpl_id (l'id du module concerné par l'assiduité):
|
||||
query?moduleimpl_id=[- int ou vide -]
|
||||
ex: query?moduleimpl_id=1234
|
||||
query?moduleimpl_od=
|
||||
Formsemstre_id (l'id du formsemestre concerné par l'assiduité)
|
||||
query?formsemstre_id=[int]
|
||||
ex query?formsemestre_id=3
|
||||
user_id (l'id de l'auteur de l'assiduité)
|
||||
query?user_id=[int]
|
||||
ex query?user_id=3
|
||||
est_just (si l'assiduité est justifié (fait aussi filtre par abs/retard))
|
||||
query?est_just=[bool]
|
||||
query?est_just=f
|
||||
query?est_just=t
|
||||
|
||||
|
||||
"""
|
||||
|
||||
etuds = request.args.get("etudids", "")
|
||||
etuds = etuds.split(",")
|
||||
try:
|
||||
etuds = [int(etu) for etu in etuds]
|
||||
except ValueError:
|
||||
return json_error(404, "Le champs etudids n'est pas correctement formé")
|
||||
|
||||
query = Identite.query.filter(Identite.id.in_(etuds))
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
|
||||
if len(etuds) != query.count() or len(etuds) == 0:
|
||||
return json_error(
|
||||
404,
|
||||
"Tous les étudiants ne sont pas dans le même département et/ou n'existe pas.",
|
||||
)
|
||||
assiduites_query = Assiduite.query.filter(Assiduite.etudid.in_(etuds))
|
||||
|
||||
if with_query:
|
||||
assiduites_query = _filter_manager(request, assiduites_query)
|
||||
|
||||
data_set: dict[list[dict]] = {key: [] for key in etuds}
|
||||
for ass in assiduites_query.all():
|
||||
data = ass.to_dict(format_api=True)
|
||||
data_set.get(data["etudid"]).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):
|
||||
"""Retourne toutes les assiduités du formsemestre"""
|
||||
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
|
||||
):
|
||||
"""Comptage des assiduités du formsemestre"""
|
||||
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
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"errors": errors, "success": success})
|
||||
|
||||
|
||||
@bp.route("/assiduites/create", methods=["POST"])
|
||||
@api_web_bp.route("/assiduites/create", methods=["POST"])
|
||||
@scodoc
|
||||
@login_required
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def assiduites_create():
|
||||
"""
|
||||
Création d'une assiduité ou plusieurs assiduites
|
||||
La requête doit avoir un content type "application/json":
|
||||
[
|
||||
{
|
||||
"date_debut": str,
|
||||
"date_fin": str,
|
||||
"etat": str,
|
||||
"etudid":int,
|
||||
},
|
||||
{
|
||||
"date_debut": str,
|
||||
"date_fin": str,
|
||||
"etat": str,
|
||||
"etudid":int,
|
||||
|
||||
"moduleimpl_id": int,
|
||||
"desc":str,
|
||||
}
|
||||
...
|
||||
]
|
||||
|
||||
"""
|
||||
|
||||
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):
|
||||
etud: Identite = Identite.query.filter_by(id=data["etudid"]).first()
|
||||
if etud is None:
|
||||
errors[i] = "Cet étudiant n'existe pas."
|
||||
continue
|
||||
|
||||
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,
|
||||
user_id=current_user.id,
|
||||
)
|
||||
|
||||
db.session.add(nouv_assiduite)
|
||||
db.session.commit()
|
||||
|
||||
return (200, {"assiduite_id": nouv_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_delete():
|
||||
"""
|
||||
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
|
||||
"est_just"?: bool
|
||||
}
|
||||
"""
|
||||
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
|
||||
|
||||
# Cas 4 : est_just
|
||||
est_just = data.get("est_just")
|
||||
if est_just is not None:
|
||||
if not isinstance(est_just, bool):
|
||||
errors.append("param 'est_just' : booléen non reconnu")
|
||||
else:
|
||||
assiduite_unique.est_just = est_just
|
||||
|
||||
if errors:
|
||||
err: str = ", ".join(errors)
|
||||
return json_error(404, err)
|
||||
|
||||
db.session.add(assiduite_unique)
|
||||
db.session.commit()
|
||||
return jsonify({"OK": True})
|
||||
|
||||
|
||||
@bp.route("/assiduites/edit", methods=["POST"])
|
||||
@api_web_bp.route("/assiduites/edit", methods=["POST"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def assiduites_edit():
|
||||
"""
|
||||
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
|
||||
"est_just"?: bool
|
||||
}
|
||||
"""
|
||||
edit_list: list[object] = request.get_json(force=True)
|
||||
|
||||
if not isinstance(edit_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(edit_list):
|
||||
assi: Identite = Assiduite.query.filter_by(id=data["assiduite_id"]).first()
|
||||
if assi is None:
|
||||
errors[i] = "Cet assiduité n'existe pas."
|
||||
continue
|
||||
|
||||
code, obj = _edit_singular(assi, data)
|
||||
if code == 404:
|
||||
errors[i] = obj
|
||||
else:
|
||||
success[i] = obj
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"errors": errors, "success": success})
|
||||
|
||||
|
||||
def _edit_singular(assiduite_unique, data):
|
||||
errors: list[str] = []
|
||||
|
||||
# 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
|
||||
|
||||
# Cas 4 : est_just
|
||||
est_just = data.get("est_just")
|
||||
if est_just is not None:
|
||||
if not isinstance(est_just, bool):
|
||||
errors.append("param 'est_just' : booléen non reconnu")
|
||||
else:
|
||||
assiduite_unique.est_just = est_just
|
||||
|
||||
if errors:
|
||||
err: str = ", ".join(errors)
|
||||
return (404, err)
|
||||
|
||||
db.session.add(assiduite_unique)
|
||||
|
||||
return (200, "OK")
|
||||
|
||||
|
||||
# -- 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", "").replace(" ", "+")
|
||||
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", "").replace(" ", "+")
|
||||
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")
|
||||
|
||||
# cas 7 : est_just
|
||||
|
||||
est_just: str = requested.args.get("est_just")
|
||||
if est_just is not None:
|
||||
trues: tuple[str] = ("v", "t", "vrai", "true")
|
||||
falses: tuple[str] = ("f", "faux", "false")
|
||||
|
||||
if est_just.lower() in trues:
|
||||
filtered["est_just"] = True
|
||||
elif est_just.lower() in falses:
|
||||
filtered["est_just"] = False
|
||||
|
||||
# cas 8 : user_id
|
||||
|
||||
user_id = requested.args.get("user_id", False)
|
||||
if user_id is not False:
|
||||
filtered["user_id"] = user_id
|
||||
|
||||
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", "").replace(" ", "+")
|
||||
deb: datetime = scu.is_iso_formated(deb, True)
|
||||
|
||||
# cas 3 : date de fin
|
||||
fin = requested.args.get("date_fin", "").replace(" ", "+")
|
||||
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)
|
||||
|
||||
# cas 6 : est_just
|
||||
|
||||
est_just: str = requested.args.get("est_just")
|
||||
if est_just is not None:
|
||||
trues: tuple[str] = ("v", "t", "vrai", "true")
|
||||
falses: tuple[str] = ("f", "faux", "false")
|
||||
|
||||
if est_just.lower() in trues:
|
||||
assiduites_query: Assiduite = scass.filter_assiduites_by_est_just(
|
||||
assiduites_query, True
|
||||
)
|
||||
elif est_just.lower() in falses:
|
||||
assiduites_query: Assiduite = scass.filter_assiduites_by_est_just(
|
||||
assiduites_query, False
|
||||
)
|
||||
|
||||
# cas 8 : user_id
|
||||
|
||||
user_id = requested.args.get("user_id", False)
|
||||
if user_id is not False:
|
||||
assiduites_query: Assiduite = scass.filter_by_user_id(assiduites_query, user_id)
|
||||
|
||||
return assiduites_query
|
73
app/api/etudiants.py
Executable file → Normal file
73
app/api/etudiants.py
Executable file → Normal file
@ -8,16 +8,17 @@
|
||||
API : accès aux étudiants
|
||||
"""
|
||||
from datetime import datetime
|
||||
from operator import attrgetter
|
||||
|
||||
from flask import g, request
|
||||
from flask_json import as_json
|
||||
from flask_login import current_user
|
||||
from flask_login import login_required
|
||||
from sqlalchemy import desc, or_
|
||||
from sqlalchemy import desc, func, or_
|
||||
from sqlalchemy.dialects.postgresql import VARCHAR
|
||||
|
||||
import app
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.api import tools
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import (
|
||||
@ -31,7 +32,8 @@ from app.scodoc import sco_bulletins
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
import app.scodoc.sco_photos as sco_photos
|
||||
from app.scodoc.sco_utils import json_error, suppress_accents
|
||||
|
||||
|
||||
# Un exemple:
|
||||
# @bp.route("/api_function/<int:arg>")
|
||||
@ -134,42 +136,6 @@ def etudiant(etudid: int = None, nip: str = None, ine: str = None):
|
||||
return etud.to_dict_api()
|
||||
|
||||
|
||||
@api_web_bp.route("/etudiant/etudid/<int:etudid>/photo")
|
||||
@api_web_bp.route("/etudiant/nip/<string:nip>/photo")
|
||||
@api_web_bp.route("/etudiant/ine/<string:ine>/photo")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def get_photo_image(etudid: int = None, nip: str = None, ine: str = None):
|
||||
"""
|
||||
Retourne la photo de l'étudiant
|
||||
correspondant ou un placeholder si non existant.
|
||||
|
||||
etudid : l'etudid de l'étudiant
|
||||
nip : le code nip de l'étudiant
|
||||
ine : le code ine de l'étudiant
|
||||
|
||||
Attention : Ne peut être qu'utilisée en tant que route de département
|
||||
"""
|
||||
|
||||
etud = tools.get_etud(etudid, nip, ine)
|
||||
|
||||
if etud is None:
|
||||
return json_error(
|
||||
404,
|
||||
message="étudiant inconnu",
|
||||
)
|
||||
if not etudid:
|
||||
filename = sco_photos.UNKNOWN_IMAGE_PATH
|
||||
|
||||
size = request.args.get("size", "orig")
|
||||
filename = sco_photos.photo_pathname(etud.photo_filename, size=size)
|
||||
if not filename:
|
||||
filename = sco_photos.UNKNOWN_IMAGE_PATH
|
||||
res = sco_photos.build_image_response(filename)
|
||||
return res
|
||||
|
||||
|
||||
@bp.route("/etudiants/etudid/<int:etudid>", methods=["GET"])
|
||||
@bp.route("/etudiants/nip/<string:nip>", methods=["GET"])
|
||||
@bp.route("/etudiants/ine/<string:ine>", methods=["GET"])
|
||||
@ -201,12 +167,39 @@ def etudiants(etudid: int = None, nip: str = None, ine: str = None):
|
||||
)
|
||||
if not None in allowed_depts:
|
||||
# restreint aux départements autorisés:
|
||||
etuds = etuds.join(Departement).filter(
|
||||
query = query.join(Departement).filter(
|
||||
or_(Departement.acronym == acronym for acronym in allowed_depts)
|
||||
)
|
||||
return [etud.to_dict_api() for etud in query]
|
||||
|
||||
|
||||
@bp.route("/etudiants/name/<string:start>")
|
||||
@api_web_bp.route("/etudiants/name/<string:start>")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def etudiants_by_name(start: str = "", min_len=3, limit=32):
|
||||
"""Liste des étudiants dont le nom débute par start.
|
||||
Si start fait moins de min_len=3 caractères, liste vide.
|
||||
La casse et les accents sont ignorés.
|
||||
"""
|
||||
if len(start) < min_len:
|
||||
return []
|
||||
start = suppress_accents(start).lower()
|
||||
query = Identite.query.filter(
|
||||
func.lower(func.unaccent(Identite.nom, type_=VARCHAR)).ilike(start + "%")
|
||||
)
|
||||
allowed_depts = current_user.get_depts_with_permission(Permission.ScoView)
|
||||
if not None in allowed_depts:
|
||||
# restreint aux départements autorisés:
|
||||
query = query.join(Departement).filter(
|
||||
or_(Departement.acronym == acronym for acronym in allowed_depts)
|
||||
)
|
||||
etuds = query.order_by(Identite.nom, Identite.prenom).limit(limit)
|
||||
# Note: on raffine le tri pour les caractères spéciaux et nom usuel ici:
|
||||
return [etud.to_dict_api() for etud in sorted(etuds, key=attrgetter("sort_key"))]
|
||||
|
||||
|
||||
@bp.route("/etudiant/etudid/<int:etudid>/formsemestres")
|
||||
@bp.route("/etudiant/nip/<string:nip>/formsemestres")
|
||||
@bp.route("/etudiant/ine/<string:ine>/formsemestres")
|
||||
|
@ -8,7 +8,7 @@
|
||||
ScoDoc 9 API : accès aux évaluations
|
||||
"""
|
||||
|
||||
from flask import g
|
||||
from flask import g, request
|
||||
from flask_json import as_json
|
||||
from flask_login import login_required
|
||||
|
||||
@ -17,7 +17,7 @@ import app
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import Evaluation, ModuleImpl, FormSemestre
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_evaluation_db, sco_saisie_notes
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
@ -28,7 +28,7 @@ import app.scodoc.sco_utils as scu
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def the_eval(evaluation_id: int):
|
||||
def evaluation(evaluation_id: int):
|
||||
"""Description d'une évaluation.
|
||||
|
||||
{
|
||||
@ -93,24 +93,22 @@ def evaluations(moduleimpl_id: int):
|
||||
@as_json
|
||||
def evaluation_notes(evaluation_id: int):
|
||||
"""
|
||||
Retourne la liste des notes à partir de l'id d'une évaluation donnée
|
||||
Retourne la liste des notes de l'évaluation
|
||||
|
||||
evaluation_id : l'id d'une évaluation
|
||||
evaluation_id : l'id de l'évaluation
|
||||
|
||||
Exemple de résultat :
|
||||
{
|
||||
"1": {
|
||||
"id": 1,
|
||||
"etudid": 10,
|
||||
"11": {
|
||||
"etudid": 11,
|
||||
"evaluation_id": 1,
|
||||
"value": 15.0,
|
||||
"comment": "",
|
||||
"date": "Wed, 20 Apr 2022 06:49:05 GMT",
|
||||
"uid": 2
|
||||
},
|
||||
"2": {
|
||||
"id": 2,
|
||||
"etudid": 1,
|
||||
"12": {
|
||||
"etudid": 12,
|
||||
"evaluation_id": 1,
|
||||
"value": 12.0,
|
||||
"comment": "",
|
||||
@ -128,8 +126,8 @@ def evaluation_notes(evaluation_id: int):
|
||||
.filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
|
||||
the_eval = query.first_or_404()
|
||||
dept = the_eval.moduleimpl.formsemestre.departement
|
||||
evaluation = query.first_or_404()
|
||||
dept = evaluation.moduleimpl.formsemestre.departement
|
||||
app.set_sco_dept(dept.acronym)
|
||||
|
||||
notes = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
|
||||
@ -137,7 +135,49 @@ def evaluation_notes(evaluation_id: int):
|
||||
# "ABS", "EXC", etc mais laisse les notes sur le barème de l'éval.
|
||||
note = notes[etudid]
|
||||
note["value"] = scu.fmt_note(note["value"], keep_numeric=True)
|
||||
note["note_max"] = the_eval.note_max
|
||||
note["note_max"] = evaluation.note_max
|
||||
del note["id"]
|
||||
|
||||
return notes
|
||||
# in JS, keys must be string, not integers
|
||||
return {str(etudid): note for etudid, note in notes.items()}
|
||||
|
||||
|
||||
@bp.route("/evaluation/<int:evaluation_id>/notes/set", methods=["POST"])
|
||||
@api_web_bp.route("/evaluation/<int:evaluation_id>/notes/set", methods=["POST"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoEnsView)
|
||||
@as_json
|
||||
def evaluation_set_notes(evaluation_id: int):
|
||||
"""Écriture de notes dans une évaluation.
|
||||
The request content type should be "application/json",
|
||||
and contains:
|
||||
{
|
||||
'notes' : [ [etudid, value], ... ],
|
||||
'comment' : optional string
|
||||
}
|
||||
Result:
|
||||
- nb_changed: nombre de notes changées
|
||||
- nb_suppress: nombre de notes effacées
|
||||
- etudids_with_decision: liste des etudiants dont la note a changé
|
||||
alors qu'ils ont une décision de jury enregistrée.
|
||||
"""
|
||||
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)
|
||||
)
|
||||
evaluation = query.first_or_404()
|
||||
dept = evaluation.moduleimpl.formsemestre.departement
|
||||
app.set_sco_dept(dept.acronym)
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
notes = data.get("notes")
|
||||
if notes is None:
|
||||
return scu.json_error(404, "no notes")
|
||||
if not isinstance(notes, list):
|
||||
return scu.json_error(404, "invalid notes argument (must be a list)")
|
||||
return sco_saisie_notes.save_notes(
|
||||
evaluation, notes, comment=data.get("comment", "")
|
||||
)
|
||||
|
323
app/api/jury.py
323
app/api/jury.py
@ -5,19 +5,38 @@
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
ScoDoc 9 API : jury WIP
|
||||
ScoDoc 9 API : jury WIP à compléter avec enregistrement décisions
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
from flask import flash, g, request, url_for
|
||||
from flask_json import as_json
|
||||
from flask_login import login_required
|
||||
from flask_login import current_user, login_required
|
||||
|
||||
import app
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app import db, log
|
||||
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR, tools
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.scodoc.sco_exceptions import ScoException
|
||||
from app.but import jury_but_results
|
||||
from app.models import FormSemestre
|
||||
from app.models import (
|
||||
ApcParcours,
|
||||
ApcValidationAnnee,
|
||||
ApcValidationRCUE,
|
||||
Formation,
|
||||
FormSemestre,
|
||||
Identite,
|
||||
ScolarAutorisationInscription,
|
||||
ScolarFormSemestreValidation,
|
||||
ScolarNews,
|
||||
Scolog,
|
||||
UniteEns,
|
||||
)
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_utils import json_error
|
||||
|
||||
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/decisions_jury")
|
||||
@ -29,10 +48,304 @@ from app.scodoc.sco_permissions import Permission
|
||||
def decisions_jury(formsemestre_id: int):
|
||||
"""Décisions du jury des étudiants du formsemestre."""
|
||||
# APC, pair:
|
||||
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
|
||||
if formsemestre.formation.is_apc():
|
||||
app.set_sco_dept(formsemestre.departement.acronym)
|
||||
rows = jury_but_results.get_jury_but_results(formsemestre)
|
||||
return rows
|
||||
else:
|
||||
raise ScoException("non implemente")
|
||||
|
||||
|
||||
def _news_delete_jury_etud(etud: Identite):
|
||||
"génère news sur effacement décision"
|
||||
# n'utilise pas g.scodoc_dept, pas toujours dispo en mode API
|
||||
url = url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=etud.departement.acronym, etudid=etud.id
|
||||
)
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_JURY,
|
||||
obj=etud.id,
|
||||
text=f"""Suppression décision jury pour <a href="{url}">{etud.nomprenom}</a>""",
|
||||
url=url,
|
||||
)
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_ue/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_ue/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def validation_ue_delete(etudid: int, validation_id: int):
|
||||
"Efface cette validation"
|
||||
return _validation_ue_delete(etudid, validation_id)
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_formsemestre/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_formsemestre/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def validation_formsemestre_delete(etudid: int, validation_id: int):
|
||||
"Efface cette validation"
|
||||
# c'est la même chose (formations classiques)
|
||||
return _validation_ue_delete(etudid, validation_id)
|
||||
|
||||
|
||||
def _validation_ue_delete(etudid: int, validation_id: int):
|
||||
"Efface cette validation (semestres classiques ou UEs)"
|
||||
etud = tools.get_etud(etudid)
|
||||
if etud is None:
|
||||
return "étudiant inconnu", 404
|
||||
validation = ScolarFormSemestreValidation.query.filter_by(
|
||||
id=validation_id, etudid=etudid
|
||||
).first_or_404()
|
||||
# Vérification de la permission:
|
||||
# A le droit de supprimer cette validation: le chef de dept ou quelqu'un ayant
|
||||
# le droit de saisir des décisions de jury dans le formsemestre concerné s'il y en a un
|
||||
# (c'est le cas pour les validations de jury, mais pas pour les "antérieures" non
|
||||
# rattachées à un formsemestre)
|
||||
if not g.scodoc_dept: # accès API
|
||||
if not current_user.has_permission(Permission.ScoEtudInscrit):
|
||||
return json_error(403, "opération non autorisée (117)")
|
||||
else:
|
||||
if validation.formsemestre:
|
||||
if (
|
||||
validation.formsemestre.dept_id != g.scodoc_dept_id
|
||||
) or not validation.formsemestre.can_edit_jury():
|
||||
return json_error(403, "opération non autorisée (123)")
|
||||
elif not current_user.has_permission(Permission.ScoEtudInscrit):
|
||||
# Validation non rattachée à un semestre: on doit être chef
|
||||
return json_error(403, "opération non autorisée (126)")
|
||||
|
||||
log(f"validation_ue_delete: etuid={etudid} {validation}")
|
||||
db.session.delete(validation)
|
||||
sco_cache.invalidate_formsemestre_etud(etud)
|
||||
db.session.commit()
|
||||
_news_delete_jury_etud(etud)
|
||||
return "ok"
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/etudiant/<int:etudid>/jury/autorisation_inscription/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/<int:etudid>/jury/autorisation_inscription/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoEtudInscrit)
|
||||
@as_json
|
||||
def autorisation_inscription_delete(etudid: int, validation_id: int):
|
||||
"Efface cette validation"
|
||||
etud = tools.get_etud(etudid)
|
||||
if etud is None:
|
||||
return "étudiant inconnu", 404
|
||||
validation = ScolarAutorisationInscription.query.filter_by(
|
||||
id=validation_id, etudid=etudid
|
||||
).first_or_404()
|
||||
log(f"autorisation_inscription_delete: etuid={etudid} {validation}")
|
||||
db.session.delete(validation)
|
||||
sco_cache.invalidate_formsemestre_etud(etud)
|
||||
db.session.commit()
|
||||
_news_delete_jury_etud(etud)
|
||||
return "ok"
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_rcue/record",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_rcue/record",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoEtudInscrit)
|
||||
@as_json
|
||||
def validation_rcue_record(etudid: int):
|
||||
"""Enregistre une validation de RCUE.
|
||||
Si une validation existe déjà pour ce RCUE, la remplace.
|
||||
The request content type should be "application/json":
|
||||
{
|
||||
"code" : str,
|
||||
"ue1_id" : int,
|
||||
"ue2_id" : int,
|
||||
// Optionnel:
|
||||
"formsemestre_id" : int,
|
||||
"date" : date_iso, // si non spécifié, now()
|
||||
"parcours_id" :int,
|
||||
}
|
||||
"""
|
||||
etud = tools.get_etud(etudid)
|
||||
if etud is None:
|
||||
return json_error(404, "étudiant inconnu")
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
code = data.get("code")
|
||||
if code is None:
|
||||
return json_error(API_CLIENT_ERROR, "missing argument: code")
|
||||
if code not in codes_cursus.CODES_JURY_RCUE:
|
||||
return json_error(API_CLIENT_ERROR, "invalid code value")
|
||||
ue1_id = data.get("ue1_id")
|
||||
if ue1_id is None:
|
||||
return json_error(API_CLIENT_ERROR, "missing argument: ue1_id")
|
||||
try:
|
||||
ue1_id = int(ue1_id)
|
||||
except ValueError:
|
||||
return json_error(API_CLIENT_ERROR, "invalid value for ue1_id")
|
||||
ue2_id = data.get("ue2_id")
|
||||
if ue2_id is None:
|
||||
return json_error(API_CLIENT_ERROR, "missing argument: ue2_id")
|
||||
try:
|
||||
ue2_id = int(ue2_id)
|
||||
except ValueError:
|
||||
return json_error(API_CLIENT_ERROR, "invalid value for ue2_id")
|
||||
formsemestre_id = data.get("formsemestre_id")
|
||||
date_validation_str = data.get("date", datetime.datetime.now().isoformat())
|
||||
parcours_id = data.get("parcours_id")
|
||||
#
|
||||
query = UniteEns.query.filter_by(id=ue1_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
ue1: UniteEns = query.first_or_404()
|
||||
query = UniteEns.query.filter_by(id=ue2_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
ue2: UniteEns = query.first_or_404()
|
||||
if ue1.niveau_competence_id != ue2.niveau_competence_id:
|
||||
return json_error(
|
||||
API_CLIENT_ERROR, "UEs non associees au meme niveau de competence"
|
||||
)
|
||||
if formsemestre_id is not None:
|
||||
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()
|
||||
if (formsemestre.formation_id != ue1.formation_id) or (
|
||||
formsemestre.formation_id != ue2.formation_id
|
||||
):
|
||||
return json_error(
|
||||
API_CLIENT_ERROR, "ues et semestre ne sont pas de la meme formation"
|
||||
)
|
||||
else:
|
||||
formsemestre = None
|
||||
try:
|
||||
date_validation = datetime.datetime.fromisoformat(date_validation_str)
|
||||
except ValueError:
|
||||
return json_error(API_CLIENT_ERROR, "invalid date string")
|
||||
if parcours_id is not None:
|
||||
parcours: ApcParcours = ApcParcours.query.get_or_404(parcours_id)
|
||||
if parcours.referentiel_id != ue1.niveau_competence.competence.referentiel_id:
|
||||
return json_error(API_CLIENT_ERROR, "niveau et parcours incompatibles")
|
||||
|
||||
# Une validation pour ce niveau de compétence existe-elle ?
|
||||
validation = (
|
||||
ApcValidationRCUE.query.filter_by(etudid=etudid)
|
||||
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue2_id)
|
||||
.filter_by(niveau_competence_id=ue2.niveau_competence_id)
|
||||
.first()
|
||||
)
|
||||
if validation:
|
||||
validation.code = code
|
||||
validation.date = date_validation
|
||||
validation.formsemestre_id = formsemestre_id
|
||||
validation.parcours_id = parcours_id
|
||||
validation.ue1_id = ue1_id
|
||||
validation.ue2_id = ue2_id
|
||||
operation = "update"
|
||||
else:
|
||||
validation = ApcValidationRCUE(
|
||||
code=code,
|
||||
date=date_validation,
|
||||
etudid=etudid,
|
||||
formsemestre_id=formsemestre_id,
|
||||
parcours_id=parcours_id,
|
||||
ue1_id=ue1_id,
|
||||
ue2_id=ue2_id,
|
||||
)
|
||||
operation = "record"
|
||||
db.session.add(validation)
|
||||
# invalider bulletins (les autres résultats ne dépendent pas des RCUEs):
|
||||
sco_cache.invalidate_formsemestre_etud(etud)
|
||||
db.session.commit()
|
||||
Scolog.logdb(
|
||||
method="validation_rcue_record",
|
||||
etudid=etudid,
|
||||
msg=f"Enregistrement {validation}",
|
||||
commit=True,
|
||||
)
|
||||
log(f"{operation} {validation}")
|
||||
return validation.to_dict()
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_rcue/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_rcue/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoEtudInscrit)
|
||||
@as_json
|
||||
def validation_rcue_delete(etudid: int, validation_id: int):
|
||||
"Efface cette validation"
|
||||
etud = tools.get_etud(etudid)
|
||||
if etud is None:
|
||||
return "étudiant inconnu", 404
|
||||
validation = ApcValidationRCUE.query.filter_by(
|
||||
id=validation_id, etudid=etudid
|
||||
).first_or_404()
|
||||
log(f"validation_ue_delete: etuid={etudid} {validation}")
|
||||
db.session.delete(validation)
|
||||
sco_cache.invalidate_formsemestre_etud(etud)
|
||||
db.session.commit()
|
||||
_news_delete_jury_etud(etud)
|
||||
return "ok"
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_annee_but/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_annee_but/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoEtudInscrit)
|
||||
@as_json
|
||||
def validation_annee_but_delete(etudid: int, validation_id: int):
|
||||
"Efface cette validation"
|
||||
etud = tools.get_etud(etudid)
|
||||
if etud is None:
|
||||
return "étudiant inconnu", 404
|
||||
validation = ApcValidationAnnee.query.filter_by(
|
||||
id=validation_id, etudid=etudid
|
||||
).first_or_404()
|
||||
log(f"validation_annee_but: etuid={etudid} {validation}")
|
||||
db.session.delete(validation)
|
||||
sco_cache.invalidate_formsemestre_etud(etud)
|
||||
db.session.commit()
|
||||
_news_delete_jury_etud(etud)
|
||||
return "ok"
|
||||
|
@ -1,596 +0,0 @@
|
||||
##############################################################################
|
||||
# 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, current_user
|
||||
|
||||
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 compute_assiduites_justified
|
||||
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
|
||||
|
||||
|
||||
# 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",
|
||||
"user_id": 1 or null,
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
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
|
||||
user_id (l'id de l'auteur du justificatif)
|
||||
query?user_id=[int]
|
||||
ex query?user_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)
|
||||
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
|
||||
compute_assiduites_justified(Justificatif.query.filter_by(etudid=etudid), True)
|
||||
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,
|
||||
user_id=current_user.id,
|
||||
)
|
||||
|
||||
db.session.add(nouv_justificatif)
|
||||
db.session.commit()
|
||||
|
||||
return (
|
||||
200,
|
||||
{
|
||||
"justif_id": nouv_justificatif.id,
|
||||
"couverture": scass.justifies(nouv_justificatif),
|
||||
},
|
||||
)
|
||||
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)
|
||||
avant_ids: list[int] = scass.justifies(justificatif_unique)
|
||||
# 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.replace(" ", "+"), 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.replace(" ", "+"), 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 ")
|
||||
|
||||
# Mise à jour des dates
|
||||
deb = deb if deb is not None else justificatif_unique.date_debut
|
||||
fin = fin if fin is not None else justificatif_unique.date_fin
|
||||
|
||||
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(
|
||||
{
|
||||
"couverture": {
|
||||
"avant": avant_ids,
|
||||
"après": compute_assiduites_justified(
|
||||
Justificatif.query.filter_by(etudid=justificatif_unique.etudid),
|
||||
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)
|
||||
|
||||
compute_assiduites_justified(
|
||||
Justificatif.query.filter_by(etudid=justificatif_unique.etudid), True
|
||||
)
|
||||
|
||||
return (200, "OK")
|
||||
|
||||
|
||||
# Partie archivage
|
||||
@bp.route("/justificatif/<int:justif_id>/import", methods=["POST"])
|
||||
@api_web_bp.route("/justificatif/<int:justif_id>/import", 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/<int:justif_id>/export/<filename>", methods=["POST"])
|
||||
@api_web_bp.route("/justificatif/<int:justif_id>/export/<filename>", methods=["POST"])
|
||||
@scodoc
|
||||
@login_required
|
||||
@permission_required(Permission.ScoView)
|
||||
# @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/<int:justif_id>/remove", methods=["POST"])
|
||||
@api_web_bp.route("/justificatif/<int:justif_id>/remove", 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/<int:justif_id>/list", methods=["GET"])
|
||||
@api_web_bp.route("/justificatif/<int:justif_id>/list", 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/<int:justif_id>/justifies", methods=["GET"])
|
||||
@api_web_bp.route("/justificatif/<int:justif_id>/justifies", methods=["GET"])
|
||||
@scodoc
|
||||
@login_required
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def justif_justifies(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", "").replace(" ", "+")
|
||||
deb: datetime = scu.is_iso_formated(deb, True)
|
||||
|
||||
# cas 3 : date de fin
|
||||
fin = requested.args.get("date_fin", "").replace(" ", "+")
|
||||
fin = scu.is_iso_formated(fin, True)
|
||||
|
||||
if (deb, fin) != (None, None):
|
||||
justificatifs_query: Justificatif = scass.filter_by_date(
|
||||
justificatifs_query, Justificatif, deb, fin
|
||||
)
|
||||
|
||||
user_id = requested.args.get("user_id", False)
|
||||
if user_id is not False:
|
||||
justificatif_query: Justificatif = scass.filter_by_user_id(
|
||||
justificatif_query, user_id
|
||||
)
|
||||
|
||||
return justificatifs_query
|
@ -12,6 +12,8 @@ from operator import attrgetter
|
||||
from flask import g, request
|
||||
from flask_json import as_json
|
||||
from flask_login import login_required
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
import app
|
||||
from app import db, log
|
||||
@ -23,6 +25,7 @@ from app.models import GroupDescr, Partition, Scolog
|
||||
from app.models.groups import group_membership
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
@ -182,10 +185,12 @@ def set_etud_group(etudid: int, group_id: int):
|
||||
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")
|
||||
|
||||
sco_groups.change_etud_group_in_partition(
|
||||
etudid, group_id, group.partition.to_dict()
|
||||
)
|
||||
|
||||
try:
|
||||
sco_groups.change_etud_group_in_partition(etudid, group)
|
||||
except ScoValueError as exc:
|
||||
return json_error(404, exc.args[0])
|
||||
except IntegrityError:
|
||||
return json_error(404, "échec de l'enregistrement")
|
||||
return {"group_id": group_id, "etudid": etudid}
|
||||
|
||||
|
||||
@ -244,19 +249,25 @@ def partition_remove_etud(partition_id: int, etudid: int):
|
||||
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)
|
||||
.filter_by(etudid=etudid)
|
||||
|
||||
db.session.execute(
|
||||
sa.text(
|
||||
"""DELETE FROM group_membership
|
||||
WHERE etudid=:etudid
|
||||
and group_id IN (
|
||||
SELECT id FROM group_descr WHERE partition_id = :partition_id
|
||||
);
|
||||
"""
|
||||
),
|
||||
{"etudid": etudid, "partition_id": partition_id},
|
||||
)
|
||||
|
||||
Scolog.logdb(
|
||||
method="partition_remove_etud",
|
||||
etudid=etud.id,
|
||||
msg=f"Retrait de la partition {partition.partition_name}",
|
||||
commit=False,
|
||||
)
|
||||
for group in groups:
|
||||
group.etuds.remove(etud)
|
||||
Scolog.logdb(
|
||||
method="partition_remove_etud",
|
||||
etudid=etud.id,
|
||||
msg=f"Retrait du groupe {group.group_name} de {group.partition.partition_name}",
|
||||
commit=True,
|
||||
)
|
||||
db.session.commit()
|
||||
# Update parcours
|
||||
partition.formsemestre.update_inscriptions_parcours_from_groups()
|
||||
@ -271,7 +282,7 @@ def partition_remove_etud(partition_id: int, etudid: int):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoEtudChangeGroups)
|
||||
@as_json
|
||||
def group_create(partition_id: int):
|
||||
def group_create(partition_id: int): # partition-group-create
|
||||
"""Création d'un groupe dans une partition
|
||||
|
||||
The request content type should be "application/json":
|
||||
|
@ -35,7 +35,7 @@ def user_info(uid: int):
|
||||
"""
|
||||
Info sur un compte utilisateur scodoc
|
||||
"""
|
||||
user: User = User.query.get(uid)
|
||||
user: User = db.session.get(User, uid)
|
||||
if user is None:
|
||||
return json_error(404, "user not found")
|
||||
if g.scodoc_dept:
|
||||
|
@ -9,7 +9,7 @@ from flask import current_app, g, redirect, request, url_for
|
||||
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth
|
||||
import flask_login
|
||||
|
||||
from app import login
|
||||
from app import db, login
|
||||
from app.auth.models import User
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
from app.scodoc.sco_utils import json_error
|
||||
@ -39,7 +39,7 @@ def basic_auth_error(status):
|
||||
@login.user_loader
|
||||
def load_user(uid: str) -> User:
|
||||
"flask-login: accès à un utilisateur"
|
||||
return User.query.get(int(uid))
|
||||
return db.session.get(User, int(uid))
|
||||
|
||||
|
||||
@token_auth.verify_token
|
||||
|
@ -225,7 +225,7 @@ class User(UserMixin, db.Model):
|
||||
return None
|
||||
except (TypeError, KeyError):
|
||||
return None
|
||||
return User.query.get(user_id)
|
||||
return db.session.get(User, user_id)
|
||||
|
||||
def to_dict(self, include_email=True):
|
||||
"""l'utilisateur comme un dict, avec des champs supplémentaires"""
|
||||
@ -376,7 +376,9 @@ class User(UserMixin, db.Model):
|
||||
"""
|
||||
if not isinstance(role, Role):
|
||||
raise ScoValueError("add_role: rôle invalide")
|
||||
self.user_roles.append(UserRole(user=self, role=role, dept=dept))
|
||||
user_role = UserRole(user=self, role=role, dept=dept)
|
||||
db.session.add(user_role)
|
||||
self.user_roles.append(user_role)
|
||||
|
||||
def add_roles(self, roles: "list[Role]", dept: str):
|
||||
"""Add roles to this user.
|
||||
|
@ -12,6 +12,7 @@ import datetime
|
||||
import numpy as np
|
||||
from flask import g, has_request_context, url_for
|
||||
|
||||
from app import db
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.models import Evaluation, FormSemestre, Identite
|
||||
from app.models.groups import GroupDescr
|
||||
@ -158,7 +159,7 @@ class BulletinBUT:
|
||||
[etud.id]
|
||||
].iterrows():
|
||||
if codes_cursus.code_ue_validant(ue_capitalisee.code):
|
||||
ue = UniteEns.query.get(ue_capitalisee.ue_id) # XXX cacher ?
|
||||
ue = db.session.get(UniteEns, ue_capitalisee.ue_id) # XXX cacher ?
|
||||
# déjà capitalisé ? montre la meilleure
|
||||
if ue.acronyme in d:
|
||||
moy_cap = d[ue.acronyme]["moyenne_num"] or 0.0
|
||||
|
@ -189,7 +189,9 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
||||
moy_ue = moy_ue.get("value", "-") if moy_ue is not None else "-"
|
||||
t = {
|
||||
"titre": f"{ue_acronym} - {ue['titre']}",
|
||||
"moyenne": Paragraph(f"""<para align=right><b>{moy_ue}</b></para>"""),
|
||||
"moyenne": Paragraph(
|
||||
f"""<para align=right><b>{moy_ue or "-"}</b></para>"""
|
||||
),
|
||||
"_css_row_class": "note_bold",
|
||||
"_pdf_row_markup": ["b"],
|
||||
"_pdf_style": [
|
||||
|
@ -14,17 +14,14 @@ Classe raccordant avec ScoDoc 7:
|
||||
|
||||
"""
|
||||
import collections
|
||||
from typing import Union
|
||||
from operator import attrgetter
|
||||
|
||||
from flask import g, url_for
|
||||
|
||||
from app import db
|
||||
from app import log
|
||||
from app import db, log
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
|
||||
from app.comp import res_sem
|
||||
|
||||
from app.models.but_refcomp import (
|
||||
ApcAnneeParcours,
|
||||
ApcCompetence,
|
||||
@ -37,7 +34,6 @@ from app.models import Scolog, ScolarAutorisationInscription
|
||||
from app.models.but_validations import (
|
||||
ApcValidationAnnee,
|
||||
ApcValidationRCUE,
|
||||
RegroupementCoherentUE,
|
||||
)
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.formations import Formation
|
||||
@ -45,7 +41,8 @@ from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
||||
from app.models.ues import UniteEns
|
||||
from app.models.validations import ScolarFormSemestreValidation
|
||||
from app.scodoc import codes_cursus as sco_codes
|
||||
from app.scodoc.codes_cursus import RED, UE_STANDARD
|
||||
from app.scodoc.codes_cursus import code_ue_validant, CODES_UE_VALIDES, UE_STANDARD
|
||||
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
|
||||
|
||||
@ -72,6 +69,7 @@ class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
|
||||
class EtudCursusBUT:
|
||||
"""L'état de l'étudiant dans son cursus BUT
|
||||
Liste des niveaux validés/à valider
|
||||
(utilisé pour le résumé sur la fiche étudiant)
|
||||
"""
|
||||
|
||||
def __init__(self, etud: Identite, formation: Formation):
|
||||
@ -103,8 +101,8 @@ class EtudCursusBUT:
|
||||
"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_by_annee: dict[int, list[ApcNiveau]] = {}
|
||||
"{ annee:int : liste des niveaux à valider }"
|
||||
self.niveaux: dict[int, ApcNiveau] = {}
|
||||
"cache les niveaux"
|
||||
for annee in (1, 2, 3):
|
||||
@ -118,21 +116,6 @@ class EtudCursusBUT:
|
||||
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 (la "meilleure"), ... } }"""
|
||||
@ -145,8 +128,8 @@ class EtudCursusBUT:
|
||||
).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"]]
|
||||
sco_codes.BUT_CODES_ORDER[validation_rcue.code]
|
||||
> sco_codes.BUT_CODES_ORDER[previous_validation.code]
|
||||
):
|
||||
self.validation_par_competence_et_annee[niveau.competence.id][
|
||||
niveau.annee
|
||||
@ -206,6 +189,28 @@ class EtudCursusBUT:
|
||||
)
|
||||
return d
|
||||
|
||||
def competence_annee_has_niveau(self, competence_id: int, annee: str) -> bool:
|
||||
"vrai si la compétence à un niveau dans cette annee ('BUT1') pour le parcour de cet etud"
|
||||
# slow, utile pour affichage fiche
|
||||
return annee in [n.annee for n in self.competences[competence_id].niveaux]
|
||||
|
||||
def load_validation_by_niveau(self) -> dict[int, list[ApcValidationRCUE]]:
|
||||
"""Cherche les validations de jury enregistrées pour chaque niveau
|
||||
Résultat: { niveau_id : [ ApcValidationRCUE ] }
|
||||
meilleure validation pour ce niveau
|
||||
"""
|
||||
validations_by_niveau = collections.defaultdict(lambda: [])
|
||||
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=self.etud):
|
||||
validations_by_niveau[validation_rcue.niveau().id].append(validation_rcue)
|
||||
validation_by_niveau = {
|
||||
niveau_id: sorted(
|
||||
validations, key=lambda v: sco_codes.BUT_CODES_ORDER[v.code]
|
||||
)[0]
|
||||
for niveau_id, validations in validations_by_niveau.items()
|
||||
if validations
|
||||
}
|
||||
return validation_by_niveau
|
||||
|
||||
|
||||
class FormSemestreCursusBUT:
|
||||
"""L'état des étudiants d'un formsemestre dans leur cursus BUT
|
||||
@ -246,7 +251,9 @@ class FormSemestreCursusBUT:
|
||||
parcour = None
|
||||
else:
|
||||
if parcour_id not in self.parcours_by_id:
|
||||
self.parcours_by_id[parcour_id] = ApcParcours.query.get(parcour_id)
|
||||
self.parcours_by_id[parcour_id] = db.session.get(
|
||||
ApcParcours, parcour_id
|
||||
)
|
||||
parcour = self.parcours_by_id[parcour_id]
|
||||
|
||||
return self.get_niveaux_parcours_by_annee(parcour)
|
||||
@ -303,8 +310,8 @@ class FormSemestreCursusBUT:
|
||||
).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"]]
|
||||
sco_codes.BUT_CODES_ORDER[validation_rcue.code]
|
||||
> sco_codes.BUT_CODES_ORDER[previous_validation["code"]]
|
||||
):
|
||||
self.validation_par_competence_et_annee[niveau.competence.id][
|
||||
niveau.annee
|
||||
@ -340,8 +347,8 @@ class FormSemestreCursusBUT:
|
||||
).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"]]
|
||||
sco_codes.BUT_CODES_ORDER[validation_rcue.code]
|
||||
> sco_codes.BUT_CODES_ORDER[previous_validation["code"]]
|
||||
):
|
||||
self.validation_par_competence_et_annee[niveau.competence.id][
|
||||
niveau.annee
|
||||
@ -358,6 +365,66 @@ class FormSemestreCursusBUT:
|
||||
"cache { competence_id : competence }"
|
||||
|
||||
|
||||
def but_ects_valides(etud: Identite, referentiel_competence_id: int) -> float:
|
||||
"""Nombre d'ECTS validés par etud dans le BUT de référentiel indiqué.
|
||||
Ne prend que les UE associées à des niveaux de compétences,
|
||||
et ne les compte qu'une fois même en cas de redoublement avec re-validation.
|
||||
"""
|
||||
validations = (
|
||||
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
|
||||
.filter(ScolarFormSemestreValidation.ue_id != None)
|
||||
.join(UniteEns)
|
||||
.join(ApcNiveau)
|
||||
.join(ApcCompetence)
|
||||
.filter_by(referentiel_id=referentiel_competence_id)
|
||||
)
|
||||
|
||||
ects_dict = {}
|
||||
for v in validations:
|
||||
key = (v.ue.semestre_idx, v.ue.niveau_competence.id)
|
||||
if v.code in CODES_UE_VALIDES:
|
||||
ects_dict[key] = v.ue.ects
|
||||
|
||||
return sum(ects_dict.values()) if ects_dict else 0.0
|
||||
|
||||
|
||||
def etud_ues_de_but1_non_validees(
|
||||
etud: Identite, formation: Formation, parcour: ApcParcours
|
||||
) -> list[UniteEns]:
|
||||
"""Liste des UEs de S1 et S2 non validées, dans son parcours"""
|
||||
# Les UEs avec décisions, dans les S1 ou S2 d'une formation de même code:
|
||||
validations = (
|
||||
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
|
||||
.filter(ScolarFormSemestreValidation.ue_id != None)
|
||||
.join(UniteEns)
|
||||
.filter(db.or_(UniteEns.semestre_idx == 1, UniteEns.semestre_idx == 2))
|
||||
.join(Formation)
|
||||
.filter_by(formation_code=formation.formation_code)
|
||||
)
|
||||
codes_validations_by_ue_code = collections.defaultdict(list)
|
||||
for v in validations:
|
||||
codes_validations_by_ue_code[v.ue.ue_code].append(v.code)
|
||||
|
||||
# Les UEs du parcours en S1 et S2:
|
||||
ues = formation.query_ues_parcour(parcour).filter(
|
||||
db.or_(UniteEns.semestre_idx == 1, UniteEns.semestre_idx == 2)
|
||||
)
|
||||
# Liste triée des ues non validées
|
||||
return sorted(
|
||||
[
|
||||
ue
|
||||
for ue in ues
|
||||
if not any(
|
||||
(
|
||||
code_ue_validant(code)
|
||||
for code in codes_validations_by_ue_code[ue.ue_code]
|
||||
)
|
||||
)
|
||||
],
|
||||
key=attrgetter("numero", "acronyme"),
|
||||
)
|
||||
|
||||
|
||||
def formsemestre_warning_apc_setup(
|
||||
formsemestre: FormSemestre, res: ResultatsSemestreBUT
|
||||
) -> str:
|
||||
@ -413,3 +480,122 @@ def formsemestre_warning_apc_setup(
|
||||
</p>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def ue_associee_au_niveau_du_parcours(
|
||||
ues_possibles: list[UniteEns], niveau: ApcNiveau, sem_name: str = "S"
|
||||
) -> UniteEns:
|
||||
"L'UE associée à ce niveau, ou None"
|
||||
ues = [ue for ue in ues_possibles if ue.niveau_competence_id == niveau.id]
|
||||
if len(ues) > 1:
|
||||
# plusieurs UEs associées à ce niveau: élimine celles sans parcours
|
||||
ues_pair_avec_parcours = [ue for ue in ues if ue.parcours]
|
||||
if ues_pair_avec_parcours:
|
||||
ues = ues_pair_avec_parcours
|
||||
if len(ues) > 1:
|
||||
log(f"_niveau_ues: {len(ues)} associées au niveau {niveau} / {sem_name}")
|
||||
return ues[0] if ues else None
|
||||
|
||||
|
||||
def parcour_formation_competences(parcour: ApcParcours, formation: Formation) -> list:
|
||||
"""
|
||||
[
|
||||
{
|
||||
'competence' : ApcCompetence,
|
||||
'niveaux' : {
|
||||
1 : { ... },
|
||||
2 : { ... },
|
||||
3 : {
|
||||
'niveau' : ApcNiveau,
|
||||
'ue_impair' : UniteEns, # actuellement associée
|
||||
'ues_impair' : list[UniteEns], # choix possibles
|
||||
'ue_pair' : UniteEns,
|
||||
'ues_pair' : list[UniteEns],
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
"""
|
||||
refcomp: ApcReferentielCompetences = formation.referentiel_competence
|
||||
|
||||
def _niveau_ues(competence: ApcCompetence, annee: int) -> dict:
|
||||
"""niveau et ues pour cette compétence de cette année du parcours.
|
||||
Si parcour est None, les niveaux du tronc commun
|
||||
"""
|
||||
if parcour is not None:
|
||||
# L'étudiant est inscrit à un parcours: cherche les niveaux
|
||||
niveaux = ApcNiveau.niveaux_annee_de_parcours(
|
||||
parcour, annee, competence=competence
|
||||
)
|
||||
else:
|
||||
# sans parcours, on cherche les niveaux du Tronc Commun de cette année
|
||||
niveaux = [
|
||||
niveau
|
||||
for niveau in refcomp.get_niveaux_by_parcours(annee)[1]["TC"]
|
||||
if niveau.competence_id == competence.id
|
||||
]
|
||||
|
||||
if len(niveaux) > 0:
|
||||
if len(niveaux) > 1:
|
||||
log(
|
||||
f"""_niveau_ues: plus d'un niveau pour {competence}
|
||||
annee {annee} {("parcours " + parcour.code) if parcour else ""}"""
|
||||
)
|
||||
niveau = niveaux[0]
|
||||
elif len(niveaux) == 0:
|
||||
return {
|
||||
"niveau": None,
|
||||
"ue_pair": None,
|
||||
"ue_impair": None,
|
||||
"ues_pair": [],
|
||||
"ues_impair": [],
|
||||
}
|
||||
# Toutes les UEs de la formation dans ce parcours ou tronc commun
|
||||
ues = [
|
||||
ue
|
||||
for ue in formation.ues
|
||||
if (
|
||||
(not ue.parcours)
|
||||
or (parcour is not None and (parcour.id in (p.id for p in ue.parcours)))
|
||||
)
|
||||
and ue.type == UE_STANDARD
|
||||
]
|
||||
ues_pair_possibles = [ue for ue in ues if ue.semestre_idx == (2 * annee)]
|
||||
ues_impair_possibles = [ue for ue in ues if ue.semestre_idx == (2 * annee - 1)]
|
||||
|
||||
# UE associée au niveau dans ce parcours
|
||||
ue_pair = ue_associee_au_niveau_du_parcours(
|
||||
ues_pair_possibles, niveau, f"S{2*annee}"
|
||||
)
|
||||
ue_impair = ue_associee_au_niveau_du_parcours(
|
||||
ues_impair_possibles, niveau, f"S{2*annee-1}"
|
||||
)
|
||||
|
||||
return {
|
||||
"niveau": niveau,
|
||||
"ue_pair": ue_pair,
|
||||
"ues_pair": [
|
||||
ue
|
||||
for ue in ues_pair_possibles
|
||||
if (not ue.niveau_competence) or ue.niveau_competence.id == niveau.id
|
||||
],
|
||||
"ue_impair": ue_impair,
|
||||
"ues_impair": [
|
||||
ue
|
||||
for ue in ues_impair_possibles
|
||||
if (not ue.niveau_competence) or ue.niveau_competence.id == niveau.id
|
||||
],
|
||||
}
|
||||
|
||||
competences = [
|
||||
{
|
||||
"competence": competence,
|
||||
"niveaux": {annee: _niveau_ues(competence, annee) for annee in (1, 2, 3)},
|
||||
}
|
||||
for competence in (
|
||||
parcour.query_competences()
|
||||
if parcour
|
||||
else refcomp.competences.order_by(ApcCompetence.numero)
|
||||
)
|
||||
]
|
||||
return competences
|
||||
|
1146
app/but/jury_but.py
1146
app/but/jury_but.py
File diff suppressed because it is too large
Load Diff
@ -12,6 +12,7 @@ from openpyxl.styles import Font, Border, Side, Alignment, PatternFill
|
||||
|
||||
from app import log
|
||||
from app.but import jury_but
|
||||
from app.but.cursus_but import but_ects_valides
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
@ -109,6 +110,11 @@ def pvjury_table_but(
|
||||
"""
|
||||
# remplace pour le BUT la fonction sco_pv_forms.pvjury_table
|
||||
annee_but = (formsemestre.semestre_id + 1) // 2
|
||||
referentiel_competence_id = formsemestre.formation.referentiel_competence_id
|
||||
if referentiel_competence_id is None:
|
||||
raise ScoValueError(
|
||||
"pas de référentiel de compétences associé à la formation de ce semestre !"
|
||||
)
|
||||
titles = {
|
||||
"nom": "Code" if anonymous else "Nom",
|
||||
"cursus": "Cursus",
|
||||
@ -153,7 +159,7 @@ def pvjury_table_but(
|
||||
etudid=etud.id,
|
||||
),
|
||||
"cursus": _descr_cursus_but(etud),
|
||||
"ects": f"{deca.formsemestre_ects():g}",
|
||||
"ects": f"""{deca.ects_annee():g}<br><br>Tot. {but_ects_valides(etud, referentiel_competence_id ):g}""",
|
||||
"ues": deca.descr_ues_validation(line_sep=line_sep) if deca else "-",
|
||||
"niveaux": deca.descr_niveaux_validation(line_sep=line_sep)
|
||||
if deca
|
||||
|
@ -48,9 +48,9 @@ def _get_jury_but_etud_result(
|
||||
# --- Les RCUEs
|
||||
rcue_list = []
|
||||
if deca:
|
||||
for rcue in deca.rcues_annee:
|
||||
dec_rcue = deca.dec_rcue_by_ue.get(rcue.ue_1.id)
|
||||
if dec_rcue is not None: # None si l'UE n'est pas associée à un niveau
|
||||
for dec_rcue in deca.get_decisions_rcues_annee():
|
||||
rcue = dec_rcue.rcue
|
||||
if rcue.complete: # n'exporte que les RCUEs complets
|
||||
dec_ue1 = deca.decisions_ues[rcue.ue_1.id]
|
||||
dec_ue2 = deca.decisions_ues[rcue.ue_2.id]
|
||||
rcue_dict = {
|
||||
|
@ -6,24 +6,22 @@
|
||||
|
||||
"""Jury BUT: calcul des décisions de jury annuelles "automatiques"
|
||||
"""
|
||||
from flask import g, url_for
|
||||
|
||||
from app import db
|
||||
from app.but import jury_but
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.models import Identite, FormSemestre, ScolarNews
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
|
||||
def formsemestre_validation_auto_but(
|
||||
formsemestre: FormSemestre, only_adm: bool = True, no_overwrite: bool = True
|
||||
formsemestre: FormSemestre, only_adm: bool = True
|
||||
) -> int:
|
||||
"""Calcul automatique des décisions de jury sur une "année" BUT.
|
||||
|
||||
- N'enregistre jamais de décisions de l'année scolaire précédente, même
|
||||
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
|
||||
@ -38,9 +36,17 @@ def formsemestre_validation_auto_but(
|
||||
for etudid in formsemestre.etuds_inscriptions:
|
||||
etud = Identite.get_etud(etudid)
|
||||
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
|
||||
nb_etud_modif += deca.record_all(
|
||||
no_overwrite=no_overwrite, only_validantes=only_adm
|
||||
)
|
||||
nb_etud_modif += deca.record_all(only_validantes=only_adm)
|
||||
|
||||
db.session.commit()
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_JURY,
|
||||
obj=formsemestre.id,
|
||||
text=f"""Calcul jury automatique du semestre {formsemestre.html_link_status()}""",
|
||||
url=url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
),
|
||||
)
|
||||
return nb_etud_modif
|
||||
|
@ -31,9 +31,11 @@ from app.models import (
|
||||
UniteEns,
|
||||
ScolarAutorisationInscription,
|
||||
ScolarFormSemestreValidation,
|
||||
ScolarNews,
|
||||
)
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import codes_cursus as sco_codes
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_utils as scu
|
||||
@ -91,35 +93,25 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
|
||||
<div class="titre">RCUE</div>
|
||||
"""
|
||||
)
|
||||
for niveau in deca.niveaux_competences:
|
||||
for dec_rcue in deca.get_decisions_rcues_annee():
|
||||
rcue = dec_rcue.rcue
|
||||
niveau = rcue.niveau
|
||||
H.append(
|
||||
f"""<div class="but_niveau_titre">
|
||||
<div title="{niveau.competence.titre_long}">{niveau.competence.titre}</div>
|
||||
</div>"""
|
||||
)
|
||||
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
|
||||
ue_impair, ue_pair = rcue.ue_1, rcue.ue_2
|
||||
# Les UEs à afficher,
|
||||
# qui seront toujours en readonly sur le formsemestre de l'année précédente du redoublant
|
||||
# qui
|
||||
ues_ro = [
|
||||
(
|
||||
ue_impair,
|
||||
(deca.a_cheval and deca.formsemestre_id != deca.formsemestre_impair.id),
|
||||
rcue.ue_cur_impair is None,
|
||||
),
|
||||
(
|
||||
ue_pair,
|
||||
deca.a_cheval and deca.formsemestre_id != deca.formsemestre_pair.id,
|
||||
rcue.ue_cur_pair is None,
|
||||
),
|
||||
]
|
||||
# Ordonne selon les dates des 2 semestres considérés:
|
||||
@ -153,17 +145,22 @@ def _gen_but_select(
|
||||
code_valide: str,
|
||||
disabled: bool = False,
|
||||
klass: str = "",
|
||||
data: dict = {},
|
||||
data: dict = None,
|
||||
code_valide_label: str = "",
|
||||
) -> str:
|
||||
"Le menu html select avec les codes"
|
||||
# if disabled: # mauvaise idée car le disabled est traité en JS
|
||||
# return f"""<div class="but_code {klass}">{code_valide}</div>"""
|
||||
data = data or {}
|
||||
options_htm = "\n".join(
|
||||
[
|
||||
f"""<option value="{code}"
|
||||
{'selected' if code == code_valide else ''}
|
||||
class="{'recorded' if code == code_valide else ''}"
|
||||
>{code}</option>"""
|
||||
>{code
|
||||
if ((code != code_valide) or not code_valide_label)
|
||||
else code_valide_label
|
||||
}</option>"""
|
||||
for code in codes
|
||||
]
|
||||
)
|
||||
@ -202,20 +199,54 @@ def _gen_but_niveau_ue(
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
elif dec_ue.formsemestre is None:
|
||||
# Validation d'UE antérieure (semestre hors année scolaire courante)
|
||||
if dec_ue.validation:
|
||||
moy_ue_str = f"""<span>{scu.fmt_note(dec_ue.validation.moy_ue)}</span>"""
|
||||
scoplement = f"""<div class="scoplement">
|
||||
<div>
|
||||
<b>UE {ue.acronyme} antérieure </b>
|
||||
<span>validée {dec_ue.validation.code}
|
||||
le {dec_ue.validation.event_date.strftime("%d/%m/%Y")}
|
||||
</span>
|
||||
</div>
|
||||
<div>Non reprise dans l'année en cours</div>
|
||||
</div>
|
||||
"""
|
||||
else:
|
||||
moy_ue_str = """<span>-</span>"""
|
||||
scoplement = """<div class="scoplement">
|
||||
<div>
|
||||
<b>Pas d'UE en cours ou validée dans cette compétence de ce côté.</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")}
|
||||
date_str = (
|
||||
f"""enregistré le {dec_ue.validation.event_date.strftime("%d/%m/%Y")}
|
||||
à {dec_ue.validation.event_date.strftime("%Hh%M")}
|
||||
"""
|
||||
if dec_ue.validation and dec_ue.validation.event_date
|
||||
else ""
|
||||
)
|
||||
scoplement = f"""<div class="scoplement">
|
||||
<div>Code {dec_ue.code_valide} {date_str}
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
else:
|
||||
scoplement = ""
|
||||
|
||||
return f"""<div class="but_niveau_ue {
|
||||
'recorded' if dec_ue.code_valide is not None else ''}
|
||||
ue_class = "" # 'recorded' if dec_ue.code_valide is not None else ''
|
||||
if dec_ue.code_valide is not None and dec_ue.codes:
|
||||
if dec_ue.code_valide == dec_ue.codes[0]:
|
||||
ue_class = "recorded"
|
||||
else:
|
||||
ue_class = "recorded_different"
|
||||
|
||||
return f"""<div class="but_niveau_ue {ue_class}
|
||||
{'annee_prec' if annee_prec else ''}
|
||||
">
|
||||
<div title="{ue.titre}">{ue.acronyme}</div>
|
||||
@ -236,7 +267,7 @@ def _gen_but_niveau_ue(
|
||||
|
||||
|
||||
def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str:
|
||||
if dec_rcue is None:
|
||||
if dec_rcue is None or not dec_rcue.rcue.complete:
|
||||
return """
|
||||
<div class="but_niveau_rcue niveau_vide with_scoplement">
|
||||
<div></div>
|
||||
@ -244,13 +275,25 @@ def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str:
|
||||
</div>
|
||||
"""
|
||||
|
||||
scoplement = (
|
||||
f"""<div class="scoplement">{
|
||||
dec_rcue.validation.to_html()
|
||||
}</div>"""
|
||||
if dec_rcue.validation
|
||||
else ""
|
||||
)
|
||||
code_propose_menu = dec_rcue.code_valide # le code enregistré
|
||||
code_valide_label = code_propose_menu
|
||||
if dec_rcue.validation:
|
||||
if dec_rcue.code_valide == dec_rcue.codes[0]:
|
||||
descr_validation = dec_rcue.validation.html()
|
||||
else: # on une validation enregistrée différence de celle proposée
|
||||
descr_validation = f"""Décision recommandée: <b>{dec_rcue.codes[0]}.</b>
|
||||
Il y avait {dec_rcue.validation.html()}"""
|
||||
if (
|
||||
sco_codes.BUT_CODES_ORDER[dec_rcue.codes[0]]
|
||||
> sco_codes.BUT_CODES_ORDER[dec_rcue.code_valide]
|
||||
):
|
||||
code_propose_menu = dec_rcue.codes[0]
|
||||
code_valide_label = (
|
||||
f"{dec_rcue.codes[0]} (actuel {dec_rcue.code_valide})"
|
||||
)
|
||||
scoplement = f"""<div class="scoplement">{descr_validation}</div>"""
|
||||
else:
|
||||
scoplement = "" # "pas de validation"
|
||||
|
||||
# Déjà enregistré ?
|
||||
niveau_rcue_class = ""
|
||||
@ -270,10 +313,11 @@ def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str:
|
||||
<div class="but_code">
|
||||
{_gen_but_select("code_rcue_"+str(niveau.id),
|
||||
dec_rcue.codes,
|
||||
dec_rcue.code_valide,
|
||||
code_propose_menu,
|
||||
disabled=True,
|
||||
klass="manual code_rcue",
|
||||
data = { "niveau_id" : str(niveau.id)}
|
||||
data = { "niveau_id" : str(niveau.id)},
|
||||
code_valide_label = code_valide_label,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -351,6 +395,16 @@ def jury_but_semestriel(
|
||||
flash(
|
||||
f"autorisation de passage en S{formsemestre.semestre_id + 1} annulée"
|
||||
)
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_JURY,
|
||||
obj=formsemestre.id,
|
||||
text=f"""Saisie décision jury dans {formsemestre.html_link_status()}""",
|
||||
url=url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
),
|
||||
)
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.formsemestre_validation_but",
|
||||
@ -394,7 +448,7 @@ def jury_but_semestriel(
|
||||
{warning}
|
||||
</div>
|
||||
|
||||
<form method="post" id="jury_but">
|
||||
<form method="post" class="jury_but_box" id="jury_but">
|
||||
""",
|
||||
]
|
||||
|
||||
|
67
app/but/jury_edit_manual.py
Normal file
67
app/but/jury_edit_manual.py
Normal file
@ -0,0 +1,67 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""Jury édition manuelle des décisions (correction d'erreurs, parcours hors normes)
|
||||
|
||||
Non spécifique au BUT.
|
||||
"""
|
||||
|
||||
from flask import render_template
|
||||
import sqlalchemy as sa
|
||||
|
||||
from app.models import (
|
||||
ApcValidationAnnee,
|
||||
ApcValidationRCUE,
|
||||
Identite,
|
||||
UniteEns,
|
||||
ScolarAutorisationInscription,
|
||||
ScolarFormSemestreValidation,
|
||||
)
|
||||
from app.views import ScoData
|
||||
|
||||
|
||||
def jury_delete_manual(etud: Identite):
|
||||
"""Vue (réservée au chef de dept.)
|
||||
présentant *toutes* les décisions de jury concernant cet étudiant
|
||||
et permettant de les supprimer une à une.
|
||||
"""
|
||||
sem_vals = ScolarFormSemestreValidation.query.filter_by(
|
||||
etudid=etud.id, ue_id=None
|
||||
).order_by(ScolarFormSemestreValidation.event_date)
|
||||
ue_vals = (
|
||||
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
|
||||
.join(UniteEns)
|
||||
.order_by(
|
||||
sa.extract("year", ScolarFormSemestreValidation.event_date),
|
||||
UniteEns.semestre_idx,
|
||||
UniteEns.numero,
|
||||
UniteEns.acronyme,
|
||||
)
|
||||
)
|
||||
autorisations = ScolarAutorisationInscription.query.filter_by(
|
||||
etudid=etud.id
|
||||
).order_by(
|
||||
ScolarAutorisationInscription.semestre_id, ScolarAutorisationInscription.date
|
||||
)
|
||||
rcue_vals = (
|
||||
ApcValidationRCUE.query.filter_by(etudid=etud.id)
|
||||
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id)
|
||||
.order_by(UniteEns.semestre_idx, UniteEns.numero, ApcValidationRCUE.date)
|
||||
)
|
||||
annee_but_vals = ApcValidationAnnee.query.filter_by(etudid=etud.id).order_by(
|
||||
ApcValidationAnnee.ordre, ApcValidationAnnee.date
|
||||
)
|
||||
return render_template(
|
||||
"jury/jury_delete_manual.j2",
|
||||
etud=etud,
|
||||
sem_vals=sem_vals,
|
||||
ue_vals=ue_vals,
|
||||
autorisations=autorisations,
|
||||
rcue_vals=rcue_vals,
|
||||
annee_but_vals=annee_but_vals,
|
||||
sco=ScoData(),
|
||||
title=f"Toutes les décisions de jury enregistrées pour {etud.html_link_fiche()}",
|
||||
)
|
253
app/but/rcue.py
Normal file
253
app/but/rcue.py
Normal file
@ -0,0 +1,253 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""Jury BUT: un RCUE, ou Regroupe Cohérent d'UEs
|
||||
"""
|
||||
from typing import Union
|
||||
from flask_sqlalchemy.query import Query
|
||||
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.models import (
|
||||
ApcNiveau,
|
||||
ApcValidationRCUE,
|
||||
Identite,
|
||||
ScolarFormSemestreValidation,
|
||||
UniteEns,
|
||||
)
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc.codes_cursus import BUT_CODES_ORDER
|
||||
|
||||
|
||||
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 RCUE déclenche la compensation des UEs.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
etud: Identite,
|
||||
niveau: ApcNiveau,
|
||||
res_pair: ResultatsSemestreBUT,
|
||||
res_impair: ResultatsSemestreBUT,
|
||||
semestre_id_impair: int,
|
||||
cur_ues_pair: list[UniteEns],
|
||||
cur_ues_impair: list[UniteEns],
|
||||
):
|
||||
"""
|
||||
res_pair, res_impair: résultats des formsemestre de l'année en cours, ou None
|
||||
cur_ues_pair, cur_ues_impair: ues auxquelles l'étudiant est inscrit cette année
|
||||
"""
|
||||
self.semestre_id_impair = semestre_id_impair
|
||||
self.semestre_id_pair = semestre_id_impair + 1
|
||||
self.etud: Identite = etud
|
||||
self.niveau: ApcNiveau = niveau
|
||||
"Le niveau de compétences de ce RCUE"
|
||||
# Chercher l'UE en cours pour pair, impair
|
||||
# une UE à laquelle l'étudiant est inscrit (non dispensé)
|
||||
# dans l'un des formsemestre en cours
|
||||
ues = [ue for ue in cur_ues_pair if ue.niveau_competence_id == niveau.id]
|
||||
self.ue_cur_pair = ues[0] if ues else None
|
||||
"UE paire en cours"
|
||||
ues = [ue for ue in cur_ues_impair if ue.niveau_competence_id == niveau.id]
|
||||
self.ue_cur_impair = ues[0] if ues else None
|
||||
"UE impaire en cours"
|
||||
|
||||
self.validation_ue_cur_pair = (
|
||||
ScolarFormSemestreValidation.query.filter_by(
|
||||
etudid=etud.id,
|
||||
formsemestre_id=res_pair.formsemestre.id,
|
||||
ue_id=self.ue_cur_pair.id,
|
||||
).first()
|
||||
if self.ue_cur_pair
|
||||
else None
|
||||
)
|
||||
self.validation_ue_cur_impair = (
|
||||
ScolarFormSemestreValidation.query.filter_by(
|
||||
etudid=etud.id,
|
||||
formsemestre_id=res_impair.formsemestre.id,
|
||||
ue_id=self.ue_cur_impair.id,
|
||||
).first()
|
||||
if self.ue_cur_impair
|
||||
else None
|
||||
)
|
||||
|
||||
# Autres validations pour l'UE paire
|
||||
self.validation_ue_best_pair = best_autre_ue_validation(
|
||||
etud.id,
|
||||
niveau.id,
|
||||
semestre_id_impair + 1,
|
||||
res_pair.formsemestre.id if (res_pair and self.ue_cur_pair) else None,
|
||||
)
|
||||
self.validation_ue_best_impair = best_autre_ue_validation(
|
||||
etud.id,
|
||||
niveau.id,
|
||||
semestre_id_impair,
|
||||
res_impair.formsemestre.id if (res_impair and self.ue_cur_impair) else None,
|
||||
)
|
||||
|
||||
# Suis-je complet ? (= en cours ou validé sur les deux moitiés)
|
||||
self.complete = (self.ue_cur_pair or self.validation_ue_best_pair) and (
|
||||
self.ue_cur_impair or self.validation_ue_best_impair
|
||||
)
|
||||
if not self.complete:
|
||||
self.moy_rcue = None
|
||||
|
||||
# Stocke les moyennes d'UE
|
||||
self.res_impair = None
|
||||
"résultats formsemestre de l'UE si elle est courante, None sinon"
|
||||
self.ue_status_impair = None
|
||||
if self.ue_cur_impair:
|
||||
ue_status = res_impair.get_etud_ue_status(etud.id, self.ue_cur_impair.id)
|
||||
self.moy_ue_1 = ue_status["moy"] if ue_status else None # avec capitalisée
|
||||
self.ue_1 = self.ue_cur_impair
|
||||
self.res_impair = res_impair
|
||||
self.ue_status_impair = ue_status
|
||||
elif self.validation_ue_best_impair:
|
||||
self.moy_ue_1 = self.validation_ue_best_impair.moy_ue
|
||||
self.ue_1 = self.validation_ue_best_impair.ue
|
||||
else:
|
||||
self.moy_ue_1, self.ue_1 = None, None
|
||||
self.moy_ue_1_val = self.moy_ue_1 if self.moy_ue_1 is not None else 0.0
|
||||
|
||||
self.res_pair = None
|
||||
"résultats formsemestre de l'UE si elle est courante, None sinon"
|
||||
self.ue_status_pair = None
|
||||
if self.ue_cur_pair:
|
||||
ue_status = res_pair.get_etud_ue_status(etud.id, self.ue_cur_pair.id)
|
||||
self.moy_ue_2 = ue_status["moy"] if ue_status else None # avec capitalisée
|
||||
self.ue_2 = self.ue_cur_pair
|
||||
self.res_pair = res_pair
|
||||
self.ue_status_pair = ue_status
|
||||
elif self.validation_ue_best_pair:
|
||||
self.moy_ue_2 = self.validation_ue_best_pair.moy_ue
|
||||
self.ue_2 = self.validation_ue_best_pair.ue
|
||||
else:
|
||||
self.moy_ue_2, self.ue_2 = None, None
|
||||
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 ou antérieures)
|
||||
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 = (
|
||||
self.moy_ue_1 * self.ue_1.coef_rcue
|
||||
+ self.moy_ue_2 * self.ue_2.coef_rcue
|
||||
) / (self.ue_1.coef_rcue + self.ue_2.coef_rcue)
|
||||
else:
|
||||
self.moy_rcue = None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"""<{self.__class__.__name__} {
|
||||
self.ue_1.acronyme if self.ue_1 else "?"}({self.moy_ue_1}) {
|
||||
self.ue_2.acronyme if self.ue_2 else "?"}({self.moy_ue_2})>"""
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"""RCUE {
|
||||
self.ue_1.acronyme if self.ue_1 else "?"}({self.moy_ue_1}) + {
|
||||
self.ue_2.acronyme if self.ue_2 else "?"}({self.moy_ue_2})"""
|
||||
|
||||
def query_validations(
|
||||
self,
|
||||
) -> Query: # list[ApcValidationRCUE]
|
||||
"""Les validations de jury enregistrées pour ce RCUE"""
|
||||
return (
|
||||
ApcValidationRCUE.query.filter_by(
|
||||
etudid=self.etud.id,
|
||||
)
|
||||
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue2_id)
|
||||
.join(ApcNiveau, UniteEns.niveau_competence_id == ApcNiveau.id)
|
||||
.filter(ApcNiveau.id == self.niveau.id)
|
||||
)
|
||||
|
||||
def other_ue(self, ue: UniteEns) -> UniteEns:
|
||||
"""L'autre UE du regroupement. Si ue ne fait pas partie du regroupement, ValueError"""
|
||||
if ue.id == self.ue_1.id:
|
||||
return self.ue_2
|
||||
elif ue.id == self.ue_2.id:
|
||||
return self.ue_1
|
||||
raise ValueError(f"ue {ue} hors RCUE {self}")
|
||||
|
||||
def est_enregistre(self) -> bool:
|
||||
"""Vrai si ce RCUE, donc le niveau de compétences correspondant
|
||||
a une décision jury enregistrée
|
||||
"""
|
||||
return self.query_validations().count() > 0
|
||||
|
||||
def est_compensable(self):
|
||||
"""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)
|
||||
and (self.moy_rcue > codes_cursus.BUT_BARRE_RCUE)
|
||||
and (
|
||||
(self.moy_ue_1_val < codes_cursus.NOTES_BARRE_GEN)
|
||||
or (self.moy_ue_2_val < codes_cursus.NOTES_BARRE_GEN)
|
||||
)
|
||||
)
|
||||
|
||||
def est_suffisant(self) -> bool:
|
||||
"""Vrai si ce RCUE est > 8"""
|
||||
return (self.moy_rcue is not None) and (
|
||||
self.moy_rcue > codes_cursus.BUT_RCUE_SUFFISANT
|
||||
)
|
||||
|
||||
def est_validable(self) -> bool:
|
||||
"""Vrai si ce RCUE satisfait les conditions pour être validé,
|
||||
c'est à dire que la moyenne des UE qui le constituent soit > 10
|
||||
"""
|
||||
return (self.moy_rcue is not None) and (
|
||||
self.moy_rcue > codes_cursus.BUT_BARRE_RCUE
|
||||
)
|
||||
|
||||
def code_valide(self) -> Union[ApcValidationRCUE, None]:
|
||||
"Si ce RCUE est ADM, CMP ou ADJ, la validation. Sinon, None"
|
||||
validation = self.query_validations().first()
|
||||
if (validation is not None) and (
|
||||
validation.code in codes_cursus.CODES_RCUE_VALIDES
|
||||
):
|
||||
return validation
|
||||
return None
|
||||
|
||||
|
||||
def best_autre_ue_validation(
|
||||
etudid: int, niveau_id: int, semestre_id: int, formsemestre_id: int
|
||||
) -> ScolarFormSemestreValidation:
|
||||
"""La "meilleure" validation validante d'UE pour ce niveau/semestre"""
|
||||
validations = (
|
||||
ScolarFormSemestreValidation.query.filter_by(etudid=etudid)
|
||||
.join(UniteEns)
|
||||
.filter_by(semestre_idx=semestre_id)
|
||||
.join(ApcNiveau)
|
||||
.filter(ApcNiveau.id == niveau_id)
|
||||
)
|
||||
validations = [v for v in validations if codes_cursus.code_ue_validant(v.code)]
|
||||
# Elimine l'UE en cours si elle existe
|
||||
if formsemestre_id is not None:
|
||||
validations = [v for v in validations if v.formsemestre_id != formsemestre_id]
|
||||
validations = sorted(validations, key=lambda v: BUT_CODES_ORDER.get(v.code, 0))
|
||||
return validations[-1] if validations else None
|
||||
|
||||
|
||||
# def compute_ues_by_niveau(
|
||||
# niveaux: list[ApcNiveau],
|
||||
# ) -> dict[int, tuple[list[UniteEns], list[UniteEns]]]:
|
||||
# """UEs à valider cette année pour cet étudiant, selon son parcours.
|
||||
# Considérer les UEs associées aux niveaux et non celles des formsemestres
|
||||
# en cours. Notez que même si l'étudiant n'est pas inscrit ("dispensé") à une UE
|
||||
# dans le formsemestre origine, elle doit apparaitre sur la page jury.
|
||||
# Return: { niveau_id : ( [ues impair], [ues pair]) }
|
||||
# """
|
||||
# # Les UEs associées à ce niveau, toutes formations confondues
|
||||
# return {
|
||||
# niveau.id: (
|
||||
# [ue for ue in niveau.ues if ue.semestre_idx % 2],
|
||||
# [ue for ue in niveau.ues if not (ue.semestre_idx % 2)],
|
||||
# )
|
||||
# for niveau in niveaux
|
||||
# }
|
117
app/but/validations_view.py
Normal file
117
app/but/validations_view.py
Normal file
@ -0,0 +1,117 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""Jury édition manuelle des décisions (correction d'erreurs, parcours hors normes)
|
||||
|
||||
Non spécifique au BUT.
|
||||
"""
|
||||
|
||||
from flask import render_template
|
||||
import sqlalchemy as sa
|
||||
|
||||
from app import log
|
||||
from app.but import cursus_but
|
||||
from app.models import (
|
||||
ApcCompetence,
|
||||
ApcNiveau,
|
||||
ApcReferentielCompetences,
|
||||
# ApcValidationAnnee, # TODO
|
||||
ApcValidationRCUE,
|
||||
Formation,
|
||||
FormSemestre,
|
||||
Identite,
|
||||
UniteEns,
|
||||
# ScolarAutorisationInscription,
|
||||
ScolarFormSemestreValidation,
|
||||
)
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences
|
||||
from app.views import ScoData
|
||||
|
||||
|
||||
def validation_rcues(etud: Identite, formsemestre: FormSemestre, edit: bool = False):
|
||||
"""Page de saisie des décisions de RCUEs "antérieures"
|
||||
On peut l'utiliser pour saisir la validation de n'importe quel RCUE
|
||||
d'une année antérieure et de la formation du formsemestre indiqué.
|
||||
"""
|
||||
formation: Formation = formsemestre.formation
|
||||
refcomp = formation.referentiel_competence
|
||||
if refcomp is None:
|
||||
raise ScoNoReferentielCompetences(formation=formation)
|
||||
parcour = formsemestre.etuds_inscriptions[etud.id].parcour
|
||||
# Si non inscrit à un parcours, prend toutes les compétences
|
||||
competences_parcour = cursus_but.parcour_formation_competences(parcour, formation)
|
||||
|
||||
ue_validation_by_niveau = get_ue_validation_by_niveau(refcomp, etud)
|
||||
rcue_validation_by_niveau = get_rcue_validation_by_niveau(refcomp, etud)
|
||||
ects_total = sum((v.ects() for v in ue_validation_by_niveau.values()))
|
||||
return render_template(
|
||||
"but/validation_rcues.j2",
|
||||
competences_parcour=competences_parcour,
|
||||
edit=edit,
|
||||
ects_total=ects_total,
|
||||
formation=formation,
|
||||
parcour=parcour,
|
||||
rcue_validation_by_niveau=rcue_validation_by_niveau,
|
||||
rcue_codes=sorted(codes_cursus.CODES_JURY_RCUE),
|
||||
sco=ScoData(formsemestre=formsemestre, etud=etud),
|
||||
title=f"{formation.acronyme} - Niveaux et UEs",
|
||||
ue_validation_by_niveau=ue_validation_by_niveau,
|
||||
)
|
||||
|
||||
|
||||
def get_ue_validation_by_niveau(
|
||||
refcomp: ApcReferentielCompetences, etud: Identite
|
||||
) -> dict[tuple[int, str], ScolarFormSemestreValidation]:
|
||||
"""Les validations d'UEs de cet étudiant liées à ce référentiel de compétences.
|
||||
Pour chaque niveau / pair ou impair, choisi la "meilleure" validation
|
||||
"""
|
||||
validations: list[ScolarFormSemestreValidation] = (
|
||||
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
|
||||
.join(UniteEns)
|
||||
.join(ApcNiveau)
|
||||
.join(ApcCompetence)
|
||||
.filter_by(referentiel_id=refcomp.id)
|
||||
.all()
|
||||
)
|
||||
# La meilleure validation pour chaque UE
|
||||
ue_validation_by_niveau = {} # { (niveau_id, pair|impair) : validation }
|
||||
for validation in validations:
|
||||
if validation.ue.niveau_competence is None:
|
||||
log(
|
||||
f"""validation_rcues: ignore validation d'UE {
|
||||
validation.ue.id} pas de niveau de competence"""
|
||||
)
|
||||
key = (
|
||||
validation.ue.niveau_competence.id,
|
||||
"impair" if validation.ue.semestre_idx % 2 else "pair",
|
||||
)
|
||||
existing = ue_validation_by_niveau.get(key, None)
|
||||
if (not existing) or (
|
||||
codes_cursus.BUT_CODES_ORDER[existing.code]
|
||||
< codes_cursus.BUT_CODES_ORDER[validation.code]
|
||||
):
|
||||
ue_validation_by_niveau[key] = validation
|
||||
return ue_validation_by_niveau
|
||||
|
||||
|
||||
def get_rcue_validation_by_niveau(
|
||||
refcomp: ApcReferentielCompetences, etud: Identite
|
||||
) -> dict[int, ApcValidationRCUE]:
|
||||
"""Les validations d'UEs de cet étudiant liées à ce référentiel de compétences.
|
||||
Pour chaque niveau / pair ou impair, choisi la "meilleure" validation
|
||||
"""
|
||||
validations: list[ApcValidationRCUE] = (
|
||||
ApcValidationRCUE.query.filter_by(etudid=etud.id)
|
||||
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue2_id)
|
||||
.join(ApcNiveau, UniteEns.niveau_competence_id == ApcNiveau.id)
|
||||
.join(ApcCompetence)
|
||||
.filter_by(referentiel_id=refcomp.id)
|
||||
.all()
|
||||
)
|
||||
return {
|
||||
validation.ue2.niveau_competence.id: validation for validation in validations
|
||||
}
|
@ -18,7 +18,7 @@ import pandas as pd
|
||||
|
||||
from flask import g
|
||||
|
||||
from app.scodoc.codes_cursus import UE_SPORT
|
||||
from app.scodoc.codes_cursus import UE_SPORT, UE_STANDARD
|
||||
from app.scodoc.codes_cursus import CursusDUT, CursusDUTMono
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
@ -194,7 +194,7 @@ class BonusSportAdditif(BonusSport):
|
||||
# les points au dessus du seuil sont comptés (defaut: seuil_moy_gen):
|
||||
seuil_comptage = None
|
||||
proportion_point = 0.05 # multiplie les points au dessus du seuil
|
||||
bonux_max = 20.0 # le bonus ne peut dépasser 20 points
|
||||
bonus_max = 20.0 # le bonus ne peut dépasser 20 points
|
||||
bonus_min = 0.0 # et ne peut pas être négatif
|
||||
|
||||
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||||
@ -435,8 +435,11 @@ 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>Le bonus est compris entre 0 et 0,2 points.
|
||||
et est reporté sur les moyennes d'UE.
|
||||
</p>
|
||||
<p>La valeur saisie doit être entre 0 et 0,2: toute valeur
|
||||
supérieure à 0,2 entraine un bonus de 0,2.
|
||||
</p>
|
||||
"""
|
||||
|
||||
@ -444,7 +447,7 @@ class BonusBesanconVesoul(BonusSportAdditif):
|
||||
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
|
||||
proportion_point = 1
|
||||
bonus_max = 0.2
|
||||
|
||||
|
||||
@ -740,6 +743,7 @@ class BonusGrenobleIUT1(BonusSportMultiplicatif):
|
||||
|
||||
name = "bonus_iut1grenoble_2017"
|
||||
displayed_name = "IUT de Grenoble 1"
|
||||
|
||||
# C'est un bonus "multiplicatif": on l'exprime en additif,
|
||||
# sur chaque moyenne d'UE m_0
|
||||
# Augmenter de 5% correspond à multiplier par a=1.05
|
||||
@ -782,6 +786,7 @@ class BonusIUTRennes1(BonusSportAdditif):
|
||||
seuil_moy_gen = 10.0
|
||||
proportion_point = 1 / 20.0
|
||||
classic_use_bonus_ues = False
|
||||
|
||||
# S'applique aussi en classic, sur la moy. gen.
|
||||
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||||
"""calcul du bonus"""
|
||||
@ -822,16 +827,32 @@ class BonusStMalo(BonusIUTRennes1):
|
||||
class BonusLaRocheSurYon(BonusSportAdditif):
|
||||
"""Bonus IUT de La Roche-sur-Yon
|
||||
|
||||
Si une note de bonus est saisie, l'étudiant est gratifié de 0,2 points
|
||||
sur sa moyenne générale ou, en BUT, sur la moyenne de chaque UE.
|
||||
<p>
|
||||
<b>La note saisie s'applique directement</b>: si on saisit 0,2, un bonus de 0,2 points est appliqué
|
||||
aux moyennes.
|
||||
La valeur maximale du bonus est 1 point. Il est appliqué sur les moyennes d'UEs en BUT,
|
||||
ou sur la moyenne générale dans les autres formations.
|
||||
</p>
|
||||
<p>Pour les <b>semestres antérieurs à janvier 2023</b>: si une note de bonus est saisie,
|
||||
l'étudiant est gratifié de 0,2 points sur sa moyenne générale ou, en BUT, sur la
|
||||
moyenne de chaque UE.
|
||||
</p>
|
||||
"""
|
||||
|
||||
name = "bonus_larochesuryon"
|
||||
displayed_name = "IUT de La Roche-sur-Yon"
|
||||
seuil_moy_gen = 0.0
|
||||
seuil_comptage = 0.0
|
||||
proportion_point = 1e10 # le moindre point sature le bonus
|
||||
bonus_max = 0.2 # à 0.2
|
||||
|
||||
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, 12, 31):
|
||||
self.proportion_point = 1.0
|
||||
self.bonus_max = 1
|
||||
else: # ancienne règle
|
||||
self.proportion_point = 1e10 # le moindre point sature le bonus
|
||||
self.bonus_max = 0.2 # à 0.2
|
||||
super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
|
||||
|
||||
|
||||
class BonusLaRochelle(BonusSportAdditif):
|
||||
@ -1055,6 +1076,36 @@ class BonusLyon(BonusSportAdditif):
|
||||
)
|
||||
|
||||
|
||||
class BonusLyon3(BonusSportAdditif):
|
||||
"""IUT de Lyon 3 (septembre 2022)
|
||||
|
||||
<p>Nous avons deux types de bonifications : sport et/ou culture
|
||||
</p>
|
||||
<p>
|
||||
Pour chaque point au-dessus de 10 obtenu en sport ou en culture nous
|
||||
ajoutons 0,03 points à toutes les moyennes d’UE du semestre. Exemple : 16 en
|
||||
sport ajoute 6*0,03 = 0,18 points à toutes les moyennes d’UE du semestre.
|
||||
</p>
|
||||
<p>
|
||||
Les bonifications sport et culture peuvent se cumuler dans la limite de 0,3
|
||||
points ajoutés aux moyennes des UE. Exemple : 17 en sport et 16 en culture
|
||||
conduisent au calcul (7 + 6)*0,03 = 0,39 qui dépasse 0,3. La bonification
|
||||
dans ce cas ne sera que de 0,3 points ajoutés à toutes les moyennes d’UE du
|
||||
semestre.
|
||||
</p>
|
||||
<p>
|
||||
Dans Scodoc on déclarera une UE Sport&Culture dans laquelle on aura un
|
||||
module pour le Sport et un autre pour la Culture avec pour chaque module la
|
||||
note sur 20 obtenue en sport ou en culture par l’étudiant.
|
||||
</p>
|
||||
"""
|
||||
|
||||
name = "bonus_lyon3"
|
||||
displayed_name = "IUT de Lyon 3"
|
||||
proportion_point = 0.03
|
||||
bonus_max = 0.3
|
||||
|
||||
|
||||
class BonusMantes(BonusSportAdditif):
|
||||
"""Calcul bonus modules optionnels (investissement, ...), IUT de Mantes en Yvelines.
|
||||
|
||||
@ -1336,6 +1387,7 @@ class BonusStNazaire(BonusSport):
|
||||
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
|
||||
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):
|
||||
@ -1533,6 +1585,63 @@ class BonusIUTV(BonusSportAdditif):
|
||||
# c'est le bonus par défaut: aucune méthode à surcharger
|
||||
|
||||
|
||||
# Finalement inutile: un bonus direct est mieux adapté à leurs besoins.
|
||||
# # class BonusMastersUSPNIG(BonusSportAdditif):
|
||||
# """Calcul bonus modules optionnels (sport, culture), règle Masters de l'Institut Galilée (USPN)
|
||||
|
||||
# Les étudiants peuvent suivre des enseignements optionnels
|
||||
# de l'USPN (sports, musique, deuxième langue, culture, etc) dans une
|
||||
# UE libre. Les points au-dessus de 10 sur 20 obtenus dans cette UE
|
||||
# libre sont ajoutés au total des points obtenus pour les UE obligatoires
|
||||
# du semestre concerné.
|
||||
# """
|
||||
|
||||
# name = "bonus_masters__uspn_ig"
|
||||
# displayed_name = "Masters de l'Institut Galilée (USPN)"
|
||||
# proportion_point = 1.0
|
||||
# seuil_moy_gen = 10.0
|
||||
|
||||
# def __init__(
|
||||
# self,
|
||||
# formsemestre: "FormSemestre",
|
||||
# sem_modimpl_moys: np.array,
|
||||
# ues: list,
|
||||
# modimpl_inscr_df: pd.DataFrame,
|
||||
# modimpl_coefs: np.array,
|
||||
# etud_moy_gen,
|
||||
# etud_moy_ue,
|
||||
# ):
|
||||
# # Pour ce bonus, il nous faut la somme des coefs des modules non bonus
|
||||
# # du formsemestre (et non auxquels les étudiants sont inscrits !)
|
||||
# self.sum_coefs = sum(
|
||||
# [
|
||||
# m.module.coefficient
|
||||
# for m in formsemestre.modimpls_sorted
|
||||
# if (m.module.module_type == ModuleType.STANDARD)
|
||||
# and (m.module.ue.type == UE_STANDARD)
|
||||
# ]
|
||||
# )
|
||||
# super().__init__(
|
||||
# formsemestre,
|
||||
# sem_modimpl_moys,
|
||||
# ues,
|
||||
# modimpl_inscr_df,
|
||||
# modimpl_coefs,
|
||||
# etud_moy_gen,
|
||||
# etud_moy_ue,
|
||||
# )
|
||||
# # Bonus sur la moyenne générale seulement
|
||||
# # On a dans bonus_moy_arr le bonus additif classique
|
||||
# # Sa valeur sera appliquée comme moy_gen += bonus_moy_gen
|
||||
# # or ici on veut
|
||||
# # moy_gen = (somme des notes + bonus_moy_arr) / somme des coefs
|
||||
# # moy_gen += bonus_moy_arr / somme des coefs
|
||||
|
||||
# self.bonus_moy_gen = (
|
||||
# None if self.bonus_moy_gen is None else self.bonus_moy_gen / self.sum_coefs
|
||||
# )
|
||||
|
||||
|
||||
def get_bonus_class_dict(start=BonusSport, d=None):
|
||||
"""Dictionnaire des classes de bonus
|
||||
(liste les sous-classes de BonusSport ayant un nom)
|
||||
|
@ -10,8 +10,17 @@ import pandas as pd
|
||||
import sqlalchemy as sa
|
||||
|
||||
from app import db
|
||||
from app.models import FormSemestre, Identite, ScolarFormSemestreValidation, UniteEns
|
||||
from app.comp.res_cache import ResultatsCache
|
||||
from app.models import (
|
||||
ApcValidationAnnee,
|
||||
ApcValidationRCUE,
|
||||
Formation,
|
||||
FormSemestre,
|
||||
Identite,
|
||||
ScolarAutorisationInscription,
|
||||
ScolarFormSemestreValidation,
|
||||
UniteEns,
|
||||
)
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import codes_cursus
|
||||
|
||||
@ -81,7 +90,7 @@ class ValidationsSemestre(ResultatsCache):
|
||||
|
||||
# UEs: { etudid : { ue_id : {"code":, "ects":, "event_date":} }}
|
||||
decisions_jury_ues = {}
|
||||
# Parcours les décisions d'UE:
|
||||
# Parcoure les décisions d'UE:
|
||||
for decision in (
|
||||
decisions_jury_q.filter(db.text("ue_id is not NULL"))
|
||||
.join(UniteEns)
|
||||
@ -172,3 +181,79 @@ def formsemestre_get_ue_capitalisees(formsemestre: FormSemestre) -> pd.DataFrame
|
||||
with db.engine.begin() as connection:
|
||||
df = pd.read_sql_query(query, connection, params=params, index_col="etudid")
|
||||
return df
|
||||
|
||||
|
||||
def erase_decisions_annee_formation(
|
||||
etud: Identite, formation: Formation, annee: int, delete=False
|
||||
) -> list:
|
||||
"""Efface toutes les décisions de jury de l'étudiant dans les formations de même code
|
||||
que celle donnée pour cette année de la formation:
|
||||
UEs, RCUEs de l'année BUT, année BUT, passage vers l'année suivante.
|
||||
Ne considère pas l'origine de la décision.
|
||||
annee: entier, 1, 2, 3, ...
|
||||
Si delete est faux, renvoie la liste des validations qu'il faudrait effacer, sans y toucher.
|
||||
"""
|
||||
sem1, sem2 = annee * 2 - 1, annee * 2
|
||||
# UEs
|
||||
validations = (
|
||||
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
|
||||
.join(UniteEns)
|
||||
.filter(db.or_(UniteEns.semestre_idx == sem1, UniteEns.semestre_idx == sem2))
|
||||
.join(Formation)
|
||||
.filter_by(formation_code=formation.formation_code)
|
||||
.order_by(
|
||||
UniteEns.acronyme, UniteEns.numero
|
||||
) # acronyme d'abord car 2 semestres
|
||||
.all()
|
||||
)
|
||||
# RCUEs (a priori inutile de matcher sur l'ue2_id)
|
||||
validations += (
|
||||
ApcValidationRCUE.query.filter_by(etudid=etud.id)
|
||||
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id)
|
||||
.filter_by(semestre_idx=sem1)
|
||||
.join(Formation)
|
||||
.filter_by(formation_code=formation.formation_code)
|
||||
.order_by(UniteEns.acronyme, UniteEns.numero)
|
||||
.all()
|
||||
)
|
||||
# Validation de semestres classiques
|
||||
validations += (
|
||||
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id, ue_id=None)
|
||||
.join(
|
||||
FormSemestre,
|
||||
FormSemestre.id == ScolarFormSemestreValidation.formsemestre_id,
|
||||
)
|
||||
.filter(
|
||||
db.or_(FormSemestre.semestre_id == sem1, FormSemestre.semestre_id == sem2)
|
||||
)
|
||||
.join(Formation)
|
||||
.filter_by(formation_code=formation.formation_code)
|
||||
.all()
|
||||
)
|
||||
# Année BUT
|
||||
validations += ApcValidationAnnee.query.filter_by(
|
||||
etudid=etud.id,
|
||||
ordre=annee,
|
||||
referentiel_competence_id=formation.referentiel_competence_id,
|
||||
).all()
|
||||
# Autorisations vers les semestres suivants ceux de l'année:
|
||||
validations += (
|
||||
ScolarAutorisationInscription.query.filter_by(
|
||||
etudid=etud.id, formation_code=formation.formation_code
|
||||
)
|
||||
.filter(
|
||||
db.or_(
|
||||
ScolarAutorisationInscription.semestre_id == sem1 + 1,
|
||||
ScolarAutorisationInscription.semestre_id == sem2 + 1,
|
||||
)
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
if delete:
|
||||
for validation in validations:
|
||||
db.session.delete(validation)
|
||||
db.session.commit()
|
||||
sco_cache.invalidate_formsemestre_etud(etud)
|
||||
return []
|
||||
return validations
|
||||
|
@ -134,7 +134,7 @@ class ModuleImplResults:
|
||||
manque des notes) ssi il y a des étudiants inscrits au semestre et au module
|
||||
qui ont des notes ATT.
|
||||
"""
|
||||
moduleimpl = ModuleImpl.query.get(self.moduleimpl_id)
|
||||
moduleimpl = db.session.get(ModuleImpl, self.moduleimpl_id)
|
||||
self.etudids = self._etudids()
|
||||
|
||||
# --- Calcul nombre d'inscrits pour déterminer les évaluations "completes":
|
||||
@ -225,8 +225,8 @@ class ModuleImplResults:
|
||||
"""
|
||||
return [
|
||||
inscr.etudid
|
||||
for inscr in ModuleImpl.query.get(
|
||||
self.moduleimpl_id
|
||||
for inscr in db.session.get(
|
||||
ModuleImpl, self.moduleimpl_id
|
||||
).formsemestre.inscriptions
|
||||
]
|
||||
|
||||
@ -319,10 +319,16 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
||||
ou NaN si les évaluations (dans lesquelles l'étudiant a des notes)
|
||||
ne donnent pas de coef vers cette UE.
|
||||
"""
|
||||
modimpl = ModuleImpl.query.get(self.moduleimpl_id)
|
||||
modimpl = db.session.get(ModuleImpl, self.moduleimpl_id)
|
||||
nb_etuds, nb_evals = self.evals_notes.shape
|
||||
nb_ues = evals_poids_df.shape[1]
|
||||
assert evals_poids_df.shape[0] == nb_evals # compat notes/poids
|
||||
if evals_poids_df.shape[0] != nb_evals:
|
||||
# compat notes/poids: race condition ?
|
||||
app.critical_error(
|
||||
f"""compute_module_moy: evals_poids_df.shape[0] != nb_evals ({
|
||||
evals_poids_df.shape[0]} != {nb_evals})
|
||||
"""
|
||||
)
|
||||
if nb_etuds == 0:
|
||||
return pd.DataFrame(index=[], columns=evals_poids_df.columns)
|
||||
if nb_ues == 0:
|
||||
@ -413,7 +419,7 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
|
||||
|
||||
Résultat: (evals_poids, liste de UEs du semestre sauf le sport)
|
||||
"""
|
||||
modimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id)
|
||||
modimpl: ModuleImpl = db.session.get(ModuleImpl, moduleimpl_id)
|
||||
evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all()
|
||||
ues = modimpl.formsemestre.get_ues(with_sport=False)
|
||||
ue_ids = [ue.id for ue in ues]
|
||||
@ -492,7 +498,7 @@ class ModuleImplResultsClassic(ModuleImplResults):
|
||||
ou NaN si les évaluations (dans lesquelles l'étudiant a des notes)
|
||||
ne donnent pas de coef.
|
||||
"""
|
||||
modimpl = ModuleImpl.query.get(self.moduleimpl_id)
|
||||
modimpl = db.session.get(ModuleImpl, self.moduleimpl_id)
|
||||
nb_etuds, nb_evals = self.evals_notes.shape
|
||||
if nb_etuds == 0:
|
||||
return pd.Series()
|
||||
|
@ -30,7 +30,10 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from flask import flash, g, Markup, url_for
|
||||
from flask import flash, g, url_for
|
||||
from markupsafe import Markup
|
||||
|
||||
from app import db
|
||||
from app.models.formations import Formation
|
||||
|
||||
|
||||
@ -78,7 +81,7 @@ def compute_sem_moys_apc_using_ects(
|
||||
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)
|
||||
formation = db.session.get(Formation, formation_id)
|
||||
flash(
|
||||
Markup(
|
||||
f"""Calcul moyenne générale impossible: ECTS des UE manquants !<br>
|
||||
@ -92,7 +95,7 @@ def compute_sem_moys_apc_using_ects(
|
||||
return moy_gen
|
||||
|
||||
|
||||
def comp_ranks_series(notes: pd.Series) -> (pd.Series, pd.Series):
|
||||
def comp_ranks_series(notes: pd.Series) -> tuple[pd.Series, pd.Series]:
|
||||
"""Calcul rangs à partir d'une séries ("vecteur") de notes (index etudid, valeur
|
||||
numérique) en tenant compte des ex-aequos.
|
||||
|
||||
|
@ -30,6 +30,7 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
import app
|
||||
from app import db
|
||||
from app import models
|
||||
from app.models import (
|
||||
@ -167,8 +168,14 @@ def notes_sem_assemble_cube(modimpls_notes: list[pd.DataFrame]) -> np.ndarray:
|
||||
"""
|
||||
assert len(modimpls_notes)
|
||||
modimpls_notes_arr = [df.values for df in modimpls_notes]
|
||||
modimpls_notes = np.stack(modimpls_notes_arr)
|
||||
# passe de (mod x etud x ue) à (etud x mod x ue)
|
||||
try:
|
||||
modimpls_notes = np.stack(modimpls_notes_arr)
|
||||
# passe de (mod x etud x ue) à (etud x mod x ue)
|
||||
except ValueError:
|
||||
app.critical_error(
|
||||
f"""notes_sem_assemble_cube: shapes {
|
||||
", ".join([x.shape for x in modimpls_notes_arr])}"""
|
||||
)
|
||||
return modimpls_notes.swapaxes(0, 1)
|
||||
|
||||
|
||||
|
@ -10,17 +10,17 @@ import time
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from app import log
|
||||
from app import db, log
|
||||
from app.comp import moy_ue, moy_sem, inscr_mod
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.comp.bonus_spo import BonusSport
|
||||
from app.models import ScoDocSiteConfig
|
||||
from app.models import FormSemestreInscription, ScoDocSiteConfig
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
from app.models.but_refcomp import ApcParcours, ApcNiveau
|
||||
from app.models.ues import DispenseUE, UniteEns
|
||||
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc.codes_cursus import UE_SPORT
|
||||
from app.scodoc.codes_cursus import BUT_CODES_ORDER, UE_SPORT
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
|
||||
@ -44,7 +44,8 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
"""Parcours de chaque étudiant { etudid : parcour_id }"""
|
||||
self.ues_ids_by_parcour: dict[set[int]] = {}
|
||||
"""{ parcour_id : set }, ue_id de chaque parcours"""
|
||||
|
||||
self.validations_annee: dict[int, ApcValidationAnnee] = {}
|
||||
"""chargé par get_validations_annee: jury annuel BUT"""
|
||||
if not self.load_cached():
|
||||
t0 = time.time()
|
||||
self.compute()
|
||||
@ -288,9 +289,9 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
if ref_comp is None:
|
||||
return set()
|
||||
if parcour_id is None:
|
||||
ues_ids = {ue.id for ue in self.ues}
|
||||
ues_ids = {ue.id for ue in self.ues if ue.type != UE_SPORT}
|
||||
else:
|
||||
parcour: ApcParcours = ApcParcours.query.get(parcour_id)
|
||||
parcour: ApcParcours = db.session.get(ApcParcours, parcour_id)
|
||||
annee = (self.formsemestre.semestre_id + 1) // 2
|
||||
niveaux = ApcNiveau.niveaux_annee_de_parcours(parcour, annee, ref_comp)
|
||||
# Les UEs du formsemestre associées à ces niveaux:
|
||||
@ -306,12 +307,13 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
|
||||
return ues_ids
|
||||
|
||||
def etud_has_decision(self, etudid):
|
||||
"""True s'il y a une décision de jury pour cet étudiant émanant de ce formsemestre.
|
||||
def etud_has_decision(self, etudid) -> bool:
|
||||
"""True s'il y a une décision (quelconque) de jury
|
||||
émanant de ce formsemestre pour cet étudiant.
|
||||
prend aussi en compte les autorisations de passage.
|
||||
Sous-classée en BUT pour les RCUEs et années.
|
||||
Ici sous-classée (BUT) pour les RCUEs et années.
|
||||
"""
|
||||
return (
|
||||
return bool(
|
||||
super().etud_has_decision(etudid)
|
||||
or ApcValidationAnnee.query.filter_by(
|
||||
formsemestre_id=self.formsemestre.id, etudid=etudid
|
||||
@ -320,3 +322,40 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
formsemestre_id=self.formsemestre.id, etudid=etudid
|
||||
).count()
|
||||
)
|
||||
|
||||
def get_validations_annee(self) -> dict[int, ApcValidationAnnee]:
|
||||
"""Les validations des étudiants de ce semestre
|
||||
pour l'année BUT d'une formation compatible avec celle de ce semestre.
|
||||
Attention:
|
||||
1) la validation ne provient pas nécessairement de ce semestre
|
||||
(redoublants, pair/impair, extérieurs).
|
||||
2) l'étudiant a pu démissionner ou défaillir.
|
||||
3) S'il y a plusieurs validations pour le même étudiant, prend la "meilleure".
|
||||
|
||||
Mémorise le résultat (dans l'instance, pas en cache: TODO voir au profiler)
|
||||
"""
|
||||
if self.validations_annee:
|
||||
return self.validations_annee
|
||||
annee_but = (self.formsemestre.semestre_id + 1) // 2
|
||||
validations = ApcValidationAnnee.query.filter_by(
|
||||
ordre=annee_but,
|
||||
referentiel_competence_id=self.formsemestre.formation.referentiel_competence_id,
|
||||
).join(
|
||||
FormSemestreInscription,
|
||||
db.and_(
|
||||
FormSemestreInscription.etudid == ApcValidationAnnee.etudid,
|
||||
FormSemestreInscription.formsemestre_id == self.formsemestre.id,
|
||||
),
|
||||
)
|
||||
validation_by_etud = {}
|
||||
for validation in validations:
|
||||
if validation.etudid in validation_by_etud:
|
||||
# keep the "best"
|
||||
if BUT_CODES_ORDER.get(validation.code, 0) > BUT_CODES_ORDER.get(
|
||||
validation_by_etud[validation.etudid].code, 0
|
||||
):
|
||||
validation_by_etud[validation.etudid] = validation
|
||||
else:
|
||||
validation_by_etud[validation.etudid] = validation
|
||||
self.validations_annee = validation_by_etud
|
||||
return self.validations_annee
|
||||
|
@ -17,6 +17,7 @@ import pandas as pd
|
||||
|
||||
from flask import g, url_for
|
||||
|
||||
from app import db
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_cache import ResultatsCache
|
||||
from app.comp.jury import ValidationsSemestre
|
||||
@ -31,6 +32,7 @@ from app.scodoc.codes_cursus import UE_SPORT
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
# Il faut bien distinguer
|
||||
# - ce qui est caché de façon persistente (via redis):
|
||||
# ce sont les attributs listés dans `_cached_attrs`
|
||||
@ -137,7 +139,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
def etud_ues(self, etudid: int) -> Generator[UniteEns]:
|
||||
"""Liste des UE auxquelles l'étudiant est inscrit
|
||||
(sans bonus, en BUT prend en compte le parcours de l'étudiant)."""
|
||||
return (UniteEns.query.get(ue_id) for ue_id in self.etud_ues_ids(etudid))
|
||||
return (db.session.get(UniteEns, ue_id) for ue_id in self.etud_ues_ids(etudid))
|
||||
|
||||
def etud_ects_tot_sem(self, etudid: int) -> float:
|
||||
"""Le total des ECTS associées à ce semestre (que l'étudiant peut ou non valider)"""
|
||||
@ -351,7 +353,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 = UniteEns.query.get(ue_id)
|
||||
ue: UniteEns = db.session.get(UniteEns, ue_id)
|
||||
ue_dict = ue.to_dict()
|
||||
|
||||
if ue.type == UE_SPORT:
|
||||
@ -381,7 +383,11 @@ class ResultatsSemestre(ResultatsCache):
|
||||
was_capitalized = False
|
||||
if etudid in self.validations.ue_capitalisees.index:
|
||||
ue_cap = self._get_etud_ue_cap(etudid, ue)
|
||||
if ue_cap and not np.isnan(ue_cap["moy_ue"]):
|
||||
if (
|
||||
ue_cap
|
||||
and (ue_cap["moy_ue"] is not None)
|
||||
and not np.isnan(ue_cap["moy_ue"])
|
||||
):
|
||||
was_capitalized = True
|
||||
if ue_cap["moy_ue"] > cur_moy_ue or np.isnan(cur_moy_ue):
|
||||
moy_ue = ue_cap["moy_ue"]
|
||||
@ -397,7 +403,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
if (not coef_ue) and is_capitalized: # étudiant non inscrit dans l'UE courante
|
||||
if self.is_apc:
|
||||
# Coefs de l'UE capitalisée en formation APC: donné par ses ECTS
|
||||
ue_capitalized = UniteEns.query.get(ue_cap["ue_id"])
|
||||
ue_capitalized = db.session.get(UniteEns, ue_cap["ue_id"])
|
||||
coef_ue = ue_capitalized.ects
|
||||
if coef_ue is None:
|
||||
orig_sem = FormSemestre.get_formsemestre(ue_cap["formsemestre_id"])
|
||||
|
@ -9,9 +9,10 @@
|
||||
from functools import cached_property
|
||||
import pandas as pd
|
||||
|
||||
from flask import flash, g, Markup, url_for
|
||||
from flask import flash, g, url_for
|
||||
from markupsafe import Markup
|
||||
|
||||
from app import log
|
||||
from app import db, log
|
||||
from app.comp import moy_sem
|
||||
from app.comp.aux_stats import StatsMoyenne
|
||||
from app.comp.res_common import ResultatsSemestre
|
||||
@ -283,12 +284,12 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
]
|
||||
return etudids
|
||||
|
||||
def etud_has_decision(self, etudid):
|
||||
def etud_has_decision(self, etudid) -> bool:
|
||||
"""True s'il y a une décision de jury pour cet étudiant émanant de ce formsemestre.
|
||||
prend aussi en compte les autorisations de passage.
|
||||
Sous-classée en BUT pour les RCUEs et années.
|
||||
"""
|
||||
return (
|
||||
return bool(
|
||||
self.get_etud_decisions_ue(etudid)
|
||||
or self.get_etud_decision_sem(etudid)
|
||||
or ScolarAutorisationInscription.query.filter_by(
|
||||
@ -393,7 +394,7 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
de ce module.
|
||||
Évaluation "complete" ssi toutes notes saisies ou en attente.
|
||||
"""
|
||||
modimpl = ModuleImpl.query.get(moduleimpl_id)
|
||||
modimpl = db.session.get(ModuleImpl, moduleimpl_id)
|
||||
modimpl_results = self.modimpls_results.get(moduleimpl_id)
|
||||
if not modimpl_results:
|
||||
return [] # safeguard
|
||||
|
@ -55,6 +55,9 @@ from wtforms.validators import (
|
||||
)
|
||||
from wtforms.widgets import ListWidget, CheckboxInput
|
||||
|
||||
from app import db
|
||||
from app.auth.models import User
|
||||
from app.entreprises import SIRET_PROVISOIRE_START
|
||||
from app.entreprises.models import (
|
||||
Entreprise,
|
||||
EntrepriseCorrespondant,
|
||||
@ -62,9 +65,6 @@ from app.entreprises.models import (
|
||||
EntrepriseSite,
|
||||
EntrepriseTaxeApprentissage,
|
||||
)
|
||||
from app import db
|
||||
from app.auth.models import User
|
||||
from app.entreprises import SIRET_PROVISOIRE_START
|
||||
from app.models import Identite, Departement
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
@ -122,13 +122,13 @@ class EntrepriseCreationForm(FlaskForm):
|
||||
origine = _build_string_field("Origine du correspondant", required=False)
|
||||
notes = _build_string_field("Notes sur le correspondant", required=False)
|
||||
|
||||
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||
submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
|
||||
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
||||
|
||||
def validate(self):
|
||||
def validate(self, extra_validators=None):
|
||||
validate = True
|
||||
if not FlaskForm.validate(self):
|
||||
validate = False
|
||||
if not super().validate(extra_validators):
|
||||
return False
|
||||
|
||||
if EntreprisePreferences.get_check_siret() and self.siret.data != "":
|
||||
siret_data = self.siret.data.strip().replace(" ", "")
|
||||
@ -248,13 +248,13 @@ class SiteCreationForm(FlaskForm):
|
||||
codepostal = _build_string_field("Code postal (*)")
|
||||
ville = _build_string_field("Ville (*)")
|
||||
pays = _build_string_field("Pays", required=False)
|
||||
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||
submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
|
||||
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
||||
|
||||
def validate(self):
|
||||
def validate(self, extra_validators=None):
|
||||
validate = True
|
||||
if not FlaskForm.validate(self):
|
||||
validate = False
|
||||
if not super().validate(extra_validators):
|
||||
return False
|
||||
|
||||
site = EntrepriseSite.query.filter_by(
|
||||
entreprise_id=self.hidden_entreprise_id.data, nom=self.nom.data
|
||||
@ -278,10 +278,10 @@ class SiteModificationForm(FlaskForm):
|
||||
submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE)
|
||||
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
||||
|
||||
def validate(self):
|
||||
def validate(self, extra_validators=None):
|
||||
validate = True
|
||||
if not FlaskForm.validate(self):
|
||||
validate = False
|
||||
if not super().validate(extra_validators):
|
||||
return False
|
||||
|
||||
site = EntrepriseSite.query.filter(
|
||||
EntrepriseSite.entreprise_id == self.hidden_entreprise_id.data,
|
||||
@ -326,7 +326,7 @@ class OffreCreationForm(FlaskForm):
|
||||
FileAllowed(["pdf", "docx"], "Fichier .pdf ou .docx uniquement"),
|
||||
],
|
||||
)
|
||||
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||
submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
|
||||
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -344,10 +344,10 @@ class OffreCreationForm(FlaskForm):
|
||||
(dept.id, dept.acronym) for dept in Departement.query.all()
|
||||
]
|
||||
|
||||
def validate(self):
|
||||
def validate(self, extra_validators=None):
|
||||
validate = True
|
||||
if not FlaskForm.validate(self):
|
||||
validate = False
|
||||
if not super().validate(extra_validators):
|
||||
return False
|
||||
|
||||
if len(self.depts.data) < 1:
|
||||
self.depts.errors.append("Choisir au moins un département")
|
||||
@ -392,10 +392,10 @@ class OffreModificationForm(FlaskForm):
|
||||
(dept.id, dept.acronym) for dept in Departement.query.all()
|
||||
]
|
||||
|
||||
def validate(self):
|
||||
def validate(self, extra_validators=None):
|
||||
validate = True
|
||||
if not FlaskForm.validate(self):
|
||||
validate = False
|
||||
if not super().validate(extra_validators):
|
||||
return False
|
||||
|
||||
if len(self.depts.data) < 1:
|
||||
self.depts.errors.append("Choisir au moins un département")
|
||||
@ -442,10 +442,10 @@ class CorrespondantCreationForm(FlaskForm):
|
||||
"Notes", required=False, render_kw={"class": "form-control"}
|
||||
)
|
||||
|
||||
def validate(self):
|
||||
def validate(self, extra_validators=None):
|
||||
validate = True
|
||||
if not FlaskForm.validate(self):
|
||||
validate = False
|
||||
if not super().validate(extra_validators):
|
||||
return False
|
||||
|
||||
if not self.telephone.data and not self.mail.data:
|
||||
msg = "Saisir un moyen de contact (mail ou téléphone)"
|
||||
@ -458,13 +458,13 @@ class CorrespondantCreationForm(FlaskForm):
|
||||
class CorrespondantsCreationForm(FlaskForm):
|
||||
hidden_site_id = HiddenField()
|
||||
correspondants = FieldList(FormField(CorrespondantCreationForm), min_entries=1)
|
||||
submit = SubmitField("Envoyer")
|
||||
submit = SubmitField("Enregistrer")
|
||||
cancel = SubmitField("Annuler")
|
||||
|
||||
def validate(self):
|
||||
def validate(self, extra_validators=None):
|
||||
validate = True
|
||||
if not FlaskForm.validate(self):
|
||||
validate = False
|
||||
if not super().validate(extra_validators):
|
||||
return False
|
||||
|
||||
correspondant_list = []
|
||||
for entry in self.correspondants.entries:
|
||||
@ -531,10 +531,10 @@ class CorrespondantModificationForm(FlaskForm):
|
||||
.all()
|
||||
]
|
||||
|
||||
def validate(self):
|
||||
def validate(self, extra_validators=None):
|
||||
validate = True
|
||||
if not FlaskForm.validate(self):
|
||||
validate = False
|
||||
if not super().validate(extra_validators):
|
||||
return False
|
||||
|
||||
correspondant = EntrepriseCorrespondant.query.filter(
|
||||
EntrepriseCorrespondant.id != self.hidden_correspondant_id.data,
|
||||
@ -566,7 +566,7 @@ class ContactCreationForm(FlaskForm):
|
||||
render_kw={"placeholder": "Tapez le nom de l'utilisateur"},
|
||||
)
|
||||
notes = TextAreaField("Notes (*)", validators=[DataRequired(message=CHAMP_REQUIS)])
|
||||
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||
submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
|
||||
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
||||
|
||||
def validate_utilisateur(self, utilisateur):
|
||||
@ -613,8 +613,9 @@ class ContactModificationForm(FlaskForm):
|
||||
class StageApprentissageCreationForm(FlaskForm):
|
||||
etudiant = _build_string_field(
|
||||
"Étudiant (*)",
|
||||
render_kw={"placeholder": "Tapez le nom de l'étudiant"},
|
||||
render_kw={"placeholder": "Tapez le nom de l'étudiant", "autocomplete": "off"},
|
||||
)
|
||||
etudid = HiddenField()
|
||||
type_offre = SelectField(
|
||||
"Type de l'offre (*)",
|
||||
choices=[("Stage"), ("Alternance")],
|
||||
@ -627,12 +628,12 @@ class StageApprentissageCreationForm(FlaskForm):
|
||||
"Date fin (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
|
||||
)
|
||||
notes = TextAreaField("Notes")
|
||||
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||
submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
|
||||
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
||||
|
||||
def validate(self):
|
||||
def validate(self, extra_validators=None):
|
||||
validate = True
|
||||
if not FlaskForm.validate(self):
|
||||
if not super().validate(extra_validators):
|
||||
validate = False
|
||||
|
||||
if (
|
||||
@ -646,64 +647,27 @@ class StageApprentissageCreationForm(FlaskForm):
|
||||
|
||||
return validate
|
||||
|
||||
def validate_etudiant(self, etudiant):
|
||||
etudiant_data = etudiant.data.upper().strip()
|
||||
stm = text(
|
||||
"SELECT id, CONCAT(nom, ' ', prenom) as nom_prenom FROM Identite WHERE CONCAT(nom, ' ', prenom)=:nom_prenom"
|
||||
)
|
||||
etudiant = (
|
||||
Identite.query.from_statement(stm).params(nom_prenom=etudiant_data).first()
|
||||
)
|
||||
def validate_etudid(self, field):
|
||||
"L'etudid doit avoit été placé par le JS"
|
||||
etudid = int(field.data) if field.data else None
|
||||
etudiant = db.session.get(Identite, etudid) if etudid is not None else None
|
||||
if etudiant is None:
|
||||
raise ValidationError("Champ incorrect (selectionnez dans la liste)")
|
||||
raise ValidationError("Étudiant introuvable (sélectionnez dans la liste)")
|
||||
|
||||
|
||||
class StageApprentissageModificationForm(FlaskForm):
|
||||
etudiant = _build_string_field(
|
||||
"Étudiant (*)",
|
||||
render_kw={"placeholder": "Tapez le nom de l'étudiant"},
|
||||
)
|
||||
type_offre = SelectField(
|
||||
"Type de l'offre (*)",
|
||||
choices=[("Stage"), ("Alternance")],
|
||||
validators=[DataRequired(message=CHAMP_REQUIS)],
|
||||
)
|
||||
date_debut = DateField(
|
||||
"Date début (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
|
||||
)
|
||||
date_fin = DateField(
|
||||
"Date fin (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
|
||||
)
|
||||
notes = TextAreaField("Notes")
|
||||
submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE)
|
||||
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
||||
class FrenchFloatField(StringField):
|
||||
"A field allowing to enter . or ,"
|
||||
|
||||
def validate(self):
|
||||
validate = True
|
||||
if not FlaskForm.validate(self):
|
||||
validate = False
|
||||
|
||||
if (
|
||||
self.date_debut.data
|
||||
and self.date_fin.data
|
||||
and self.date_debut.data > self.date_fin.data
|
||||
):
|
||||
self.date_debut.errors.append("Les dates sont incompatibles")
|
||||
self.date_fin.errors.append("Les dates sont incompatibles")
|
||||
validate = False
|
||||
|
||||
return validate
|
||||
|
||||
def validate_etudiant(self, etudiant):
|
||||
etudiant_data = etudiant.data.upper().strip()
|
||||
stm = text(
|
||||
"SELECT id, CONCAT(nom, ' ', prenom) as nom_prenom FROM Identite WHERE CONCAT(nom, ' ', prenom)=:nom_prenom"
|
||||
)
|
||||
etudiant = (
|
||||
Identite.query.from_statement(stm).params(nom_prenom=etudiant_data).first()
|
||||
)
|
||||
if etudiant is None:
|
||||
raise ValidationError("Champ incorrect (selectionnez dans la liste)")
|
||||
def process_formdata(self, valuelist):
|
||||
"catch incoming data"
|
||||
if not valuelist:
|
||||
return
|
||||
try:
|
||||
value = valuelist[0].replace(",", ".")
|
||||
self.data = float(value)
|
||||
except ValueError as exc:
|
||||
self.data = None
|
||||
raise ValueError(self.gettext("Not a valid decimal value.")) from exc
|
||||
|
||||
|
||||
class TaxeApprentissageForm(FlaskForm):
|
||||
@ -720,25 +684,26 @@ class TaxeApprentissageForm(FlaskForm):
|
||||
],
|
||||
default=int(datetime.now().strftime("%Y")),
|
||||
)
|
||||
montant = IntegerField(
|
||||
montant = FrenchFloatField(
|
||||
"Montant (*)",
|
||||
validators=[
|
||||
DataRequired(message=CHAMP_REQUIS),
|
||||
NumberRange(
|
||||
min=1,
|
||||
message="Le montant doit être supérieur à 0",
|
||||
),
|
||||
# NumberRange(
|
||||
# min=0.1,
|
||||
# max=1e8,
|
||||
# message="Le montant doit être supérieur à 0",
|
||||
# ),
|
||||
],
|
||||
default=1,
|
||||
)
|
||||
notes = TextAreaField("Notes")
|
||||
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||
submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
|
||||
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
||||
|
||||
def validate(self):
|
||||
def validate(self, extra_validators=None):
|
||||
validate = True
|
||||
if not FlaskForm.validate(self):
|
||||
validate = False
|
||||
if not super().validate(extra_validators):
|
||||
return False
|
||||
|
||||
taxe = EntrepriseTaxeApprentissage.query.filter_by(
|
||||
entreprise_id=self.hidden_entreprise_id.data, annee=self.annee.data
|
||||
@ -788,12 +753,12 @@ class EnvoiOffreForm(FlaskForm):
|
||||
submit = SubmitField("Envoyer")
|
||||
cancel = SubmitField("Annuler")
|
||||
|
||||
def validate(self):
|
||||
def validate(self, extra_validators=None):
|
||||
validate = True
|
||||
list_select = True
|
||||
|
||||
if not FlaskForm.validate(self):
|
||||
validate = False
|
||||
if not super().validate(extra_validators):
|
||||
return False
|
||||
|
||||
for entry in self.responsables.entries:
|
||||
if entry.data:
|
||||
|
@ -164,7 +164,10 @@ class EntrepriseStageApprentissage(db.Model):
|
||||
entreprise_id = db.Column(
|
||||
db.Integer, db.ForeignKey("are_entreprises.id", ondelete="cascade")
|
||||
)
|
||||
etudid = db.Column(db.Integer)
|
||||
etudid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||
)
|
||||
type_offre = db.Column(db.Text)
|
||||
date_debut = db.Column(db.Date)
|
||||
date_fin = db.Column(db.Date)
|
||||
@ -180,7 +183,7 @@ class EntrepriseTaxeApprentissage(db.Model):
|
||||
db.Integer, db.ForeignKey("are_entreprises.id", ondelete="cascade")
|
||||
)
|
||||
annee = db.Column(db.Integer)
|
||||
montant = db.Column(db.Integer)
|
||||
montant = db.Column(db.Float)
|
||||
notes = db.Column(db.Text)
|
||||
|
||||
|
||||
|
@ -28,7 +28,6 @@ from app.entreprises.forms import (
|
||||
ContactCreationForm,
|
||||
ContactModificationForm,
|
||||
StageApprentissageCreationForm,
|
||||
StageApprentissageModificationForm,
|
||||
EnvoiOffreForm,
|
||||
AjoutFichierForm,
|
||||
TaxeApprentissageForm,
|
||||
@ -239,7 +238,7 @@ def delete_validation_entreprise(entreprise_id):
|
||||
text=f"Non validation de la fiche entreprise ({entreprise.nom})",
|
||||
)
|
||||
db.session.add(log)
|
||||
flash("L'entreprise a été supprimé de la liste des entreprise à valider.")
|
||||
flash("L'entreprise a été supprimée de la liste des entreprises à valider.")
|
||||
return redirect(url_for("entreprises.validation"))
|
||||
return render_template(
|
||||
"entreprises/form_confirmation.j2",
|
||||
@ -770,7 +769,7 @@ def delete_taxe_apprentissage(entreprise_id, taxe_id):
|
||||
)
|
||||
db.session.add(log)
|
||||
db.session.commit()
|
||||
flash("La taxe d'apprentissage a été supprimé de la liste.")
|
||||
flash("La taxe d'apprentissage a été supprimée de la liste.")
|
||||
return redirect(
|
||||
url_for("entreprises.fiche_entreprise", entreprise_id=taxe.entreprise_id)
|
||||
)
|
||||
@ -966,7 +965,7 @@ def delete_offre(entreprise_id, offre_id):
|
||||
)
|
||||
db.session.add(log)
|
||||
db.session.commit()
|
||||
flash("L'offre a été supprimé de la fiche entreprise.")
|
||||
flash("L'offre a été supprimée de la fiche entreprise.")
|
||||
return redirect(
|
||||
url_for("entreprises.fiche_entreprise", entreprise_id=offre.entreprise_id)
|
||||
)
|
||||
@ -1473,7 +1472,8 @@ def delete_contact(entreprise_id, contact_id):
|
||||
@permission_required(Permission.RelationsEntreprisesChange)
|
||||
def add_stage_apprentissage(entreprise_id):
|
||||
"""
|
||||
Permet d'ajouter un étudiant ayant réalisé un stage ou une alternance sur la fiche entreprise de l'entreprise
|
||||
Permet d'ajouter un étudiant ayant réalisé un stage ou alternance
|
||||
sur la fiche de l'entreprise
|
||||
"""
|
||||
entreprise = Entreprise.query.filter_by(
|
||||
id=entreprise_id, visible=True
|
||||
@ -1484,15 +1484,8 @@ def add_stage_apprentissage(entreprise_id):
|
||||
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise_id)
|
||||
)
|
||||
if form.validate_on_submit():
|
||||
etudiant_nomcomplet = form.etudiant.data.upper().strip()
|
||||
stm = text(
|
||||
"SELECT id, CONCAT(nom, ' ', prenom) as nom_prenom FROM Identite WHERE CONCAT(nom, ' ', prenom)=:nom_prenom"
|
||||
)
|
||||
etudiant = (
|
||||
Identite.query.from_statement(stm)
|
||||
.params(nom_prenom=etudiant_nomcomplet)
|
||||
.first()
|
||||
)
|
||||
etudid = form.etudid.data
|
||||
etudiant = Identite.query.get_or_404(etudid)
|
||||
formation = etudiant.inscription_courante_date(
|
||||
form.date_debut.data, form.date_fin.data
|
||||
)
|
||||
@ -1538,7 +1531,7 @@ def add_stage_apprentissage(entreprise_id):
|
||||
@permission_required(Permission.RelationsEntreprisesChange)
|
||||
def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
|
||||
"""
|
||||
Permet de modifier un étudiant ayant réalisé un stage ou une alternance sur la fiche entreprise de l'entreprise
|
||||
Permet de modifier un étudiant ayant réalisé un stage ou alternance sur la fiche de l'entreprise
|
||||
"""
|
||||
stage_apprentissage = EntrepriseStageApprentissage.query.filter_by(
|
||||
id=stage_apprentissage_id, entreprise_id=entreprise_id
|
||||
@ -1548,21 +1541,14 @@ def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
|
||||
etudiant = Identite.query.filter_by(id=stage_apprentissage.etudid).first_or_404(
|
||||
description=f"etudiant {stage_apprentissage.etudid} inconnue"
|
||||
)
|
||||
form = StageApprentissageModificationForm()
|
||||
form = StageApprentissageCreationForm()
|
||||
if request.method == "POST" and form.cancel.data:
|
||||
return redirect(
|
||||
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise_id)
|
||||
)
|
||||
if form.validate_on_submit():
|
||||
etudiant_nomcomplet = form.etudiant.data.upper().strip()
|
||||
stm = text(
|
||||
"SELECT id, CONCAT(nom, ' ', prenom) as nom_prenom FROM Identite WHERE CONCAT(nom, ' ', prenom)=:nom_prenom"
|
||||
)
|
||||
etudiant = (
|
||||
Identite.query.from_statement(stm)
|
||||
.params(nom_prenom=etudiant_nomcomplet)
|
||||
.first()
|
||||
)
|
||||
etudid = form.etudid.data
|
||||
etudiant = Identite.query.get_or_404(etudid)
|
||||
formation = etudiant.inscription_courante_date(
|
||||
form.date_debut.data, form.date_fin.data
|
||||
)
|
||||
@ -1577,6 +1563,7 @@ def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
|
||||
formation.formsemestre.formsemestre_id if formation else None,
|
||||
)
|
||||
stage_apprentissage.notes = form.notes.data.strip()
|
||||
db.session.add(stage_apprentissage)
|
||||
log = EntrepriseHistorique(
|
||||
authenticated_user=current_user.user_name,
|
||||
entreprise_id=stage_apprentissage.entreprise_id,
|
||||
@ -1593,7 +1580,9 @@ def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
|
||||
)
|
||||
)
|
||||
elif request.method == "GET":
|
||||
form.etudiant.data = f"{sco_etud.format_nom(etudiant.nom)} {sco_etud.format_prenom(etudiant.prenom)}"
|
||||
form.etudiant.data = f"""{sco_etud.format_nom(etudiant.nom)} {
|
||||
sco_etud.format_prenom(etudiant.prenom)}"""
|
||||
form.etudid.data = etudiant.id
|
||||
form.type_offre.data = stage_apprentissage.type_offre
|
||||
form.date_debut.data = stage_apprentissage.date_debut
|
||||
form.date_fin.data = stage_apprentissage.date_fin
|
||||
|
@ -65,6 +65,7 @@ class CodesDecisionsForm(FlaskForm):
|
||||
ADJ = _build_code_field("ADJ")
|
||||
ADJR = _build_code_field("ADJR")
|
||||
ADM = _build_code_field("ADM")
|
||||
ADSUP = _build_code_field("ADSUP")
|
||||
AJ = _build_code_field("AJ")
|
||||
ATB = _build_code_field("ATB")
|
||||
ATJ = _build_code_field("ATJ")
|
||||
@ -81,7 +82,8 @@ class CodesDecisionsForm(FlaskForm):
|
||||
|
||||
NOTES_FMT = StringField(
|
||||
label="Format notes exportées",
|
||||
description="""Format des notes. Par défaut <tt style="font-family: monotype;">%3.2f</tt> (deux chiffres après la virgule)""",
|
||||
description="""Format des notes. Par défaut
|
||||
<tt style="font-family: monotype;">%3.2f</tt> (deux chiffres après la virgule)""",
|
||||
validators=[
|
||||
validators.Length(
|
||||
max=SHORT_STR_LEN,
|
||||
|
@ -81,5 +81,3 @@ from app.models.but_refcomp import (
|
||||
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
||||
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
|
||||
from app.models.assiduites import Assiduite, Justificatif
|
||||
|
@ -1,341 +0,0 @@
|
||||
# -*- 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_utils import (
|
||||
EtatAssiduite,
|
||||
EtatJustificatif,
|
||||
localize_datetime,
|
||||
is_period_overlapping,
|
||||
)
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_utils import (
|
||||
EtatAssiduite,
|
||||
EtatJustificatif,
|
||||
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, nullable=False)
|
||||
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())
|
||||
|
||||
user_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("user.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
)
|
||||
|
||||
est_just = db.Column(db.Boolean, server_default="false", nullable=False)
|
||||
|
||||
def to_dict(self, format_api=True) -> dict:
|
||||
"""Retourne la représentation json de l'assiduité"""
|
||||
etat = self.etat
|
||||
|
||||
if format_api:
|
||||
etat = EtatAssiduite.inverse().get(self.etat).name
|
||||
data = {
|
||||
"assiduite_id": self.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,
|
||||
"user_id": self.user_id,
|
||||
"est_just": self.est_just,
|
||||
}
|
||||
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,
|
||||
user_id: int = None,
|
||||
est_just: bool = False,
|
||||
) -> 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,
|
||||
user_id=user_id,
|
||||
est_just=est_just,
|
||||
)
|
||||
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,
|
||||
user_id=user_id,
|
||||
est_just=est_just,
|
||||
)
|
||||
|
||||
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,
|
||||
est_just: bool = False,
|
||||
) -> 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,
|
||||
est_just=est_just,
|
||||
)
|
||||
|
||||
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())
|
||||
|
||||
user_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("user.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
index=True,
|
||||
)
|
||||
|
||||
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,
|
||||
"user_id": self.user_id,
|
||||
}
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def create_justificatif(
|
||||
cls,
|
||||
etud: Identite,
|
||||
date_debut: datetime,
|
||||
date_fin: datetime,
|
||||
etat: EtatJustificatif,
|
||||
raison: str = None,
|
||||
entry_date: datetime = None,
|
||||
user_id: int = 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,
|
||||
etudiant=etud,
|
||||
raison=raison,
|
||||
entry_date=entry_date,
|
||||
user_id=user_id,
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
def compute_assiduites_justified(
|
||||
justificatifs: Justificatif = Justificatif, reset: bool = False
|
||||
) -> list[int]:
|
||||
"""Calcule et modifie les champs "est_just" de chaque assiduité lié à l'étud
|
||||
retourne la liste des assiduite_id justifiées
|
||||
|
||||
Si reset alors : met à false toutes les assiduités non justifiées par les justificatifs donnés
|
||||
"""
|
||||
|
||||
list_assiduites_id: set[int] = set()
|
||||
for justi in justificatifs:
|
||||
assiduites: Assiduite = (
|
||||
Assiduite.query.join(Justificatif, Justificatif.etudid == Assiduite.etudid)
|
||||
.filter(justi.etat == EtatJustificatif.VALIDE)
|
||||
.filter(
|
||||
Assiduite.date_debut < justi.date_fin,
|
||||
Assiduite.date_fin > justi.date_debut,
|
||||
)
|
||||
)
|
||||
|
||||
for assi in assiduites:
|
||||
assi.est_just = True
|
||||
list_assiduites_id.add(assi.id)
|
||||
db.session.add(assi)
|
||||
|
||||
if reset:
|
||||
un_justified: Assiduite = Assiduite.query.filter(
|
||||
Assiduite.id.not_in(list_assiduites_id)
|
||||
).join(Justificatif, Justificatif.etudid == Assiduite.etudid)
|
||||
|
||||
for assi in un_justified:
|
||||
assi.est_just = False
|
||||
db.session.add(assi)
|
||||
|
||||
db.session.commit()
|
||||
return list(list_assiduites_id)
|
@ -9,6 +9,7 @@ from datetime import datetime
|
||||
import functools
|
||||
from operator import attrgetter
|
||||
|
||||
from flask import g
|
||||
from flask_sqlalchemy.query import Query
|
||||
from sqlalchemy.orm import class_mapper
|
||||
import sqlalchemy
|
||||
@ -93,6 +94,11 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
||||
backref="referentiel_competence",
|
||||
order_by="Formation.acronyme, Formation.version",
|
||||
)
|
||||
validations_annee = db.relationship(
|
||||
"ApcValidationAnnee",
|
||||
backref="referentiel_competence",
|
||||
lazy="dynamic",
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ApcReferentielCompetences {self.id} {self.specialite!r} {self.departement!r}>"
|
||||
@ -358,6 +364,9 @@ class ApcNiveau(db.Model, XMLModel):
|
||||
return f"""<{self.__class__.__name__} {self.id} ordre={self.ordre!r} annee={
|
||||
self.annee!r} {self.competence!r}>"""
|
||||
|
||||
def __str__(self):
|
||||
return f"""{self.competence.titre} niveau {self.ordre}"""
|
||||
|
||||
def to_dict(self, with_app_critiques=True):
|
||||
"as a dict, recursif (ou non) sur les AC"
|
||||
return {
|
||||
@ -388,7 +397,9 @@ class ApcNiveau(db.Model, XMLModel):
|
||||
return (
|
||||
ApcParcours.query.join(ApcAnneeParcours)
|
||||
.filter_by(ordre=annee)
|
||||
.join(ApcParcoursNiveauCompetence, ApcCompetence, ApcNiveau)
|
||||
.join(ApcParcoursNiveauCompetence)
|
||||
.join(ApcCompetence)
|
||||
.join(ApcNiveau)
|
||||
.filter_by(id=self.id)
|
||||
.order_by(ApcParcours.numero, ApcParcours.code)
|
||||
.all()
|
||||
@ -412,6 +423,20 @@ class ApcNiveau(db.Model, XMLModel):
|
||||
(dans ce cas, spécifier referentiel_competence)
|
||||
Si competence est indiquée, filtre les niveaux de cette compétence.
|
||||
"""
|
||||
key = (
|
||||
parcour.id if parcour else None,
|
||||
annee,
|
||||
referentiel_competence.id if referentiel_competence else None,
|
||||
competence.id if competence else None,
|
||||
)
|
||||
_cache = getattr(g, "_niveaux_annee_de_parcours_cache", None)
|
||||
if _cache:
|
||||
result = g._niveaux_annee_de_parcours_cache.get(key, False)
|
||||
if result is not False:
|
||||
return result
|
||||
else:
|
||||
g._niveaux_annee_de_parcours_cache = {}
|
||||
_cache = g._niveaux_annee_de_parcours_cache
|
||||
if annee not in {1, 2, 3}:
|
||||
raise ValueError("annee invalide pour un parcours BUT")
|
||||
referentiel_competence = (
|
||||
@ -428,10 +453,13 @@ class ApcNiveau(db.Model, XMLModel):
|
||||
)
|
||||
if competence is not None:
|
||||
query = query.filter(ApcCompetence.id == competence.id)
|
||||
return query.all()
|
||||
result = query.all()
|
||||
_cache[key] = result
|
||||
return result
|
||||
|
||||
annee_parcour: ApcAnneeParcours = parcour.annees.filter_by(ordre=annee).first()
|
||||
if not annee_parcour:
|
||||
_cache[key] = []
|
||||
return []
|
||||
|
||||
if competence is None:
|
||||
@ -443,9 +471,17 @@ class ApcNiveau(db.Model, XMLModel):
|
||||
for pn in parcour_niveaux
|
||||
]
|
||||
else:
|
||||
niveaux: list[ApcNiveau] = competence.niveaux.filter_by(
|
||||
annee=f"BUT{int(annee)}"
|
||||
).all()
|
||||
niveaux: list[ApcNiveau] = (
|
||||
ApcNiveau.query.filter_by(annee=f"BUT{int(annee)}")
|
||||
.join(ApcCompetence)
|
||||
.filter_by(id=competence.id)
|
||||
.join(ApcParcoursNiveauCompetence)
|
||||
.filter(ApcParcoursNiveauCompetence.niveau == ApcNiveau.ordre)
|
||||
.join(ApcAnneeParcours)
|
||||
.filter_by(parcours_id=parcour.id)
|
||||
.all()
|
||||
)
|
||||
_cache[key] = niveaux
|
||||
return niveaux
|
||||
|
||||
|
||||
@ -587,7 +623,8 @@ class ApcParcours(db.Model, XMLModel):
|
||||
def query_competences(self) -> Query:
|
||||
"Les compétences associées à ce parcours"
|
||||
return (
|
||||
ApcCompetence.query.join(ApcParcoursNiveauCompetence, ApcAnneeParcours)
|
||||
ApcCompetence.query.join(ApcParcoursNiveauCompetence)
|
||||
.join(ApcAnneeParcours)
|
||||
.filter_by(parcours_id=self.id)
|
||||
.order_by(ApcCompetence.numero)
|
||||
)
|
||||
@ -596,7 +633,8 @@ class ApcParcours(db.Model, XMLModel):
|
||||
"La compétence de titre donné dans ce parcours, ou None"
|
||||
return (
|
||||
ApcCompetence.query.filter_by(titre=titre)
|
||||
.join(ApcParcoursNiveauCompetence, ApcAnneeParcours)
|
||||
.join(ApcParcoursNiveauCompetence)
|
||||
.join(ApcAnneeParcours)
|
||||
.filter_by(parcours_id=self.id)
|
||||
.order_by(ApcCompetence.numero)
|
||||
.first()
|
||||
|
@ -2,9 +2,6 @@
|
||||
|
||||
"""Décisions de jury (validations) des RCUE et années du BUT
|
||||
"""
|
||||
from typing import Union
|
||||
|
||||
from flask_sqlalchemy.query import Query
|
||||
|
||||
from app import db
|
||||
from app.models import CODE_STR_LEN
|
||||
@ -13,8 +10,6 @@ from app.models.etudiants import Identite
|
||||
from app.models.formations import Formation
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.models.ues import UniteEns
|
||||
from app.scodoc import codes_cursus as sco_codes
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
class ApcValidationRCUE(db.Model):
|
||||
@ -22,7 +17,7 @@ class ApcValidationRCUE(db.Model):
|
||||
|
||||
aka "regroupements cohérents d'UE" dans le jargon BUT.
|
||||
|
||||
Le formsemestre est celui du semestre PAIR du niveau de compétence
|
||||
Le formsemestre est l'origine, utilisé pour effacer
|
||||
"""
|
||||
|
||||
__tablename__ = "apc_validation_rcue"
|
||||
@ -41,7 +36,7 @@ class ApcValidationRCUE(db.Model):
|
||||
formsemestre_id = db.Column(
|
||||
db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, nullable=True
|
||||
)
|
||||
"formsemestre pair du RCUE"
|
||||
"formsemestre origine du RCUE (celui d'où a été émis la validation)"
|
||||
# Les deux UE associées à ce niveau:
|
||||
ue1_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False)
|
||||
ue2_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False)
|
||||
@ -66,7 +61,7 @@ class ApcValidationRCUE(db.Model):
|
||||
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:
|
||||
def html(self) -> str:
|
||||
"description en HTML"
|
||||
return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}:
|
||||
<b>{self.code}</b>
|
||||
@ -87,6 +82,10 @@ class ApcValidationRCUE(db.Model):
|
||||
"as a dict"
|
||||
d = dict(self.__dict__)
|
||||
d.pop("_sa_instance_state", None)
|
||||
d["etud"] = self.etud.to_dict_short()
|
||||
d["ue1"] = self.ue1.to_dict()
|
||||
d["ue2"] = self.ue2.to_dict()
|
||||
|
||||
return d
|
||||
|
||||
def to_dict_bul(self) -> dict:
|
||||
@ -109,204 +108,14 @@ class ApcValidationRCUE(db.Model):
|
||||
}
|
||||
|
||||
|
||||
# Attention: ce n'est pas un modèle mais une classe ordinaire:
|
||||
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 RCUE déclenche la compensation des UE.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
etud: Identite,
|
||||
formsemestre_1: FormSemestre,
|
||||
dec_ue_1: "DecisionsProposeesUE",
|
||||
formsemestre_2: FormSemestre,
|
||||
dec_ue_2: "DecisionsProposeesUE",
|
||||
inscription_etat: str,
|
||||
):
|
||||
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_1, formsemestre_1),
|
||||
)
|
||||
assert formsemestre_1.semestre_id % 2 == 1
|
||||
assert formsemestre_2.semestre_id % 2 == 0
|
||||
assert abs(formsemestre_1.semestre_id - formsemestre_2.semestre_id) == 1
|
||||
assert ue_1.niveau_competence_id == ue_2.niveau_competence_id
|
||||
self.etud = etud
|
||||
self.formsemestre_1 = formsemestre_1
|
||||
"semestre impair"
|
||||
self.ue_1 = ue_1
|
||||
self.formsemestre_2 = formsemestre_2
|
||||
"semestre pair"
|
||||
self.ue_2 = ue_2
|
||||
# Stocke les moyennes d'UE
|
||||
if inscription_etat != scu.INSCRIT:
|
||||
self.moy_rcue = None
|
||||
self.moy_ue_1 = self.moy_ue_2 = "-"
|
||||
self.moy_ue_1_val = self.moy_ue_2_val = 0.0
|
||||
return
|
||||
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 = (
|
||||
self.moy_ue_1 * ue_1.coef_rcue + self.moy_ue_2 * ue_2.coef_rcue
|
||||
) / (ue_1.coef_rcue + ue_2.coef_rcue)
|
||||
else:
|
||||
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})>"""
|
||||
|
||||
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,
|
||||
) -> Query: # list[ApcValidationRCUE]
|
||||
"""Les validations de jury enregistrées pour ce RCUE"""
|
||||
niveau = self.ue_2.niveau_competence
|
||||
|
||||
return (
|
||||
ApcValidationRCUE.query.filter_by(
|
||||
etudid=self.etud.id,
|
||||
)
|
||||
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue2_id)
|
||||
.join(ApcNiveau, UniteEns.niveau_competence_id == ApcNiveau.id)
|
||||
.filter(ApcNiveau.id == niveau.id)
|
||||
)
|
||||
|
||||
def other_ue(self, ue: UniteEns) -> UniteEns:
|
||||
"""L'autre UE du regroupement. Si ue ne fait pas partie du regroupement, ValueError"""
|
||||
if ue.id == self.ue_1.id:
|
||||
return self.ue_2
|
||||
elif ue.id == self.ue_2.id:
|
||||
return self.ue_1
|
||||
raise ValueError(f"ue {ue} hors RCUE {self}")
|
||||
|
||||
def est_enregistre(self) -> bool:
|
||||
"""Vrai si ce RCUE, donc le niveau de compétences correspondant
|
||||
a une décision jury enregistrée
|
||||
"""
|
||||
return self.query_validations().count() > 0
|
||||
|
||||
def est_compensable(self):
|
||||
"""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)
|
||||
and (self.moy_rcue > sco_codes.BUT_BARRE_RCUE)
|
||||
and (
|
||||
(self.moy_ue_1_val < sco_codes.NOTES_BARRE_GEN)
|
||||
or (self.moy_ue_2_val < sco_codes.NOTES_BARRE_GEN)
|
||||
)
|
||||
)
|
||||
|
||||
def est_suffisant(self) -> bool:
|
||||
"""Vrai si ce RCUE est > 8"""
|
||||
return (self.moy_rcue is not None) and (
|
||||
self.moy_rcue > sco_codes.BUT_RCUE_SUFFISANT
|
||||
)
|
||||
|
||||
def est_validable(self) -> bool:
|
||||
"""Vrai si ce RCUE satisfait les conditions pour être validé,
|
||||
c'est à dire que la moyenne des UE qui le constituent soit > 10
|
||||
"""
|
||||
return (self.moy_rcue is not None) and (
|
||||
self.moy_rcue > sco_codes.BUT_BARRE_RCUE
|
||||
)
|
||||
|
||||
def code_valide(self) -> Union[ApcValidationRCUE, None]:
|
||||
"Si ce RCUE est ADM, CMP ou ADJ, la validation. Sinon, None"
|
||||
validation = self.query_validations().first()
|
||||
if (validation is not None) and (
|
||||
validation.code in sco_codes.CODES_RCUE_VALIDES
|
||||
):
|
||||
return validation
|
||||
return None
|
||||
|
||||
|
||||
# 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.
|
||||
|
||||
# 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 []
|
||||
|
||||
# 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
|
||||
|
||||
# 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.get_formsemestre(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):
|
||||
"""Validation des années du BUT"""
|
||||
|
||||
__tablename__ = "apc_validation_annee"
|
||||
# Assure unicité de la décision:
|
||||
__table_args__ = (db.UniqueConstraint("etudid", "annee_scolaire", "ordre"),)
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint("etudid", "ordre", "referentiel_competence_id"),
|
||||
)
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
etudid = db.Column(
|
||||
db.Integer,
|
||||
@ -319,8 +128,11 @@ class ApcValidationAnnee(db.Model):
|
||||
formsemestre_id = db.Column(
|
||||
db.Integer, db.ForeignKey("notes_formsemestre.id"), nullable=True
|
||||
)
|
||||
"le semestre IMPAIR (le 1er) de l'année"
|
||||
annee_scolaire = db.Column(db.Integer, nullable=False) # 2021
|
||||
"le semestre origine, normalement l'IMPAIR (le 1er) de l'année"
|
||||
referentiel_competence_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_referentiel_competences.id"), nullable=False
|
||||
)
|
||||
annee_scolaire = db.Column(db.Integer, nullable=False) # eg 2021
|
||||
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True)
|
||||
|
||||
@ -338,25 +150,50 @@ class ApcValidationAnnee(db.Model):
|
||||
"dict pour bulletins"
|
||||
return {
|
||||
"annee_scolaire": self.annee_scolaire,
|
||||
"date": self.date.isoformat(),
|
||||
"date": self.date.isoformat() if self.date else "",
|
||||
"code": self.code,
|
||||
"ordre": self.ordre,
|
||||
}
|
||||
|
||||
def html(self) -> str:
|
||||
"Affichage html"
|
||||
date_str = (
|
||||
f"""le {self.date.strftime("%d/%m/%Y")} à {self.date.strftime("%Hh%M")}"""
|
||||
if self.date
|
||||
else "(sans date)"
|
||||
)
|
||||
link = (
|
||||
self.formsemestre.html_link_status(
|
||||
label=f"{self.formsemestre.titre_formation(with_sem_idx=1)}",
|
||||
title=self.formsemestre.titre_annee(),
|
||||
)
|
||||
if self.formsemestre
|
||||
else "externe/antérieure"
|
||||
)
|
||||
return f"""Validation <b>année BUT{self.ordre}</b> émise par
|
||||
{link}
|
||||
: <b>{self.code}</b>
|
||||
{date_str}
|
||||
"""
|
||||
|
||||
|
||||
def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
|
||||
"""
|
||||
Un dict avec les décisions de jury BUT enregistrées:
|
||||
- decision_rcue : list[dict]
|
||||
- decision_annee : dict
|
||||
- decision_annee : dict (décision issue de ce semestre seulement (à confirmer ?))
|
||||
Ne reprend pas les décisions d'UE, non spécifiques au BUT.
|
||||
"""
|
||||
decisions = {}
|
||||
# --- RCUEs: seulement sur semestres pairs XXX à améliorer
|
||||
if formsemestre.semestre_id % 2 == 0:
|
||||
# validations émises depuis ce formsemestre:
|
||||
validations_rcues = ApcValidationRCUE.query.filter_by(
|
||||
etudid=etud.id, formsemestre_id=formsemestre.id
|
||||
validations_rcues = (
|
||||
ApcValidationRCUE.query.filter_by(
|
||||
etudid=etud.id, formsemestre_id=formsemestre.id
|
||||
)
|
||||
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id)
|
||||
.order_by(UniteEns.numero, UniteEns.acronyme)
|
||||
)
|
||||
decisions["decision_rcue"] = [v.to_dict_bul() for v in validations_rcues]
|
||||
titres_rcues = []
|
||||
@ -378,16 +215,11 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
|
||||
decisions["descr_decisions_rcue"] = ""
|
||||
decisions["descr_decisions_niveaux"] = ""
|
||||
# --- Année: prend la validation pour l'année scolaire de ce semestre
|
||||
validation = (
|
||||
ApcValidationAnnee.query.filter_by(
|
||||
etudid=etud.id,
|
||||
annee_scolaire=formsemestre.annee_scolaire(),
|
||||
)
|
||||
.join(ApcValidationAnnee.formsemestre)
|
||||
.join(FormSemestre.formation)
|
||||
.filter(Formation.formation_code == formsemestre.formation.formation_code)
|
||||
.first()
|
||||
)
|
||||
validation = ApcValidationAnnee.query.filter_by(
|
||||
etudid=etud.id,
|
||||
annee_scolaire=formsemestre.annee_scolaire(),
|
||||
referentiel_competence_id=formsemestre.formation.referentiel_competence_id,
|
||||
).first()
|
||||
if validation:
|
||||
decisions["decision_annee"] = validation.to_dict_bul()
|
||||
else:
|
||||
|
@ -15,6 +15,7 @@ from app.scodoc.codes_cursus import (
|
||||
ADJ,
|
||||
ADJR,
|
||||
ADM,
|
||||
ADSUP,
|
||||
AJ,
|
||||
ATB,
|
||||
ATJ,
|
||||
@ -37,6 +38,7 @@ CODES_SCODOC_TO_APO = {
|
||||
ADJ: "ADM",
|
||||
ADJR: "ADM",
|
||||
ADM: "ADM",
|
||||
ADSUP: "ADM",
|
||||
AJ: "AJ",
|
||||
ATB: "AJAC",
|
||||
ATJ: "AJAC",
|
||||
|
@ -43,8 +43,8 @@ class Identite(db.Model):
|
||||
"optionnel (si present, affiché à la place du nom)"
|
||||
civilite = db.Column(db.String(1), nullable=False)
|
||||
|
||||
# données d'état-civil. Si présent remplace les données d'usage dans les documents officiels (bulletins, PV)
|
||||
# cf nomprenom_etat_civil()
|
||||
# données d'état-civil. Si présent remplace les données d'usage dans les documents
|
||||
# officiels (bulletins, PV): voir nomprenom_etat_civil()
|
||||
civilite_etat_civil = db.Column(db.String(1), nullable=False, server_default="X")
|
||||
prenom_etat_civil = db.Column(db.Text(), nullable=False, server_default="")
|
||||
|
||||
@ -73,15 +73,17 @@ class Identite(db.Model):
|
||||
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 (
|
||||
f"<Etud {self.id}/{self.departement.acronym} {self.nom!r} {self.prenom!r}>"
|
||||
)
|
||||
|
||||
def html_link_fiche(self) -> str:
|
||||
"lien vers la fiche"
|
||||
return f"""<a class="stdlink" href="{
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=self.id)
|
||||
}">{self.nomprenom}</a>"""
|
||||
|
||||
@classmethod
|
||||
def from_request(cls, etudid=None, code_nip=None) -> "Identite":
|
||||
"""Étudiant à partir de l'etudid ou du code_nip, soit
|
||||
@ -224,7 +226,7 @@ class Identite(db.Model):
|
||||
}
|
||||
args_dict = {}
|
||||
for key, value in args.items():
|
||||
if hasattr(cls, key):
|
||||
if hasattr(cls, key) and not isinstance(getattr(cls, key, None), property):
|
||||
# compat scodoc7 (mauvaise idée de l'époque)
|
||||
if key in fs_empty_stored_as_nulls and value == "":
|
||||
value = None
|
||||
|
@ -145,6 +145,18 @@ class Evaluation(db.Model):
|
||||
db.session.add(copy)
|
||||
return copy
|
||||
|
||||
def is_matin(self) -> bool:
|
||||
"Evaluation ayant lieu le matin (faux si pas de date)"
|
||||
heure_debut_dt = self.heure_debut or datetime.time(8, 00)
|
||||
# 8:00 au cas ou pas d'heure (note externe?)
|
||||
return bool(self.jour) and heure_debut_dt < datetime.time(12, 00)
|
||||
|
||||
def is_apresmidi(self) -> bool:
|
||||
"Evaluation ayant lieu l'après midi (faux si pas de date)"
|
||||
heure_debut_dt = self.heure_debut or datetime.time(8, 00)
|
||||
# 8:00 au cas ou pas d'heure (note externe?)
|
||||
return bool(self.jour) and heure_debut_dt >= datetime.time(12, 00)
|
||||
|
||||
def set_default_poids(self) -> bool:
|
||||
"""Initialize les poids bvers les UE à leurs valeurs par défaut
|
||||
C'est à dire à 1 si le coef. module/UE est non nul, 0 sinon.
|
||||
@ -178,8 +190,10 @@ class Evaluation(db.Model):
|
||||
"""
|
||||
L = []
|
||||
for ue_id, poids in ue_poids_dict.items():
|
||||
ue = UniteEns.query.get(ue_id)
|
||||
L.append(EvaluationUEPoids(evaluation=self, ue=ue, poids=poids))
|
||||
ue = db.session.get(UniteEns, ue_id)
|
||||
ue_poids = EvaluationUEPoids(evaluation=self, ue=ue, poids=poids)
|
||||
L.append(ue_poids)
|
||||
db.session.add(ue_poids)
|
||||
self.ue_poids = L # backref # pylint:disable=attribute-defined-outside-init
|
||||
self.moduleimpl.invalidate_evaluations_poids() # inval cache
|
||||
|
||||
@ -326,7 +340,7 @@ def check_evaluation_args(args):
|
||||
jour = args.get("jour", None)
|
||||
args["jour"] = jour
|
||||
if jour:
|
||||
modimpl = ModuleImpl.query.get(moduleimpl_id)
|
||||
modimpl = db.session.get(ModuleImpl, moduleimpl_id)
|
||||
formsemestre = modimpl.formsemestre
|
||||
y, m, d = [int(x) for x in ndb.DateDMYtoISO(jour).split("-")]
|
||||
jour = datetime.date(y, m, d)
|
||||
|
@ -54,14 +54,17 @@ class ScolarNews(db.Model):
|
||||
NEWS_APO = "APO" # changements de codes APO
|
||||
NEWS_FORM = "FORM" # modification formation (object=formation_id)
|
||||
NEWS_INSCR = "INSCR" # inscription d'étudiants (object=None ou formsemestre_id)
|
||||
NEWS_JURY = "JURY" # saisie jury
|
||||
NEWS_MISC = "MISC" # unused
|
||||
NEWS_NOTE = "NOTES" # saisie note (object=moduleimpl_id)
|
||||
NEWS_SEM = "SEM" # creation semestre (object=None)
|
||||
|
||||
NEWS_MAP = {
|
||||
NEWS_ABS: "saisie absence",
|
||||
NEWS_APO: "modif. code Apogée",
|
||||
NEWS_FORM: "modification formation",
|
||||
NEWS_INSCR: "inscription d'étudiants",
|
||||
NEWS_JURY: "saisie jury",
|
||||
NEWS_MISC: "opération", # unused
|
||||
NEWS_NOTE: "saisie note",
|
||||
NEWS_SEM: "création semestre",
|
||||
@ -130,10 +133,10 @@ class ScolarNews(db.Model):
|
||||
return query.order_by(cls.date.desc()).limit(n).all()
|
||||
|
||||
@classmethod
|
||||
def add(cls, typ, obj=None, text="", url=None, max_frequency=0):
|
||||
def add(cls, typ, obj=None, text="", url=None, max_frequency=600):
|
||||
"""Enregistre une nouvelle
|
||||
Si max_frequency, ne génère pas 2 nouvelles "identiques"
|
||||
à moins de max_frequency secondes d'intervalle.
|
||||
à moins de max_frequency secondes d'intervalle (10 minutes par défaut).
|
||||
Deux nouvelles sont considérées comme "identiques" si elles ont
|
||||
même (obj, typ, user).
|
||||
La nouvelle enregistrée est aussi envoyée par mail.
|
||||
@ -153,7 +156,10 @@ class ScolarNews(db.Model):
|
||||
if last_news:
|
||||
now = datetime.datetime.now(tz=last_news.date.tzinfo)
|
||||
if (now - last_news.date) < datetime.timedelta(seconds=max_frequency):
|
||||
# on n'enregistre pas
|
||||
# pas de nouvel event, mais met à jour l'heure
|
||||
last_news.date = datetime.datetime.now()
|
||||
db.session.add(last_news)
|
||||
db.session.commit()
|
||||
return
|
||||
|
||||
news = ScolarNews(
|
||||
@ -181,14 +187,14 @@ class ScolarNews(db.Model):
|
||||
elif self.type == self.NEWS_NOTE:
|
||||
moduleimpl_id = self.object
|
||||
if moduleimpl_id:
|
||||
modimpl = ModuleImpl.query.get(moduleimpl_id)
|
||||
modimpl = db.session.get(ModuleImpl, moduleimpl_id)
|
||||
if modimpl is None:
|
||||
return None # module does not exists anymore
|
||||
formsemestre_id = modimpl.formsemestre_id
|
||||
|
||||
if not formsemestre_id:
|
||||
return None
|
||||
formsemestre = FormSemestre.query.get(formsemestre_id)
|
||||
formsemestre = db.session.get(FormSemestre, formsemestre_id)
|
||||
return formsemestre
|
||||
|
||||
def notify_by_mail(self):
|
||||
@ -259,11 +265,8 @@ class ScolarNews(db.Model):
|
||||
|
||||
# Informations générales
|
||||
H.append(
|
||||
f"""<div>
|
||||
Pour être informé des évolutions de ScoDoc,
|
||||
vous pouvez vous
|
||||
<a class="stdlink" href="{scu.SCO_ANNONCES_WEBSITE}">
|
||||
abonner à la liste de diffusion</a>.
|
||||
f"""<div><a class="discretelink" href="{scu.SCO_ANNONCES_WEBSITE}">
|
||||
Pour en savoir plus sur ScoDoc voir le site scodoc.org</a>.
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
|
@ -60,7 +60,7 @@ class Formation(db.Model):
|
||||
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:
|
||||
def html(self) -> str:
|
||||
"titre complet pour affichage"
|
||||
return f"""Formation {self.titre} ({self.acronyme}) [version {self.version}] code {self.formation_code}"""
|
||||
|
||||
|
@ -16,7 +16,7 @@ from operator import attrgetter
|
||||
|
||||
from flask_login import current_user
|
||||
|
||||
from flask import flash, g
|
||||
from flask import flash, g, url_for
|
||||
from sqlalchemy.sql import text
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
@ -163,6 +163,14 @@ class FormSemestre(db.Model):
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} {self.id} {self.titre_annee()}>"
|
||||
|
||||
def html_link_status(self, label=None, title=None) -> str:
|
||||
"html link to status page"
|
||||
return f"""<a class="stdlink" href="{
|
||||
url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=self.id,)
|
||||
}" title="{title or ''}">{label or self.titre_mois()}</a>
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_formsemestre(cls, formsemestre_id: int) -> "FormSemestre":
|
||||
""" "FormSemestre ou 404, cherche uniquement dans le département courant"""
|
||||
@ -297,6 +305,17 @@ class FormSemestre(db.Model):
|
||||
- et sont associées à l'un des parcours de ce formsemestre
|
||||
(ou à aucun, donc tronc commun).
|
||||
"""
|
||||
# per-request caching
|
||||
key = (self.id, with_sport)
|
||||
_cache = getattr(g, "_formsemestre_get_ues_cache", None)
|
||||
if _cache:
|
||||
result = _cache.get(key, False)
|
||||
if result is not False:
|
||||
return result
|
||||
else:
|
||||
g._formsemestre_get_ues_cache = {}
|
||||
_cache = g._formsemestre_get_ues_cache
|
||||
|
||||
formation: Formation = self.formation
|
||||
if formation.is_apc():
|
||||
# UEs de tronc commun (sans parcours indiqué)
|
||||
@ -316,8 +335,7 @@ class FormSemestre(db.Model):
|
||||
).filter(UniteEns.semestre_idx == self.semestre_id)
|
||||
}
|
||||
)
|
||||
ues = sem_ues.values()
|
||||
return sorted(ues, key=attrgetter("numero"))
|
||||
ues = sorted(sem_ues.values(), key=attrgetter("numero", "acronyme"))
|
||||
else:
|
||||
sem_ues = db.session.query(UniteEns).filter(
|
||||
ModuleImpl.formsemestre_id == self.id,
|
||||
@ -326,7 +344,9 @@ class FormSemestre(db.Model):
|
||||
)
|
||||
if not with_sport:
|
||||
sem_ues = sem_ues.filter(UniteEns.type != codes_cursus.UE_SPORT)
|
||||
return sem_ues.order_by(UniteEns.numero).all()
|
||||
ues = sem_ues.order_by(UniteEns.numero).all()
|
||||
_cache[key] = ues
|
||||
return ues
|
||||
|
||||
@cached_property
|
||||
def modimpls_sorted(self) -> list[ModuleImpl]:
|
||||
@ -374,7 +394,7 @@ class FormSemestre(db.Model):
|
||||
),
|
||||
{"formsemestre_id": self.id, "parcours_id": parcours.id},
|
||||
)
|
||||
return [ModuleImpl.query.get(modimpl_id) for modimpl_id in cursor]
|
||||
return [db.session.get(ModuleImpl, modimpl_id) for modimpl_id in cursor]
|
||||
|
||||
def can_be_edited_by(self, user):
|
||||
"""Vrai si user peut modifier ce semestre (est chef ou l'un des responsables)"""
|
||||
@ -518,6 +538,11 @@ class FormSemestre(db.Model):
|
||||
return ""
|
||||
return ", ".join(sorted([etape.etape_apo for etape in self.etapes if etape]))
|
||||
|
||||
def add_etape(self, etape_apo: str):
|
||||
"Ajoute une étape"
|
||||
etape = FormSemestreEtape(formsemestre_id=self.id, etape_apo=etape_apo)
|
||||
db.session.add(etape)
|
||||
|
||||
def regroupements_coherents_etud(self) -> list[tuple[UniteEns, UniteEns]]:
|
||||
"""Calcule la liste des regroupements cohérents d'UE impliquant ce
|
||||
formsemestre.
|
||||
@ -560,6 +585,17 @@ class FormSemestre(db.Model):
|
||||
user
|
||||
)
|
||||
|
||||
def can_change_groups(self, user: User = None) -> bool:
|
||||
"""Vrai si l'utilisateur (par def. current) peut changer les groupes dans
|
||||
ce semestre: vérifie permission et verrouillage.
|
||||
"""
|
||||
if not self.etat:
|
||||
return False # semestre verrouillé
|
||||
user = user or current_user
|
||||
if user.has_permission(Permission.ScoEtudChangeGroups):
|
||||
return True # typiquement admin, chef dept
|
||||
return self.est_responsable(user)
|
||||
|
||||
def can_edit_jury(self, user: User = None):
|
||||
"""Vrai si utilisateur (par def. current) peut saisir decision de jury
|
||||
dans ce semestre: vérifie permission et verrouillage.
|
||||
@ -782,6 +818,8 @@ class FormSemestre(db.Model):
|
||||
Les groupes de parcours sont ceux de la partition scu.PARTITION_PARCOURS
|
||||
et leur nom est le code du parcours (eg "Cyber").
|
||||
"""
|
||||
if self.formation.referentiel_competence_id is None:
|
||||
return # safety net
|
||||
partition = Partition.query.filter_by(
|
||||
formsemestre_id=self.id, partition_name=scu.PARTITION_PARCOURS
|
||||
).first()
|
||||
@ -805,7 +843,10 @@ class FormSemestre(db.Model):
|
||||
query = (
|
||||
ApcParcours.query.filter_by(code=group.group_name)
|
||||
.join(ApcReferentielCompetences)
|
||||
.filter_by(dept_id=g.scodoc_dept_id)
|
||||
.filter_by(
|
||||
dept_id=g.scodoc_dept_id,
|
||||
id=self.formation.referentiel_competence_id,
|
||||
)
|
||||
)
|
||||
if query.count() != 1:
|
||||
log(
|
||||
@ -854,15 +895,12 @@ class FormSemestre(db.Model):
|
||||
.order_by(UniteEns.numero)
|
||||
.all()
|
||||
)
|
||||
vals_annee = (
|
||||
vals_annee = ( # issues de cette année scolaire seulement
|
||||
ApcValidationAnnee.query.filter_by(
|
||||
etudid=etudid,
|
||||
annee_scolaire=self.annee_scolaire(),
|
||||
)
|
||||
.join(ApcValidationAnnee.formsemestre)
|
||||
.join(FormSemestre.formation)
|
||||
.filter(Formation.formation_code == self.formation.formation_code)
|
||||
.all()
|
||||
referentiel_competence_id=self.formation.referentiel_competence_id,
|
||||
).all()
|
||||
)
|
||||
H = []
|
||||
for vals in (vals_sem, vals_ues, vals_rcues, vals_annee):
|
||||
|
@ -8,11 +8,13 @@
|
||||
"""ScoDoc models: Groups & partitions
|
||||
"""
|
||||
from operator import attrgetter
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from app import db
|
||||
from app import db, log
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models import GROUPNAME_STR_LEN
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||
|
||||
|
||||
class Partition(db.Model):
|
||||
@ -117,6 +119,81 @@ class Partition(db.Model):
|
||||
.first()
|
||||
)
|
||||
|
||||
def set_etud_group(self, etud: "Identite", group: "GroupDescr") -> bool:
|
||||
"""Affect etudid to group_id in given partition.
|
||||
Raises IntegrityError si conflit,
|
||||
or ValueError si ce group_id n'est pas dans cette partition
|
||||
ou que l'étudiant n'est pas inscrit au semestre.
|
||||
Return True si changement, False s'il était déjà dans ce groupe.
|
||||
"""
|
||||
if not group.id in (g.id for g in self.groups):
|
||||
raise ScoValueError(
|
||||
f"""Le groupe {group.id} n'est pas dans la partition {
|
||||
self.partition_name or "tous"}"""
|
||||
)
|
||||
if etud.id not in (e.id for e in self.formsemestre.etuds):
|
||||
raise ScoValueError(
|
||||
f"""étudiant {etud.nomprenom} non inscrit au formsemestre du groupe {
|
||||
group.group_name}"""
|
||||
)
|
||||
try:
|
||||
existing_row = (
|
||||
db.session.query(group_membership)
|
||||
.filter_by(etudid=etud.id)
|
||||
.join(GroupDescr)
|
||||
.filter_by(partition_id=self.id)
|
||||
.first()
|
||||
)
|
||||
if existing_row:
|
||||
existing_group_id = existing_row[1]
|
||||
if group.id == existing_group_id:
|
||||
return False
|
||||
# Fait le changement avec l'ORM sinon risque élevé de blocage
|
||||
existing_group = db.session.get(GroupDescr, existing_group_id)
|
||||
db.session.commit()
|
||||
group.etuds.append(etud)
|
||||
existing_group.etuds.remove(etud)
|
||||
db.session.add(etud)
|
||||
db.session.add(existing_group)
|
||||
db.session.add(group)
|
||||
else:
|
||||
new_row = group_membership.insert().values(
|
||||
etudid=etud.id, group_id=group.id
|
||||
)
|
||||
db.session.execute(new_row)
|
||||
db.session.commit()
|
||||
except IntegrityError:
|
||||
db.session.rollback()
|
||||
raise
|
||||
return True
|
||||
|
||||
def create_group(self, group_name="", default=False) -> "GroupDescr":
|
||||
"Crée un groupe dans cette partition"
|
||||
if not self.formsemestre.can_change_groups():
|
||||
raise AccessDenied(
|
||||
"""Vous n'avez pas le droit d'effectuer cette opération,
|
||||
ou bien le semestre est verrouillé !"""
|
||||
)
|
||||
if group_name:
|
||||
group_name = group_name.strip()
|
||||
if not group_name and not default:
|
||||
raise ValueError("invalid group name: ()")
|
||||
if not GroupDescr.check_name(self, group_name, default=default):
|
||||
raise ScoValueError(
|
||||
f"Le groupe {group_name} existe déjà dans cette partition"
|
||||
)
|
||||
numeros = [g.numero if g.numero is not None else 0 for g in self.groups]
|
||||
if len(numeros) > 0:
|
||||
new_numero = max(numeros) + 1
|
||||
else:
|
||||
new_numero = 0
|
||||
group = GroupDescr(partition=self, group_name=group_name, numero=new_numero)
|
||||
db.session.add(group)
|
||||
db.session.commit()
|
||||
log(f"create_group: created group_id={group.id}")
|
||||
#
|
||||
return group
|
||||
|
||||
|
||||
class GroupDescr(db.Model):
|
||||
"""Description d'un groupe d'une partition"""
|
||||
|
@ -122,22 +122,6 @@ class ModuleImpl(db.Model):
|
||||
raise AccessDenied(f"Modification impossible pour {user}")
|
||||
return False
|
||||
|
||||
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(
|
||||
|
@ -55,7 +55,7 @@ class Module(db.Model):
|
||||
secondary=parcours_modules,
|
||||
lazy="subquery",
|
||||
backref=db.backref("modules", lazy=True),
|
||||
order_by="ApcParcours.numero",
|
||||
order_by="ApcParcours.numero, ApcParcours.code",
|
||||
)
|
||||
|
||||
app_critiques = db.relationship(
|
||||
@ -198,7 +198,7 @@ class Module(db.Model):
|
||||
else:
|
||||
# crée nouveau coef:
|
||||
if coef != 0.0:
|
||||
ue = UniteEns.query.get(ue_id)
|
||||
ue = db.session.get(UniteEns, ue_id)
|
||||
ue_coef = ModuleUECoef(module=self, ue=ue, coef=coef)
|
||||
db.session.add(ue_coef)
|
||||
self.ue_coefs.append(ue_coef)
|
||||
@ -229,19 +229,19 @@ class Module(db.Model):
|
||||
"""delete coef"""
|
||||
if self.formation.has_locked_sems(self.ue.semestre_idx):
|
||||
current_app.logguer.info(
|
||||
f"delete_ue_coef: locked formation, ignoring request"
|
||||
"delete_ue_coef: locked formation, ignoring request"
|
||||
)
|
||||
raise ScoValueError("Formation verrouillée")
|
||||
ue_coef = ModuleUECoef.query.get((self.id, ue.id))
|
||||
ue_coef = db.session.get(ModuleUECoef, (self.id, ue.id))
|
||||
if ue_coef:
|
||||
db.session.delete(ue_coef)
|
||||
self.formation.invalidate_module_coefs()
|
||||
|
||||
def get_ue_coefs_sorted(self):
|
||||
"les coefs d'UE, trié par numéro d'UE"
|
||||
"les coefs d'UE, trié par numéro et acronyme d'UE"
|
||||
# je n'ai pas su mettre un order_by sur le backref sans avoir
|
||||
# à redéfinir les relationships...
|
||||
return sorted(self.ue_coefs, key=lambda x: x.ue.numero)
|
||||
return sorted(self.ue_coefs, key=lambda uc: (uc.ue.numero, uc.ue.acronyme))
|
||||
|
||||
def ue_coefs_list(
|
||||
self, include_zeros=True, ues: list["UniteEns"] = None
|
||||
|
@ -56,8 +56,8 @@ class NotesNotes(db.Model):
|
||||
"pour debug"
|
||||
from app.models.evaluations import Evaluation
|
||||
|
||||
return f"""<{self.__class__.__name__} {self.id} v={self.value} {self.date.isoformat()
|
||||
} {Evaluation.query.get(self.evaluation_id) if self.evaluation_id else "X" }>"""
|
||||
return f"""<{self.__class__.__name__} {self.id} etudid={self.etudid} v={self.value} {self.date.isoformat()
|
||||
} {db.session.get(Evaluation, self.evaluation_id) if self.evaluation_id else "X" }>"""
|
||||
|
||||
|
||||
class NotesNotesLog(db.Model):
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""ScoDoc 9 models : Unités d'Enseignement (UE)
|
||||
"""
|
||||
|
||||
from flask import g
|
||||
import pandas as pd
|
||||
|
||||
from app import db, log
|
||||
@ -8,7 +9,6 @@ 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
|
||||
|
||||
|
||||
@ -58,7 +58,10 @@ class UniteEns(db.Model):
|
||||
|
||||
# Une UE appartient soit à tous les parcours (tronc commun), soit à un sous-ensemble
|
||||
parcours = db.relationship(
|
||||
ApcParcours, secondary="ue_parcours", backref=db.backref("ues", lazy=True)
|
||||
ApcParcours,
|
||||
secondary="ue_parcours",
|
||||
backref=db.backref("ues", lazy=True),
|
||||
order_by="ApcParcours.numero, ApcParcours.code",
|
||||
)
|
||||
|
||||
# relations
|
||||
@ -104,6 +107,17 @@ class UniteEns(db.Model):
|
||||
If convert_objects, convert all attributes to native types
|
||||
(suitable for json encoding).
|
||||
"""
|
||||
# cache car très utilisé par anciens codes
|
||||
key = (self.id, convert_objects, with_module_ue_coefs)
|
||||
_cache = getattr(g, "_ue_to_dict_cache", None)
|
||||
if _cache:
|
||||
result = g._ue_to_dict_cache.get(key, False)
|
||||
if result is not False:
|
||||
return result
|
||||
else:
|
||||
g._ue_to_dict_cache = {}
|
||||
_cache = g._ue_to_dict_cache
|
||||
|
||||
e = dict(self.__dict__)
|
||||
e.pop("_sa_instance_state", None)
|
||||
e.pop("evaluation_ue_poids", None)
|
||||
@ -130,6 +144,7 @@ class UniteEns(db.Model):
|
||||
]
|
||||
else:
|
||||
e.pop("module_ue_coefs", None)
|
||||
_cache[key] = e
|
||||
return e
|
||||
|
||||
def annee(self) -> int:
|
||||
@ -177,12 +192,23 @@ class UniteEns(db.Model):
|
||||
le parcours indiqué.
|
||||
"""
|
||||
if parcour is not None:
|
||||
key = (parcour.id, self.id, only_parcours)
|
||||
ue_ects_cache = getattr(g, "_ue_ects_cache", None)
|
||||
if ue_ects_cache:
|
||||
ects = g._ue_ects_cache.get(key, False)
|
||||
if ects is not False:
|
||||
return ects
|
||||
else:
|
||||
g._ue_ects_cache = {}
|
||||
ue_ects_cache = g._ue_ects_cache
|
||||
ue_parcour = UEParcours.query.filter_by(
|
||||
ue_id=self.id, parcours_id=parcour.id
|
||||
).first()
|
||||
if ue_parcour is not None and ue_parcour.ects is not None:
|
||||
ue_ects_cache[key] = ue_parcour.ects
|
||||
return ue_parcour.ects
|
||||
if only_parcours:
|
||||
ue_ects_cache[key] = None
|
||||
return None
|
||||
return self.ects
|
||||
|
||||
|
@ -8,10 +8,13 @@ from app import log
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models import CODE_STR_LEN
|
||||
from app.models.events import Scolog
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.codes_cursus import CODES_UE_VALIDES
|
||||
|
||||
|
||||
class ScolarFormSemestreValidation(db.Model):
|
||||
"""Décisions de jury"""
|
||||
"""Décisions de jury (sur semestre ou UEs)"""
|
||||
|
||||
__tablename__ = "scolar_formsemestre_validation"
|
||||
# Assure unicité de la décision:
|
||||
@ -54,18 +57,30 @@ class ScolarFormSemestreValidation(db.Model):
|
||||
)
|
||||
|
||||
ue = db.relationship("UniteEns", lazy="select", uselist=False)
|
||||
etud = db.relationship("Identite", backref="validations")
|
||||
formsemestre = db.relationship(
|
||||
"FormSemestre", lazy="select", uselist=False, foreign_keys=[formsemestre_id]
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}(sem={self.formsemestre_id}, etuid={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:
|
||||
# Note: si l'objet vient d'être créé, ue_id peut exister mais pas ue !
|
||||
return f"""décision sur UE {self.ue.acronyme if self.ue else self.ue_id}: {self.code}"""
|
||||
return f"""décision sur semestre {self.formsemestre.titre_mois()} du {self.event_date.strftime("%d/%m/%Y")}"""
|
||||
return f"""décision sur UE {self.ue.acronyme if self.ue else self.ue_id
|
||||
} ({self.ue_id}): {self.code}"""
|
||||
return f"""décision sur semestre {self.formsemestre.titre_mois()} du {
|
||||
self.event_date.strftime("%d/%m/%Y")}"""
|
||||
|
||||
def delete(self):
|
||||
"Efface cette validation"
|
||||
log(f"{self.__class__.__name__}.delete({self})")
|
||||
etud = self.etud
|
||||
db.session.delete(self)
|
||||
db.session.commit()
|
||||
sco_cache.invalidate_formsemestre_etud(etud)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"as a dict"
|
||||
@ -73,6 +88,49 @@ class ScolarFormSemestreValidation(db.Model):
|
||||
d.pop("_sa_instance_state", None)
|
||||
return d
|
||||
|
||||
def html(self, detail=False) -> str:
|
||||
"Affichage html"
|
||||
if self.ue_id is not None:
|
||||
moyenne = (
|
||||
f", moyenne {scu.fmt_note(self.moy_ue)}/20 "
|
||||
if self.moy_ue is not None
|
||||
else ""
|
||||
)
|
||||
link = (
|
||||
self.formsemestre.html_link_status(
|
||||
label=f"{self.formsemestre.titre_formation(with_sem_idx=1)}",
|
||||
title=self.formsemestre.titre_annee(),
|
||||
)
|
||||
if self.formsemestre
|
||||
else "externe/antérieure"
|
||||
)
|
||||
return f"""Validation
|
||||
{'<span class="redboldtext">externe</span>' if self.is_external else ""}
|
||||
de l'UE <b>{self.ue.acronyme}</b>
|
||||
{('parcours <span class="parcours">'
|
||||
+ ", ".join([p.code for p in self.ue.parcours]))
|
||||
+ "</span>"
|
||||
if self.ue.parcours else ""}
|
||||
{("émise par " + link)}
|
||||
: <b>{self.code}</b>{moyenne}
|
||||
le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")}
|
||||
"""
|
||||
else:
|
||||
return f"""Validation du semestre S{
|
||||
self.formsemestre.semestre_id if self.formsemestre else "?"}
|
||||
{self.formsemestre.html_link_status() if self.formsemestre else ""}
|
||||
: <b>{self.code}</b>
|
||||
le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")}
|
||||
"""
|
||||
|
||||
def ects(self) -> float:
|
||||
"Les ECTS acquis par cette validation. (0 si ce n'est pas une validation d'UE)"
|
||||
return (
|
||||
self.ue.ects
|
||||
if (self.ue is not None) and (self.code in CODES_UE_VALIDES)
|
||||
else 0.0
|
||||
)
|
||||
|
||||
|
||||
class ScolarAutorisationInscription(db.Model):
|
||||
"""Autorisation d'inscription dans un semestre"""
|
||||
@ -93,6 +151,7 @@ class ScolarAutorisationInscription(db.Model):
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_formsemestre.id"),
|
||||
)
|
||||
origin_formsemestre = db.relationship("FormSemestre", lazy="select", uselist=False)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"""{self.__class__.__name__}(id={self.id}, etudid={
|
||||
@ -104,6 +163,21 @@ class ScolarAutorisationInscription(db.Model):
|
||||
d.pop("_sa_instance_state", None)
|
||||
return d
|
||||
|
||||
def html(self) -> str:
|
||||
"Affichage html"
|
||||
link = (
|
||||
self.origin_formsemestre.html_link_status(
|
||||
label=f"{self.origin_formsemestre.titre_formation(with_sem_idx=1)}",
|
||||
title=self.origin_formsemestre.titre_annee(),
|
||||
)
|
||||
if self.origin_formsemestre
|
||||
else "externe/antérieure"
|
||||
)
|
||||
return f"""Autorisation de passage vers <b>S{self.semestre_id}</b> émise par
|
||||
{link}
|
||||
le {self.date.strftime("%d/%m/%Y")} à {self.date.strftime("%Hh%M")}
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def autorise_etud(
|
||||
cls,
|
||||
|
@ -36,7 +36,7 @@ Created on Fri Sep 9 09:15:05 2016
|
||||
@author: barasc
|
||||
"""
|
||||
|
||||
from app import log
|
||||
from app import db, log
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre
|
||||
@ -487,7 +487,7 @@ def comp_coeff_pond(coeffs, ponderations):
|
||||
# -----------------------------------------------------------------------------
|
||||
def get_moduleimpl(modimpl_id) -> dict:
|
||||
"""Renvoie l'objet modimpl dont l'id est modimpl_id"""
|
||||
modimpl = ModuleImpl.query.get(modimpl_id)
|
||||
modimpl = db.session.get(ModuleImpl, modimpl_id)
|
||||
if modimpl:
|
||||
return modimpl
|
||||
if SemestreTag.DEBUG:
|
||||
|
@ -1,43 +0,0 @@
|
||||
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("")
|
@ -122,6 +122,7 @@ ABAN = "ABAN"
|
||||
ABL = "ABL"
|
||||
ADM = "ADM" # moyenne gen., barres UE, assiduité: sem. validé
|
||||
ADC = "ADC" # admis par compensation (eg moy(S1, S2) > 10)
|
||||
ADSUP = "ADSUP" # BUT: UE ou RCUE validé par niveau supérieur
|
||||
ADJ = "ADJ" # admis par le jury
|
||||
ADJR = "ADJR" # UE admise car son RCUE est ADJ
|
||||
ATT = "ATT" #
|
||||
@ -162,6 +163,7 @@ CODES_EXPL = {
|
||||
ADJ: "Validé par le Jury",
|
||||
ADJR: "UE validée car son RCUE est validé ADJ par le jury",
|
||||
ADM: "Validé",
|
||||
ADSUP: "UE ou RCUE validé car le niveau supérieur est validé",
|
||||
AJ: "Ajourné (ou UE/BC de BUT en attente pour problème de moyenne)",
|
||||
ATB: "Décision en attente d'un autre semestre (au moins une UE sous la barre)",
|
||||
ATJ: "Décision en attente d'un autre semestre (assiduité insuffisante)",
|
||||
@ -194,18 +196,23 @@ CODES_SEM_ATTENTES = {ATT, ATB, ATJ} # semestre en attente
|
||||
|
||||
CODES_SEM_REO = {NAR} # reorientation
|
||||
|
||||
# Les codes d'UEs
|
||||
CODES_JURY_UE = {ADM, CMP, ADJ, ADJR, ADSUP, AJ, ATJ, RAT, DEF, ABAN, DEM, UEBSL}
|
||||
CODES_UE_VALIDES_DE_DROIT = {ADM, CMP} # validation "de droit"
|
||||
CODES_UE_VALIDES = CODES_UE_VALIDES_DE_DROIT | {ADJ, ADJR}
|
||||
CODES_UE_VALIDES = CODES_UE_VALIDES_DE_DROIT | {ADJ, ADJR, ADSUP}
|
||||
"UE validée"
|
||||
CODES_UE_CAPITALISANTS = {ADM}
|
||||
"UE capitalisée"
|
||||
|
||||
CODES_JURY_RCUE = {ADM, ADJ, ADSUP, CMP, AJ, ATJ, RAT, DEF, ABAN}
|
||||
"codes de jury utilisables sur les RCUEs"
|
||||
CODES_RCUE_VALIDES_DE_DROIT = {ADM, CMP}
|
||||
CODES_RCUE_VALIDES = CODES_RCUE_VALIDES_DE_DROIT | {ADJ}
|
||||
CODES_RCUE_VALIDES = CODES_RCUE_VALIDES_DE_DROIT | {ADJ, ADSUP}
|
||||
"Niveau RCUE validé"
|
||||
|
||||
# Pour le BUT:
|
||||
CODES_ANNEE_BUT_VALIDES_DE_DROIT = {ADM, PASD}
|
||||
CODES_ANNEE_BUT_VALIDES_DE_DROIT = {ADM, PASD} # PASD pour enregistrement auto
|
||||
CODES_ANNEE_BUT_VALIDES = {ADM, ADSUP}
|
||||
CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL}
|
||||
BUT_BARRE_UE8 = 8.0 - NOTES_TOLERANCE
|
||||
BUT_BARRE_UE = BUT_BARRE_RCUE = 10.0 - NOTES_TOLERANCE
|
||||
@ -219,17 +226,25 @@ BUT_CODES_PASSAGE = {
|
||||
}
|
||||
# les codes, du plus "défavorable" à l'étudiant au plus favorable:
|
||||
# (valeur par défaut 0)
|
||||
BUT_CODES_ORDERED = {
|
||||
NAR: 0,
|
||||
BUT_CODES_ORDER = {
|
||||
ABAN: 0,
|
||||
ABL: 0,
|
||||
DEM: 0,
|
||||
DEF: 0,
|
||||
EXCLU: 0,
|
||||
NAR: 0,
|
||||
UEBSL: 0,
|
||||
RAT: 5,
|
||||
RED: 6,
|
||||
AJ: 10,
|
||||
ATJ: 20,
|
||||
CMP: 50,
|
||||
ADC: 50,
|
||||
PASD: 50,
|
||||
PAS1NCI: 60,
|
||||
PAS1NCI: 50,
|
||||
PASD: 60,
|
||||
ADJR: 90,
|
||||
ADJ: 100,
|
||||
ADSUP: 90,
|
||||
ADJ: 90,
|
||||
ADM: 100,
|
||||
}
|
||||
|
||||
@ -249,6 +264,16 @@ def code_ue_validant(code: str) -> bool:
|
||||
return code in CODES_UE_VALIDES
|
||||
|
||||
|
||||
def code_rcue_validant(code: str) -> bool:
|
||||
"Vrai si ce code d'RCUE est validant"
|
||||
return code in CODES_RCUE_VALIDES
|
||||
|
||||
|
||||
def code_annee_validant(code: str) -> bool:
|
||||
"Vrai si code d'année BUT validant"
|
||||
return code in CODES_ANNEE_BUT_VALIDES
|
||||
|
||||
|
||||
DEVENIR_EXPL = {
|
||||
NEXT: "Passage au semestre suivant",
|
||||
REDOANNEE: "Redoublement année",
|
||||
|
@ -88,7 +88,7 @@ class DEFAULT_TABLE_PREFERENCES(object):
|
||||
return self.values[k]
|
||||
|
||||
|
||||
class GenTable(object):
|
||||
class GenTable:
|
||||
"""Simple 2D tables with export to HTML, PDF, Excel, CSV.
|
||||
Can be sub-classed to generate fancy formats.
|
||||
"""
|
||||
@ -197,6 +197,9 @@ class GenTable(object):
|
||||
def __repr__(self):
|
||||
return f"<gen_table( nrows={self.get_nb_rows()}, ncols={self.get_nb_cols()} )>"
|
||||
|
||||
def __len__(self):
|
||||
return len(self.rows)
|
||||
|
||||
def get_nb_cols(self):
|
||||
return len(self.columns_ids)
|
||||
|
||||
|
4
app/scodoc/html_sidebar.py
Executable file → Normal file
4
app/scodoc/html_sidebar.py
Executable file → Normal file
@ -126,7 +126,7 @@ def sidebar(etudid: int = None):
|
||||
if current_user.has_permission(Permission.ScoAbsChange):
|
||||
H.append(
|
||||
f"""
|
||||
<li><a href="{ url_for('assiduites.signal_assiduites_etud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Ajouter</a></li>
|
||||
<li><a href="{ url_for('absences.SignaleAbsenceEtud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Ajouter</a></li>
|
||||
<li><a href="{ url_for('absences.JustifAbsenceEtud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Justifier</a></li>
|
||||
<li><a href="{ url_for('absences.AnnuleAbsenceEtud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Supprimer</a></li>
|
||||
"""
|
||||
@ -138,7 +138,7 @@ def sidebar(etudid: int = None):
|
||||
H.append(
|
||||
f"""
|
||||
<li><a href="{ url_for('absences.CalAbs', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Calendrier</a></li>
|
||||
<li><a href="{ url_for('assiduites.liste_assiduites_etud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Liste</a></li>
|
||||
<li><a href="{ url_for('absences.ListeAbsEtud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Liste</a></li>
|
||||
</ul>
|
||||
"""
|
||||
)
|
||||
|
41
app/scodoc/sco_abs.py
Executable file → Normal file
41
app/scodoc/sco_abs.py
Executable file → Normal file
@ -42,8 +42,6 @@ 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, Justificatif
|
||||
import app.scodoc.sco_assiduites as scass
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
# --- Misc tools.... ------------------
|
||||
@ -1054,45 +1052,6 @@ 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 + "_assiduites"
|
||||
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_fin_iso, True)
|
||||
|
||||
assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid)
|
||||
justificatifs: Justificatif = Justificatif.query.filter_by(etudid=etudid)
|
||||
|
||||
assiduites = scass.filter_by_date(assiduites, Assiduite, date_debut, date_fin)
|
||||
justificatifs = scass.filter_by_date(
|
||||
justificatifs, Justificatif, date_debut, date_fin
|
||||
)
|
||||
|
||||
calculator: scass.CountCalculator = scass.CountCalculator()
|
||||
calculator.compute_assiduites(assiduites)
|
||||
nb_abs: dict = calculator.to_dict()["demi"]
|
||||
|
||||
abs_just: list[Assiduite] = scass.get_all_justified(
|
||||
etudid, date_debut, date_fin
|
||||
)
|
||||
|
||||
calculator.reset()
|
||||
calculator.compute_assiduites(abs_just)
|
||||
nb_abs_just: dict = calculator.to_dict()["demi"]
|
||||
|
||||
r = (nb_abs, nb_abs_just)
|
||||
ans = sco_cache.AbsSemEtudCache.set(key, r)
|
||||
if not ans:
|
||||
log("warning: get_assiduites_count failed to cache")
|
||||
return r
|
||||
|
||||
|
||||
def invalidate_abs_count(etudid, sem):
|
||||
"""Invalidate (clear) cached counts"""
|
||||
date_debut = sem["date_debut_iso"]
|
||||
|
@ -51,7 +51,14 @@ from app import log
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.models import FormSemestre, Identite, ApcValidationAnnee
|
||||
from app.models import (
|
||||
ApcValidationAnnee,
|
||||
ApcValidationRCUE,
|
||||
FormSemestre,
|
||||
Identite,
|
||||
ScolarFormSemestreValidation,
|
||||
)
|
||||
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
from app.scodoc.sco_apogee_reader import (
|
||||
APO_DECIMAL_SEP,
|
||||
@ -64,6 +71,7 @@ from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc.sco_vdi import ApoEtapeVDI
|
||||
from app.scodoc.codes_cursus import code_semestre_validant
|
||||
from app.scodoc.codes_cursus import (
|
||||
ADSUP,
|
||||
DEF,
|
||||
DEM,
|
||||
NAR,
|
||||
@ -216,7 +224,12 @@ class ApoEtud(dict):
|
||||
break
|
||||
self.col_elts[code] = elt
|
||||
if elt is None:
|
||||
self.new_cols[col_id] = self.cols[col_id]
|
||||
try:
|
||||
self.new_cols[col_id] = self.cols[col_id]
|
||||
except KeyError as exc:
|
||||
raise ScoFormatError(
|
||||
f"""Fichier Apogee invalide : ligne mal formatée ? <br>colonne <tt>{col_id}</tt> non déclarée ?"""
|
||||
) from exc
|
||||
else:
|
||||
try:
|
||||
self.new_cols[col_id] = sco_elts[code][
|
||||
@ -323,14 +336,22 @@ class ApoEtud(dict):
|
||||
x.strip() for x in ue["code_apogee"].split(",")
|
||||
}:
|
||||
if self.export_res_ues:
|
||||
if decisions_ue and ue["ue_id"] in decisions_ue:
|
||||
if (
|
||||
decisions_ue and ue["ue_id"] in decisions_ue
|
||||
) or self.export_res_sdj:
|
||||
ue_status = res.get_etud_ue_status(etudid, ue["ue_id"])
|
||||
code_decision_ue = decisions_ue[ue["ue_id"]]["code"]
|
||||
if decisions_ue and ue["ue_id"] in decisions_ue:
|
||||
code_decision_ue = decisions_ue[ue["ue_id"]]["code"]
|
||||
code_decision_ue_apo = ScoDocSiteConfig.get_code_apo(
|
||||
code_decision_ue
|
||||
)
|
||||
else:
|
||||
code_decision_ue_apo = ""
|
||||
return dict(
|
||||
N=self.fmt_note(ue_status["moy"] if ue_status else ""),
|
||||
B=20,
|
||||
J="",
|
||||
R=ScoDocSiteConfig.get_code_apo(code_decision_ue),
|
||||
R=code_decision_ue_apo,
|
||||
M="",
|
||||
)
|
||||
else:
|
||||
@ -343,14 +364,17 @@ class ApoEtud(dict):
|
||||
module_code_found = False
|
||||
for modimpl in modimpls:
|
||||
module = modimpl["module"]
|
||||
if module["code_apogee"] and code in {
|
||||
x.strip() for x in module["code_apogee"].split(",")
|
||||
}:
|
||||
if (
|
||||
res.modimpl_inscr_df[modimpl["moduleimpl_id"]][etudid]
|
||||
and module["code_apogee"]
|
||||
and code in {x.strip() for x in module["code_apogee"].split(",")}
|
||||
):
|
||||
n = res.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid)
|
||||
if n != "NI" and self.export_res_modules:
|
||||
return dict(N=self.fmt_note(n), B=20, J="", R="")
|
||||
else:
|
||||
module_code_found = True
|
||||
|
||||
if module_code_found:
|
||||
return VOID_APO_RES
|
||||
#
|
||||
@ -473,7 +497,10 @@ class ApoEtud(dict):
|
||||
)
|
||||
|
||||
def _but_load_validation_annuelle(self):
|
||||
"charge la validation de jury BUT annuelle"
|
||||
"""charge la validation de jury BUT annuelle.
|
||||
Ici impose qu'elle soit issue d'un semestre de l'année en cours
|
||||
(pas forcément nécessaire, voir selon les retours des équipes ?)
|
||||
"""
|
||||
# le semestre impair de l'année scolaire
|
||||
if self.cur_res.formsemestre.semestre_id % 2:
|
||||
formsemestre = self.cur_res.formsemestre
|
||||
@ -488,11 +515,11 @@ class ApoEtud(dict):
|
||||
# ne trouve pas de semestre impair
|
||||
self.validation_annee_but = None
|
||||
return
|
||||
self.validation_annee_but: ApcValidationAnnee = (
|
||||
ApcValidationAnnee.query.filter_by(
|
||||
formsemestre_id=formsemestre.id, etudid=self.etud["etudid"]
|
||||
).first()
|
||||
)
|
||||
self.validation_annee_but: ApcValidationAnnee = ApcValidationAnnee.query.filter_by(
|
||||
formsemestre_id=formsemestre.id,
|
||||
etudid=self.etud["etudid"],
|
||||
referentiel_competence_id=self.cur_res.formsemestre.formation.referentiel_competence_id,
|
||||
).first()
|
||||
self.is_nar = (
|
||||
self.validation_annee_but and self.validation_annee_but.code == NAR
|
||||
)
|
||||
@ -892,6 +919,75 @@ class ApoData:
|
||||
)
|
||||
return T
|
||||
|
||||
def build_adsup_table(self):
|
||||
"""Construit une table listant les ADSUP émis depuis les formsemestres
|
||||
NIP nom prenom nom_formsemestre etape UE
|
||||
"""
|
||||
validations_ues, validations_rcue = self.list_adsup()
|
||||
rows = [
|
||||
{
|
||||
"code_nip": v.etud.code_nip,
|
||||
"nom": v.etud.nom,
|
||||
"prenom": v.etud.prenom,
|
||||
"formsemestre": v.formsemestre.titre_formation(with_sem_idx=1),
|
||||
"etape": v.formsemestre.etapes_apo_str(),
|
||||
"ue": v.ue.acronyme,
|
||||
}
|
||||
for v in validations_ues
|
||||
]
|
||||
rows += [
|
||||
{
|
||||
"code_nip": v.etud.code_nip,
|
||||
"nom": v.etud.nom,
|
||||
"prenom": v.etud.prenom,
|
||||
"formsemestre": v.formsemestre.titre_formation(with_sem_idx=1),
|
||||
"etape": "", # on ne sait pas à quel étape rattacher le RCUE
|
||||
"rcue": f"{v.ue1.acronyme}/{v.ue2.acronyme}",
|
||||
}
|
||||
for v in validations_rcue
|
||||
]
|
||||
|
||||
return GenTable(
|
||||
columns_ids=(
|
||||
"code_nip",
|
||||
"nom",
|
||||
"prenom",
|
||||
"formsemestre",
|
||||
"etape",
|
||||
"ue",
|
||||
"rcue",
|
||||
),
|
||||
titles={
|
||||
"code_nip": "NIP",
|
||||
"nom": "Nom",
|
||||
"prenom": "Prénom",
|
||||
"formsemestre": "Semestre",
|
||||
"etape": "Etape",
|
||||
"ue": "UE",
|
||||
"rcue": "RCUE",
|
||||
},
|
||||
rows=rows,
|
||||
xls_sheet_name="ADSUPs",
|
||||
)
|
||||
|
||||
def list_adsup(
|
||||
self,
|
||||
) -> tuple[list[ScolarFormSemestreValidation], list[ApcValidationRCUE]]:
|
||||
"""Liste les validations ADSUP émises par des formsemestres de cet ensemble"""
|
||||
validations_ues = (
|
||||
ScolarFormSemestreValidation.query.filter_by(code=ADSUP)
|
||||
.filter(ScolarFormSemestreValidation.ue_id != None)
|
||||
.filter(
|
||||
ScolarFormSemestreValidation.formsemestre_id.in_(
|
||||
self.etape_formsemestre_ids
|
||||
)
|
||||
)
|
||||
)
|
||||
validations_rcue = ApcValidationRCUE.query.filter_by(code=ADSUP).filter(
|
||||
ApcValidationRCUE.formsemestre_id.in_(self.etape_formsemestre_ids)
|
||||
)
|
||||
return validations_ues, validations_rcue
|
||||
|
||||
|
||||
def comp_apo_sems(etape_apogee, annee_scolaire: int) -> list[dict]:
|
||||
"""
|
||||
@ -1018,6 +1114,10 @@ def export_csv_to_apogee(
|
||||
cr_table = apo_data.build_cr_table()
|
||||
cr_xls = cr_table.excel()
|
||||
|
||||
# ADSUPs
|
||||
adsup_table = apo_data.build_adsup_table()
|
||||
adsup_xls = adsup_table.excel() if len(adsup_table) else None
|
||||
|
||||
# Create ZIP
|
||||
if not dest_zip:
|
||||
data = io.BytesIO()
|
||||
@ -1043,6 +1143,7 @@ def export_csv_to_apogee(
|
||||
log_filename = "scodoc-" + basename + ".log.txt"
|
||||
nar_filename = basename + "-nar" + scu.XLSX_SUFFIX
|
||||
cr_filename = basename + "-decisions" + scu.XLSX_SUFFIX
|
||||
adsup_filename = f"{basename}-adsups{scu.XLSX_SUFFIX}"
|
||||
|
||||
logf = io.StringIO()
|
||||
logf.write(f"export_to_apogee du {time.ctime()}\n\n")
|
||||
@ -1079,6 +1180,8 @@ def export_csv_to_apogee(
|
||||
"\n\nElements Apogee inconnus dans ces semestres ScoDoc:\n"
|
||||
+ "\n".join(apo_data.list_unknown_elements())
|
||||
)
|
||||
if adsup_xls:
|
||||
logf.write(f"\n\nADSUP générés: {len(adsup_table)}\n")
|
||||
log(logf.getvalue()) # sortie aussi sur le log ScoDoc
|
||||
|
||||
# Write data to ZIP
|
||||
@ -1087,6 +1190,8 @@ def export_csv_to_apogee(
|
||||
if nar_xls:
|
||||
dest_zip.writestr(nar_filename, nar_xls)
|
||||
dest_zip.writestr(cr_filename, cr_xls)
|
||||
if adsup_xls:
|
||||
dest_zip.writestr(adsup_filename, adsup_xls)
|
||||
|
||||
if my_zip:
|
||||
dest_zip.close()
|
||||
|
@ -295,8 +295,15 @@ class ApoCSVReadWrite:
|
||||
filename=self.get_filename(),
|
||||
)
|
||||
cols = {} # { col_id : value }
|
||||
for i, field in enumerate(fields):
|
||||
cols[self.col_ids[i]] = field
|
||||
try:
|
||||
for i, field in enumerate(fields):
|
||||
cols[self.col_ids[i]] = field
|
||||
except IndexError as exc:
|
||||
raise
|
||||
raise ScoFormatError(
|
||||
f"Fichier Apogee incorrect (colonnes excédentaires ? (<tt>{i}/{field}</tt>))",
|
||||
filename=self.get_filename(),
|
||||
) from exc
|
||||
etud_tuples.append(
|
||||
ApoEtudTuple(
|
||||
nip=fields[0], # id etudiant
|
||||
|
@ -68,9 +68,9 @@ from app import log, ScoDocJSONEncoder
|
||||
from app.but import jury_but_pv
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre
|
||||
from app.models import Departement, FormSemestre
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||
from app.scodoc.sco_exceptions import ScoPermissionDenied
|
||||
from app.scodoc.sco_exceptions import ScoException, ScoPermissionDenied
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_bulletins_pdf
|
||||
from app.scodoc import sco_groups
|
||||
@ -86,11 +86,6 @@ 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:
|
||||
@ -112,8 +107,6 @@ class BaseArchiver(object):
|
||||
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: int):
|
||||
"""
|
||||
@ -121,7 +114,8 @@ class BaseArchiver(object):
|
||||
If directory does not yet exist, create it.
|
||||
"""
|
||||
self.initialize()
|
||||
dept_dir = os.path.join(self.root, str(self.dept_id))
|
||||
dept = Departement.query.filter_by(acronym=g.scodoc_dept).first()
|
||||
dept_dir = os.path.join(self.root, str(dept.id))
|
||||
try:
|
||||
scu.GSL.acquire()
|
||||
if not os.path.isdir(dept_dir):
|
||||
@ -131,6 +125,12 @@ class BaseArchiver(object):
|
||||
if not os.path.isdir(obj_dir):
|
||||
log(f"creating directory {obj_dir}")
|
||||
os.mkdir(obj_dir)
|
||||
except FileExistsError as exc:
|
||||
raise ScoException(
|
||||
f"""BaseArchiver error: obj_dir={obj_dir} exists={
|
||||
os.path.exists(obj_dir)
|
||||
} isdir={os.path.isdir(obj_dir)}"""
|
||||
) from exc
|
||||
finally:
|
||||
scu.GSL.release()
|
||||
return obj_dir
|
||||
@ -140,7 +140,8 @@ class BaseArchiver(object):
|
||||
:return: list of archive oids
|
||||
"""
|
||||
self.initialize()
|
||||
base = os.path.join(self.root, str(self.dept_id)) + os.path.sep
|
||||
dept = Departement.query.filter_by(acronym=g.scodoc_dept).first()
|
||||
base = os.path.join(self.root, str(dept.id)) + os.path.sep
|
||||
dirs = glob.glob(base + "*")
|
||||
return [os.path.split(x)[1] for x in dirs]
|
||||
|
||||
@ -343,7 +344,7 @@ def do_formsemestre_archive(
|
||||
if data:
|
||||
PVArchive.store(archive_id, "Tableau_moyennes" + scu.XLSX_SUFFIX, data)
|
||||
# Tableau recap notes en HTML (pour tous les etudiants, n'utilise pas les groupes)
|
||||
table_html, _ = gen_formsemestre_recapcomplet_html_table(
|
||||
table_html, _, _ = gen_formsemestre_recapcomplet_html_table(
|
||||
formsemestre, res, include_evaluations=True
|
||||
)
|
||||
if table_html:
|
||||
|
@ -1,215 +0,0 @@
|
||||
"""
|
||||
Gestion de l'archivage des justificatifs
|
||||
|
||||
Ecrit par Matthias HARTMANN
|
||||
"""
|
||||
import os
|
||||
from datetime import datetime
|
||||
from shutil import rmtree
|
||||
|
||||
from app.models import Identite
|
||||
from app.scodoc.sco_archives import BaseArchiver
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_utils import is_iso_formated
|
||||
|
||||
|
||||
class Trace:
|
||||
"""gestionnaire de la trace des fichiers justificatifs"""
|
||||
|
||||
def __init__(self, path: str) -> None:
|
||||
self.path: str = path + "/_trace.csv"
|
||||
self.content: dict[str, list[datetime, datetime]] = {}
|
||||
self.import_from_file()
|
||||
|
||||
def import_from_file(self):
|
||||
"""import trace from file"""
|
||||
if os.path.isfile(self.path):
|
||||
with open(self.path, "r", encoding="utf-8") as file:
|
||||
for line in file.readlines():
|
||||
csv = line.split(",")
|
||||
fname: str = csv[0]
|
||||
entry_date: datetime = is_iso_formated(csv[1], True)
|
||||
delete_date: datetime = is_iso_formated(csv[2], True)
|
||||
|
||||
self.content[fname] = [entry_date, delete_date]
|
||||
|
||||
def set_trace(self, *fnames: str, mode: str = "entry"):
|
||||
"""Ajoute une trace du fichier donné
|
||||
mode : entry / delete
|
||||
"""
|
||||
modes: list[str] = ["entry", "delete"]
|
||||
for fname in fnames:
|
||||
if fname in modes:
|
||||
continue
|
||||
traced: list[datetime, datetime] = self.content.get(fname, False)
|
||||
if not traced:
|
||||
self.content[fname] = [None, None]
|
||||
traced = self.content[fname]
|
||||
|
||||
traced[modes.index(mode)] = datetime.now()
|
||||
self.save_trace()
|
||||
|
||||
def save_trace(self):
|
||||
"""Enregistre la trace dans le fichier _trace.csv"""
|
||||
lines: list[str] = []
|
||||
for fname, traced in self.content.items():
|
||||
date_fin: datetime or None = traced[1].isoformat() if traced[1] else "None"
|
||||
|
||||
lines.append(f"{fname},{traced[0].isoformat()},{date_fin}")
|
||||
with open(self.path, "w", encoding="utf-8") as file:
|
||||
file.write("\n".join(lines))
|
||||
|
||||
def get_trace(self, fnames: list[str] = ()) -> dict[str, list[datetime, datetime]]:
|
||||
"""Récupère la trace pour les noms de fichiers.
|
||||
si aucun nom n'est donné, récupère tous les fichiers"""
|
||||
|
||||
if fnames is None or len(fnames) == 0:
|
||||
return self.content
|
||||
|
||||
traced: dict = {}
|
||||
for fname in fnames:
|
||||
traced[fname] = self.content.get(fname, None)
|
||||
|
||||
return traced
|
||||
|
||||
|
||||
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>
|
||||
├── [_trace.csv]
|
||||
└── <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)
|
||||
|
||||
trace = Trace(self.get_obj_dir(etudid))
|
||||
trace.set_trace(fname, "entry")
|
||||
|
||||
return self.get_archive_name(archive_id), fname
|
||||
|
||||
def delete_justificatif(
|
||||
self,
|
||||
etudid: int,
|
||||
archive_name: str,
|
||||
filename: str = None,
|
||||
has_trace: bool = True,
|
||||
):
|
||||
"""
|
||||
Supprime une archive ou un fichier particulier de l'archivage de l'étudiant donné
|
||||
|
||||
Si trace == True : sauvegarde le nom du/des fichier(s) supprimé(s) dans la trace de l'étudiant
|
||||
"""
|
||||
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):
|
||||
if has_trace:
|
||||
trace = Trace(self.get_obj_dir(etudid))
|
||||
trace.set_trace(filename, "delete")
|
||||
os.remove(path)
|
||||
|
||||
else:
|
||||
if has_trace:
|
||||
trace = Trace(self.get_obj_dir(etudid))
|
||||
trace.set_trace(*self.list_archive(archive_id), mode="delete")
|
||||
|
||||
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)
|
||||
|
||||
def remove_dept_archive(self, dept_id: int = None):
|
||||
"""
|
||||
Supprime toutes les archives d'un département (ou de tous les départements)
|
||||
⚠ Supprime aussi les fichiers de trace ⚠
|
||||
"""
|
||||
self.set_dept_id(1)
|
||||
self.initialize()
|
||||
|
||||
if dept_id is None:
|
||||
rmtree(self.root, ignore_errors=True)
|
||||
else:
|
||||
rmtree(os.path.join(self.root, str(dept_id)), ignore_errors=True)
|
||||
|
||||
def get_trace(
|
||||
self, etudid: int, *fnames: str
|
||||
) -> dict[str, list[datetime, datetime]]:
|
||||
"""Récupère la trace des justificatifs de l'étudiant"""
|
||||
trace = Trace(self.get_obj_dir(etudid))
|
||||
return trace.get_trace(fnames)
|
@ -1,352 +0,0 @@
|
||||
"""
|
||||
Ecrit par Matthias Hartmann.
|
||||
"""
|
||||
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:
|
||||
"""Classe qui gére le comptage des assiduités"""
|
||||
|
||||
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 reset(self):
|
||||
"""Remet à zero le compteur"""
|
||||
self.days = []
|
||||
self.half_days = []
|
||||
self.hours = 0.0
|
||||
self.count = 0
|
||||
|
||||
def add_half_day(self, day: date, is_morning: bool = True):
|
||||
"""Ajoute une demi journée dans le comptage"""
|
||||
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):
|
||||
"""Ajoute un jour dans le comptage"""
|
||||
if day not in self.days:
|
||||
self.days.append(day)
|
||||
|
||||
def check_in_morning(self, period: tuple[datetime, datetime]) -> bool:
|
||||
"""Vérifiée si la période donnée fait partie du matin
|
||||
(Test sur la date de début)
|
||||
"""
|
||||
|
||||
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, bornes=False
|
||||
)
|
||||
return in_morning
|
||||
|
||||
def check_in_evening(self, period: tuple[datetime, datetime]) -> bool:
|
||||
"""Vérifie si la période fait partie de l'aprèm
|
||||
(test sur la date de début)
|
||||
"""
|
||||
|
||||
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):
|
||||
"""Calcule les métriques sur une assiduité longue (plus d'un jour)"""
|
||||
|
||||
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):
|
||||
"""Calcule les métriques pour la collection d'assiduité donnée"""
|
||||
assi: Assiduite
|
||||
assiduites: list[Assiduite] = (
|
||||
assiduites.all() if isinstance(assiduites, Assiduite) else assiduites
|
||||
)
|
||||
for assi in assiduites:
|
||||
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]:
|
||||
"""Retourne les métriques sous la forme d'un dictionnaire"""
|
||||
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:
|
||||
"""Compte les assiduités en fonction des filtres"""
|
||||
|
||||
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])
|
||||
elif key == "est_just":
|
||||
assiduites = filter_assiduites_by_est_just(assiduites, filtered[key])
|
||||
elif key == "user_id":
|
||||
assiduites = filter_by_user_id(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 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_assiduites_by_est_just(
|
||||
assiduites: Assiduite, est_just: bool
|
||||
) -> Justificatif:
|
||||
"""
|
||||
Filtrage d'une collection d'assiduites en fonction de s'ils sont justifiés
|
||||
"""
|
||||
return assiduites.filter_by(est_just=est_just)
|
||||
|
||||
|
||||
def filter_by_user_id(
|
||||
collection: Assiduite or Justificatif,
|
||||
user_id: int,
|
||||
) -> Justificatif:
|
||||
"""
|
||||
Filtrage d'une collection en fonction de l'user_id
|
||||
"""
|
||||
return collection.filter_by(user_id=user_id)
|
||||
|
||||
|
||||
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_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, obj: bool = False) -> list[int]:
|
||||
"""
|
||||
Retourne la liste des assiduite_id qui sont justifié par la justification
|
||||
Une assiduité est justifiée si elle est COMPLETEMENT ou PARTIELLEMENT comprise dans la plage du justificatif
|
||||
et que l'état du justificatif est "valide"
|
||||
renvoie des id si obj == False, sinon les Assiduités
|
||||
"""
|
||||
|
||||
if justi.etat != scu.EtatJustificatif.VALIDE:
|
||||
return []
|
||||
|
||||
assiduites_query: Assiduite = Assiduite.query.join(
|
||||
Justificatif, Assiduite.etudid == Justificatif.etudid
|
||||
).filter(
|
||||
Assiduite.date_debut <= justi.date_fin,
|
||||
Assiduite.date_fin >= justi.date_debut,
|
||||
)
|
||||
|
||||
if not obj:
|
||||
return [assi.id for assi in assiduites_query.all()]
|
||||
|
||||
return assiduites_query
|
||||
|
||||
|
||||
def get_all_justified(
|
||||
etudid: int, date_deb: datetime = None, date_fin: datetime = None
|
||||
) -> list[Assiduite]:
|
||||
"""Retourne toutes les assiduités justifiées sur une période"""
|
||||
|
||||
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)
|
||||
justified = Assiduite.query.filter_by(est_just=True, etudid=etudid)
|
||||
after = filter_by_date(
|
||||
justified,
|
||||
Assiduite,
|
||||
date_deb,
|
||||
date_fin,
|
||||
)
|
||||
return after
|
@ -38,7 +38,7 @@ from flask import flash, render_template, url_for
|
||||
from flask_json import json_response
|
||||
from flask_login import current_user
|
||||
|
||||
from app import email
|
||||
from app import db, email
|
||||
from app import log
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.but import bulletin_but
|
||||
@ -354,7 +354,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
|
||||
"modules_capitalized"
|
||||
] = [] # modules de l'UE capitalisée (liste vide si pas capitalisée)
|
||||
if ue_status["is_capitalized"] and ue_status["formsemestre_id"] is not None:
|
||||
sem_origin = FormSemestre.query.get(ue_status["formsemestre_id"])
|
||||
sem_origin = db.session.get(FormSemestre, ue_status["formsemestre_id"])
|
||||
u[
|
||||
"ue_descr_txt"
|
||||
] = f'capitalisée le {ndb.DateISOtoDMY(ue_status["event_date"])}'
|
||||
@ -369,7 +369,9 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
|
||||
)
|
||||
if ue_status["moy"] != "NA":
|
||||
# détail des modules de l'UE capitalisée
|
||||
formsemestre_cap = FormSemestre.query.get(ue_status["formsemestre_id"])
|
||||
formsemestre_cap = db.session.get(
|
||||
FormSemestre, ue_status["formsemestre_id"]
|
||||
)
|
||||
nt_cap: NotesTableCompat = res_sem.load_formsemestre_results(
|
||||
formsemestre_cap
|
||||
)
|
||||
@ -749,7 +751,7 @@ def etud_descr_situation_semestre(
|
||||
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
|
||||
parcour_id = res.etuds_parcour_id[etudid]
|
||||
parcour: ApcParcours = (
|
||||
ApcParcours.query.get(parcour_id) if parcour_id is not None else None
|
||||
db.session.get(ApcParcours, parcour_id) if parcour_id is not None else None
|
||||
)
|
||||
if parcour:
|
||||
infos["parcours_titre"] = parcour.libelle or ""
|
||||
@ -928,7 +930,7 @@ def formsemestre_bulletinetud(
|
||||
|
||||
"""
|
||||
format = format or "html"
|
||||
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
|
||||
if not formsemestre:
|
||||
raise ScoValueError(f"semestre {formsemestre_id} inconnu !")
|
||||
|
||||
@ -943,7 +945,7 @@ def formsemestre_bulletinetud(
|
||||
)[0]
|
||||
|
||||
if format not in {"html", "pdfmail"}:
|
||||
filename = scu.bul_filename(formsemestre, etud, format)
|
||||
filename = scu.bul_filename(formsemestre, etud)
|
||||
mime, suffix = scu.get_mime_suffix(format)
|
||||
return scu.send_file(bulletin, filename, mime=mime, suffix=suffix)
|
||||
elif format == "pdfmail":
|
||||
@ -1238,7 +1240,7 @@ def make_menu_autres_operations(
|
||||
"enabled": current_user.has_permission(Permission.ScoImplement),
|
||||
},
|
||||
{
|
||||
"title": "Enregistrer une validation d'UE antérieure",
|
||||
"title": "Gérer les validations d'UEs antérieures",
|
||||
"endpoint": "notes.formsemestre_validate_previous_ue",
|
||||
"args": {
|
||||
"formsemestre_id": formsemestre.id,
|
||||
|
@ -33,7 +33,7 @@ import json
|
||||
|
||||
from flask import abort
|
||||
|
||||
from app import ScoDocJSONEncoder
|
||||
from app import db, ScoDocJSONEncoder
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import but_validations
|
||||
@ -245,7 +245,7 @@ def formsemestre_bulletinetud_published_dict(
|
||||
u["module"] = []
|
||||
# Structure UE/Matière/Module
|
||||
# Recodé en 2022
|
||||
ue = UniteEns.query.get(ue_id)
|
||||
ue = db.session.get(UniteEns, ue_id)
|
||||
u["matiere"] = [
|
||||
{
|
||||
"matiere_id": mat.id,
|
||||
|
@ -54,7 +54,7 @@ import traceback
|
||||
from flask import g
|
||||
|
||||
import app
|
||||
from app import log
|
||||
from app import db, log
|
||||
from app.scodoc import notesdb as ndb
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import ScoException
|
||||
@ -266,7 +266,7 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
|
||||
# appel via API ou tests sans dept:
|
||||
formsemestre = None
|
||||
if formsemestre_id:
|
||||
formsemestre = FormSemestre.query.get(formsemestre_id)
|
||||
formsemestre = db.session.get(FormSemestre, formsemestre_id)
|
||||
if formsemestre is None:
|
||||
raise ScoException("invalidate_formsemestre: departement must be set")
|
||||
app.set_sco_dept(formsemestre.departement.acronym, open_cnx=False)
|
||||
@ -315,6 +315,19 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
|
||||
SemBulletinsPDFCache.invalidate_sems(formsemestre_ids)
|
||||
|
||||
|
||||
def invalidate_formsemestre_etud(etud: "Identite"):
|
||||
"""Invalide tous les formsemestres auxquels l'étudiant est inscrit"""
|
||||
from app.models import FormSemestre, FormSemestreInscription
|
||||
|
||||
inscriptions = (
|
||||
FormSemestreInscription.query.filter_by(etudid=etud.id)
|
||||
.join(FormSemestre)
|
||||
.filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
for inscription in inscriptions:
|
||||
invalidate_formsemestre(inscription.formsemestre_id)
|
||||
|
||||
|
||||
class DeferredSemCacheManager:
|
||||
"""Contexte pour effectuer des opérations indépendantes dans la
|
||||
même requete qui invalident le cache. Par exemple, quand on inscrit
|
||||
|
@ -949,6 +949,7 @@ def do_formsemestre_validate_ue(
|
||||
"ue_id": ue_id,
|
||||
"semestre_id": semestre_id,
|
||||
"is_external": is_external,
|
||||
"moy_ue": moy_ue,
|
||||
}
|
||||
if date:
|
||||
args["event_date"] = date
|
||||
@ -965,14 +966,13 @@ def do_formsemestre_validate_ue(
|
||||
cursor.execute("delete from scolar_formsemestre_validation where " + cond, args)
|
||||
# insert
|
||||
args["code"] = code
|
||||
if code == ADM:
|
||||
if moy_ue is None:
|
||||
# stocke la moyenne d'UE capitalisée:
|
||||
ue_status = nt.get_etud_ue_status(etudid, ue_id)
|
||||
moy_ue = ue_status["moy"] if ue_status else ""
|
||||
args["moy_ue"] = moy_ue
|
||||
if (code == ADM) and (moy_ue is None):
|
||||
# stocke la moyenne d'UE capitalisée:
|
||||
ue_status = nt.get_etud_ue_status(etudid, ue_id)
|
||||
args["moy_ue"] = ue_status["moy"] if ue_status else ""
|
||||
|
||||
log("formsemestre_validate_ue: create %s" % args)
|
||||
if code != None:
|
||||
if code is not None:
|
||||
scolar_formsemestre_validation_create(cnx, args)
|
||||
else:
|
||||
log("formsemestre_validate_ue: code is None, not recording validation")
|
||||
|
@ -82,7 +82,7 @@ def html_edit_formation_apc(
|
||||
if None in ects:
|
||||
ects_by_sem[semestre_idx] = '<span class="missing_ue_ects">manquant</span>'
|
||||
else:
|
||||
ects_by_sem[semestre_idx] = sum(ects)
|
||||
ects_by_sem[semestre_idx] = f"{sum(ects):g}"
|
||||
|
||||
arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags()
|
||||
|
||||
|
@ -103,7 +103,7 @@ def do_formation_delete(formation_id):
|
||||
"""delete a formation (and all its UE, matieres, modules)
|
||||
Warning: delete all ues, will ask if there are validations !
|
||||
"""
|
||||
formation: Formation = Formation.query.get(formation_id)
|
||||
formation: Formation = db.session.get(Formation, formation_id)
|
||||
if formation is None:
|
||||
return
|
||||
acronyme = formation.acronyme
|
||||
@ -132,6 +132,7 @@ def do_formation_delete(formation_id):
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=formation_id,
|
||||
text=f"Suppression de la formation {acronyme}",
|
||||
max_frequency=0,
|
||||
)
|
||||
|
||||
|
||||
@ -329,6 +330,7 @@ def do_formation_create(args: dict) -> Formation:
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
text=f"""Création de la formation {
|
||||
formation.titre} ({formation.acronyme}) version {formation.version}""",
|
||||
max_frequency=0,
|
||||
)
|
||||
return formation
|
||||
|
||||
|
@ -30,13 +30,13 @@
|
||||
"""
|
||||
import flask
|
||||
from flask import g, url_for, request
|
||||
from app.models.events import ScolarNews
|
||||
from app.models.formations import Matiere
|
||||
|
||||
from app import db, log
|
||||
from app.models import Formation, Matiere, UniteEns, ScolarNews
|
||||
|
||||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import log
|
||||
from app.models import Formation
|
||||
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
|
||||
from app.scodoc.sco_exceptions import (
|
||||
ScoValueError,
|
||||
@ -73,7 +73,7 @@ def do_matiere_edit(*args, **kw):
|
||||
# edit
|
||||
_matiereEditor.edit(cnx, *args, **kw)
|
||||
formation_id = sco_edit_ue.ue_list({"ue_id": mat["ue_id"]})[0]["formation_id"]
|
||||
Formation.query.get(formation_id).invalidate_cached_sems()
|
||||
db.session.get(Formation, formation_id).invalidate_cached_sems()
|
||||
|
||||
|
||||
def do_matiere_create(args):
|
||||
@ -88,12 +88,11 @@ def do_matiere_create(args):
|
||||
r = _matiereEditor.create(cnx, args)
|
||||
|
||||
# news
|
||||
formation = Formation.query.get(ue["formation_id"])
|
||||
formation = db.session.get(Formation, ue["formation_id"])
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=ue["formation_id"],
|
||||
text=f"Modification de la formation {formation.acronyme}",
|
||||
max_frequency=10 * 60,
|
||||
)
|
||||
formation.invalidate_cached_sems()
|
||||
return r
|
||||
@ -101,13 +100,12 @@ def do_matiere_create(args):
|
||||
|
||||
def matiere_create(ue_id=None):
|
||||
"""Creation d'une matiere"""
|
||||
from app.scodoc import sco_edit_ue
|
||||
|
||||
UE = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0]
|
||||
ue: UniteEns = UniteEns.query.get_or_404(ue_id)
|
||||
default_numero = max([mat.numero for mat in ue.matieres] or [9]) + 1
|
||||
H = [
|
||||
html_sco_header.sco_header(page_title="Création d'une matière"),
|
||||
"""<h2>Création d'une matière dans l'UE %(titre)s (%(acronyme)s)</h2>""" % UE,
|
||||
"""<p class="help">Les matières sont des groupes de modules dans une UE
|
||||
f"""<h2>Création d'une matière dans l'UE {ue.titre} ({ue.acronyme})</h2>
|
||||
<p class="help">Les matières sont des groupes de modules dans une UE
|
||||
d'une formation donnée. Les matières servent surtout pour la
|
||||
présentation (bulletins, etc) mais <em>n'ont pas de rôle dans le calcul
|
||||
des notes.</em>
|
||||
@ -127,13 +125,21 @@ associé.
|
||||
scu.get_request_args(),
|
||||
(
|
||||
("ue_id", {"input_type": "hidden", "default": ue_id}),
|
||||
("titre", {"size": 30, "explanation": "nom de la matière."}),
|
||||
(
|
||||
"titre",
|
||||
{
|
||||
"size": 30,
|
||||
"explanation": "nom de la matière.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"numero",
|
||||
{
|
||||
"size": 2,
|
||||
"explanation": "numéro (1,2,3,4...) pour affichage",
|
||||
"type": "int",
|
||||
"default": default_numero,
|
||||
"allow_null": False,
|
||||
},
|
||||
),
|
||||
),
|
||||
@ -141,7 +147,7 @@ associé.
|
||||
)
|
||||
|
||||
dest_url = url_for(
|
||||
"notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=UE["formation_id"]
|
||||
"notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=ue.formation_id
|
||||
)
|
||||
|
||||
if tf[0] == 0:
|
||||
@ -194,12 +200,11 @@ def do_matiere_delete(oid):
|
||||
_matiereEditor.delete(cnx, oid)
|
||||
|
||||
# news
|
||||
formation = Formation.query.get(ue["formation_id"])
|
||||
formation = db.session.get(Formation, ue["formation_id"])
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=ue["formation_id"],
|
||||
text=f"Modification de la formation {formation.acronyme}",
|
||||
max_frequency=10 * 60,
|
||||
)
|
||||
formation.invalidate_cached_sems()
|
||||
|
||||
|
@ -98,10 +98,10 @@ def module_list(*args, **kw):
|
||||
|
||||
def do_module_create(args) -> int:
|
||||
"Create a module. Returns id of new object."
|
||||
formation = Formation.query.get(args["formation_id"])
|
||||
formation = db.session.get(Formation, args["formation_id"])
|
||||
# refuse de créer un module APC avec semestres incohérents:
|
||||
if formation.is_apc():
|
||||
ue = UniteEns.query.get(args["ue_id"])
|
||||
ue = db.session.get(UniteEns, args["ue_id"])
|
||||
if int(args.get("semestre_id", 0)) != ue.semestre_idx:
|
||||
raise ScoValueError("Formation incompatible: contacter le support ScoDoc")
|
||||
# create
|
||||
@ -114,7 +114,6 @@ def do_module_create(args) -> int:
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=formation.id,
|
||||
text=f"Modification de la formation {formation.acronyme}",
|
||||
max_frequency=10 * 60,
|
||||
)
|
||||
formation.invalidate_cached_sems()
|
||||
return module_id
|
||||
@ -186,7 +185,6 @@ def do_module_delete(oid):
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=mod["formation_id"],
|
||||
text=f"Modification de la formation {formation.acronyme}",
|
||||
max_frequency=10 * 60,
|
||||
)
|
||||
formation.invalidate_cached_sems()
|
||||
|
||||
@ -250,7 +248,7 @@ def do_module_edit(vals: dict) -> None:
|
||||
# edit
|
||||
cnx = ndb.GetDBConnexion()
|
||||
_moduleEditor.edit(cnx, vals)
|
||||
Formation.query.get(mod["formation_id"]).invalidate_cached_sems()
|
||||
db.session.get(Formation, mod["formation_id"]).invalidate_cached_sems()
|
||||
|
||||
|
||||
def check_module_code_unicity(code, field, formation_id, module_id=None):
|
||||
@ -661,6 +659,7 @@ def module_edit(
|
||||
"explanation": "numéro (1, 2, 3, 4, ...) pour ordre d'affichage",
|
||||
"type": "int",
|
||||
"default": default_num,
|
||||
"allow_null": False,
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -806,7 +805,7 @@ def module_edit(
|
||||
if create:
|
||||
if not matiere_id:
|
||||
# formulaire avec choix UE de rattachement
|
||||
ue = UniteEns.query.get(tf[2]["ue_id"])
|
||||
ue = db.session.get(UniteEns, tf[2]["ue_id"])
|
||||
if ue is None:
|
||||
raise ValueError("UE invalide")
|
||||
matiere = ue.matieres.first()
|
||||
@ -820,7 +819,7 @@ def module_edit(
|
||||
|
||||
tf[2]["semestre_id"] = ue.semestre_idx
|
||||
module_id = do_module_create(tf[2])
|
||||
module = Module.query.get(module_id)
|
||||
module = db.session.get(Module, module_id)
|
||||
else: # EDITION MODULE
|
||||
# l'UE de rattachement peut changer
|
||||
tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!")
|
||||
@ -838,7 +837,7 @@ def module_edit(
|
||||
)
|
||||
# En APC, force le semestre égal à celui de l'UE
|
||||
if is_apc:
|
||||
selected_ue = UniteEns.query.get(tf[2]["ue_id"])
|
||||
selected_ue = db.session.get(UniteEns, tf[2]["ue_id"])
|
||||
if selected_ue is None:
|
||||
raise ValueError("UE invalide")
|
||||
tf[2]["semestre_id"] = selected_ue.semestre_idx
|
||||
@ -854,13 +853,13 @@ def module_edit(
|
||||
module.parcours = formation.referentiel_competence.parcours.all()
|
||||
else:
|
||||
module.parcours = [
|
||||
ApcParcours.query.get(int(parcour_id_str))
|
||||
db.session.get(ApcParcours, int(parcour_id_str))
|
||||
for parcour_id_str in tf[2]["parcours"]
|
||||
]
|
||||
# Modifie les AC
|
||||
if "app_critiques" in tf[2]:
|
||||
module.app_critiques = [
|
||||
ApcAppCritique.query.get(int(ac_id_str))
|
||||
db.session.get(ApcAppCritique, int(ac_id_str))
|
||||
for ac_id_str in tf[2]["app_critiques"]
|
||||
]
|
||||
db.session.add(module)
|
||||
|
@ -36,8 +36,7 @@ from flask import flash, render_template, url_for
|
||||
from flask import g, request
|
||||
from flask_login import current_user
|
||||
|
||||
from app import db
|
||||
from app import log
|
||||
from app import db, log
|
||||
from app.but import apc_edit_ue
|
||||
from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN
|
||||
from app.models import (
|
||||
@ -137,15 +136,14 @@ def do_ue_create(args):
|
||||
ue_id = _ueEditor.create(cnx, args)
|
||||
log(f"do_ue_create: created {ue_id} with {args}")
|
||||
|
||||
formation: Formation = Formation.query.get(args["formation_id"])
|
||||
formation: Formation = db.session.get(Formation, args["formation_id"])
|
||||
formation.invalidate_module_coefs()
|
||||
# news
|
||||
formation = Formation.query.get(args["formation_id"])
|
||||
formation = db.session.get(Formation, args["formation_id"])
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=args["formation_id"],
|
||||
text=f"Modification de la formation {formation.acronyme}",
|
||||
max_frequency=10 * 60,
|
||||
)
|
||||
formation.invalidate_cached_sems()
|
||||
return ue_id
|
||||
@ -230,7 +228,6 @@ def do_ue_delete(ue: UniteEns, delete_validations=False, force=False):
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=formation.id,
|
||||
text=f"Modification de la formation {formation.acronyme}",
|
||||
max_frequency=10 * 60,
|
||||
)
|
||||
#
|
||||
if not force:
|
||||
@ -286,7 +283,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
||||
}
|
||||
submitlabel = "Créer cette UE"
|
||||
can_change_semestre_id = True
|
||||
formation = Formation.query.get(formation_id)
|
||||
formation = db.session.get(Formation, formation_id)
|
||||
if not formation:
|
||||
raise ScoValueError(f"Formation inexistante ! (id={formation_id})")
|
||||
cursus = formation.get_cursus()
|
||||
@ -443,6 +440,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
||||
{
|
||||
"input_type": "boolcheckbox",
|
||||
"title": "UE externe",
|
||||
"readonly": not create, # ne permet pas de transformer une UE existante en externe
|
||||
"explanation": "réservé pour les capitalisations d'UE effectuées à l'extérieur de l'établissement",
|
||||
},
|
||||
),
|
||||
@ -503,7 +501,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
||||
else:
|
||||
clone_form = ""
|
||||
bonus_div = """<div id="bonus_description"></div>"""
|
||||
ue_div = """<div id="ue_list_code"></div>"""
|
||||
ue_div = """<div id="ue_list_code" class="sco_box sco_green_bg"></div>"""
|
||||
return (
|
||||
"\n".join(H)
|
||||
+ tf[1]
|
||||
@ -544,9 +542,11 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
||||
"semestre_id": tf[2]["semestre_idx"],
|
||||
},
|
||||
)
|
||||
ue = UniteEns.query.get(ue_id)
|
||||
ue = db.session.get(UniteEns, ue_id)
|
||||
flash(f"UE créée (code {ue.ue_code})")
|
||||
else:
|
||||
if not tf[2]["numero"]:
|
||||
tf[2]["numero"] = 0
|
||||
do_ue_edit(tf[2])
|
||||
flash("UE modifiée")
|
||||
|
||||
@ -596,7 +596,7 @@ def next_ue_numero(formation_id, semestre_id=None):
|
||||
"""Numero d'une nouvelle UE dans cette formation.
|
||||
Si le semestre est specifie, cherche les UE ayant des modules de ce semestre
|
||||
"""
|
||||
formation = Formation.query.get(formation_id)
|
||||
formation = db.session.get(Formation, formation_id)
|
||||
ues = ue_list(args={"formation_id": formation_id})
|
||||
if not ues:
|
||||
return 0
|
||||
@ -660,7 +660,7 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
|
||||
"""
|
||||
from app.scodoc import sco_formsemestre_validation
|
||||
|
||||
formation: Formation = Formation.query.get(formation_id)
|
||||
formation: Formation = db.session.get(Formation, formation_id)
|
||||
if not formation:
|
||||
raise ScoValueError("invalid formation_id")
|
||||
parcours = formation.get_cursus()
|
||||
@ -756,7 +756,7 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
|
||||
],
|
||||
page_title=f"Programme {formation.acronyme} v{formation.version}",
|
||||
),
|
||||
f"""<h2>{formation.to_html()} {lockicon}
|
||||
f"""<h2>{formation.html()} {lockicon}
|
||||
</h2>
|
||||
""",
|
||||
]
|
||||
@ -1009,12 +1009,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
|
||||
<p><ul>"""
|
||||
)
|
||||
for formsemestre in formsemestres:
|
||||
H.append(
|
||||
f"""<li><a class="stdlink" href="{
|
||||
url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id
|
||||
)}">{formsemestre.titre_mois()}</a>"""
|
||||
)
|
||||
H.append(f"""<li>{formsemestre.html_link_status()}""")
|
||||
if not formsemestre.etat:
|
||||
H.append(" [verrouillé]")
|
||||
else:
|
||||
@ -1381,13 +1376,12 @@ def _ue_table_modules(
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
def ue_sharing_code(ue_code=None, ue_id=None, hide_ue_id=None):
|
||||
def ue_sharing_code(ue_code: str = "", ue_id: int = None, hide_ue_id: int = None):
|
||||
"""HTML list of UE sharing this code
|
||||
Either ue_code or ue_id may be specified.
|
||||
hide_ue_id spécifie un id à retirer de la liste.
|
||||
"""
|
||||
ue_code = str(ue_code)
|
||||
if ue_id:
|
||||
if ue_id is not None:
|
||||
ue = UniteEns.query.get_or_404(ue_id)
|
||||
if not ue_code:
|
||||
ue_code = ue.ue_code
|
||||
@ -1406,29 +1400,36 @@ def ue_sharing_code(ue_code=None, ue_id=None, hide_ue_id=None):
|
||||
.filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
|
||||
if hide_ue_id: # enlève l'ue de depart
|
||||
if hide_ue_id is not None: # enlève l'ue de depart
|
||||
q_ues = q_ues.filter(UniteEns.id != hide_ue_id)
|
||||
|
||||
ues = q_ues.all()
|
||||
msg = " dans les formations du département "
|
||||
if not ues:
|
||||
if ue_id:
|
||||
return (
|
||||
f"""<span class="ue_share">Seule UE avec code {ue_code or '-'}</span>"""
|
||||
)
|
||||
if ue_id is not None:
|
||||
return f"""<span class="ue_share">Seule UE avec code {
|
||||
ue_code if ue_code is not None else '-'}{msg}</span>"""
|
||||
else:
|
||||
return f"""<span class="ue_share">Aucune UE avec code {ue_code or '-'}</span>"""
|
||||
return f"""<span class="ue_share">Aucune UE avec code {
|
||||
ue_code if ue_code is not None else '-'}{msg}</span>"""
|
||||
H = []
|
||||
if ue_id:
|
||||
H.append(
|
||||
f"""<span class="ue_share">Autres UE avec le code {ue_code or '-'}:</span>"""
|
||||
f"""<span class="ue_share">Pour information, autres UEs avec le code {
|
||||
ue_code if ue_code is not None else '-'}{msg}:</span>"""
|
||||
)
|
||||
else:
|
||||
H.append(f"""<span class="ue_share">UE avec le code {ue_code or '-'}:</span>""")
|
||||
H.append(
|
||||
f"""<span class="ue_share">UE avec le code {
|
||||
ue_code if ue_code is not None else '-'}{msg}:</span>"""
|
||||
)
|
||||
H.append("<ul>")
|
||||
for ue in ues:
|
||||
H.append(
|
||||
f"""<li>{ue.acronyme} ({ue.titre}) dans <a class="stdlink"
|
||||
href="{url_for("notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=ue.formation.id)}"
|
||||
f"""<li>{ue.acronyme} ({ue.titre}) dans
|
||||
<a class="stdlink" href="{
|
||||
url_for("notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept, formation_id=ue.formation.id)}"
|
||||
>{ue.formation.acronyme} ({ue.formation.titre})</a>, version {ue.formation.version}
|
||||
</li>
|
||||
"""
|
||||
@ -1460,7 +1461,7 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False):
|
||||
cnx = ndb.GetDBConnexion()
|
||||
_ueEditor.edit(cnx, args)
|
||||
|
||||
formation = Formation.query.get(ue["formation_id"])
|
||||
formation = db.session.get(Formation, ue["formation_id"])
|
||||
if not dont_invalidate_cache:
|
||||
# Invalide les semestres utilisant cette formation
|
||||
# ainsi que les poids et coefs
|
||||
|
@ -62,7 +62,9 @@ def format_etud_ident(etud):
|
||||
else:
|
||||
etud["prenom_etat_civil"] = ""
|
||||
etud["civilite_str"] = format_civilite(etud["civilite"])
|
||||
etud["civilite_etat_civil_str"] = format_civilite(etud["civilite_etat_civil"])
|
||||
etud["civilite_etat_civil_str"] = format_civilite(
|
||||
etud.get("civilite_etat_civil", "X")
|
||||
)
|
||||
# Nom à afficher:
|
||||
if etud["nom_usuel"]:
|
||||
etud["nom_disp"] = etud["nom_usuel"]
|
||||
@ -145,7 +147,7 @@ def format_civilite(civilite):
|
||||
|
||||
def format_etat_civil(etud: dict):
|
||||
if etud["prenom_etat_civil"]:
|
||||
civ = {"M": "M.", "F": "Mme", "X": ""}[etud["civilite_etat_civil"]]
|
||||
civ = {"M": "M.", "F": "Mme", "X": ""}[etud.get("civilite_etat_civil", "X")]
|
||||
return f'{civ} {etud["prenom_etat_civil"]} {etud["nom"]}'
|
||||
else:
|
||||
return etud["nomprenom"]
|
||||
@ -260,7 +262,7 @@ def identite_list(cnx, *a, **kw):
|
||||
|
||||
def identite_edit_nocheck(cnx, args):
|
||||
"""Modifie les champs mentionnes dans args, sans verification ni notification."""
|
||||
etud = Identite.query.get(args["etudid"])
|
||||
etud = db.session.get(Identite, args["etudid"])
|
||||
etud.from_dict(args)
|
||||
db.session.commit()
|
||||
|
||||
@ -669,6 +671,7 @@ def create_etud(cnx, args: dict = None):
|
||||
typ=ScolarNews.NEWS_INSCR,
|
||||
text='Nouvel étudiant <a href="%(url)s">%(nomprenom)s</a>' % etud,
|
||||
url=etud["url"],
|
||||
max_frequency=0,
|
||||
)
|
||||
return etud
|
||||
|
||||
|
@ -129,7 +129,7 @@ def do_evaluation_create(
|
||||
)
|
||||
args = locals()
|
||||
log("do_evaluation_create: args=" + str(args))
|
||||
modimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id)
|
||||
modimpl: ModuleImpl = db.session.get(ModuleImpl, moduleimpl_id)
|
||||
if modimpl is None:
|
||||
raise ValueError("module not found")
|
||||
check_evaluation_args(args)
|
||||
@ -252,12 +252,11 @@ def do_evaluation_delete(evaluation_id):
|
||||
def do_evaluation_get_all_notes(
|
||||
evaluation_id, table="notes_notes", filter_suppressed=True, by_uid=None
|
||||
):
|
||||
"""Toutes les notes pour une evaluation: { etudid : { 'value' : value, 'date' : date ... }}
|
||||
"""Toutes les notes pour une évaluation: { etudid : { 'value' : value, 'date' : date ... }}
|
||||
Attention: inclut aussi les notes des étudiants qui ne sont plus inscrits au module.
|
||||
"""
|
||||
do_cache = (
|
||||
filter_suppressed and table == "notes_notes" and (by_uid is None)
|
||||
) # pas de cache pour (rares) appels via undo_notes ou specifiant un enseignant
|
||||
# pas de cache pour (rares) appels via undo_notes ou specifiant un enseignant
|
||||
do_cache = filter_suppressed and table == "notes_notes" and (by_uid is None)
|
||||
if do_cache:
|
||||
r = sco_cache.EvaluationCache.get(evaluation_id)
|
||||
if r is not None:
|
||||
|
@ -37,11 +37,8 @@ from flask_login import current_user
|
||||
from flask import request
|
||||
|
||||
from app import db
|
||||
from app import log
|
||||
from app import models
|
||||
from app.models.evaluations import Evaluation
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
from app.models import Evaluation, FormSemestre, ModuleImpl
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
@ -62,7 +59,7 @@ def evaluation_create_form(
|
||||
):
|
||||
"Formulaire création/édition d'une évaluation (pas de ses notes)"
|
||||
if evaluation_id is not None:
|
||||
evaluation: Evaluation = models.Evaluation.query.get(evaluation_id)
|
||||
evaluation: Evaluation = db.session.get(Evaluation, evaluation_id)
|
||||
if evaluation is None:
|
||||
raise ScoValueError("Cette évaluation n'existe pas ou plus !")
|
||||
moduleimpl_id = evaluation.moduleimpl_id
|
||||
@ -363,7 +360,7 @@ def evaluation_create_form(
|
||||
evaluation_id = sco_evaluation_db.do_evaluation_create(**tf[2])
|
||||
if is_apc:
|
||||
# Set poids
|
||||
evaluation = models.Evaluation.query.get(evaluation_id)
|
||||
evaluation = db.session.get(Evaluation, evaluation_id)
|
||||
for ue in sem_ues:
|
||||
evaluation.set_ue_poids(ue, tf[2][f"poids_{ue.id}"])
|
||||
db.session.add(evaluation)
|
||||
|
@ -12,6 +12,7 @@ Sur une idée de Pascal Bouron, de Lyon.
|
||||
import time
|
||||
from flask import g, url_for
|
||||
|
||||
from app import db
|
||||
from app.models import Evaluation, FormSemestre
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
@ -113,7 +114,7 @@ def evaluations_recap_table(formsemestre: FormSemestre) -> list[dict]:
|
||||
rows.append(row)
|
||||
line_idx += 1
|
||||
for evaluation_id in modimpl_results.evals_notes:
|
||||
e = Evaluation.query.get(evaluation_id)
|
||||
e = db.session.get(Evaluation, evaluation_id)
|
||||
eval_etat = modimpl_results.evaluations_etat[evaluation_id]
|
||||
row = {
|
||||
"type": "",
|
||||
|
@ -433,7 +433,7 @@ def excel_simple_table(
|
||||
return ws.generate()
|
||||
|
||||
|
||||
def excel_feuille_saisie(e, titreannee, description, lines):
|
||||
def excel_feuille_saisie(evaluation: "Evaluation", titreannee, description, lines):
|
||||
"""Genere feuille excel pour saisie des notes.
|
||||
E: evaluation (dict)
|
||||
lines: liste de tuples
|
||||
@ -512,18 +512,20 @@ def excel_feuille_saisie(e, titreannee, description, lines):
|
||||
# description evaluation
|
||||
ws.append_single_cell_row(scu.unescape_html(description), style_titres)
|
||||
ws.append_single_cell_row(
|
||||
"Evaluation du %s (coef. %g)" % (e["jour"], e["coefficient"]), style
|
||||
"Evaluation du %s (coef. %g)"
|
||||
% (evaluation.jour or "sans date", evaluation.coefficient or 0.0),
|
||||
style,
|
||||
)
|
||||
# ligne blanche
|
||||
ws.append_blank_row()
|
||||
# code et titres colonnes
|
||||
ws.append_row(
|
||||
[
|
||||
ws.make_cell("!%s" % e["evaluation_id"], style_ro),
|
||||
ws.make_cell("!%s" % evaluation.id, style_ro),
|
||||
ws.make_cell("Nom", style_titres),
|
||||
ws.make_cell("Prénom", style_titres),
|
||||
ws.make_cell("Groupe", style_titres),
|
||||
ws.make_cell("Note sur %g" % e["note_max"], style_titres),
|
||||
ws.make_cell("Note sur %g" % (evaluation.note_max or 0.0), style_titres),
|
||||
ws.make_cell("Remarque", style_titres),
|
||||
]
|
||||
)
|
||||
|
@ -28,15 +28,15 @@
|
||||
"""Table recap formation (avec champs éditables)
|
||||
"""
|
||||
import io
|
||||
from zipfile import ZipFile, BadZipfile
|
||||
from zipfile import ZipFile
|
||||
|
||||
from flask import Response
|
||||
from flask import send_file, url_for
|
||||
from flask import g, request
|
||||
from flask_login import current_user
|
||||
|
||||
from app.models import Formation, FormSemestre, UniteEns, Module
|
||||
from app.models.formations import Matiere
|
||||
from app import db
|
||||
from app.models import Formation, FormSemestre, Matiere, Module, UniteEns
|
||||
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
@ -178,7 +178,7 @@ def export_recap_formations_annee_scolaire(annee_scolaire):
|
||||
)
|
||||
formation_ids = {formsemestre.formation.id for formsemestre in formsemestres}
|
||||
for formation_id in formation_ids:
|
||||
formation = Formation.query.get(formation_id)
|
||||
formation = db.session.get(Formation, formation_id)
|
||||
xls = formation_table_recap(formation_id, format="xlsx").data
|
||||
filename = (
|
||||
scu.sanitize_filename(formation.get_titre_version()) + scu.XLSX_SUFFIX
|
||||
|
@ -200,31 +200,31 @@ def do_formsemestres_associate_new_version(
|
||||
|
||||
# New formation:
|
||||
(
|
||||
formation_id,
|
||||
new_formation_id,
|
||||
modules_old2new,
|
||||
ues_old2new,
|
||||
) = sco_formations.formation_create_new_version(formation_id, redirect=False)
|
||||
# Log new ues:
|
||||
for ue_id in ues_old2new:
|
||||
ue = UniteEns.query.get(ue_id)
|
||||
new_ue = UniteEns.query.get(ues_old2new[ue_id])
|
||||
ue = db.session.get(UniteEns, ue_id)
|
||||
new_ue = db.session.get(UniteEns, ues_old2new[ue_id])
|
||||
assert ue.semestre_idx == new_ue.semestre_idx
|
||||
log(f"{ue} -> {new_ue}")
|
||||
# Log new modules
|
||||
for module_id in modules_old2new:
|
||||
mod = Module.query.get(module_id)
|
||||
new_mod = Module.query.get(modules_old2new[module_id])
|
||||
mod = db.session.get(Module, module_id)
|
||||
new_mod = db.session.get(Module, modules_old2new[module_id])
|
||||
assert mod.semestre_id == new_mod.semestre_id
|
||||
log(f"{mod} -> {new_mod}")
|
||||
# re-associate
|
||||
for formsemestre_id in formsemestre_ids:
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
formsemestre.formation_id = formation_id
|
||||
formsemestre.formation_id = new_formation_id
|
||||
db.session.add(formsemestre)
|
||||
_reassociate_moduleimpls(formsemestre, ues_old2new, modules_old2new)
|
||||
|
||||
db.session.commit()
|
||||
return formation_id
|
||||
return new_formation_id
|
||||
|
||||
|
||||
def _reassociate_moduleimpls(
|
||||
@ -246,8 +246,12 @@ def _reassociate_moduleimpls(
|
||||
Evaluation.moduleimpl_id == ModuleImpl.id,
|
||||
ModuleImpl.formsemestre_id == formsemestre.id,
|
||||
):
|
||||
poids.ue_id = ues_old2new[poids.ue_id]
|
||||
db.session.add(poids)
|
||||
if poids.ue_id in ues_old2new:
|
||||
poids.ue_id = ues_old2new[poids.ue_id]
|
||||
db.session.add(poids)
|
||||
else:
|
||||
# poids vers une UE qui n'est pas ou plus dans notre formation
|
||||
db.session.delete(poids)
|
||||
|
||||
# update decisions:
|
||||
for event in ScolarEvent.query.filter_by(formsemestre_id=formsemestre.id):
|
||||
@ -258,8 +262,9 @@ def _reassociate_moduleimpls(
|
||||
for validation in ScolarFormSemestreValidation.query.filter_by(
|
||||
formsemestre_id=formsemestre.id
|
||||
):
|
||||
if validation.ue_id is not None:
|
||||
if (validation.ue_id is not None) and validation.ue_id in ues_old2new:
|
||||
validation.ue_id = ues_old2new[validation.ue_id]
|
||||
# si l'UE n'est pas ou plus dans notre formation, laisse.
|
||||
db.session.add(validation)
|
||||
|
||||
db.session.commit()
|
||||
|
@ -163,7 +163,7 @@ def formation_export_dict(
|
||||
if tags:
|
||||
mod["tags"] = [{"name": x} for x in tags]
|
||||
#
|
||||
module: Module = Module.query.get(module_id)
|
||||
module: Module = db.session.get(Module, module_id)
|
||||
if module.is_apc():
|
||||
# Exporte les coefficients
|
||||
if ue_reference_style == "id":
|
||||
@ -359,7 +359,7 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
|
||||
referentiel_competence_id, ue_info[1]
|
||||
)
|
||||
ue_id = sco_edit_ue.do_ue_create(ue_info[1])
|
||||
ue: UniteEns = UniteEns.query.get(ue_id)
|
||||
ue: UniteEns = db.session.get(UniteEns, ue_id)
|
||||
assert ue
|
||||
if xml_ue_id:
|
||||
ues_old2new[xml_ue_id] = ue_id
|
||||
@ -424,7 +424,7 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
|
||||
if xml_module_id:
|
||||
modules_old2new[int(xml_module_id)] = mod_id
|
||||
if len(mod_info) > 2:
|
||||
module: Module = Module.query.get(mod_id)
|
||||
module: Module = db.session.get(Module, mod_id)
|
||||
tag_names = []
|
||||
ue_coef_dict = {}
|
||||
for child in mod_info[2]:
|
||||
@ -626,7 +626,9 @@ def formation_list_table() -> GenTable:
|
||||
def formation_create_new_version(formation_id, redirect=True):
|
||||
"duplicate formation, with new version number"
|
||||
formation = Formation.query.get_or_404(formation_id)
|
||||
resp = formation_export(formation_id, export_ids=True, format="xml")
|
||||
resp = formation_export(
|
||||
formation_id, export_ids=True, export_external_ues=True, format="xml"
|
||||
)
|
||||
xml_data = resp.get_data(as_text=True)
|
||||
new_id, modules_old2new, ues_old2new = formation_import_xml(
|
||||
xml_data, use_local_refcomp=True
|
||||
@ -636,6 +638,7 @@ def formation_create_new_version(formation_id, redirect=True):
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=new_id,
|
||||
text=f"Nouvelle version de la formation {formation.acronyme}",
|
||||
max_frequency=0,
|
||||
)
|
||||
if redirect:
|
||||
flash("Nouvelle version !")
|
||||
|
@ -261,6 +261,7 @@ def do_formsemestre_create(args, silent=False):
|
||||
typ=ScolarNews.NEWS_SEM,
|
||||
text='Création du semestre <a href="%(url)s">%(titre)s</a>' % args,
|
||||
url=args["url"],
|
||||
max_frequency=0,
|
||||
)
|
||||
return formsemestre_id
|
||||
|
||||
|
@ -793,7 +793,16 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
|
||||
{tf[1]}
|
||||
"""
|
||||
elif tf[0] == -1:
|
||||
return "<h4>annulation</h4>"
|
||||
if formsemestre:
|
||||
return redirect(
|
||||
url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
)
|
||||
)
|
||||
else:
|
||||
return redirect(url_for("notes.index_html", scodoc_dept=g.scodoc_dept))
|
||||
else:
|
||||
if tf[2]["gestion_compensation_lst"]:
|
||||
tf[2]["gestion_compensation"] = True
|
||||
@ -941,7 +950,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
if "parcours" in tf[2]:
|
||||
formsemestre.parcours = [
|
||||
ApcParcours.query.get(int(parcour_id_str))
|
||||
db.session.get(ApcParcours, int(parcour_id_str))
|
||||
for parcour_id_str in tf[2]["parcours"]
|
||||
]
|
||||
db.session.add(formsemestre)
|
||||
@ -1035,7 +1044,7 @@ def formsemestre_delete_moduleimpls(formsemestre_id, module_ids_to_del):
|
||||
ok = True
|
||||
msg = []
|
||||
for module_id in module_ids_to_del:
|
||||
module = Module.query.get(module_id)
|
||||
module = db.session.get(Module, module_id)
|
||||
if module is None:
|
||||
continue # ignore invalid ids
|
||||
modimpls = ModuleImpl.query.filter_by(
|
||||
@ -1215,7 +1224,7 @@ def do_formsemestre_clone(
|
||||
args["etat"] = 1 # non verrouillé
|
||||
formsemestre_id = sco_formsemestre.do_formsemestre_create(args)
|
||||
log(f"created formsemestre {formsemestre_id}")
|
||||
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
|
||||
# 2- create moduleimpls
|
||||
mods_orig = sco_moduleimpl.moduleimpl_list(formsemestre_id=orig_formsemestre_id)
|
||||
for mod_orig in mods_orig:
|
||||
@ -1333,11 +1342,18 @@ Ceci n'est possible que si :
|
||||
cancelbutton="Annuler",
|
||||
)
|
||||
if tf[0] == 0:
|
||||
if formsemestre_has_decisions_or_compensations(formsemestre):
|
||||
has_decisions, message = formsemestre_has_decisions_or_compensations(
|
||||
formsemestre
|
||||
)
|
||||
if has_decisions:
|
||||
H.append(
|
||||
"""<p><b>Ce semestre ne peut pas être supprimé !
|
||||
(il y a des décisions de jury ou des compensations par d'autres semestres)</b>
|
||||
</p>"""
|
||||
f"""<p><b>Ce semestre ne peut pas être supprimé !</b></p>
|
||||
<p>il y a des décisions de jury ou des compensations par d'autres semestres:
|
||||
</p>
|
||||
<ul>
|
||||
<li>{message}</li>
|
||||
</ul>
|
||||
"""
|
||||
)
|
||||
else:
|
||||
H.append(tf[1])
|
||||
@ -1372,32 +1388,46 @@ def formsemestre_delete2(formsemestre_id, dialog_confirmed=False):
|
||||
return flask.redirect(scu.ScoURL())
|
||||
|
||||
|
||||
def formsemestre_has_decisions_or_compensations(formsemestre: FormSemestre):
|
||||
def formsemestre_has_decisions_or_compensations(
|
||||
formsemestre: FormSemestre,
|
||||
) -> tuple[bool, str]:
|
||||
"""True if decision de jury (sem. UE, RCUE, année) émanant de ce semestre
|
||||
ou compensation de ce semestre par d'autres semestres
|
||||
ou autorisations de passage.
|
||||
"""
|
||||
# Validations de semestre ou d'UEs
|
||||
if ScolarFormSemestreValidation.query.filter_by(
|
||||
nb_validations = ScolarFormSemestreValidation.query.filter_by(
|
||||
formsemestre_id=formsemestre.id
|
||||
).count():
|
||||
return True
|
||||
if ScolarFormSemestreValidation.query.filter_by(
|
||||
).count()
|
||||
if nb_validations:
|
||||
return True, f"{nb_validations} validations de semestre ou d'UE"
|
||||
nb_validations = ScolarFormSemestreValidation.query.filter_by(
|
||||
compense_formsemestre_id=formsemestre.id
|
||||
).count():
|
||||
return True
|
||||
).count()
|
||||
if nb_validations:
|
||||
return True, f"{nb_validations} compensations utilisées dans d'autres semestres"
|
||||
# Autorisations d'inscription:
|
||||
if ScolarAutorisationInscription.query.filter_by(
|
||||
nb_validations = ScolarAutorisationInscription.query.filter_by(
|
||||
origin_formsemestre_id=formsemestre.id
|
||||
).count():
|
||||
return True
|
||||
).count()
|
||||
if nb_validations:
|
||||
return (
|
||||
True,
|
||||
f"{nb_validations} autorisations d'inscriptions émanant de ce semestre",
|
||||
)
|
||||
# Validations d'années BUT
|
||||
if ApcValidationAnnee.query.filter_by(formsemestre_id=formsemestre.id).count():
|
||||
return True
|
||||
nb_validations = ApcValidationAnnee.query.filter_by(
|
||||
formsemestre_id=formsemestre.id
|
||||
).count()
|
||||
if nb_validations:
|
||||
return True, f"{nb_validations} validations d'année BUT utilisant ce semestre"
|
||||
# Validations de RCUEs
|
||||
if ApcValidationRCUE.query.filter_by(formsemestre_id=formsemestre.id).count():
|
||||
return True
|
||||
return False
|
||||
nb_validations = ApcValidationRCUE.query.filter_by(
|
||||
formsemestre_id=formsemestre.id
|
||||
).count()
|
||||
if nb_validations:
|
||||
return True, f"{nb_validations} validations de RCUE utilisant ce semestre"
|
||||
return False, ""
|
||||
|
||||
|
||||
def do_formsemestre_delete(formsemestre_id):
|
||||
@ -1500,6 +1530,7 @@ def do_formsemestre_delete(formsemestre_id):
|
||||
typ=ScolarNews.NEWS_SEM,
|
||||
obj=formsemestre_id,
|
||||
text="Suppression du semestre %(titre)s" % sem,
|
||||
max_frequency=0,
|
||||
)
|
||||
|
||||
|
||||
|
@ -517,7 +517,7 @@ def _record_ue_validations_and_coefs(
|
||||
)
|
||||
assert code is None or (note) # si code validant, il faut une note
|
||||
sco_formsemestre_validation.do_formsemestre_validate_previous_ue(
|
||||
formsemestre.id,
|
||||
formsemestre,
|
||||
etud.id,
|
||||
ue.id,
|
||||
note,
|
||||
|
@ -175,9 +175,7 @@ def do_formsemestre_demission(
|
||||
)
|
||||
db.session.add(event)
|
||||
db.session.commit()
|
||||
sco_cache.invalidate_formsemestre(
|
||||
formsemestre_id=formsemestre_id
|
||||
) # > démission ou défaillance
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id)
|
||||
if etat_new == scu.DEMISSION:
|
||||
flash("Démission enregistrée")
|
||||
elif etat_new == scu.DEF:
|
||||
@ -210,7 +208,7 @@ def do_formsemestre_desinscription(etudid, formsemestre_id):
|
||||
|
||||
if nt.etud_has_decision(etudid):
|
||||
raise ScoValueError(
|
||||
"""désinscription impossible: l'étudiant {etud.nomprenom} a
|
||||
f"""désinscription impossible: l'étudiant {etud.nomprenom} a
|
||||
une décision de jury (la supprimer avant si nécessaire)"""
|
||||
)
|
||||
|
||||
|
62
app/scodoc/sco_formsemestre_status.py
Executable file → Normal file
62
app/scodoc/sco_formsemestre_status.py
Executable file → Normal file
@ -36,14 +36,20 @@ from flask import request
|
||||
from flask import flash, redirect, render_template, url_for
|
||||
from flask_login import current_user
|
||||
|
||||
from app import log
|
||||
from app import db, log
|
||||
from app.but.cursus_but import formsemestre_warning_apc_setup
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_common import ResultatsSemestre
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import Evaluation, Formation, Module, ModuleImpl, NotesNotes
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.models import (
|
||||
Evaluation,
|
||||
Formation,
|
||||
FormSemestre,
|
||||
Identite,
|
||||
Module,
|
||||
ModuleImpl,
|
||||
NotesNotes,
|
||||
)
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
@ -254,7 +260,7 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
|
||||
},
|
||||
]
|
||||
# debug :
|
||||
if current_app.config["ENV"] == "development":
|
||||
if current_app.config["DEBUG"]:
|
||||
menu_semestre.append(
|
||||
{
|
||||
"title": "Vérifier l'intégrité",
|
||||
@ -594,6 +600,7 @@ def formsemestre_description_table(
|
||||
formsemestre: FormSemestre = FormSemestre.query.filter_by(
|
||||
id=formsemestre_id, dept_id=g.scodoc_dept_id
|
||||
).first_or_404()
|
||||
is_apc = formsemestre.formation.is_apc()
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id)
|
||||
parcours = codes_cursus.get_cursus_from_code(formsemestre.formation.type_parcours)
|
||||
@ -607,7 +614,7 @@ def formsemestre_description_table(
|
||||
else:
|
||||
ues = formsemestre.get_ues()
|
||||
columns_ids += [f"ue_{ue.id}" for ue in ues]
|
||||
if sco_preferences.get_preference("bul_show_ects", formsemestre_id):
|
||||
if sco_preferences.get_preference("bul_show_ects", formsemestre_id) and not is_apc:
|
||||
columns_ids += ["ects"]
|
||||
columns_ids += ["Inscrits", "Responsable", "Enseignants"]
|
||||
if with_evals:
|
||||
@ -634,6 +641,7 @@ def formsemestre_description_table(
|
||||
sum_coef = 0
|
||||
sum_ects = 0
|
||||
last_ue_id = None
|
||||
formsemestre_parcours_ids = {p.id for p in formsemestre.parcours}
|
||||
for modimpl in formsemestre.modimpls_sorted:
|
||||
# Ligne UE avec ECTS:
|
||||
ue = modimpl.module.ue
|
||||
@ -660,7 +668,7 @@ def formsemestre_description_table(
|
||||
ue_info[
|
||||
f"_{k}_td_attrs"
|
||||
] = f'style="background-color: {ue.color} !important;"'
|
||||
if not formsemestre.formation.is_apc():
|
||||
if not is_apc:
|
||||
# n'affiche la ligne UE qu'en formation classique
|
||||
# car l'UE de rattachement n'a pas d'intérêt en BUT
|
||||
rows.append(ue_info)
|
||||
@ -701,8 +709,17 @@ def formsemestre_description_table(
|
||||
for ue in ues:
|
||||
row[f"ue_{ue.id}"] = coef_dict.get(ue.id, 0.0) or ""
|
||||
if with_parcours:
|
||||
# Intersection des parcours du module avec ceux du formsemestre
|
||||
row["parcours"] = ", ".join(
|
||||
sorted([pa.code for pa in modimpl.module.parcours])
|
||||
[
|
||||
pa.code
|
||||
for pa in (
|
||||
modimpl.module.parcours
|
||||
if modimpl.module.parcours
|
||||
else modimpl.formsemestre.parcours
|
||||
)
|
||||
if pa.id in formsemestre_parcours_ids
|
||||
]
|
||||
)
|
||||
|
||||
rows.append(row)
|
||||
@ -742,7 +759,7 @@ def formsemestre_description_table(
|
||||
e["publish_incomplete_str"] = "Non"
|
||||
e["_publish_incomplete_str_td_attrs"] = 'style="color: red;"'
|
||||
# Poids vers UEs (en APC)
|
||||
evaluation: Evaluation = Evaluation.query.get(e["evaluation_id"])
|
||||
evaluation: Evaluation = db.session.get(Evaluation, e["evaluation_id"])
|
||||
for ue_id, poids in evaluation.get_ue_poids_dict().items():
|
||||
e[f"ue_{ue_id}"] = poids or ""
|
||||
e[f"_ue_{ue_id}_class"] = "poids"
|
||||
@ -829,9 +846,9 @@ def _make_listes_sem(formsemestre: FormSemestre, with_absences=True):
|
||||
</td>
|
||||
<td>
|
||||
<form action="{url_for(
|
||||
"assiduites.signal_assiduites_group", scodoc_dept=g.scodoc_dept
|
||||
"absences.SignaleAbsenceGrSemestre", scodoc_dept=g.scodoc_dept
|
||||
)}" method="get">
|
||||
<input type="hidden" name="date" value="{
|
||||
<input type="hidden" name="datefin" value="{
|
||||
formsemestre.date_fin.strftime("%d/%m/%Y")}"/>
|
||||
<input type="hidden" name="group_ids" value="%(group_id)s"/>
|
||||
<input type="hidden" name="destination" value="{destination}"/>
|
||||
@ -848,8 +865,8 @@ def _make_listes_sem(formsemestre: FormSemestre, with_absences=True):
|
||||
</select>
|
||||
|
||||
<a href="{
|
||||
url_for("assiduites.signal_assiduites_group", scodoc_dept=g.scodoc_dept)
|
||||
}?group_ids=%(group_id)s&jour={datetime.date.today().isoformat()}&formsemestre_id={formsemestre.id}">saisie par semaine</a>
|
||||
url_for("absences.choix_semaine", scodoc_dept=g.scodoc_dept)
|
||||
}?group_id=%(group_id)s">saisie par semaine</a>
|
||||
</form></td>
|
||||
"""
|
||||
else:
|
||||
@ -864,11 +881,15 @@ def _make_listes_sem(formsemestre: FormSemestre, with_absences=True):
|
||||
H.append("<h4>Tous les étudiants</h4>")
|
||||
else:
|
||||
H.append("<h4>Groupes de %(partition_name)s</h4>" % partition)
|
||||
partition_is_empty = True
|
||||
groups = sco_groups.get_partition_groups(partition)
|
||||
if groups:
|
||||
H.append("<table>")
|
||||
for group in groups:
|
||||
n_members = len(sco_groups.get_group_members(group["group_id"]))
|
||||
if n_members == 0:
|
||||
continue # skip empty groups
|
||||
partition_is_empty = False
|
||||
group["url_etat"] = url_for(
|
||||
"absences.EtatAbsencesGr",
|
||||
group_ids=group["group_id"],
|
||||
@ -901,13 +922,14 @@ def _make_listes_sem(formsemestre: FormSemestre, with_absences=True):
|
||||
|
||||
H.append("</tr>")
|
||||
H.append("</table>")
|
||||
else:
|
||||
H.append('<p class="help indent">Aucun groupe dans cette partition')
|
||||
if partition_is_empty:
|
||||
H.append('<p class="help indent">Aucun groupe peuplé dans cette partition')
|
||||
if sco_groups.sco_permissions_check.can_change_groups(formsemestre.id):
|
||||
H.append(
|
||||
f""" (<a href="{url_for("scolar.affect_groups",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
partition_id=partition["partition_id"])
|
||||
f""" (<a href="{url_for("scolar.partition_editor",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
edit_partition=1)
|
||||
}" class="stdlink">créer</a>)"""
|
||||
)
|
||||
H.append("</p>")
|
||||
@ -959,7 +981,7 @@ def html_expr_diagnostic(diagnostics):
|
||||
|
||||
def formsemestre_status_head(formsemestre_id: int = None, page_title: str = None):
|
||||
"""En-tête HTML des pages "semestre" """
|
||||
sem: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||
sem: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
|
||||
if not sem:
|
||||
raise ScoValueError("Semestre inexistant (il a peut être été supprimé ?)")
|
||||
formation: Formation = sem.formation
|
||||
@ -1210,7 +1232,7 @@ def formsemestre_tableau_modules(
|
||||
H = []
|
||||
prev_ue_id = None
|
||||
for modimpl in modimpls:
|
||||
mod: Module = Module.query.get(modimpl["module_id"])
|
||||
mod: Module = db.session.get(Module, modimpl["module_id"])
|
||||
moduleimpl_status_url = url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
|
@ -31,15 +31,17 @@ import time
|
||||
|
||||
import flask
|
||||
from flask import url_for, flash, g, request
|
||||
from app.models.etudiants import Identite
|
||||
from flask_login import current_user
|
||||
import sqlalchemy as sa
|
||||
|
||||
from app.models.etudiants import Identite
|
||||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import db, log
|
||||
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import Formation, FormSemestre, UniteEns
|
||||
from app.models import Formation, FormSemestre, UniteEns, ScolarNews
|
||||
from app.models.notes import etud_has_notes_attente
|
||||
from app.models.validations import (
|
||||
ScolarAutorisationInscription,
|
||||
@ -65,6 +67,8 @@ from app.scodoc.sco_cursus_dut import etud_est_inscrit_ue
|
||||
from app.scodoc import sco_photos
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_pv_dict
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------
|
||||
def formsemestre_validation_etud_form(
|
||||
@ -396,7 +400,7 @@ def formsemestre_validation_etud(
|
||||
selected_choice = choice
|
||||
break
|
||||
if not selected_choice:
|
||||
raise ValueError("code choix invalide ! (%s)" % codechoice)
|
||||
raise ValueError(f"code choix invalide ! ({codechoice})")
|
||||
#
|
||||
Se.valide_decision(selected_choice) # enregistre
|
||||
return _redirect_valid_choice(
|
||||
@ -511,7 +515,7 @@ def decisions_possible_rows(Se, assiduite, subtitle="", trclass=""):
|
||||
|
||||
|
||||
def formsemestre_recap_parcours_table(
|
||||
Se,
|
||||
situation_etud_cursus: sco_cursus_dut.SituationEtudCursus,
|
||||
etudid,
|
||||
with_links=False,
|
||||
with_all_columns=True,
|
||||
@ -549,16 +553,18 @@ def formsemestre_recap_parcours_table(
|
||||
"""
|
||||
)
|
||||
# titres des UE
|
||||
H.append("<th></th>" * Se.nb_max_ue)
|
||||
H.append("<th></th>" * situation_etud_cursus.nb_max_ue)
|
||||
#
|
||||
if with_links:
|
||||
H.append("<th></th>")
|
||||
H.append("<th></th></tr>")
|
||||
|
||||
num_sem = 0
|
||||
for sem in Se.get_semestres():
|
||||
is_prev = Se.prev and (Se.prev["formsemestre_id"] == sem["formsemestre_id"])
|
||||
is_cur = Se.formsemestre_id == sem["formsemestre_id"]
|
||||
for sem in situation_etud_cursus.get_semestres():
|
||||
is_prev = situation_etud_cursus.prev and (
|
||||
situation_etud_cursus.prev["formsemestre_id"] == sem["formsemestre_id"]
|
||||
)
|
||||
is_cur = situation_etud_cursus.formsemestre_id == sem["formsemestre_id"]
|
||||
num_sem += 1
|
||||
|
||||
dpv = sco_pv_dict.dict_pvjury(sem["formsemestre_id"], etudids=[etudid])
|
||||
@ -570,7 +576,7 @@ def formsemestre_recap_parcours_table(
|
||||
else:
|
||||
ass = ""
|
||||
|
||||
formsemestre = FormSemestre.query.get(sem["formsemestre_id"])
|
||||
formsemestre = db.session.get(FormSemestre, sem["formsemestre_id"])
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
if is_cur:
|
||||
type_sem = "*" # now unused
|
||||
@ -581,7 +587,7 @@ def formsemestre_recap_parcours_table(
|
||||
else:
|
||||
type_sem = ""
|
||||
class_sem = "sem_autre"
|
||||
if sem["formation_code"] != Se.formation.formation_code:
|
||||
if sem["formation_code"] != situation_etud_cursus.formation.formation_code:
|
||||
class_sem += " sem_autre_formation"
|
||||
if sem["bul_bgcolor"]:
|
||||
bgcolor = sem["bul_bgcolor"]
|
||||
@ -645,7 +651,7 @@ def formsemestre_recap_parcours_table(
|
||||
H.append("<td><em>en cours</em></td>")
|
||||
H.append(f"""<td class="rcp_nonass">{ass}</td>""") # abs
|
||||
# acronymes UEs auxquelles l'étudiant est inscrit (ou capitalisé)
|
||||
ues = list(nt.etud_ues(etudid))
|
||||
ues = list(nt.etud_ues(etudid)) # nb: en BUT, les UE "dispensées" sont incluses
|
||||
cnx = ndb.GetDBConnexion()
|
||||
etud_ue_status = {ue.id: nt.get_etud_ue_status(etudid, ue.id) for ue in ues}
|
||||
if not nt.is_apc:
|
||||
@ -659,8 +665,10 @@ def formsemestre_recap_parcours_table(
|
||||
|
||||
for ue in ues:
|
||||
H.append(f"""<td class="ue_acro"><span>{ue.acronyme}</span></td>""")
|
||||
if len(ues) < Se.nb_max_ue:
|
||||
H.append(f"""<td colspan="{Se.nb_max_ue - len(ues)}"></td>""")
|
||||
if len(ues) < situation_etud_cursus.nb_max_ue:
|
||||
H.append(
|
||||
f"""<td colspan="{situation_etud_cursus.nb_max_ue - len(ues)}"></td>"""
|
||||
)
|
||||
# indique le semestre compensé par celui ci:
|
||||
if decision_sem and decision_sem["compense_formsemestre_id"]:
|
||||
csem = sco_formsemestre.get_formsemestre(
|
||||
@ -685,7 +693,7 @@ def formsemestre_recap_parcours_table(
|
||||
if not sem["etat"]: # locked
|
||||
lockicon = scu.icontag("lock32_img", title="verrouillé", border="0")
|
||||
default_sem_info += lockicon
|
||||
if sem["formation_code"] != Se.formation.formation_code:
|
||||
if sem["formation_code"] != situation_etud_cursus.formation.formation_code:
|
||||
default_sem_info += f"""Autre formation: {sem["formation_code"]}"""
|
||||
H.append(
|
||||
'<td class="datefin">%s</td><td class="sem_info">%s</td>'
|
||||
@ -722,14 +730,21 @@ def formsemestre_recap_parcours_table(
|
||||
explanation_ue.append(
|
||||
f"""Capitalisée le {ue_status["event_date"] or "?"}."""
|
||||
)
|
||||
|
||||
# Dispense BUT ?
|
||||
if (etudid, ue.id) in nt.dispense_ues:
|
||||
moy_ue_txt = "❎" if (ue_status and ue_status["is_capitalized"]) else "⭕"
|
||||
explanation_ue.append("non inscrit (dispense)")
|
||||
else:
|
||||
moy_ue_txt = scu.fmt_note(moy_ue)
|
||||
H.append(
|
||||
f"""<td class="{class_ue}" title="{
|
||||
" ".join(explanation_ue)
|
||||
}">{scu.fmt_note(moy_ue)}</td>"""
|
||||
}">{moy_ue_txt}</td>"""
|
||||
)
|
||||
if len(ues) < situation_etud_cursus.nb_max_ue:
|
||||
H.append(
|
||||
f"""<td colspan="{situation_etud_cursus.nb_max_ue - len(ues)}"></td>"""
|
||||
)
|
||||
if len(ues) < Se.nb_max_ue:
|
||||
H.append(f"""<td colspan="{Se.nb_max_ue - len(ues)}"></td>""")
|
||||
|
||||
H.append("<td></td>")
|
||||
if with_links:
|
||||
@ -991,16 +1006,26 @@ def do_formsemestre_validation_auto(formsemestre_id):
|
||||
)
|
||||
nb_valid += 1
|
||||
log(
|
||||
"do_formsemestre_validation_auto: %d validations, %d conflicts"
|
||||
% (nb_valid, len(conflicts))
|
||||
f"do_formsemestre_validation_auto: {nb_valid} validations, {len(conflicts)} conflicts"
|
||||
)
|
||||
H = [html_sco_header.sco_header(page_title="Saisie automatique")]
|
||||
H.append(
|
||||
"""<h2>Saisie automatique des décisions du semestre %s</h2>
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_JURY,
|
||||
obj=formsemestre.id,
|
||||
text=f"""Calcul jury automatique du semestre {formsemestre.html_link_status()
|
||||
} ({nb_valid} décisions)""",
|
||||
url=url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
),
|
||||
)
|
||||
H = [
|
||||
f"""{html_sco_header.sco_header(page_title="Saisie automatique")}
|
||||
<h2>Saisie automatique des décisions du semestre {formsemestre.titre_annee()}</h2>
|
||||
<p>Opération effectuée.</p>
|
||||
<p>%d étudiants validés (sur %s)</p>"""
|
||||
% (sem["titreannee"], nb_valid, len(etudids))
|
||||
)
|
||||
<p>{nb_valid} étudiants validés sur {len(etudids)}</p>
|
||||
"""
|
||||
]
|
||||
if conflicts:
|
||||
H.append(
|
||||
f"""<p><b>Attention:</b> {len(conflicts)} étudiants non modifiés
|
||||
@ -1059,64 +1084,44 @@ def formsemestre_validation_suppress_etud(formsemestre_id, etudid):
|
||||
) # > suppr. decision jury (peut affecter de plusieurs semestres utilisant UE capitalisée)
|
||||
|
||||
|
||||
def formsemestre_validate_previous_ue(formsemestre_id, etudid):
|
||||
def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite):
|
||||
"""Form. saisie UE validée hors ScoDoc
|
||||
(pour étudiants arrivant avec un UE antérieurement validée).
|
||||
"""
|
||||
from app.scodoc import sco_formations
|
||||
formation: Formation = formsemestre.formation
|
||||
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
formation: Formation = Formation.query.get_or_404(sem["formation_id"])
|
||||
H = [
|
||||
html_sco_header.sco_header(
|
||||
page_title="Validation UE",
|
||||
javascripts=["js/validate_previous_ue.js"],
|
||||
# Toutes les UEs non bonus de cette formation sont présentées
|
||||
# avec indice de semestre <= semestre courant ou NULL
|
||||
ues = formation.ues.filter(
|
||||
UniteEns.type != UE_SPORT,
|
||||
db.or_(
|
||||
UniteEns.semestre_idx == None,
|
||||
UniteEns.semestre_idx <= formsemestre.semestre_id,
|
||||
),
|
||||
'<table style="width: 100%"><tr><td>',
|
||||
"""<h2 class="formsemestre">%s: validation d'une UE antérieure</h2>"""
|
||||
% etud["nomprenom"],
|
||||
(
|
||||
'</td><td style="text-align: right;"><a href="%s">%s</a></td></tr></table>'
|
||||
% (
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
|
||||
sco_photos.etud_photo_html(etud, title="fiche de %s" % etud["nom"]),
|
||||
)
|
||||
),
|
||||
f"""<p class="help">Utiliser cette page pour enregistrer une UE validée antérieurement,
|
||||
<em>dans un semestre hors ScoDoc</em>.</p>
|
||||
<p><b>Les UE validées dans ScoDoc sont déjà
|
||||
automatiquement prises en compte</b>. Cette page n'est utile que pour les étudiants ayant
|
||||
suivi un début de cursus dans <b>un autre établissement</b>, ou bien dans un semestre géré <b>sans
|
||||
ScoDoc</b> et qui <b>redouble</b> ce semestre
|
||||
(<em>ne pas utiliser pour les semestres précédents !</em>).
|
||||
</p>
|
||||
<p>Notez que l'UE est validée, avec enregistrement immédiat de la décision et
|
||||
l'attribution des ECTS.</p>
|
||||
<p>On ne peut prendre en compte ici que les UE du cursus <b>{formation.titre}</b></p>
|
||||
""",
|
||||
).order_by(UniteEns.semestre_idx, UniteEns.numero)
|
||||
|
||||
ue_names = ["Choisir..."] + [
|
||||
f"""{('S'+str(ue.semestre_idx)+' : ') if ue.semestre_idx is not None else ''
|
||||
}{ue.acronyme} {ue.titre} ({ue.ue_code or ""})"""
|
||||
for ue in ues
|
||||
]
|
||||
|
||||
# Toutes les UE de cette formation sont présentées (même celles des autres semestres)
|
||||
ues = formation.ues.order_by(UniteEns.numero)
|
||||
ue_names = ["Choisir..."] + [f"{ue.acronyme} {ue.titre}" for ue in ues]
|
||||
ue_ids = [""] + [ue.id for ue in ues]
|
||||
tf = TrivialFormulator(
|
||||
request.base_url,
|
||||
scu.get_request_args(),
|
||||
form_descr = [
|
||||
("etudid", {"input_type": "hidden"}),
|
||||
("formsemestre_id", {"input_type": "hidden"}),
|
||||
(
|
||||
("etudid", {"input_type": "hidden"}),
|
||||
("formsemestre_id", {"input_type": "hidden"}),
|
||||
(
|
||||
"ue_id",
|
||||
{
|
||||
"input_type": "menu",
|
||||
"title": "Unité d'Enseignement (UE)",
|
||||
"allow_null": False,
|
||||
"allowed_values": ue_ids,
|
||||
"labels": ue_names,
|
||||
},
|
||||
),
|
||||
"ue_id",
|
||||
{
|
||||
"input_type": "menu",
|
||||
"title": "Unité d'Enseignement (UE)",
|
||||
"allow_null": False,
|
||||
"allowed_values": ue_ids,
|
||||
"labels": ue_names,
|
||||
},
|
||||
),
|
||||
]
|
||||
if not formation.is_apc():
|
||||
form_descr.append(
|
||||
(
|
||||
"semestre_id",
|
||||
{
|
||||
@ -1127,69 +1132,185 @@ def formsemestre_validate_previous_ue(formsemestre_id, etudid):
|
||||
"allowed_values": [""] + [x for x in range(11)],
|
||||
"labels": ["-"] + list(range(11)),
|
||||
},
|
||||
),
|
||||
(
|
||||
"date",
|
||||
{
|
||||
"input_type": "date",
|
||||
"size": 9,
|
||||
"explanation": "j/m/a",
|
||||
"default": time.strftime("%d/%m/%Y"),
|
||||
},
|
||||
),
|
||||
(
|
||||
"moy_ue",
|
||||
{
|
||||
"type": "float",
|
||||
"allow_null": False,
|
||||
"min_value": 0,
|
||||
"max_value": 20,
|
||||
"title": "Moyenne (/20) obtenue dans cette UE:",
|
||||
},
|
||||
),
|
||||
)
|
||||
)
|
||||
ue_codes = sorted(codes_cursus.CODES_JURY_UE)
|
||||
form_descr += [
|
||||
(
|
||||
"date",
|
||||
{
|
||||
"input_type": "date",
|
||||
"size": 9,
|
||||
"explanation": "j/m/a",
|
||||
"default": time.strftime("%d/%m/%Y"),
|
||||
},
|
||||
),
|
||||
cancelbutton="Annuler",
|
||||
(
|
||||
"moy_ue",
|
||||
{
|
||||
"type": "float",
|
||||
"allow_null": False,
|
||||
"min_value": 0,
|
||||
"max_value": 20,
|
||||
"title": "Moyenne (/20) obtenue dans cette UE:",
|
||||
},
|
||||
),
|
||||
(
|
||||
"code_jury",
|
||||
{
|
||||
"input_type": "menu",
|
||||
"title": "Code jury",
|
||||
"explanation": " code donné par le jury (ADM si validée normalement)",
|
||||
"allow_null": True,
|
||||
"allowed_values": [""] + ue_codes,
|
||||
"labels": ["-"] + ue_codes,
|
||||
"default": ADM,
|
||||
},
|
||||
),
|
||||
]
|
||||
tf = TrivialFormulator(
|
||||
request.base_url,
|
||||
scu.get_request_args(),
|
||||
form_descr,
|
||||
cancelbutton="Revenir au bulletin",
|
||||
submitlabel="Enregistrer validation d'UE",
|
||||
)
|
||||
if tf[0] == 0:
|
||||
X = """
|
||||
<div id="ue_list_etud_validations"><!-- filled by get_etud_ue_cap_html --></div>
|
||||
<div id="ue_list_code"><!-- filled by ue_sharing_code --></div>
|
||||
"""
|
||||
warn, ue_multiples = check_formation_ues(formation.id)
|
||||
return "\n".join(H) + tf[1] + X + warn + html_sco_header.sco_footer()
|
||||
elif tf[0] == -1:
|
||||
return flask.redirect(
|
||||
scu.NotesURL()
|
||||
+ "/formsemestre_status?formsemestre_id="
|
||||
+ str(formsemestre_id)
|
||||
)
|
||||
else:
|
||||
if tf[2]["semestre_id"]:
|
||||
semestre_id = int(tf[2]["semestre_id"])
|
||||
else:
|
||||
semestre_id = None
|
||||
do_formsemestre_validate_previous_ue(
|
||||
formsemestre_id,
|
||||
etudid,
|
||||
tf[2]["ue_id"],
|
||||
tf[2]["moy_ue"],
|
||||
tf[2]["date"],
|
||||
semestre_id=semestre_id,
|
||||
)
|
||||
flash("Validation d'UE enregistrée")
|
||||
return f"""
|
||||
{html_sco_header.sco_header(
|
||||
page_title="Validation UE antérieure",
|
||||
javascripts=["js/validate_previous_ue.js"],
|
||||
cssstyles=["css/jury_delete_manual.css"],
|
||||
etudid=etud.id,
|
||||
formsemestre_id=formsemestre.id,
|
||||
)}
|
||||
<h2 class="formsemestre">Gestion des validations d'UEs antérieures
|
||||
de {etud.html_link_fiche()}
|
||||
</h2>
|
||||
|
||||
<p class="help">Utiliser cette page pour enregistrer des UEs validées antérieurement,
|
||||
<em>dans un semestre hors ScoDoc</em>.</p>
|
||||
<p class="expl"><b>Les UE validées dans ScoDoc sont
|
||||
automatiquement prises en compte</b>.
|
||||
</p>
|
||||
<p>Cette page est surtout utile pour les étudiants ayant
|
||||
suivi un début de cursus dans <b>un autre établissement</b>, ou qui
|
||||
ont suivi une UE à l'étranger ou dans un semestre géré <b>sans ScoDoc</b>.
|
||||
</p>
|
||||
<p>Pour les semestres précédents gérés avec ScoDoc, passer par la page jury normale.
|
||||
</p>
|
||||
<p>Notez que l'UE est validée, avec enregistrement immédiat de la décision et
|
||||
l'attribution des ECTS si le code jury est validant (ADM).
|
||||
</p>
|
||||
<p>On ne peut valider ici que les UEs du cursus <b>{formation.titre}</b></p>
|
||||
|
||||
{_get_etud_ue_cap_html(etud, formsemestre)}
|
||||
|
||||
<div class="sco_box">
|
||||
<div class="sco_box_title">
|
||||
Enregistrer une UE antérieure
|
||||
</div>
|
||||
{tf[1]}
|
||||
</div>
|
||||
<div id="ue_list_code" class="sco_box sco_green_bg">
|
||||
<!-- filled by ue_sharing_code -->
|
||||
</div>
|
||||
{check_formation_ues(formation.id)[0]}
|
||||
{html_sco_header.sco_footer()}
|
||||
"""
|
||||
|
||||
dest_url = url_for(
|
||||
"notes.formsemestre_validate_previous_ue",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
etudid=etud.id,
|
||||
)
|
||||
if tf[0] == -1:
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.formsemestre_bulletinetud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
etudid=etudid,
|
||||
formsemestre_id=formsemestre.id,
|
||||
etudid=etud.id,
|
||||
)
|
||||
)
|
||||
if tf[2].get("semestre_id"):
|
||||
semestre_id = int(tf[2]["semestre_id"])
|
||||
else:
|
||||
semestre_id = None
|
||||
|
||||
if tf[2]["code_jury"] not in CODES_JURY_UE:
|
||||
flash("Code UE invalide")
|
||||
return flask.redirect(dest_url)
|
||||
do_formsemestre_validate_previous_ue(
|
||||
formsemestre,
|
||||
etud.id,
|
||||
tf[2]["ue_id"],
|
||||
tf[2]["moy_ue"],
|
||||
tf[2]["date"],
|
||||
code=tf[2]["code_jury"],
|
||||
semestre_id=semestre_id,
|
||||
)
|
||||
flash("Validation d'UE enregistrée")
|
||||
return flask.redirect(dest_url)
|
||||
|
||||
|
||||
def _get_etud_ue_cap_html(etud: Identite, formsemestre: FormSemestre) -> str:
|
||||
"""HTML listant les validations d'UEs pour cet étudiant dans des formations de même
|
||||
code que celle du formsemestre indiqué.
|
||||
"""
|
||||
validations: list[ScolarFormSemestreValidation] = (
|
||||
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
|
||||
.join(UniteEns)
|
||||
.join(Formation)
|
||||
.filter_by(formation_code=formsemestre.formation.formation_code)
|
||||
.order_by(
|
||||
sa.desc(UniteEns.semestre_idx),
|
||||
UniteEns.acronyme,
|
||||
sa.desc(ScolarFormSemestreValidation.event_date),
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
if not validations:
|
||||
return ""
|
||||
H = [
|
||||
f"""<div class="sco_box sco_lightgreen_bg ue_list_etud_validations">
|
||||
<div class="sco_box_title">Validations d'UEs dans cette formation</div>
|
||||
<div class="help">Liste de toutes les UEs validées par {etud.html_link_fiche()},
|
||||
sur des semestres ou déclarées comme "antérieures" (externes).
|
||||
</div>
|
||||
<ul class="liste_validations">"""
|
||||
]
|
||||
for validation in validations:
|
||||
if validation.formsemestre_id is None:
|
||||
origine = " enregistrée d'un parcours antérieur (hors ScoDoc)"
|
||||
else:
|
||||
origine = f", du semestre {formsemestre.html_link_status()}"
|
||||
if validation.semestre_id is not None:
|
||||
origine += f" (<b>S{validation.semestre_id}</b>)"
|
||||
H.append(f"""<li>{validation.html()}""")
|
||||
if (validation.formsemestre and validation.formsemestre.can_edit_jury()) or (
|
||||
current_user and current_user.has_permission(Permission.ScoEtudInscrit)
|
||||
):
|
||||
H.append(
|
||||
f"""
|
||||
<form class="inline-form">
|
||||
<button
|
||||
data-v_id="{validation.id}" data-type="validation_ue" data-etudid="{etud.id}"
|
||||
>effacer</button>
|
||||
</form>
|
||||
""",
|
||||
)
|
||||
else:
|
||||
H.append(scu.icontag("lock_img", border="0", title="Semestre verrouillé"))
|
||||
H.append("</li>")
|
||||
H.append("</ul></div>")
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
def do_formsemestre_validate_previous_ue(
|
||||
formsemestre_id,
|
||||
formsemestre: FormSemestre,
|
||||
etudid,
|
||||
ue_id,
|
||||
moy_ue,
|
||||
@ -1202,21 +1323,20 @@ def do_formsemestre_validate_previous_ue(
|
||||
Si le coefficient est spécifié, modifie le coefficient de
|
||||
cette UE (utile seulement pour les semestres extérieurs).
|
||||
"""
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
ue: UniteEns = UniteEns.query.get_or_404(ue_id)
|
||||
|
||||
cnx = ndb.GetDBConnexion()
|
||||
if ue_coefficient != None:
|
||||
if ue_coefficient is not None:
|
||||
sco_formsemestre.do_formsemestre_uecoef_edit_or_create(
|
||||
cnx, formsemestre_id, ue_id, ue_coefficient
|
||||
cnx, formsemestre.id, ue_id, ue_coefficient
|
||||
)
|
||||
else:
|
||||
sco_formsemestre.do_formsemestre_uecoef_delete(cnx, formsemestre_id, ue_id)
|
||||
sco_formsemestre.do_formsemestre_uecoef_delete(cnx, formsemestre.id, ue_id)
|
||||
sco_cursus_dut.do_formsemestre_validate_ue(
|
||||
cnx,
|
||||
nt,
|
||||
formsemestre_id, # "importe" cette UE dans le semestre (new 3/2015)
|
||||
formsemestre.id, # "importe" cette UE dans le semestre (new 3/2015)
|
||||
etudid,
|
||||
ue_id,
|
||||
code,
|
||||
@ -1254,62 +1374,6 @@ def _invalidate_etud_formation_caches(etudid, formation_id):
|
||||
) # > modif decision UE (inval tous semestres avec cet etudiant, ok mais conservatif)
|
||||
|
||||
|
||||
def get_etud_ue_cap_html(etudid, formsemestre_id, ue_id):
|
||||
"""Ramene bout de HTML pour pouvoir supprimer une validation de cette UE"""
|
||||
valids = ndb.SimpleDictFetch(
|
||||
"""SELECT SFV.*
|
||||
FROM scolar_formsemestre_validation SFV
|
||||
WHERE ue_id=%(ue_id)s
|
||||
AND etudid=%(etudid)s""",
|
||||
{"etudid": etudid, "ue_id": ue_id},
|
||||
)
|
||||
if not valids:
|
||||
return ""
|
||||
H = [
|
||||
'<div class="existing_valids"><span>Validations existantes pour cette UE:</span><ul>'
|
||||
]
|
||||
for valid in valids:
|
||||
valid["event_date"] = ndb.DateISOtoDMY(valid["event_date"])
|
||||
if valid["moy_ue"] != None:
|
||||
valid["m"] = ", moyenne %(moy_ue)g/20" % valid
|
||||
else:
|
||||
valid["m"] = ""
|
||||
if valid["formsemestre_id"]:
|
||||
sem = sco_formsemestre.get_formsemestre(valid["formsemestre_id"])
|
||||
valid["s"] = ", du semestre %s" % sem["titreannee"]
|
||||
else:
|
||||
valid["s"] = " enregistrée d'un parcours antérieur (hors ScoDoc)"
|
||||
if valid["semestre_id"]:
|
||||
valid["s"] += " (<b>S%d</b>)" % valid["semestre_id"]
|
||||
valid["ds"] = formsemestre_id
|
||||
H.append(
|
||||
'<li>%(code)s%(m)s%(s)s, le %(event_date)s <a class="stdlink" href="etud_ue_suppress_validation?etudid=%(etudid)s&ue_id=%(ue_id)s&formsemestre_id=%(ds)s" title="supprime cette validation">effacer</a></li>'
|
||||
% valid
|
||||
)
|
||||
H.append("</ul></div>")
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
def etud_ue_suppress_validation(etudid, formsemestre_id, ue_id):
|
||||
"""Suppress a validation (ue_id, etudid) and redirect to formsemestre"""
|
||||
log("etud_ue_suppress_validation( %s, %s, %s)" % (etudid, formsemestre_id, ue_id))
|
||||
cnx = ndb.GetDBConnexion()
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
cursor.execute(
|
||||
"DELETE FROM scolar_formsemestre_validation WHERE etudid=%(etudid)s and ue_id=%(ue_id)s",
|
||||
{"etudid": etudid, "ue_id": ue_id},
|
||||
)
|
||||
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
_invalidate_etud_formation_caches(etudid, sem["formation_id"])
|
||||
|
||||
return flask.redirect(
|
||||
scu.NotesURL()
|
||||
+ "/formsemestre_validate_previous_ue?etudid=%s&formsemestre_id=%s"
|
||||
% (etudid, formsemestre_id)
|
||||
)
|
||||
|
||||
|
||||
def check_formation_ues(formation_id):
|
||||
"""Verifie que les UE d'une formation sont chacune utilisée dans un seul semestre_id
|
||||
Si ce n'est pas le cas, c'est probablement (mais pas forcément) une erreur de
|
||||
|
@ -34,7 +34,6 @@ Optimisation possible:
|
||||
|
||||
"""
|
||||
import collections
|
||||
import operator
|
||||
import time
|
||||
|
||||
from xml.etree import ElementTree
|
||||
@ -45,15 +44,14 @@ from flask import g, request
|
||||
from flask import url_for, make_response
|
||||
from sqlalchemy.sql import text
|
||||
|
||||
from app import db
|
||||
from app import cache, db, log
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre, Identite
|
||||
from app.models import FormSemestre, Identite, Scolog
|
||||
from app.models import GROUPNAME_STR_LEN, SHORT_STR_LEN
|
||||
from app.models.groups import GroupDescr, Partition
|
||||
from app.models.groups import GroupDescr, Partition, group_membership
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app import log, cache
|
||||
from app.scodoc.scolog import logdb
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_cache
|
||||
@ -94,7 +92,7 @@ groupEditor = ndb.EditableTable(
|
||||
group_list = groupEditor.list
|
||||
|
||||
|
||||
def get_group(group_id: int) -> dict:
|
||||
def get_group(group_id: int) -> dict: # OBSOLETE !
|
||||
"""Returns group object, with partition"""
|
||||
r = ndb.SimpleDictFetch(
|
||||
"""SELECT gd.id AS group_id, gd.*, p.id AS partition_id, p.*
|
||||
@ -124,7 +122,7 @@ def group_delete(group_id: int):
|
||||
)
|
||||
|
||||
|
||||
def get_partition(partition_id):
|
||||
def get_partition(partition_id): # OBSOLETE
|
||||
r = ndb.SimpleDictFetch(
|
||||
"""SELECT p.id AS partition_id, p.*
|
||||
FROM partition p
|
||||
@ -200,7 +198,7 @@ def get_formsemestre_etuds_groups(formsemestre_id: int) -> dict:
|
||||
return d
|
||||
|
||||
|
||||
def get_partition_groups(partition):
|
||||
def get_partition_groups(partition): # OBSOLETE !
|
||||
"""List of groups in this partition (list of dicts).
|
||||
Some groups may be empty."""
|
||||
return ndb.SimpleDictFetch(
|
||||
@ -243,7 +241,7 @@ def get_default_group(formsemestre_id, fix_if_missing=False):
|
||||
return group.id
|
||||
# debug check
|
||||
if len(r) != 1:
|
||||
raise ScoException(f"invalid group structure for {formsemestre_id}")
|
||||
log(f"invalid group structure for {formsemestre_id}: {len(r)}")
|
||||
group_id = r[0]["group_id"]
|
||||
return group_id
|
||||
|
||||
@ -452,7 +450,7 @@ def get_etud_formsemestre_groups(
|
||||
),
|
||||
{"etudid": etud.id, "formsemestre_id": formsemestre.id},
|
||||
)
|
||||
return [GroupDescr.query.get(group_id) for group_id in cursor]
|
||||
return [db.session.get(GroupDescr, group_id) for group_id in cursor]
|
||||
|
||||
|
||||
# Ancienne fonction:
|
||||
@ -562,10 +560,10 @@ def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
|
||||
x_group = Element(
|
||||
"group",
|
||||
partition_id=str(partition_id),
|
||||
partition_name=partition["partition_name"],
|
||||
partition_name=partition["partition_name"] or "",
|
||||
groups_editable=str(int(partition["groups_editable"])),
|
||||
group_id=str(group["group_id"]),
|
||||
group_name=group["group_name"],
|
||||
group_name=group["group_name"] or "",
|
||||
)
|
||||
x_response.append(x_group)
|
||||
for e in get_group_members(group["group_id"]):
|
||||
@ -574,10 +572,10 @@ def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
|
||||
Element(
|
||||
"etud",
|
||||
etudid=str(e["etudid"]),
|
||||
civilite=etud["civilite_str"],
|
||||
sexe=etud["civilite_str"], # compat
|
||||
nom=sco_etud.format_nom(etud["nom"]),
|
||||
prenom=sco_etud.format_prenom(etud["prenom"]),
|
||||
civilite=etud["civilite_str"] or "",
|
||||
sexe=etud["civilite_str"] or "", # compat
|
||||
nom=sco_etud.format_nom(etud["nom"] or ""),
|
||||
prenom=sco_etud.format_prenom(etud["prenom"] or ""),
|
||||
origin=_comp_etud_origin(etud, formsemestre),
|
||||
)
|
||||
)
|
||||
@ -589,7 +587,7 @@ def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
|
||||
x_group = Element(
|
||||
"group",
|
||||
partition_id=str(partition_id),
|
||||
partition_name=partition["partition_name"],
|
||||
partition_name=partition["partition_name"] or "",
|
||||
groups_editable=str(int(partition["groups_editable"])),
|
||||
group_id="_none_",
|
||||
group_name="",
|
||||
@ -601,9 +599,9 @@ def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
|
||||
Element(
|
||||
"etud",
|
||||
etudid=str(etud["etudid"]),
|
||||
sexe=etud["civilite_str"],
|
||||
nom=sco_etud.format_nom(etud["nom"]),
|
||||
prenom=sco_etud.format_prenom(etud["prenom"]),
|
||||
sexe=etud["civilite_str"] or "",
|
||||
nom=sco_etud.format_nom(etud["nom"] or ""),
|
||||
prenom=sco_etud.format_prenom(etud["prenom"] or ""),
|
||||
origin=_comp_etud_origin(etud, formsemestre),
|
||||
)
|
||||
)
|
||||
@ -637,7 +635,7 @@ def _comp_etud_origin(etud: dict, cur_formsemestre: FormSemestre):
|
||||
return "" # parcours normal, ne le signale pas
|
||||
|
||||
|
||||
def set_group(etudid: int, group_id: int) -> bool:
|
||||
def set_group(etudid: int, group_id: int) -> bool: # OBSOLETE !
|
||||
"""Inscrit l'étudiant au groupe.
|
||||
Return True if ok, False si deja inscrit.
|
||||
Warning:
|
||||
@ -664,55 +662,33 @@ def set_group(etudid: int, group_id: int) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def change_etud_group_in_partition(etudid: int, group_id: int, partition: dict = None):
|
||||
"""Inscrit etud au groupe de cette partition,
|
||||
et le desinscrit d'autres groupes de cette partition.
|
||||
def change_etud_group_in_partition(etudid: int, group: GroupDescr) -> bool:
|
||||
"""Inscrit etud au groupe
|
||||
(et le désinscrit d'autres groupes de cette partition)
|
||||
Return True si changement, False s'il était déjà dans ce groupe.
|
||||
"""
|
||||
log("change_etud_group_in_partition: etudid=%s group_id=%s" % (etudid, group_id))
|
||||
# 0- La partition
|
||||
group = get_group(group_id)
|
||||
if partition:
|
||||
# verifie que le groupe est bien dans cette partition:
|
||||
if group["partition_id"] != partition["partition_id"]:
|
||||
raise ValueError(
|
||||
"inconsistent group/partition (group_id=%s, partition_id=%s)"
|
||||
% (group_id, partition["partition_id"])
|
||||
)
|
||||
else:
|
||||
partition = get_partition(group["partition_id"])
|
||||
# 1- Supprime membership dans cette partition
|
||||
ndb.SimpleQuery(
|
||||
"""DELETE FROM group_membership gm
|
||||
WHERE EXISTS
|
||||
(SELECT 1 FROM group_descr gd
|
||||
WHERE gm.etudid = %(etudid)s
|
||||
AND gm.group_id = gd.id
|
||||
AND gd.partition_id = %(partition_id)s)
|
||||
""",
|
||||
{"etudid": etudid, "partition_id": partition["partition_id"]},
|
||||
)
|
||||
# 2- associe au nouveau groupe
|
||||
set_group(etudid, group_id)
|
||||
etud: Identite = Identite.query.get_or_404(etudid)
|
||||
if not group.partition.set_etud_group(etud, group):
|
||||
return # pas de changement
|
||||
|
||||
# 3- log
|
||||
formsemestre_id = partition["formsemestre_id"]
|
||||
cnx = ndb.GetDBConnexion()
|
||||
logdb(
|
||||
cnx,
|
||||
# - log
|
||||
formsemestre: FormSemestre = group.partition.formsemestre
|
||||
log(f"change_etud_group_in_partition: etudid={etudid} group={group}")
|
||||
Scolog.logdb(
|
||||
method="changeGroup",
|
||||
etudid=etudid,
|
||||
msg="formsemestre_id=%s,partition_name=%s, group_name=%s"
|
||||
% (formsemestre_id, partition["partition_name"], group["group_name"]),
|
||||
msg=f"""formsemestre_id={formsemestre.id}, partition_name={
|
||||
group.partition.partition_name or ""}, group_name={group.group_name or ""}""",
|
||||
commit=True,
|
||||
)
|
||||
cnx.commit()
|
||||
|
||||
# 5- Update parcours
|
||||
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||
formsemestre.update_inscriptions_parcours_from_groups()
|
||||
# - Update parcours
|
||||
if group.partition.partition_name == scu.PARTITION_PARCOURS:
|
||||
formsemestre.update_inscriptions_parcours_from_groups()
|
||||
|
||||
# 6- invalidate cache
|
||||
# - invalidate cache
|
||||
sco_cache.invalidate_formsemestre(
|
||||
formsemestre_id=formsemestre_id
|
||||
formsemestre_id=formsemestre.id
|
||||
) # > change etud group
|
||||
|
||||
|
||||
@ -729,7 +705,6 @@ def setGroups(
|
||||
|
||||
Ne peux pas modifier les groupes des partitions non éditables.
|
||||
"""
|
||||
from app.scodoc import sco_formsemestre
|
||||
|
||||
def xml_error(msg, code=404):
|
||||
data = (
|
||||
@ -739,26 +714,27 @@ def setGroups(
|
||||
response.headers["Content-Type"] = scu.XML_MIMETYPE
|
||||
return response
|
||||
|
||||
partition = get_partition(partition_id)
|
||||
if not partition["groups_editable"] and (groupsToCreate or groupsToDelete):
|
||||
partition: Partition = db.session.get(Partition, partition_id)
|
||||
if not partition.groups_editable and (groupsToCreate or groupsToDelete):
|
||||
msg = "setGroups: partition non editable"
|
||||
log(msg)
|
||||
return xml_error(msg, code=403)
|
||||
formsemestre_id = partition["formsemestre_id"]
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
if not sco_permissions_check.can_change_groups(formsemestre_id):
|
||||
|
||||
if not sco_permissions_check.can_change_groups(partition.formsemestre.id):
|
||||
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
||||
log("***setGroups: partition_id=%s" % partition_id)
|
||||
log("groupsLists=%s" % groupsLists)
|
||||
log("groupsToCreate=%s" % groupsToCreate)
|
||||
log("groupsToDelete=%s" % groupsToDelete)
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
if not sem["etat"]:
|
||||
|
||||
if not partition.formsemestre.etat:
|
||||
raise AccessDenied("Modification impossible: semestre verrouillé")
|
||||
|
||||
groupsToDelete = [g for g in groupsToDelete.split(";") if g]
|
||||
|
||||
etud_groups = formsemestre_get_etud_groupnames(formsemestre_id, attr="group_id")
|
||||
etud_groups = formsemestre_get_etud_groupnames(
|
||||
partition.formsemestre.id, attr="group_id"
|
||||
)
|
||||
for line in groupsLists.split("\n"): # for each group_id (one per line)
|
||||
fs = line.split(";")
|
||||
group_id = fs[0].strip()
|
||||
@ -769,26 +745,23 @@ def setGroups(
|
||||
except ValueError:
|
||||
log(f"setGroups: ignoring invalid group_id={group_id}")
|
||||
continue
|
||||
group = get_group(group_id)
|
||||
group: GroupDescr = GroupDescr.query.get_or_404(group_id)
|
||||
# Anciens membres du groupe:
|
||||
old_members = get_group_members(group_id)
|
||||
old_members_set = set([x["etudid"] for x in old_members])
|
||||
old_members_set = {etud.id for etud in group.etuds}
|
||||
# Place dans ce groupe les etudiants indiqués:
|
||||
for etudid_str in fs[1:-1]:
|
||||
etudid = int(etudid_str)
|
||||
if etudid in old_members_set:
|
||||
old_members_set.remove(
|
||||
etudid
|
||||
) # a nouveau dans ce groupe, pas besoin de l'enlever
|
||||
# était dans ce groupe, l'enlever
|
||||
old_members_set.remove(etudid)
|
||||
if (etudid not in etud_groups) or (
|
||||
group_id != etud_groups[etudid].get(partition_id, "")
|
||||
): # pas le meme groupe qu'actuel
|
||||
change_etud_group_in_partition(etudid, group_id, partition)
|
||||
change_etud_group_in_partition(etudid, group)
|
||||
# Retire les anciens membres:
|
||||
cnx = ndb.GetDBConnexion()
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
for etudid in old_members_set:
|
||||
log("removing %s from group %s" % (etudid, group_id))
|
||||
ndb.SimpleQuery(
|
||||
"DELETE FROM group_membership WHERE etudid=%(etudid)s and group_id=%(group_id)s",
|
||||
{"etudid": etudid, "group_id": group_id},
|
||||
@ -798,8 +771,8 @@ def setGroups(
|
||||
cnx,
|
||||
method="removeFromGroup",
|
||||
etudid=etudid,
|
||||
msg="formsemestre_id=%s,partition_name=%s, group_name=%s"
|
||||
% (formsemestre_id, partition["partition_name"], group["group_name"]),
|
||||
msg=f"""formsemestre_id={partition.formsemestre.id},partition_name={
|
||||
partition.partition_name}, group_name={group.group_name}""",
|
||||
)
|
||||
|
||||
# Supprime les groupes indiqués comme supprimés:
|
||||
@ -819,10 +792,10 @@ def setGroups(
|
||||
return xml_error(msg, code=404)
|
||||
# Place dans ce groupe les etudiants indiqués:
|
||||
for etudid in fs[1:-1]:
|
||||
change_etud_group_in_partition(etudid, group.id, partition)
|
||||
change_etud_group_in_partition(etudid, group)
|
||||
|
||||
# Update parcours
|
||||
formsemestre.update_inscriptions_parcours_from_groups()
|
||||
partition.formsemestre.update_inscriptions_parcours_from_groups()
|
||||
|
||||
data = (
|
||||
'<?xml version="1.0" encoding="utf-8"?><response>Groupes enregistrés</response>'
|
||||
@ -835,6 +808,7 @@ def setGroups(
|
||||
def create_group(partition_id, group_name="", default=False) -> GroupDescr:
|
||||
"""Create a new group in this partition.
|
||||
If default, create default partition (with no name)
|
||||
Obsolete: utiliser Partition.create_group
|
||||
"""
|
||||
partition = Partition.query.get_or_404(partition_id)
|
||||
if not sco_permissions_check.can_change_groups(partition.formsemestre_id):
|
||||
@ -856,7 +830,7 @@ def create_group(partition_id, group_name="", default=False) -> GroupDescr:
|
||||
group = GroupDescr(partition=partition, group_name=group_name, numero=new_numero)
|
||||
db.session.add(group)
|
||||
db.session.commit()
|
||||
log("create_group: created group_id={group.id}")
|
||||
log(f"create_group: created group_id={group.id}")
|
||||
#
|
||||
return group
|
||||
|
||||
@ -976,10 +950,20 @@ def edit_partition_form(formsemestre_id=None):
|
||||
}
|
||||
</script>
|
||||
""",
|
||||
r"""<h2>Partitions du semestre</h2>
|
||||
f"""<h2>Partitions du semestre</h2>
|
||||
<p class="help">
|
||||
👉💡 vous pourriez essayer <a href="{
|
||||
url_for("scolar.partition_editor",
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
|
||||
}" class="stdlink">le nouvel éditeur</a>
|
||||
</p>
|
||||
|
||||
<form name="editpart" id="editpart" method="POST" action="partition_create">
|
||||
<div id="epmsg"></div>
|
||||
<table><tr class="eptit"><th></th><th></th><th></th><th>Partition</th><th>Groupes</th><th></th><th></th><th></th></tr>
|
||||
<table>
|
||||
<tr class="eptit">
|
||||
<th></th><th></th><th></th><th>Partition</th><th>Groupes</th><th></th><th></th><th></th>
|
||||
</tr>
|
||||
""",
|
||||
]
|
||||
i = 0
|
||||
@ -1400,14 +1384,16 @@ def groups_auto_repartition(partition_id=None):
|
||||
"""Reparti les etudiants dans des groupes dans une partition, en respectant le niveau
|
||||
et la mixité.
|
||||
"""
|
||||
partition = get_partition(partition_id)
|
||||
if not partition["groups_editable"]:
|
||||
partition: Partition = Partition.query.get_or_404(partition_id)
|
||||
if not partition.groups_editable:
|
||||
raise AccessDenied("Partition non éditable")
|
||||
formsemestre_id = partition["formsemestre_id"]
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
# renvoie sur page édition groupes
|
||||
formsemestre_id = partition.formsemestre_id
|
||||
formsemestre = partition.formsemestre
|
||||
# renvoie sur page édition partitions et groupes
|
||||
dest_url = url_for(
|
||||
"scolar.affect_groups", scodoc_dept=g.scodoc_dept, partition_id=partition_id
|
||||
"scolar.partition_editor",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
)
|
||||
if not sco_permissions_check.can_change_groups(formsemestre_id):
|
||||
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
||||
@ -1427,12 +1413,14 @@ def groups_auto_repartition(partition_id=None):
|
||||
|
||||
H = [
|
||||
html_sco_header.sco_header(page_title="Répartition des groupes"),
|
||||
"<h2>Répartition des groupes de %s</h2>" % partition["partition_name"],
|
||||
f"<p>Semestre {formsemestre.titre_annee()}</p>",
|
||||
"""<p class="help">Les groupes existants seront <b>effacés</b> et remplacés par
|
||||
f"""<h2>Répartition des groupes de {partition.partition_name}</h2>
|
||||
<p>Semestre {formsemestre.titre_annee()}</p>
|
||||
<p class="help">Les groupes existants seront <b>effacés</b> et remplacés par
|
||||
ceux créés ici. La répartition aléatoire tente d'uniformiser le niveau
|
||||
des groupes (en utilisant la dernière moyenne générale disponible pour
|
||||
chaque étudiant) et de maximiser la mixité de chaque groupe.</p>""",
|
||||
chaque étudiant) et de maximiser la mixité de chaque groupe.
|
||||
</p>
|
||||
""",
|
||||
]
|
||||
|
||||
tf = TrivialFormulator(
|
||||
@ -1450,25 +1438,23 @@ def groups_auto_repartition(partition_id=None):
|
||||
return flask.redirect(dest_url)
|
||||
else:
|
||||
# form submission
|
||||
log(
|
||||
"groups_auto_repartition( partition_id=%s partition_name=%s"
|
||||
% (partition_id, partition["partition_name"])
|
||||
)
|
||||
groupNames = tf[2]["groupNames"]
|
||||
group_names = sorted(set([x.strip() for x in groupNames.split(",")]))
|
||||
log(f"groups_auto_repartition({partition})")
|
||||
group_names = tf[2]["groupNames"]
|
||||
group_names = sorted({x.strip() for x in group_names.split(",")})
|
||||
# Détruit les groupes existant de cette partition
|
||||
for old_group in get_partition_groups(partition):
|
||||
group_delete(old_group["group_id"])
|
||||
for group in partition.groups:
|
||||
db.session.delete(group)
|
||||
db.session.commit()
|
||||
# Crée les nouveaux groupes
|
||||
group_ids = []
|
||||
groups = []
|
||||
for group_name in group_names:
|
||||
if group_name.strip():
|
||||
group_ids.append(create_group(partition_id, group_name).id)
|
||||
groups.append(partition.create_group(group_name))
|
||||
#
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
identdict = nt.identdict
|
||||
# build: { civilite : liste etudids trie par niveau croissant }
|
||||
civilites = set([x["civilite"] for x in identdict.values()])
|
||||
civilites = {x["civilite"] for x in identdict.values()}
|
||||
listes = {}
|
||||
for civilite in civilites:
|
||||
listes[civilite] = [
|
||||
@ -1481,16 +1467,19 @@ def groups_auto_repartition(partition_id=None):
|
||||
# affect aux groupes:
|
||||
n = len(identdict)
|
||||
igroup = 0
|
||||
nbgroups = len(group_ids)
|
||||
nbgroups = len(groups)
|
||||
while n > 0:
|
||||
log(f"n={n}")
|
||||
for civilite in civilites:
|
||||
log(f"civilite={civilite}")
|
||||
if len(listes[civilite]):
|
||||
n -= 1
|
||||
etudid = listes[civilite].pop()[1]
|
||||
group_id = group_ids[igroup]
|
||||
group = groups[igroup]
|
||||
igroup = (igroup + 1) % nbgroups
|
||||
change_etud_group_in_partition(etudid, group_id, partition)
|
||||
log("%s in group %s" % (etudid, group_id))
|
||||
log(f"in {etudid} in group {group.id}")
|
||||
change_etud_group_in_partition(etudid, group)
|
||||
log(f"{etudid} in group {group.id}")
|
||||
return flask.redirect(dest_url)
|
||||
|
||||
|
||||
@ -1498,15 +1487,13 @@ def _get_prev_moy(etudid, formsemestre_id):
|
||||
"""Donne la derniere moyenne generale calculee pour cette étudiant,
|
||||
ou 0 si on n'en trouve pas (nouvel inscrit,...).
|
||||
"""
|
||||
from app.scodoc import sco_cursus_dut
|
||||
|
||||
info = sco_etud.get_etud_info(etudid=etudid, filled=True)
|
||||
if not info:
|
||||
raise ScoValueError("etudiant invalide: etudid=%s" % etudid)
|
||||
etud = info[0]
|
||||
Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
|
||||
if Se.prev:
|
||||
prev_sem = FormSemestre.query.get(Se.prev["formsemestre_id"])
|
||||
prev_sem = db.session.get(FormSemestre, Se.prev["formsemestre_id"])
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(prev_sem)
|
||||
return nt.get_etud_moy_gen(etudid)
|
||||
else:
|
||||
@ -1520,10 +1507,11 @@ def create_etapes_partition(formsemestre_id, partition_name="apo_etapes"):
|
||||
Si la partition existe déjà, ses groupes sont mis à jour (les groupes devenant
|
||||
vides ne sont pas supprimés).
|
||||
"""
|
||||
# A RE-ECRIRE pour utiliser les modèles.
|
||||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
|
||||
partition_name = str(partition_name)
|
||||
log("create_etapes_partition(%s)" % formsemestre_id)
|
||||
log(f"create_etapes_partition({formsemestre_id})")
|
||||
ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
|
||||
args={"formsemestre_id": formsemestre_id}
|
||||
)
|
||||
@ -1542,20 +1530,17 @@ def create_etapes_partition(formsemestre_id, partition_name="apo_etapes"):
|
||||
pid = partition_create(
|
||||
formsemestre_id, partition_name=partition_name, redirect=False
|
||||
)
|
||||
partition = get_partition(pid)
|
||||
groups = get_partition_groups(partition)
|
||||
groups_by_names = {g["group_name"]: g for g in groups}
|
||||
partition: Partition = db.session.get(Partition, pid)
|
||||
groups = partition.groups
|
||||
groups_by_names = {g.group_name: g for g in groups}
|
||||
for etape in etapes:
|
||||
if not (etape in groups_by_names):
|
||||
if etape not in groups_by_names:
|
||||
new_group = create_group(pid, etape)
|
||||
g = get_group(new_group.id) # XXX transition: recupere old style dict
|
||||
groups_by_names[etape] = g
|
||||
groups_by_names[etape] = new_group
|
||||
# Place les etudiants dans les groupes
|
||||
for i in ins:
|
||||
if i["etape"]:
|
||||
change_etud_group_in_partition(
|
||||
i["etudid"], groups_by_names[i["etape"]]["group_id"], partition
|
||||
)
|
||||
change_etud_group_in_partition(i["etudid"], groups_by_names[i["etape"]])
|
||||
|
||||
|
||||
def do_evaluation_listeetuds_groups(
|
||||
|
@ -36,16 +36,12 @@ import time
|
||||
|
||||
from flask import g, url_for
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app import log
|
||||
from app import db, log
|
||||
from app.models import ScolarNews, GroupDescr
|
||||
from app.models.etudiants import input_civilite
|
||||
from app.scodoc.sco_excel import COLORS
|
||||
from app.scodoc.sco_formsemestre_inscriptions import (
|
||||
do_formsemestre_inscription_with_modules,
|
||||
)
|
||||
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc.sco_excel import COLORS
|
||||
from app.scodoc.sco_exceptions import (
|
||||
AccessDenied,
|
||||
ScoFormatError,
|
||||
@ -55,7 +51,6 @@ from app.scodoc.sco_exceptions import (
|
||||
ScoLockedFormError,
|
||||
ScoGenError,
|
||||
)
|
||||
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_etud
|
||||
@ -63,6 +58,11 @@ from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_excel
|
||||
from app.scodoc import sco_groups_view
|
||||
from app.scodoc import sco_preferences
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc.sco_formsemestre_inscriptions import (
|
||||
do_formsemestre_inscription_with_modules,
|
||||
)
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
# format description (in tools/)
|
||||
FORMAT_FILE = "format_import_etudiants.txt"
|
||||
@ -480,6 +480,7 @@ def scolars_import_excel_file(
|
||||
text="Inscription de %d étudiants" # peuvent avoir ete inscrits a des semestres differents
|
||||
% len(created_etudids),
|
||||
obj=formsemestre_id,
|
||||
max_frequency=0,
|
||||
)
|
||||
|
||||
log("scolars_import_excel_file: completing transaction")
|
||||
@ -638,10 +639,10 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None
|
||||
fields = adm_get_fields(titles, formsemestre_id)
|
||||
idx_nom = None
|
||||
idx_prenom = None
|
||||
for idx in fields:
|
||||
if fields[idx][0] == "nom":
|
||||
for idx, field in fields.items():
|
||||
if field[0] == "nom":
|
||||
idx_nom = idx
|
||||
if fields[idx][0] == "prenom":
|
||||
if field[0] == "prenom":
|
||||
idx_prenom = idx
|
||||
if (idx_nom is None) or (idx_prenom is None):
|
||||
log("fields indices=" + ", ".join([str(x) for x in fields]))
|
||||
@ -663,21 +664,20 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None
|
||||
# Retrouve l'étudiant parmi ceux du semestre par (nom, prenom)
|
||||
nom = adm_normalize_string(line[idx_nom])
|
||||
prenom = adm_normalize_string(line[idx_prenom])
|
||||
if not (nom, prenom) in etuds_by_nomprenom:
|
||||
log(
|
||||
"unable to find %s %s among members" % (line[idx_nom], line[idx_prenom])
|
||||
)
|
||||
if (nom, prenom) not in etuds_by_nomprenom:
|
||||
msg = f"""Étudiant <b>{line[idx_nom]} {line[idx_prenom]} inexistant</b>"""
|
||||
diag.append(msg)
|
||||
else:
|
||||
etud = etuds_by_nomprenom[(nom, prenom)]
|
||||
cur_adm = sco_etud.admission_list(cnx, args={"etudid": etud["etudid"]})[0]
|
||||
# peuple les champs presents dans le tableau
|
||||
args = {}
|
||||
for idx in fields:
|
||||
field_name, convertor = fields[idx]
|
||||
for idx, field in fields.items():
|
||||
field_name, convertor = field
|
||||
if field_name in modifiable_fields:
|
||||
try:
|
||||
val = convertor(line[idx])
|
||||
except ValueError:
|
||||
except ValueError as exc:
|
||||
raise ScoFormatError(
|
||||
'scolars_import_admission: valeur invalide, ligne %d colonne %s: "%s"'
|
||||
% (nline, field_name, line[idx]),
|
||||
@ -686,7 +686,7 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
),
|
||||
)
|
||||
) from exc
|
||||
if val is not None: # note: ne peut jamais supprimer une valeur
|
||||
args[field_name] = val
|
||||
if args:
|
||||
@ -719,10 +719,10 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None
|
||||
)
|
||||
|
||||
for group_id in group_ids:
|
||||
group = GroupDescr.query.get(group_id)
|
||||
group = db.session.get(GroupDescr, group_id)
|
||||
if group.partition.groups_editable:
|
||||
sco_groups.change_etud_group_in_partition(
|
||||
args["etudid"], group_id
|
||||
args["etudid"], group
|
||||
)
|
||||
else:
|
||||
log("scolars_import_admission: partition non editable")
|
||||
|
@ -35,14 +35,13 @@ from flask import url_for, g, request
|
||||
|
||||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import log
|
||||
from app.models import Formation, FormSemestre
|
||||
from app import db, log
|
||||
from app.models import Formation, FormSemestre, GroupDescr
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_formations
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
from app.scodoc import sco_groups
|
||||
@ -177,7 +176,8 @@ def do_inscrit(sem, etudids, inscrit_groupes=False):
|
||||
(la liste doit avoir été vérifiée au préalable)
|
||||
En option: inscrit aux mêmes groupes que dans le semestre origine
|
||||
"""
|
||||
formsemestre: FormSemestre = FormSemestre.query.get(sem["formsemestre_id"])
|
||||
# TODO à ré-écrire pour utiliser le smodèle, notamment GroupDescr
|
||||
formsemestre: FormSemestre = db.session.get(FormSemestre, sem["formsemestre_id"])
|
||||
formsemestre.setup_parcours_groups()
|
||||
log(f"do_inscrit (inscrit_groupes={inscrit_groupes}): {etudids}")
|
||||
for etudid in etudids:
|
||||
@ -220,11 +220,10 @@ def do_inscrit(sem, etudids, inscrit_groupes=False):
|
||||
|
||||
# Inscrit aux groupes
|
||||
for partition_group in partition_groups:
|
||||
sco_groups.change_etud_group_in_partition(
|
||||
etudid,
|
||||
partition_group["group_id"],
|
||||
partition_group,
|
||||
group: GroupDescr = db.session.get(
|
||||
GroupDescr, partition_group["group_id"]
|
||||
)
|
||||
sco_groups.change_etud_group_in_partition(etudid, group)
|
||||
|
||||
|
||||
def do_desinscrit(sem, etudids):
|
||||
@ -416,10 +415,10 @@ def formsemestre_inscr_passage(
|
||||
): # il y a au moins une vraie partition
|
||||
H.append(
|
||||
f"""<li><a class="stdlink" href="{
|
||||
url_for("scolar.affect_groups",
|
||||
scodoc_dept=g.scodoc_dept, partition_id=partition["partition_id"])
|
||||
}">Répartir les groupes de {partition["partition_name"]}</a></li>
|
||||
"""
|
||||
url_for("scolar.partition_editor", scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id)
|
||||
}">Répartir les groupes de {partition["partition_name"]}</a></li>
|
||||
"""
|
||||
)
|
||||
|
||||
#
|
||||
@ -436,7 +435,7 @@ def _build_page(
|
||||
inscrit_groupes=False,
|
||||
ignore_jury=False,
|
||||
):
|
||||
formsemestre: FormSemestre = FormSemestre.query.get(sem["formsemestre_id"])
|
||||
formsemestre: FormSemestre = db.session.get(FormSemestre, sem["formsemestre_id"])
|
||||
inscrit_groupes = int(inscrit_groupes)
|
||||
ignore_jury = int(ignore_jury)
|
||||
if inscrit_groupes:
|
||||
|
@ -33,7 +33,7 @@ import numpy as np
|
||||
import flask
|
||||
from flask import url_for, g, request
|
||||
|
||||
from app import log
|
||||
from app import db, log
|
||||
from app import models
|
||||
from app.comp import res_sem
|
||||
from app.comp import moy_mod
|
||||
@ -79,7 +79,7 @@ def do_evaluation_listenotes(
|
||||
return "<p>Aucune évaluation !</p>", "ScoDoc"
|
||||
|
||||
E = evals[0] # il y a au moins une evaluation
|
||||
modimpl = ModuleImpl.query.get(E["moduleimpl_id"])
|
||||
modimpl = db.session.get(ModuleImpl, E["moduleimpl_id"])
|
||||
# description de l'evaluation
|
||||
if mode == "eval":
|
||||
H = [sco_evaluations.evaluation_describe(evaluation_id=evaluation_id)]
|
||||
@ -624,7 +624,7 @@ def _make_table_notes(
|
||||
]
|
||||
commentkeys = list(key_mgr.items()) # [ (comment, key), ... ]
|
||||
commentkeys.sort(key=lambda x: int(x[1]))
|
||||
for (comment, key) in commentkeys:
|
||||
for comment, key in commentkeys:
|
||||
C.append(
|
||||
'<span class="colcomment">(%s)</span> <em>%s</em><br>' % (key, comment)
|
||||
)
|
||||
@ -673,7 +673,7 @@ def _add_eval_columns(
|
||||
sum_notes = 0
|
||||
notes = [] # liste des notes numeriques, pour calcul histogramme uniquement
|
||||
evaluation_id = e["evaluation_id"]
|
||||
e_o = Evaluation.query.get(evaluation_id) # XXX en attendant ré-écriture
|
||||
e_o = db.session.get(Evaluation, evaluation_id) # XXX en attendant ré-écriture
|
||||
inscrits = e_o.moduleimpl.formsemestre.etudids_actifs # set d'etudids
|
||||
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
|
||||
|
||||
|
@ -31,15 +31,16 @@
|
||||
from flask_login import current_user
|
||||
import psycopg2
|
||||
|
||||
from app import db
|
||||
|
||||
from app.models import Formation
|
||||
from app.scodoc import scolog
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_cache
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_exceptions import ScoValueError, AccessDenied
|
||||
from app import log
|
||||
from app import models
|
||||
from app.scodoc import scolog
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_cache
|
||||
|
||||
# --- Gestion des "Implémentations de Modules"
|
||||
# Un "moduleimpl" correspond a la mise en oeuvre d'un module
|
||||
@ -170,7 +171,7 @@ def moduleimpl_withmodule_list(
|
||||
mi["matiere"] = matieres[matiere_id]
|
||||
|
||||
mod = modimpls[0]["module"]
|
||||
formation = models.Formation.query.get(mod["formation_id"])
|
||||
formation = db.session.get(Formation, mod["formation_id"])
|
||||
|
||||
if formation.is_apc():
|
||||
# tri par numero_module
|
||||
|
@ -28,12 +28,13 @@
|
||||
"""Opérations d'inscriptions aux modules (interface pour gérer options ou parcours)
|
||||
"""
|
||||
import collections
|
||||
from operator import itemgetter
|
||||
from operator import attrgetter
|
||||
|
||||
import flask
|
||||
from flask import url_for, g, request
|
||||
from flask_login import current_user
|
||||
|
||||
from app import db, log
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import (
|
||||
@ -43,9 +44,6 @@ from app.models import (
|
||||
ScolarFormSemestreValidation,
|
||||
UniteEns,
|
||||
)
|
||||
|
||||
from app import log
|
||||
from app.tables import list_etuds
|
||||
from app.scodoc.scolog import logdb
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import htmlutils
|
||||
@ -62,6 +60,7 @@ import app.scodoc.notesdb as ndb
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.tables import list_etuds
|
||||
|
||||
|
||||
def moduleimpl_inscriptions_edit(moduleimpl_id, etuds=[], submitted=False):
|
||||
@ -520,7 +519,7 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->
|
||||
else set()
|
||||
)
|
||||
ues = sorted(
|
||||
(UniteEns.query.get(ue_id) for ue_id in ue_ids),
|
||||
(db.session.get(UniteEns, ue_id) for ue_id in ue_ids),
|
||||
key=lambda u: (u.numero or 0, u.acronyme),
|
||||
)
|
||||
H.append(
|
||||
@ -553,8 +552,11 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->
|
||||
>{etud.nomprenom}</a></td>"""
|
||||
)
|
||||
# Parcours:
|
||||
group = partition_parcours.get_etud_group(etud.id)
|
||||
parcours_name = group.group_name if group else ""
|
||||
if partition_parcours:
|
||||
group = partition_parcours.get_etud_group(etud.id)
|
||||
parcours_name = group.group_name if group else ""
|
||||
else:
|
||||
parcours_name = ""
|
||||
H.append(f"""<td class="parcours">{parcours_name}</td>""")
|
||||
# UEs:
|
||||
for ue in ues:
|
||||
@ -578,7 +580,7 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->
|
||||
.all()
|
||||
)
|
||||
validations_ue.sort(
|
||||
key=lambda v: codes_cursus.BUT_CODES_ORDERED.get(v.code, 0)
|
||||
key=lambda v: codes_cursus.BUT_CODES_ORDER.get(v.code, 0)
|
||||
)
|
||||
validation = validations_ue[-1] if validations_ue else None
|
||||
expl_validation = (
|
||||
@ -668,7 +670,7 @@ def descr_inscrs_module(moduleimpl_id, set_all, partitions):
|
||||
gr.append((partition["partition_name"], grp))
|
||||
#
|
||||
d = []
|
||||
for (partition_name, grp) in gr:
|
||||
for partition_name, grp in gr:
|
||||
if grp:
|
||||
d.append("groupes de %s: %s" % (partition_name, ", ".join(grp)))
|
||||
r = []
|
||||
@ -680,25 +682,25 @@ def descr_inscrs_module(moduleimpl_id, set_all, partitions):
|
||||
return False, len(ins), " et ".join(r)
|
||||
|
||||
|
||||
def _fmt_etud_set(ins, max_list_size=7):
|
||||
def _fmt_etud_set(etudids, max_list_size=7) -> str:
|
||||
# max_list_size est le nombre max de noms d'etudiants listés
|
||||
# au delà, on indique juste le nombre, sans les noms.
|
||||
if len(ins) > max_list_size:
|
||||
return "%d étudiants" % len(ins)
|
||||
if len(etudids) > max_list_size:
|
||||
return f"{len(etudids)} étudiants"
|
||||
etuds = []
|
||||
for etudid in ins:
|
||||
etuds.append(sco_etud.get_etud_info(etudid=etudid, filled=True)[0])
|
||||
etuds.sort(key=itemgetter("nom"))
|
||||
for etudid in etudids:
|
||||
etud = db.session.get(Identite, etudid)
|
||||
if etud:
|
||||
etuds.append(etud)
|
||||
|
||||
return ", ".join(
|
||||
[
|
||||
'<a class="discretelink" href="%s">%s</a>'
|
||||
% (
|
||||
f"""<a class="discretelink" href="{
|
||||
url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
|
||||
),
|
||||
etud["nomprenom"],
|
||||
)
|
||||
for etud in etuds
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id
|
||||
)
|
||||
}">{etud.nomprenom}</a>"""
|
||||
for etud in sorted(etuds, key=attrgetter("sort_key"))
|
||||
]
|
||||
)
|
||||
|
||||
|
@ -57,6 +57,7 @@ from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_permissions_check
|
||||
from app.tables import list_etuds
|
||||
|
||||
|
||||
# menu evaluation dans moduleimpl
|
||||
def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str:
|
||||
"Menu avec actions sur une evaluation"
|
||||
@ -226,7 +227,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
||||
)
|
||||
arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags()
|
||||
#
|
||||
module_resp = User.query.get(modimpl.responsable_id)
|
||||
module_resp = db.session.get(User, modimpl.responsable_id)
|
||||
mod_type_name = scu.MODULE_TYPE_NAMES[module.module_type]
|
||||
H = [
|
||||
html_sco_header.sco_header(
|
||||
@ -528,7 +529,7 @@ def _ligne_evaluation(
|
||||
) -> str:
|
||||
"""Ligne <tr> décrivant une évaluation dans le tableau de bord moduleimpl."""
|
||||
H = []
|
||||
# evaluation: Evaluation = Evaluation.query.get(eval_dict["evaluation_id"])
|
||||
# evaluation: Evaluation = db.session.get(Evaluation, eval_dict["evaluation_id"])
|
||||
etat = sco_evaluations.do_evaluation_etat(
|
||||
evaluation.id,
|
||||
partition_id=partition_id,
|
||||
@ -732,7 +733,7 @@ def _ligne_evaluation(
|
||||
)
|
||||
if etat["moy"]:
|
||||
H.append(
|
||||
f"""<b>{etat["moy"]} / {evaluation.note_max:g}</b>
|
||||
f"""<b>{etat["moy"]} / 20</b>
|
||||
(<a class="stdlink" href="{
|
||||
url_for('notes.evaluation_listenotes',
|
||||
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id)
|
||||
@ -837,7 +838,7 @@ def _evaluation_poids_html(evaluation: Evaluation, max_poids: float = 0.0) -> st
|
||||
"></div>
|
||||
</div>"""
|
||||
for ue, poids in (
|
||||
(UniteEns.query.get(ue_id), poids)
|
||||
(db.session.get(UniteEns, ue_id), poids)
|
||||
for ue_id, poids in ue_poids.items()
|
||||
)
|
||||
]
|
||||
|
@ -33,10 +33,8 @@
|
||||
from flask import abort, url_for, g, render_template, request
|
||||
from flask_login import current_user
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app import log
|
||||
from app.but import cursus_but, jury_but_view
|
||||
from app import db, log
|
||||
from app.but import cursus_but
|
||||
from app.models.etudiants import Identite, make_etud_args
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.scodoc import html_sco_header
|
||||
@ -57,13 +55,17 @@ from app.scodoc.sco_bulletins import etud_descr_situation_semestre
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_formsemestre_validation import formsemestre_recap_parcours_table
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
|
||||
|
||||
def _menu_scolarite(authuser, sem: dict, etudid: int):
|
||||
def _menu_scolarite(
|
||||
authuser, formsemestre: FormSemestre, etudid: int, etat_inscription: str
|
||||
):
|
||||
"""HTML pour menu "scolarite" pour un etudiant dans un semestre.
|
||||
Le contenu du menu depend des droits de l'utilisateur et de l'état de l'étudiant.
|
||||
"""
|
||||
locked = not sem["etat"]
|
||||
locked = not formsemestre.etat
|
||||
if locked:
|
||||
lockicon = scu.icontag("lock32_img", title="verrouillé", border="0")
|
||||
return lockicon # no menu
|
||||
@ -71,10 +73,10 @@ def _menu_scolarite(authuser, sem: dict, etudid: int):
|
||||
Permission.ScoEtudInscrit
|
||||
) and not authuser.has_permission(Permission.ScoEtudChangeGroups):
|
||||
return "" # no menu
|
||||
ins = sem["ins"]
|
||||
args = {"etudid": etudid, "formsemestre_id": ins["formsemestre_id"]}
|
||||
|
||||
if ins["etat"] != "D":
|
||||
args = {"etudid": etudid, "formsemestre_id": formsemestre.id}
|
||||
|
||||
if etat_inscription != scu.DEMISSION:
|
||||
dem_title = "Démission"
|
||||
dem_url = "scolar.form_dem"
|
||||
else:
|
||||
@ -82,14 +84,14 @@ def _menu_scolarite(authuser, sem: dict, etudid: int):
|
||||
dem_url = "scolar.do_cancel_dem"
|
||||
|
||||
# Note: seul un etudiant inscrit (I) peut devenir défaillant.
|
||||
if ins["etat"] != codes_cursus.DEF:
|
||||
if etat_inscription != codes_cursus.DEF:
|
||||
def_title = "Déclarer défaillance"
|
||||
def_url = "scolar.form_def"
|
||||
elif ins["etat"] == codes_cursus.DEF:
|
||||
elif etat_inscription == codes_cursus.DEF:
|
||||
def_title = "Annuler la défaillance"
|
||||
def_url = "scolar.do_cancel_def"
|
||||
def_enabled = (
|
||||
(ins["etat"] != "D")
|
||||
(etat_inscription != scu.DEMISSION)
|
||||
and authuser.has_permission(Permission.ScoEtudInscrit)
|
||||
and not locked
|
||||
)
|
||||
@ -128,6 +130,12 @@ def _menu_scolarite(authuser, sem: dict, etudid: int):
|
||||
"enabled": authuser.has_permission(Permission.ScoEtudInscrit)
|
||||
and not locked,
|
||||
},
|
||||
{
|
||||
"title": "Gérer les validations d'UEs antérieures",
|
||||
"endpoint": "notes.formsemestre_validate_previous_ue",
|
||||
"args": args,
|
||||
"enabled": formsemestre.can_edit_jury(),
|
||||
},
|
||||
{
|
||||
"title": "Inscrire à un autre semestre",
|
||||
"endpoint": "notes.formsemestre_inscription_with_modules_form",
|
||||
@ -250,8 +258,10 @@ def ficheEtud(etudid=None):
|
||||
info["last_formsemestre_id"] = ""
|
||||
sem_info = {}
|
||||
for sem in info["sems"]:
|
||||
formsemestre: FormSemestre = db.session.get(
|
||||
FormSemestre, sem["formsemestre_id"]
|
||||
)
|
||||
if sem["ins"]["etat"] != scu.INSCRIT:
|
||||
formsemestre: FormSemestre = FormSemestre.query.get(sem["formsemestre_id"])
|
||||
descr, _ = etud_descr_situation_semestre(
|
||||
etudid,
|
||||
formsemestre,
|
||||
@ -283,7 +293,7 @@ def ficheEtud(etudid=None):
|
||||
)
|
||||
grlink = ", ".join(grlinks)
|
||||
# infos ajoutées au semestre dans le parcours (groupe, menu)
|
||||
menu = _menu_scolarite(authuser, sem, etudid)
|
||||
menu = _menu_scolarite(authuser, formsemestre, etudid, sem["ins"]["etat"])
|
||||
if menu:
|
||||
sem_info[sem["formsemestre_id"]] = (
|
||||
"<table><tr><td>" + grlink + "</td><td>" + menu + "</td></tr></table>"
|
||||
@ -303,16 +313,39 @@ def ficheEtud(etudid=None):
|
||||
)
|
||||
info[
|
||||
"link_bul_pdf"
|
||||
] = f"""<span class="link_bul_pdf"><a class="stdlink" href="{
|
||||
url_for("notes.etud_bulletins_pdf", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">tous les bulletins</a></span>"""
|
||||
] = f"""
|
||||
<span class="link_bul_pdf">
|
||||
<a class="stdlink" href="{
|
||||
url_for("notes.etud_bulletins_pdf", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">Tous les bulletins</a>
|
||||
</span>
|
||||
"""
|
||||
last_formsemestre: FormSemestre = db.session.get(
|
||||
FormSemestre, info["sems"][0]["formsemestre_id"]
|
||||
)
|
||||
if last_formsemestre.formation.is_apc() and last_formsemestre.semestre_id > 2:
|
||||
info[
|
||||
"link_bul_pdf"
|
||||
] += f"""
|
||||
<span class="link_bul_pdf">
|
||||
<a class="stdlink" href="{
|
||||
url_for("notes.validation_rcues", scodoc_dept=g.scodoc_dept, etudid=etudid, formsemestre_id=last_formsemestre.id)
|
||||
}">Visualiser les compétences BUT</a>
|
||||
</span>
|
||||
"""
|
||||
if authuser.has_permission(Permission.ScoEtudInscrit):
|
||||
info[
|
||||
"link_inscrire_ailleurs"
|
||||
] = f"""<span class="link_bul_pdf"><a class="stdlink" href="{
|
||||
url_for("notes.formsemestre_inscription_with_modules_form",
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">inscrire à un autre semestre</a></span>"""
|
||||
}">Inscrire à un autre semestre</a></span>
|
||||
<span class="link_bul_pdf"><a class="stdlink" href="{
|
||||
url_for("notes.jury_delete_manual",
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">Éditer toutes décisions de jury</a></span>
|
||||
"""
|
||||
|
||||
else:
|
||||
info["link_inscrire_ailleurs"] = ""
|
||||
else:
|
||||
@ -337,17 +370,18 @@ def ficheEtud(etudid=None):
|
||||
if not sco_permissions_check.can_suppress_annotation(a["id"]):
|
||||
a["dellink"] = ""
|
||||
else:
|
||||
a[
|
||||
"dellink"
|
||||
] = '<td class="annodel"><a href="doSuppressAnnotation?etudid=%s&annotation_id=%s">%s</a></td>' % (
|
||||
etudid,
|
||||
a["id"],
|
||||
scu.icontag(
|
||||
"delete_img",
|
||||
border="0",
|
||||
alt="suppress",
|
||||
title="Supprimer cette annotation",
|
||||
),
|
||||
a["dellink"] = (
|
||||
'<td class="annodel"><a href="doSuppressAnnotation?etudid=%s&annotation_id=%s">%s</a></td>'
|
||||
% (
|
||||
etudid,
|
||||
a["id"],
|
||||
scu.icontag(
|
||||
"delete_img",
|
||||
border="0",
|
||||
alt="suppress",
|
||||
title="Supprimer cette annotation",
|
||||
),
|
||||
)
|
||||
)
|
||||
author = sco_users.user_info(a["author"])
|
||||
alist.append(
|
||||
@ -446,7 +480,7 @@ def ficheEtud(etudid=None):
|
||||
info[
|
||||
"inscriptions_mkup"
|
||||
] = f"""<div class="ficheinscriptions" id="ficheinscriptions">
|
||||
<div class="fichetitre">Parcours</div>{info["liste_inscriptions"]}
|
||||
<div class="fichetitre">Cursus</div>{info["liste_inscriptions"]}
|
||||
{info["link_bul_pdf"]} {info["link_inscrire_ailleurs"]}
|
||||
</div>"""
|
||||
|
||||
@ -474,11 +508,26 @@ def ficheEtud(etudid=None):
|
||||
last_sem = FormSemestre.query.get_or_404(info["sems"][0]["formsemestre_id"])
|
||||
if last_sem.formation.is_apc():
|
||||
but_cursus = cursus_but.EtudCursusBUT(etud, last_sem.formation)
|
||||
info["but_cursus_mkup"] = render_template(
|
||||
"but/cursus_etud.j2",
|
||||
cursus=but_cursus,
|
||||
scu=scu,
|
||||
)
|
||||
info[
|
||||
"but_cursus_mkup"
|
||||
] = f"""
|
||||
<div class="section_but">
|
||||
{render_template(
|
||||
"but/cursus_etud.j2",
|
||||
cursus=but_cursus,
|
||||
scu=scu,
|
||||
)}
|
||||
<div class="link_validation_rcues">
|
||||
<a href="{url_for("notes.validation_rcues",
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid,
|
||||
formsemestre_id=last_formsemestre.id)}"
|
||||
title="Visualiser les compétences BUT"
|
||||
>
|
||||
<img src="/ScoDoc/static/icons/parcours-but.png" alt="validation_rcues" height="100px"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
tmpl = """<div class="menus_etud">%(menus_etud)s</div>
|
||||
<div class="ficheEtud" id="ficheEtud"><table>
|
||||
|
@ -84,6 +84,8 @@ def SU(s: str) -> str:
|
||||
s = html.unescape(s)
|
||||
# Remplace les <br> par des <br/>
|
||||
s = re.sub(r"<br\s*>", "<br/>", s)
|
||||
# And substitute unicode characters not supported by ReportLab
|
||||
s = s.replace("‐", "-")
|
||||
return s
|
||||
|
||||
|
||||
|
@ -6,6 +6,7 @@
|
||||
from flask import g
|
||||
from flask_login import current_user
|
||||
|
||||
from app import db
|
||||
from app.auth.models import User
|
||||
from app.models import FormSemestre
|
||||
import app.scodoc.notesdb as ndb
|
||||
@ -131,7 +132,10 @@ def check_access_diretud(formsemestre_id, required_permission=Permission.ScoImpl
|
||||
"<h2>Opération non autorisée pour %s</h2>" % current_user,
|
||||
"<p>Responsable de ce semestre : <b>%s</b></p>"
|
||||
% ", ".join(
|
||||
[User.query.get(i).get_prenomnom() for i in sem["responsables"]]
|
||||
[
|
||||
db.session.get(User, i).get_prenomnom()
|
||||
for i in sem["responsables"]
|
||||
]
|
||||
),
|
||||
footer,
|
||||
]
|
||||
@ -142,7 +146,9 @@ def check_access_diretud(formsemestre_id, required_permission=Permission.ScoImpl
|
||||
|
||||
|
||||
def can_change_groups(formsemestre_id: int) -> bool:
|
||||
"Vrai si l'utilisateur peut changer les groupes dans ce semestre"
|
||||
"""Vrai si l'utilisateur peut changer les groupes dans ce semestre
|
||||
Obsolete: utiliser FormSemestre.can_change_groups
|
||||
"""
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
if not formsemestre.etat:
|
||||
return False # semestre verrouillé
|
||||
|
4
app/scodoc/sco_photos.py
Executable file → Normal file
4
app/scodoc/sco_photos.py
Executable file → Normal file
@ -148,11 +148,11 @@ def get_photo_image(etudid=None, size="small"):
|
||||
filename = photo_pathname(etud.photo_filename, size=size)
|
||||
if not filename:
|
||||
filename = UNKNOWN_IMAGE_PATH
|
||||
r = build_image_response(filename)
|
||||
r = _http_jpeg_file(filename)
|
||||
return r
|
||||
|
||||
|
||||
def build_image_response(filename):
|
||||
def _http_jpeg_file(filename):
|
||||
"""returns an image as a Flask response"""
|
||||
st = os.stat(filename)
|
||||
last_modified = st.st_mtime # float timestamp
|
||||
|
@ -489,6 +489,7 @@ def _normalize_apo_fields(infolist):
|
||||
recode les champs: paiementinscription (-> booleen), datefinalisationinscription (date)
|
||||
ajoute le champs 'paiementinscription_str' : 'ok', 'Non' ou '?'
|
||||
ajoute les champs 'etape' (= None) et 'prenom' ('') s'ils ne sont pas présents.
|
||||
ajoute le champ 'civilite_etat_civil' (='X'), et 'prenom_etat_civil' (='') si non présent.
|
||||
"""
|
||||
for infos in infolist:
|
||||
if "paiementinscription" in infos:
|
||||
@ -520,6 +521,15 @@ def _normalize_apo_fields(infolist):
|
||||
if "prenom" not in infos:
|
||||
infos["prenom"] = ""
|
||||
|
||||
if "civilite_etat_civil" not in infos:
|
||||
infos["civilite_etat_civil"] = "X"
|
||||
|
||||
if "civilite_etat_civil" not in infos:
|
||||
infos["civilite_etat_civil"] = "X"
|
||||
|
||||
if "prenom_etat_civil" not in infos:
|
||||
infos["prenom_etat_civil"] = ""
|
||||
|
||||
return infolist
|
||||
|
||||
|
||||
|
@ -111,13 +111,11 @@ get_base_preferences(formsemestre_id)
|
||||
|
||||
"""
|
||||
import flask
|
||||
|
||||
from flask import current_app, flash, g, request, url_for
|
||||
|
||||
|
||||
from app import db, log
|
||||
from app.models import Departement
|
||||
from app.scodoc import sco_cache
|
||||
from app import log
|
||||
from app.scodoc.sco_exceptions import ScoValueError, ScoException
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||
import app.scodoc.notesdb as ndb
|
||||
@ -274,7 +272,7 @@ class BasePreferences(object):
|
||||
)
|
||||
|
||||
def __init__(self, dept_id: int):
|
||||
dept = Departement.query.get(dept_id)
|
||||
dept = db.session.get(Departement, dept_id)
|
||||
if not dept:
|
||||
raise ScoValueError(f"BasePreferences: Invalid departement: {dept_id}")
|
||||
self.dept_id = dept.id
|
||||
|
@ -30,7 +30,7 @@
|
||||
"""
|
||||
from operator import itemgetter
|
||||
|
||||
from app import log
|
||||
from app import db
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import (
|
||||
@ -63,25 +63,32 @@ def dict_pvjury(
|
||||
Si with_parcours_decisions: ajoute infos sur code decision jury de tous les semestre du parcours
|
||||
Résultat:
|
||||
{
|
||||
'date' : date de la decision la plus recente,
|
||||
'formsemestre' : sem,
|
||||
'is_apc' : bool,
|
||||
'formation' : { 'acronyme' :, 'titre': ... }
|
||||
'decisions' : { [ { 'identite' : {'nom' :, 'prenom':, ...,},
|
||||
'etat' : I ou D ou DEF
|
||||
'decision_sem' : {'code':, 'code_prev': },
|
||||
'decisions_ue' : { ue_id : { 'code' : ADM|CMP|AJ, 'event_date' :,
|
||||
'acronyme', 'numero': } },
|
||||
'autorisations' : [ { 'semestre_id' : { ... } } ],
|
||||
'validation_parcours' : True si parcours validé (diplome obtenu)
|
||||
'prev_code' : code (calculé slt si with_prev),
|
||||
'mention' : mention (en fct moy gen),
|
||||
'sum_ects' : total ECTS acquis dans ce semestre (incluant les UE capitalisées)
|
||||
'sum_ects_capitalises' : somme des ECTS des UE capitalisees
|
||||
}
|
||||
]
|
||||
},
|
||||
'decisions_dict' : { etudid : decision (comme ci-dessus) },
|
||||
'date' : str = date de la decision la plus recente, format dd/mm/yyyy,
|
||||
'formsemestre' : dict = formsemestre,
|
||||
'is_apc' : bool,
|
||||
'formation' : { 'acronyme' :, 'titre': ... }
|
||||
'decisions' : [
|
||||
{
|
||||
'identite' : {'nom' :, 'prenom':, ...,},
|
||||
'etat' : I ou D ou DEF
|
||||
'decision_sem' : {'code':, 'code_prev': },
|
||||
'decisions_ue' : {
|
||||
ue_id : {
|
||||
'code' : ADM|CMP|AJ,
|
||||
'ects' : float,
|
||||
'event_date' :str = "dd/mm/yyyy",
|
||||
},
|
||||
},
|
||||
'autorisations' : [ { 'semestre_id' : { ... } } ],
|
||||
'validation_parcours' : True si parcours validé (diplome obtenu)
|
||||
'prev_code' : code (calculé slt si with_prev),
|
||||
'mention' : mention (en fct moy gen),
|
||||
'sum_ects' : total ECTS acquis dans ce semestre (incluant les UE capitalisées)
|
||||
'sum_ects_capitalises' : somme des ECTS des UE capitalisees
|
||||
},
|
||||
...
|
||||
],
|
||||
'decisions_dict' : { etudid : decision (comme ci-dessus) },
|
||||
}
|
||||
"""
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
@ -253,7 +260,7 @@ def _comp_ects_by_ue_code(nt, decisions_ue):
|
||||
ects_by_ue_code = {}
|
||||
for ue_id in decisions_ue:
|
||||
d = decisions_ue[ue_id]
|
||||
ue = UniteEns.query.get(ue_id)
|
||||
ue = db.session.get(UniteEns, ue_id)
|
||||
ects_by_ue_code[ue.ue_code] = d["ects"]
|
||||
|
||||
return ects_by_ue_code
|
||||
|
@ -42,6 +42,7 @@ from reportlab.platypus import PageBreak, Table, Image
|
||||
from reportlab.platypus.doctemplate import BaseDocTemplate
|
||||
from reportlab.lib import styles
|
||||
|
||||
from app import db
|
||||
from app.models import FormSemestre, Identite
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
@ -70,7 +71,7 @@ def pdf_lettres_individuelles(
|
||||
if not dpv:
|
||||
return ""
|
||||
#
|
||||
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
|
||||
prefs = sco_preferences.SemPreferences(formsemestre_id)
|
||||
params = {
|
||||
"date_jury": date_jury,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user