forked from ScoDoc/ScoDoc
Compare commits
176 Commits
3d91d2f456
...
e90f3077f8
Author | SHA1 | Date |
---|---|---|
Emmanuel Viennet | e90f3077f8 | |
Emmanuel Viennet | 5472d16f54 | |
Emmanuel Viennet | e2fa39393c | |
Emmanuel Viennet | a5a1a84436 | |
Emmanuel Viennet | 50d26db09f | |
Emmanuel Viennet | 55b786e655 | |
leonard_montalbano | f0ce0b574e | |
leonard_montalbano | 1c87405f42 | |
Emmanuel Viennet | ddef0cb0e0 | |
Emmanuel Viennet | 5df1d90101 | |
Emmanuel Viennet | 62c65176b6 | |
leonard_montalbano | 46d99a5735 | |
leonard_montalbano | ce2b7334c6 | |
leonard_montalbano | c6187cbe0d | |
leonard_montalbano | c3eeec29d8 | |
leonard_montalbano | 2cf1e162df | |
leonard_montalbano | be3df71ad6 | |
leonard_montalbano | 4c28d140a6 | |
Emmanuel Viennet | 458aae82d0 | |
leonard_montalbano | 06844380ad | |
Emmanuel Viennet | d000e7b5a7 | |
Emmanuel Viennet | 5379e4b93a | |
Emmanuel Viennet | 7b0eae1970 | |
Emmanuel Viennet | 6d06242cbb | |
leonard_montalbano | fd597b87d4 | |
leonard_montalbano | b383c378f6 | |
Emmanuel Viennet | dcddec1ee6 | |
Sébastien Lehmann | 664fa4eeb3 | |
leonard_montalbano | 79b79ddc76 | |
Emmanuel Viennet | ca3336e9be | |
Sébastien Lehmann | bae33cf86b | |
Emmanuel Viennet | 2c98f47d49 | |
Emmanuel Viennet | 64eccde6bc | |
Emmanuel Viennet | 7f80b18990 | |
Sébastien Lehmann | a9f7980000 | |
Sébastien Lehmann | b596a3820c | |
Emmanuel Viennet | 7ab130499b | |
Emmanuel Viennet | 59b94c9a45 | |
Emmanuel Viennet | 75f8c3b500 | |
Emmanuel Viennet | 9212f72581 | |
Emmanuel Viennet | b4f5634f2b | |
Emmanuel Viennet | 14aabab746 | |
Emmanuel Viennet | adbe466392 | |
Emmanuel Viennet | ee8cfc6191 | |
Emmanuel Viennet | eb5b8d69da | |
Emmanuel Viennet | 16f97b17be | |
Emmanuel Viennet | 27c6f18c29 | |
Emmanuel Viennet | 545c04968f | |
leonard_montalbano | 07123089e2 | |
Emmanuel Viennet | 13a8184601 | |
Emmanuel Viennet | b653cb0218 | |
Emmanuel Viennet | a52bd6e7fe | |
Emmanuel Viennet | 97d306d9d0 | |
Emmanuel Viennet | 98c3a7f740 | |
Emmanuel Viennet | 34bbfec443 | |
leonard_montalbano | 29715a740f | |
Emmanuel Viennet | 4fea9701cb | |
Emmanuel Viennet | 557c5eec4f | |
Emmanuel Viennet | dd9351ca6e | |
Emmanuel Viennet | 41254e06da | |
Emmanuel Viennet | bc6c220f70 | |
Jean-Marie PLACE | 9ad21c6a38 | |
Jean-Marie PLACE | 3b0a42435b | |
Emmanuel Viennet | 94e13d0c1d | |
Emmanuel Viennet | 24b6d1f4cc | |
Emmanuel Viennet | d4d84fcb1e | |
Emmanuel Viennet | 44893acb4a | |
Emmanuel Viennet | 0fe10a383e | |
Emmanuel Viennet | f63c1fadb8 | |
Emmanuel Viennet | 2e6e7675bf | |
leonard_montalbano | 6aba3a3ccd | |
Emmanuel Viennet | 2dbaacf460 | |
leonard_montalbano | 2000565d82 | |
leonard_montalbano | 92e9f4bc5f | |
Emmanuel Viennet | 21cfe4ebf8 | |
Emmanuel Viennet | 0275054510 | |
Jean-Marie PLACE | dab0f78279 | |
Emmanuel Viennet | 954a8c8e81 | |
Emmanuel Viennet | dc9da88fc1 | |
leonard_montalbano | 208aeb35a0 | |
Emmanuel Viennet | 93556ca372 | |
Emmanuel Viennet | c7dbb9b0a9 | |
Emmanuel Viennet | c580d98414 | |
Emmanuel Viennet | 5578dcf5a3 | |
Emmanuel Viennet | 68723f63ee | |
Emmanuel Viennet | 424d376692 | |
Emmanuel Viennet | fb5425c3f6 | |
Emmanuel Viennet | ea3b67852e | |
Emmanuel Viennet | c4b45e11b3 | |
Emmanuel Viennet | 7e5ccfb2d8 | |
Emmanuel Viennet | 2673552389 | |
Jean-Marie PLACE | 68001714f0 | |
Emmanuel Viennet | 721a15d5ec | |
Emmanuel Viennet | 70db38bbb4 | |
leonard_montalbano | 1841fdf896 | |
leonard_montalbano | f1273f7bb2 | |
Emmanuel Viennet | ed09d6263b | |
Emmanuel Viennet | e51b09e7f6 | |
Emmanuel Viennet | 4d7349403d | |
Emmanuel Viennet | 4d257e63e8 | |
Emmanuel Viennet | 570e2dc308 | |
Emmanuel Viennet | 705aa54d77 | |
Emmanuel Viennet | a9f0fcdd6d | |
Emmanuel Viennet | 1153bc9a7c | |
Emmanuel Viennet | 6ac096d29d | |
Emmanuel Viennet | 7a8c77add4 | |
Emmanuel Viennet | ec57ba4ef7 | |
Emmanuel Viennet | c488ad3a62 | |
Emmanuel Viennet | 3ab0e89c2f | |
Emmanuel Viennet | 01c0328636 | |
Emmanuel Viennet | 7577097db8 | |
Emmanuel Viennet | 078b8be33d | |
Emmanuel Viennet | 592a2a33c5 | |
Emmanuel Viennet | f42b54244e | |
Martin Murzeau | 62e9c02680 | |
Emmanuel Viennet | fe69aec8d6 | |
Emmanuel Viennet | bee87cf58b | |
Emmanuel Viennet | 6b49c8472d | |
Emmanuel Viennet | 841ae1c7ab | |
Emmanuel Viennet | 40921efe28 | |
Emmanuel Viennet | 187f4721eb | |
Emmanuel Viennet | 83d538e2a2 | |
Emmanuel Viennet | 795de44c0c | |
Emmanuel Viennet | 0d726aa428 | |
Emmanuel Viennet | c65689b2a3 | |
Emmanuel Viennet | ae0baf8c1a | |
Emmanuel Viennet | 1e5ef96f8f | |
Emmanuel Viennet | 0b28583953 | |
Emmanuel Viennet | e270ad5520 | |
Emmanuel Viennet | 95000ed8a8 | |
Emmanuel Viennet | eaa7c64e41 | |
Emmanuel Viennet | 482c2a24ec | |
Emmanuel Viennet | b8bf6e5c41 | |
Emmanuel Viennet | b85ca4299b | |
Emmanuel Viennet | c0f83262e4 | |
Emmanuel Viennet | b5f317083c | |
Emmanuel Viennet | f349c8cad9 | |
Emmanuel Viennet | 2d2d62fb71 | |
Emmanuel Viennet | 501d43f709 | |
Emmanuel Viennet | 4b160ef25e | |
Emmanuel Viennet | 553770f4ba | |
Emmanuel Viennet | 8a4b26d2e1 | |
leonard_montalbano | a43f1e0e22 | |
leonard_montalbano | 8e36201482 | |
leonard_montalbano | 433b4b8f5c | |
leonard_montalbano | 288cad21cc | |
leonard_montalbano | f9817966cf | |
leonard_montalbano | 28ec8a482a | |
leonard_montalbano | c4f2f0925d | |
leonard_montalbano | 47123aeb1e | |
leonard_montalbano | 90e292341e | |
leonard_montalbano | b1e0def55a | |
leonard_montalbano | c1b11bd9d1 | |
leonard_montalbano | 4d49de397c | |
leonard_montalbano | e9656dc07f | |
leonard_montalbano | 6bab2c00ad | |
leonard_montalbano | 1cd7a84b15 | |
leonard_montalbano | 1c271bbad4 | |
leonard_montalbano | f0bdb5e9bd | |
leonard_montalbano | afe43f98e3 | |
leonard_montalbano | 8ea9f04ea6 | |
leonard_montalbano | cd961e6e3e | |
leonard_montalbano | 35be7ebb4c | |
leonard_montalbano | d468a3f49e | |
leonard_montalbano | ba164481a6 | |
leonard_montalbano | 976fdf5b4e | |
leonard_montalbano | 3fab8300a1 | |
leonard_montalbano | 677094aaac | |
leonard_montalbano | ba0062135b | |
leonard_montalbano | 401a43378d | |
leonard_montalbano | 84f43e1b36 | |
leonard_montalbano | 79b5530813 | |
leonard_montalbano | 25b0648284 | |
leonard_montalbano | 6842669ffe | |
leonard_montalbano | 2e1dcce69d | |
Emmanuel Viennet | 1daafc9d16 |
|
@ -69,7 +69,12 @@ Puis remplacer `/opt/scodoc` par un clone du git.
|
|||
cd /opt
|
||||
git clone https://scodoc.org/git/viennet/ScoDoc.git
|
||||
# (ou bien utiliser votre clone gitea si vous l'avez déjà créé !)
|
||||
mv ScoDoc scodoc # important !
|
||||
|
||||
# Renommer le répertoire:
|
||||
mv ScoDoc scodoc
|
||||
|
||||
# Et donner ce répertoire à l'utilisateur scodoc:
|
||||
chown -R scodoc.scodoc /opt/scodoc
|
||||
|
||||
Il faut ensuite installer l'environnement et le fichier de configuration:
|
||||
|
||||
|
|
|
@ -23,4 +23,13 @@ def requested_format(default_format="json", allowed_formats=None):
|
|||
|
||||
from app.api import tokens
|
||||
from app.api import sco_api
|
||||
from app.api import test_api
|
||||
from app.api import departements
|
||||
from app.api import etudiants
|
||||
from app.api import formations
|
||||
from app.api import formsemestres
|
||||
from app.api import partitions
|
||||
from app.api import evaluations
|
||||
from app.api import jury
|
||||
from app.api import absences
|
||||
from app.api import logos
|
||||
|
|
|
@ -0,0 +1,150 @@
|
|||
#################################################### Absences #########################################################
|
||||
|
||||
from flask import jsonify
|
||||
|
||||
from app.api import bp
|
||||
from app.api.errors import error_response
|
||||
from app.api.auth import token_permission_required
|
||||
from app.api.tools import get_etu_from_etudid_or_nip_or_ine
|
||||
from app.scodoc import notesdb as ndb
|
||||
|
||||
from app.scodoc import sco_abs
|
||||
from app.scodoc.sco_groups import get_group_members
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
|
||||
@bp.route("/absences/etudid/<int:etudid>", methods=["GET"])
|
||||
@bp.route("/absences/nip/<int:nip>", methods=["GET"])
|
||||
@bp.route("/absences/ine/<int:ine>", methods=["GET"])
|
||||
@token_permission_required(Permission.APIView)
|
||||
def absences(etudid: int = None, nip: int = None, ine: int = None):
|
||||
"""
|
||||
Retourne la liste des absences d'un étudiant donné
|
||||
|
||||
etudid : l'etudid d'un étudiant
|
||||
nip: le code nip d'un étudiant
|
||||
ine : le code ine d'un étudiant
|
||||
|
||||
Exemple de résultat:
|
||||
[
|
||||
{
|
||||
"jour": "2022-04-15",
|
||||
"matin": true,
|
||||
"estabs": true,
|
||||
"estjust": true,
|
||||
"description": "",
|
||||
"begin": "2022-04-15 08:00:00",
|
||||
"end": "2022-04-15 11:59:59"
|
||||
},
|
||||
{
|
||||
"jour": "2022-04-15",
|
||||
"matin": false,
|
||||
"estabs": true,
|
||||
"estjust": false,
|
||||
"description": "",
|
||||
"begin": "2022-04-15 12:00:00",
|
||||
"end": "2022-04-15 17:59:59"
|
||||
}
|
||||
]
|
||||
"""
|
||||
if etudid is None:
|
||||
# Récupération de l'étudiant
|
||||
etud = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine)
|
||||
if etud is None:
|
||||
return error_response(
|
||||
409,
|
||||
message="La requête ne peut être traitée en l’état actuel.\n "
|
||||
"Veuillez vérifier que l'id de l'étudiant (etudid, nip, ine) est valide",
|
||||
)
|
||||
etudid = etud.etudid
|
||||
|
||||
# Récupération des absences de l'étudiant
|
||||
ndb.open_db_connection()
|
||||
absences = sco_abs.list_abs_date(etudid)
|
||||
for absence in absences:
|
||||
absence["jour"] = absence["jour"].isoformat()
|
||||
return jsonify(absences)
|
||||
|
||||
|
||||
@bp.route("/absences/etudid/<int:etudid>/just", methods=["GET"])
|
||||
@bp.route("/absences/nip/<int:nip>/just", methods=["GET"])
|
||||
@bp.route("/absences/ine/<int:ine>/just", methods=["GET"])
|
||||
@token_permission_required(Permission.APIView)
|
||||
def absences_just(etudid: int = None, nip: int = None, ine: int = None):
|
||||
"""
|
||||
Retourne la liste des absences justifiées d'un étudiant donné
|
||||
|
||||
etudid : l'etudid d'un étudiant
|
||||
nip: le code nip d'un étudiant
|
||||
ine : le code ine d'un étudiant
|
||||
|
||||
Exemple de résultat :
|
||||
[
|
||||
{
|
||||
"jour": "2022-04-15",
|
||||
"matin": true,
|
||||
"estabs": true,
|
||||
"estjust": true,
|
||||
"description": "",
|
||||
"begin": "2022-04-15 08:00:00",
|
||||
"end": "2022-04-15 11:59:59"
|
||||
},
|
||||
{
|
||||
"jour": "Fri, 15 Apr 2022 00:00:00 GMT",
|
||||
"matin": false,
|
||||
"estabs": true,
|
||||
"estjust": false,
|
||||
"description": "",
|
||||
"begin": "2022-04-15 12:00:00",
|
||||
"end": "2022-04-15 17:59:59"
|
||||
}
|
||||
]
|
||||
"""
|
||||
if etudid is None:
|
||||
# Récupération de l'étudiant
|
||||
try:
|
||||
etu = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine)
|
||||
etudid = etu.etudid
|
||||
except AttributeError:
|
||||
return error_response(
|
||||
409,
|
||||
message="La requête ne peut être traitée en l’état actuel.\n "
|
||||
"Veuillez vérifier que l'id de l'étudiant (etudid, nip, ine) est valide",
|
||||
)
|
||||
|
||||
# Récupération des absences justifiées de l'étudiant
|
||||
absences = sco_abs.list_abs_date(etudid)
|
||||
for absence in [absence for absence in absences if absence["estjust"]]:
|
||||
absence["jour"] = absence["jour"].isoformat()
|
||||
return jsonify(absences)
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/absences/abs_group_etat/?group_id=<int:group_id>&date_debut=date_debut&date_fin=date_fin",
|
||||
methods=["GET"],
|
||||
)
|
||||
@token_permission_required(Permission.APIView)
|
||||
def abs_groupe_etat( # XXX A REVOIR XXX
|
||||
group_id: int, date_debut, date_fin, with_boursier=True, format="html"
|
||||
):
|
||||
"""
|
||||
Retoune la liste des absences d'un ou plusieurs groupes entre deux dates
|
||||
"""
|
||||
# Fonction utilisée : app.scodoc.sco_groups.get_group_members() et app.scodoc.sco_abs.list_abs_date()
|
||||
|
||||
try:
|
||||
# Utilisation de la fonction get_group_members
|
||||
members = get_group_members(group_id)
|
||||
except ValueError:
|
||||
return error_response(
|
||||
409, message="La requête ne peut être traitée en l’état actuel"
|
||||
)
|
||||
|
||||
data = []
|
||||
# Filtre entre les deux dates renseignées
|
||||
for member in members:
|
||||
abs = sco_abs.list_abs_date(member.id, date_debut, date_fin)
|
||||
data.append(abs)
|
||||
|
||||
# return jsonify(data) # XXX TODO faire en sorte de pouvoir renvoyer sa (ex to_dict() dans absences)
|
||||
return error_response(501, message="Not implemented")
|
|
@ -24,6 +24,10 @@
|
|||
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
from functools import wraps
|
||||
|
||||
|
||||
from flask import abort
|
||||
from flask import g
|
||||
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth
|
||||
from app.auth.models import User
|
||||
|
@ -63,15 +67,17 @@ def get_user_roles(user):
|
|||
return user.roles
|
||||
|
||||
|
||||
# def token_permission_required(permission):
|
||||
# def decorator(f):
|
||||
# @wraps(f)
|
||||
# def decorated_function(*args, **kwargs):
|
||||
# scodoc_dept = getattr(g, "scodoc_dept", None)
|
||||
# if not current_user.has_permission(permission, scodoc_dept):
|
||||
# abort(403)
|
||||
# return f(*args, **kwargs)
|
||||
def token_permission_required(permission):
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
scodoc_dept = getattr(g, "scodoc_dept", None)
|
||||
if hasattr(g, "current_user") and not g.current_user.has_permission(
|
||||
permission, scodoc_dept
|
||||
):
|
||||
abort(403)
|
||||
return f(*args, **kwargs)
|
||||
|
||||
# return login_required(decorated_function)
|
||||
return decorated_function # login_required(decorated_function)
|
||||
|
||||
# return decorator
|
||||
return decorator
|
||||
|
|
|
@ -0,0 +1,286 @@
|
|||
############################################### Departements ##########################################################
|
||||
import app
|
||||
|
||||
from app import models
|
||||
from app.api import bp
|
||||
from app.api.auth import token_permission_required
|
||||
from app.api.errors import error_response
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
from flask import jsonify
|
||||
|
||||
|
||||
@bp.route("/departements", methods=["GET"])
|
||||
@token_permission_required(Permission.APIView)
|
||||
def departements():
|
||||
"""
|
||||
Retourne la liste des ids de départements visibles
|
||||
|
||||
Exemple de résultat : [2, 5, 8, 1, 4, 18]
|
||||
"""
|
||||
# Récupération de tous les départements
|
||||
depts = models.Departement.query.filter_by(visible=True)
|
||||
|
||||
# Mise en place de la liste avec tous les ids de départements
|
||||
depts_ids = [d.id for d in depts]
|
||||
|
||||
return jsonify(depts_ids)
|
||||
|
||||
|
||||
@bp.route("/departements/<string:dept>/etudiants/liste", methods=["GET"])
|
||||
@bp.route(
|
||||
"/departements/<string:dept>/etudiants/liste/<int:formsemestre_id>", methods=["GET"]
|
||||
)
|
||||
@token_permission_required(Permission.APIView)
|
||||
def liste_etudiants(dept: str, formsemestre_id=None):
|
||||
"""
|
||||
Retourne la liste des étudiants d'un département
|
||||
|
||||
dept: l'acronym d'un département
|
||||
formsemestre_id: l'id d'un formesemestre
|
||||
|
||||
Exemple de résultat :
|
||||
[
|
||||
{
|
||||
"civilite": "X",
|
||||
"code_ine": null,
|
||||
"code_nip": null,
|
||||
"date_naissance": null,
|
||||
"email": null,
|
||||
"emailperso": null,
|
||||
"etudid": 18,
|
||||
"nom": "MOREL",
|
||||
"prenom": "JACQUES"
|
||||
},
|
||||
{
|
||||
"civilite": "X",
|
||||
"code_ine": null,
|
||||
"code_nip": null,
|
||||
"date_naissance": null,
|
||||
"email": null,
|
||||
"emailperso": null,
|
||||
"etudid": 19,
|
||||
"nom": "FOURNIER",
|
||||
"prenom": "ANNE"
|
||||
},
|
||||
...
|
||||
]
|
||||
"""
|
||||
# Si le formsemestre_id a été renseigné
|
||||
if formsemestre_id is not None:
|
||||
# Récupération du formsemestre
|
||||
formsemestre = models.FormSemestre.query.filter_by(
|
||||
id=formsemestre_id
|
||||
).first_or_404()
|
||||
# Récupération du département
|
||||
departement = formsemestre.departement
|
||||
|
||||
# Si le formsemestre_id n'a pas été renseigné
|
||||
else:
|
||||
# Récupération du formsemestre
|
||||
departement = models.Departement.query.filter_by(acronym=dept).first_or_404()
|
||||
|
||||
# Récupération des étudiants
|
||||
etudiants = departement.etudiants.all()
|
||||
|
||||
# Mise en forme des données
|
||||
list_etu = [etu.to_dict_bul(include_urls=False) for etu in etudiants]
|
||||
|
||||
return jsonify(list_etu)
|
||||
|
||||
|
||||
@bp.route("/departements/<string:dept>/semestres_courants", methods=["GET"])
|
||||
@token_permission_required(Permission.APIView)
|
||||
def liste_semestres_courant(dept: str):
|
||||
"""
|
||||
Liste des semestres actifs d'un départements donné
|
||||
|
||||
dept: l'acronym d'un département
|
||||
|
||||
Exemple de résultat :
|
||||
[
|
||||
{
|
||||
"titre": "master machine info",
|
||||
"gestion_semestrielle": false,
|
||||
"scodoc7_id": null,
|
||||
"date_debut": "01/09/2021",
|
||||
"bul_bgcolor": null,
|
||||
"date_fin": "15/12/2022",
|
||||
"resp_can_edit": false,
|
||||
"dept_id": 1,
|
||||
"etat": true,
|
||||
"resp_can_change_ens": false,
|
||||
"id": 1,
|
||||
"modalite": "FI",
|
||||
"ens_can_edit_eval": false,
|
||||
"formation_id": 1,
|
||||
"gestion_compensation": false,
|
||||
"elt_sem_apo": null,
|
||||
"semestre_id": 1,
|
||||
"bul_hide_xml": false,
|
||||
"elt_annee_apo": null,
|
||||
"block_moyennes": false,
|
||||
"formsemestre_id": 1,
|
||||
"titre_num": "master machine info semestre 1",
|
||||
"date_debut_iso": "2021-09-01",
|
||||
"date_fin_iso": "2022-12-15",
|
||||
"responsables": [
|
||||
3,
|
||||
2
|
||||
]
|
||||
},
|
||||
...
|
||||
]
|
||||
"""
|
||||
# Récupération des départements comportant l'acronym mit en paramètre
|
||||
dept = models.Departement.query.filter_by(acronym=dept).first_or_404()
|
||||
|
||||
# Récupération des semestres suivant id_dept
|
||||
semestres = models.FormSemestre.query.filter_by(dept_id=dept.id, etat=True)
|
||||
|
||||
# Mise en forme des données
|
||||
data = [d.to_dict() for d in semestres]
|
||||
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/departements/<string:dept>/formations/<int:formation_id>/referentiel_competences",
|
||||
methods=["GET"],
|
||||
)
|
||||
@token_permission_required(Permission.APIView)
|
||||
def referenciel_competences(dept: str, formation_id: int):
|
||||
"""
|
||||
Retourne le référentiel de compétences
|
||||
|
||||
dept : l'acronym d'un département
|
||||
formation_id : l'id d'une formation
|
||||
"""
|
||||
dept = models.Departement.query.filter_by(acronym=dept).first_or_404()
|
||||
|
||||
formation = models.Formation.query.filter_by(
|
||||
id=formation_id, dept_id=dept.id
|
||||
).first_or_404()
|
||||
|
||||
ref_comp = formation.referentiel_competence_id
|
||||
|
||||
if ref_comp is None:
|
||||
return error_response(
|
||||
204, message="Pas de référenciel de compétences pour cette formation"
|
||||
)
|
||||
else:
|
||||
return jsonify(ref_comp)
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/departements/<string:dept>/formsemestre/<string:formsemestre_id>/programme",
|
||||
methods=["GET"],
|
||||
)
|
||||
@token_permission_required(Permission.APIView)
|
||||
def semestre_index(dept: str, formsemestre_id: int):
|
||||
"""
|
||||
Retourne la liste des Ues, ressources et SAE d'un semestre
|
||||
|
||||
dept : l'acronym d'un département
|
||||
formsemestre_id : l'id d'un formesemestre
|
||||
|
||||
Exemple de résultat :
|
||||
{
|
||||
"ues": [
|
||||
{
|
||||
"type": 0,
|
||||
"formation_id": 1,
|
||||
"ue_code": "UCOD11",
|
||||
"id": 1,
|
||||
"ects": 12.0,
|
||||
"acronyme": "RT1.1",
|
||||
"is_external": false,
|
||||
"numero": 1,
|
||||
"code_apogee": "",
|
||||
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
|
||||
"coefficient": 0.0,
|
||||
"semestre_idx": 1,
|
||||
"color": "#B80004",
|
||||
"ue_id": 1
|
||||
},
|
||||
...
|
||||
],
|
||||
"ressources": [
|
||||
{
|
||||
"titre": "Fondamentaux de la programmation",
|
||||
"coefficient": 1.0,
|
||||
"module_type": 2,
|
||||
"id": 17,
|
||||
"ects": null,
|
||||
"abbrev": null,
|
||||
"ue_id": 3,
|
||||
"code": "R107",
|
||||
"formation_id": 1,
|
||||
"heures_cours": 0.0,
|
||||
"matiere_id": 3,
|
||||
"heures_td": 0.0,
|
||||
"semestre_id": 1,
|
||||
"heures_tp": 0.0,
|
||||
"numero": 70,
|
||||
"code_apogee": "",
|
||||
"module_id": 17
|
||||
},
|
||||
...
|
||||
],
|
||||
"saes": [
|
||||
{
|
||||
"titre": "Se pr\u00e9senter sur Internet",
|
||||
"coefficient": 1.0,
|
||||
"module_type": 3,
|
||||
"id": 14,
|
||||
"ects": null,
|
||||
"abbrev": null,
|
||||
"ue_id": 3,
|
||||
"code": "SAE14",
|
||||
"formation_id": 1,
|
||||
"heures_cours": 0.0,
|
||||
"matiere_id": 3,
|
||||
"heures_td": 0.0,
|
||||
"semestre_id": 1,
|
||||
"heures_tp": 0.0,
|
||||
"numero": 40,
|
||||
"code_apogee": "",
|
||||
"module_id": 14
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
app.set_sco_dept(dept)
|
||||
|
||||
formsemestre = models.FormSemestre.query.filter_by(
|
||||
id=formsemestre_id
|
||||
).first_or_404()
|
||||
|
||||
ues = formsemestre.query_ues()
|
||||
|
||||
ues_dict = []
|
||||
ressources = []
|
||||
saes = []
|
||||
|
||||
for ue in ues:
|
||||
ues_dict.append(ue.to_dict())
|
||||
ressources = ue.get_ressources()
|
||||
saes = ue.get_saes()
|
||||
|
||||
data_ressources = []
|
||||
for ressource in ressources:
|
||||
data_ressources.append(ressource.to_dict())
|
||||
|
||||
data_saes = []
|
||||
for sae in saes:
|
||||
data_saes.append(sae.to_dict())
|
||||
|
||||
data = {
|
||||
"ues": ues_dict,
|
||||
"ressources": data_ressources,
|
||||
"saes": data_saes,
|
||||
}
|
||||
|
||||
return data
|
|
@ -0,0 +1,432 @@
|
|||
#################################################### Etudiants ########################################################
|
||||
|
||||
from flask import jsonify
|
||||
|
||||
import app
|
||||
from app import models
|
||||
from app.api import bp
|
||||
from app.api.errors import error_response
|
||||
from app.api.auth import token_permission_required
|
||||
from app.api.tools import get_etu_from_etudid_or_nip_or_ine
|
||||
from app.models import FormSemestreInscription, FormSemestre, Identite
|
||||
from app.scodoc import sco_bulletins
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc import notesdb as ndb
|
||||
|
||||
|
||||
@bp.route("/etudiants/courant", methods=["GET"])
|
||||
@token_permission_required(Permission.APIView)
|
||||
def etudiants_courant():
|
||||
"""
|
||||
Retourne la liste des étudiants courant
|
||||
|
||||
Exemple de résultat :
|
||||
{
|
||||
{
|
||||
"civilite": "X",
|
||||
"code_ine": null,
|
||||
"code_nip": null,
|
||||
"date_naissance": null,
|
||||
"email": null,
|
||||
"emailperso": null,
|
||||
"etudid": 18,
|
||||
"nom": "MOREL",
|
||||
"prenom": "JACQUES"
|
||||
},
|
||||
{
|
||||
"civilite": "X",
|
||||
"code_ine": null,
|
||||
"code_nip": null,
|
||||
"date_naissance": null,
|
||||
"email": null,
|
||||
"emailperso": null,
|
||||
"etudid": 19,
|
||||
"nom": "FOURNIER",
|
||||
"prenom": "ANNE"
|
||||
},
|
||||
...
|
||||
}
|
||||
"""
|
||||
# Récupération de tous les étudiants
|
||||
etuds = Identite.query.filter(
|
||||
Identite.id == FormSemestreInscription.etudid,
|
||||
FormSemestreInscription.formsemestre_id == FormSemestre.id,
|
||||
FormSemestre.date_debut <= app.db.func.now(),
|
||||
FormSemestre.date_fin >= app.db.func.now(),
|
||||
)
|
||||
|
||||
data = [etu.to_dict_bul(include_urls=False) for etu in etuds]
|
||||
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
@bp.route("/etudiant/etudid/<int:etudid>", methods=["GET"])
|
||||
@bp.route("/etudiant/nip/<int:nip>", methods=["GET"])
|
||||
@bp.route("/etudiant/ine/<int:ine>", methods=["GET"])
|
||||
@token_permission_required(Permission.APIView)
|
||||
def etudiant(etudid: int = None, nip: int = None, ine: int = None):
|
||||
"""
|
||||
Retourne les informations de l'étudiant correspondant à l'id passé en paramètres.
|
||||
|
||||
etudid : l'etudid d'un étudiant
|
||||
nip : le code nip d'un étudiant
|
||||
ine : le code ine d'un étudiant
|
||||
|
||||
Exemple de résultat :
|
||||
{
|
||||
"civilite": "X",
|
||||
"code_ine": null,
|
||||
"code_nip": null,
|
||||
"date_naissance": null,
|
||||
"email": null,
|
||||
"emailperso": null,
|
||||
"etudid": 18,
|
||||
"nom": "MOREL",
|
||||
"prenom": "JACQUES"
|
||||
}
|
||||
"""
|
||||
# Récupération de l'étudiant
|
||||
etud = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine)
|
||||
|
||||
# Mise en forme des données
|
||||
data = etud.to_dict_bul(include_urls=False)
|
||||
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
@bp.route("/etudiant/etudid/<int:etudid>/formsemestres")
|
||||
@bp.route("/etudiant/nip/<int:nip>/formsemestres")
|
||||
@bp.route("/etudiant/ine/<int:ine>/formsemestres")
|
||||
@token_permission_required(Permission.APIView)
|
||||
def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None):
|
||||
"""
|
||||
Retourne la liste des semestres qu'un étudiant a suivis, triés par ordre chronologique.
|
||||
|
||||
etudid : l'etudid d'un étudiant
|
||||
nip : le code nip d'un étudiant
|
||||
ine : le code ine d'un étudiant
|
||||
|
||||
Exemple de résultat :
|
||||
[
|
||||
{
|
||||
"titre": "master machine info",
|
||||
"gestion_semestrielle": false,
|
||||
"date_debut": "01/09/2021",
|
||||
"bul_bgcolor": null,
|
||||
"date_fin": "15/12/2022",
|
||||
"resp_can_edit": false,
|
||||
"dept_id": 1,
|
||||
"etat": true,
|
||||
"resp_can_change_ens": false,
|
||||
"id": 1,
|
||||
"modalite": "FI",
|
||||
"ens_can_edit_eval": false,
|
||||
"formation_id": 1,
|
||||
"gestion_compensation": false,
|
||||
"elt_sem_apo": null,
|
||||
"semestre_id": 1,
|
||||
"bul_hide_xml": false,
|
||||
"elt_annee_apo": null,
|
||||
"block_moyennes": false,
|
||||
"formsemestre_id": 1,
|
||||
"titre_num": "master machine info semestre 1",
|
||||
"date_debut_iso": "2021-09-01",
|
||||
"date_fin_iso": "2022-12-15",
|
||||
"responsables": [
|
||||
3,
|
||||
2
|
||||
]
|
||||
},
|
||||
...
|
||||
]
|
||||
"""
|
||||
# Récupération de l'étudiant
|
||||
etud = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine)
|
||||
|
||||
formsemestres = models.FormSemestre.query.filter(
|
||||
models.FormSemestreInscription.etudid == etud.id,
|
||||
models.FormSemestreInscription.formsemestre_id == models.FormSemestre.id,
|
||||
).order_by(models.FormSemestre.date_debut)
|
||||
|
||||
return jsonify([formsemestre.to_dict() for formsemestre in formsemestres])
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin",
|
||||
methods=["GET"],
|
||||
)
|
||||
@bp.route(
|
||||
"/etudiant/nip/<int:nip>/formsemestre/<int:formsemestre_id>/bulletin",
|
||||
methods=["GET"],
|
||||
)
|
||||
@bp.route(
|
||||
"/etudiant/ine/<int:ine>/formsemestre/<int:formsemestre_id>/bulletin",
|
||||
methods=["GET"],
|
||||
)
|
||||
@token_permission_required(Permission.APIView)
|
||||
def etudiant_bulletin_semestre(
|
||||
formsemestre_id, etudid: int = None, nip: int = None, ine: int = None
|
||||
):
|
||||
"""
|
||||
Retourne le bulletin d'un étudiant en fonction de son id et d'un semestre donné
|
||||
|
||||
formsemestre_id : l'id d'un formsemestre
|
||||
etudid : l'etudid d'un étudiant
|
||||
nip : le code nip d'un étudiant
|
||||
ine : le code ine d'un étudiant
|
||||
Exemple de résultat :
|
||||
{
|
||||
"version": "0",
|
||||
"type": "BUT",
|
||||
"date": "2022-04-27T07:18:16.450634Z",
|
||||
"publie": true,
|
||||
"etudiant": {
|
||||
"civilite": "X",
|
||||
"code_ine": "1",
|
||||
"code_nip": "1",
|
||||
"date_naissance": "",
|
||||
"email": "SACHA.COSTA@example.com",
|
||||
"emailperso": "",
|
||||
"etudid": 1,
|
||||
"nom": "COSTA",
|
||||
"prenom": "SACHA",
|
||||
"nomprenom": "Sacha COSTA",
|
||||
"lieu_naissance": "",
|
||||
"dept_naissance": "",
|
||||
"nationalite": "",
|
||||
"boursier": "",
|
||||
"fiche_url": "/ScoDoc/TAPI/Scolarite/ficheEtud?etudid=1",
|
||||
"photo_url": "/ScoDoc/TAPI/Scolarite/get_photo_image?etudid=1&size=small",
|
||||
"id": 1,
|
||||
"codepostaldomicile": "",
|
||||
"paysdomicile": "",
|
||||
"telephonemobile": "",
|
||||
"typeadresse": "domicile",
|
||||
"domicile": "",
|
||||
"villedomicile": "",
|
||||
"telephone": "",
|
||||
"fax": "",
|
||||
"description": ""
|
||||
},
|
||||
"formation": {
|
||||
"id": 1,
|
||||
"acronyme": "BUT R&T",
|
||||
"titre_officiel": "Bachelor technologique r\u00e9seaux et t\u00e9l\u00e9communications",
|
||||
"titre": "BUT R&T"
|
||||
},
|
||||
"formsemestre_id": 1,
|
||||
"etat_inscription": "I",
|
||||
"options": {
|
||||
"show_abs": true,
|
||||
"show_abs_modules": false,
|
||||
"show_ects": true,
|
||||
"show_codemodules": false,
|
||||
"show_matieres": false,
|
||||
"show_rangs": true,
|
||||
"show_ue_rangs": true,
|
||||
"show_mod_rangs": true,
|
||||
"show_moypromo": false,
|
||||
"show_minmax": false,
|
||||
"show_minmax_mod": false,
|
||||
"show_minmax_eval": false,
|
||||
"show_coef": true,
|
||||
"show_ue_cap_details": false,
|
||||
"show_ue_cap_current": true,
|
||||
"show_temporary": true,
|
||||
"temporary_txt": "Provisoire",
|
||||
"show_uevalid": true,
|
||||
"show_date_inscr": true
|
||||
},
|
||||
"ressources": {
|
||||
"R101": {
|
||||
"id": 1,
|
||||
"titre": "Initiation aux r\u00e9seaux informatiques",
|
||||
"code_apogee": null,
|
||||
"url": "/ScoDoc/TAPI/Scolarite/Notes/moduleimpl_status?moduleimpl_id=1",
|
||||
"moyenne": {},
|
||||
"evaluations": [
|
||||
{
|
||||
"id": 1,
|
||||
"description": "eval1",
|
||||
"date": "2022-04-20",
|
||||
"heure_debut": "08:00",
|
||||
"heure_fin": "09:00",
|
||||
"coef": "01.00",
|
||||
"poids": {
|
||||
"RT1.1": 1.0,
|
||||
},
|
||||
"note": {
|
||||
"value": "12.00",
|
||||
"min": "00.00",
|
||||
"max": "18.00",
|
||||
"moy": "10.88"
|
||||
},
|
||||
"url": "/ScoDoc/TAPI/Scolarite/Notes/evaluation_listenotes?evaluation_id=1"
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
"saes": {
|
||||
"SAE11": {
|
||||
"id": 2,
|
||||
"titre": "Se sensibiliser \u00e0 l'hygi\u00e8ne informatique et \u00e0 la cybers\u00e9curit\u00e9",
|
||||
"code_apogee": null,
|
||||
"url": "/ScoDoc/TAPI/Scolarite/Notes/moduleimpl_status?moduleimpl_id=2",
|
||||
"moyenne": {},
|
||||
"evaluations": []
|
||||
},
|
||||
},
|
||||
"ues": {
|
||||
"RT1.1": {
|
||||
"id": 1,
|
||||
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
|
||||
"numero": 1,
|
||||
"type": 0,
|
||||
"color": "#B80004",
|
||||
"competence": null,
|
||||
"moyenne": {
|
||||
"value": "08.50",
|
||||
"min": "06.00",
|
||||
"max": "16.50",
|
||||
"moy": "11.31",
|
||||
"rang": "12",
|
||||
"total": 16
|
||||
},
|
||||
"bonus": "00.00",
|
||||
"malus": "00.00",
|
||||
"capitalise": null,
|
||||
"ressources": {
|
||||
"R101": {
|
||||
"id": 1,
|
||||
"coef": 12.0,
|
||||
"moyenne": "12.00"
|
||||
},
|
||||
},
|
||||
"saes": {
|
||||
"SAE11": {
|
||||
"id": 2,
|
||||
"coef": 16.0,
|
||||
"moyenne": "~"
|
||||
},
|
||||
},
|
||||
"ECTS": {
|
||||
"acquis": 0.0,
|
||||
"total": 12.0
|
||||
}
|
||||
},
|
||||
"semestre": {
|
||||
"etapes": [],
|
||||
"date_debut": "2021-09-01",
|
||||
"date_fin": "2022-08-31",
|
||||
"annee_universitaire": "2021 - 2022",
|
||||
"numero": 1,
|
||||
"inscription": "",
|
||||
"groupes": [],
|
||||
"absences": {
|
||||
"injustifie": 1,
|
||||
"total": 2
|
||||
},
|
||||
"ECTS": {
|
||||
"acquis": 0,
|
||||
"total": 30.0
|
||||
},
|
||||
"notes": {
|
||||
"value": "10.60",
|
||||
"min": "02.40",
|
||||
"moy": "11.05",
|
||||
"max": "17.40"
|
||||
},
|
||||
"rang": {
|
||||
"value": "10",
|
||||
"total": 16
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
formsemestre = models.FormSemestre.query.filter_by(
|
||||
id=formsemestre_id
|
||||
).first_or_404()
|
||||
|
||||
dept = models.Departement.query.filter_by(id=formsemestre.dept_id).first_or_404()
|
||||
|
||||
app.set_sco_dept(dept.acronym)
|
||||
|
||||
# Récupération de l'étudiant
|
||||
try:
|
||||
etu = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine)
|
||||
except AttributeError:
|
||||
return error_response(
|
||||
409,
|
||||
message="La requête ne peut être traitée en l’état actuel.\n "
|
||||
"Veuillez vérifier que l'id de l'étudiant (etudid, nip, ine) est valide",
|
||||
)
|
||||
|
||||
return sco_bulletins.get_formsemestre_bulletin_etud_json(formsemestre, etu)
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/etudiant/etudid/<int:etudid>/semestre/<int:formsemestre_id>/groups",
|
||||
methods=["GET"],
|
||||
)
|
||||
@bp.route(
|
||||
"/etudiant/nip/<int:nip>/semestre/<int:formsemestre_id>/groups", methods=["GET"]
|
||||
)
|
||||
@bp.route(
|
||||
"/etudiant/ine/<int:ine>/semestre/<int:formsemestre_id>/groups", methods=["GET"]
|
||||
)
|
||||
@token_permission_required(Permission.APIView)
|
||||
def etudiant_groups(
|
||||
formsemestre_id: int, etudid: int = None, nip: int = None, ine: int = None
|
||||
):
|
||||
"""
|
||||
Retourne la liste des groupes auxquels appartient l'étudiant dans le semestre indiqué
|
||||
|
||||
formsemestre_id : l'id d'un formsemestre
|
||||
etudid : l'etudid d'un étudiant
|
||||
nip : le code nip d'un étudiant
|
||||
ine : le code ine d'un étudiant
|
||||
|
||||
Exemple de résultat :
|
||||
[
|
||||
{
|
||||
"partition_id": 1,
|
||||
"id": 1,
|
||||
"formsemestre_id": 1,
|
||||
"partition_name": null,
|
||||
"numero": 0,
|
||||
"bul_show_rank": false,
|
||||
"show_in_lists": true,
|
||||
"group_id": 1,
|
||||
"group_name": null
|
||||
},
|
||||
{
|
||||
"partition_id": 2,
|
||||
"id": 2,
|
||||
"formsemestre_id": 1,
|
||||
"partition_name": "TD",
|
||||
"numero": 1,
|
||||
"bul_show_rank": false,
|
||||
"show_in_lists": true,
|
||||
"group_id": 2,
|
||||
"group_name": "A"
|
||||
}
|
||||
]
|
||||
"""
|
||||
if etudid is None:
|
||||
etud = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine)
|
||||
if etud is None:
|
||||
return error_response(
|
||||
409,
|
||||
message="La requête ne peut être traitée en l’état actuel.\n "
|
||||
"Veuillez vérifier que l'id de l'étudiant (etudid, nip, ine) est valide",
|
||||
)
|
||||
etudid = etud.etudid
|
||||
|
||||
# Récupération du formsemestre
|
||||
sem = models.FormSemestre.query.filter_by(id=formsemestre_id).first_or_404()
|
||||
dept = models.Departement.query.get(sem.dept_id)
|
||||
app.set_sco_dept(dept.acronym)
|
||||
data = sco_groups.get_etud_groups(etudid, sem.id)
|
||||
|
||||
return jsonify(data)
|
|
@ -0,0 +1,111 @@
|
|||
############################################### Evaluations ###########################################################
|
||||
from flask import jsonify
|
||||
|
||||
import app
|
||||
|
||||
from app import models
|
||||
from app.api import bp
|
||||
from app.api.auth import token_permission_required
|
||||
from app.api.errors import error_response
|
||||
from app.scodoc.sco_evaluation_db import do_evaluation_get_all_notes
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
|
||||
@bp.route("/evaluations/<int:moduleimpl_id>", methods=["GET"])
|
||||
@token_permission_required(Permission.APIView)
|
||||
def evaluations(moduleimpl_id: int):
|
||||
"""
|
||||
Retourne la liste des évaluations à partir de l'id d'un moduleimpl
|
||||
|
||||
moduleimpl_id : l'id d'un moduleimpl
|
||||
|
||||
Exemple de résultat :
|
||||
[
|
||||
{
|
||||
"moduleimpl_id": 1,
|
||||
"jour": "20/04/2022",
|
||||
"heure_debut": "08h00",
|
||||
"description": "eval1",
|
||||
"coefficient": 1.0,
|
||||
"publish_incomplete": false,
|
||||
"numero": 0,
|
||||
"id": 1,
|
||||
"heure_fin": "09h00",
|
||||
"note_max": 20.0,
|
||||
"visibulletin": true,
|
||||
"evaluation_type": 0,
|
||||
"evaluation_id": 1,
|
||||
"jouriso": "2022-04-20",
|
||||
"duree": "1h",
|
||||
"descrheure": " de 08h00 \u00e0 09h00",
|
||||
"matin": 1,
|
||||
"apresmidi": 0
|
||||
},
|
||||
...
|
||||
]
|
||||
"""
|
||||
# Récupération de toutes les évaluations
|
||||
evals = models.Evaluation.query.filter_by(id=moduleimpl_id)
|
||||
|
||||
# Mise en forme des données
|
||||
data = [d.to_dict() for d in evals]
|
||||
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
@bp.route("/evaluations/eval_notes/<int:evaluation_id>", methods=["GET"])
|
||||
@token_permission_required(Permission.APIView)
|
||||
def evaluation_notes(evaluation_id: int):
|
||||
"""
|
||||
Retourne la liste des notes à partir de l'id d'une évaluation donnée
|
||||
|
||||
evaluation_id : l'id d'une évaluation
|
||||
|
||||
Exemple de résultat :
|
||||
{
|
||||
"1": {
|
||||
"id": 1,
|
||||
"etudid": 10,
|
||||
"evaluation_id": 1,
|
||||
"value": 15.0,
|
||||
"comment": "",
|
||||
"date": "Wed, 20 Apr 2022 06:49:05 GMT",
|
||||
"uid": 2
|
||||
},
|
||||
"2": {
|
||||
"id": 2,
|
||||
"etudid": 1,
|
||||
"evaluation_id": 1,
|
||||
"value": 12.0,
|
||||
"comment": "",
|
||||
"date": "Wed, 20 Apr 2022 06:49:06 GMT",
|
||||
"uid": 2
|
||||
},
|
||||
...
|
||||
}
|
||||
"""
|
||||
# Fonction utilisée : app.scodoc.sco_evaluation_db.do_evaluation_get_all_notes()
|
||||
|
||||
eval = models.Evaluation.query.filter_by(id=evaluation_id).first_or_404()
|
||||
|
||||
moduleimpl = models.ModuleImpl.query.filter_by(id=eval.moduleimpl_id).first_or_404()
|
||||
|
||||
formsemestre = models.FormSemestre.query.filter_by(
|
||||
id=moduleimpl.formsemestre_id
|
||||
).first_or_404()
|
||||
|
||||
dept = models.Departement.query.filter_by(id=formsemestre.dept_id).first_or_404()
|
||||
|
||||
app.set_sco_dept(dept.acronym)
|
||||
|
||||
try:
|
||||
# Utilisation de la fonction do_evaluation_get_all_notes
|
||||
data = do_evaluation_get_all_notes(evaluation_id)
|
||||
except AttributeError:
|
||||
return error_response(
|
||||
409,
|
||||
message="La requête ne peut être traitée en l’état actuel. \n"
|
||||
"Veillez vérifier la conformité du 'evaluation_id'",
|
||||
)
|
||||
|
||||
return jsonify(data)
|
|
@ -0,0 +1,260 @@
|
|||
##############################################" Formations ############################################################
|
||||
from flask import jsonify
|
||||
|
||||
from app import models
|
||||
from app.api import bp
|
||||
from app.api.errors import error_response
|
||||
from app.api.auth import token_permission_required
|
||||
from app.scodoc.sco_formations import formation_export
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
|
||||
@bp.route("/formations", methods=["GET"])
|
||||
@token_permission_required(Permission.APIView)
|
||||
def formations():
|
||||
"""
|
||||
Retourne la liste des formations
|
||||
|
||||
Exemple de résultat :
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"acronyme": "BUT R&T",
|
||||
"titre_officiel": "Bachelor technologique r\u00e9seaux et t\u00e9l\u00e9communications",
|
||||
"formation_code": "V1RET",
|
||||
"code_specialite": null,
|
||||
"dept_id": 1,
|
||||
"titre": "BUT R&T",
|
||||
"version": 1,
|
||||
"type_parcours": 700,
|
||||
"referentiel_competence_id": null,
|
||||
"formation_id": 1
|
||||
},
|
||||
...
|
||||
]
|
||||
|
||||
"""
|
||||
# Récupération de toutes les formations
|
||||
list_formations = models.Formation.query.all()
|
||||
|
||||
# Mise en forme des données
|
||||
data = [d.to_dict() for d in list_formations]
|
||||
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
@bp.route("/formations/<int:formation_id>", methods=["GET"])
|
||||
@token_permission_required(Permission.APIView)
|
||||
def formations_by_id(formation_id: int):
|
||||
"""
|
||||
Retourne une formation en fonction d'un id donné
|
||||
|
||||
formation_id : l'id d'une formation
|
||||
|
||||
Exemple de résultat :
|
||||
{
|
||||
"id": 1,
|
||||
"acronyme": "BUT R&T",
|
||||
"titre_officiel": "Bachelor technologique r\u00e9seaux et t\u00e9l\u00e9communications",
|
||||
"formation_code": "V1RET",
|
||||
"code_specialite": null,
|
||||
"dept_id": 1,
|
||||
"titre": "BUT R&T",
|
||||
"version": 1,
|
||||
"type_parcours": 700,
|
||||
"referentiel_competence_id": null,
|
||||
"formation_id": 1
|
||||
}
|
||||
"""
|
||||
# Récupération de la formation
|
||||
forma = models.Formation.query.filter_by(id=formation_id).first_or_404()
|
||||
|
||||
# Mise en forme des données
|
||||
data = forma.to_dict()
|
||||
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
@bp.route("/formations/formation_export/<int:formation_id>", methods=["GET"])
|
||||
@token_permission_required(Permission.APIView)
|
||||
def formation_export_by_formation_id(formation_id: int, export_ids=False):
|
||||
"""
|
||||
Retourne la formation, avec UE, matières, modules
|
||||
|
||||
formation_id : l'id d'une formation
|
||||
|
||||
Exemple de résultat :
|
||||
{
|
||||
"id": 1,
|
||||
"acronyme": "BUT R&T",
|
||||
"titre_officiel": "Bachelor technologique r\u00e9seaux et t\u00e9l\u00e9communications",
|
||||
"formation_code": "V1RET",
|
||||
"code_specialite": null,
|
||||
"dept_id": 1,
|
||||
"titre": "BUT R&T",
|
||||
"version": 1,
|
||||
"type_parcours": 700,
|
||||
"referentiel_competence_id": null,
|
||||
"formation_id": 1,
|
||||
"ue": [
|
||||
{
|
||||
"acronyme": "RT1.1",
|
||||
"numero": 1,
|
||||
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
|
||||
"type": 0,
|
||||
"ue_code": "UCOD11",
|
||||
"ects": 12.0,
|
||||
"is_external": false,
|
||||
"code_apogee": "",
|
||||
"coefficient": 0.0,
|
||||
"semestre_idx": 1,
|
||||
"color": "#B80004",
|
||||
"reference": 1,
|
||||
"matiere": [
|
||||
{
|
||||
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
|
||||
"numero": 1,
|
||||
"module": [
|
||||
{
|
||||
"titre": "Initiation aux r\u00e9seaux informatiques",
|
||||
"abbrev": "Init aux r\u00e9seaux informatiques",
|
||||
"code": "R101",
|
||||
"heures_cours": 0.0,
|
||||
"heures_td": 0.0,
|
||||
"heures_tp": 0.0,
|
||||
"coefficient": 1.0,
|
||||
"ects": "",
|
||||
"semestre_id": 1,
|
||||
"numero": 10,
|
||||
"code_apogee": "",
|
||||
"module_type": 2,
|
||||
"coefficients": [
|
||||
{
|
||||
"ue_reference": "1",
|
||||
"coef": "12.0"
|
||||
},
|
||||
{
|
||||
"ue_reference": "2",
|
||||
"coef": "4.0"
|
||||
},
|
||||
{
|
||||
"ue_reference": "3",
|
||||
"coef": "4.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"titre": "Se sensibiliser \u00e0 l'hygi\u00e8ne informatique et \u00e0 la cybers\u00e9curit\u00e9",
|
||||
"abbrev": "Hygi\u00e8ne informatique",
|
||||
"code": "SAE11",
|
||||
"heures_cours": 0.0,
|
||||
"heures_td": 0.0,
|
||||
"heures_tp": 0.0,
|
||||
"coefficient": 1.0,
|
||||
"ects": "",
|
||||
"semestre_id": 1,
|
||||
"numero": 10,
|
||||
"code_apogee": "",
|
||||
"module_type": 3,
|
||||
"coefficients": [
|
||||
{
|
||||
"ue_reference": "1",
|
||||
"coef": "16.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
...
|
||||
]
|
||||
},
|
||||
...
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
"""
|
||||
# Fonction utilité : app.scodoc.sco_formations.formation_export()
|
||||
|
||||
try:
|
||||
# Utilisation de la fonction formation_export
|
||||
data = formation_export(formation_id)
|
||||
except ValueError:
|
||||
return error_response(
|
||||
409,
|
||||
message="La requête ne peut être traitée en l’état actuel. \n"
|
||||
"Veillez vérifier la conformité du 'formation_id'",
|
||||
)
|
||||
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
@bp.route("/formations/moduleimpl/<int:moduleimpl_id>", methods=["GET"])
|
||||
@token_permission_required(Permission.APIView)
|
||||
def moduleimpls(moduleimpl_id: int):
|
||||
"""
|
||||
Retourne la liste des moduleimpl
|
||||
|
||||
moduleimpl_id : l'id d'un moduleimpl
|
||||
"""
|
||||
# Récupération des tous les moduleimpl
|
||||
list_moduleimpls = models.ModuleImpl.query.filter_by(id=moduleimpl_id)
|
||||
|
||||
# Mise en forme des données
|
||||
data = [moduleimpl.to_dict() for moduleimpl in list_moduleimpls]
|
||||
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/formations/moduleimpl/formsemestre/<int:formsemestre_id>/liste",
|
||||
methods=["GET"],
|
||||
) # XXX TODO penser à changer la route sur la doc
|
||||
@token_permission_required(Permission.APIView)
|
||||
def moduleimpls_sem(formsemestre_id: int):
|
||||
"""
|
||||
Retourne la liste des moduleimpl d'un semestre
|
||||
|
||||
formsemestre_id : l'id d'un formsemestre
|
||||
|
||||
Exemple d'utilisation :
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"formsemestre_id": 1,
|
||||
"computation_expr": null,
|
||||
"module_id": 1,
|
||||
"responsable_id": 2,
|
||||
"module": {
|
||||
"heures_tp": 0.0,
|
||||
"code_apogee": "",
|
||||
"titre": "Initiation aux r\u00e9seaux informatiques",
|
||||
"coefficient": 1.0,
|
||||
"module_type": 2,
|
||||
"id": 1,
|
||||
"ects": null,
|
||||
"abbrev": "Init aux r\u00e9seaux informatiques",
|
||||
"ue_id": 1,
|
||||
"code": "R101",
|
||||
"formation_id": 1,
|
||||
"heures_cours": 0.0,
|
||||
"matiere_id": 1,
|
||||
"heures_td": 0.0,
|
||||
"semestre_id": 1,
|
||||
"numero": 10,
|
||||
"module_id": 1
|
||||
},
|
||||
"moduleimpl_id": 1,
|
||||
"ens": []
|
||||
},
|
||||
...
|
||||
]
|
||||
"""
|
||||
formsemestre = models.FormSemestre.query.filter_by(
|
||||
id=formsemestre_id
|
||||
).first_or_404()
|
||||
|
||||
moduleimpls = formsemestre.modimpls_sorted
|
||||
|
||||
data = [moduleimpl.to_dict() for moduleimpl in moduleimpls]
|
||||
|
||||
return jsonify(data)
|
|
@ -0,0 +1,472 @@
|
|||
########################################## Formsemestres ##############################################################
|
||||
from flask import jsonify
|
||||
|
||||
import app
|
||||
from app import models
|
||||
from app.api import bp
|
||||
from app.api.errors import error_response
|
||||
from app.api.auth import token_permission_required
|
||||
from app.api.tools import get_etu_from_etudid_or_nip_or_ine
|
||||
from app.models import FormSemestre, FormSemestreEtape
|
||||
from app.scodoc.sco_bulletins import get_formsemestre_bulletin_etud_json
|
||||
from app.scodoc.sco_bulletins_json import make_json_formsemestre_bulletinetud
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_pvjury import formsemestre_pvjury
|
||||
|
||||
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>", methods=["GET"])
|
||||
@token_permission_required(Permission.APIView)
|
||||
def formsemestre(formsemestre_id: int):
|
||||
"""
|
||||
Retourne l'information sur le formsemestre correspondant au formsemestre_id
|
||||
|
||||
formsemestre_id : l'id d'un formsemestre
|
||||
|
||||
Exemple de résultat :
|
||||
{
|
||||
"date_fin": "31/08/2022",
|
||||
"resp_can_edit": false,
|
||||
"dept_id": 1,
|
||||
"etat": true,
|
||||
"resp_can_change_ens": true,
|
||||
"id": 1,
|
||||
"modalite": "FI",
|
||||
"ens_can_edit_eval": false,
|
||||
"formation_id": 1,
|
||||
"gestion_compensation": false,
|
||||
"elt_sem_apo": null,
|
||||
"semestre_id": 1,
|
||||
"bul_hide_xml": false,
|
||||
"elt_annee_apo": null,
|
||||
"titre": "Semestre test",
|
||||
"block_moyennes": false,
|
||||
"scodoc7_id": null,
|
||||
"date_debut": "01/09/2021",
|
||||
"gestion_semestrielle": false,
|
||||
"bul_bgcolor": "white",
|
||||
"formsemestre_id": 1,
|
||||
"titre_num": "Semestre test semestre 1",
|
||||
"date_debut_iso": "2021-09-01",
|
||||
"date_fin_iso": "2022-08-31",
|
||||
"responsables": []
|
||||
}
|
||||
|
||||
"""
|
||||
# Récupération de tous les formsemestres
|
||||
formsemetre = models.FormSemestre.query.filter_by(id=formsemestre_id).first_or_404()
|
||||
|
||||
# Mise en forme des données
|
||||
data = formsemetre.to_dict()
|
||||
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
@bp.route("/formsemestre/apo/<string:etape_apo>", methods=["GET"])
|
||||
@token_permission_required(Permission.APIView)
|
||||
def formsemestre_apo(etape_apo: str):
|
||||
"""
|
||||
Retourne les informations sur les formsemestres
|
||||
|
||||
etape_apo : l'id d'une étape apogée
|
||||
"""
|
||||
formsemestres = FormSemestre.query.filter(
|
||||
FormSemestreEtape.etape_apo == etape_apo,
|
||||
FormSemestreEtape.formsemestre_id == FormSemestre.id,
|
||||
)
|
||||
|
||||
return jsonify([formsemestre.to_dict() for formsemestre in formsemestres])
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/formsemestre/<int:formsemestre_id>/departements/<string:dept>/etudiant/etudid/<int:etudid>/bulletin",
|
||||
methods=["GET"],
|
||||
)
|
||||
@bp.route(
|
||||
"/formsemestre/<int:formsemestre_id>/departements/<string:dept>/etudiant/nip/<int:nip>/bulletin",
|
||||
methods=["GET"],
|
||||
)
|
||||
@bp.route(
|
||||
"/formsemestre/<int:formsemestre_id>/departements/<string:dept>/etudiant/ine/<int:ine>/bulletin",
|
||||
methods=["GET"],
|
||||
)
|
||||
@token_permission_required(Permission.APIView)
|
||||
def etudiant_bulletin(
|
||||
formsemestre_id,
|
||||
dept,
|
||||
etudid: int = None,
|
||||
nip: int = None,
|
||||
ine: int = None,
|
||||
):
|
||||
"""
|
||||
Retourne le bulletin de note d'un étudiant
|
||||
|
||||
formsemestre_id : l'id d'un formsemestre
|
||||
etudid : l'etudid d'un étudiant
|
||||
nip : le code nip d'un étudiant
|
||||
ine : le code ine d'un étudiant
|
||||
|
||||
Exemple de résultat :
|
||||
{
|
||||
"etudid":1,
|
||||
"formsemestre_id":1,
|
||||
"date":"2022-04-27T10:44:47.448094",
|
||||
"publie":true,
|
||||
"etapes":[
|
||||
|
||||
],
|
||||
"etudiant":{
|
||||
"etudid":1,
|
||||
"code_nip":"1",
|
||||
"code_ine":"1",
|
||||
"nom":"COSTA",
|
||||
"prenom":"Sacha",
|
||||
"civilite":"",
|
||||
"photo_url":"/ScoDoc/TAPI/Scolarite/get_photo_image?etudid=1&size=small",
|
||||
"email":"SACHA.COSTA@example.com",
|
||||
"emailperso":"",
|
||||
"sexe":""
|
||||
},
|
||||
"note":{
|
||||
"value":"10.60",
|
||||
"min":"-",
|
||||
"max":"-",
|
||||
"moy":"-"
|
||||
},
|
||||
"rang":{
|
||||
"value":"10",
|
||||
"ninscrits":16
|
||||
},
|
||||
"rang_group":[
|
||||
{
|
||||
"group_type":"TD",
|
||||
"group_name":"",
|
||||
"value":"",
|
||||
"ninscrits":""
|
||||
}
|
||||
],
|
||||
"note_max":{
|
||||
"value":20
|
||||
},
|
||||
"bonus_sport_culture":{
|
||||
"value":0.0
|
||||
},
|
||||
"ue":[
|
||||
{
|
||||
"id":1,
|
||||
"numero":"1",
|
||||
"acronyme":"RT1.1",
|
||||
"titre":"Administrer les r\u00e9seaux et l\u2019Internet",
|
||||
"note":{
|
||||
"value":"08.50",
|
||||
"min":"06.00",
|
||||
"max":"16.50",
|
||||
"moy":"11.31"
|
||||
},
|
||||
"rang":"12",
|
||||
"effectif":16,
|
||||
"ects":"12",
|
||||
"code_apogee":"",
|
||||
"module":[
|
||||
{
|
||||
"id":1,
|
||||
"code":"R101",
|
||||
"coefficient":1.0,
|
||||
"numero":10,
|
||||
"titre":"Initiation aux r\u00e9seaux informatiques",
|
||||
"abbrev":"Init aux r\u00e9seaux informatiques",
|
||||
"note":{
|
||||
"value":"12.00",
|
||||
"moy":"-",
|
||||
"max":"-",
|
||||
"min":"-",
|
||||
"nb_notes":"-",
|
||||
"nb_missing":"-",
|
||||
"nb_valid_evals":"-"
|
||||
},
|
||||
"code_apogee":"",
|
||||
"evaluation":[
|
||||
{
|
||||
"jour":"2022-04-20",
|
||||
"heure_debut":"08:00:00",
|
||||
"heure_fin":"09:00:00",
|
||||
"coefficient":1.0,
|
||||
"evaluation_type":0,
|
||||
"evaluation_id":1,
|
||||
"description":"eval1",
|
||||
"note":"12.00"
|
||||
}
|
||||
]
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
],
|
||||
"ue_capitalisee":[],
|
||||
"absences":{
|
||||
"nbabs":2,
|
||||
"nbabsjust":1
|
||||
},
|
||||
"appreciation":[]
|
||||
}
|
||||
"""
|
||||
# Fonction utilisée : app.scodoc.sco_bulletins_json.make_json_formsemestre_bulletinetud()
|
||||
|
||||
try:
|
||||
app.set_sco_dept(dept)
|
||||
except:
|
||||
return error_response(
|
||||
409,
|
||||
message="La requête ne peut être traitée en l’état actuel.\n "
|
||||
"Veilliez vérifier que le nom de département est valide",
|
||||
)
|
||||
if etudid is None:
|
||||
# Récupération de l'étudiant
|
||||
try:
|
||||
etu = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine)
|
||||
etudid = etu.etudid
|
||||
except AttributeError:
|
||||
return error_response(
|
||||
409,
|
||||
message="La requête ne peut être traitée en l’état actuel.\n "
|
||||
"Veilliez vérifier que l'id de l'étudiant (etudid, nip, ine) est valide",
|
||||
)
|
||||
|
||||
data = make_json_formsemestre_bulletinetud(formsemestre_id, etudid)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins", methods=["GET"])
|
||||
@token_permission_required(Permission.APIView)
|
||||
def bulletins(formsemestre_id: int):
|
||||
"""
|
||||
Retourne les bulletins d'un formsemestre donné
|
||||
|
||||
formsemestre_id : l'id d'un formesemestre
|
||||
|
||||
Exemple de résultat :
|
||||
[
|
||||
{
|
||||
"version": "0",
|
||||
"type": "BUT",
|
||||
"date": "2022-04-27T07:18:16.450634Z",
|
||||
"publie": true,
|
||||
"etudiant": {
|
||||
"civilite": "X",
|
||||
"code_ine": "1",
|
||||
"code_nip": "1",
|
||||
"date_naissance": "",
|
||||
"email": "SACHA.COSTA@example.com",
|
||||
"emailperso": "",
|
||||
"etudid": 1,
|
||||
"nom": "COSTA",
|
||||
"prenom": "SACHA",
|
||||
"nomprenom": "Sacha COSTA",
|
||||
"lieu_naissance": "",
|
||||
"dept_naissance": "",
|
||||
"nationalite": "",
|
||||
"boursier": "",
|
||||
"fiche_url": "/ScoDoc/TAPI/Scolarite/ficheEtud?etudid=1",
|
||||
"photo_url": "/ScoDoc/TAPI/Scolarite/get_photo_image?etudid=1&size=small",
|
||||
"id": 1,
|
||||
"codepostaldomicile": "",
|
||||
"paysdomicile": "",
|
||||
"telephonemobile": "",
|
||||
"typeadresse": "domicile",
|
||||
"domicile": "",
|
||||
"villedomicile": "",
|
||||
"telephone": "",
|
||||
"fax": "",
|
||||
"description": ""
|
||||
},
|
||||
"formation": {
|
||||
"id": 1,
|
||||
"acronyme": "BUT R&T",
|
||||
"titre_officiel": "Bachelor technologique r\u00e9seaux et t\u00e9l\u00e9communications",
|
||||
"titre": "BUT R&T"
|
||||
},
|
||||
"formsemestre_id": 1,
|
||||
"etat_inscription": "I",
|
||||
"options": {
|
||||
"show_abs": true,
|
||||
"show_abs_modules": false,
|
||||
"show_ects": true,
|
||||
"show_codemodules": false,
|
||||
"show_matieres": false,
|
||||
"show_rangs": true,
|
||||
"show_ue_rangs": true,
|
||||
"show_mod_rangs": true,
|
||||
"show_moypromo": false,
|
||||
"show_minmax": false,
|
||||
"show_minmax_mod": false,
|
||||
"show_minmax_eval": false,
|
||||
"show_coef": true,
|
||||
"show_ue_cap_details": false,
|
||||
"show_ue_cap_current": true,
|
||||
"show_temporary": true,
|
||||
"temporary_txt": "Provisoire",
|
||||
"show_uevalid": true,
|
||||
"show_date_inscr": true
|
||||
},
|
||||
"ressources": {
|
||||
"R101": {
|
||||
"id": 1,
|
||||
"titre": "Initiation aux r\u00e9seaux informatiques",
|
||||
"code_apogee": null,
|
||||
"url": "/ScoDoc/TAPI/Scolarite/Notes/moduleimpl_status?moduleimpl_id=1",
|
||||
"moyenne": {},
|
||||
"evaluations": [
|
||||
{
|
||||
"id": 1,
|
||||
"description": "eval1",
|
||||
"date": "2022-04-20",
|
||||
"heure_debut": "08:00",
|
||||
"heure_fin": "09:00",
|
||||
"coef": "01.00",
|
||||
"poids": {
|
||||
"RT1.1": 1.0,
|
||||
},
|
||||
"note": {
|
||||
"value": "12.00",
|
||||
"min": "00.00",
|
||||
"max": "18.00",
|
||||
"moy": "10.88"
|
||||
},
|
||||
"url": "/ScoDoc/TAPI/Scolarite/Notes/evaluation_listenotes?evaluation_id=1"
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
"saes": {
|
||||
"SAE11": {
|
||||
"id": 2,
|
||||
"titre": "Se sensibiliser \u00e0 l'hygi\u00e8ne informatique et \u00e0 la cybers\u00e9curit\u00e9",
|
||||
"code_apogee": null,
|
||||
"url": "/ScoDoc/TAPI/Scolarite/Notes/moduleimpl_status?moduleimpl_id=2",
|
||||
"moyenne": {},
|
||||
"evaluations": []
|
||||
},
|
||||
},
|
||||
"ues": {
|
||||
"RT1.1": {
|
||||
"id": 1,
|
||||
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
|
||||
"numero": 1,
|
||||
"type": 0,
|
||||
"color": "#B80004",
|
||||
"competence": null,
|
||||
"moyenne": {
|
||||
"value": "08.50",
|
||||
"min": "06.00",
|
||||
"max": "16.50",
|
||||
"moy": "11.31",
|
||||
"rang": "12",
|
||||
"total": 16
|
||||
},
|
||||
"bonus": "00.00",
|
||||
"malus": "00.00",
|
||||
"capitalise": null,
|
||||
"ressources": {
|
||||
"R101": {
|
||||
"id": 1,
|
||||
"coef": 12.0,
|
||||
"moyenne": "12.00"
|
||||
},
|
||||
},
|
||||
"saes": {
|
||||
"SAE11": {
|
||||
"id": 2,
|
||||
"coef": 16.0,
|
||||
"moyenne": "~"
|
||||
},
|
||||
},
|
||||
"ECTS": {
|
||||
"acquis": 0.0,
|
||||
"total": 12.0
|
||||
}
|
||||
},
|
||||
"semestre": {
|
||||
"etapes": [],
|
||||
"date_debut": "2021-09-01",
|
||||
"date_fin": "2022-08-31",
|
||||
"annee_universitaire": "2021 - 2022",
|
||||
"numero": 1,
|
||||
"inscription": "",
|
||||
"groupes": [],
|
||||
"absences": {
|
||||
"injustifie": 1,
|
||||
"total": 2
|
||||
},
|
||||
"ECTS": {
|
||||
"acquis": 0,
|
||||
"total": 30.0
|
||||
},
|
||||
"notes": {
|
||||
"value": "10.60",
|
||||
"min": "02.40",
|
||||
"moy": "11.05",
|
||||
"max": "17.40"
|
||||
},
|
||||
"rang": {
|
||||
"value": "10",
|
||||
"total": 16
|
||||
}
|
||||
}
|
||||
},
|
||||
...
|
||||
]
|
||||
"""
|
||||
# Fonction utilisée : app.scodoc.sco_bulletins.get_formsemestre_bulletin_etud_json()
|
||||
|
||||
formsemestre = models.FormSemestre.query.filter_by(
|
||||
id=formsemestre_id
|
||||
).first_or_404()
|
||||
|
||||
dept = models.Departement.query.filter_by(id=formsemestre.dept_id).first_or_404()
|
||||
|
||||
app.set_sco_dept(dept.acronym)
|
||||
|
||||
etuds = formsemestre.etuds
|
||||
|
||||
data = []
|
||||
for etu in etuds:
|
||||
bul_etu = get_formsemestre_bulletin_etud_json(formsemestre, etu)
|
||||
data.append(bul_etu.json)
|
||||
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/jury", methods=["GET"])
|
||||
@token_permission_required(Permission.APIView)
|
||||
def jury(formsemestre_id: int):
|
||||
"""
|
||||
Retourne le récapitulatif des décisions jury
|
||||
|
||||
formsemestre_id : l'id d'un formsemestre
|
||||
|
||||
Exemple de résultat :
|
||||
|
||||
"""
|
||||
# Fonction utilisée : app.scodoc.sco_pvjury.formsemestre_pvjury()
|
||||
|
||||
formsemestre = models.FormSemestre.query.filter_by(
|
||||
id=formsemestre_id
|
||||
).first_or_404()
|
||||
|
||||
dept = models.Departement.query.filter_by(id=formsemestre.dept_id).first_or_404()
|
||||
|
||||
app.set_sco_dept(dept.acronym)
|
||||
|
||||
data = formsemestre_pvjury(formsemestre_id)
|
||||
|
||||
# try:
|
||||
# # Utilisation de la fonction formsemestre_pvjury
|
||||
# data = formsemestre_pvjury(formsemestre_id)
|
||||
# except AttributeError:
|
||||
# return error_response(
|
||||
# 409,
|
||||
# message="La requête ne peut être traitée en l’état actuel. \n"
|
||||
# "Veillez vérifier la conformité du 'formation_id'",
|
||||
# )
|
||||
|
||||
return jsonify(data)
|
|
@ -0,0 +1,41 @@
|
|||
#################################################### Jury #############################################################
|
||||
from flask import jsonify
|
||||
|
||||
from app import models
|
||||
from app.api import bp
|
||||
from app.api.errors import error_response
|
||||
from app.api.auth import token_permission_required
|
||||
from app.scodoc.sco_prepajury import feuille_preparation_jury
|
||||
from app.scodoc.sco_pvjury import formsemestre_pvjury
|
||||
|
||||
|
||||
@bp.route("/jury/formsemestre/<int:formsemestre_id>/preparation_jury", methods=["GET"])
|
||||
# @token_permission_required(Permission.?)
|
||||
def jury_preparation(formsemestre_id: int):
|
||||
"""
|
||||
Retourne la feuille de préparation du jury
|
||||
|
||||
formsemestre_id : l'id d'un formsemestre
|
||||
"""
|
||||
# Fonction utilisée : app.scodoc.sco_prepajury.feuille_preparation_jury()
|
||||
|
||||
# Utilisation de la fonction feuille_preparation_jury
|
||||
prepa_jury = feuille_preparation_jury(formsemestre_id)
|
||||
|
||||
return error_response(501, message="Not implemented")
|
||||
|
||||
|
||||
@bp.route("/jury/formsemestre/<int:formsemestre_id>/decisions_jury", methods=["GET"])
|
||||
# @token_permission_required(Permission.?)
|
||||
def jury_decisions(formsemestre_id: int):
|
||||
"""
|
||||
Retourne les décisions du jury suivant un formsemestre donné
|
||||
|
||||
formsemestre_id : l'id d'un formsemestre
|
||||
"""
|
||||
# Fonction utilisée : app.scodoc.sco_pvjury.formsemestre_pvjury()
|
||||
|
||||
# Utilisation de la fonction formsemestre_pvjury
|
||||
decision_jury = formsemestre_pvjury(formsemestre_id)
|
||||
|
||||
return error_response(501, message="Not implemented")
|
|
@ -36,13 +36,15 @@ from app.api import bp
|
|||
from app.api import requested_format
|
||||
from app.api.auth import token_auth
|
||||
from app.api.errors import error_response
|
||||
from app.decorators import permission_required
|
||||
from app.models import Departement
|
||||
from app.scodoc.sco_logos import list_logos, find_logo
|
||||
from app.api.auth import token_auth, token_permission_required
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
|
||||
@bp.route("/logos", methods=["GET"])
|
||||
@token_auth.login_required
|
||||
@token_permission_required(Permission.APIView)
|
||||
def api_get_glob_logos():
|
||||
if not g.current_user.has_permission(Permission.ScoSuperAdmin, None):
|
||||
return error_response(401, message="accès interdit")
|
||||
|
@ -54,7 +56,7 @@ def api_get_glob_logos():
|
|||
|
||||
|
||||
@bp.route("/logos/<string:logoname>", methods=["GET"])
|
||||
@token_auth.login_required
|
||||
@token_permission_required(Permission.APIView)
|
||||
def api_get_glob_logo(logoname):
|
||||
if not g.current_user.has_permission(Permission.ScoSuperAdmin, None):
|
||||
return error_response(401, message="accès interdit")
|
||||
|
@ -70,7 +72,7 @@ def api_get_glob_logo(logoname):
|
|||
|
||||
|
||||
@bp.route("/departements/<string:departement>/logos", methods=["GET"])
|
||||
@token_auth.login_required
|
||||
@token_permission_required(Permission.APIView)
|
||||
def api_get_local_logos(departement):
|
||||
dept_id = Departement.from_acronym(departement).id
|
||||
if not g.current_user.has_permission(Permission.ScoChangePreferences, departement):
|
||||
|
@ -80,7 +82,7 @@ def api_get_local_logos(departement):
|
|||
|
||||
|
||||
@bp.route("/departements/<string:departement>/logos/<string:logoname>", methods=["GET"])
|
||||
@token_auth.login_required
|
||||
@token_permission_required(Permission.APIView)
|
||||
def api_get_local_logo(departement, logoname):
|
||||
# format = requested_format("jpg", ['png', 'jpg']) XXX ?
|
||||
dept_id = Departement.from_acronym(departement).id
|
||||
|
|
|
@ -0,0 +1,150 @@
|
|||
############################################### Partitions ############################################################
|
||||
from flask import jsonify
|
||||
|
||||
from app import models
|
||||
from app.api import bp
|
||||
|
||||
from app.api.errors import error_response
|
||||
from app.api.auth import token_permission_required
|
||||
from app.scodoc.sco_groups import get_group_members, setGroups, get_partitions_list
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
|
||||
@bp.route("/partitions/<int:formsemestre_id>", methods=["GET"])
|
||||
@token_permission_required(Permission.APIView)
|
||||
def partition(formsemestre_id: int):
|
||||
"""
|
||||
Retourne la liste de toutes les partitions d'un formsemestre
|
||||
|
||||
formsemestre_id : l'id d'un formsemestre
|
||||
|
||||
Exemple de résultat :
|
||||
[
|
||||
{
|
||||
"partition_id": 2,
|
||||
"id": 2,
|
||||
"formsemestre_id": 1,
|
||||
"partition_name": "TD",
|
||||
"numero": 1,
|
||||
"bul_show_rank": false,
|
||||
"show_in_lists": true
|
||||
},
|
||||
{
|
||||
"partition_id": 1,
|
||||
"id": 1,
|
||||
"formsemestre_id": 1,
|
||||
"partition_name": null,
|
||||
"numero": 0,
|
||||
"bul_show_rank": false,
|
||||
"show_in_lists": true
|
||||
}
|
||||
]
|
||||
"""
|
||||
# # Récupération de toutes les partitions
|
||||
# partitions = models.Partition.query.filter_by(id=formsemestre_id)
|
||||
#
|
||||
# # Mise en forme des données
|
||||
# data = [partition.to_dict() for partition in partitions]
|
||||
|
||||
data = get_partitions_list(formsemestre_id)
|
||||
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
@bp.route("/partitions/groups/<int:group_id>", methods=["GET"])
|
||||
@bp.route("/partitions/groups/<int:group_id>/etat/<string:etat>", methods=["GET"])
|
||||
@token_permission_required(Permission.APIView)
|
||||
def etud_in_group(group_id: int, etat=None):
|
||||
"""
|
||||
Retourne la liste des étudiants dans un groupe
|
||||
|
||||
group_id : l'id d'un groupe
|
||||
etat : état de l'inscription
|
||||
|
||||
Exemple de résultat :
|
||||
[
|
||||
{
|
||||
"etudid": 10,
|
||||
"id": 10,
|
||||
"dept_id": 1,
|
||||
"nom": "BOUTET",
|
||||
"prenom": "Marguerite",
|
||||
"nom_usuel": "",
|
||||
"civilite": "F",
|
||||
"date_naissance": null,
|
||||
"lieu_naissance": null,
|
||||
"dept_naissance": null,
|
||||
"nationalite": null,
|
||||
"statut": null,
|
||||
"boursier": null,
|
||||
"photo_filename": null,
|
||||
"code_nip": "10",
|
||||
"code_ine": "10",
|
||||
"scodoc7_id": null,
|
||||
"email": "MARGUERITE.BOUTET@example.com",
|
||||
"emailperso": null,
|
||||
"domicile": null,
|
||||
"codepostaldomicile": null,
|
||||
"villedomicile": null,
|
||||
"paysdomicile": null,
|
||||
"telephone": null,
|
||||
"telephonemobile": null,
|
||||
"fax": null,
|
||||
"typeadresse": "domicile",
|
||||
"description": null,
|
||||
"group_id": 1,
|
||||
"etat": "I",
|
||||
"civilite_str": "Mme",
|
||||
"nom_disp": "BOUTET",
|
||||
"nomprenom": "Mme Marguerite BOUTET",
|
||||
"ne": "e",
|
||||
"email_default": "MARGUERITE.BOUTET@example.com"
|
||||
},
|
||||
...
|
||||
]
|
||||
"""
|
||||
# Fonction utilisée : app.scodoc.sco_groups.get_group_members()
|
||||
|
||||
if etat is None:
|
||||
data = get_group_members(group_id)
|
||||
else:
|
||||
data = get_group_members(group_id, etat)
|
||||
|
||||
if len(data) == 0:
|
||||
return error_response(
|
||||
409,
|
||||
message="La requête ne peut être traitée en l’état actuel. \n"
|
||||
"Aucun groupe ne correspond au 'group_id' renseigné",
|
||||
)
|
||||
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/partitions/set_groups?partition_id=<int:partition_id>&groups_lists=<int:groups_lists>&"
|
||||
"groups_to_create=<int:groups_to_create>&groups_to_delete=<int:groups_to_delete>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@token_permission_required(Permission.APIEtudChangeGroups)
|
||||
def set_groups(
|
||||
partition_id: int, groups_lists: int, groups_to_delete: int, groups_to_create: int
|
||||
):
|
||||
"""
|
||||
Set les groups
|
||||
|
||||
partition_id : l'id d'une partition
|
||||
groups_lists : membres de chaque groupe existant
|
||||
groups_ti_delete : les groupes à supprimer
|
||||
groups_to_create : les groupes à créer
|
||||
"""
|
||||
# Fonction utilisée : app.scodoc.sco_groups.setGroups()
|
||||
try:
|
||||
# Utilisation de la fonction setGroups
|
||||
setGroups(partition_id, groups_lists, groups_to_create, groups_to_delete)
|
||||
return error_response(200, message="Groups set")
|
||||
except ValueError:
|
||||
return error_response(
|
||||
409,
|
||||
message="La requête ne peut être traitée en l’état actuel. \n"
|
||||
"Veillez vérifier la conformité des éléments passé en paramètres",
|
||||
)
|
|
@ -0,0 +1,253 @@
|
|||
# @bp.route("/etudiants", methods=["GET"])
|
||||
# @token_permission_required(Permission.APIView)
|
||||
# def etudiants():
|
||||
# """
|
||||
# Retourne la liste de tous les étudiants
|
||||
#
|
||||
# Exemple de résultat :
|
||||
# {
|
||||
# "civilite": "X",
|
||||
# "code_ine": null,
|
||||
# "code_nip": null,
|
||||
# "date_naissance": null,
|
||||
# "email": null,
|
||||
# "emailperso": null,
|
||||
# "etudid": 18,
|
||||
# "nom": "MOREL",
|
||||
# "prenom": "JACQUES"
|
||||
# },
|
||||
# {
|
||||
# "civilite": "X",
|
||||
# "code_ine": null,
|
||||
# "code_nip": null,
|
||||
# "date_naissance": null,
|
||||
# "email": null,
|
||||
# "emailperso": null,
|
||||
# "etudid": 19,
|
||||
# "nom": "FOURNIER",
|
||||
# "prenom": "ANNE"
|
||||
# },
|
||||
# ...
|
||||
# """
|
||||
# # Récupération de tous les étudiants
|
||||
# etu = models.Identite.query.all()
|
||||
#
|
||||
# # Mise en forme des données
|
||||
# data = [d.to_dict_bul(include_urls=False) for d in etu]
|
||||
#
|
||||
# return jsonify(data)
|
||||
|
||||
|
||||
# @bp.route(
|
||||
# "/evaluations/eval_set_notes?eval_id=<int:eval_id>&etudid=<int:etudid>¬e=<float:note>",
|
||||
# methods=["POST"],
|
||||
# )
|
||||
# @bp.route(
|
||||
# "/evaluations/eval_set_notes?eval_id=<int:eval_id>&nip=<int:nip>¬e=<float:note>",
|
||||
# methods=["POST"],
|
||||
# )
|
||||
# @bp.route(
|
||||
# "/evaluations/eval_set_notes?eval_id=<int:eval_id>&ine=<int:ine>¬e=<float:note>",
|
||||
# methods=["POST"],
|
||||
# )
|
||||
# @token_permission_required(Permission.APIEditAllNotes)
|
||||
# def evaluation_set_notes(
|
||||
# eval_id: int, note: float, etudid: int = None, nip: int = None, ine: int = None
|
||||
# ):
|
||||
# """
|
||||
# Set les notes d'une évaluation pour un étudiant donnée
|
||||
#
|
||||
# eval_id : l'id d'une évaluation
|
||||
# note : la note à attribuer
|
||||
# etudid : l'etudid d'un étudiant
|
||||
# nip : le code nip d'un étudiant
|
||||
# ine : le code ine d'un étudiant
|
||||
# """
|
||||
# # Fonction utilisée : app.scodoc.sco_saisie_notes.notes_add()
|
||||
#
|
||||
# # Qu'est ce qu'un user ???
|
||||
# # notes_add()
|
||||
# return error_response(501, message="Not implemented")
|
||||
|
||||
|
||||
# ### Inutil en définitif ###
|
||||
# @bp.route(
|
||||
# "/absences/abs_signale?etudid=<int:etudid>&date=<string:date>&matin=<string:matin>&justif=<string:justif>"
|
||||
# "&description=<string:description>",
|
||||
# methods=["POST"],
|
||||
# )
|
||||
# @bp.route(
|
||||
# "/absences/abs_signale?nip=<int:nip>&date=<string:date>&matin=<string:matin>&justif=<string:justif>"
|
||||
# "&description=<string:description>",
|
||||
# methods=["POST"],
|
||||
# )
|
||||
# @bp.route(
|
||||
# "/absences/abs_signale?ine=<int:ine>&date=<string:date>&matin=<string:matin>&justif=<string:justif>"
|
||||
# "&description=<string:description>",
|
||||
# methods=["POST"],
|
||||
# )
|
||||
# @bp.route(
|
||||
# "/absences/abs_signale?ine=<int:ine>&date=<string:date>&matin=<string:matin>&justif=<string:justif>"
|
||||
# "&description=<string:description>&moduleimpl_id=<int:moduleimpl_id>",
|
||||
# methods=["POST"],
|
||||
# )
|
||||
# @token_permission_required(Permission.APIAbsChange)
|
||||
# def abs_signale(
|
||||
# date: datetime,
|
||||
# matin: bool,
|
||||
# justif: bool,
|
||||
# etudid: int = None,
|
||||
# nip: int = None,
|
||||
# ine: int = None, ### Inutil en définitif
|
||||
# description: str = None,
|
||||
# moduleimpl_id: int = None,
|
||||
# ):
|
||||
# """
|
||||
# Permet d'ajouter une absence en base
|
||||
#
|
||||
# date : la date de l'absence
|
||||
# matin : True ou False
|
||||
# justif : True ou False
|
||||
# etudid : l'etudid d'un étudiant
|
||||
# nip: le code nip d'un étudiant
|
||||
# ine : le code ine d'un étudiant
|
||||
# description : description possible à ajouter sur l'absence
|
||||
# moduleimpl_id : l'id d'un moduleimpl
|
||||
# """
|
||||
# # Fonctions utilisées : app.scodoc.sco_abs.add_absence() et app.scodoc.sco_abs.add_justif()
|
||||
#
|
||||
# if etudid is None:
|
||||
# # Récupération de l'étudiant
|
||||
# try:
|
||||
# etu = get_etu_from_request(etudid, nip, ine)
|
||||
# etudid = etu.etudid
|
||||
# except AttributeError:
|
||||
# return error_response(
|
||||
# 409,
|
||||
# message="La requête ne peut être traitée en l’état actuel.\n "
|
||||
# "Veilliez vérifier que l'id de l'étudiant (etudid, nip, ine) est valide",
|
||||
# )
|
||||
# try:
|
||||
# # Utilisation de la fonction add_absence
|
||||
# add_absence(etudid, date, matin, justif, description, moduleimpl_id)
|
||||
# if justif == True:
|
||||
# # Utilisation de la fonction add_justif
|
||||
# add_justif(etudid, date, matin, description)
|
||||
# except ValueError:
|
||||
# return error_response(
|
||||
# 409, message="La requête ne peut être traitée en l’état actuel"
|
||||
# )
|
||||
# @bp.route(
|
||||
# "/absences/abs_annule_justif?etudid=<int:etudid>&jour=<string:jour>&matin=<string:matin>",
|
||||
# methods=["POST"],
|
||||
# )
|
||||
# @bp.route(
|
||||
# "/absences/abs_annule_justif?nip=<int:nip>&jour=<string:jour>&matin=<string:matin>",
|
||||
# methods=["POST"],
|
||||
# )
|
||||
# @bp.route(
|
||||
# "/absences/abs_annule_justif?ine=<int:ine>&jour=<string:jour>&matin=<string:matin>",
|
||||
# methods=["POST"],
|
||||
# )
|
||||
# @token_permission_required(Permission.APIAbsChange)
|
||||
# def abs_annule_justif(
|
||||
# jour: datetime, matin: str, etudid: int = None, nip: int = None, ine: int = None
|
||||
# ):
|
||||
# """
|
||||
# Retourne un html
|
||||
|
||||
# jour : la date de l'absence a annulé
|
||||
# matin : True ou False
|
||||
# etudid : l'etudid d'un étudiant
|
||||
# nip: le code nip d'un étudiant
|
||||
# ine : le code ine d'un étudiant
|
||||
# """
|
||||
# # Fonction utilisée : app.scodoc.sco_abs.annule_justif()
|
||||
|
||||
# if etudid is None:
|
||||
# # Récupération de l'étudiant
|
||||
# try:
|
||||
# etu = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine)
|
||||
# etudid = etu.etudid
|
||||
# except AttributeError:
|
||||
# return error_response(
|
||||
# 409,
|
||||
# message="La requête ne peut être traitée en l’état actuel.\n "
|
||||
# "Veilliez vérifier que l'id de l'étudiant (etudid, nip, ine) est valide",
|
||||
# )
|
||||
# try:
|
||||
# # Utilisation de la fonction annule_justif
|
||||
# annule_justif(etudid, jour, matin)
|
||||
# except ValueError:
|
||||
# return error_response(
|
||||
# 409,
|
||||
# message="La requête ne peut être traitée en l’état actuel.\n "
|
||||
# "Veilliez vérifier que le 'jour' et le 'matin' sont valides",
|
||||
# )
|
||||
|
||||
# return error_response(200, message="OK")
|
||||
|
||||
# @bp.route(
|
||||
# "/jury/set_decision/etudid?etudid=<int:etudid>&formsemestre_id=<int:formesemestre_id>"
|
||||
# "&jury=<string:decision_jury>&devenir=<string:devenir_jury>&assiduite=<bool>",
|
||||
# methods=["POST"],
|
||||
# )
|
||||
# @bp.route(
|
||||
# "/jury/set_decision/nip?etudid=<int:etudid>&formsemestre_id=<int:formesemestre_id>"
|
||||
# "&jury=<string:decision_jury>&devenir=<string:devenir_jury>&assiduite=<bool>",
|
||||
# methods=["POST"],
|
||||
# )
|
||||
# @bp.route(
|
||||
# "/jury/set_decision/ine?etudid=<int:etudid>&formsemestre_id=<int:formesemestre_id>"
|
||||
# "&jury=<string:decision_jury>&devenir=<string:devenir_jury>&assiduite=<bool>",
|
||||
# methods=["POST"],
|
||||
# )
|
||||
# # @token_permission_required(Permission.)
|
||||
# def set_decision_jury(
|
||||
# formsemestre_id: int,
|
||||
# decision_jury: str,
|
||||
# devenir_jury: str,
|
||||
# assiduite: bool,
|
||||
# etudid: int = None,
|
||||
# nip: int = None,
|
||||
# ine: int = None,
|
||||
# ):
|
||||
# """
|
||||
# Attribuer la décision du jury et le devenir à un etudiant
|
||||
#
|
||||
# formsemestre_id : l'id d'un formsemestre
|
||||
# decision_jury : la décision du jury
|
||||
# devenir_jury : le devenir du jury
|
||||
# assiduite : True ou False
|
||||
# etudid : l'etudid d'un étudiant
|
||||
# nip: le code nip d'un étudiant
|
||||
# ine : le code ine d'un étudiant
|
||||
# """
|
||||
# return error_response(501, message="Not implemented")
|
||||
#
|
||||
#
|
||||
# @bp.route(
|
||||
# "/jury/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/annule_decision",
|
||||
# methods=["DELETE"],
|
||||
# )
|
||||
# @bp.route(
|
||||
# "/jury/nip/<int:nip>/formsemestre/<int:formsemestre_id>/annule_decision",
|
||||
# methods=["DELETE"],
|
||||
# )
|
||||
# @bp.route(
|
||||
# "/jury/ine/<int:ine>/formsemestre/<int:formsemestre_id>/annule_decision",
|
||||
# methods=["DELETE"],
|
||||
# )
|
||||
# # @token_permission_required(Permission.)
|
||||
# def annule_decision_jury(
|
||||
# formsemestre_id: int, etudid: int = None, nip: int = None, ine: int = None
|
||||
# ):
|
||||
# """
|
||||
# Supprime la déciosion du jury pour un étudiant donné
|
||||
#
|
||||
# formsemestre_id : l'id d'un formsemestre
|
||||
# etudid : l'etudid d'un étudiant
|
||||
# nip: le code nip d'un étudiant
|
||||
# ine : le code ine d'un étudiant
|
||||
# """
|
||||
# return error_response(501, message="Not implemented")
|
|
@ -50,442 +50,103 @@ from app.api.errors import error_response
|
|||
from app import models
|
||||
from app.models import FormSemestre, FormSemestreInscription, Identite
|
||||
from app.models import ApcReferentielCompetences
|
||||
from app.scodoc.sco_abs import (
|
||||
annule_absence,
|
||||
annule_justif,
|
||||
add_absence,
|
||||
add_justif,
|
||||
list_abs_date,
|
||||
)
|
||||
from app.scodoc.sco_bulletins import formsemestre_bulletinetud_dict
|
||||
from app.scodoc.sco_bulletins_json import make_json_formsemestre_bulletinetud
|
||||
from app.scodoc.sco_evaluation_db import do_evaluation_get_all_notes
|
||||
from app.scodoc.sco_formations import formation_export
|
||||
from app.scodoc.sco_formsemestre_inscriptions import (
|
||||
do_formsemestre_inscription_listinscrits,
|
||||
)
|
||||
from app.scodoc.sco_groups import setGroups, get_etud_groups, get_group_members
|
||||
from app.scodoc.sco_logos import list_logos, find_logo, _list_dept_logos
|
||||
from app.scodoc.sco_moduleimpl import moduleimpl_list
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
|
||||
@bp.route("/list_depts", methods=["GET"])
|
||||
@token_auth.login_required
|
||||
def list_depts():
|
||||
depts = models.Departement.query.filter_by(visible=True).all()
|
||||
data = [d.to_dict() for d in depts]
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
@bp.route("/etudiants/courant", methods=["GET"])
|
||||
@token_auth.login_required
|
||||
def etudiants():
|
||||
"""Liste de tous les étudiants actuellement inscrits à un semestre
|
||||
en cours.
|
||||
"""
|
||||
# Vérification de l'accès: permission Observateur sur tous les départements
|
||||
# (c'est un exemple à compléter)
|
||||
if not g.current_user.has_permission(Permission.ScoObservateur, None):
|
||||
return error_response(401, message="accès interdit")
|
||||
|
||||
query = db.session.query(Identite).filter(
|
||||
FormSemestreInscription.formsemestre_id == FormSemestre.id,
|
||||
FormSemestreInscription.etudid == Identite.id,
|
||||
FormSemestre.date_debut <= func.now(),
|
||||
FormSemestre.date_fin >= func.now(),
|
||||
)
|
||||
return jsonify([e.to_dict_bul(include_urls=False) for e in query])
|
||||
|
||||
|
||||
######################## Departements ##################################
|
||||
|
||||
|
||||
@bp.route("/departements", methods=["GET"])
|
||||
@token_auth.login_required
|
||||
def departements():
|
||||
"""
|
||||
Liste des ids de départements
|
||||
"""
|
||||
depts = models.Departement.query.filter_by(visible=True).all()
|
||||
data = [d.id for d in depts]
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
@bp.route("/departements/<string:dept>/etudiants/liste/<int:sem_id>", methods=["GET"])
|
||||
@token_auth.login_required
|
||||
def liste_etudiants(dept, *args, sem_id): # XXX TODO A REVOIR
|
||||
"""
|
||||
Liste des étudiants d'un département
|
||||
"""
|
||||
# Test si le sem_id à été renseigné ou non
|
||||
if sem_id is not None:
|
||||
# Récupération du/des depts
|
||||
list_depts = models.Departement.query.filter(
|
||||
models.Departement.acronym == dept,
|
||||
models.FormSemestre.semestre_id == sem_id,
|
||||
)
|
||||
list_etuds = []
|
||||
for dept in list_depts:
|
||||
# Récupération des étudiants d'un département
|
||||
x = models.Identite.query.filter(models.Identite.dept_id == dept.getId())
|
||||
for y in x:
|
||||
# Ajout des étudiants dans la liste global
|
||||
list_etuds.append(y)
|
||||
else:
|
||||
list_depts = models.Departement.query.filter(
|
||||
models.Departement.acronym == dept,
|
||||
models.FormSemestre.semestre_id == models.Departement.formsemestres,
|
||||
)
|
||||
list_etuds = []
|
||||
for dept in list_depts:
|
||||
x = models.Identite.query.filter(models.Identite.dept_id == dept.getId())
|
||||
for y in x:
|
||||
list_etuds.append(y)
|
||||
|
||||
data = [d.to_dict() for d in list_etuds]
|
||||
# return jsonify(data)
|
||||
return error_response(501, message="Not implemented")
|
||||
|
||||
|
||||
@bp.route("/departements/<string:dept>/semestres_actifs", methods=["GET"])
|
||||
@token_auth.login_required
|
||||
def liste_semestres_actifs(dept): # TODO : changer nom
|
||||
"""
|
||||
Liste des semestres actifs d'un départements donné
|
||||
"""
|
||||
# Récupération de l'id du dept
|
||||
dept_id = models.Departement.query.filter(models.Departement.acronym == dept)
|
||||
# Puis ici récupération du FormSemestre correspondant
|
||||
depts_actifs = models.FormSemestre.query.filter_by(
|
||||
etat=True,
|
||||
dept_id=dept_id,
|
||||
)
|
||||
data = [da.to_dict() for da in depts_actifs]
|
||||
|
||||
# return jsonify(data)
|
||||
return error_response(501, message="Not implemented")
|
||||
|
||||
|
||||
@bp.route("/referentiel_competences/<int:referentiel_competence_id>")
|
||||
@token_auth.login_required
|
||||
def referentiel_competences(referentiel_competence_id):
|
||||
"""
|
||||
Le référentiel de compétences
|
||||
"""
|
||||
ref = ApcReferentielCompetences.query.get_or_404(referentiel_competence_id)
|
||||
return jsonify(ref.to_dict())
|
||||
|
||||
|
||||
####################### Etudiants ##################################
|
||||
|
||||
|
||||
@bp.route("/etudiant/<int:etudid>", methods=["GET"])
|
||||
@token_auth.login_required
|
||||
def etudiant(etudid):
|
||||
"""
|
||||
Un dictionnaire avec les informations de l'étudiant correspondant à l'id passé en paramètres.
|
||||
"""
|
||||
etud: Identite = Identite.query.get_or_404(etudid)
|
||||
return jsonify(etud.to_dict_bul())
|
||||
|
||||
|
||||
@bp.route("/etudiant/<int:etudid>/semestre/<int:sem_id>/bulletin", methods=["GET"])
|
||||
@token_auth.login_required
|
||||
def etudiant_bulletin_semestre(etudid, sem_id):
|
||||
"""
|
||||
Le bulletin d'un étudiant en fonction de son id et d'un semestre donné
|
||||
"""
|
||||
# return jsonify(models.BulAppreciations.query.filter_by(etudid=etudid, formsemestre_id=sem_id))
|
||||
return error_response(501, message="Not implemented")
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/formsemestre/<int:formsemestre_id>/departements/<string:dept>/etudiant/nip/<int:NIP>/releve",
|
||||
methods=["GET"],
|
||||
)
|
||||
@bp.route(
|
||||
"/formsemestre/<int:formsemestre_id>/departements/<string:dept>/etudiant/id/<int:etudid>/releve",
|
||||
methods=["GET"],
|
||||
)
|
||||
@bp.route(
|
||||
"/formsemestre/<int:formsemestre_id>/departements/<string:dept>/etudiant/ine/<int:numScodoc>/releve",
|
||||
methods=["GET"],
|
||||
)
|
||||
@token_auth.login_required
|
||||
def etudiant_bulletin(formsemestre_id, dept, etudid, format="json", *args, size):
|
||||
"""
|
||||
Un bulletin de note
|
||||
"""
|
||||
formsemestres = models.FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
depts = models.Departement.query.filter_by(acronym=dept)
|
||||
etud = ""
|
||||
|
||||
data = []
|
||||
if args[0] == "short":
|
||||
pass
|
||||
elif args[0] == "selectevals":
|
||||
pass
|
||||
elif args[0] == "long":
|
||||
pass
|
||||
else:
|
||||
return "erreur"
|
||||
|
||||
# return jsonify(data)
|
||||
return error_response(501, message="Not implemented")
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/etudiant/<int:etudid>/semestre/<int:formsemestre_id>/groups", methods=["GET"]
|
||||
)
|
||||
@token_auth.login_required
|
||||
def etudiant_groups(etudid: int, formsemestre_id: int):
|
||||
"""
|
||||
Liste des groupes auxquels appartient l'étudiant dans le semestre indiqué
|
||||
"""
|
||||
semestre = models.FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
etudiant = models.Identite.query.filter_by(id=etudid)
|
||||
|
||||
groups = models.Partition.query.filter(
|
||||
models.Partition.formsemestre_id == semestre,
|
||||
models.GroupDescr.etudiants == etudiant,
|
||||
)
|
||||
data = [d.to_dict() for d in groups]
|
||||
# return jsonify(data)
|
||||
|
||||
return error_response(501, message="Not implemented")
|
||||
|
||||
|
||||
#######################" Programmes de formations #########################
|
||||
|
||||
|
||||
@bp.route("/formations", methods=["GET"])
|
||||
@bp.route("/formations/<int:formation_id>", methods=["GET"])
|
||||
@token_auth.login_required
|
||||
def formations(formation_id: int):
|
||||
"""
|
||||
Liste des formations
|
||||
"""
|
||||
formations = models.Formation.query.filter_by(id=formation_id)
|
||||
data = [d.to_dict() for d in formations]
|
||||
# return jsonify(data)
|
||||
|
||||
return error_response(501, message="Not implemented")
|
||||
|
||||
|
||||
@bp.route("/formations/formation_export/<int:formation_id>", methods=["GET"])
|
||||
@token_auth.login_required
|
||||
def formation_export(formation_id: int, export_ids=False):
|
||||
"""
|
||||
La formation, avec UE, matières, modules
|
||||
"""
|
||||
return error_response(501, message="Not implemented")
|
||||
|
||||
|
||||
###################### UE #######################################
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/departements/<string:dept>/formations/programme/<string:sem_id>", methods=["GET"]
|
||||
)
|
||||
@token_auth.login_required
|
||||
def eus(dept: str, sem_id: int):
|
||||
"""
|
||||
Liste des UES, ressources et SAE d'un semestre
|
||||
"""
|
||||
return error_response(501, message="Not implemented")
|
||||
|
||||
|
||||
######## Semestres de formation ###############
|
||||
|
||||
|
||||
@bp.route("/formations/formsemestre/<int:formsemestre_id>", methods=["GET"])
|
||||
@bp.route("/formations/apo/<int:etape_apo>", methods=["GET"])
|
||||
@token_auth.login_required
|
||||
def formsemestre(
|
||||
id: int,
|
||||
):
|
||||
"""
|
||||
Information sur les formsemestres
|
||||
"""
|
||||
return error_response(501, message="Not implemented")
|
||||
|
||||
|
||||
############ Modules de formation ##############
|
||||
|
||||
|
||||
@bp.route("/formations/moduleimpl/<int:moduleimpl_id>", methods=["GET"])
|
||||
@bp.route(
|
||||
"/formations/moduleimpl/<int:moduleimpl_id>/formsemestre/<int:formsemestre_id>",
|
||||
methods=["GET"],
|
||||
)
|
||||
@token_auth.login_required
|
||||
def moduleimpl(id: int):
|
||||
"""
|
||||
Liste de moduleimpl
|
||||
"""
|
||||
return error_response(501, message="Not implemented")
|
||||
|
||||
|
||||
########### Groupes et partitions ###############
|
||||
|
||||
|
||||
@bp.route("/partitions/<int:formsemestre_id>", methods=["GET"])
|
||||
@token_auth.login_required
|
||||
def partition(formsemestre_id: int):
|
||||
"""
|
||||
La liste de toutes les partitions d'un formsemestre
|
||||
"""
|
||||
partitions = models.Partition.query.filter_by(id=formsemestre_id)
|
||||
data = [d.to_dict() for d in partitions]
|
||||
|
||||
# return jsonify(data)
|
||||
return error_response(501, message="Not implemented")
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/partitions/formsemestre/<int:formsemestre_id>/groups/group_ids?with_codes=&all_groups=&etat=",
|
||||
methods=["GET"],
|
||||
)
|
||||
@token_auth.login_required
|
||||
def groups(formsemestre_id: int, group_ids: int):
|
||||
"""
|
||||
Liste des étudiants dans un groupe
|
||||
"""
|
||||
return error_response(501, message="Not implemented")
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/partitions/set_groups?partition_id=<int:partition_id>&groups=<int:groups>&groups_to_delete=<int:groups_to_delete>&groups_to_create=<int:groups_to_create>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@token_auth.login_required
|
||||
def set_groups(
|
||||
partition_id: int, groups: int, groups_to_delete: int, groups_to_create: int
|
||||
):
|
||||
"""
|
||||
Set les groups
|
||||
"""
|
||||
return error_response(501, message="Not implemented")
|
||||
|
||||
|
||||
####### Bulletins de notes ###########
|
||||
|
||||
|
||||
@bp.route("/evaluations/<int:moduleimpl_id>", methods=["GET"])
|
||||
@token_auth.login_required
|
||||
def evaluations(moduleimpl_id: int):
|
||||
"""
|
||||
Liste des évaluations à partir de l'id d'un moduleimpl
|
||||
"""
|
||||
evals = models.Evaluation.query.filter_by(id=moduleimpl_id)
|
||||
data = [d.to_dict() for d in evals]
|
||||
|
||||
# return jsonify(data)
|
||||
return error_response(501, message="Not implemented")
|
||||
|
||||
|
||||
@bp.route("/evaluations/eval_notes/<int:evaluation_id>", methods=["GET"])
|
||||
@token_auth.login_required
|
||||
def evaluation_notes(evaluation_id: int):
|
||||
"""
|
||||
Liste des notes à partir de l'id d'une évaluation donnée
|
||||
"""
|
||||
evals = models.Evaluation.query.filter_by(id=evaluation_id)
|
||||
notes = evals.get_notes()
|
||||
|
||||
data = [d.to_dict() for d in notes]
|
||||
# return jsonify(data)
|
||||
return error_response(501, message="Not implemented")
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/evaluations/eval_set_notes?eval_id=<int:eval_id>&etudid=<int:etudid>¬e=<int:note>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@token_auth.login_required
|
||||
def evaluation_set_notes(eval_id: int, etudid: int, note: float):
|
||||
"""
|
||||
Set les notes d'une évaluation pour un étudiant donnée
|
||||
"""
|
||||
return error_response(501, message="Not implemented")
|
||||
|
||||
|
||||
############## Absences #############
|
||||
|
||||
|
||||
@bp.route("/absences/<int:etudid>", methods=["GET"])
|
||||
@bp.route("/absences/<int:etudid>/abs_just_only", methods=["GET"])
|
||||
def absences(etudid: int):
|
||||
"""
|
||||
Liste des absences d'un étudiant donnée
|
||||
"""
|
||||
abs = models.Absence.query.filter_by(id=etudid)
|
||||
|
||||
data = [d.to_dict() for d in abs]
|
||||
|
||||
# return jsonify(data)
|
||||
return error_response(501, message="Not implemented")
|
||||
|
||||
|
||||
@bp.route("/absences/abs_signale", methods=["POST"])
|
||||
@token_auth.login_required
|
||||
def abs_signale():
|
||||
"""
|
||||
Retourne un html
|
||||
"""
|
||||
return error_response(501, message="Not implemented")
|
||||
|
||||
|
||||
@bp.route("/absences/abs_annule", methods=["POST"])
|
||||
@token_auth.login_required
|
||||
def abs_annule():
|
||||
"""
|
||||
Retourne un html
|
||||
"""
|
||||
return error_response(501, message="Not implemented")
|
||||
|
||||
|
||||
@bp.route("/absences/abs_annule_justif", methods=["POST"])
|
||||
@token_auth.login_required
|
||||
def abs_annule_justif():
|
||||
"""
|
||||
Retourne un html
|
||||
"""
|
||||
return error_response(501, message="Not implemented")
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/absences/abs_group_etat/?group_ids=<int:group_ids>&date_debut=date_debut&date_fin=date_fin",
|
||||
methods=["GET"],
|
||||
)
|
||||
@token_auth.login_required
|
||||
def abs_groupe_etat(
|
||||
group_ids: int, date_debut, date_fin, with_boursier=True, format="html"
|
||||
):
|
||||
"""
|
||||
Liste des absences d'un ou plusieurs groupes entre deux dates
|
||||
"""
|
||||
return error_response(501, message="Not implemented")
|
||||
|
||||
|
||||
################ Logos ################
|
||||
|
||||
|
||||
@bp.route("/logos", methods=["GET"])
|
||||
@token_auth.login_required
|
||||
def liste_logos(format="json"):
|
||||
"""
|
||||
Liste des logos définis pour le site scodoc.
|
||||
"""
|
||||
return error_response(501, message="Not implemented")
|
||||
|
||||
|
||||
@bp.route("/logos/<string:nom>", methods=["GET"])
|
||||
@token_auth.login_required
|
||||
def recup_logo_global(nom: str):
|
||||
"""
|
||||
Retourne l'image au format png ou jpg
|
||||
"""
|
||||
return error_response(501, message="Not implemented")
|
||||
|
||||
|
||||
@bp.route("/departements/<string:dept>/logos", methods=["GET"])
|
||||
@token_auth.login_required
|
||||
def logo_dept(dept: str):
|
||||
"""
|
||||
Liste des logos définis pour le département visé.
|
||||
"""
|
||||
return error_response(501, message="Not implemented")
|
||||
|
||||
|
||||
@bp.route("/departement/<string:dept>/logos/<string:nom>", methods=["GET"])
|
||||
@token_auth.login_required
|
||||
def recup_logo_dept_global(dept: str, nom: str):
|
||||
"""
|
||||
L'image format png ou jpg
|
||||
"""
|
||||
return error_response(501, message="Not implemented")
|
||||
# ###################################################### Logos ##########################################################
|
||||
#
|
||||
# # XXX TODO voir get_logo déjà existant dans app/views/scodoc.py
|
||||
#
|
||||
# @bp.route("/logos", methods=["GET"])
|
||||
# def liste_logos(format="json"):
|
||||
# """
|
||||
# Liste des logos définis pour le site scodoc.
|
||||
# """
|
||||
# # fonction to use : list_logos()
|
||||
# # try:
|
||||
# # res = list_logos()
|
||||
# # except ValueError:
|
||||
# # return error_response(409, message="La requête ne peut être traitée en l’état actuel")
|
||||
# #
|
||||
# # if res is None:
|
||||
# # return error_response(200, message="Aucun logo trouvé correspondant aux informations renseignés")
|
||||
# #
|
||||
# # return res
|
||||
#
|
||||
#
|
||||
#
|
||||
# @bp.route("/logos/<string:logo_name>", methods=["GET"])
|
||||
# def recup_logo_global(logo_name: str):
|
||||
# """
|
||||
# Retourne l'image au format png ou jpg
|
||||
#
|
||||
# logo_name : le nom du logo rechercher
|
||||
# """
|
||||
# # fonction to use find_logo
|
||||
# # try:
|
||||
# # res = find_logo(logo_name)
|
||||
# # except ValueError:
|
||||
# # return error_response(409, message="La requête ne peut être traitée en l’état actuel")
|
||||
# #
|
||||
# # if res is None:
|
||||
# # return error_response(200, message="Aucun logo trouvé correspondant aux informations renseignés")
|
||||
# #
|
||||
# # return res
|
||||
#
|
||||
#
|
||||
# @bp.route("/departements/<string:dept>/logos", methods=["GET"])
|
||||
# def logo_dept(dept: str):
|
||||
# """
|
||||
# Liste des logos définis pour le département visé.
|
||||
#
|
||||
# dept : l'id d'un département
|
||||
# """
|
||||
# # fonction to use: _list_dept_logos
|
||||
# # dept_id = models.Departement.query.filter_by(acronym=dept).first()
|
||||
# # try:
|
||||
# # res = _list_dept_logos(dept_id.id)
|
||||
# # except ValueError:
|
||||
# # return error_response(409, message="La requête ne peut être traitée en l’état actuel")
|
||||
# #
|
||||
# # if res is None:
|
||||
# # return error_response(200, message="Aucun logo trouvé correspondant aux informations renseignés")
|
||||
# #
|
||||
# # return res
|
||||
#
|
||||
#
|
||||
# @bp.route("/departement/<string:dept>/logos/<string:logo_name>", methods=["GET"])
|
||||
# def recup_logo_dept_global(dept: str, logo_name: str):
|
||||
# """
|
||||
# L'image format png ou jpg
|
||||
#
|
||||
# dept : l'id d'un département
|
||||
# logo_name : le nom du logo rechercher
|
||||
# """
|
||||
# # fonction to use find_logo
|
||||
# # dept_id = models.Departement.query.filter_by(acronym=dept).first()
|
||||
# # try:
|
||||
# # res = find_logo(logo_name, dept_id.id)
|
||||
# # except ValueError:
|
||||
# # return error_response(409, message="La requête ne peut être traitée en l’état actuel")
|
||||
# #
|
||||
# # if res is None:
|
||||
# # return error_response(200, message="Aucun logo trouvé correspondant aux informations renseignés")
|
||||
# #
|
||||
# # return res
|
||||
|
|
|
@ -0,0 +1,444 @@
|
|||
################################################## Tests ##############################################################
|
||||
|
||||
|
||||
# XXX OBSOLETE ??? XXX
|
||||
|
||||
import requests
|
||||
import os
|
||||
|
||||
from app import models
|
||||
from app.api import bp, requested_format
|
||||
from app.api.auth import token_auth
|
||||
from app.api.errors import error_response
|
||||
|
||||
SCODOC_USER = "test"
|
||||
SCODOC_PASSWORD = "test"
|
||||
SCODOC_URL = "http://192.168.1.12:5000"
|
||||
CHECK_CERTIFICATE = bool(int(os.environ.get("CHECK_CERTIFICATE", False)))
|
||||
|
||||
HEADERS = None
|
||||
|
||||
|
||||
def get_token():
|
||||
"""
|
||||
Permet de set le token dans le header
|
||||
"""
|
||||
global HEADERS
|
||||
global SCODOC_USER
|
||||
global SCODOC_PASSWORD
|
||||
|
||||
r0 = requests.post(
|
||||
SCODOC_URL + "/ScoDoc/api/tokens", auth=(SCODOC_USER, SCODOC_PASSWORD)
|
||||
)
|
||||
token = r0.json()["token"]
|
||||
HEADERS = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
DEPT = None
|
||||
FORMSEMESTRE = None
|
||||
ETU = None
|
||||
|
||||
|
||||
@bp.route("/test_dept", methods=["GET"])
|
||||
def get_departement():
|
||||
"""
|
||||
Permet de tester departements() mais également de set un département dans DEPT pour la suite des tests
|
||||
"""
|
||||
|
||||
get_token()
|
||||
|
||||
global HEADERS
|
||||
global CHECK_CERTIFICATE
|
||||
global SCODOC_USER
|
||||
global SCODOC_PASSWORD
|
||||
|
||||
# print(HEADERS)
|
||||
# departements
|
||||
r = requests.get(
|
||||
SCODOC_URL + "/ScoDoc/api/departements",
|
||||
headers=HEADERS,
|
||||
verify=CHECK_CERTIFICATE,
|
||||
)
|
||||
|
||||
if r.status_code == 200:
|
||||
dept_id = r.json()[0]
|
||||
# print(dept_id)
|
||||
|
||||
dept = models.Departement.query.filter_by(id=dept_id).first()
|
||||
dept = dept.to_dict()
|
||||
|
||||
fields = ["id", "acronym", "description", "visible", "date_creation"]
|
||||
|
||||
for field in dept:
|
||||
if field not in fields:
|
||||
return error_response(501, field + " field missing")
|
||||
|
||||
global DEPT
|
||||
DEPT = dept
|
||||
|
||||
return error_response(200, "OK")
|
||||
|
||||
return error_response(409, "La requête ne peut être traitée en l’état actuel")
|
||||
|
||||
|
||||
@bp.route("/test_formsemestre", methods=["GET"])
|
||||
def get_formsemestre():
|
||||
"""
|
||||
Permet de tester liste_semestres_courant() mais également de set un formsemestre dans FORMSEMESTRE
|
||||
pour la suite des tests
|
||||
"""
|
||||
get_departement()
|
||||
|
||||
global DEPT
|
||||
dept_acronym = DEPT["acronym"]
|
||||
|
||||
# liste_semestres_courant
|
||||
r = requests.get(
|
||||
SCODOC_URL + "/ScoDoc/api/departements/" + dept_acronym + "/semestres_courants",
|
||||
auth=(SCODOC_USER, SCODOC_PASSWORD),
|
||||
)
|
||||
|
||||
if r.status_code == 200:
|
||||
formsemestre = r.json()[0]
|
||||
print(r.json()[0])
|
||||
|
||||
fields = [
|
||||
"gestion_semestrielle",
|
||||
"titre",
|
||||
"scodoc7_id",
|
||||
"date_debut",
|
||||
"bul_bgcolor",
|
||||
"date_fin",
|
||||
"resp_can_edit",
|
||||
"dept_id",
|
||||
"etat",
|
||||
"resp_can_change_ens",
|
||||
"id",
|
||||
"modalite",
|
||||
"ens_can_edit_eval",
|
||||
"formation_id",
|
||||
"gestion_compensation",
|
||||
"elt_sem_apo",
|
||||
"semestre_id",
|
||||
"bul_hide_xml",
|
||||
"elt_annee_apo",
|
||||
"block_moyennes",
|
||||
"formsemestre_id",
|
||||
"titre_num",
|
||||
"date_debut_iso",
|
||||
"date_fin_iso",
|
||||
"responsables",
|
||||
]
|
||||
|
||||
for field in formsemestre:
|
||||
if field not in fields:
|
||||
return error_response(501, field + " field missing")
|
||||
|
||||
global FORMSEMESTRE
|
||||
FORMSEMESTRE = formsemestre
|
||||
|
||||
return error_response(200, "OK")
|
||||
|
||||
return error_response(409, "La requête ne peut être traitée en l’état actuel")
|
||||
|
||||
|
||||
@bp.route("/test_etu", methods=["GET"])
|
||||
def get_etudiant():
|
||||
"""
|
||||
Permet de tester etudiants() mais également de set un etudiant dans ETU pour la suite des tests
|
||||
"""
|
||||
|
||||
# etudiants
|
||||
r = requests.get(
|
||||
SCODOC_URL + "/ScoDoc/api/etudiants/courant",
|
||||
auth=(SCODOC_USER, SCODOC_PASSWORD),
|
||||
)
|
||||
|
||||
if r.status_code == 200:
|
||||
etu = r.json()[0]
|
||||
|
||||
fields = [
|
||||
"civilite",
|
||||
"code_ine",
|
||||
"code_nip",
|
||||
"date_naissance",
|
||||
"email",
|
||||
"emailperso",
|
||||
"etudid",
|
||||
"nom",
|
||||
"prenom",
|
||||
]
|
||||
|
||||
for field in etu:
|
||||
if field not in fields:
|
||||
return error_response(501, field + " field missing")
|
||||
|
||||
global ETU
|
||||
ETU = etu
|
||||
print(etu)
|
||||
|
||||
return error_response(200, "OK")
|
||||
|
||||
return error_response(409, "La requête ne peut être traitée en l’état actuel")
|
||||
|
||||
|
||||
############################################### Departements ##########################################################
|
||||
|
||||
|
||||
@bp.route("/test_liste_etudiants")
|
||||
def test_departements_liste_etudiants():
|
||||
"""
|
||||
Test la route liste_etudiants
|
||||
"""
|
||||
# Set un département et un formsemestre pour les tests
|
||||
get_departement()
|
||||
get_formsemestre()
|
||||
|
||||
global DEPT
|
||||
global FORMSEMESTRE
|
||||
|
||||
# Set les fields à vérifier
|
||||
fields = [
|
||||
"civilite",
|
||||
"code_ine",
|
||||
"code_nip",
|
||||
"date_naissance",
|
||||
"email",
|
||||
"emailperso",
|
||||
"etudid",
|
||||
"nom",
|
||||
"prenom",
|
||||
]
|
||||
|
||||
# liste_etudiants (sans formsemestre)
|
||||
r1 = requests.get(
|
||||
SCODOC_URL + "/ScoDoc/api/departements/" + DEPT["acronym"] + "/etudiants/liste",
|
||||
auth=(SCODOC_USER, SCODOC_PASSWORD),
|
||||
)
|
||||
|
||||
if r1.status_code == 200: # Si la requête est "OK"
|
||||
# On récupère la liste des étudiants
|
||||
etudiants = r1.json()
|
||||
|
||||
# Vérification que tous les étudiants ont bien tous les bons champs
|
||||
for etu in etudiants:
|
||||
for field in etu:
|
||||
if field not in fields:
|
||||
return error_response(501, field + " field missing")
|
||||
|
||||
# liste_etudiants (avec formsemestre)
|
||||
r2 = requests.get(
|
||||
SCODOC_URL
|
||||
+ "/ScoDoc/api/departements/"
|
||||
+ DEPT["acronym"]
|
||||
+ "/etudiants/liste/"
|
||||
+ str(FORMSEMESTRE["formsemestre_id"]),
|
||||
auth=(SCODOC_USER, SCODOC_PASSWORD),
|
||||
)
|
||||
|
||||
if r2.status_code == 200: # Si la requête est "OK"
|
||||
# On récupère la liste des étudiants
|
||||
etudiants = r2.json()
|
||||
|
||||
# Vérification que tous les étudiants ont bien tous les bons champs
|
||||
for etu in etudiants:
|
||||
for field in etu:
|
||||
if field not in fields:
|
||||
return error_response(501, field + " field missing")
|
||||
|
||||
return error_response(200, "OK")
|
||||
|
||||
return error_response(409, "La requête ne peut être traitée en l’état actuel")
|
||||
|
||||
|
||||
@bp.route("/test_referenciel_competences")
|
||||
def test_departements_referenciel_competences():
|
||||
"""
|
||||
Test la route referenciel_competences
|
||||
"""
|
||||
get_departement()
|
||||
get_formsemestre()
|
||||
|
||||
global DEPT
|
||||
global FORMSEMESTRE
|
||||
|
||||
# referenciel_competences
|
||||
r = requests.post(
|
||||
SCODOC_URL
|
||||
+ "/ScoDoc/api/departements/"
|
||||
+ DEPT["acronym"]
|
||||
+ "/formations/"
|
||||
+ FORMSEMESTRE["formation_id"]
|
||||
+ "/referentiel_competences",
|
||||
auth=(SCODOC_USER, SCODOC_PASSWORD),
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/test_liste_semestre_index")
|
||||
def test_departements_semestre_index():
|
||||
"""
|
||||
Test la route semestre_index
|
||||
"""
|
||||
# semestre_index
|
||||
r5 = requests.post(
|
||||
SCODOC_URL
|
||||
+ "/ScoDoc/api/departements/"
|
||||
+ DEPT["acronym"]
|
||||
+ "/formsemestre/"
|
||||
+ FORMSEMESTRE["formation_id"]
|
||||
+ "/programme",
|
||||
auth=(SCODOC_USER, SCODOC_PASSWORD),
|
||||
)
|
||||
|
||||
|
||||
#################################################### Etudiants ########################################################
|
||||
|
||||
|
||||
def test_routes_etudiants():
|
||||
"""
|
||||
Test les routes de la partie Etudiants
|
||||
"""
|
||||
# etudiants
|
||||
r1 = requests.get(
|
||||
SCODOC_URL + "/ScoDoc/api/etudiants", auth=(SCODOC_USER, SCODOC_PASSWORD)
|
||||
)
|
||||
|
||||
# etudiants_courant
|
||||
r2 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
# etudiant
|
||||
r3 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
# etudiant_formsemestres
|
||||
r4 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
# etudiant_bulletin_semestre
|
||||
r5 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
# etudiant_groups
|
||||
r6 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
|
||||
def test_routes_formation():
|
||||
"""
|
||||
Test les routes de la partie Formation
|
||||
"""
|
||||
# formations
|
||||
r1 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
# formations_by_id
|
||||
r2 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
# formation_export_by_formation_id
|
||||
r3 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
# formsemestre_apo
|
||||
r4 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
# moduleimpls
|
||||
r5 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
# moduleimpls_sem
|
||||
r6 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
|
||||
def test_routes_formsemestres():
|
||||
"""
|
||||
Test les routes de la partie Formsemestres
|
||||
"""
|
||||
# formsemestre
|
||||
r1 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
# etudiant_bulletin
|
||||
r2 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
# bulletins
|
||||
r3 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
# jury
|
||||
r4 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
|
||||
def test_routes_partitions():
|
||||
"""
|
||||
Test les routes de la partie Partitions
|
||||
"""
|
||||
# partition
|
||||
r1 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
# etud_in_group
|
||||
r2 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
# set_groups
|
||||
r3 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
|
||||
def test_routes_evaluations():
|
||||
"""
|
||||
Test les routes de la partie Evaluations
|
||||
"""
|
||||
# evaluations
|
||||
r1 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
# evaluation_notes
|
||||
r2 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
# evaluation_set_notes
|
||||
r3 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
|
||||
def test_routes_jury():
|
||||
"""
|
||||
Test les routes de la partie Jury
|
||||
"""
|
||||
# jury_preparation
|
||||
r1 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
# jury_decisions
|
||||
r2 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
# set_decision_jury
|
||||
r3 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
# annule_decision_jury
|
||||
r4 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
|
||||
def test_routes_absences():
|
||||
"""
|
||||
Test les routes de la partie Absences
|
||||
"""
|
||||
# absences
|
||||
r1 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
# absences_justify
|
||||
r2 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
# abs_signale
|
||||
r3 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
# abs_annule
|
||||
r4 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
# abs_annule_justif
|
||||
r5 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
# abs_groupe_etat
|
||||
r6 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
|
||||
def test_routes_logos():
|
||||
"""
|
||||
Test les routes de la partie Logos
|
||||
"""
|
||||
# liste_logos
|
||||
r1 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
# recup_logo_global
|
||||
r2 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
# logo_dept
|
||||
r3 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
||||
|
||||
# recup_logo_dept_global
|
||||
r4 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD))
|
|
@ -0,0 +1,22 @@
|
|||
from app import models
|
||||
|
||||
|
||||
def get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine):
|
||||
"""
|
||||
Fonction qui retourne un etudiant en fonction de l'etudid, code nip et code ine rentré en paramètres
|
||||
|
||||
etudid : None ou un int etudid
|
||||
nip : None ou un int code_nip
|
||||
ine : None ou un int code_ine
|
||||
|
||||
Exemple de résultat: <Itendite>
|
||||
"""
|
||||
if etudid is None:
|
||||
if nip is None: # si ine
|
||||
etud = models.Identite.query.filter_by(code_ine=str(ine)).first()
|
||||
else: # si nip
|
||||
etud = models.Identite.query.filter_by(code_nip=str(nip)).first()
|
||||
else: # si etudid
|
||||
etud = models.Identite.query.filter_by(id=etudid).first()
|
||||
|
||||
return etud
|
|
@ -0,0 +1 @@
|
|||
# empty but required for pylint
|
|
@ -374,7 +374,7 @@ class BulletinBUT:
|
|||
d["filigranne"] = sco_bulletins_pdf.get_filigranne(
|
||||
etud_etat,
|
||||
self.prefs,
|
||||
decision_sem=d["semestre"].get("decision_sem"),
|
||||
decision_sem=d["semestre"].get("decision"),
|
||||
)
|
||||
if etud_etat == scu.DEMISSION:
|
||||
d["demission"] = "(Démission)"
|
||||
|
|
|
@ -21,7 +21,8 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
|||
self.infos est le dict issu de BulletinBUT.bulletin_etud_complet()
|
||||
"""
|
||||
|
||||
list_in_menu = False # spécialisation du BulletinGeneratorStandard, ne pas présenter à l'utilisateur
|
||||
# spécialisation du BulletinGeneratorStandard, ne pas présenter à l'utilisateur:
|
||||
list_in_menu = False
|
||||
scale_table_in_page = False # pas de mise à l'échelle pleine page auto
|
||||
multi_pages = True # plusieurs pages par bulletins
|
||||
small_fontsize = "8"
|
||||
|
@ -78,7 +79,8 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
|||
"coef": 2 * cm,
|
||||
}
|
||||
title_bg = tuple(x / 255.0 for x in title_bg)
|
||||
nota_bene = "La moyenne des ressources et SAÉs dans une UE dépend des poids donnés aux évaluations."
|
||||
nota_bene = """La moyenne des ressources et SAÉs dans une UE
|
||||
dépend des poids donnés aux évaluations."""
|
||||
# elems pour générer table avec gen_table (liste de dicts)
|
||||
rows = [
|
||||
# Ligne de titres
|
||||
|
@ -130,7 +132,9 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
|||
t = {
|
||||
"titre": f"{ue_acronym} - {ue['titre']}",
|
||||
"moyenne": Paragraph(
|
||||
f"""<para align=right><b>{moy_ue.get("value", "-") if moy_ue is not None else "-"}</b></para>"""
|
||||
f"""<para align=right><b>{moy_ue.get("value", "-")
|
||||
if moy_ue is not None else "-"
|
||||
}</b></para>"""
|
||||
),
|
||||
"_css_row_class": "note_bold",
|
||||
"_pdf_row_markup": ["b"],
|
||||
|
@ -331,7 +335,8 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
|||
col_idx = 1 # 1ere col. poids
|
||||
for ue_acro in ue_acros:
|
||||
t[ue_acro] = Paragraph(
|
||||
f"""<para align=right fontSize={self.small_fontsize}><i>{e["poids"].get(ue_acro, "") or ""}</i></para>"""
|
||||
f"""<para align=right fontSize={self.small_fontsize}><i>{
|
||||
e["poids"].get(ue_acro, "") or ""}</i></para>"""
|
||||
)
|
||||
t["_pdf_style"].append(
|
||||
(
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
# empty but required for pylint
|
|
@ -228,7 +228,7 @@ class BonusSportAdditif(BonusSport):
|
|||
axis=1,
|
||||
)
|
||||
# Seuil: bonus dans [min, max] (défaut [0,20])
|
||||
bonus_max = self.bonus_max or 0.0
|
||||
bonus_max = self.bonus_max or 20.0
|
||||
np.clip(bonus_moy_arr, self.bonus_min, bonus_max, out=bonus_moy_arr)
|
||||
|
||||
self.bonus_additif(bonus_moy_arr)
|
||||
|
@ -418,17 +418,46 @@ class BonusAmiens(BonusSportAdditif):
|
|||
|
||||
|
||||
class BonusBethune(BonusSportMultiplicatif):
|
||||
"""Calcul bonus modules optionnels (sport), règle IUT de Béthune.
|
||||
|
||||
Les points au dessus de la moyenne de 10 apportent un bonus pour le semestre.
|
||||
Ce bonus est égal au nombre de points divisé par 200 et multiplié par la
|
||||
moyenne générale du semestre de l'étudiant.
|
||||
"""
|
||||
Calcul bonus modules optionnels (sport, culture), règle IUT de Béthune.
|
||||
<p>
|
||||
<b>Pour le BUT :</b>
|
||||
La note de sport est sur 20, et on calcule une bonification (en %)
|
||||
qui va s'appliquer à <b>la moyenne de chaque UE</b> du semestre en appliquant
|
||||
la formule : bonification (en %) = max(note-10, 0)*(1/<b>500</b>).
|
||||
</p><p>
|
||||
<em>La bonification ne s'applique que si la note est supérieure à 10.</em>
|
||||
</p><p>
|
||||
(Une note de 10 donne donc 0% de bonif,
|
||||
1 point au dessus de 10 augmente la moyenne des UE de 0.2%)
|
||||
</p>
|
||||
<p>
|
||||
<b>Pour le DUT/LP :</b>
|
||||
La note de sport est sur 20, et on calcule une bonification (en %)
|
||||
qui va s'appliquer à <b>la moyenne générale</b> du semestre en appliquant
|
||||
la formule : bonification (en %) = max(note-10, 0)*(1/<b>200</b>).
|
||||
</p><p>
|
||||
<em>La bonification ne s'applique que si la note est supérieure à 10.</em>
|
||||
</p><p>
|
||||
(Une note de 10 donne donc 0% de bonif,
|
||||
1 point au dessus de 10 augmente la moyenne des UE de 0.5%)
|
||||
</p>
|
||||
"""
|
||||
|
||||
name = "bonus_iutbethune"
|
||||
displayed_name = "IUT de Béthune"
|
||||
seuil_moy_gen = 10.0
|
||||
amplitude = 0.005
|
||||
seuil_moy_gen = 10.0 # points comptés au dessus de 10.
|
||||
|
||||
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||||
"""calcul du bonus"""
|
||||
if self.formsemestre.formation.is_apc():
|
||||
self.amplitude = 0.002
|
||||
else:
|
||||
self.amplitude = 0.005
|
||||
|
||||
return super().compute_bonus(
|
||||
sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan
|
||||
)
|
||||
|
||||
|
||||
class BonusBezier(BonusSportAdditif):
|
||||
|
@ -502,10 +531,11 @@ class BonusCachan1(BonusSportAdditif):
|
|||
|
||||
<ul>
|
||||
<li> DUT/LP : la meilleure note d'option, si elle est supérieure à 10,
|
||||
bonifie les moyennes d'UE (<b>sauf l'UE41 dont le code est UE41_E</b>) à raison
|
||||
bonifie les moyennes d'UE (uniquement UE13_E pour le semestre 1, UE23_E
|
||||
pour le semestre 2, UE33_E pour le semestre 3 et UE43_E pour le semestre
|
||||
4) à raison
|
||||
de <em>bonus = (option - 10)/10</em>.
|
||||
</li>
|
||||
|
||||
<li> BUT : la meilleure note d'option, si elle est supérieure à 10, bonifie
|
||||
les moyennes d'UE à raison de <em>bonus = (option - 10) * 3%</em>.</li>
|
||||
</ul>
|
||||
|
@ -516,6 +546,7 @@ class BonusCachan1(BonusSportAdditif):
|
|||
seuil_moy_gen = 10.0 # tous les points sont comptés
|
||||
proportion_point = 0.03
|
||||
classic_use_bonus_ues = True
|
||||
ues_bonifiables_cachan = {"UE13_E", "UE23_E", "UE33_E", "UE43_E"}
|
||||
|
||||
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||||
"""calcul du bonus, avec réglage différent suivant le type de formation"""
|
||||
|
@ -540,7 +571,7 @@ class BonusCachan1(BonusSportAdditif):
|
|||
dtype=float,
|
||||
)
|
||||
else: # --- DUT
|
||||
# pareil mais proportion différente et exclusion d'une UE
|
||||
# pareil mais proportion différente et application à certaines UEs
|
||||
proportion_point = 0.1
|
||||
bonus_moy_arr = np.where(
|
||||
note_bonus_max > self.seuil_moy_gen,
|
||||
|
@ -553,10 +584,10 @@ class BonusCachan1(BonusSportAdditif):
|
|||
columns=ues_idx,
|
||||
dtype=float,
|
||||
)
|
||||
# Pas de bonus sur la ou les ue de code "UE41_E"
|
||||
ue_exclues = [ue for ue in ues if ue.ue_code == "UE41_E"]
|
||||
for ue in ue_exclues:
|
||||
self.bonus_ues[ue.id] = 0.0
|
||||
# Applique bonus seulement sur certaines UE de code connu:
|
||||
for ue in ues:
|
||||
if ue.ue_code not in self.ues_bonifiables_cachan:
|
||||
self.bonus_ues[ue.id] = 0.0 # annule
|
||||
|
||||
|
||||
class BonusCalais(BonusSportAdditif):
|
||||
|
@ -676,17 +707,40 @@ class BonusLaRochelle(BonusSportAdditif):
|
|||
proportion_point = 0.01 # 1%
|
||||
|
||||
|
||||
class BonusLeHavre(BonusSportMultiplicatif):
|
||||
"""Bonus sport IUT du Havre sur moyenne générale et UE
|
||||
class BonusLeHavre(BonusSportAdditif):
|
||||
"""Bonus sport IUT du Havre sur les moyennes d'UE
|
||||
|
||||
Les points des modules bonus au dessus de 10/20 sont ajoutés,
|
||||
et les moyennes d'UE augmentées de 5% de ces points.
|
||||
<p>
|
||||
Les enseignements optionnels de langue, préprofessionnalisation,
|
||||
PIX (compétences numériques), l'entrepreneuriat étudiant, l'engagement
|
||||
bénévole au sein d’association dès lors qu’une grille d’évaluation des
|
||||
compétences existe ainsi que les activités sportives et culturelles
|
||||
seront traités au niveau semestriel.
|
||||
</p><p>
|
||||
Le maximum de bonification qu’un étudiant peut obtenir sur sa moyenne
|
||||
est plafonné à 0.5 point.
|
||||
</p><p>
|
||||
Lorsqu’un étudiant suit plus de deux matières qui donnent droit à
|
||||
bonification, l’étudiant choisit les deux notes à retenir.
|
||||
</p><p>
|
||||
Les points bonus ne sont acquis que pour une note supérieure à 10/20.
|
||||
</p><p>
|
||||
La bonification est calculée de la manière suivante :<br>
|
||||
|
||||
Pour chaque matière (max. 2) donnant lieu à bonification :<br>
|
||||
|
||||
Bonification = (N-10) x 0,05,
|
||||
N étant la note de l’activité sur 20.
|
||||
</p>
|
||||
"""
|
||||
|
||||
# note: ScoDoc ne vérifie pas que le nombre de modules avec inscription n'excède pas 2
|
||||
name = "bonus_iutlh"
|
||||
displayed_name = "IUT du Havre"
|
||||
classic_use_bonus_ues = True # sur les UE, même en DUT et LP
|
||||
seuil_moy_gen = 10.0 # seuls les points au dessus du seuil sont comptés
|
||||
amplitude = 0.005 # multiplie les points au dessus du seuil
|
||||
proportion_point = 0.05
|
||||
bonus_max = 0.5 #
|
||||
|
||||
|
||||
class BonusLeMans(BonusSportAdditif):
|
||||
|
@ -900,6 +954,19 @@ class BonusStBrieuc(BonusSportAdditif):
|
|||
super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
|
||||
|
||||
|
||||
class BonusStEtienne(BonusSportAdditif):
|
||||
"""IUT de Saint-Etienne.
|
||||
|
||||
Le bonus est compris entre 0 et 0.6 points.
|
||||
"""
|
||||
|
||||
name = "bonus_iutse"
|
||||
displayed_name = "IUT de Saint-Etienne"
|
||||
seuil_moy_gen = 0.0
|
||||
bonus_max = 0.6 # plafonnement à 0.6 points
|
||||
proportion_point = 1
|
||||
|
||||
|
||||
class BonusStDenis(BonusSportAdditif):
|
||||
"""Calcul bonus modules optionnels (sport, culture), règle IUT Saint-Denis
|
||||
|
||||
|
@ -946,7 +1013,7 @@ class BonusTarbes(BonusSportAdditif):
|
|||
"""
|
||||
|
||||
name = "bonus_tarbes"
|
||||
displayed_name = "IUT de Tazrbes"
|
||||
displayed_name = "IUT de Tarbes"
|
||||
seuil_moy_gen = 10.0
|
||||
proportion_point = 1 / 30.0
|
||||
classic_use_bonus_ues = True
|
||||
|
@ -1005,6 +1072,29 @@ class BonusTours(BonusDirect):
|
|||
)
|
||||
|
||||
|
||||
class BonusIUTvannes(BonusSportAdditif):
|
||||
"""Calcul bonus modules optionels (sport, culture), règle IUT Vannes
|
||||
|
||||
<p><b>Ne concerne actuellement que les DUT et LP</b></p>
|
||||
<p>Les étudiants de l'IUT peuvent suivre des enseignements optionnels
|
||||
de l'U.B.S. (sports, musique, deuxième langue, culture, etc) non
|
||||
rattachés à une unité d'enseignement.
|
||||
</p><p>
|
||||
Les points au-dessus de 10 sur 20 obtenus dans chacune des matières
|
||||
optionnelles sont cumulés.
|
||||
</p><p>
|
||||
3% de ces points cumulés s'ajoutent à la moyenne générale du semestre
|
||||
déjà obtenue par l'étudiant.
|
||||
</p>
|
||||
"""
|
||||
|
||||
name = "bonus_iutvannes"
|
||||
displayed_name = "IUT de Vannes"
|
||||
seuil_moy_gen = 10.0
|
||||
proportion_point = 0.03 # 3%
|
||||
classic_use_bonus_ues = False # seulement sur moy gen.
|
||||
|
||||
|
||||
class BonusVilleAvray(BonusSport):
|
||||
"""Bonus modules optionnels (sport, culture), règle IUT Ville d'Avray.
|
||||
|
||||
|
|
|
@ -3,11 +3,9 @@
|
|||
|
||||
"""Matrices d'inscription aux modules d'un semestre
|
||||
"""
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from app import db
|
||||
from app import models
|
||||
|
||||
#
|
||||
# Le chargement des inscriptions est long: matrice nb_module x nb_etuds
|
||||
|
@ -36,7 +34,7 @@ def df_load_modimpl_inscr(formsemestre) -> pd.DataFrame:
|
|||
)
|
||||
df = df.merge(ins_df, how="left", left_index=True, right_index=True)
|
||||
# Force columns names to integers (moduleimpl ids)
|
||||
df.columns = pd.Int64Index([int(x) for x in df.columns], dtype="int")
|
||||
df.columns = pd.Index([int(x) for x in df.columns], dtype=int)
|
||||
# les colonnes de df sont en float (Nan) quand il n'y a
|
||||
# aucun inscrit au module.
|
||||
df.fillna(0, inplace=True) # les non-inscrits
|
||||
|
|
|
@ -16,7 +16,7 @@ from app.scodoc import sco_codes_parcours
|
|||
|
||||
|
||||
class ValidationsSemestre(ResultatsCache):
|
||||
""" """
|
||||
"""Les décisions de jury pour un semestre"""
|
||||
|
||||
_cached_attrs = (
|
||||
"decisions_jury",
|
||||
|
|
|
@ -92,6 +92,8 @@ class ModuleImplResults:
|
|||
ou NaN si les évaluations (dans lesquelles l'étudiant a des notes)
|
||||
ne donnent pas de coef vers cette UE.
|
||||
"""
|
||||
self.evals_etudids_sans_note = {}
|
||||
"""dict: evaluation_id : set des etudids non notés dans cette eval, sans les démissions."""
|
||||
self.load_notes()
|
||||
self.etuds_use_session2 = pd.Series(False, index=self.evals_notes.index)
|
||||
"""1 bool par etud, indique si sa moyenne de module vient de la session2"""
|
||||
|
@ -142,12 +144,13 @@ class ModuleImplResults:
|
|||
# ou évaluation déclarée "à prise en compte immédiate"
|
||||
# Les évaluations de rattrapage et 2eme session sont toujours incomplètes
|
||||
# car on calcule leur moyenne à part.
|
||||
etudids_sans_note = inscrits_module - set(eval_df.index) # sans les dem.
|
||||
is_complete = (evaluation.evaluation_type == scu.EVALUATION_NORMALE) and (
|
||||
evaluation.publish_incomplete
|
||||
or (not (inscrits_module - set(eval_df.index)))
|
||||
evaluation.publish_incomplete or (not etudids_sans_note)
|
||||
)
|
||||
self.evaluations_completes.append(is_complete)
|
||||
self.evaluations_completes_dict[evaluation.id] = is_complete
|
||||
self.evals_etudids_sans_note[evaluation.id] = etudids_sans_note
|
||||
|
||||
# NULL en base => ABS (= -999)
|
||||
eval_df.fillna(scu.NOTES_ABSENCE, inplace=True)
|
||||
|
@ -166,9 +169,7 @@ class ModuleImplResults:
|
|||
self.en_attente = True
|
||||
|
||||
# Force columns names to integers (evaluation ids)
|
||||
evals_notes.columns = pd.Int64Index(
|
||||
[int(x) for x in evals_notes.columns], dtype="int"
|
||||
)
|
||||
evals_notes.columns = pd.Index([int(x) for x in evals_notes.columns], dtype=int)
|
||||
self.evals_notes = evals_notes
|
||||
|
||||
def _load_evaluation_notes(self, evaluation: Evaluation) -> pd.DataFrame:
|
||||
|
@ -193,7 +194,9 @@ class ModuleImplResults:
|
|||
return eval_df
|
||||
|
||||
def _etudids(self):
|
||||
"""L'index du dataframe est la liste de tous les étudiants inscrits au semestre"""
|
||||
"""L'index du dataframe est la liste de tous les étudiants inscrits au semestre
|
||||
(incluant les DEM et DEF)
|
||||
"""
|
||||
return [
|
||||
inscr.etudid
|
||||
for inscr in ModuleImpl.query.get(
|
||||
|
|
|
@ -100,8 +100,9 @@ def comp_ranks_series(notes: pd.Series) -> (pd.Series, pd.Series):
|
|||
if (notes is None) or (len(notes) == 0):
|
||||
return (pd.Series([], dtype=object), pd.Series([], dtype=int))
|
||||
notes = notes.sort_values(ascending=False) # Serie, tri par ordre décroissant
|
||||
rangs_str = pd.Series(index=notes.index, dtype=str) # le rang est une chaîne
|
||||
rangs_int = pd.Series(index=notes.index, dtype=int) # le rang numérique pour tris
|
||||
rangs_str = pd.Series("", index=notes.index, dtype=str) # le rang est une chaîne
|
||||
# le rang numérique pour tris:
|
||||
rangs_int = pd.Series(0, index=notes.index, dtype=int)
|
||||
N = len(notes)
|
||||
nb_ex = 0 # nb d'ex-aequo consécutifs en cours
|
||||
notes_i = notes.iat
|
||||
|
@ -128,4 +129,5 @@ def comp_ranks_series(notes: pd.Series) -> (pd.Series, pd.Series):
|
|||
rangs_int[etudid] = i + 1
|
||||
srang = "%d" % (i + 1)
|
||||
rangs_str[etudid] = srang
|
||||
assert rangs_int.dtype == int
|
||||
return rangs_str, rangs_int
|
||||
|
|
|
@ -271,7 +271,7 @@ def compute_ue_moys_apc(
|
|||
)
|
||||
# Annule les coefs des modules NaN
|
||||
modimpl_coefs_etuds_no_nan = np.where(np.isnan(sem_cube), 0.0, modimpl_coefs_etuds)
|
||||
if modimpl_coefs_etuds_no_nan.dtype == np.object: # arrive sur des tableaux vides
|
||||
if modimpl_coefs_etuds_no_nan.dtype == object: # arrive sur des tableaux vides
|
||||
modimpl_coefs_etuds_no_nan = modimpl_coefs_etuds_no_nan.astype(np.float)
|
||||
#
|
||||
# Version vectorisée
|
||||
|
@ -356,7 +356,7 @@ def compute_ue_moys_classic(
|
|||
modimpl_coefs_etuds_no_nan = np.where(
|
||||
np.isnan(sem_matrix), 0.0, modimpl_coefs_etuds
|
||||
)
|
||||
if modimpl_coefs_etuds_no_nan.dtype == np.object: # arrive sur des tableaux vides
|
||||
if modimpl_coefs_etuds_no_nan.dtype == object: # arrive sur des tableaux vides
|
||||
modimpl_coefs_etuds_no_nan = modimpl_coefs_etuds_no_nan.astype(np.float)
|
||||
# --------------------- Calcul des moyennes d'UE
|
||||
ue_modules = np.array(
|
||||
|
@ -367,7 +367,7 @@ def compute_ue_moys_classic(
|
|||
)
|
||||
# nb_ue x nb_etuds x nb_mods : coefs prenant en compte NaN et inscriptions:
|
||||
coefs = (modimpl_coefs_etuds_no_nan_stacked * ue_modules).swapaxes(1, 2)
|
||||
if coefs.dtype == np.object: # arrive sur des tableaux vides
|
||||
if coefs.dtype == object: # arrive sur des tableaux vides
|
||||
coefs = coefs.astype(np.float)
|
||||
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
|
||||
etud_moy_ue = (
|
||||
|
@ -462,7 +462,7 @@ def compute_mat_moys_classic(
|
|||
modimpl_coefs_etuds_no_nan = np.where(
|
||||
np.isnan(sem_matrix), 0.0, modimpl_coefs_etuds
|
||||
)
|
||||
if modimpl_coefs_etuds_no_nan.dtype == np.object: # arrive sur des tableaux vides
|
||||
if modimpl_coefs_etuds_no_nan.dtype == object: # arrive sur des tableaux vides
|
||||
modimpl_coefs_etuds_no_nan = modimpl_coefs_etuds_no_nan.astype(np.float)
|
||||
|
||||
etud_moy_mat = (modimpl_coefs_etuds_no_nan * sem_matrix_inscrits).sum(
|
||||
|
|
|
@ -19,7 +19,6 @@ from app.models.moduleimpls import ModuleImpl
|
|||
from app.models.ues import UniteEns
|
||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||
from app.scodoc import sco_preferences
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
|
||||
class ResultatsSemestreBUT(NotesTableCompat):
|
||||
|
@ -33,7 +32,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||
|
||||
def __init__(self, formsemestre):
|
||||
super().__init__(formsemestre)
|
||||
"""DataFrame, row UEs(sans bonus), cols modimplid, value coef"""
|
||||
|
||||
self.sem_cube = None
|
||||
"""ndarray (etuds x modimpl x ue)"""
|
||||
|
||||
|
@ -44,7 +43,8 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||
self.store()
|
||||
t2 = time.time()
|
||||
log(
|
||||
f"ResultatsSemestreBUT: cached formsemestre_id={formsemestre.id} ({(t1-t0):g}s +{(t2-t1):g}s)"
|
||||
f"""ResultatsSemestreBUT: cached formsemestre_id={formsemestre.id
|
||||
} ({(t1-t0):g}s +{(t2-t1):g}s)"""
|
||||
)
|
||||
|
||||
def compute(self):
|
||||
|
|
|
@ -11,6 +11,14 @@ from app.models import FormSemestre
|
|||
|
||||
|
||||
class ResultatsCache:
|
||||
"""Résultats cachés (via redis)
|
||||
L'attribut _cached_attrs donne la liste des noms des attributs à cacher
|
||||
(doivent être sérialisables facilement, se limiter à des types simples)
|
||||
|
||||
store() enregistre les attributs dans le cache, et
|
||||
load_cached() les recharge.
|
||||
"""
|
||||
|
||||
_cached_attrs = () # virtual
|
||||
|
||||
def __init__(self, formsemestre: FormSemestre, cache_class=None):
|
||||
|
|
|
@ -50,7 +50,8 @@ class ResultatsSemestreClassic(NotesTableCompat):
|
|||
self.store()
|
||||
t2 = time.time()
|
||||
log(
|
||||
f"ResultatsSemestreClassic: cached formsemestre_id={formsemestre.id} ({(t1-t0):g}s +{(t2-t1):g}s)"
|
||||
f"""ResultatsSemestreClassic: cached formsemestre_id={
|
||||
formsemestre.id} ({(t1-t0):g}s +{(t2-t1):g}s)"""
|
||||
)
|
||||
# recalculé (aussi rapide que de les cacher)
|
||||
self.moy_min = self.etud_moy_gen.min()
|
||||
|
@ -220,36 +221,29 @@ class ResultatsSemestreClassic(NotesTableCompat):
|
|||
moyenne générale.
|
||||
Coef = somme des coefs des modules de l'UE auxquels il est inscrit
|
||||
"""
|
||||
c = comp_etud_sum_coef_modules_ue(self.formsemestre.id, etudid, ue["ue_id"])
|
||||
if c is not None: # inscrit à au moins un module de cette UE
|
||||
return c
|
||||
coef = comp_etud_sum_coef_modules_ue(self.formsemestre.id, etudid, ue["ue_id"])
|
||||
if coef is not None: # inscrit à au moins un module de cette UE
|
||||
return coef
|
||||
# arfff: aucun moyen de déterminer le coefficient de façon sûre
|
||||
log(
|
||||
"* oups: calcul coef UE impossible\nformsemestre_id='%s'\netudid='%s'\nue=%s"
|
||||
% (self.formsemestre.id, etudid, ue)
|
||||
f"""* oups: calcul coef UE impossible\nformsemestre_id='{self.formsemestre.id
|
||||
}'\netudid='{etudid}'\nue={ue}"""
|
||||
)
|
||||
etud: Identite = Identite.query.get(etudid)
|
||||
raise ScoValueError(
|
||||
"""<div class="scovalueerror"><p>Coefficient de l'UE capitalisée %s impossible à déterminer
|
||||
pour l'étudiant <a href="%s" class="discretelink">%s</a></p>
|
||||
<p>Il faut <a href="%s">saisir le coefficient de cette UE avant de continuer</a></p>
|
||||
f"""<div class="scovalueerror"><p>Coefficient de l'UE capitalisée {ue.acronyme}
|
||||
impossible à déterminer pour l'étudiant <a href="{
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}" class="discretelink">{etud.nom_disp()}</a></p>
|
||||
<p>Il faut <a href="{
|
||||
url_for("notes.formsemestre_edit_uecoefs", scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=self.formsemestre.id, err_ue_id=ue["ue_id"],
|
||||
)
|
||||
}">saisir le coefficient de cette UE avant de continuer</a></p>
|
||||
</div>
|
||||
"""
|
||||
% (
|
||||
ue.acronyme,
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
|
||||
etud.nom_disp(),
|
||||
url_for(
|
||||
"notes.formsemestre_edit_uecoefs",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=self.formsemestre.id,
|
||||
err_ue_id=ue["ue_id"],
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return 0.0 # ?
|
||||
|
||||
|
||||
def notes_sem_load_matrix(formsemestre: FormSemestre) -> tuple[np.ndarray, dict]:
|
||||
"""Calcule la matrice des notes du semestre
|
||||
|
@ -279,7 +273,7 @@ def notes_sem_assemble_matrix(modimpls_notes: list[pd.Series]) -> np.ndarray:
|
|||
(Series rendus par compute_module_moy, index: etud)
|
||||
Resultat: ndarray (etud x module)
|
||||
"""
|
||||
if not len(modimpls_notes):
|
||||
if not modimpls_notes:
|
||||
return np.zeros((0, 0), dtype=float)
|
||||
modimpls_notes_arr = [s.values for s in modimpls_notes]
|
||||
modimpls_notes = np.stack(modimpls_notes_arr)
|
||||
|
|
|
@ -4,6 +4,9 @@
|
|||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""Résultats semestre: méthodes communes aux formations classiques et APC
|
||||
"""
|
||||
|
||||
from collections import Counter
|
||||
from functools import cached_property
|
||||
import numpy as np
|
||||
|
@ -11,21 +14,21 @@ import pandas as pd
|
|||
|
||||
from flask import g, url_for
|
||||
|
||||
from app.auth.models import User
|
||||
from app.comp.res_cache import ResultatsCache
|
||||
from app.comp import res_sem
|
||||
from app.comp.moy_mod import ModuleImplResults
|
||||
from app.models import FormSemestre, FormSemestreUECoef
|
||||
from app.models import FormSemestre, FormSemestreUECoef, formsemestre
|
||||
from app.models import Identite
|
||||
from app.models import ModuleImpl, ModuleImplInscription
|
||||
from app.models.ues import UniteEns
|
||||
from app.scodoc.sco_cache import ResultatsSemestreCache
|
||||
from app.scodoc.sco_codes_parcours import UE_SPORT, DEF, DEM
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_users
|
||||
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`
|
||||
|
@ -41,6 +44,8 @@ class ResultatsSemestre(ResultatsCache):
|
|||
"""
|
||||
|
||||
_cached_attrs = (
|
||||
"bonus",
|
||||
"bonus_ues",
|
||||
"etud_moy_gen_ranks",
|
||||
"etud_moy_gen",
|
||||
"etud_moy_ue",
|
||||
|
@ -55,6 +60,10 @@ class ResultatsSemestre(ResultatsCache):
|
|||
# BUT ou standard ? (apc == "approche par compétences")
|
||||
self.is_apc = formsemestre.formation.is_apc()
|
||||
# Attributs "virtuels", définis dans les sous-classes
|
||||
self.bonus: pd.Series = None # virtuel
|
||||
"Bonus sur moy. gen. Series de float, index etudid"
|
||||
self.bonus_ues: pd.DataFrame = None # virtuel
|
||||
"DataFrame de float, index etudid, columns: ue.id"
|
||||
# ResultatsSemestreBUT ou ResultatsSemestreClassic
|
||||
self.etud_moy_ue = {}
|
||||
"etud_moy_ue: DataFrame columns UE, rows etudid"
|
||||
|
@ -102,6 +111,14 @@ class ResultatsSemestre(ResultatsCache):
|
|||
"dict { etudid : indice dans les inscrits }"
|
||||
return {e.id: idx for idx, e in enumerate(self.etuds)}
|
||||
|
||||
def modimpl_notes(self, modimpl_id: int, ue_id: int) -> np.ndarray:
|
||||
"""Les notes moyennes des étudiants du sem. à ce modimpl dans cette ue.
|
||||
Utile pour stats bottom tableau recap.
|
||||
Résultat: 1d array of float
|
||||
"""
|
||||
# différent en BUT et classique: virtuelle
|
||||
raise NotImplementedError
|
||||
|
||||
@cached_property
|
||||
def etuds_dict(self) -> dict[int, Identite]:
|
||||
"""dict { etudid : Identite } inscrits au semestre,
|
||||
|
@ -230,6 +247,13 @@ class ResultatsSemestre(ResultatsCache):
|
|||
0.0, min(self.etud_moy_gen[etudid], 20.0)
|
||||
)
|
||||
|
||||
def get_etud_etat(self, etudid: int) -> str:
|
||||
"Etat de l'etudiant: 'I', 'D', DEF ou '' (si pas connu dans ce semestre)"
|
||||
ins = self.formsemestre.etuds_inscriptions.get(etudid, None)
|
||||
if ins is None:
|
||||
return ""
|
||||
return ins.etat
|
||||
|
||||
def _get_etud_ue_cap(self, etudid: int, ue: UniteEns) -> dict:
|
||||
"""Donne les informations sur la capitalisation de l'UE ue pour cet étudiant.
|
||||
Résultat:
|
||||
|
@ -304,11 +328,11 @@ class ResultatsSemestre(ResultatsCache):
|
|||
if coef_ue is None:
|
||||
orig_sem = FormSemestre.query.get(ue_cap["formsemestre_id"])
|
||||
raise ScoValueError(
|
||||
f"""L'UE capitalisée {ue_capitalized.acronyme}
|
||||
f"""L'UE capitalisée {ue_capitalized.acronyme}
|
||||
du semestre {orig_sem.titre_annee()}
|
||||
n'a pas d'indication d'ECTS.
|
||||
Corrigez ou faite corriger le programme
|
||||
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
|
||||
Corrigez ou faite corriger le programme
|
||||
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
|
||||
formation_id=ue_capitalized.formation_id)}">via cette page</a>.
|
||||
"""
|
||||
)
|
||||
|
@ -333,6 +357,11 @@ class ResultatsSemestre(ResultatsCache):
|
|||
"capitalized_ue_id": ue_cap["ue_id"] if is_capitalized else None,
|
||||
}
|
||||
|
||||
def compute_etud_ue_coef(self, etudid: int, ue: UniteEns) -> float:
|
||||
"Détermine le coefficient de l'UE pour cet étudiant."
|
||||
# calcul différent en classqiue et BUT
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_etud_ue_cap_coef(self, etudid, ue, ue_cap):
|
||||
"""Calcule le coefficient d'une UE capitalisée, pour cet étudiant,
|
||||
injectée dans le semestre courant.
|
||||
|
@ -359,7 +388,9 @@ class ResultatsSemestre(ResultatsCache):
|
|||
|
||||
# --- TABLEAU RECAP
|
||||
|
||||
def get_table_recap(self, convert_values=False):
|
||||
def get_table_recap(
|
||||
self, convert_values=False, include_evaluations=False, modejury=False
|
||||
):
|
||||
"""Result: tuple avec
|
||||
- rows: liste de dicts { column_id : value }
|
||||
- titles: { column_id : title }
|
||||
|
@ -385,48 +416,73 @@ class ResultatsSemestre(ResultatsCache):
|
|||
- les colonnes de modules (SAE ou res.) d'une UE ont la classe mod_ue_<ue_id>
|
||||
_<column_id>_order : clé de tri
|
||||
"""
|
||||
|
||||
if convert_values:
|
||||
fmt_note = scu.fmt_note
|
||||
else:
|
||||
fmt_note = lambda x: x
|
||||
|
||||
barre_moy = (
|
||||
self.formsemestre.formation.get_parcours().BARRE_MOY - scu.NOTES_TOLERANCE
|
||||
)
|
||||
barre_valid_ue = self.formsemestre.formation.get_parcours().NOTES_BARRE_VALID_UE
|
||||
parcours = self.formsemestre.formation.get_parcours()
|
||||
barre_moy = parcours.BARRE_MOY - scu.NOTES_TOLERANCE
|
||||
barre_valid_ue = parcours.NOTES_BARRE_VALID_UE
|
||||
barre_warning_ue = parcours.BARRE_UE_DISPLAY_WARNING
|
||||
NO_NOTE = "-" # contenu des cellules sans notes
|
||||
rows = []
|
||||
titles = {"rang": "Rg"} # column_id : title
|
||||
# column_id : title
|
||||
titles = {}
|
||||
# les titres en footer: les mêmes, mais avec des bulles et liens:
|
||||
titles_bot = {}
|
||||
dict_nom_res = {} # cache uid : nomcomplet
|
||||
|
||||
def add_cell(
|
||||
row: dict, col_id: str, title: str, content: str, classes: str = ""
|
||||
row: dict,
|
||||
col_id: str,
|
||||
title: str,
|
||||
content: str,
|
||||
classes: str = "",
|
||||
idx: int = 100,
|
||||
):
|
||||
"Add a row to our table. classes is a list of css class names"
|
||||
row[col_id] = content
|
||||
if classes:
|
||||
row[f"_{col_id}_class"] = classes
|
||||
row[f"_{col_id}_class"] = classes + f" c{idx}"
|
||||
if not col_id in titles:
|
||||
titles[col_id] = title
|
||||
titles[f"_{col_id}_col_order"] = idx
|
||||
if classes:
|
||||
titles[f"_{col_id}_class"] = classes
|
||||
return idx + 1
|
||||
|
||||
etuds_inscriptions = self.formsemestre.etuds_inscriptions
|
||||
ues = self.formsemestre.query_ues(with_sport=True) # avec bonus
|
||||
ues_sans_bonus = [ue for ue in ues if ue.type != UE_SPORT]
|
||||
modimpl_ids = set() # modimpl effectivement présents dans la table
|
||||
for etudid in etuds_inscriptions:
|
||||
idx = 0 # index de la colonne
|
||||
etud = Identite.query.get(etudid)
|
||||
row = {"etudid": etudid}
|
||||
# --- Codes (seront cachés, mais exportés en excel)
|
||||
idx = add_cell(row, "etudid", "etudid", etudid, "codes", idx)
|
||||
idx = add_cell(
|
||||
row, "code_nip", "code_nip", etud.code_nip or "", "codes", idx
|
||||
)
|
||||
# --- Rang
|
||||
add_cell(row, "rang", "Rg", self.etud_moy_gen_ranks[etudid], "rang")
|
||||
idx = add_cell(
|
||||
row, "rang", "Rg", self.etud_moy_gen_ranks[etudid], "rang", idx
|
||||
)
|
||||
row["_rang_order"] = f"{self.etud_moy_gen_ranks_int[etudid]:05d}"
|
||||
# --- Identité étudiant
|
||||
add_cell(row, "civilite_str", "Civ.", etud.civilite_str, "identite_detail")
|
||||
add_cell(row, "nom_disp", "Nom", etud.nom_disp(), "identite_detail")
|
||||
add_cell(row, "prenom", "Prénom", etud.prenom, "identite_detail")
|
||||
add_cell(row, "nom_short", "Nom", etud.nom_short, "identite_court")
|
||||
idx = add_cell(
|
||||
row, "civilite_str", "Civ.", etud.civilite_str, "identite_detail", idx
|
||||
)
|
||||
idx = add_cell(
|
||||
row, "nom_disp", "Nom", etud.nom_disp(), "identite_detail", idx
|
||||
)
|
||||
row["_nom_disp_order"] = etud.sort_key
|
||||
idx = add_cell(row, "prenom", "Prénom", etud.prenom, "identite_detail", idx)
|
||||
idx = add_cell(
|
||||
row, "nom_short", "Nom", etud.nom_short, "identite_court", idx
|
||||
)
|
||||
row["_nom_short_order"] = etud.sort_key
|
||||
row["_nom_short_target"] = url_for(
|
||||
"notes.formsemestre_bulletinetud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
|
@ -436,26 +492,29 @@ class ResultatsSemestre(ResultatsCache):
|
|||
row["_nom_short_target_attrs"] = f'class="etudinfo" id="{etudid}"'
|
||||
row["_nom_disp_target"] = row["_nom_short_target"]
|
||||
row["_nom_disp_target_attrs"] = row["_nom_short_target_attrs"]
|
||||
self._recap_etud_groups_infos(etudid, row, titles)
|
||||
|
||||
idx = 30 # début des colonnes de notes
|
||||
# --- Moyenne générale
|
||||
moy_gen = self.etud_moy_gen.get(etudid, False)
|
||||
note_class = ""
|
||||
if moy_gen is False:
|
||||
moy_gen = NO_NOTE
|
||||
elif isinstance(moy_gen, float) and moy_gen < barre_moy:
|
||||
note_class = " moy_inf"
|
||||
add_cell(
|
||||
note_class = " moy_ue_warning" # en rouge
|
||||
idx = add_cell(
|
||||
row,
|
||||
"moy_gen",
|
||||
"Moy",
|
||||
fmt_note(moy_gen),
|
||||
"col_moy_gen" + note_class,
|
||||
idx,
|
||||
)
|
||||
titles_bot["_moy_gen_target_attrs"] = (
|
||||
'title="moyenne indicative"' if self.is_apc else ""
|
||||
)
|
||||
# --- Moyenne d'UE
|
||||
for ue in [ue for ue in ues if ue.type != UE_SPORT]:
|
||||
nb_ues_validables, nb_ues_warning = 0, 0
|
||||
for ue in ues_sans_bonus:
|
||||
ue_status = self.get_etud_ue_status(etudid, ue.id)
|
||||
if ue_status is not None:
|
||||
col_id = f"moy_ue_{ue.id}"
|
||||
|
@ -466,17 +525,43 @@ class ResultatsSemestre(ResultatsCache):
|
|||
note_class = " moy_inf"
|
||||
elif val >= barre_valid_ue:
|
||||
note_class = " moy_ue_valid"
|
||||
add_cell(
|
||||
nb_ues_validables += 1
|
||||
if val < barre_warning_ue:
|
||||
note_class = " moy_ue_warning" # notes très basses
|
||||
nb_ues_warning += 1
|
||||
idx = add_cell(
|
||||
row,
|
||||
col_id,
|
||||
ue.acronyme,
|
||||
fmt_note(val),
|
||||
"col_ue" + note_class,
|
||||
idx,
|
||||
)
|
||||
titles_bot[
|
||||
f"_{col_id}_target_attrs"
|
||||
] = f"""title="{ue.titre} S{ue.semestre_idx or '?'}" """
|
||||
if modejury:
|
||||
# pas d'autre colonnes de résultats
|
||||
continue
|
||||
# Bonus (sport) dans cette UE ?
|
||||
# Le bonus sport appliqué sur cette UE
|
||||
if (self.bonus_ues is not None) and (ue.id in self.bonus_ues):
|
||||
val = self.bonus_ues[ue.id][etud.id] or ""
|
||||
val_fmt = val_fmt_html = fmt_note(val)
|
||||
if val:
|
||||
val_fmt_html = f'<span class="green-arrow-up"></span><span class="sp2l">{val_fmt}</span>'
|
||||
idx = add_cell(
|
||||
row,
|
||||
f"bonus_ue_{ue.id}",
|
||||
f"Bonus {ue.acronyme}",
|
||||
val_fmt_html,
|
||||
"col_ue_bonus",
|
||||
idx,
|
||||
)
|
||||
row[f"_bonus_ue_{ue.id}_xls"] = val_fmt
|
||||
# Les moyennes des modules (ou ressources et SAÉs) dans cette UE
|
||||
idx_malus = idx # place pour colonne malus à gauche des modules
|
||||
idx += 1
|
||||
for modimpl in self.modimpls_in_ue(ue.id, etudid, with_bonus=False):
|
||||
if ue_status["is_capitalized"]:
|
||||
val = "-c-"
|
||||
|
@ -502,109 +587,176 @@ class ResultatsSemestre(ResultatsCache):
|
|||
col_id = (
|
||||
f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}"
|
||||
)
|
||||
add_cell(
|
||||
val_fmt = val_fmt_html = fmt_note(val)
|
||||
if modimpl.module.module_type == scu.ModuleType.MALUS:
|
||||
val_fmt_html = (
|
||||
(scu.EMO_RED_TRIANGLE_DOWN + val_fmt) if val else ""
|
||||
)
|
||||
idx = add_cell(
|
||||
row,
|
||||
col_id,
|
||||
modimpl.module.code,
|
||||
fmt_note(val),
|
||||
val_fmt_html,
|
||||
# class col_res mod_ue_123
|
||||
f"col_{modimpl.module.type_abbrv()} mod_ue_{ue.id}",
|
||||
idx,
|
||||
)
|
||||
row[f"_{col_id}_xls"] = val_fmt
|
||||
if modimpl.module.module_type == scu.ModuleType.MALUS:
|
||||
titles[f"_{col_id}_col_order"] = idx_malus
|
||||
titles_bot[f"_{col_id}_target"] = url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=modimpl.id,
|
||||
)
|
||||
nom_resp = dict_nom_res.get(modimpl.responsable_id)
|
||||
if nom_resp is None:
|
||||
user = User.query.get(modimpl.responsable_id)
|
||||
nom_resp = user.get_nomcomplet() if user else ""
|
||||
dict_nom_res[modimpl.responsable_id] = nom_resp
|
||||
titles_bot[
|
||||
f"_{col_id}_target_attrs"
|
||||
] = f"""
|
||||
title="{modimpl.module.titre}
|
||||
({sco_users.user_info(modimpl.responsable_id)['nomcomplet']})" """
|
||||
] = f""" title="{modimpl.module.titre} ({nom_resp})" """
|
||||
modimpl_ids.add(modimpl.id)
|
||||
|
||||
ue_valid_txt = (
|
||||
ue_valid_txt_html
|
||||
) = f"{nb_ues_validables}/{len(ues_sans_bonus)}"
|
||||
if nb_ues_warning:
|
||||
ue_valid_txt_html += " " + scu.EMO_WARNING
|
||||
add_cell(
|
||||
row,
|
||||
"ues_validables",
|
||||
"UEs",
|
||||
ue_valid_txt_html,
|
||||
"col_ue col_ues_validables",
|
||||
29, # juste avant moy. gen.
|
||||
)
|
||||
row["_ues_validables_xls"] = ue_valid_txt
|
||||
if nb_ues_warning:
|
||||
row["_ues_validables_class"] += " moy_ue_warning"
|
||||
elif nb_ues_validables < len(ues_sans_bonus):
|
||||
row["_ues_validables_class"] += " moy_inf"
|
||||
row["_ues_validables_order"] = nb_ues_validables # pour tri
|
||||
if modejury:
|
||||
idx = add_cell(
|
||||
row,
|
||||
"jury_link",
|
||||
"",
|
||||
f"""<a href="{url_for('notes.formsemestre_validation_etud_form',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=self.formsemestre.id, etudid=etudid
|
||||
)
|
||||
}">saisir décision</a>""",
|
||||
"col_jury_link",
|
||||
1000,
|
||||
)
|
||||
rows.append(row)
|
||||
|
||||
self._recap_add_partitions(rows, titles)
|
||||
self._recap_add_admissions(rows, titles)
|
||||
|
||||
# tri par rang croissant
|
||||
rows.sort(key=lambda e: e["_rang_order"])
|
||||
|
||||
# INFOS POUR FOOTER
|
||||
bottom_infos = self._recap_bottom_infos(
|
||||
[ue for ue in ues if ue.type != UE_SPORT], modimpl_ids, fmt_note
|
||||
)
|
||||
bottom_infos = self._recap_bottom_infos(ues_sans_bonus, modimpl_ids, fmt_note)
|
||||
if include_evaluations:
|
||||
self._recap_add_evaluations(rows, titles, bottom_infos)
|
||||
|
||||
# Ajoute style "col_empty" aux colonnes de modules vides
|
||||
for col_id in titles:
|
||||
c_class = f"_{col_id}_class"
|
||||
if "col_empty" in bottom_infos["moy"].get(c_class, ""):
|
||||
for row in rows:
|
||||
row[c_class] = row.get(c_class, "") + " col_empty"
|
||||
titles[c_class] += " col_empty"
|
||||
for row in bottom_infos.values():
|
||||
row[c_class] = row.get(c_class, "") + " col_empty"
|
||||
|
||||
# --- TABLE FOOTER: ECTS, moyennes, min, max...
|
||||
footer_rows = []
|
||||
for bottom_line in bottom_infos:
|
||||
row = bottom_infos[bottom_line]
|
||||
for (bottom_line, row) in bottom_infos.items():
|
||||
# Cases vides à styler:
|
||||
row["moy_gen"] = row.get("moy_gen", "")
|
||||
row["_moy_gen_class"] = "col_moy_gen"
|
||||
# titre de la ligne:
|
||||
row["prenom"] = row["nom_short"] = bottom_line.capitalize()
|
||||
row["_tr_class"] = bottom_line.lower()
|
||||
row["prenom"] = row["nom_short"] = (
|
||||
row.get("_title", "") or bottom_line.capitalize()
|
||||
)
|
||||
row["_tr_class"] = bottom_line.lower() + (
|
||||
(" " + row["_tr_class"]) if "_tr_class" in row else ""
|
||||
)
|
||||
footer_rows.append(row)
|
||||
titles_bot.update(titles)
|
||||
footer_rows.append(titles_bot)
|
||||
return (
|
||||
rows,
|
||||
footer_rows,
|
||||
titles,
|
||||
[title for title in titles if not title.startswith("_")],
|
||||
column_ids = [title for title in titles if not title.startswith("_")]
|
||||
column_ids.sort(
|
||||
key=lambda col_id: titles.get("_" + col_id + "_col_order", 1000)
|
||||
)
|
||||
return (rows, footer_rows, titles, column_ids)
|
||||
|
||||
def _recap_bottom_infos(self, ues, modimpl_ids: set, fmt_note) -> dict:
|
||||
"""Les informations à mettre en bas de la table: min, max, moy, ECTS"""
|
||||
bottom_infos = { # { key : row } avec key = min, max, moy, coef
|
||||
"min": {},
|
||||
"max": {},
|
||||
"moy": {},
|
||||
"coef": {},
|
||||
}
|
||||
row_min, row_max, row_moy, row_coef, row_ects, row_apo = (
|
||||
{"_tr_class": "bottom_info", "_title": "Min."},
|
||||
{"_tr_class": "bottom_info"},
|
||||
{"_tr_class": "bottom_info"},
|
||||
{"_tr_class": "bottom_info"},
|
||||
{"_tr_class": "bottom_info"},
|
||||
{"_tr_class": "bottom_info", "_title": "Code Apogée"},
|
||||
)
|
||||
# --- ECTS
|
||||
row = {}
|
||||
for ue in ues:
|
||||
row[f"moy_ue_{ue.id}"] = ue.ects
|
||||
row[f"_moy_ue_{ue.id}_class"] = "col_ue"
|
||||
colid = f"moy_ue_{ue.id}"
|
||||
row_ects[colid] = ue.ects
|
||||
row_ects[f"_{colid}_class"] = "col_ue"
|
||||
# style cases vides pour borders verticales
|
||||
bottom_infos["coef"][f"moy_ue_{ue.id}"] = ""
|
||||
bottom_infos["coef"][f"_moy_ue_{ue.id}_class"] = "col_ue"
|
||||
row["moy_gen"] = sum([ue.ects or 0 for ue in ues if ue.type != UE_SPORT])
|
||||
row["_moy_gen_class"] = "col_moy_gen"
|
||||
bottom_infos["ects"] = row
|
||||
row_coef[colid] = ""
|
||||
row_coef[f"_{colid}_class"] = "col_ue"
|
||||
# row_apo[colid] = ue.code_apogee or ""
|
||||
row_ects["moy_gen"] = sum([ue.ects or 0 for ue in ues if ue.type != UE_SPORT])
|
||||
row_ects["_moy_gen_class"] = "col_moy_gen"
|
||||
|
||||
# --- MIN, MAX, MOY, APO
|
||||
|
||||
# --- MIN, MAX, MOY
|
||||
row_min, row_max, row_moy = {}, {}, {}
|
||||
row_min["moy_gen"] = fmt_note(self.etud_moy_gen.min())
|
||||
row_max["moy_gen"] = fmt_note(self.etud_moy_gen.max())
|
||||
row_moy["moy_gen"] = fmt_note(self.etud_moy_gen.mean())
|
||||
for ue in [ue for ue in ues if ue.type != UE_SPORT]:
|
||||
col_id = f"moy_ue_{ue.id}"
|
||||
row_min[col_id] = fmt_note(self.etud_moy_ue[ue.id].min())
|
||||
row_max[col_id] = fmt_note(self.etud_moy_ue[ue.id].max())
|
||||
row_moy[col_id] = fmt_note(self.etud_moy_ue[ue.id].mean())
|
||||
row_min[f"_{col_id}_class"] = "col_ue"
|
||||
row_max[f"_{col_id}_class"] = "col_ue"
|
||||
row_moy[f"_{col_id}_class"] = "col_ue"
|
||||
for ue in ues:
|
||||
colid = f"moy_ue_{ue.id}"
|
||||
row_min[colid] = fmt_note(self.etud_moy_ue[ue.id].min())
|
||||
row_max[colid] = fmt_note(self.etud_moy_ue[ue.id].max())
|
||||
row_moy[colid] = fmt_note(self.etud_moy_ue[ue.id].mean())
|
||||
row_min[f"_{colid}_class"] = "col_ue"
|
||||
row_max[f"_{colid}_class"] = "col_ue"
|
||||
row_moy[f"_{colid}_class"] = "col_ue"
|
||||
row_apo[colid] = ue.code_apogee or ""
|
||||
|
||||
for modimpl in self.formsemestre.modimpls_sorted:
|
||||
if modimpl.id in modimpl_ids:
|
||||
col_id = f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}"
|
||||
colid = f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}"
|
||||
if self.is_apc:
|
||||
coef = self.modimpl_coefs_df[modimpl.id][ue.id] * (
|
||||
modimpl.module.coefficient or 0.0
|
||||
)
|
||||
coef = self.modimpl_coefs_df[modimpl.id][ue.id]
|
||||
else:
|
||||
coef = modimpl.module.coefficient or 0
|
||||
bottom_infos["coef"][col_id] = fmt_note(coef)
|
||||
row_coef[colid] = fmt_note(coef)
|
||||
notes = self.modimpl_notes(modimpl.id, ue.id)
|
||||
row_min[col_id] = fmt_note(np.nanmin(notes))
|
||||
row_max[col_id] = fmt_note(np.nanmax(notes))
|
||||
row_moy[col_id] = fmt_note(np.nanmean(notes))
|
||||
row_min[colid] = fmt_note(np.nanmin(notes))
|
||||
row_max[colid] = fmt_note(np.nanmax(notes))
|
||||
moy = np.nanmean(notes)
|
||||
row_moy[colid] = fmt_note(moy)
|
||||
if np.isnan(moy):
|
||||
# aucune note dans ce module
|
||||
row_moy[f"_{colid}_class"] = "col_empty"
|
||||
row_apo[colid] = modimpl.module.code_apogee or ""
|
||||
|
||||
bottom_infos["min"] = row_min
|
||||
bottom_infos["max"] = row_max
|
||||
bottom_infos["moy"] = row_moy
|
||||
return bottom_infos
|
||||
return { # { key : row } avec key = min, max, moy, coef
|
||||
"min": row_min,
|
||||
"max": row_max,
|
||||
"moy": row_moy,
|
||||
"coef": row_coef,
|
||||
"ects": row_ects,
|
||||
"apo": row_apo,
|
||||
}
|
||||
|
||||
def _recap_etud_groups_infos(self, etudid: int, row: dict, titles: dict):
|
||||
"""Table recap: ajoute à row les colonnes sur les groupes pour cet etud"""
|
||||
|
@ -612,7 +764,7 @@ class ResultatsSemestre(ResultatsCache):
|
|||
# if dec:
|
||||
# codes_nb[dec["code"]] += 1
|
||||
row_class = ""
|
||||
etud_etat = self.get_etud_etat(etudid) # dans NotesTableCompat, à revoir
|
||||
etud_etat = self.get_etud_etat(etudid)
|
||||
if etud_etat == DEM:
|
||||
gr_name = "Dém."
|
||||
row_class = "dem"
|
||||
|
@ -629,3 +781,136 @@ class ResultatsSemestre(ResultatsCache):
|
|||
if row_class:
|
||||
row["_tr_class"] = " ".join([row.get("_tr_class", ""), row_class])
|
||||
titles["group"] = "Gr"
|
||||
|
||||
def _recap_add_admissions(self, rows: list[dict], titles: dict):
|
||||
"""Ajoute les colonnes "admission"
|
||||
rows est une liste de dict avec une clé "etudid"
|
||||
Les colonnes ont la classe css "admission"
|
||||
"""
|
||||
fields = {
|
||||
"bac": "Bac",
|
||||
"specialite": "Spécialité",
|
||||
"type_admission": "Type Adm.",
|
||||
"classement": "Rg. Adm.",
|
||||
}
|
||||
first = True
|
||||
for i, cid in enumerate(fields):
|
||||
titles[f"_{cid}_col_order"] = 10000 + i # tout à droite
|
||||
if first:
|
||||
titles[f"_{cid}_class"] = "admission admission_first"
|
||||
first = False
|
||||
else:
|
||||
titles[f"_{cid}_class"] = "admission"
|
||||
titles.update(fields)
|
||||
for row in rows:
|
||||
etud = Identite.query.get(row["etudid"])
|
||||
admission = etud.admission.first()
|
||||
first = True
|
||||
for cid in fields:
|
||||
row[cid] = getattr(admission, cid) or ""
|
||||
if first:
|
||||
row[f"_{cid}_class"] = "admission admission_first"
|
||||
first = False
|
||||
else:
|
||||
row[f"_{cid}_class"] = "admission"
|
||||
|
||||
def _recap_add_partitions(self, rows: list[dict], titles: dict):
|
||||
"""Ajoute les colonnes indiquant les groupes
|
||||
rows est une liste de dict avec une clé "etudid"
|
||||
Les colonnes ont la classe css "partition"
|
||||
"""
|
||||
partitions, partitions_etud_groups = sco_groups.get_formsemestre_groups(
|
||||
self.formsemestre.id
|
||||
)
|
||||
first_partition = True
|
||||
for partition in partitions:
|
||||
cid = f"part_{partition['partition_id']}"
|
||||
titles[cid] = partition["partition_name"]
|
||||
if first_partition:
|
||||
klass = "partition"
|
||||
else:
|
||||
klass = "partition partition_aux"
|
||||
titles[f"_{cid}_class"] = klass
|
||||
titles[f"_{cid}_col_order"] = 10
|
||||
partition_etud_groups = partitions_etud_groups[partition["partition_id"]]
|
||||
for row in rows:
|
||||
# dans NotesTableCompat, à revoir
|
||||
etud_etat = self.get_etud_etat(row["etudid"])
|
||||
if etud_etat == "D":
|
||||
gr_name = "Dém."
|
||||
row["_tr_class"] = "dem"
|
||||
elif etud_etat == DEF:
|
||||
gr_name = "Déf."
|
||||
row["_tr_class"] = "def"
|
||||
else:
|
||||
group = partition_etud_groups.get(row["etudid"])
|
||||
gr_name = group["group_name"] if group else ""
|
||||
if gr_name:
|
||||
row[f"{cid}"] = gr_name
|
||||
row[f"_{cid}_class"] = klass
|
||||
first_partition = False
|
||||
|
||||
def _recap_add_evaluations(
|
||||
self, rows: list[dict], titles: dict, bottom_infos: dict
|
||||
):
|
||||
"""Ajoute les colonnes avec les notes aux évaluations
|
||||
rows est une liste de dict avec une clé "etudid"
|
||||
Les colonnes ont la classe css "evaluation"
|
||||
"""
|
||||
# nouvelle ligne pour description évaluations:
|
||||
bottom_infos["descr_evaluation"] = {
|
||||
"_tr_class": "bottom_info",
|
||||
"_title": "Description évaluation",
|
||||
}
|
||||
first_eval = True
|
||||
index_col = 9000 # à droite
|
||||
for modimpl in self.formsemestre.modimpls_sorted:
|
||||
evals = self.modimpls_results[modimpl.id].get_evaluations_completes(modimpl)
|
||||
eval_index = len(evals) - 1
|
||||
inscrits = {i.etudid for i in modimpl.inscriptions}
|
||||
first_eval_of_mod = True
|
||||
for e in evals:
|
||||
cid = f"eval_{e.id}"
|
||||
titles[
|
||||
cid
|
||||
] = f'{modimpl.module.code} {eval_index} {e.jour.isoformat() if e.jour else ""}'
|
||||
klass = "evaluation"
|
||||
if first_eval:
|
||||
klass += " first"
|
||||
elif first_eval_of_mod:
|
||||
klass += " first_of_mod"
|
||||
titles[f"_{cid}_class"] = klass
|
||||
first_eval_of_mod = first_eval = False
|
||||
titles[f"_{cid}_col_order"] = index_col
|
||||
index_col += 1
|
||||
eval_index -= 1
|
||||
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(
|
||||
e.evaluation_id
|
||||
)
|
||||
for row in rows:
|
||||
etudid = row["etudid"]
|
||||
if etudid in inscrits:
|
||||
if etudid in notes_db:
|
||||
val = notes_db[etudid]["value"]
|
||||
else:
|
||||
# Note manquante mais prise en compte immédiate: affiche ATT
|
||||
val = scu.NOTES_ATTENTE
|
||||
row[cid] = scu.fmt_note(val)
|
||||
row[f"_{cid}_class"] = klass + {
|
||||
"ABS": " abs",
|
||||
"ATT": " att",
|
||||
"EXC": " exc",
|
||||
}.get(row[cid], "")
|
||||
else:
|
||||
row[cid] = "ni"
|
||||
row[f"_{cid}_class"] = klass + " non_inscrit"
|
||||
|
||||
bottom_infos["coef"][cid] = e.coefficient
|
||||
bottom_infos["min"][cid] = "0"
|
||||
bottom_infos["max"][cid] = scu.fmt_note(e.note_max)
|
||||
bottom_infos["descr_evaluation"][cid] = e.description or ""
|
||||
bottom_infos["descr_evaluation"][f"_{cid}_target"] = url_for(
|
||||
"notes.evaluation_listenotes",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
evaluation_id=e.id,
|
||||
)
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
"""
|
||||
from functools import cached_property
|
||||
|
||||
from flask import g, flash
|
||||
from flask import flash, g, Markup, url_for
|
||||
|
||||
from app import log
|
||||
from app.comp import moy_sem
|
||||
|
@ -32,8 +32,6 @@ class NotesTableCompat(ResultatsSemestre):
|
|||
"""
|
||||
|
||||
_cached_attrs = ResultatsSemestre._cached_attrs + (
|
||||
"bonus",
|
||||
"bonus_ues",
|
||||
"malus",
|
||||
"etud_moy_gen_ranks",
|
||||
"etud_moy_gen_ranks_int",
|
||||
|
@ -44,8 +42,6 @@ class NotesTableCompat(ResultatsSemestre):
|
|||
super().__init__(formsemestre)
|
||||
|
||||
nb_etuds = len(self.etuds)
|
||||
self.bonus = None # virtuel
|
||||
self.bonus_ues = None # virtuel
|
||||
self.ue_rangs = {u.id: (None, nb_etuds) for u in self.ues}
|
||||
self.mod_rangs = None # sera surchargé en Classic, mais pas en APC
|
||||
"""{ modimpl_id : (rangs, effectif) }"""
|
||||
|
@ -123,8 +119,20 @@ class NotesTableCompat(ResultatsSemestre):
|
|||
if check_apc_ects and self.is_apc and not hasattr(g, "checked_apc_ects"):
|
||||
g.checked_apc_ects = True
|
||||
if None in [ue.ects for ue in ues if ue.type != UE_SPORT]:
|
||||
formation = self.formsemestre.formation
|
||||
ue_sans_ects = [
|
||||
ue for ue in ues if ue.type != UE_SPORT and ue.ects is None
|
||||
]
|
||||
flash(
|
||||
"""Calcul moyenne générale impossible: ECTS des UE manquants !""",
|
||||
Markup(
|
||||
f"""Calcul moyenne générale impossible: ECTS des UE manquants !<br>
|
||||
(dans {' ,'.join([ue.acronyme for ue in ue_sans_ects])}
|
||||
de la formation: <a href="{url_for("notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept, formation_id=formation.id)
|
||||
}">{formation.get_titre_version()}</a>)
|
||||
)
|
||||
"""
|
||||
),
|
||||
category="danger",
|
||||
)
|
||||
return ues_dict
|
||||
|
@ -135,7 +143,7 @@ class NotesTableCompat(ResultatsSemestre):
|
|||
"""
|
||||
modimpls_dict = []
|
||||
for modimpl in self.formsemestre.modimpls_sorted:
|
||||
if ue_id == None or modimpl.module.ue.id == ue_id:
|
||||
if (ue_id is None) or (modimpl.module.ue.id == ue_id):
|
||||
d = modimpl.to_dict()
|
||||
# compat ScoDoc < 9.2: ajoute matières
|
||||
d["mat"] = modimpl.module.matiere.to_dict()
|
||||
|
@ -239,13 +247,6 @@ class NotesTableCompat(ResultatsSemestre):
|
|||
)
|
||||
return self.validations.decisions_jury.get(etudid, None)
|
||||
|
||||
def get_etud_etat(self, etudid: int) -> str:
|
||||
"Etat de l'etudiant: 'I', 'D', DEF ou '' (si pas connu dans ce semestre)"
|
||||
ins = self.formsemestre.etuds_inscriptions.get(etudid, None)
|
||||
if ins is None:
|
||||
return ""
|
||||
return ins.etat
|
||||
|
||||
def get_etud_mat_moy(self, matiere_id: int, etudid: int) -> str:
|
||||
"""moyenne d'un étudiant dans une matière (ou NA si pas de notes)"""
|
||||
if not self.moyennes_matieres:
|
||||
|
@ -274,7 +275,8 @@ class NotesTableCompat(ResultatsSemestre):
|
|||
def get_etud_ects_pot(self, etudid: int) -> dict:
|
||||
"""
|
||||
Un dict avec les champs
|
||||
ects_pot : (float) nb de crédits ECTS qui seraient validés (sous réserve de validation par le jury)
|
||||
ects_pot : (float) nb de crédits ECTS qui seraient validés
|
||||
(sous réserve de validation par le jury)
|
||||
ects_pot_fond: (float) nb d'ECTS issus d'UE fondamentales (non électives)
|
||||
|
||||
Ce sont les ECTS des UE au dessus de la barre (10/20 en principe), avant le jury (donc non
|
||||
|
@ -296,10 +298,14 @@ class NotesTableCompat(ResultatsSemestre):
|
|||
"ects_pot_fond": 0.0, # not implemented (anciennemment pour école ingé)
|
||||
}
|
||||
|
||||
def get_etud_rang(self, etudid: int):
|
||||
def get_etud_rang(self, etudid: int) -> str:
|
||||
"""Le rang (classement) de l'étudiant dans le semestre.
|
||||
Result: "13" ou "12 ex"
|
||||
"""
|
||||
return self.etud_moy_gen_ranks.get(etudid, 99999)
|
||||
|
||||
def get_etud_rang_group(self, etudid: int, group_id: int):
|
||||
"Le rang de l'étudiant dans ce groupe (NON IMPLEMENTE)"
|
||||
return (None, 0) # XXX unimplemented TODO
|
||||
|
||||
def get_evals_in_mod(self, moduleimpl_id: int) -> list[dict]:
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
# -*- coding: UTF-8 -*
|
||||
"""Decorators for permissions, roles and ScoDoc7 Zope compatibility
|
||||
"""
|
||||
import functools
|
||||
from functools import wraps
|
||||
import inspect
|
||||
import types
|
||||
import logging
|
||||
|
||||
|
||||
import werkzeug
|
||||
from werkzeug.exceptions import BadRequest
|
||||
import flask
|
||||
from flask import g, current_app, request
|
||||
from flask import abort, url_for, redirect
|
||||
|
|
|
@ -15,6 +15,7 @@ from app.scodoc import sco_preferences
|
|||
|
||||
|
||||
def send_async_email(app, msg):
|
||||
"Send an email, async"
|
||||
with app.app_context():
|
||||
mail.send(msg)
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
# empty but required for pylint
|
|
@ -0,0 +1 @@
|
|||
# empty but required for pylint
|
|
@ -29,17 +29,13 @@
|
|||
Formulaires configuration Exports Apogée (codes)
|
||||
"""
|
||||
|
||||
from flask import flash, url_for, redirect, render_template
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import SubmitField, validators
|
||||
from wtforms.fields.simple import StringField
|
||||
|
||||
from app import models
|
||||
from app.models import ScoDocSiteConfig
|
||||
from app.models import SHORT_STR_LEN
|
||||
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
def _build_code_field(code):
|
||||
|
@ -61,6 +57,7 @@ def _build_code_field(code):
|
|||
|
||||
|
||||
class CodesDecisionsForm(FlaskForm):
|
||||
"Formulaire code décisions Apogée"
|
||||
ADC = _build_code_field("ADC")
|
||||
ADJ = _build_code_field("ADJ")
|
||||
ADM = _build_code_field("ADM")
|
||||
|
@ -73,5 +70,16 @@ class CodesDecisionsForm(FlaskForm):
|
|||
DEM = _build_code_field("DEM")
|
||||
NAR = _build_code_field("NAR")
|
||||
RAT = _build_code_field("RAT")
|
||||
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)""",
|
||||
validators=[
|
||||
validators.Length(
|
||||
max=SHORT_STR_LEN,
|
||||
message=f"Le format ne doit pas dépasser {SHORT_STR_LEN} caractères",
|
||||
),
|
||||
validators.DataRequired("format requis"),
|
||||
],
|
||||
)
|
||||
submit = SubmitField("Valider")
|
||||
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
||||
|
|
|
@ -41,14 +41,9 @@ from wtforms.fields.simple import StringField, HiddenField
|
|||
from app.models import Departement
|
||||
from app.scodoc import sco_logos, html_sco_header
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_config_actions import (
|
||||
LogoDelete,
|
||||
LogoUpdate,
|
||||
LogoInsert,
|
||||
)
|
||||
|
||||
from app.scodoc.sco_config_actions import LogoInsert
|
||||
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_logos import find_logo
|
||||
|
||||
|
||||
|
@ -122,6 +117,8 @@ def logo_name_validator(message=None):
|
|||
class AddLogoForm(FlaskForm):
|
||||
"""Formulaire permettant l'ajout d'un logo (dans un département)"""
|
||||
|
||||
from app.scodoc.sco_config_actions import LogoInsert
|
||||
|
||||
dept_key = HiddenField()
|
||||
name = StringField(
|
||||
label="Nom",
|
||||
|
@ -153,7 +150,7 @@ class AddLogoForm(FlaskForm):
|
|||
dept_id = dept_key_to_id(self.dept_key.data)
|
||||
if dept_id == GLOBAL:
|
||||
dept_id = None
|
||||
if find_logo(logoname=name.data, dept_id=dept_id) is not None:
|
||||
if find_logo(logoname=name.data, dept_id=dept_id, strict=True) is not None:
|
||||
raise validators.ValidationError("Un logo de même nom existe déjà")
|
||||
|
||||
def select_action(self):
|
||||
|
@ -162,6 +159,14 @@ class AddLogoForm(FlaskForm):
|
|||
return LogoInsert.build_action(self.data)
|
||||
return None
|
||||
|
||||
def opened(self):
|
||||
if self.do_insert.data:
|
||||
if self.name.errors:
|
||||
return "open"
|
||||
if self.upload.errors:
|
||||
return "open"
|
||||
return ""
|
||||
|
||||
|
||||
class LogoForm(FlaskForm):
|
||||
"""Embed both presentation of a logo (cf. template file configuration.html)
|
||||
|
@ -178,7 +183,18 @@ class LogoForm(FlaskForm):
|
|||
)
|
||||
],
|
||||
)
|
||||
do_delete = SubmitField("Supprimer l'image")
|
||||
do_delete = SubmitField("Supprimer")
|
||||
do_rename = SubmitField("Renommer")
|
||||
new_name = StringField(
|
||||
label="Nom",
|
||||
validators=[
|
||||
logo_name_validator("Nom de logo invalide (alphanumérique, _)"),
|
||||
validators.Length(
|
||||
max=20, message="Un nom ne doit pas dépasser 20 caractères"
|
||||
),
|
||||
validators.DataRequired("Nom de logo requis (alphanumériques ou '-')"),
|
||||
],
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs["meta"] = {"csrf": False}
|
||||
|
@ -207,12 +223,25 @@ class LogoForm(FlaskForm):
|
|||
self.titre = "Logo pied de page"
|
||||
|
||||
def select_action(self):
|
||||
from app.scodoc.sco_config_actions import LogoRename
|
||||
from app.scodoc.sco_config_actions import LogoUpdate
|
||||
from app.scodoc.sco_config_actions import LogoDelete
|
||||
|
||||
if self.do_delete.data and self.can_delete:
|
||||
return LogoDelete.build_action(self.data)
|
||||
if self.upload.data and self.validate():
|
||||
return LogoUpdate.build_action(self.data)
|
||||
if self.do_rename.data and self.validate():
|
||||
return LogoRename.build_action(self.data)
|
||||
return None
|
||||
|
||||
def opened(self):
|
||||
if self.upload.data and self.upload.errors:
|
||||
return "open"
|
||||
if self.new_name.data and self.new_name.errors:
|
||||
return "open"
|
||||
return ""
|
||||
|
||||
|
||||
class DeptForm(FlaskForm):
|
||||
dept_key = HiddenField()
|
||||
|
@ -246,6 +275,23 @@ class DeptForm(FlaskForm):
|
|||
return self
|
||||
return self.index.get(logoname, None)
|
||||
|
||||
def opened(self):
|
||||
if self.add_logo.opened():
|
||||
return "open"
|
||||
for logo_form in self.logos:
|
||||
if logo_form.opened():
|
||||
return "open"
|
||||
return ""
|
||||
|
||||
def count(self):
|
||||
compte = len(self.logos.entries)
|
||||
if compte == 0:
|
||||
return "vide"
|
||||
elif compte == 1:
|
||||
return "1 élément"
|
||||
else:
|
||||
return f"{compte} éléments"
|
||||
|
||||
|
||||
def _make_dept_id_name():
|
||||
"""Cette section assure que tous les départements sont traités (y compris ceux qu'ont pas de logo au départ)
|
||||
|
|
|
@ -29,7 +29,6 @@
|
|||
Formulaires création département
|
||||
"""
|
||||
|
||||
from flask import flash, url_for, redirect, render_template
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import SubmitField, validators
|
||||
from wtforms.fields.simple import StringField, BooleanField
|
||||
|
|
|
@ -27,6 +27,20 @@ class Absence(db.Model):
|
|||
# XXX TODO: contrainte ajoutée: vérifier suppression du module
|
||||
# (mettre à NULL sans supprimer)
|
||||
|
||||
def to_dict(self):
|
||||
data = {
|
||||
"id": self.id,
|
||||
"etudid": self.etudid,
|
||||
"jour": self.jour,
|
||||
"estabs": self.estabs,
|
||||
"estjust": self.estjust,
|
||||
"matin": self.matin,
|
||||
"description": self.description,
|
||||
"entry_date": self.entry_date,
|
||||
"moduleimpl_id": self.moduleimpl_id,
|
||||
}
|
||||
return data
|
||||
|
||||
|
||||
class AbsenceNotification(db.Model):
|
||||
"""Notification d'absence émise"""
|
||||
|
|
|
@ -6,8 +6,6 @@
|
|||
"""ScoDoc 9 models : Référentiel Compétence BUT 2021
|
||||
"""
|
||||
from datetime import datetime
|
||||
from enum import unique
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.orm import class_mapper
|
||||
import sqlalchemy
|
||||
|
@ -28,6 +26,8 @@ def attribute_names(cls):
|
|||
|
||||
|
||||
class XMLModel:
|
||||
"""Mixin class, to ease loading Orebut XMLs"""
|
||||
|
||||
_xml_attribs = {} # to be overloaded
|
||||
id = "_"
|
||||
|
||||
|
@ -162,6 +162,7 @@ class ApcCompetence(db.Model, XMLModel):
|
|||
|
||||
|
||||
class ApcSituationPro(db.Model, XMLModel):
|
||||
"Situation professionnelle"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
competence_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_competence.id"), nullable=False
|
||||
|
@ -173,6 +174,7 @@ class ApcSituationPro(db.Model, XMLModel):
|
|||
|
||||
|
||||
class ApcComposanteEssentielle(db.Model, XMLModel):
|
||||
"Composante essentielle"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
competence_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_competence.id"), nullable=False
|
||||
|
@ -199,6 +201,9 @@ class ApcNiveau(db.Model, XMLModel):
|
|||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} ordre={self.ordre}>"
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"libelle": self.libelle,
|
||||
|
@ -222,14 +227,14 @@ class ApcAppCritique(db.Model, XMLModel):
|
|||
backref=db.backref("app_critiques", lazy="dynamic"),
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
def to_dict(self) -> dict:
|
||||
return {"libelle": self.libelle}
|
||||
|
||||
def get_label(self):
|
||||
def get_label(self) -> str:
|
||||
return self.code + " - " + self.titre
|
||||
|
||||
def __repr__(self):
|
||||
return "<AppCritique {}>".format(self.code)
|
||||
return f"<{self.__class__.__name__} {self.code}>"
|
||||
|
||||
def get_saes(self):
|
||||
"""Liste des SAE associées"""
|
||||
|
@ -258,6 +263,9 @@ class ApcParcours(db.Model, XMLModel):
|
|||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} {self.code}>"
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"code": self.code,
|
||||
|
@ -274,6 +282,9 @@ class ApcAnneeParcours(db.Model, XMLModel):
|
|||
)
|
||||
ordre = db.Column(db.Integer)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} ordre={self.ordre}>"
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"ordre": self.ordre,
|
||||
|
@ -320,3 +331,6 @@ class ApcParcoursNiveauCompetence(db.Model):
|
|||
cascade="save-update, merge, delete, delete-orphan",
|
||||
),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} {self.competence} {self.annee_parcours}>"
|
||||
|
|
|
@ -36,6 +36,7 @@ CODES_SCODOC_TO_APO = {
|
|||
DEM: "NAR",
|
||||
NAR: "NAR",
|
||||
RAT: "ATT",
|
||||
"NOTES_FMT": "%3.2f",
|
||||
}
|
||||
|
||||
|
||||
|
@ -157,32 +158,6 @@ class ScoDocSiteConfig(db.Model):
|
|||
class_list.sort(key=lambda x: x[1].replace(" du ", " de "))
|
||||
return [("", "")] + class_list
|
||||
|
||||
@classmethod
|
||||
def get_bonus_sport_func(cls):
|
||||
"""Fonction bonus_sport ScoDoc 7 XXX
|
||||
Transitoire pour les tests durant la transition #sco92
|
||||
"""
|
||||
"""returns bonus func with specified name.
|
||||
If name not specified, return the configured function.
|
||||
None if no bonus function configured.
|
||||
Raises ScoValueError if func_name not found in module bonus_sport.
|
||||
"""
|
||||
from app.scodoc import bonus_sport
|
||||
|
||||
c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first()
|
||||
if c is None:
|
||||
return None
|
||||
func_name = c.value
|
||||
if func_name == "": # pas de bonus défini
|
||||
return None
|
||||
try:
|
||||
return getattr(bonus_sport, func_name)
|
||||
except AttributeError:
|
||||
raise ScoValueError(
|
||||
f"""Fonction de calcul de l'UE bonus inexistante: "{func_name}".
|
||||
(contacter votre administrateur local)."""
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_code_apo(cls, code: str) -> str:
|
||||
"""La représentation d'un code pour les exports Apogée.
|
||||
|
|
|
@ -16,6 +16,7 @@ from app import models
|
|||
|
||||
from app.scodoc import notesdb as ndb
|
||||
from app.scodoc.sco_bac import Baccalaureat
|
||||
from app.scodoc.sco_exceptions import ScoInvalidParamError
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
|
||||
|
@ -131,7 +132,10 @@ class Identite(db.Model):
|
|||
@cached_property
|
||||
def sort_key(self) -> tuple:
|
||||
"clé pour tris par ordre alphabétique"
|
||||
return (self.nom_usuel or self.nom).lower(), self.prenom.lower()
|
||||
return (
|
||||
scu.suppress_accents(self.nom_usuel or self.nom or "").lower(),
|
||||
scu.suppress_accents(self.prenom or "").lower(),
|
||||
)
|
||||
|
||||
def get_first_email(self, field="email") -> str:
|
||||
"Le mail associé à la première adrese de l'étudiant, ou None"
|
||||
|
@ -298,22 +302,46 @@ class Identite(db.Model):
|
|||
else:
|
||||
date_ins = events[0].event_date
|
||||
situation += date_ins.strftime(" le %d/%m/%Y")
|
||||
elif inscr.etat == scu.DEF:
|
||||
situation = f"défaillant en {inscr.formsemestre.titre_mois()}"
|
||||
event = (
|
||||
models.ScolarEvent.query.filter_by(
|
||||
etudid=self.id,
|
||||
formsemestre_id=inscr.formsemestre.id,
|
||||
event_type="DEFAILLANCE",
|
||||
)
|
||||
.order_by(models.ScolarEvent.event_date)
|
||||
.first()
|
||||
)
|
||||
if not event:
|
||||
log(
|
||||
f"*** situation inconsistante pour {self} (def mais pas d'event)"
|
||||
)
|
||||
situation += "???" # ???
|
||||
else:
|
||||
date_def = event.event_date
|
||||
situation += date_def.strftime(" le %d/%m/%Y")
|
||||
|
||||
else:
|
||||
situation = f"démission de {inscr.formsemestre.titre_mois()}"
|
||||
# Cherche la date de demission dans scolar_events:
|
||||
events = models.ScolarEvent.query.filter_by(
|
||||
etudid=self.id,
|
||||
formsemestre_id=inscr.formsemestre.id,
|
||||
event_type="DEMISSION",
|
||||
).all()
|
||||
if not events:
|
||||
event = (
|
||||
models.ScolarEvent.query.filter_by(
|
||||
etudid=self.id,
|
||||
formsemestre_id=inscr.formsemestre.id,
|
||||
event_type="DEMISSION",
|
||||
)
|
||||
.order_by(models.ScolarEvent.event_date)
|
||||
.first()
|
||||
)
|
||||
if not event:
|
||||
log(
|
||||
f"*** situation inconsistante pour {self} (demission mais pas d'event)"
|
||||
)
|
||||
date_dem = "???" # ???
|
||||
situation += "???" # ???
|
||||
else:
|
||||
date_dem = events[0].event_date
|
||||
situation += date_dem.strftime(" le %d/%m/%Y")
|
||||
date_dem = event.event_date
|
||||
situation += date_dem.strftime(" le %d/%m/%Y")
|
||||
else:
|
||||
situation = "non inscrit" + self.e
|
||||
|
||||
|
@ -351,7 +379,10 @@ def make_etud_args(
|
|||
"""
|
||||
args = None
|
||||
if etudid:
|
||||
args = {"etudid": etudid}
|
||||
try:
|
||||
args = {"etudid": int(etudid)}
|
||||
except ValueError as exc:
|
||||
raise ScoInvalidParamError() from exc
|
||||
elif code_nip:
|
||||
args = {"code_nip": code_nip}
|
||||
elif use_request: # use form from current request (Flask global)
|
||||
|
|
|
@ -2,9 +2,21 @@
|
|||
|
||||
"""Evenements et logs divers
|
||||
"""
|
||||
import datetime
|
||||
import re
|
||||
|
||||
from flask import g, url_for
|
||||
from flask_login import current_user
|
||||
|
||||
from app import db
|
||||
from app import email
|
||||
from app import log
|
||||
from app.auth.models import User
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc import sco_preferences
|
||||
|
||||
|
||||
class Scolog(db.Model):
|
||||
|
@ -24,13 +36,222 @@ class Scolog(db.Model):
|
|||
class ScolarNews(db.Model):
|
||||
"""Nouvelles pour page d'accueil"""
|
||||
|
||||
NEWS_ABS = "ABS" # saisie absence
|
||||
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_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_MISC: "opération", # unused
|
||||
NEWS_NOTE: "saisie note",
|
||||
NEWS_SEM: "création semestre",
|
||||
}
|
||||
NEWS_TYPES = list(NEWS_MAP.keys())
|
||||
|
||||
__tablename__ = "scolar_news"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
|
||||
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
authenticated_user = db.Column(db.Text) # login, sans contrainte
|
||||
date = db.Column(
|
||||
db.DateTime(timezone=True), server_default=db.func.now(), index=True
|
||||
)
|
||||
authenticated_user = db.Column(db.Text, index=True) # login, sans contrainte
|
||||
# type in 'INSCR', 'NOTES', 'FORM', 'SEM', 'MISC'
|
||||
type = db.Column(db.String(SHORT_STR_LEN))
|
||||
object = db.Column(db.Integer) # moduleimpl_id, formation_id, formsemestre_id
|
||||
type = db.Column(db.String(SHORT_STR_LEN), index=True)
|
||||
object = db.Column(
|
||||
db.Integer, index=True
|
||||
) # moduleimpl_id, formation_id, formsemestre_id
|
||||
text = db.Column(db.Text)
|
||||
url = db.Column(db.Text)
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
f"<{self.__class__.__name__}(id={self.id}, date='{self.date.isoformat()}')>"
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
"'Chargement notes dans Stage (S3 FI) par Aurélie Dupont'"
|
||||
formsemestre = self.get_news_formsemestre()
|
||||
user = User.query.filter_by(user_name=self.authenticated_user).first()
|
||||
|
||||
sem_text = (
|
||||
f"""(<a href="{url_for('notes.formsemestre_status', scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
|
||||
}">{formsemestre.sem_modalite()}</a>)"""
|
||||
if formsemestre
|
||||
else ""
|
||||
)
|
||||
author = f"par {user.get_nomcomplet()}" if user else ""
|
||||
return f"{self.text} {sem_text} {author}"
|
||||
|
||||
def formatted_date(self) -> str:
|
||||
"06 Avr 14h23"
|
||||
mois = scu.MONTH_NAMES_ABBREV[self.date.month - 1]
|
||||
return f"{self.date.day} {mois} {self.date.hour:02d}h{self.date.minute:02d}"
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"date": {
|
||||
"display": self.date.strftime("%d/%m/%Y %H:%M"),
|
||||
"timestamp": self.date.timestamp(),
|
||||
},
|
||||
"type": self.NEWS_MAP.get(self.type, "?"),
|
||||
"authenticated_user": self.authenticated_user,
|
||||
"text": self.text,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def last_news(cls, n=1, dept_id=None, filter_dept=True) -> list:
|
||||
"The most recent n news. Returns list of ScolarNews instances."
|
||||
query = cls.query
|
||||
if filter_dept:
|
||||
if dept_id is None:
|
||||
dept_id = g.scodoc_dept_id
|
||||
query = query.filter_by(dept_id=dept_id)
|
||||
|
||||
return query.order_by(cls.date.desc()).limit(n).all()
|
||||
|
||||
@classmethod
|
||||
def add(cls, typ, obj=None, text="", url=None, max_frequency=0):
|
||||
"""Enregistre une nouvelle
|
||||
Si max_frequency, ne génère pas 2 nouvelles "identiques"
|
||||
à moins de max_frequency secondes d'intervalle.
|
||||
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.
|
||||
"""
|
||||
if max_frequency:
|
||||
last_news = (
|
||||
cls.query.filter_by(
|
||||
dept_id=g.scodoc_dept_id,
|
||||
authenticated_user=current_user.user_name,
|
||||
type=typ,
|
||||
object=obj,
|
||||
)
|
||||
.order_by(cls.date.desc())
|
||||
.limit(1)
|
||||
.first()
|
||||
)
|
||||
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
|
||||
return
|
||||
|
||||
news = ScolarNews(
|
||||
dept_id=g.scodoc_dept_id,
|
||||
authenticated_user=current_user.user_name,
|
||||
type=typ,
|
||||
object=obj,
|
||||
text=text,
|
||||
url=url,
|
||||
)
|
||||
db.session.add(news)
|
||||
db.session.commit()
|
||||
log(f"news: {news}")
|
||||
news.notify_by_mail()
|
||||
|
||||
def get_news_formsemestre(self) -> FormSemestre:
|
||||
"""formsemestre concerné par la nouvelle
|
||||
None si inexistant
|
||||
"""
|
||||
formsemestre_id = None
|
||||
if self.type == self.NEWS_INSCR:
|
||||
formsemestre_id = self.object
|
||||
elif self.type == self.NEWS_NOTE:
|
||||
moduleimpl_id = self.object
|
||||
if moduleimpl_id:
|
||||
modimpl = ModuleImpl.query.get(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)
|
||||
return formsemestre
|
||||
|
||||
def notify_by_mail(self):
|
||||
"""Notify by email"""
|
||||
formsemestre = self.get_news_formsemestre()
|
||||
|
||||
prefs = sco_preferences.SemPreferences(
|
||||
formsemestre_id=formsemestre.id if formsemestre else None
|
||||
)
|
||||
destinations = prefs["emails_notifications"] or ""
|
||||
destinations = [x.strip() for x in destinations.split(",")]
|
||||
destinations = [x for x in destinations if x]
|
||||
if not destinations:
|
||||
return
|
||||
#
|
||||
txt = self.text
|
||||
if formsemestre:
|
||||
txt += f"""\n\nSemestre {formsemestre.titre_mois()}\n\n"""
|
||||
txt += f"""<a href="{url_for("notes.formsemestre_status", _external=True,
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
|
||||
}">{formsemestre.sem_modalite()}</a>
|
||||
"""
|
||||
user = User.query.filter_by(user_name=self.authenticated_user).first()
|
||||
if user:
|
||||
txt += f"\n\nEffectué par: {user.get_nomcomplet()}\n"
|
||||
|
||||
txt = (
|
||||
"\n"
|
||||
+ txt
|
||||
+ """\n
|
||||
--- Ceci est un message de notification automatique issu de ScoDoc
|
||||
--- vous recevez ce message car votre adresse est indiquée dans les paramètres de ScoDoc.
|
||||
"""
|
||||
)
|
||||
|
||||
# Transforme les URL en URL absolues
|
||||
base = scu.ScoURL()
|
||||
txt = re.sub('href=/.*?"', 'href="' + base + "/", txt)
|
||||
|
||||
# Transforme les liens HTML en texte brut: '<a href="url">texte</a>' devient 'texte: url'
|
||||
# (si on veut des messages non html)
|
||||
txt = re.sub(r'<a.*?href\s*=\s*"(.*?)".*?>(.*?)</a>', r"\2: \1", txt)
|
||||
|
||||
subject = "[ScoDoc] " + self.NEWS_MAP.get(self.type, "?")
|
||||
sender = prefs["email_from_addr"]
|
||||
|
||||
email.send_email(subject, sender, destinations, txt)
|
||||
|
||||
@classmethod
|
||||
def scolar_news_summary_html(cls, n=5) -> str:
|
||||
"""News summary, formated in HTML"""
|
||||
news_list = cls.last_news(n=n)
|
||||
if not news_list:
|
||||
return ""
|
||||
H = [
|
||||
f"""<div class="news"><span class="newstitle"><a href="{
|
||||
url_for("scolar.dept_news", scodoc_dept=g.scodoc_dept)
|
||||
}">Dernières opérations</a>
|
||||
</span><ul class="newslist">"""
|
||||
]
|
||||
|
||||
for news in news_list:
|
||||
H.append(
|
||||
f"""<li class="newslist"><span class="newsdate">{news.formatted_date()}</span><span
|
||||
class="newstext">{news}</span></li>"""
|
||||
)
|
||||
|
||||
H.append("</ul>")
|
||||
|
||||
# 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>.
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
|
||||
H.append("</div>")
|
||||
return "\n".join(H)
|
||||
|
|
|
@ -7,7 +7,6 @@ from app.comp import df_cache
|
|||
from app.models import SHORT_STR_LEN
|
||||
from app.models.modules import Module
|
||||
from app.models.ues import UniteEns
|
||||
from app.scodoc import notesdb as ndb
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
@ -146,7 +145,8 @@ class Formation(db.Model):
|
|||
db.session.add(ue)
|
||||
|
||||
db.session.commit()
|
||||
app.clear_scodoc_cache()
|
||||
if change:
|
||||
app.clear_scodoc_cache()
|
||||
|
||||
|
||||
class Matiere(db.Model):
|
||||
|
|
|
@ -286,7 +286,7 @@ class FormSemestre(db.Model):
|
|||
"""
|
||||
if not self.etapes:
|
||||
return ""
|
||||
return ", ".join([str(x.etape_apo) for x in self.etapes])
|
||||
return ", ".join(sorted([str(x.etape_apo) for x in self.etapes]))
|
||||
|
||||
def responsables_str(self, abbrev_prenom=True) -> str:
|
||||
"""chaîne "J. Dupond, X. Martin"
|
||||
|
@ -374,6 +374,16 @@ class FormSemestre(db.Model):
|
|||
return self.titre
|
||||
return f"{self.titre} {self.formation.get_parcours().SESSION_NAME} {self.semestre_id}"
|
||||
|
||||
def sem_modalite(self) -> str:
|
||||
"""Le semestre et la modalité, ex "S2 FI" ou "S3 APP" """
|
||||
if self.semestre_id > 0:
|
||||
descr_sem = f"S{self.semestre_id}"
|
||||
else:
|
||||
descr_sem = ""
|
||||
if self.modalite:
|
||||
descr_sem += " " + self.modalite
|
||||
return descr_sem
|
||||
|
||||
def get_abs_count(self, etudid):
|
||||
"""Les comptes d'absences de cet étudiant dans ce semestre:
|
||||
tuple (nb abs, nb abs justifiées)
|
||||
|
@ -423,7 +433,7 @@ notes_formsemestre_responsables = db.Table(
|
|||
|
||||
|
||||
class FormSemestreEtape(db.Model):
|
||||
"""Étape Apogée associées au semestre"""
|
||||
"""Étape Apogée associée au semestre"""
|
||||
|
||||
__tablename__ = "notes_formsemestre_etapes"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
@ -580,6 +590,9 @@ class FormSemestreInscription(db.Model):
|
|||
# etape apogee d'inscription (experimental 2020)
|
||||
etape = db.Column(db.String(APO_CODE_STR_LEN))
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} {self.id} etudid={self.etudid} sem={self.formsemestre_id} etat={self.etat}>"
|
||||
|
||||
|
||||
class NotesSemSet(db.Model):
|
||||
"""semsets: ensemble de formsemestres pour exports Apogée"""
|
||||
|
|
|
@ -74,6 +74,10 @@ class GroupDescr(db.Model):
|
|||
f"""<{self.__class__.__name__} {self.id} "{self.group_name or '(tous)'}">"""
|
||||
)
|
||||
|
||||
def get_nom_with_part(self) -> str:
|
||||
"Nom avec partition: 'TD A'"
|
||||
return f"{self.partition.partition_name or ''} {self.group_name or '-'}"
|
||||
|
||||
|
||||
group_membership = db.Table(
|
||||
"group_membership",
|
||||
|
|
|
@ -9,7 +9,6 @@ from app.comp import df_cache
|
|||
from app.models.etudiants import Identite
|
||||
from app.models.modules import Module
|
||||
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
|
|
|
@ -72,23 +72,20 @@ class NotesNotesLog(db.Model):
|
|||
|
||||
def etud_has_notes_attente(etudid, formsemestre_id):
|
||||
"""Vrai si cet etudiant a au moins une note en attente dans ce semestre.
|
||||
(ne compte que les notes en attente dans des évaluation avec coef. non nul).
|
||||
(ne compte que les notes en attente dans des évaluations avec coef. non nul).
|
||||
"""
|
||||
# XXX ancienne méthode de notes_table à ré-écrire
|
||||
cnx = ndb.GetDBConnexion()
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
cursor.execute(
|
||||
"""SELECT n.*
|
||||
cursor = db.session.execute(
|
||||
"""SELECT COUNT(*)
|
||||
FROM notes_notes n, notes_evaluation e, notes_moduleimpl m,
|
||||
notes_moduleimpl_inscription i
|
||||
WHERE n.etudid = %(etudid)s
|
||||
and n.value = %(code_attente)s
|
||||
WHERE n.etudid = :etudid
|
||||
and n.value = :code_attente
|
||||
and n.evaluation_id = e.id
|
||||
and e.moduleimpl_id = m.id
|
||||
and m.formsemestre_id = %(formsemestre_id)s
|
||||
and m.formsemestre_id = :formsemestre_id
|
||||
and e.coefficient != 0
|
||||
and m.id = i.moduleimpl_id
|
||||
and i.etudid=%(etudid)s
|
||||
and i.etudid = :etudid
|
||||
""",
|
||||
{
|
||||
"formsemestre_id": formsemestre_id,
|
||||
|
@ -96,4 +93,4 @@ def etud_has_notes_attente(etudid, formsemestre_id):
|
|||
"code_attente": scu.NOTES_ATTENTE,
|
||||
},
|
||||
)
|
||||
return len(cursor.fetchall()) > 0
|
||||
return cursor.fetchone()[0] > 0
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
# empty but required for pylint
|
|
@ -486,7 +486,10 @@ class JuryPE(object):
|
|||
sesdates = [
|
||||
pe_tagtable.conversionDate_StrToDate(sem["date_fin"]) for sem in sessems
|
||||
] # association 1 date -> 1 semestrePE pour les semestres de l'étudiant
|
||||
lastdate = max(sesdates) # date de fin de l'inscription la plus récente
|
||||
if sesdates:
|
||||
lastdate = max(sesdates) # date de fin de l'inscription la plus récente
|
||||
else:
|
||||
return False
|
||||
|
||||
# if PETable.AFFICHAGE_DEBUG_PE == True : pe_tools.pe_print(" derniere inscription = ", lastDateSem)
|
||||
|
||||
|
@ -585,7 +588,7 @@ class JuryPE(object):
|
|||
for (i, fid) in enumerate(lesFids):
|
||||
if pe_tools.PE_DEBUG:
|
||||
pe_tools.pe_print(
|
||||
u"%d) Semestre taggué %s (avec classement dans groupe)"
|
||||
"%d) Semestre taggué %s (avec classement dans groupe)"
|
||||
% (i + 1, fid)
|
||||
)
|
||||
self.add_semtags_in_jury(fid)
|
||||
|
@ -620,7 +623,7 @@ class JuryPE(object):
|
|||
nbinscrit = self.semTagDict[fid].get_nbinscrits()
|
||||
if pe_tools.PE_DEBUG:
|
||||
pe_tools.pe_print(
|
||||
u" - %d étudiants classés " % (nbinscrit)
|
||||
" - %d étudiants classés " % (nbinscrit)
|
||||
+ ": "
|
||||
+ ",".join(
|
||||
[etudid for etudid in self.semTagDict[fid].get_etudids()]
|
||||
|
@ -628,12 +631,12 @@ class JuryPE(object):
|
|||
)
|
||||
if lesEtudidsManquants:
|
||||
pe_tools.pe_print(
|
||||
u" - dont %d étudiants manquants ajoutés aux données du jury"
|
||||
" - dont %d étudiants manquants ajoutés aux données du jury"
|
||||
% (len(lesEtudidsManquants))
|
||||
+ ": "
|
||||
+ ", ".join(lesEtudidsManquants)
|
||||
)
|
||||
pe_tools.pe_print(u" - Export csv")
|
||||
pe_tools.pe_print(" - Export csv")
|
||||
filename = self.NOM_EXPORT_ZIP + self.semTagDict[fid].nom + ".csv"
|
||||
self.zipfile.writestr(filename, self.semTagDict[fid].str_tagtable())
|
||||
|
||||
|
@ -742,7 +745,7 @@ class JuryPE(object):
|
|||
|
||||
for fid in fids_finaux:
|
||||
if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 1:
|
||||
pe_tools.pe_print(u" - semestre final %s" % (fid))
|
||||
pe_tools.pe_print(" - semestre final %s" % (fid))
|
||||
settag = pe_settag.SetTag(
|
||||
nom, parcours=parcours
|
||||
) # Le set tag fusionnant les données
|
||||
|
@ -762,7 +765,7 @@ class JuryPE(object):
|
|||
for ffid in settag.get_Fids_in_settag():
|
||||
if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 1:
|
||||
pe_tools.pe_print(
|
||||
u" -> ajout du semestre tagué %s" % (ffid)
|
||||
" -> ajout du semestre tagué %s" % (ffid)
|
||||
)
|
||||
self.add_semtags_in_jury(ffid)
|
||||
settag.set_SemTagDict(
|
||||
|
@ -791,7 +794,7 @@ class JuryPE(object):
|
|||
if nbreEtudInscrits > 0:
|
||||
if pe_tools.PE_DEBUG:
|
||||
pe_tools.pe_print(
|
||||
u"%d) %s avec interclassement sur la promo" % (i + 1, nom)
|
||||
"%d) %s avec interclassement sur la promo" % (i + 1, nom)
|
||||
)
|
||||
if nom in ["S1", "S2", "S3", "S4"]:
|
||||
settag.set_SetTagDict(self.semTagDict)
|
||||
|
@ -802,7 +805,7 @@ class JuryPE(object):
|
|||
else:
|
||||
if pe_tools.PE_DEBUG:
|
||||
pe_tools.pe_print(
|
||||
u"%d) Pas d'interclassement %s sur la promo faute de notes"
|
||||
"%d) Pas d'interclassement %s sur la promo faute de notes"
|
||||
% (i + 1, nom)
|
||||
)
|
||||
|
||||
|
@ -1152,11 +1155,14 @@ class JuryPE(object):
|
|||
return sesSems
|
||||
|
||||
# **********************************************
|
||||
def calcul_anneePromoDUT_d_un_etudiant(self, etudid):
|
||||
def calcul_anneePromoDUT_d_un_etudiant(self, etudid) -> int:
|
||||
"""Calcule et renvoie la date de diplome prévue pour un étudiant fourni avec son etudid
|
||||
en fonction de sesSemestres de scolarisation"""
|
||||
sesSemestres = self.get_semestresDUT_d_un_etudiant(etudid)
|
||||
return max([get_annee_diplome_semestre(sem) for sem in sesSemestres])
|
||||
en fonction de ses semestres de scolarisation"""
|
||||
semestres = self.get_semestresDUT_d_un_etudiant(etudid)
|
||||
if semestres:
|
||||
return max([get_annee_diplome_semestre(sem) for sem in semestres])
|
||||
else:
|
||||
return None
|
||||
|
||||
# *********************************************
|
||||
# Fonctions d'affichage pour debug
|
||||
|
@ -1184,18 +1190,21 @@ class JuryPE(object):
|
|||
chaine += "\n"
|
||||
return chaine
|
||||
|
||||
def get_date_entree_etudiant(self, etudid):
|
||||
"""Renvoie la date d'entree d'un étudiant"""
|
||||
return str(
|
||||
min([int(sem["annee_debut"]) for sem in self.ETUDINFO_DICT[etudid]["sems"]])
|
||||
)
|
||||
def get_date_entree_etudiant(self, etudid) -> str:
|
||||
"""Renvoie la date d'entree d'un étudiant: "1996" """
|
||||
annees_debut = [
|
||||
int(sem["annee_debut"]) for sem in self.ETUDINFO_DICT[etudid]["sems"]
|
||||
]
|
||||
if annees_debut:
|
||||
return str(min(annees_debut))
|
||||
return ""
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------------------
|
||||
# Fonctions
|
||||
|
||||
# ----------------------------------------------------------------------------------------
|
||||
def get_annee_diplome_semestre(sem):
|
||||
def get_annee_diplome_semestre(sem) -> int:
|
||||
"""Pour un semestre donne, décrit par le biais du dictionnaire sem usuel :
|
||||
sem = {'formestre_id': ..., 'semestre_id': ..., 'annee_debut': ...},
|
||||
à condition qu'il soit un semestre de formation DUT,
|
||||
|
|
|
@ -51,27 +51,34 @@ from app.pe import pe_avislatex
|
|||
def _pe_view_sem_recap_form(formsemestre_id):
|
||||
H = [
|
||||
html_sco_header.sco_header(page_title="Avis de poursuite d'études"),
|
||||
"""<h2 class="formsemestre">Génération des avis de poursuites d'études</h2>
|
||||
f"""<h2 class="formsemestre">Génération des avis de poursuites d'études</h2>
|
||||
<p class="help">
|
||||
Cette fonction génère un ensemble de fichiers permettant d'éditer des avis de poursuites d'études.
|
||||
Cette fonction génère un ensemble de fichiers permettant d'éditer des avis de
|
||||
poursuites d'études.
|
||||
<br/>
|
||||
De nombreux aspects sont paramétrables:
|
||||
<a href="https://scodoc.org/AvisPoursuiteEtudes" target="_blank" rel="noopener noreferrer">
|
||||
De nombreux aspects sont paramétrables:
|
||||
<a href="https://scodoc.org/AvisPoursuiteEtudes" target="_blank" rel="noopener">
|
||||
voir la documentation</a>.
|
||||
</p>
|
||||
<form method="post" action="pe_view_sem_recap" id="pe_view_sem_recap_form" enctype="multipart/form-data">
|
||||
<form method="post" action="pe_view_sem_recap" id="pe_view_sem_recap_form"
|
||||
enctype="multipart/form-data">
|
||||
<div class="pe_template_up">
|
||||
Les templates sont généralement installés sur le serveur ou dans le paramétrage de ScoDoc.<br/>
|
||||
Au besoin, vous pouvez spécifier ici votre propre fichier de template (<tt>un_avis.tex</tt>):
|
||||
<div class="pe_template_upb">Template: <input type="file" size="30" name="avis_tmpl_file"/></div>
|
||||
<div class="pe_template_upb">Pied de page: <input type="file" size="30" name="footer_tmpl_file"/></div>
|
||||
Les templates sont généralement installés sur le serveur ou dans le
|
||||
paramétrage de ScoDoc.
|
||||
<br/>
|
||||
Au besoin, vous pouvez spécifier ici votre propre fichier de template
|
||||
(<tt>un_avis.tex</tt>):
|
||||
<div class="pe_template_upb">Template:
|
||||
<input type="file" size="30" name="avis_tmpl_file"/>
|
||||
</div>
|
||||
<div class="pe_template_upb">Pied de page:
|
||||
<input type="file" size="30" name="footer_tmpl_file"/>
|
||||
</div>
|
||||
</div>
|
||||
<input type="submit" value="Générer les documents"/>
|
||||
<input type="hidden" name="formsemestre_id" value="{formsemestre_id}">
|
||||
</form>
|
||||
""".format(
|
||||
formsemestre_id=formsemestre_id
|
||||
),
|
||||
""",
|
||||
]
|
||||
return "\n".join(H) + html_sco_header.sco_footer()
|
||||
|
||||
|
|
|
@ -121,6 +121,7 @@ class GenTable(object):
|
|||
html_with_td_classes=False, # put class=column_id in each <td>
|
||||
html_before_table="", # html snippet to put before the <table> in the page
|
||||
html_empty_element="", # replace table when empty
|
||||
html_table_attrs="", # for html
|
||||
base_url=None,
|
||||
origin=None, # string added to excel and xml versions
|
||||
filename="table", # filename, without extension
|
||||
|
@ -146,6 +147,7 @@ class GenTable(object):
|
|||
self.html_header = html_header
|
||||
self.html_before_table = html_before_table
|
||||
self.html_empty_element = html_empty_element
|
||||
self.html_table_attrs = html_table_attrs
|
||||
self.page_title = page_title
|
||||
self.pdf_link = pdf_link
|
||||
self.xls_link = xls_link
|
||||
|
@ -209,7 +211,8 @@ class GenTable(object):
|
|||
omit_hidden_lines=False,
|
||||
pdf_mode=False, # apply special pdf reportlab processing
|
||||
pdf_style_list=[], # modified: list of platypus table style commands
|
||||
):
|
||||
xls_mode=False, # get xls content if available
|
||||
) -> list:
|
||||
"table data as a list of lists (rows)"
|
||||
T = []
|
||||
line_num = 0 # line number in input data
|
||||
|
@ -237,9 +240,14 @@ class GenTable(object):
|
|||
# if colspan_count > 0:
|
||||
# continue # skip cells after a span
|
||||
if pdf_mode:
|
||||
content = row.get(f"_{cid}_pdf", "") or row.get(cid, "") or ""
|
||||
content = row.get(f"_{cid}_pdf", False) or row.get(cid, "")
|
||||
elif xls_mode:
|
||||
content = row.get(f"_{cid}_xls", False) or row.get(cid, "")
|
||||
else:
|
||||
content = row.get(cid, "") or "" # nota: None converted to ''
|
||||
content = row.get(cid, "")
|
||||
# Convert None to empty string ""
|
||||
content = "" if content is None else content
|
||||
|
||||
colspan = row.get("_%s_colspan" % cid, 0)
|
||||
if colspan > 1:
|
||||
pdf_style_list.append(
|
||||
|
@ -299,7 +307,7 @@ class GenTable(object):
|
|||
return self.xml()
|
||||
elif format == "json":
|
||||
return self.json()
|
||||
raise ValueError("GenTable: invalid format: %s" % format)
|
||||
raise ValueError(f"GenTable: invalid format: {format}")
|
||||
|
||||
def _gen_html_row(self, row, line_num=0, elem="td", css_classes=""):
|
||||
"row is a dict, returns a string <tr...>...</tr>"
|
||||
|
@ -377,12 +385,16 @@ class GenTable(object):
|
|||
colspan_count = colspan
|
||||
else:
|
||||
colspan_txt = ""
|
||||
attrs = row.get("_%s_td_attrs" % cid, "")
|
||||
order = row.get(f"_{cid}_order")
|
||||
if order:
|
||||
attrs += f' data-order="{order}"'
|
||||
r.append(
|
||||
"<%s%s %s%s%s>%s</%s>"
|
||||
% (
|
||||
elem,
|
||||
std,
|
||||
row.get("_%s_td_attrs" % cid, ""),
|
||||
attrs,
|
||||
klass,
|
||||
colspan_txt,
|
||||
content,
|
||||
|
@ -407,8 +419,7 @@ class GenTable(object):
|
|||
cls = ' class="%s"' % " ".join(tablclasses)
|
||||
else:
|
||||
cls = ""
|
||||
|
||||
H = [self.html_before_table, "<table%s%s>" % (hid, cls)]
|
||||
H = [self.html_before_table, f"<table{hid}{cls} {self.html_table_attrs}>"]
|
||||
|
||||
line_num = 0
|
||||
# thead
|
||||
|
@ -479,23 +490,23 @@ class GenTable(object):
|
|||
def excel(self, wb=None):
|
||||
"""Simple Excel representation of the table"""
|
||||
if wb is None:
|
||||
ses = sco_excel.ScoExcelSheet(sheet_name=self.xls_sheet_name, wb=wb)
|
||||
sheet = sco_excel.ScoExcelSheet(sheet_name=self.xls_sheet_name, wb=wb)
|
||||
else:
|
||||
ses = wb.create_sheet(sheet_name=self.xls_sheet_name)
|
||||
ses.rows += self.xls_before_table
|
||||
sheet = wb.create_sheet(sheet_name=self.xls_sheet_name)
|
||||
sheet.rows += self.xls_before_table
|
||||
style_bold = sco_excel.excel_make_style(bold=True)
|
||||
style_base = sco_excel.excel_make_style()
|
||||
ses.append_row(ses.make_row(self.get_titles_list(), style_bold))
|
||||
for line in self.get_data_list():
|
||||
ses.append_row(ses.make_row(line, style_base))
|
||||
sheet.append_row(sheet.make_row(self.get_titles_list(), style_bold))
|
||||
for line in self.get_data_list(xls_mode=True):
|
||||
sheet.append_row(sheet.make_row(line, style_base))
|
||||
if self.caption:
|
||||
ses.append_blank_row() # empty line
|
||||
ses.append_single_cell_row(self.caption, style_base)
|
||||
sheet.append_blank_row() # empty line
|
||||
sheet.append_single_cell_row(self.caption, style_base)
|
||||
if self.origin:
|
||||
ses.append_blank_row() # empty line
|
||||
ses.append_single_cell_row(self.origin, style_base)
|
||||
sheet.append_blank_row() # empty line
|
||||
sheet.append_single_cell_row(self.origin, style_base)
|
||||
if wb is None:
|
||||
return ses.generate()
|
||||
return sheet.generate()
|
||||
|
||||
def text(self):
|
||||
"raw text representation of the table"
|
||||
|
|
|
@ -57,7 +57,6 @@ def sidebar_common():
|
|||
<a href="{scu.AbsencesURL()}" class="sidebar">Absences</a> <br/>
|
||||
"""
|
||||
]
|
||||
|
||||
if current_user.has_permission(
|
||||
Permission.ScoUsersAdmin
|
||||
) or current_user.has_permission(Permission.ScoUsersView):
|
||||
|
@ -86,9 +85,9 @@ def sidebar():
|
|||
f"""<div class="sidebar">
|
||||
{ sidebar_common() }
|
||||
<div class="box-chercheetud">Chercher étudiant:<br/>
|
||||
<form method="get" id="form-chercheetud"
|
||||
action="{ url_for('scolar.search_etud_in_dept', scodoc_dept=g.scodoc_dept) }">
|
||||
<div><input type="text" size="12" id="in-expnom" name="expnom" spellcheck="false"></input></div>
|
||||
<form method="get" id="form-chercheetud"
|
||||
action="{url_for('scolar.search_etud_in_dept', scodoc_dept=g.scodoc_dept) }">
|
||||
<div><input type="text" size="12" class="in-expnom" name="expnom" spellcheck="false"></input></div>
|
||||
</form></div>
|
||||
<div class="etud-insidebar">
|
||||
"""
|
||||
|
|
|
@ -51,8 +51,8 @@ from app.scodoc.sco_formsemestre import (
|
|||
from app.scodoc.sco_codes_parcours import (
|
||||
DEF,
|
||||
UE_SPORT,
|
||||
UE_is_fondamentale,
|
||||
UE_is_professionnelle,
|
||||
ue_is_fondamentale,
|
||||
ue_is_professionnelle,
|
||||
)
|
||||
from app.scodoc.sco_parcours_dut import formsemestre_get_etud_capitalisation
|
||||
from app.scodoc import sco_codes_parcours
|
||||
|
@ -826,11 +826,11 @@ class NotesTable:
|
|||
and mu["moy"] >= self.parcours.NOTES_BARRE_VALID_UE
|
||||
):
|
||||
mu["ects_pot"] = ue["ects"] or 0.0
|
||||
if UE_is_fondamentale(ue["type"]):
|
||||
if ue_is_fondamentale(ue["type"]):
|
||||
mu["ects_pot_fond"] = mu["ects_pot"]
|
||||
else:
|
||||
mu["ects_pot_fond"] = 0.0
|
||||
if UE_is_professionnelle(ue["type"]):
|
||||
if ue_is_professionnelle(ue["type"]):
|
||||
mu["ects_pot_pro"] = mu["ects_pot"]
|
||||
else:
|
||||
mu["ects_pot_pro"] = 0.0
|
||||
|
|
|
@ -31,9 +31,7 @@
|
|||
import calendar
|
||||
import datetime
|
||||
import html
|
||||
import string
|
||||
import time
|
||||
import types
|
||||
|
||||
from app.scodoc import notesdb as ndb
|
||||
from app import log
|
||||
|
@ -42,9 +40,9 @@ from app.scodoc.sco_exceptions import ScoValueError, ScoInvalidDateError
|
|||
from app.scodoc import sco_abs_notification
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
from app.scodoc import sco_preferences
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
# --- Misc tools.... ------------------
|
||||
|
||||
|
@ -229,8 +227,11 @@ def DateRangeISO(date_beg, date_end, workable=1):
|
|||
date_end = date_beg
|
||||
r = []
|
||||
work_saturday = is_work_saturday()
|
||||
cur = ddmmyyyy(date_beg, work_saturday=work_saturday)
|
||||
end = ddmmyyyy(date_end, work_saturday=work_saturday)
|
||||
try:
|
||||
cur = ddmmyyyy(date_beg, work_saturday=work_saturday)
|
||||
end = ddmmyyyy(date_end, work_saturday=work_saturday)
|
||||
except (AttributeError, ValueError) as e:
|
||||
raise ScoValueError("date invalide !") from e
|
||||
while cur <= end:
|
||||
if (not workable) or cur.iswork():
|
||||
r.append(cur)
|
||||
|
@ -244,9 +245,9 @@ def day_names():
|
|||
If work_saturday property is set, include saturday
|
||||
"""
|
||||
if is_work_saturday():
|
||||
return ["Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"]
|
||||
return scu.DAY_NAMES[:-1]
|
||||
else:
|
||||
return ["Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi"]
|
||||
return scu.DAY_NAMES[:-2]
|
||||
|
||||
|
||||
def next_iso_day(date):
|
||||
|
@ -302,12 +303,14 @@ def YearTable(
|
|||
return "\n".join(T)
|
||||
|
||||
|
||||
def list_abs_in_range(etudid, debut, fin, matin=None, moduleimpl_id=None, cursor=None):
|
||||
def list_abs_in_range(
|
||||
etudid, debut=None, fin=None, matin=None, moduleimpl_id=None, cursor=None
|
||||
):
|
||||
"""Liste des absences entre deux dates.
|
||||
|
||||
Args:
|
||||
etudid:
|
||||
debut: string iso date ("2020-03-12")
|
||||
debut: string iso date ("2020-03-12") ou None
|
||||
end: string iso date ("2020-03-12")
|
||||
matin: None, True, False
|
||||
moduleimpl_id: restreint le comptage aux absences dans ce module
|
||||
|
@ -334,9 +337,13 @@ def list_abs_in_range(etudid, debut, fin, matin=None, moduleimpl_id=None, cursor
|
|||
AND A.ESTABS"""
|
||||
+ ismatin
|
||||
+ modul
|
||||
+ """
|
||||
+ (
|
||||
""
|
||||
if debut is None
|
||||
else """
|
||||
AND A.JOUR BETWEEN %(debut)s AND %(fin)s
|
||||
""",
|
||||
"""
|
||||
),
|
||||
{
|
||||
"etudid": etudid,
|
||||
"debut": debut,
|
||||
|
@ -411,22 +418,29 @@ WHERE A.ETUDID = %(etudid)s
|
|||
return res
|
||||
|
||||
|
||||
def list_abs_date(etudid, beg_date, end_date):
|
||||
def list_abs_date(etudid, beg_date=None, end_date=None):
|
||||
"""Liste des absences et justifs entre deux dates (inclues)."""
|
||||
print("On rentre")
|
||||
cnx = ndb.GetDBConnexion()
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
print("Juste avant le SQL")
|
||||
|
||||
cursor.execute(
|
||||
"""SELECT jour, matin, estabs, estjust, description FROM ABSENCES A
|
||||
WHERE A.ETUDID = %(etudid)s
|
||||
WHERE A.ETUDID = %(etudid)s"""
|
||||
+ ""
|
||||
if beg_date is None
|
||||
else """
|
||||
AND A.jour >= %(beg_date)s
|
||||
AND A.jour <= %(end_date)s
|
||||
""",
|
||||
vars(),
|
||||
)
|
||||
Abs = cursor.dictfetchall()
|
||||
|
||||
absences = cursor.dictfetchall()
|
||||
# remove duplicates
|
||||
A = {} # { (jour, matin) : abs }
|
||||
for a in Abs:
|
||||
for a in absences:
|
||||
jour, matin = a["jour"], a["matin"]
|
||||
if (jour, matin) in A:
|
||||
# garde toujours la description
|
||||
|
|
|
@ -272,9 +272,15 @@ def _build_etud_res(e, apo_data):
|
|||
r = {}
|
||||
for elt_code in apo_data.apo_elts:
|
||||
elt = apo_data.apo_elts[elt_code]
|
||||
col_ids_type = [
|
||||
(ec["apoL_a01_code"], ec["Type R\xc3\xa9s."]) for ec in elt.cols
|
||||
] # les colonnes de cet élément
|
||||
try:
|
||||
# les colonnes de cet élément
|
||||
col_ids_type = [
|
||||
(ec["apoL_a01_code"], ec["Type R\xc3\xa9s."]) for ec in elt.cols
|
||||
]
|
||||
except KeyError as exc:
|
||||
raise ScoValueError(
|
||||
"Erreur: un élément sans 'Type R\xc3\xa9s.'. Vérifiez l'encodage de vos fichiers."
|
||||
) from exc
|
||||
r[elt_code] = {}
|
||||
for (col_id, type_res) in col_ids_type:
|
||||
r[elt_code][type_res] = e.cols[col_id]
|
||||
|
|
|
@ -83,6 +83,7 @@ XXX A vérifier:
|
|||
import collections
|
||||
import datetime
|
||||
from functools import reduce
|
||||
import functools
|
||||
import io
|
||||
import os
|
||||
import pprint
|
||||
|
@ -125,7 +126,7 @@ APO_SEP = "\t"
|
|||
APO_NEWLINE = "\r\n"
|
||||
|
||||
|
||||
def _apo_fmt_note(note):
|
||||
def _apo_fmt_note(note, fmt="%3.2f"):
|
||||
"Formatte une note pour Apogée (séparateur décimal: ',')"
|
||||
# if not note and isinstance(note, float): changé le 31/1/2022, étrange ?
|
||||
# return ""
|
||||
|
@ -133,7 +134,7 @@ def _apo_fmt_note(note):
|
|||
val = float(note)
|
||||
except ValueError:
|
||||
return ""
|
||||
return ("%3.2f" % val).replace(".", APO_DECIMAL_SEP)
|
||||
return (fmt % val).replace(".", APO_DECIMAL_SEP)
|
||||
|
||||
|
||||
def guess_data_encoding(text, threshold=0.6):
|
||||
|
@ -270,6 +271,9 @@ class ApoEtud(dict):
|
|||
self.export_res_modules = export_res_modules
|
||||
self.export_res_sdj = export_res_sdj # export meme si pas de decision de jury
|
||||
self.export_res_rat = export_res_rat
|
||||
self.fmt_note = functools.partial(
|
||||
_apo_fmt_note, fmt=ScoDocSiteConfig.get_code_apo("NOTES_FMT") or "3.2f"
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return "ApoEtud( nom='%s', nip='%s' )" % (self["nom"], self["nip"])
|
||||
|
@ -392,7 +396,7 @@ class ApoEtud(dict):
|
|||
|
||||
# Element etape (annuel ou non):
|
||||
if sco_formsemestre.sem_has_etape(sem, code) or (
|
||||
code in sem["elt_annee_apo"].split(",")
|
||||
code in {x.strip() for x in sem["elt_annee_apo"].split(",")}
|
||||
):
|
||||
export_res_etape = self.export_res_etape
|
||||
if (not export_res_etape) and cur_sem:
|
||||
|
@ -408,7 +412,7 @@ class ApoEtud(dict):
|
|||
return VOID_APO_RES
|
||||
|
||||
# Element semestre:
|
||||
if code in sem["elt_sem_apo"].split(","):
|
||||
if code in {x.strip() for x in sem["elt_sem_apo"].split(",")}:
|
||||
if self.export_res_sem:
|
||||
return self.comp_elt_semestre(nt, decision, etudid)
|
||||
else:
|
||||
|
@ -417,13 +421,15 @@ class ApoEtud(dict):
|
|||
# Elements UE
|
||||
decisions_ue = nt.get_etud_decision_ues(etudid)
|
||||
for ue in nt.get_ues_stat_dict():
|
||||
if ue["code_apogee"] and code in ue["code_apogee"].split(","):
|
||||
if ue["code_apogee"] and code in {
|
||||
x.strip() for x in ue["code_apogee"].split(",")
|
||||
}:
|
||||
if self.export_res_ues:
|
||||
if decisions_ue and ue["ue_id"] in decisions_ue:
|
||||
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
|
||||
code_decision_ue = decisions_ue[ue["ue_id"]]["code"]
|
||||
return dict(
|
||||
N=_apo_fmt_note(ue_status["moy"] if ue_status else ""),
|
||||
N=self.fmt_note(ue_status["moy"] if ue_status else ""),
|
||||
B=20,
|
||||
J="",
|
||||
R=ScoDocSiteConfig.get_code_apo(code_decision_ue),
|
||||
|
@ -438,12 +444,13 @@ class ApoEtud(dict):
|
|||
modimpls = nt.get_modimpls_dict()
|
||||
module_code_found = False
|
||||
for modimpl in modimpls:
|
||||
if modimpl["module"]["code_apogee"] and code in modimpl["module"][
|
||||
"code_apogee"
|
||||
].split(","):
|
||||
module = modimpl["module"]
|
||||
if module["code_apogee"] and code in {
|
||||
x.strip() for x in module["code_apogee"].split(",")
|
||||
}:
|
||||
n = nt.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid)
|
||||
if n != "NI" and self.export_res_modules:
|
||||
return dict(N=_apo_fmt_note(n), B=20, J="", R="")
|
||||
return dict(N=self.fmt_note(n), B=20, J="", R="")
|
||||
else:
|
||||
module_code_found = True
|
||||
if module_code_found:
|
||||
|
@ -465,7 +472,7 @@ class ApoEtud(dict):
|
|||
if decision_apo == "DEF" or decision["code"] == DEM or decision["code"] == DEF:
|
||||
note_str = "0,01" # note non nulle pour les démissionnaires
|
||||
else:
|
||||
note_str = _apo_fmt_note(note)
|
||||
note_str = self.fmt_note(note)
|
||||
return dict(N=note_str, B=20, J="", R=decision_apo, M="")
|
||||
|
||||
def comp_elt_annuel(self, etudid, cur_sem, autre_sem):
|
||||
|
@ -531,7 +538,7 @@ class ApoEtud(dict):
|
|||
moy_annuelle = (note + autre_note) / 2
|
||||
except TypeError:
|
||||
moy_annuelle = ""
|
||||
note_str = _apo_fmt_note(moy_annuelle)
|
||||
note_str = self.fmt_note(moy_annuelle)
|
||||
|
||||
if code_semestre_validant(autre_decision["code"]):
|
||||
decision_apo_annuelle = decision_apo
|
||||
|
@ -945,8 +952,9 @@ class ApoData(object):
|
|||
return maq_elems, sem_elems
|
||||
|
||||
def get_codes_by_sem(self):
|
||||
"""Pour chaque semestre associé, donne l'ensemble des codes Apogée qui s'y trouvent
|
||||
(dans le semestre, les UE et les modules)
|
||||
"""Pour chaque semestre associé, donne l'ensemble des codes de cette maquette Apogée
|
||||
qui s'y trouvent (dans le semestre, les UE ou les modules).
|
||||
Return: { formsemestre_id : { 'code1', 'code2', ... }}
|
||||
"""
|
||||
codes_by_sem = {}
|
||||
for sem in self.sems_etape:
|
||||
|
@ -957,8 +965,8 @@ class ApoData(object):
|
|||
# associé à l'étape, l'année ou les semestre:
|
||||
if (
|
||||
sco_formsemestre.sem_has_etape(sem, code)
|
||||
or (code in sem["elt_sem_apo"].split(","))
|
||||
or (code in sem["elt_annee_apo"].split(","))
|
||||
or (code in {x.strip() for x in sem["elt_sem_apo"].split(",")})
|
||||
or (code in {x.strip() for x in sem["elt_annee_apo"].split(",")})
|
||||
):
|
||||
s.add(code)
|
||||
continue
|
||||
|
@ -966,17 +974,20 @@ class ApoData(object):
|
|||
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
for ue in nt.get_ues_stat_dict():
|
||||
if ue["code_apogee"] and code in ue["code_apogee"].split(","):
|
||||
s.add(code)
|
||||
continue
|
||||
if ue["code_apogee"]:
|
||||
codes = {x.strip() for x in ue["code_apogee"].split(",")}
|
||||
if code in codes:
|
||||
s.add(code)
|
||||
continue
|
||||
# associé à un module:
|
||||
modimpls = nt.get_modimpls_dict()
|
||||
for modimpl in modimpls:
|
||||
if modimpl["module"]["code_apogee"] and code in modimpl["module"][
|
||||
"code_apogee"
|
||||
].split(","):
|
||||
s.add(code)
|
||||
continue
|
||||
module = modimpl["module"]
|
||||
if module["code_apogee"]:
|
||||
codes = {x.strip() for x in module["code_apogee"].split(",")}
|
||||
if code in codes:
|
||||
s.add(code)
|
||||
continue
|
||||
# log('codes_by_sem=%s' % pprint.pformat(codes_by_sem))
|
||||
return codes_by_sem
|
||||
|
||||
|
|
|
@ -47,14 +47,15 @@
|
|||
qui est une description (humaine, format libre) de l'archive.
|
||||
|
||||
"""
|
||||
import chardet
|
||||
import datetime
|
||||
import glob
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import time
|
||||
import chardet
|
||||
|
||||
import flask
|
||||
from flask import g, request
|
||||
|
@ -63,7 +64,9 @@ from flask_login import current_user
|
|||
import app.scodoc.sco_utils as scu
|
||||
from config import Config
|
||||
from app import log
|
||||
from app.models import Departement
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import Departement, FormSemestre
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||
from app.scodoc.sco_exceptions import (
|
||||
AccessDenied,
|
||||
|
@ -95,8 +98,8 @@ class BaseArchiver(object):
|
|||
self.root = os.path.join(*dirs)
|
||||
log("initialized archiver, path=" + self.root)
|
||||
path = dirs[0]
|
||||
for dir in dirs[1:]:
|
||||
path = os.path.join(path, dir)
|
||||
for directory in dirs[1:]:
|
||||
path = os.path.join(path, directory)
|
||||
try:
|
||||
scu.GSL.acquire()
|
||||
if not os.path.isdir(path):
|
||||
|
@ -117,11 +120,11 @@ class BaseArchiver(object):
|
|||
try:
|
||||
scu.GSL.acquire()
|
||||
if not os.path.isdir(dept_dir):
|
||||
log("creating directory %s" % dept_dir)
|
||||
log(f"creating directory {dept_dir}")
|
||||
os.mkdir(dept_dir)
|
||||
obj_dir = os.path.join(dept_dir, str(oid))
|
||||
if not os.path.isdir(obj_dir):
|
||||
log("creating directory %s" % obj_dir)
|
||||
log(f"creating directory {obj_dir}")
|
||||
os.mkdir(obj_dir)
|
||||
finally:
|
||||
scu.GSL.release()
|
||||
|
@ -163,8 +166,9 @@ class BaseArchiver(object):
|
|||
|
||||
def get_archive_date(self, archive_id):
|
||||
"""Returns date (as a DateTime object) of an archive"""
|
||||
dt = [int(x) for x in os.path.split(archive_id)[1].split("-")]
|
||||
return datetime.datetime(*dt)
|
||||
return datetime.datetime(
|
||||
*[int(x) for x in os.path.split(archive_id)[1].split("-")]
|
||||
)
|
||||
|
||||
def list_archive(self, archive_id: str) -> str:
|
||||
"""Return list of filenames (without path) in archive"""
|
||||
|
@ -195,8 +199,7 @@ class BaseArchiver(object):
|
|||
archive_id = os.path.join(self.get_obj_dir(oid), archive_name)
|
||||
if not os.path.isdir(archive_id):
|
||||
log(
|
||||
"invalid archive name: %s, oid=%s, archive_id=%s"
|
||||
% (archive_name, oid, archive_id)
|
||||
f"invalid archive name: {archive_name}, oid={oid}, archive_id={archive_id}"
|
||||
)
|
||||
raise ValueError("invalid archive name")
|
||||
return archive_id
|
||||
|
@ -223,7 +226,7 @@ class BaseArchiver(object):
|
|||
+ os.path.sep
|
||||
+ "-".join(["%02d" % x for x in time.localtime()[:6]])
|
||||
)
|
||||
log("creating archive: %s" % archive_id)
|
||||
log(f"creating archive: {archive_id}")
|
||||
try:
|
||||
scu.GSL.acquire()
|
||||
os.mkdir(archive_id) # if exists, raises an OSError
|
||||
|
@ -302,9 +305,14 @@ def do_formsemestre_archive(
|
|||
Store:
|
||||
- tableau recap (xls), pv jury (xls et pdf), bulletins (xml et pdf), lettres individuelles (pdf)
|
||||
"""
|
||||
from app.scodoc.sco_recapcomplet import make_formsemestre_recapcomplet
|
||||
from app.scodoc.sco_recapcomplet import (
|
||||
gen_formsemestre_recapcomplet_excel,
|
||||
gen_formsemestre_recapcomplet_html,
|
||||
gen_formsemestre_recapcomplet_json,
|
||||
)
|
||||
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
sem_archive_id = formsemestre_id
|
||||
archive_id = PVArchive.create_obj_archive(sem_archive_id, description)
|
||||
date = PVArchive.get_archive_date(archive_id).strftime("%d/%m/%Y à %H:%M")
|
||||
|
@ -319,37 +327,38 @@ def do_formsemestre_archive(
|
|||
etudids = [m["etudid"] for m in groups_infos.members]
|
||||
|
||||
# Tableau recap notes en XLS (pour tous les etudiants, n'utilise pas les groupes)
|
||||
data, _, _ = make_formsemestre_recapcomplet(formsemestre_id, format="xls")
|
||||
data, _ = gen_formsemestre_recapcomplet_excel(
|
||||
formsemestre, res, include_evaluations=True, format="xls"
|
||||
)
|
||||
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)
|
||||
data, _, _ = make_formsemestre_recapcomplet(
|
||||
formsemestre_id, format="html", disable_etudlink=True
|
||||
table_html = gen_formsemestre_recapcomplet_html(
|
||||
formsemestre, res, include_evaluations=True
|
||||
)
|
||||
if data:
|
||||
if table_html:
|
||||
data = "\n".join(
|
||||
[
|
||||
html_sco_header.sco_header(
|
||||
page_title="Moyennes archivées le %s" % date,
|
||||
head_message="Moyennes archivées le %s" % date,
|
||||
page_title=f"Moyennes archivées le {date}",
|
||||
head_message=f"Moyennes archivées le {date}",
|
||||
no_side_bar=True,
|
||||
),
|
||||
'<h2 class="fontorange">Valeurs archivées le %s</h2>' % date,
|
||||
f'<h2 class="fontorange">Valeurs archivées le {date}</h2>',
|
||||
'<style type="text/css">table.notes_recapcomplet tr { color: rgb(185,70,0); }</style>',
|
||||
data,
|
||||
table_html,
|
||||
html_sco_header.sco_footer(),
|
||||
]
|
||||
)
|
||||
data = data.encode(scu.SCO_ENCODING)
|
||||
PVArchive.store(archive_id, "Tableau_moyennes.html", data)
|
||||
|
||||
# Bulletins en XML (pour tous les etudiants, n'utilise pas les groupes)
|
||||
data, _, _ = make_formsemestre_recapcomplet(
|
||||
formsemestre_id, format="xml", xml_with_decisions=True
|
||||
)
|
||||
# Bulletins en JSON
|
||||
data = gen_formsemestre_recapcomplet_json(formsemestre_id, xml_with_decisions=True)
|
||||
data_js = json.dumps(data, indent=1, cls=scu.ScoDocJSONEncoder)
|
||||
data_js = data_js.encode(scu.SCO_ENCODING)
|
||||
if data:
|
||||
data = data.encode(scu.SCO_ENCODING)
|
||||
PVArchive.store(archive_id, "Bulletins.xml", data)
|
||||
PVArchive.store(archive_id, "Bulletins.json", data_js)
|
||||
# Decisions de jury, en XLS
|
||||
data = sco_pvjury.formsemestre_pvjury(formsemestre_id, format="xls", publish=False)
|
||||
if data:
|
||||
|
|
|
@ -32,7 +32,7 @@ import email
|
|||
import time
|
||||
|
||||
from flask import g, request
|
||||
from flask import flash, render_template, url_for
|
||||
from flask import flash, jsonify, render_template, url_for
|
||||
from flask_login import current_user
|
||||
|
||||
from app import email
|
||||
|
@ -76,6 +76,34 @@ from app.scodoc import sco_bulletins_legacy
|
|||
from app.scodoc import sco_bulletins_ucac # format expérimental UCAC Cameroun
|
||||
|
||||
|
||||
def get_formsemestre_bulletin_etud_json(
|
||||
formsemestre: FormSemestre,
|
||||
etud: Identite,
|
||||
force_publishing=False,
|
||||
version="long",
|
||||
) -> str:
|
||||
"""Le JSON du bulletin d'un étudiant, quel que soit le type de formation."""
|
||||
if formsemestre.formation.is_apc():
|
||||
r = bulletin_but.BulletinBUT(formsemestre)
|
||||
return jsonify(
|
||||
r.bulletin_etud(
|
||||
etud,
|
||||
formsemestre,
|
||||
force_publishing=force_publishing,
|
||||
version=version,
|
||||
)
|
||||
)
|
||||
return formsemestre_bulletinetud(
|
||||
etud,
|
||||
formsemestre_id=formsemestre.id,
|
||||
format="json",
|
||||
version=version,
|
||||
xml_with_decisions=True,
|
||||
force_publishing=force_publishing,
|
||||
)
|
||||
|
||||
|
||||
# -------------
|
||||
def make_context_dict(formsemestre: FormSemestre, etud: dict) -> dict:
|
||||
"""Construit dictionnaire avec valeurs pour substitution des textes
|
||||
(preferences bul_pdf_*)
|
||||
|
@ -799,7 +827,10 @@ def formsemestre_bulletinetud(
|
|||
force_publishing=False, # force publication meme si semestre non publie sur "portail"
|
||||
prefer_mail_perso=False,
|
||||
):
|
||||
"page bulletin de notes"
|
||||
"""Page bulletin de notes
|
||||
pour les formations classiques hors BUT (page HTML)
|
||||
ou le format "oldjson".
|
||||
"""
|
||||
format = format or "html"
|
||||
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||
if not formsemestre:
|
||||
|
|
|
@ -49,7 +49,6 @@
|
|||
# sco_cache.EvaluationCache.get(evaluation_id), set(evaluation_id, value), delete(evaluation_id),
|
||||
#
|
||||
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from flask import g
|
||||
|
@ -198,6 +197,26 @@ class SemInscriptionsCache(ScoDocCache):
|
|||
duration = 12 * 60 * 60 # ttl 12h
|
||||
|
||||
|
||||
class TableRecapCache(ScoDocCache):
|
||||
"""Cache table recap (pour get_table_recap)
|
||||
Clé: formsemestre_id
|
||||
Valeur: le html (<div class="table_recap">...</div>)
|
||||
"""
|
||||
|
||||
prefix = "RECAP"
|
||||
duration = 12 * 60 * 60 # ttl 12h
|
||||
|
||||
|
||||
class TableRecapWithEvalsCache(ScoDocCache):
|
||||
"""Cache table recap (pour get_table_recap)
|
||||
Clé: formsemestre_id
|
||||
Valeur: le html (<div class="table_recap">...</div>)
|
||||
"""
|
||||
|
||||
prefix = "RECAPWITHEVALS"
|
||||
duration = 12 * 60 * 60 # ttl 12h
|
||||
|
||||
|
||||
def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=False)
|
||||
formsemestre_id=None, pdfonly=False
|
||||
):
|
||||
|
@ -209,7 +228,7 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
|
|||
if getattr(g, "defer_cache_invalidation", False):
|
||||
g.sem_to_invalidate.add(formsemestre_id)
|
||||
return
|
||||
log("inval_cache, formsemestre_id=%s pdfonly=%s" % (formsemestre_id, pdfonly))
|
||||
log("inval_cache, formsemestre_id={formsemestre_id} pdfonly={pdfonly}")
|
||||
if formsemestre_id is None:
|
||||
# clear all caches
|
||||
log("----- invalidate_formsemestre: clearing all caches -----")
|
||||
|
@ -247,6 +266,9 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
|
|||
SemInscriptionsCache.delete_many(formsemestre_ids)
|
||||
ResultatsSemestreCache.delete_many(formsemestre_ids)
|
||||
ValidationsSemestreCache.delete_many(formsemestre_ids)
|
||||
TableRecapCache.delete_many(formsemestre_ids)
|
||||
TableRecapWithEvalsCache.delete_many(formsemestre_ids)
|
||||
|
||||
SemBulletinsPDFCache.invalidate_sems(formsemestre_ids)
|
||||
|
||||
|
||||
|
|
|
@ -81,11 +81,11 @@ UE_PROFESSIONNELLE = 5 # UE "professionnelle" (ISCID, ...)
|
|||
UE_OPTIONNELLE = 6 # UE non fondamentales (ILEPS, ...)
|
||||
|
||||
|
||||
def UE_is_fondamentale(ue_type):
|
||||
def ue_is_fondamentale(ue_type):
|
||||
return ue_type in (UE_STANDARD, UE_STAGE_LP, UE_PROFESSIONNELLE)
|
||||
|
||||
|
||||
def UE_is_professionnelle(ue_type):
|
||||
def ue_is_professionnelle(ue_type):
|
||||
return (
|
||||
ue_type == UE_PROFESSIONNELLE
|
||||
) # NB: les UE_PROFESSIONNELLE sont à la fois fondamentales et pro
|
||||
|
@ -211,7 +211,7 @@ DEVENIRS_NEXT2 = {NEXT_OR_NEXT2: 1, NEXT2: 1}
|
|||
|
||||
NO_SEMESTRE_ID = -1 # code semestre si pas de semestres
|
||||
|
||||
# Regles gestion parcours
|
||||
# Règles gestion parcours
|
||||
class DUTRule(object):
|
||||
def __init__(self, rule_id, premise, conclusion):
|
||||
self.rule_id = rule_id
|
||||
|
@ -222,13 +222,13 @@ class DUTRule(object):
|
|||
def match(self, state):
|
||||
"True if state match rule premise"
|
||||
assert len(state) == len(self.premise)
|
||||
for i in range(len(state)):
|
||||
for i, stat in enumerate(state):
|
||||
prem = self.premise[i]
|
||||
if isinstance(prem, (list, tuple)):
|
||||
if not state[i] in prem:
|
||||
if not stat in prem:
|
||||
return False
|
||||
else:
|
||||
if prem != ALL and prem != state[i]:
|
||||
if prem not in (ALL, stat):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
@ -244,6 +244,7 @@ class TypeParcours(object):
|
|||
COMPENSATION_UE = True # inutilisé
|
||||
BARRE_MOY = 10.0
|
||||
BARRE_UE_DEFAULT = 8.0
|
||||
BARRE_UE_DISPLAY_WARNING = 8.0
|
||||
BARRE_UE = {}
|
||||
NOTES_BARRE_VALID_UE_TH = 10.0 # seuil pour valider UE
|
||||
NOTES_BARRE_VALID_UE = NOTES_BARRE_VALID_UE_TH - NOTES_TOLERANCE # barre sur UE
|
||||
|
|
|
@ -28,11 +28,10 @@
|
|||
"""
|
||||
|
||||
"""
|
||||
from app.models import ScoDocSiteConfig
|
||||
from app.scodoc.sco_logos import write_logo, find_logo, delete_logo
|
||||
import app
|
||||
from flask import current_app
|
||||
|
||||
from app.scodoc.sco_logos import find_logo
|
||||
|
||||
|
||||
class Action:
|
||||
"""Base class for all classes describing an action from from config form."""
|
||||
|
@ -42,9 +41,9 @@ class Action:
|
|||
self.parameters = parameters
|
||||
|
||||
@staticmethod
|
||||
def build_action(parameters, stream=None):
|
||||
def build_action(parameters):
|
||||
"""Check (from parameters) if some action has to be done and
|
||||
then return list of action (or else return empty list)."""
|
||||
then return list of action (or else return None)."""
|
||||
raise NotImplementedError
|
||||
|
||||
def display(self):
|
||||
|
@ -59,6 +58,45 @@ class Action:
|
|||
GLOBAL = "_"
|
||||
|
||||
|
||||
class LogoRename(Action):
|
||||
"""Action: rename a logo
|
||||
dept_id: dept_id or '-'
|
||||
logo_id: logo_id (old name)
|
||||
new_name: new_name
|
||||
"""
|
||||
|
||||
def __init__(self, parameters):
|
||||
super().__init__(
|
||||
f"Renommage du logo {parameters['logo_id']} en {parameters['new_name']}",
|
||||
parameters,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def build_action(parameters):
|
||||
dept_id = parameters["dept_key"]
|
||||
if dept_id == GLOBAL:
|
||||
dept_id = None
|
||||
parameters["dept_id"] = dept_id
|
||||
if parameters["new_name"]:
|
||||
logo = find_logo(
|
||||
logoname=parameters["new_name"],
|
||||
dept_id=parameters["dept_key"],
|
||||
strict=True,
|
||||
)
|
||||
if logo is None:
|
||||
return LogoRename(parameters)
|
||||
|
||||
def execute(self):
|
||||
from app.scodoc.sco_logos import rename_logo
|
||||
|
||||
current_app.logger.info(self.message)
|
||||
rename_logo(
|
||||
old_name=self.parameters["logo_id"],
|
||||
new_name=self.parameters["new_name"],
|
||||
dept_id=self.parameters["dept_id"],
|
||||
)
|
||||
|
||||
|
||||
class LogoUpdate(Action):
|
||||
"""Action: change a logo
|
||||
dept_id: dept_id or '_',
|
||||
|
@ -83,6 +121,8 @@ class LogoUpdate(Action):
|
|||
return None
|
||||
|
||||
def execute(self):
|
||||
from app.scodoc.sco_logos import write_logo
|
||||
|
||||
current_app.logger.info(self.message)
|
||||
write_logo(
|
||||
stream=self.parameters["upload"],
|
||||
|
@ -113,6 +153,8 @@ class LogoDelete(Action):
|
|||
return None
|
||||
|
||||
def execute(self):
|
||||
from app.scodoc.sco_logos import delete_logo
|
||||
|
||||
current_app.logger.info(self.message)
|
||||
delete_logo(name=self.parameters["logo_id"], dept_id=self.parameters["dept_id"])
|
||||
|
||||
|
@ -136,13 +178,15 @@ class LogoInsert(Action):
|
|||
parameters["dept_id"] = None
|
||||
if parameters["upload"] and parameters["name"]:
|
||||
logo = find_logo(
|
||||
logoname=parameters["name"], dept_id=parameters["dept_key"]
|
||||
logoname=parameters["name"], dept_id=parameters["dept_key"], strict=True
|
||||
)
|
||||
if logo is None:
|
||||
return LogoInsert(parameters)
|
||||
return None
|
||||
|
||||
def execute(self):
|
||||
from app.scodoc.sco_logos import write_logo
|
||||
|
||||
dept_id = self.parameters["dept_key"]
|
||||
if dept_id == GLOBAL:
|
||||
dept_id = None
|
||||
|
|
|
@ -29,9 +29,11 @@
|
|||
"""
|
||||
|
||||
from flask import g, request
|
||||
from flask import url_for
|
||||
from flask_login import current_user
|
||||
|
||||
import app
|
||||
from app.models import ScolarNews
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
@ -40,9 +42,7 @@ import app.scodoc.notesdb as ndb
|
|||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
from app.scodoc import sco_modalites
|
||||
from app.scodoc import sco_news
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_up_to_date
|
||||
from app.scodoc import sco_users
|
||||
|
||||
|
||||
|
@ -53,7 +53,7 @@ def index_html(showcodes=0, showsemtable=0):
|
|||
H = []
|
||||
|
||||
# News:
|
||||
H.append(sco_news.scolar_news_summary_html())
|
||||
H.append(ScolarNews.scolar_news_summary_html())
|
||||
|
||||
# Avertissement de mise à jour:
|
||||
H.append("""<div id="update_warning"></div>""")
|
||||
|
@ -80,7 +80,7 @@ def index_html(showcodes=0, showsemtable=0):
|
|||
sco_formsemestre.sem_set_responsable_name(sem)
|
||||
|
||||
if showcodes:
|
||||
sem["tmpcode"] = "<td><tt>%s</tt></td>" % sem["formsemestre_id"]
|
||||
sem["tmpcode"] = f"<td><tt>{sem['formsemestre_id']}</tt></td>"
|
||||
else:
|
||||
sem["tmpcode"] = ""
|
||||
# Nombre d'inscrits:
|
||||
|
@ -122,26 +122,27 @@ def index_html(showcodes=0, showsemtable=0):
|
|||
|
||||
if showsemtable:
|
||||
H.append(
|
||||
"""<hr/>
|
||||
<h2>Semestres de %s</h2>
|
||||
f"""<hr>
|
||||
<h2>Semestres de {sco_preferences.get_preference("DeptName")}</h2>
|
||||
"""
|
||||
% sco_preferences.get_preference("DeptName")
|
||||
)
|
||||
H.append(_sem_table_gt(sems, showcodes=showcodes).html())
|
||||
H.append("</table>")
|
||||
if not showsemtable:
|
||||
H.append(
|
||||
'<hr/><p><a href="%s?showsemtable=1">Voir tous les semestres</a></p>'
|
||||
% request.base_url
|
||||
f"""<hr>
|
||||
<p><a class="stdlink" href="{url_for('scolar.index_html', scodoc_dept=g.scodoc_dept, showsemtable=1)
|
||||
}">Voir tous les semestres ({len(othersems)} verrouillés)</a>
|
||||
</p>"""
|
||||
)
|
||||
|
||||
H.append(
|
||||
"""<p><form action="%s/view_formsemestre_by_etape">
|
||||
Chercher étape courante: <input name="etape_apo" type="text" size="8" spellcheck="false"></input>
|
||||
</form
|
||||
</p>
|
||||
"""
|
||||
% scu.NotesURL()
|
||||
f"""<p>
|
||||
<form action="{url_for('notes.view_formsemestre_by_etape', scodoc_dept=g.scodoc_dept)}">
|
||||
Chercher étape courante:
|
||||
<input name="etape_apo" type="text" size="8" spellcheck="false"></input>
|
||||
</form>
|
||||
</p>"""
|
||||
)
|
||||
#
|
||||
if current_user.has_permission(Permission.ScoEtudInscrit):
|
||||
|
@ -149,23 +150,26 @@ Chercher étape courante: <input name="etape_apo" type="text" size="8" spellchec
|
|||
"""<hr>
|
||||
<h3>Gestion des étudiants</h3>
|
||||
<ul>
|
||||
<li><a class="stdlink" href="etudident_create_form">créer <em>un</em> nouvel étudiant</a></li>
|
||||
<li><a class="stdlink" href="form_students_import_excel">importer de nouveaux étudiants</a> (ne pas utiliser sauf cas particulier, utilisez plutôt le lien dans
|
||||
le tableau de bord semestre si vous souhaitez inscrire les
|
||||
étudiants importés à un semestre)</li>
|
||||
<li><a class="stdlink" href="etudident_create_form">créer <em>un</em> nouvel étudiant</a>
|
||||
</li>
|
||||
<li><a class="stdlink" href="form_students_import_excel">importer de nouveaux étudiants</a>
|
||||
(ne pas utiliser sauf cas particulier, utilisez plutôt le lien dans
|
||||
le tableau de bord semestre si vous souhaitez inscrire les
|
||||
étudiants importés à un semestre)
|
||||
</li>
|
||||
</ul>
|
||||
"""
|
||||
)
|
||||
#
|
||||
if current_user.has_permission(Permission.ScoEditApo):
|
||||
H.append(
|
||||
"""<hr>
|
||||
f"""<hr>
|
||||
<h3>Exports Apogée</h3>
|
||||
<ul>
|
||||
<li><a class="stdlink" href="%s/semset_page">Années scolaires / exports Apogée</a></li>
|
||||
<li><a class="stdlink" href="{url_for('notes.semset_page', scodoc_dept=g.scodoc_dept)
|
||||
}">Années scolaires / exports Apogée</a></li>
|
||||
</ul>
|
||||
"""
|
||||
% scu.NotesURL()
|
||||
)
|
||||
#
|
||||
H.append(
|
||||
|
@ -177,7 +181,13 @@ Chercher étape courante: <input name="etape_apo" type="text" size="8" spellchec
|
|||
"""
|
||||
)
|
||||
#
|
||||
return html_sco_header.sco_header() + "\n".join(H) + html_sco_header.sco_footer()
|
||||
return (
|
||||
html_sco_header.sco_header(
|
||||
page_title=f"ScoDoc {g.scodoc_dept}", javascripts=["js/scolar_index.js"]
|
||||
)
|
||||
+ "\n".join(H)
|
||||
+ html_sco_header.sco_footer()
|
||||
)
|
||||
|
||||
|
||||
def _sem_table(sems):
|
||||
|
@ -214,7 +224,9 @@ def _sem_table(sems):
|
|||
|
||||
|
||||
def _sem_table_gt(sems, showcodes=False):
|
||||
"""Nouvelle version de la table des semestres"""
|
||||
"""Nouvelle version de la table des semestres
|
||||
Utilise une datatables.
|
||||
"""
|
||||
_style_sems(sems)
|
||||
columns_ids = (
|
||||
"lockimg",
|
||||
|
@ -225,10 +237,15 @@ def _sem_table_gt(sems, showcodes=False):
|
|||
"titre_resp",
|
||||
"nb_inscrits",
|
||||
"etapes_apo_str",
|
||||
"elt_annee_apo",
|
||||
"elt_sem_apo",
|
||||
)
|
||||
if showcodes:
|
||||
columns_ids = ("formsemestre_id",) + columns_ids
|
||||
|
||||
html_class = "stripe cell-border compact hover order-column table_leftalign semlist"
|
||||
if current_user.has_permission(Permission.ScoEditApo):
|
||||
html_class += " apo_editable"
|
||||
tab = GenTable(
|
||||
titles={
|
||||
"formsemestre_id": "id",
|
||||
|
@ -237,14 +254,23 @@ def _sem_table_gt(sems, showcodes=False):
|
|||
"mois_debut": "Début",
|
||||
"dash_mois_fin": "Année",
|
||||
"titre_resp": "Semestre",
|
||||
"nb_inscrits": "N", # groupicon,
|
||||
"nb_inscrits": "N",
|
||||
"etapes_apo_str": "Étape Apo.",
|
||||
"elt_annee_apo": "Elt. année Apo.",
|
||||
"elt_sem_apo": "Elt. sem. Apo.",
|
||||
},
|
||||
columns_ids=columns_ids,
|
||||
rows=sems,
|
||||
html_class="table_leftalign semlist",
|
||||
table_id="semlist",
|
||||
html_class_ignore_default=True,
|
||||
html_class=html_class,
|
||||
html_sortable=True,
|
||||
# base_url = '%s?formsemestre_id=%s' % (request.base_url, formsemestre_id),
|
||||
# caption='Maquettes enregistrées',
|
||||
html_table_attrs=f"""
|
||||
data-apo_save_url="{url_for('notes.formsemestre_set_apo_etapes', scodoc_dept=g.scodoc_dept)}"
|
||||
data-elt_annee_apo_save_url="{url_for('notes.formsemestre_set_elt_annee_apo', scodoc_dept=g.scodoc_dept)}"
|
||||
data-elt_sem_apo_save_url="{url_for('notes.formsemestre_set_elt_sem_apo', scodoc_dept=g.scodoc_dept)}"
|
||||
""",
|
||||
html_with_td_classes=True,
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
)
|
||||
|
||||
|
@ -277,6 +303,16 @@ def _style_sems(sems):
|
|||
sem["semestre_id_n"] = ""
|
||||
else:
|
||||
sem["semestre_id_n"] = sem["semestre_id"]
|
||||
# pour édition codes Apogée:
|
||||
sem[
|
||||
"_etapes_apo_str_td_attrs"
|
||||
] = f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['etapes_apo_str']}" """
|
||||
sem[
|
||||
"_elt_annee_apo_td_attrs"
|
||||
] = f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['elt_annee_apo']}" """
|
||||
sem[
|
||||
"_elt_sem_apo_td_attrs"
|
||||
] = f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['elt_sem_apo']}" """
|
||||
|
||||
|
||||
def delete_dept(dept_id: int):
|
||||
|
|
|
@ -37,6 +37,7 @@ from app.models import SHORT_STR_LEN
|
|||
from app.models.formations import Formation
|
||||
from app.models.modules import Module
|
||||
from app.models.ues import UniteEns
|
||||
from app.models import ScolarNews
|
||||
|
||||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
@ -44,13 +45,10 @@ from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
|
|||
from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError
|
||||
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_edit_module
|
||||
from app.scodoc import sco_edit_ue
|
||||
from app.scodoc import sco_formations
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_news
|
||||
|
||||
|
||||
def formation_delete(formation_id=None, dialog_confirmed=False):
|
||||
|
@ -117,11 +115,10 @@ def do_formation_delete(oid):
|
|||
sco_formations._formationEditor.delete(cnx, oid)
|
||||
|
||||
# news
|
||||
sco_news.add(
|
||||
typ=sco_news.NEWS_FORM,
|
||||
object=oid,
|
||||
text="Suppression de la formation %(acronyme)s" % F,
|
||||
max_frequency=3,
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=oid,
|
||||
text=f"Suppression de la formation {F['acronyme']}",
|
||||
)
|
||||
|
||||
|
||||
|
@ -281,10 +278,9 @@ def do_formation_create(args):
|
|||
#
|
||||
r = sco_formations._formationEditor.create(cnx, args)
|
||||
|
||||
sco_news.add(
|
||||
typ=sco_news.NEWS_FORM,
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
text="Création de la formation %(titre)s (%(acronyme)s)" % args,
|
||||
max_frequency=3,
|
||||
)
|
||||
return r
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
"""
|
||||
import flask
|
||||
from flask import g, url_for, request
|
||||
from app.models.events import ScolarNews
|
||||
from app.models.formations import Matiere
|
||||
|
||||
import app.scodoc.notesdb as ndb
|
||||
|
@ -78,8 +79,7 @@ def do_matiere_edit(*args, **kw):
|
|||
def do_matiere_create(args):
|
||||
"create a matiere"
|
||||
from app.scodoc import sco_edit_ue
|
||||
from app.scodoc import sco_formations
|
||||
from app.scodoc import sco_news
|
||||
from app.models import ScolarNews
|
||||
|
||||
cnx = ndb.GetDBConnexion()
|
||||
# check
|
||||
|
@ -89,11 +89,11 @@ def do_matiere_create(args):
|
|||
|
||||
# news
|
||||
formation = Formation.query.get(ue["formation_id"])
|
||||
sco_news.add(
|
||||
typ=sco_news.NEWS_FORM,
|
||||
object=ue["formation_id"],
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=ue["formation_id"],
|
||||
text=f"Modification de la formation {formation.acronyme}",
|
||||
max_frequency=3,
|
||||
max_frequency=10 * 60,
|
||||
)
|
||||
formation.invalidate_cached_sems()
|
||||
return r
|
||||
|
@ -174,10 +174,8 @@ def can_delete_matiere(matiere: Matiere) -> tuple[bool, str]:
|
|||
|
||||
def do_matiere_delete(oid):
|
||||
"delete matiere and attached modules"
|
||||
from app.scodoc import sco_formations
|
||||
from app.scodoc import sco_edit_ue
|
||||
from app.scodoc import sco_edit_module
|
||||
from app.scodoc import sco_news
|
||||
|
||||
cnx = ndb.GetDBConnexion()
|
||||
# check
|
||||
|
@ -197,11 +195,11 @@ def do_matiere_delete(oid):
|
|||
|
||||
# news
|
||||
formation = Formation.query.get(ue["formation_id"])
|
||||
sco_news.add(
|
||||
typ=sco_news.NEWS_FORM,
|
||||
object=ue["formation_id"],
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=ue["formation_id"],
|
||||
text=f"Modification de la formation {formation.acronyme}",
|
||||
max_frequency=3,
|
||||
max_frequency=10 * 60,
|
||||
)
|
||||
formation.invalidate_cached_sems()
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ from app import models
|
|||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models import Formation, Matiere, Module, UniteEns
|
||||
from app.models import FormSemestre, ModuleImpl
|
||||
from app.models import ScolarNews
|
||||
|
||||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
@ -53,7 +54,6 @@ from app.scodoc import html_sco_header
|
|||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_edit_matiere
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_news
|
||||
|
||||
_moduleEditor = ndb.EditableTable(
|
||||
"notes_modules",
|
||||
|
@ -98,18 +98,16 @@ def module_list(*args, **kw):
|
|||
def do_module_create(args) -> int:
|
||||
"Create a module. Returns id of new object."
|
||||
# create
|
||||
from app.scodoc import sco_formations
|
||||
|
||||
cnx = ndb.GetDBConnexion()
|
||||
r = _moduleEditor.create(cnx, args)
|
||||
|
||||
# news
|
||||
formation = Formation.query.get(args["formation_id"])
|
||||
sco_news.add(
|
||||
typ=sco_news.NEWS_FORM,
|
||||
object=formation.id,
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=formation.id,
|
||||
text=f"Modification de la formation {formation.acronyme}",
|
||||
max_frequency=3,
|
||||
max_frequency=10 * 60,
|
||||
)
|
||||
formation.invalidate_cached_sems()
|
||||
return r
|
||||
|
@ -396,11 +394,11 @@ def do_module_delete(oid):
|
|||
|
||||
# news
|
||||
formation = module.formation
|
||||
sco_news.add(
|
||||
typ=sco_news.NEWS_FORM,
|
||||
object=mod["formation_id"],
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=mod["formation_id"],
|
||||
text=f"Modification de la formation {formation.acronyme}",
|
||||
max_frequency=3,
|
||||
max_frequency=10 * 60,
|
||||
)
|
||||
formation.invalidate_cached_sems()
|
||||
|
||||
|
@ -520,7 +518,7 @@ def module_edit(module_id=None):
|
|||
|
||||
H = [
|
||||
html_sco_header.sco_header(
|
||||
page_title="Modification du module %(titre)s" % module,
|
||||
page_title=f"Modification du module {a_module.code or a_module.titre or ''}",
|
||||
cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css"],
|
||||
javascripts=[
|
||||
"libjs/jQuery-tagEditor/jquery.tag-editor.min.js",
|
||||
|
@ -528,7 +526,7 @@ def module_edit(module_id=None):
|
|||
"js/module_tag_editor.js",
|
||||
],
|
||||
),
|
||||
"""<h2>Modification du module %(titre)s""" % module,
|
||||
f"""<h2>Modification du module {a_module.code or ''} {a_module.titre or ''}""",
|
||||
""" (formation %(acronyme)s, version %(version)s)</h2>""" % formation,
|
||||
render_template(
|
||||
"scodoc/help/modules.html",
|
||||
|
|
|
@ -37,15 +37,14 @@ from app import db
|
|||
from app import log
|
||||
from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN
|
||||
from app.models import Formation, UniteEns, ModuleImpl, Module
|
||||
from app.models import ScolarNews
|
||||
from app.models.formations import Matiere
|
||||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_exceptions import (
|
||||
ScoGenError,
|
||||
ScoValueError,
|
||||
ScoLockedFormError,
|
||||
ScoNonEmptyFormationObject,
|
||||
|
@ -55,16 +54,11 @@ from app.scodoc import html_sco_header
|
|||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_edit_apc
|
||||
from app.scodoc import sco_edit_formation
|
||||
from app.scodoc import sco_edit_matiere
|
||||
from app.scodoc import sco_edit_module
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_news
|
||||
from app.scodoc import sco_permissions
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_tag_module
|
||||
|
||||
_ueEditor = ndb.EditableTable(
|
||||
|
@ -138,11 +132,11 @@ def do_ue_create(args):
|
|||
ue = UniteEns.query.get(ue_id)
|
||||
flash(f"UE créée (code {ue.ue_code})")
|
||||
formation = Formation.query.get(args["formation_id"])
|
||||
sco_news.add(
|
||||
typ=sco_news.NEWS_FORM,
|
||||
object=args["formation_id"],
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=args["formation_id"],
|
||||
text=f"Modification de la formation {formation.acronyme}",
|
||||
max_frequency=3,
|
||||
max_frequency=10 * 60,
|
||||
)
|
||||
formation.invalidate_cached_sems()
|
||||
return ue_id
|
||||
|
@ -222,11 +216,11 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
|
|||
sco_cache.invalidate_formsemestre()
|
||||
# news
|
||||
F = sco_formations.formation_list(args={"formation_id": ue.formation_id})[0]
|
||||
sco_news.add(
|
||||
typ=sco_news.NEWS_FORM,
|
||||
object=ue.formation_id,
|
||||
text="Modification de la formation %(acronyme)s" % F,
|
||||
max_frequency=3,
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=ue.formation_id,
|
||||
text=f"Modification de la formation {F['acronyme']}",
|
||||
max_frequency=10 * 60,
|
||||
)
|
||||
#
|
||||
if not force:
|
||||
|
@ -1358,93 +1352,6 @@ def ue_is_locked(ue_id):
|
|||
return len(r) > 0
|
||||
|
||||
|
||||
# ---- Table recap formation
|
||||
def formation_table_recap(formation_id, format="html"):
|
||||
"""Table recapitulant formation."""
|
||||
from app.scodoc import sco_formations
|
||||
|
||||
F = sco_formations.formation_list(args={"formation_id": formation_id})
|
||||
if not F:
|
||||
raise ScoValueError("invalid formation_id")
|
||||
F = F[0]
|
||||
T = []
|
||||
ues = ue_list(args={"formation_id": formation_id})
|
||||
for ue in ues:
|
||||
Matlist = sco_edit_matiere.matiere_list(args={"ue_id": ue["ue_id"]})
|
||||
for Mat in Matlist:
|
||||
Modlist = sco_edit_module.module_list(
|
||||
args={"matiere_id": Mat["matiere_id"]}
|
||||
)
|
||||
for Mod in Modlist:
|
||||
Mod["nb_moduleimpls"] = sco_edit_module.module_count_moduleimpls(
|
||||
Mod["module_id"]
|
||||
)
|
||||
#
|
||||
T.append(
|
||||
{
|
||||
"UE_acro": ue["acronyme"],
|
||||
"Mat_tit": Mat["titre"],
|
||||
"Mod_tit": Mod["abbrev"] or Mod["titre"],
|
||||
"Mod_code": Mod["code"],
|
||||
"Mod_coef": Mod["coefficient"],
|
||||
"Mod_sem": Mod["semestre_id"],
|
||||
"nb_moduleimpls": Mod["nb_moduleimpls"],
|
||||
"heures_cours": Mod["heures_cours"],
|
||||
"heures_td": Mod["heures_td"],
|
||||
"heures_tp": Mod["heures_tp"],
|
||||
"ects": Mod["ects"],
|
||||
}
|
||||
)
|
||||
columns_ids = [
|
||||
"UE_acro",
|
||||
"Mat_tit",
|
||||
"Mod_tit",
|
||||
"Mod_code",
|
||||
"Mod_coef",
|
||||
"Mod_sem",
|
||||
"nb_moduleimpls",
|
||||
"heures_cours",
|
||||
"heures_td",
|
||||
"heures_tp",
|
||||
"ects",
|
||||
]
|
||||
titles = {
|
||||
"UE_acro": "UE",
|
||||
"Mat_tit": "Matière",
|
||||
"Mod_tit": "Module",
|
||||
"Mod_code": "Code",
|
||||
"Mod_coef": "Coef.",
|
||||
"Mod_sem": "Sem.",
|
||||
"nb_moduleimpls": "Nb utilisé",
|
||||
"heures_cours": "Cours (h)",
|
||||
"heures_td": "TD (h)",
|
||||
"heures_tp": "TP (h)",
|
||||
"ects": "ECTS",
|
||||
}
|
||||
|
||||
title = (
|
||||
"""Formation %(titre)s (%(acronyme)s) [version %(version)s] code %(formation_code)s"""
|
||||
% F
|
||||
)
|
||||
tab = GenTable(
|
||||
columns_ids=columns_ids,
|
||||
rows=T,
|
||||
titles=titles,
|
||||
origin="Généré par %s le " % scu.sco_version.SCONAME
|
||||
+ scu.timedate_human_repr()
|
||||
+ "",
|
||||
caption=title,
|
||||
html_caption=title,
|
||||
html_class="table_leftalign",
|
||||
base_url="%s?formation_id=%s" % (request.base_url, formation_id),
|
||||
page_title=title,
|
||||
html_title="<h2>" + title + "</h2>",
|
||||
pdf_title=title,
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
)
|
||||
return tab.make_page(format=format)
|
||||
|
||||
|
||||
def ue_list_semestre_ids(ue: dict):
|
||||
"""Liste triée des numeros de semestres des modules dans cette UE
|
||||
Il est recommandable que tous les modules d'une UE aient le même indice de semestre.
|
||||
|
|
|
@ -32,11 +32,8 @@
|
|||
Voir sco_apogee_csv.py pour la structure du fichier Apogée.
|
||||
|
||||
Stockage: utilise sco_archive.py
|
||||
=> /opt/scodoc/var/scodoc/archives/apo_csv/<dept_id>/2016-1/2016-07-03-16-12-19/V3ASR.csv
|
||||
pour une maquette de l'année scolaire 2016, semestre 1, etape V3ASR
|
||||
|
||||
ou bien (à partir de ScoDoc 1678) :
|
||||
/opt/scodoc/var/scodoc/archives/apo_csv/<dept_id>/2016-1/2016-07-03-16-12-19/V3ASR!111.csv
|
||||
exemple:
|
||||
/opt/scodoc-data/archives/apo_csv/<dept_id>/2016-1/2016-07-03-16-12-19/V3ASR!111.csv
|
||||
pour une maquette de l'étape V3ASR version VDI 111.
|
||||
|
||||
La version VDI sera ignorée sauf si elle est indiquée dans l'étape du semestre.
|
||||
|
|
|
@ -34,8 +34,6 @@ from zipfile import ZipFile
|
|||
import flask
|
||||
from flask import url_for, g, send_file, request
|
||||
|
||||
# from werkzeug.utils import send_file
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import log
|
||||
from app.scodoc import html_sco_header
|
||||
|
|
|
@ -637,7 +637,7 @@ def create_etud(cnx, args={}):
|
|||
Returns:
|
||||
etud, l'étudiant créé.
|
||||
"""
|
||||
from app.scodoc import sco_news
|
||||
from app.models import ScolarNews
|
||||
|
||||
# creation d'un etudiant
|
||||
etudid = etudident_create(cnx, args)
|
||||
|
@ -671,9 +671,8 @@ def create_etud(cnx, args={}):
|
|||
etud = etudident_list(cnx, {"etudid": etudid})[0]
|
||||
fill_etuds_info([etud])
|
||||
etud["url"] = url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
sco_news.add(
|
||||
typ=sco_news.NEWS_INSCR,
|
||||
object=None, # pas d'object pour ne montrer qu'un etudiant
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_INSCR,
|
||||
text='Nouvel étudiant <a href="%(url)s">%(nomprenom)s</a>' % etud,
|
||||
url=etud["url"],
|
||||
)
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
#
|
||||
##############################################################################
|
||||
|
||||
"""Vérification des abasneces à une évaluation
|
||||
"""Vérification des absences à une évaluation
|
||||
"""
|
||||
from flask import url_for, g
|
||||
|
||||
|
|
|
@ -28,7 +28,6 @@
|
|||
"""Gestion evaluations (ScoDoc7, sans SQlAlchemy)
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import pprint
|
||||
|
||||
import flask
|
||||
|
@ -37,6 +36,7 @@ from flask_login import current_user
|
|||
|
||||
from app import log
|
||||
|
||||
from app.models import ScolarNews
|
||||
from app.models.evaluations import evaluation_enrich_dict, check_evaluation_args
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
|
@ -44,9 +44,7 @@ from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
|||
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_edit_module
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_news
|
||||
from app.scodoc import sco_permissions_check
|
||||
|
||||
|
||||
|
@ -179,9 +177,9 @@ def do_evaluation_create(
|
|||
mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
||||
mod["moduleimpl_id"] = M["moduleimpl_id"]
|
||||
mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
|
||||
sco_news.add(
|
||||
typ=sco_news.NEWS_NOTE,
|
||||
object=moduleimpl_id,
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_NOTE,
|
||||
obj=moduleimpl_id,
|
||||
text='Création d\'une évaluation dans <a href="%(url)s">%(titre)s</a>' % mod,
|
||||
url=mod["url"],
|
||||
)
|
||||
|
@ -240,9 +238,9 @@ def do_evaluation_delete(evaluation_id):
|
|||
mod["url"] = (
|
||||
scu.NotesURL() + "/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
|
||||
)
|
||||
sco_news.add(
|
||||
typ=sco_news.NEWS_NOTE,
|
||||
object=moduleimpl_id,
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_NOTE,
|
||||
obj=moduleimpl_id,
|
||||
text='Suppression d\'une évaluation dans <a href="%(url)s">%(titre)s</a>' % mod,
|
||||
url=mod["url"],
|
||||
)
|
||||
|
|
|
@ -0,0 +1,148 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""Tableau recap. de toutes les évaluations d'un semestre
|
||||
avec leur état.
|
||||
|
||||
Sur une idée de Pascal Bouron, de Lyon.
|
||||
"""
|
||||
import time
|
||||
from flask import g, url_for
|
||||
|
||||
from app.models import Evaluation, FormSemestre
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.comp.moy_mod import ModuleImplResults
|
||||
from app.scodoc import html_sco_header
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
|
||||
def evaluations_recap(formsemestre_id: int) -> str:
|
||||
"""Page récap. de toutes les évaluations d'un semestre"""
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
rows, titles = evaluations_recap_table(formsemestre)
|
||||
column_ids = titles.keys()
|
||||
filename = scu.sanitize_filename(
|
||||
f"""evaluations-{formsemestre.titre_num()}-{time.strftime("%Y-%m-%d")}"""
|
||||
)
|
||||
if not rows:
|
||||
return '<div class="evaluations_recap"><div class="message">aucune évaluation</div></div>'
|
||||
H = [
|
||||
html_sco_header.sco_header(
|
||||
page_title="Évaluations du semestre",
|
||||
javascripts=["js/evaluations_recap.js"],
|
||||
),
|
||||
f"""<div class="evaluations_recap"><table class="evaluations_recap compact {
|
||||
'apc' if formsemestre.formation.is_apc() else 'classic'
|
||||
}"
|
||||
data-filename="{filename}">""",
|
||||
]
|
||||
# header
|
||||
H.append(
|
||||
f"""
|
||||
<thead>
|
||||
{scu.gen_row(column_ids, titles, "th", with_col_classes=True)}
|
||||
</thead>
|
||||
"""
|
||||
)
|
||||
# body
|
||||
H.append("<tbody>")
|
||||
for row in rows:
|
||||
H.append(f"{scu.gen_row(column_ids, row, with_col_classes=True)}\n")
|
||||
|
||||
H.append(
|
||||
"""</tbody></table></div>
|
||||
<div class="help">Les étudiants démissionnaires ou défaillants ne sont pas pris en compte dans cette table.</div>
|
||||
"""
|
||||
)
|
||||
H.append(
|
||||
html_sco_header.sco_footer(),
|
||||
)
|
||||
return "".join(H)
|
||||
|
||||
|
||||
def evaluations_recap_table(formsemestre: FormSemestre) -> list[dict]:
|
||||
"""Tableau recap. de toutes les évaluations d'un semestre
|
||||
Colonnes:
|
||||
- code (UE ou module),
|
||||
- titre
|
||||
- complete
|
||||
- publiée
|
||||
- inscrits (non dem. ni def.)
|
||||
- nb notes manquantes
|
||||
- nb ATT
|
||||
- nb ABS
|
||||
- nb EXC
|
||||
"""
|
||||
rows = []
|
||||
titles = {
|
||||
"type": "",
|
||||
"code": "Code",
|
||||
"titre": "",
|
||||
"date": "Date",
|
||||
"complete": "Comptée",
|
||||
"inscrits": "Inscrits",
|
||||
"manquantes": "Manquantes", # notes eval non entrées
|
||||
"nb_abs": "ABS",
|
||||
"nb_att": "ATT",
|
||||
"nb_exc": "EXC",
|
||||
}
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
line_idx = 0
|
||||
for modimpl in nt.formsemestre.modimpls_sorted:
|
||||
modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl.id]
|
||||
row = {
|
||||
"type": modimpl.module.type_abbrv().upper(),
|
||||
"_type_order": f"{line_idx:04d}",
|
||||
"code": modimpl.module.code,
|
||||
"_code_target": url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=modimpl.id,
|
||||
),
|
||||
"titre": modimpl.module.titre,
|
||||
"_titre_class": "titre",
|
||||
"inscrits": modimpl_results.nb_inscrits_module,
|
||||
"date": "-",
|
||||
"_date_order": "",
|
||||
"_tr_class": f"module {modimpl.module.type_abbrv()}",
|
||||
}
|
||||
rows.append(row)
|
||||
line_idx += 1
|
||||
for evaluation_id in modimpl_results.evals_notes:
|
||||
e = Evaluation.query.get(evaluation_id)
|
||||
eval_etat = modimpl_results.evaluations_etat[evaluation_id]
|
||||
row = {
|
||||
"type": "",
|
||||
"_type_order": f"{line_idx:04d}",
|
||||
"titre": e.description or "sans titre",
|
||||
"_titre_target": url_for(
|
||||
"notes.evaluation_listenotes",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
evaluation_id=evaluation_id,
|
||||
),
|
||||
"_titre_target_attrs": 'class="discretelink"',
|
||||
"date": e.jour.strftime("%d/%m/%Y") if e.jour else "",
|
||||
"_date_order": e.jour.isoformat() if e.jour else "",
|
||||
"complete": "oui" if eval_etat.is_complete else "non",
|
||||
"_complete_target": "#",
|
||||
"_complete_target_attrs": 'class="bull_link" title="prise en compte dans les moyennes"'
|
||||
if eval_etat.is_complete
|
||||
else 'class="bull_link incomplete" title="il manque des notes"',
|
||||
"manquantes": len(modimpl_results.evals_etudids_sans_note[e.id]),
|
||||
"inscrits": modimpl_results.nb_inscrits_module,
|
||||
"nb_abs": sum(modimpl_results.evals_notes[e.id] == scu.NOTES_ABSENCE),
|
||||
"nb_att": eval_etat.nb_attente,
|
||||
"nb_exc": sum(
|
||||
modimpl_results.evals_notes[e.id] == scu.NOTES_NEUTRALISE
|
||||
),
|
||||
"_tr_class": "evaluation"
|
||||
+ (" incomplete" if not eval_etat.is_complete else ""),
|
||||
}
|
||||
rows.append(row)
|
||||
line_idx += 1
|
||||
|
||||
return rows, titles
|
|
@ -36,32 +36,27 @@ from flask import g
|
|||
from flask_login import current_user
|
||||
from flask import request
|
||||
|
||||
from app import log
|
||||
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre
|
||||
from app.models import ScolarNews
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||
import sco_version
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_abs
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_edit_module
|
||||
from app.scodoc import sco_edit_ue
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_news
|
||||
from app.scodoc import sco_permissions_check
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_users
|
||||
import sco_version
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
|
@ -633,13 +628,16 @@ def evaluation_describe(evaluation_id="", edit_in_place=True):
|
|||
'<span class="evallink"><a class="stdlink" href="evaluation_listenotes?moduleimpl_id=%s">voir toutes les notes du module</a></span>'
|
||||
% moduleimpl_id
|
||||
)
|
||||
mod_descr = '<a href="moduleimpl_status?moduleimpl_id=%s">%s %s</a> <span class="resp">(resp. <a title="%s">%s</a>)</span> %s' % (
|
||||
moduleimpl_id,
|
||||
Mod["code"] or "",
|
||||
Mod["titre"] or "?",
|
||||
nomcomplet,
|
||||
resp,
|
||||
link,
|
||||
mod_descr = (
|
||||
'<a href="moduleimpl_status?moduleimpl_id=%s">%s %s</a> <span class="resp">(resp. <a title="%s">%s</a>)</span> %s'
|
||||
% (
|
||||
moduleimpl_id,
|
||||
Mod["code"] or "",
|
||||
Mod["titre"] or "?",
|
||||
nomcomplet,
|
||||
resp,
|
||||
link,
|
||||
)
|
||||
)
|
||||
|
||||
etit = E["description"] or ""
|
||||
|
|
|
@ -185,13 +185,13 @@ def excel_make_style(
|
|||
|
||||
|
||||
class ScoExcelSheet:
|
||||
"""Représente une feuille qui peut être indépendante ou intégrée dans un SCoExcelBook.
|
||||
"""Représente une feuille qui peut être indépendante ou intégrée dans un ScoExcelBook.
|
||||
En application des directives de la bibliothèque sur l'écriture optimisée, l'ordre des opérations
|
||||
est imposé:
|
||||
* instructions globales (largeur/maquage des colonnes et ligne, ...)
|
||||
* construction et ajout des cellules et ligne selon le sens de lecture (occidental)
|
||||
ligne de haut en bas et cellules de gauche à droite (i.e. A1, A2, .. B1, B2, ..)
|
||||
* pour finit appel de la méthode de génération
|
||||
* pour finir appel de la méthode de génération
|
||||
"""
|
||||
|
||||
def __init__(self, sheet_name="feuille", default_style=None, wb=None):
|
||||
|
@ -260,7 +260,7 @@ class ScoExcelSheet:
|
|||
for i, val in enumerate(value):
|
||||
self.ws.column_dimensions[self.i2col(i)].width = val
|
||||
# No keys: value is a list of widths
|
||||
elif type(cle) == str: # accepts set_column_with("D", ...)
|
||||
elif isinstance(cle, str): # accepts set_column_with("D", ...)
|
||||
self.ws.column_dimensions[cle].width = value
|
||||
else:
|
||||
self.ws.column_dimensions[self.i2col(cle)].width = value
|
||||
|
@ -337,7 +337,8 @@ class ScoExcelSheet:
|
|||
|
||||
return cell
|
||||
|
||||
def make_row(self, values: list, style=None, comments=None):
|
||||
def make_row(self, values: list, style=None, comments=None) -> list:
|
||||
"build a row"
|
||||
# TODO make possible differents styles in a row
|
||||
if comments is None:
|
||||
comments = [None] * len(values)
|
||||
|
|
|
@ -63,6 +63,17 @@ class ScoFormatError(ScoValueError):
|
|||
pass
|
||||
|
||||
|
||||
class ScoInvalidParamError(ScoValueError):
|
||||
"""Paramètres requete invalides.
|
||||
A utilisée lorsqu'une route est appelée avec des paramètres invalides
|
||||
(id strings, ...)
|
||||
"""
|
||||
|
||||
def __init__(self, msg=None, dest_url=None):
|
||||
msg = msg or "Adresse invalide. Vérifiez vos signets."
|
||||
super().__init__(msg, dest_url=dest_url)
|
||||
|
||||
|
||||
class ScoPDFFormatError(ScoValueError):
|
||||
"erreur génération PDF (templates platypus, ...)"
|
||||
|
||||
|
|
|
@ -53,12 +53,10 @@ def form_search_etud(
|
|||
):
|
||||
"form recherche par nom"
|
||||
H = []
|
||||
if title:
|
||||
H.append("<h2>%s</h2>" % title)
|
||||
H.append(
|
||||
f"""<form action="{ url_for("scolar.search_etud_in_dept", scodoc_dept=g.scodoc_dept) }" method="POST">
|
||||
<b>{title}</b>
|
||||
<input type="text" name="expnom" width="12" spellcheck="false" value="">
|
||||
<input type="text" name="expnom" class="in-expnom" width="12" spellcheck="false" value="">
|
||||
<input type="submit" value="Chercher">
|
||||
<br/>(entrer une partie du nom)
|
||||
"""
|
||||
|
|
|
@ -0,0 +1,193 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Table recap formation (avec champs éditables)
|
||||
"""
|
||||
import io
|
||||
from zipfile import ZipFile, BadZipfile
|
||||
|
||||
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.scodoc.gen_tables import GenTable
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc import sco_preferences
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
|
||||
# ---- Table recap formation
|
||||
def formation_table_recap(formation_id, format="html") -> Response:
|
||||
"""Table recapitulant formation."""
|
||||
T = []
|
||||
formation = Formation.query.get_or_404(formation_id)
|
||||
ues = formation.ues.order_by(UniteEns.semestre_idx, UniteEns.numero)
|
||||
can_edit = current_user.has_permission(Permission.ScoChangeFormation)
|
||||
li = 0
|
||||
for ue in ues:
|
||||
# L'UE
|
||||
T.append(
|
||||
{
|
||||
"sem": f"S{ue.semestre_idx}" if ue.semestre_idx is not None else "-",
|
||||
"_sem_order": f"{li:04d}",
|
||||
"code": ue.acronyme,
|
||||
"titre": ue.titre or "",
|
||||
"_titre_target": url_for(
|
||||
"notes.ue_edit",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
ue_id=ue.id,
|
||||
)
|
||||
if can_edit
|
||||
else None,
|
||||
"apo": ue.code_apogee or "",
|
||||
"_apo_td_attrs": f""" data-oid="{ue.id}" data-value="{ue.code_apogee or ''}" """,
|
||||
"coef": ue.coefficient or "",
|
||||
"ects": ue.ects,
|
||||
"_css_row_class": "ue",
|
||||
}
|
||||
)
|
||||
li += 1
|
||||
matieres = ue.matieres.order_by(Matiere.numero)
|
||||
for mat in matieres:
|
||||
modules = mat.modules.order_by(Module.numero)
|
||||
for mod in modules:
|
||||
nb_moduleimpls = mod.modimpls.count()
|
||||
# le module (ou ressource ou sae)
|
||||
T.append(
|
||||
{
|
||||
"sem": f"S{mod.semestre_id}"
|
||||
if mod.semestre_id is not None
|
||||
else "-",
|
||||
"_sem_order": f"{li:04d}",
|
||||
"code": mod.code,
|
||||
"titre": mod.abbrev or mod.titre,
|
||||
"_titre_target": url_for(
|
||||
"notes.module_edit",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
module_id=mod.id,
|
||||
)
|
||||
if can_edit
|
||||
else None,
|
||||
"apo": mod.code_apogee,
|
||||
"_apo_td_attrs": f""" data-oid="{mod.id}" data-value="{mod.code_apogee or ''}" """,
|
||||
"coef": mod.coefficient,
|
||||
"nb_moduleimpls": nb_moduleimpls,
|
||||
"heures_cours": mod.heures_cours,
|
||||
"heures_td": mod.heures_td,
|
||||
"heures_tp": mod.heures_tp,
|
||||
"_css_row_class": f"mod {mod.type_abbrv()}",
|
||||
}
|
||||
)
|
||||
columns_ids = [
|
||||
"sem",
|
||||
"code",
|
||||
"apo",
|
||||
# "mat", inutile d'afficher la matière
|
||||
"titre",
|
||||
"coef",
|
||||
"ects",
|
||||
"nb_moduleimpls",
|
||||
"heures_cours",
|
||||
"heures_td",
|
||||
"heures_tp",
|
||||
]
|
||||
titles = {
|
||||
"ue": "UE",
|
||||
"mat": "Matière",
|
||||
"titre": "Titre",
|
||||
"code": "Code",
|
||||
"apo": "Apo",
|
||||
"coef": "Coef.",
|
||||
"sem": "Sem.",
|
||||
"nb_moduleimpls": "Nb utilisé",
|
||||
"heures_cours": "Cours (h)",
|
||||
"heures_td": "TD (h)",
|
||||
"heures_tp": "TP (h)",
|
||||
"ects": "ECTS",
|
||||
}
|
||||
|
||||
title = f"""Formation {formation.titre} ({formation.acronyme})
|
||||
[version {formation.version}] code {formation.formation_code}"""
|
||||
html_class = "stripe cell-border compact hover order-column formation_table_recap"
|
||||
if current_user.has_permission(Permission.ScoEditApo):
|
||||
html_class += " apo_editable"
|
||||
|
||||
tab = GenTable(
|
||||
columns_ids=columns_ids,
|
||||
rows=T,
|
||||
titles=titles,
|
||||
origin=f"Généré par {scu.sco_version.SCONAME} le {scu.timedate_human_repr()}",
|
||||
caption=title,
|
||||
html_caption=title,
|
||||
html_class=html_class,
|
||||
html_class_ignore_default=True,
|
||||
html_table_attrs=f"""
|
||||
data-apo_ue_save_url="{url_for('notes.ue_set_apo', scodoc_dept=g.scodoc_dept)}"
|
||||
data-apo_mod_save_url="{url_for('notes.module_set_apo', scodoc_dept=g.scodoc_dept)}"
|
||||
""",
|
||||
html_with_td_classes=True,
|
||||
base_url=f"{request.base_url}?formation_id={formation_id}",
|
||||
page_title=title,
|
||||
html_title=f"<h2>{title}</h2>",
|
||||
pdf_title=title,
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
table_id="formation_table_recap",
|
||||
)
|
||||
return tab.make_page(format=format, javascripts=["js/formation_recap.js"])
|
||||
|
||||
|
||||
def export_recap_formations_annee_scolaire(annee_scolaire):
|
||||
"""Exporte un zip des recap (excel) des formatons de tous les semestres
|
||||
de l'année scolaire indiquée.
|
||||
"""
|
||||
annee_scolaire = int(annee_scolaire)
|
||||
data = io.BytesIO()
|
||||
zip_file = ZipFile(data, "w")
|
||||
formsemestres = FormSemestre.query.filter_by(dept_id=g.scodoc_dept_id).filter(
|
||||
FormSemestre.date_debut >= scu.date_debut_anne_scolaire(annee_scolaire),
|
||||
FormSemestre.date_debut <= scu.date_fin_anne_scolaire(annee_scolaire),
|
||||
)
|
||||
formation_ids = {formsemestre.formation.id for formsemestre in formsemestres}
|
||||
for formation_id in formation_ids:
|
||||
formation = Formation.query.get(formation_id)
|
||||
xls = formation_table_recap(formation_id, format="xlsx").data
|
||||
filename = (
|
||||
scu.sanitize_filename(formation.get_titre_version()) + scu.XLSX_SUFFIX
|
||||
)
|
||||
zip_file.writestr(filename, xls)
|
||||
zip_file.close()
|
||||
data.seek(0)
|
||||
return send_file(
|
||||
data,
|
||||
mimetype="application/zip",
|
||||
download_name=f"formations-{g.scodoc_dept}-{annee_scolaire}-{annee_scolaire+1}.zip",
|
||||
as_attachment=True,
|
||||
)
|
|
@ -39,12 +39,12 @@ import app.scodoc.notesdb as ndb
|
|||
from app import db
|
||||
from app import log
|
||||
from app.models import Formation, Module
|
||||
from app.models import ScolarNews
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_edit_matiere
|
||||
from app.scodoc import sco_edit_module
|
||||
from app.scodoc import sco_edit_ue
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_news
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_tag_module
|
||||
from app.scodoc import sco_xml
|
||||
|
@ -351,10 +351,13 @@ def formation_list_table(formation_id=None, args={}):
|
|||
else:
|
||||
but_locked = '<span class="but_placeholder"></span>'
|
||||
if editable and not locked:
|
||||
but_suppr = '<a class="stdlink" href="formation_delete?formation_id=%s" id="delete-formation-%s">%s</a>' % (
|
||||
f["formation_id"],
|
||||
f["acronyme"].lower().replace(" ", "-"),
|
||||
suppricon,
|
||||
but_suppr = (
|
||||
'<a class="stdlink" href="formation_delete?formation_id=%s" id="delete-formation-%s">%s</a>'
|
||||
% (
|
||||
f["formation_id"],
|
||||
f["acronyme"].lower().replace(" ", "-"),
|
||||
suppricon,
|
||||
)
|
||||
)
|
||||
else:
|
||||
but_suppr = '<span class="but_placeholder"></span>'
|
||||
|
@ -422,9 +425,9 @@ def formation_create_new_version(formation_id, redirect=True):
|
|||
new_id, modules_old2new, ues_old2new = formation_import_xml(xml_data)
|
||||
# news
|
||||
F = formation_list(args={"formation_id": new_id})[0]
|
||||
sco_news.add(
|
||||
typ=sco_news.NEWS_FORM,
|
||||
object=new_id,
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=new_id,
|
||||
text="Nouvelle version de la formation %(acronyme)s" % F,
|
||||
)
|
||||
if redirect:
|
||||
|
|
|
@ -95,9 +95,12 @@ _formsemestreEditor = ndb.EditableTable(
|
|||
|
||||
def get_formsemestre(formsemestre_id, raise_soft_exc=False):
|
||||
"list ONE formsemestre"
|
||||
if formsemestre_id is None:
|
||||
raise ValueError(f"get_formsemestre: id manquant")
|
||||
if formsemestre_id in g.stored_get_formsemestre:
|
||||
return g.stored_get_formsemestre[formsemestre_id]
|
||||
if not isinstance(formsemestre_id, int):
|
||||
log(f"get_formsemestre: invalid id '{formsemestre_id}'")
|
||||
raise ScoInvalidIdType("formsemestre_id must be an integer !")
|
||||
sems = do_formsemestre_list(args={"formsemestre_id": formsemestre_id})
|
||||
if not sems:
|
||||
|
@ -141,7 +144,6 @@ def _formsemestre_enrich(sem):
|
|||
"""Ajoute champs souvent utiles: titre + annee et dateord (pour tris)"""
|
||||
# imports ici pour eviter refs circulaires
|
||||
from app.scodoc import sco_formsemestre_edit
|
||||
from app.scodoc import sco_etud
|
||||
|
||||
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
|
||||
parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
|
||||
|
@ -229,7 +231,7 @@ def etapes_apo_str(etapes):
|
|||
def do_formsemestre_create(args, silent=False):
|
||||
"create a formsemestre"
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_news
|
||||
from app.models import ScolarNews
|
||||
|
||||
cnx = ndb.GetDBConnexion()
|
||||
formsemestre_id = _formsemestreEditor.create(cnx, args)
|
||||
|
@ -254,8 +256,8 @@ def do_formsemestre_create(args, silent=False):
|
|||
args["formsemestre_id"] = formsemestre_id
|
||||
args["url"] = "Notes/formsemestre_status?formsemestre_id=%(formsemestre_id)s" % args
|
||||
if not silent:
|
||||
sco_news.add(
|
||||
typ=sco_news.NEWS_SEM,
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_SEM,
|
||||
text='Création du semestre <a href="%(url)s">%(titre)s</a>' % args,
|
||||
url=args["url"],
|
||||
)
|
||||
|
@ -350,6 +352,7 @@ def read_formsemestre_etapes(formsemestre_id): # OBSOLETE
|
|||
"""SELECT etape_apo
|
||||
FROM notes_formsemestre_etapes
|
||||
WHERE formsemestre_id = %(formsemestre_id)s
|
||||
ORDER BY etape_apo
|
||||
""",
|
||||
{"formsemestre_id": formsemestre_id},
|
||||
)
|
||||
|
|
|
@ -36,6 +36,7 @@ from app import db
|
|||
from app.auth.models import User
|
||||
from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN
|
||||
from app.models import Module, ModuleImpl, Evaluation, EvaluationUEPoids, UniteEns
|
||||
from app.models import ScolarNews
|
||||
from app.models.formations import Formation
|
||||
from app.models.formsemestre import FormSemestre
|
||||
import app.scodoc.notesdb as ndb
|
||||
|
@ -191,10 +192,10 @@ def do_formsemestre_createwithmodules(edit=False):
|
|||
modimpl.responsable_id,
|
||||
f"inconnu numéro {modimpl.responsable_id} resp. de {modimpl.id} !",
|
||||
)
|
||||
|
||||
initvalues["responsable_id"] = uid2display.get(
|
||||
sem["responsables"][0], sem["responsables"][0]
|
||||
)
|
||||
if sem["responsables"]:
|
||||
initvalues["responsable_id"] = uid2display.get(
|
||||
sem["responsables"][0], sem["responsables"][0]
|
||||
)
|
||||
if len(sem["responsables"]) > 1:
|
||||
initvalues["responsable_id2"] = uid2display.get(
|
||||
sem["responsables"][1], sem["responsables"][1]
|
||||
|
@ -648,7 +649,7 @@ def do_formsemestre_createwithmodules(edit=False):
|
|||
# 'allowed_values' : ['X'], 'labels' : [ '' ],
|
||||
# 'title' : '' ,
|
||||
# 'explanation' : 'inscrire tous les étudiants du semestre aux modules ajoutés'}) )
|
||||
submitlabel = "Modifier ce semestre de formation"
|
||||
submitlabel = "Modifier ce semestre"
|
||||
else:
|
||||
submitlabel = "Créer ce semestre de formation"
|
||||
#
|
||||
|
@ -1314,7 +1315,7 @@ def _reassociate_moduleimpls(cnx, formsemestre_id, ues_old2new, modules_old2new)
|
|||
|
||||
def formsemestre_delete(formsemestre_id):
|
||||
"""Delete a formsemestre (affiche avertissements)"""
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
|
||||
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
|
||||
H = [
|
||||
html_sco_header.html_sem_header("Suppression du semestre"),
|
||||
|
@ -1493,11 +1494,9 @@ def do_formsemestre_delete(formsemestre_id):
|
|||
sco_formsemestre._formsemestreEditor.delete(cnx, formsemestre_id)
|
||||
|
||||
# news
|
||||
from app.scodoc import sco_news
|
||||
|
||||
sco_news.add(
|
||||
typ=sco_news.NEWS_SEM,
|
||||
object=formsemestre_id,
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_SEM,
|
||||
obj=formsemestre_id,
|
||||
text="Suppression du semestre %(titre)s" % sem,
|
||||
)
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
"""Tableau de bord semestre
|
||||
"""
|
||||
|
||||
import datetime
|
||||
from flask import current_app
|
||||
from flask import g
|
||||
from flask import request
|
||||
|
@ -357,6 +358,11 @@ def formsemestre_status_menubar(sem):
|
|||
"endpoint": "notes.formsemestre_recapcomplet",
|
||||
"args": {"formsemestre_id": formsemestre_id},
|
||||
},
|
||||
{
|
||||
"title": "État des évaluations",
|
||||
"endpoint": "notes.evaluations_recap",
|
||||
"args": {"formsemestre_id": formsemestre_id},
|
||||
},
|
||||
{
|
||||
"title": "Saisie des notes",
|
||||
"endpoint": "notes.formsemestre_status",
|
||||
|
@ -404,9 +410,6 @@ def formsemestre_status_menubar(sem):
|
|||
"args": {
|
||||
"formsemestre_id": formsemestre_id,
|
||||
"modejury": 1,
|
||||
"hidemodules": 1,
|
||||
"hidebac": 1,
|
||||
"pref_override": 0,
|
||||
},
|
||||
"enabled": sco_permissions_check.can_validate_sem(formsemestre_id),
|
||||
},
|
||||
|
@ -758,28 +761,34 @@ def _make_listes_sem(sem, with_absences=True):
|
|||
)
|
||||
|
||||
formsemestre_id = sem["formsemestre_id"]
|
||||
|
||||
# calcule dates 1er jour semaine pour absences
|
||||
weekday = datetime.datetime.today().weekday()
|
||||
try:
|
||||
if with_absences:
|
||||
first_monday = sco_abs.ddmmyyyy(sem["date_debut"]).prev_monday()
|
||||
form_abs_tmpl = f"""
|
||||
<td><form action="{url_for(
|
||||
<td>
|
||||
<a href="%(url_etat)s">absences</a>
|
||||
</td>
|
||||
<td>
|
||||
<form action="{url_for(
|
||||
"absences.SignaleAbsenceGrSemestre", scodoc_dept=g.scodoc_dept
|
||||
)}" method="get">
|
||||
<input type="hidden" name="datefin" value="{sem['date_fin']}"/>
|
||||
<input type="hidden" name="group_ids" value="%(group_id)s"/>
|
||||
<input type="hidden" name="destination" value="{destination}"/>
|
||||
<input type="submit" value="Saisir absences du" />
|
||||
<input type="submit" value="Saisir abs des" />
|
||||
<select name="datedebut" class="noprint">
|
||||
"""
|
||||
date = first_monday
|
||||
for jour in sco_abs.day_names():
|
||||
form_abs_tmpl += '<option value="%s">%s</option>' % (date, jour)
|
||||
for idx, jour in enumerate(sco_abs.day_names()):
|
||||
form_abs_tmpl += f"""<option value="{date}" {'selected' if idx == weekday else ''}>{jour}s</option>"""
|
||||
date = date.next_day()
|
||||
form_abs_tmpl += """
|
||||
form_abs_tmpl += f"""
|
||||
</select>
|
||||
<a href="%(url_etat)s">état</a>
|
||||
|
||||
<a href="{
|
||||
url_for("absences.choix_semaine", scodoc_dept=g.scodoc_dept)
|
||||
}?group_id=%(group_id)s">saisie par semaine</a>
|
||||
</form></td>
|
||||
"""
|
||||
else:
|
||||
|
@ -791,7 +800,7 @@ def _make_listes_sem(sem, with_absences=True):
|
|||
# Genere liste pour chaque partition (categorie de groupes)
|
||||
for partition in sco_groups.get_partitions_list(sem["formsemestre_id"]):
|
||||
if not partition["partition_name"]:
|
||||
H.append("<h4>Tous les étudiants</h4>" % partition)
|
||||
H.append("<h4>Tous les étudiants</h4>")
|
||||
else:
|
||||
H.append("<h4>Groupes de %(partition_name)s</h4>" % partition)
|
||||
groups = sco_groups.get_partition_groups(partition)
|
||||
|
@ -821,20 +830,6 @@ def _make_listes_sem(sem, with_absences=True):
|
|||
)
|
||||
}">{group["label"]}</a>
|
||||
</td><td>
|
||||
(<a href="{
|
||||
url_for("scolar.groups_view",
|
||||
group_ids=group["group_id"],
|
||||
format="xls",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
)
|
||||
}">tableur</a>)
|
||||
<a href="{
|
||||
url_for("scolar.groups_view",
|
||||
curtab="tab-photos",
|
||||
group_ids=group["group_id"],
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
)
|
||||
}">Photos</a>
|
||||
</td>
|
||||
<td>({n_members} étudiants)</td>
|
||||
"""
|
||||
|
@ -971,6 +966,7 @@ Il y a des notes en attente ! Le classement des étudiants n'a qu'une valeur ind
|
|||
def formsemestre_status(formsemestre_id=None):
|
||||
"""Tableau de bord semestre HTML"""
|
||||
# porté du DTML
|
||||
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
|
||||
modimpls = sco_moduleimpl.moduleimpl_withmodule_list(
|
||||
formsemestre_id=formsemestre_id
|
||||
|
@ -992,7 +988,9 @@ def formsemestre_status(formsemestre_id=None):
|
|||
use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id)
|
||||
|
||||
H = [
|
||||
html_sco_header.sco_header(page_title="Semestre %s" % sem["titreannee"]),
|
||||
html_sco_header.sco_header(
|
||||
page_title=f"{formsemestre.sem_modalite()} {formsemestre.titre_annee()}"
|
||||
),
|
||||
'<div class="formsemestre_status">',
|
||||
formsemestre_status_head(
|
||||
formsemestre_id=formsemestre_id, page_title="Tableau de bord"
|
||||
|
@ -1191,6 +1189,7 @@ def formsemestre_tableau_modules(
|
|||
H.append("<td>")
|
||||
if mod.module_type in (ModuleType.RESSOURCE, ModuleType.SAE):
|
||||
coefs = mod.ue_coefs_list()
|
||||
H.append(f'<a class="invisible_link" href="#" title="{mod_descr}">')
|
||||
for coef in coefs:
|
||||
if coef[1] > 0:
|
||||
H.append(
|
||||
|
@ -1202,6 +1201,7 @@ def formsemestre_tableau_modules(
|
|||
)
|
||||
else:
|
||||
H.append(f"""<span class="mod_coef_indicator_zero"></span>""")
|
||||
H.append("</a>")
|
||||
H.append("</td>")
|
||||
if mod.module_type in (
|
||||
None, # ne devrait pas être nécessaire car la migration a remplacé les NULLs
|
||||
|
|
|
@ -31,6 +31,7 @@ import time
|
|||
|
||||
import flask
|
||||
from flask import url_for, g, request
|
||||
from app.models.etudiants import Identite
|
||||
|
||||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
@ -107,29 +108,57 @@ def formsemestre_validation_etud_form(
|
|||
if not Se.sem["etat"]:
|
||||
raise ScoValueError("validation: semestre verrouille")
|
||||
|
||||
url_tableau = url_for(
|
||||
"notes.formsemestre_recapcomplet",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
modejury=1,
|
||||
formsemestre_id=formsemestre_id,
|
||||
selected_etudid=etudid, # va a la bonne ligne
|
||||
)
|
||||
|
||||
H = [
|
||||
html_sco_header.sco_header(
|
||||
page_title="Parcours %(nomprenom)s" % etud,
|
||||
page_title=f"Parcours {etud['nomprenom']}",
|
||||
javascripts=["js/recap_parcours.js"],
|
||||
)
|
||||
]
|
||||
|
||||
Footer = ["<p>"]
|
||||
# Navigation suivant/precedent
|
||||
if etud_index_prev != None:
|
||||
etud_p = sco_etud.get_etud_info(etudid=T[etud_index_prev][-1], filled=True)[0]
|
||||
Footer.append(
|
||||
'<span><a href="formsemestre_validation_etud_form?formsemestre_id=%s&etud_index=%s">Etud. précédent (%s)</a></span>'
|
||||
% (formsemestre_id, etud_index_prev, etud_p["nomprenom"])
|
||||
if etud_index_prev is not None:
|
||||
etud_prev = Identite.query.get(T[etud_index_prev][-1])
|
||||
url_prev = url_for(
|
||||
"notes.formsemestre_validation_etud_form",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
etud_index=etud_index_prev,
|
||||
)
|
||||
if etud_index_next != None:
|
||||
etud_n = sco_etud.get_etud_info(etudid=T[etud_index_next][-1], filled=True)[0]
|
||||
Footer.append(
|
||||
'<span style="padding-left: 50px;"><a href="formsemestre_validation_etud_form?formsemestre_id=%s&etud_index=%s">Etud. suivant (%s)</a></span>'
|
||||
% (formsemestre_id, etud_index_next, etud_n["nomprenom"])
|
||||
else:
|
||||
url_prev = None
|
||||
if etud_index_next is not None:
|
||||
etud_next = Identite.query.get(T[etud_index_next][-1])
|
||||
url_next = url_for(
|
||||
"notes.formsemestre_validation_etud_form",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
etud_index=etud_index_next,
|
||||
)
|
||||
Footer.append("</p>")
|
||||
Footer.append(html_sco_header.sco_footer())
|
||||
else:
|
||||
url_next = None
|
||||
footer = ["""<div class="jury_footer"><span>"""]
|
||||
if url_prev:
|
||||
footer.append(
|
||||
f'< <a class="stdlink" href="{url_prev}">{etud_prev.nomprenom}</a>'
|
||||
)
|
||||
footer.append(
|
||||
f"""</span><span><a class="stdlink" href="{url_tableau}">retour à la liste</a></span><span>"""
|
||||
)
|
||||
if url_next:
|
||||
footer.append(
|
||||
f'<a class="stdlink" href="{url_next}">{etud_next.nomprenom}</a> >'
|
||||
)
|
||||
footer.append("</span></div>")
|
||||
|
||||
footer.append(html_sco_header.sco_footer())
|
||||
|
||||
H.append('<table style="width: 100%"><tr><td>')
|
||||
if not check:
|
||||
|
@ -171,7 +200,7 @@ def formsemestre_validation_etud_form(
|
|||
"""
|
||||
)
|
||||
)
|
||||
return "\n".join(H + Footer)
|
||||
return "\n".join(H + footer)
|
||||
|
||||
H.append(
|
||||
formsemestre_recap_parcours_table(
|
||||
|
@ -180,21 +209,10 @@ def formsemestre_validation_etud_form(
|
|||
)
|
||||
if check:
|
||||
if not desturl:
|
||||
desturl = url_for(
|
||||
"notes.formsemestre_recapcomplet",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
modejury=1,
|
||||
hidemodules=1,
|
||||
hidebac=1,
|
||||
pref_override=0,
|
||||
formsemestre_id=formsemestre_id,
|
||||
sortcol=sortcol
|
||||
or None, # pour refaire tri sorttable du tableau de notes
|
||||
_anchor="etudid%s" % etudid, # va a la bonne ligne
|
||||
)
|
||||
desturl = url_tableau
|
||||
H.append(f'<ul><li><a href="{desturl}">Continuer</a></li></ul>')
|
||||
|
||||
return "\n".join(H + Footer)
|
||||
return "\n".join(H + footer)
|
||||
|
||||
decision_jury = Se.nt.get_etud_decision_sem(etudid)
|
||||
|
||||
|
@ -210,7 +228,7 @@ def formsemestre_validation_etud_form(
|
|||
"""
|
||||
)
|
||||
)
|
||||
return "\n".join(H + Footer)
|
||||
return "\n".join(H + footer)
|
||||
|
||||
# Infos si pas de semestre précédent
|
||||
if not Se.prev:
|
||||
|
@ -348,7 +366,7 @@ def formsemestre_validation_etud_form(
|
|||
else:
|
||||
H.append("sans semestres décalés</p>")
|
||||
|
||||
return "".join(H + Footer)
|
||||
return "".join(H + footer)
|
||||
|
||||
|
||||
def formsemestre_validation_etud(
|
||||
|
@ -437,14 +455,6 @@ def _redirect_valid_choice(formsemestre_id, etudid, Se, choice, desturl, sortcol
|
|||
# sinon renvoie au listing general,
|
||||
|
||||
|
||||
# if choice.new_code_prev:
|
||||
# flask.redirect( 'formsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s&check=1&desturl=%s' % (formsemestre_id, etudid, desturl) )
|
||||
# else:
|
||||
# if not desturl:
|
||||
# desturl = 'formsemestre_recapcomplet?modejury=1&hidemodules=1&formsemestre_id=' + str(formsemestre_id)
|
||||
# flask.redirect(desturl)
|
||||
|
||||
|
||||
def _dispcode(c):
|
||||
if not c:
|
||||
return ""
|
||||
|
@ -948,19 +958,23 @@ def do_formsemestre_validation_auto(formsemestre_id):
|
|||
)
|
||||
if conflicts:
|
||||
H.append(
|
||||
"""<p><b>Attention:</b> %d étudiants non modifiés car décisions différentes
|
||||
déja saisies :<ul>"""
|
||||
% len(conflicts)
|
||||
f"""<p><b>Attention:</b> {len(conflicts)} étudiants non modifiés
|
||||
car décisions différentes déja saisies :
|
||||
<ul>"""
|
||||
)
|
||||
for etud in conflicts:
|
||||
H.append(
|
||||
'<li><a href="formsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s&check=1">%s</li>'
|
||||
% (formsemestre_id, etud["etudid"], etud["nomprenom"])
|
||||
f"""<li><a href="{
|
||||
url_for('notes.formsemestre_validation_etud_form',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id,
|
||||
etudid=etud["etudid"], check=1)
|
||||
}">{etud["nomprenom"]}</li>"""
|
||||
)
|
||||
H.append("</ul>")
|
||||
H.append(
|
||||
'<a href="formsemestre_recapcomplet?formsemestre_id=%s&modejury=1&hidemodules=1&hidebac=1&pref_override=0">continuer</a>'
|
||||
% formsemestre_id
|
||||
f"""<a href="{url_for('notes.formsemestre_recapcomplet',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, modejury=1)
|
||||
}">continuer</a>"""
|
||||
)
|
||||
H.append(html_sco_header.sco_footer())
|
||||
return "\n".join(H)
|
||||
|
|
|
@ -40,6 +40,8 @@ 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.models import ScolarNews
|
||||
|
||||
from app.scodoc.sco_excel import COLORS
|
||||
from app.scodoc.sco_formsemestre_inscriptions import (
|
||||
do_formsemestre_inscription_with_modules,
|
||||
|
@ -54,14 +56,13 @@ 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
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_excel
|
||||
from app.scodoc import sco_groups_view
|
||||
from app.scodoc import sco_news
|
||||
from app.scodoc import sco_preferences
|
||||
|
||||
# format description (in tools/)
|
||||
|
@ -472,11 +473,11 @@ def scolars_import_excel_file(
|
|||
|
||||
diag.append("Import et inscription de %s étudiants" % len(created_etudids))
|
||||
|
||||
sco_news.add(
|
||||
typ=sco_news.NEWS_INSCR,
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_INSCR,
|
||||
text="Inscription de %d étudiants" # peuvent avoir ete inscrits a des semestres differents
|
||||
% len(created_etudids),
|
||||
object=formsemestre_id,
|
||||
obj=formsemestre_id,
|
||||
)
|
||||
|
||||
log("scolars_import_excel_file: completing transaction")
|
||||
|
|
|
@ -41,6 +41,7 @@ from app.comp.moy_mod import ModuleImplResults
|
|||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.models import FormSemestre
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.evaluations import Evaluation
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
@ -53,7 +54,6 @@ from app.scodoc import sco_formsemestre
|
|||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_users
|
||||
import sco_version
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
|
@ -320,7 +320,9 @@ def _make_table_notes(
|
|||
for etudid, etat in etudid_etats:
|
||||
css_row_class = None
|
||||
# infos identite etudiant
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
etud = Identite.query.get(etudid)
|
||||
if etud is None:
|
||||
continue
|
||||
|
||||
if etat == "I": # si inscrit, indique groupe
|
||||
groups = sco_groups.get_etud_groups(etudid, modimpl_o["formsemestre_id"])
|
||||
|
@ -332,7 +334,7 @@ def _make_table_notes(
|
|||
else:
|
||||
grc = etat
|
||||
|
||||
code = etud.get(anonymous_lst_key)
|
||||
code = getattr(etud, anonymous_lst_key)
|
||||
if not code: # laisser le code vide n'aurait aucun sens, prenons l'etudid
|
||||
code = etudid
|
||||
|
||||
|
@ -341,20 +343,20 @@ def _make_table_notes(
|
|||
"code": str(code), # INE, NIP ou etudid
|
||||
"_code_td_attrs": 'style="padding-left: 1em; padding-right: 2em;"',
|
||||
"etudid": etudid,
|
||||
"nom": etud["nom"].upper(),
|
||||
"nom": etud.nom.upper(),
|
||||
"_nomprenom_target": url_for(
|
||||
"notes.formsemestre_bulletinetud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=modimpl_o["formsemestre_id"],
|
||||
etudid=etudid,
|
||||
),
|
||||
"_nomprenom_td_attrs": f"""id="{etudid}" class="etudinfo" data-sort="{etud.get('nom', '').upper()}" """,
|
||||
"prenom": etud["prenom"].lower().capitalize(),
|
||||
"nomprenom": etud["nomprenom"],
|
||||
"_nomprenom_td_attrs": f"""id="{etudid}" class="etudinfo" data-sort="{etud.sort_key}" """,
|
||||
"prenom": etud.prenom.lower().capitalize(),
|
||||
"nomprenom": etud.nomprenom,
|
||||
"group": grc,
|
||||
"_group_td_attrs": 'class="group"',
|
||||
"email": etud["email"],
|
||||
"emailperso": etud["emailperso"],
|
||||
"email": etud.get_first_email(),
|
||||
"emailperso": etud.get_first_email("emailperso"),
|
||||
"_css_row_class": css_row_class or "",
|
||||
}
|
||||
)
|
||||
|
@ -573,9 +575,7 @@ def _make_table_notes(
|
|||
html_sortable=True,
|
||||
base_url=base_url,
|
||||
filename=filename,
|
||||
origin="Généré par %s le " % sco_version.SCONAME
|
||||
+ scu.timedate_human_repr()
|
||||
+ "",
|
||||
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
|
||||
caption=caption,
|
||||
html_next_section=html_next_section,
|
||||
page_title="Notes de " + sem["titremois"],
|
||||
|
@ -672,6 +672,21 @@ def _add_eval_columns(
|
|||
e_o = Evaluation.query.get(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)
|
||||
|
||||
if len(e["jour"]) > 0:
|
||||
titles[evaluation_id] = "%(description)s (%(jour)s)" % e
|
||||
else:
|
||||
titles[evaluation_id] = "%(description)s " % e
|
||||
|
||||
if e["eval_state"]["evalcomplete"]:
|
||||
klass = "eval_complete"
|
||||
elif e["eval_state"]["evalattente"]:
|
||||
klass = "eval_attente"
|
||||
else:
|
||||
klass = "eval_incomplete"
|
||||
titles[evaluation_id] += " (non prise en compte)"
|
||||
titles[f"_{evaluation_id}_td_attrs"] = f'class="{klass}"'
|
||||
|
||||
for row in rows:
|
||||
etudid = row["etudid"]
|
||||
if etudid in notes_db:
|
||||
|
@ -683,7 +698,7 @@ def _add_eval_columns(
|
|||
# calcul moyenne SANS LES ABSENTS ni les DEMISSIONNAIRES
|
||||
if (
|
||||
(etudid in inscrits)
|
||||
and val != None
|
||||
and val is not None
|
||||
and val != scu.NOTES_NEUTRALISE
|
||||
and val != scu.NOTES_ATTENTE
|
||||
):
|
||||
|
@ -706,14 +721,26 @@ def _add_eval_columns(
|
|||
comment,
|
||||
)
|
||||
else:
|
||||
explanation = ""
|
||||
val_fmt = ""
|
||||
val = None
|
||||
if (etudid in inscrits) and e["publish_incomplete"]:
|
||||
# Note manquante mais prise en compte immédiate: affiche ATT
|
||||
val = scu.NOTES_ATTENTE
|
||||
val_fmt = "ATT"
|
||||
explanation = "non saisie mais prise en compte immédiate"
|
||||
else:
|
||||
explanation = ""
|
||||
val_fmt = ""
|
||||
val = None
|
||||
|
||||
cell_class = klass + {"ATT": " att", "ABS": " abs", "EXC": " exc"}.get(
|
||||
val_fmt, ""
|
||||
)
|
||||
|
||||
if val is None:
|
||||
row["_" + str(evaluation_id) + "_td_attrs"] = 'class="etudabs" '
|
||||
row[f"_{evaluation_id}_td_attrs"] = f'class="etudabs {cell_class}" '
|
||||
if not row.get("_css_row_class", ""):
|
||||
row["_css_row_class"] = "etudabs"
|
||||
else:
|
||||
row[f"_{evaluation_id}_td_attrs"] = f'class="{cell_class}" '
|
||||
# regroupe les commentaires
|
||||
if explanation:
|
||||
if explanation in K:
|
||||
|
@ -766,18 +793,6 @@ def _add_eval_columns(
|
|||
else:
|
||||
row_moys[evaluation_id] = ""
|
||||
|
||||
if len(e["jour"]) > 0:
|
||||
titles[evaluation_id] = "%(description)s (%(jour)s)" % e
|
||||
else:
|
||||
titles[evaluation_id] = "%(description)s " % e
|
||||
|
||||
if e["eval_state"]["evalcomplete"]:
|
||||
titles["_" + str(evaluation_id) + "_td_attrs"] = 'class="eval_complete"'
|
||||
elif e["eval_state"]["evalattente"]:
|
||||
titles["_" + str(evaluation_id) + "_td_attrs"] = 'class="eval_attente"'
|
||||
else:
|
||||
titles["_" + str(evaluation_id) + "_td_attrs"] = 'class="eval_incomplete"'
|
||||
|
||||
return notes, nb_abs, nb_att # pour histogramme
|
||||
|
||||
|
||||
|
|
|
@ -89,6 +89,11 @@ def write_logo(stream, name, dept_id=None):
|
|||
Logo(logoname=name, dept_id=dept_id).create(stream)
|
||||
|
||||
|
||||
def rename_logo(old_name, new_name, dept_id):
|
||||
logo = find_logo(old_name, dept_id, True)
|
||||
logo.rename(new_name)
|
||||
|
||||
|
||||
def list_logos():
|
||||
"""Crée l'inventaire de tous les logos existants.
|
||||
L'inventaire se présente comme un dictionnaire de dictionnaire de Logo:
|
||||
|
@ -285,6 +290,20 @@ class Logo:
|
|||
dt = path.stat().st_mtime
|
||||
return path.stat().st_mtime
|
||||
|
||||
def rename(self, new_name):
|
||||
"""Change le nom (pas le département)
|
||||
Les éléments non utiles ne sont pas recalculés (car rechargés lors des accès ultérieurs)
|
||||
"""
|
||||
old_path = Path(self.filepath)
|
||||
self.logoname = secure_filename(new_name)
|
||||
if not self.logoname:
|
||||
self.logoname = "*** *** nom de logo invalide *** à changer ! *** ***"
|
||||
else:
|
||||
new_path = os.path.sep.join(
|
||||
[self.dirpath, self.prefix + self.logoname + "." + self.suffix]
|
||||
)
|
||||
old_path.rename(new_path)
|
||||
|
||||
|
||||
def guess_image_type(stream) -> str:
|
||||
"guess image type from header in stream"
|
||||
|
|
|
@ -1,276 +0,0 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Gestion des "nouvelles"
|
||||
"""
|
||||
import re
|
||||
import time
|
||||
|
||||
|
||||
from operator import itemgetter
|
||||
|
||||
from flask import g
|
||||
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.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_preferences
|
||||
from app import email
|
||||
|
||||
|
||||
_scolar_news_editor = ndb.EditableTable(
|
||||
"scolar_news",
|
||||
"news_id",
|
||||
("date", "authenticated_user", "type", "object", "text", "url"),
|
||||
filter_dept=True,
|
||||
sortkey="date desc",
|
||||
output_formators={"date": ndb.DateISOtoDMY},
|
||||
input_formators={"date": ndb.DateDMYtoISO},
|
||||
html_quote=False, # no user supplied data, needed to store html links
|
||||
)
|
||||
|
||||
NEWS_INSCR = "INSCR" # inscription d'étudiants (object=None ou formsemestre_id)
|
||||
NEWS_NOTE = "NOTES" # saisie note (object=moduleimpl_id)
|
||||
NEWS_FORM = "FORM" # modification formation (object=formation_id)
|
||||
NEWS_SEM = "SEM" # creation semestre (object=None)
|
||||
NEWS_MISC = "MISC" # unused
|
||||
NEWS_MAP = {
|
||||
NEWS_INSCR: "inscription d'étudiants",
|
||||
NEWS_NOTE: "saisie note",
|
||||
NEWS_FORM: "modification formation",
|
||||
NEWS_SEM: "création semestre",
|
||||
NEWS_MISC: "opération", # unused
|
||||
}
|
||||
NEWS_TYPES = list(NEWS_MAP.keys())
|
||||
|
||||
scolar_news_create = _scolar_news_editor.create
|
||||
scolar_news_list = _scolar_news_editor.list
|
||||
|
||||
_LAST_NEWS = {} # { (authuser_name, type, object) : time }
|
||||
|
||||
|
||||
def add(typ, object=None, text="", url=None, max_frequency=False):
|
||||
"""Ajoute une nouvelle.
|
||||
Si max_frequency, ne genere pas 2 nouvelles identiques à moins de max_frequency
|
||||
secondes d'intervalle.
|
||||
"""
|
||||
from app.scodoc import sco_users
|
||||
|
||||
authuser_name = current_user.user_name
|
||||
cnx = ndb.GetDBConnexion()
|
||||
args = {
|
||||
"authenticated_user": authuser_name,
|
||||
"user_info": sco_users.user_info(authuser_name),
|
||||
"type": typ,
|
||||
"object": object,
|
||||
"text": text,
|
||||
"url": url,
|
||||
}
|
||||
t = time.time()
|
||||
if max_frequency:
|
||||
last_news_time = _LAST_NEWS.get((authuser_name, typ, object), False)
|
||||
if last_news_time and (t - last_news_time < max_frequency):
|
||||
# log("not recording")
|
||||
return
|
||||
|
||||
log("news: %s" % args)
|
||||
|
||||
_LAST_NEWS[(authuser_name, typ, object)] = t
|
||||
|
||||
_send_news_by_mail(args)
|
||||
return scolar_news_create(cnx, args)
|
||||
|
||||
|
||||
def scolar_news_summary(n=5):
|
||||
"""Return last n news.
|
||||
News are "compressed", ie redondant events are joined.
|
||||
"""
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_users
|
||||
|
||||
cnx = ndb.GetDBConnexion()
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
cursor.execute(
|
||||
"""SELECT id AS news_id, *
|
||||
FROM scolar_news
|
||||
WHERE dept_id=%(dept_id)s
|
||||
ORDER BY date DESC LIMIT 100
|
||||
""",
|
||||
{"dept_id": g.scodoc_dept_id},
|
||||
)
|
||||
selected_news = {} # (type,object) : news dict
|
||||
news = cursor.dictfetchall() # la plus récente d'abord
|
||||
|
||||
for r in reversed(news): # la plus ancienne d'abord
|
||||
# si on a deja une news avec meme (type,object)
|
||||
# et du meme jour, on la remplace
|
||||
dmy = ndb.DateISOtoDMY(r["date"]) # round
|
||||
key = (r["type"], r["object"], dmy)
|
||||
selected_news[key] = r
|
||||
|
||||
news = list(selected_news.values())
|
||||
# sort by date, descending
|
||||
news.sort(key=itemgetter("date"), reverse=True)
|
||||
news = news[:n]
|
||||
# mimic EditableTable.list output formatting:
|
||||
for n in news:
|
||||
n["date822"] = n["date"].strftime("%a, %d %b %Y %H:%M:%S %z")
|
||||
# heure
|
||||
n["hm"] = n["date"].strftime("%Hh%M")
|
||||
for k in n.keys():
|
||||
if n[k] is None:
|
||||
n[k] = ""
|
||||
if k in _scolar_news_editor.output_formators:
|
||||
n[k] = _scolar_news_editor.output_formators[k](n[k])
|
||||
# date resumee
|
||||
j, m = n["date"].split("/")[:2]
|
||||
mois = scu.MONTH_NAMES_ABBREV[int(m) - 1]
|
||||
n["formatted_date"] = "%s %s %s" % (j, mois, n["hm"])
|
||||
# indication semestre si ajout notes:
|
||||
infos = _get_formsemestre_infos_from_news(n)
|
||||
if infos:
|
||||
n["text"] += (
|
||||
' (<a href="Notes/formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(descr_sem)s</a>)'
|
||||
% infos
|
||||
)
|
||||
n["text"] += (
|
||||
" par " + sco_users.user_info(n["authenticated_user"])["nomcomplet"]
|
||||
)
|
||||
return news
|
||||
|
||||
|
||||
def _get_formsemestre_infos_from_news(n):
|
||||
"""Informations sur le semestre concerné par la nouvelle n
|
||||
{} si inexistant
|
||||
"""
|
||||
formsemestre_id = None
|
||||
if n["type"] == NEWS_INSCR:
|
||||
formsemestre_id = n["object"]
|
||||
elif n["type"] == NEWS_NOTE:
|
||||
moduleimpl_id = n["object"]
|
||||
if n["object"]:
|
||||
mods = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)
|
||||
if not mods:
|
||||
return {} # module does not exists anymore
|
||||
return {} # pas d'indication du module
|
||||
mod = mods[0]
|
||||
formsemestre_id = mod["formsemestre_id"]
|
||||
|
||||
if not formsemestre_id:
|
||||
return {}
|
||||
|
||||
try:
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
except:
|
||||
# semestre n'existe plus
|
||||
return {}
|
||||
|
||||
if sem["semestre_id"] > 0:
|
||||
descr_sem = "S%d" % sem["semestre_id"]
|
||||
else:
|
||||
descr_sem = ""
|
||||
if sem["modalite"]:
|
||||
descr_sem += " " + sem["modalite"]
|
||||
return {"formsemestre_id": formsemestre_id, "sem": sem, "descr_sem": descr_sem}
|
||||
|
||||
|
||||
def scolar_news_summary_html(n=5):
|
||||
"""News summary, formated in HTML"""
|
||||
news = scolar_news_summary(n=n)
|
||||
if not news:
|
||||
return ""
|
||||
H = ['<div class="news"><span class="newstitle">Dernières opérations']
|
||||
H.append('</span><ul class="newslist">')
|
||||
|
||||
for n in news:
|
||||
H.append(
|
||||
'<li class="newslist"><span class="newsdate">%(formatted_date)s</span><span class="newstext">%(text)s</span></li>'
|
||||
% n
|
||||
)
|
||||
H.append("</ul>")
|
||||
|
||||
# Informations générales
|
||||
H.append(
|
||||
"""<div>
|
||||
Pour être informé des évolutions de ScoDoc,
|
||||
vous pouvez vous
|
||||
<a class="stdlink" href="%s">
|
||||
abonner à la liste de diffusion</a>.
|
||||
</div>
|
||||
"""
|
||||
% scu.SCO_ANNONCES_WEBSITE
|
||||
)
|
||||
|
||||
H.append("</div>")
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
def _send_news_by_mail(n):
|
||||
"""Notify by email"""
|
||||
infos = _get_formsemestre_infos_from_news(n)
|
||||
formsemestre_id = infos.get("formsemestre_id", None)
|
||||
prefs = sco_preferences.SemPreferences(formsemestre_id=formsemestre_id)
|
||||
destinations = prefs["emails_notifications"] or ""
|
||||
destinations = [x.strip() for x in destinations.split(",")]
|
||||
destinations = [x for x in destinations if x]
|
||||
if not destinations:
|
||||
return
|
||||
#
|
||||
txt = n["text"]
|
||||
if infos:
|
||||
txt += "\n\nSemestre %(titremois)s\n\n" % infos["sem"]
|
||||
txt += (
|
||||
"""<a href="Notes/formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(descr_sem)s</a>
|
||||
"""
|
||||
% infos
|
||||
)
|
||||
txt += "\n\nEffectué par: %(nomcomplet)s\n" % n["user_info"]
|
||||
|
||||
txt = (
|
||||
"\n"
|
||||
+ txt
|
||||
+ """\n
|
||||
--- Ceci est un message de notification automatique issu de ScoDoc
|
||||
--- vous recevez ce message car votre adresse est indiquée dans les paramètres de ScoDoc.
|
||||
"""
|
||||
)
|
||||
|
||||
# Transforme les URL en URL absolue
|
||||
base = scu.ScoURL()
|
||||
txt = re.sub('href=.*?"', 'href="' + base + "/", txt)
|
||||
|
||||
# Transforme les liens HTML en texte brut: '<a href="url">texte</a>' devient 'texte: url'
|
||||
# (si on veut des messages non html)
|
||||
txt = re.sub(r'<a.*?href\s*=\s*"(.*?)".*?>(.*?)</a>', r"\2: \1", txt)
|
||||
|
||||
subject = "[ScoDoc] " + NEWS_MAP.get(n["type"], "?")
|
||||
sender = prefs["email_from_addr"]
|
||||
|
||||
email.send_email(subject, sender, destinations, txt)
|
|
@ -48,6 +48,12 @@ _SCO_PERMISSIONS = (
|
|||
(1 << 25, "RelationsEntreprisesSend", "Envoyer des offres"),
|
||||
(1 << 26, "RelationsEntreprisesValidate", "Valide les entreprises"),
|
||||
(1 << 27, "RelationsEntreprisesCorrespondants", "Voir les correspondants"),
|
||||
# 27 à 39 ... réservé pour "entreprises"
|
||||
# Api scodoc9
|
||||
(1 << 40, "APIView", "Voir"),
|
||||
(1 << 41, "APIEtudChangeGroups", "Modifier les groupes"),
|
||||
(1 << 42, "APIEditAllNotes", "Modifier toutes les notes"),
|
||||
(1 << 43, "APIAbsChange", "Saisir des absences"),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -37,12 +37,20 @@ import xml.dom.minidom
|
|||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import log
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc import sco_preferences
|
||||
|
||||
SCO_CACHE_ETAPE_FILENAME = os.path.join(scu.SCO_TMP_DIR, "last_etapes.xml")
|
||||
|
||||
|
||||
class ApoInscritsEtapeCache(sco_cache.ScoDocCache):
|
||||
"""Cache liste des inscrits à une étape Apogée"""
|
||||
|
||||
timeout = 10 * 60 # 10 minutes
|
||||
prefix = "APOINSCRETAP"
|
||||
|
||||
|
||||
def has_portal():
|
||||
"True if we are connected to a portal"
|
||||
return get_portal_url()
|
||||
|
@ -139,14 +147,20 @@ get_maquette_url = _PI.get_maquette_url
|
|||
get_portal_api_version = _PI.get_portal_api_version
|
||||
|
||||
|
||||
def get_inscrits_etape(code_etape, anneeapogee=None, ntrials=2):
|
||||
def get_inscrits_etape(code_etape, anneeapogee=None, ntrials=4, use_cache=True):
|
||||
"""Liste des inscrits à une étape Apogée
|
||||
Result = list of dicts
|
||||
ntrials: try several time the same request, useful for some bad web services
|
||||
use_cache: use (redis) cache
|
||||
"""
|
||||
log("get_inscrits_etape: code=%s anneeapogee=%s" % (code_etape, anneeapogee))
|
||||
if anneeapogee is None:
|
||||
anneeapogee = str(time.localtime()[0])
|
||||
if use_cache:
|
||||
obj = ApoInscritsEtapeCache.get((code_etape, anneeapogee))
|
||||
if obj:
|
||||
log("get_inscrits_etape: using cached data")
|
||||
return obj
|
||||
|
||||
etud_url = get_etud_url()
|
||||
api_ver = get_portal_api_version()
|
||||
|
@ -189,6 +203,8 @@ def get_inscrits_etape(code_etape, anneeapogee=None, ntrials=2):
|
|||
return False # ??? pas d'annee d'inscription dans la réponse
|
||||
|
||||
etuds = [e for e in etuds if check_inscription(e)]
|
||||
if use_cache and etuds:
|
||||
ApoInscritsEtapeCache.set((code_etape, anneeapogee), etuds)
|
||||
return etuds
|
||||
|
||||
|
||||
|
|
|
@ -350,7 +350,7 @@ class BasePreferences(object):
|
|||
"initvalue": "",
|
||||
"title": "e-mails à qui notifier les opérations",
|
||||
"size": 70,
|
||||
"explanation": "adresses séparées par des virgules; notifie les opérations (saisies de notes, etc). (vous pouvez préférer utiliser le flux rss)",
|
||||
"explanation": "adresses séparées par des virgules; notifie les opérations (saisies de notes, etc).",
|
||||
"category": "general",
|
||||
"only_global": False, # peut être spécifique à un semestre
|
||||
},
|
||||
|
@ -382,18 +382,6 @@ class BasePreferences(object):
|
|||
"only_global": False,
|
||||
},
|
||||
),
|
||||
(
|
||||
"recap_hidebac",
|
||||
{
|
||||
"initvalue": 0,
|
||||
"title": "Cacher la colonne Bac",
|
||||
"explanation": "sur la table récapitulative",
|
||||
"input_type": "boolcheckbox",
|
||||
"category": "misc",
|
||||
"labels": ["non", "oui"],
|
||||
"only_global": False,
|
||||
},
|
||||
),
|
||||
# ------------------ Absences
|
||||
(
|
||||
"email_chefdpt",
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -53,6 +53,7 @@ SCO_ROLES_DEFAULTS = {
|
|||
p.ScoUsersAdmin,
|
||||
p.ScoUsersView,
|
||||
p.ScoView,
|
||||
p.APIView,
|
||||
),
|
||||
# RespPE est le responsable poursuites d'études
|
||||
# il peut ajouter des tags sur les formations:
|
||||
|
@ -76,6 +77,8 @@ SCO_ROLES_DEFAULTS = {
|
|||
p.RelationsEntreprisesValidate,
|
||||
p.RelationsEntreprisesCorrespondants,
|
||||
),
|
||||
# LecteurAPI peut utiliser l'API en lecture
|
||||
"LecteurAPI": (p.APIView,),
|
||||
# Super Admin est un root: création/suppression de départements
|
||||
# _tous_ les droits
|
||||
# Afin d'avoir tous les droits, il ne doit pas être asscoié à un département
|
||||
|
@ -90,4 +93,5 @@ ROLES_ATTRIBUABLES_SCODOC = (
|
|||
"ObservateurEntreprise",
|
||||
"UtilisateurEntreprise",
|
||||
"AdminEntreprise",
|
||||
"LecteurAPI",
|
||||
)
|
||||
|
|
|
@ -39,6 +39,7 @@ from flask_login import current_user
|
|||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre
|
||||
from app.models import ScolarNews
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
import app.scodoc.notesdb as ndb
|
||||
|
@ -48,6 +49,7 @@ from app.scodoc.sco_exceptions import (
|
|||
InvalidNoteValue,
|
||||
NoteProcessError,
|
||||
ScoGenError,
|
||||
ScoInvalidParamError,
|
||||
ScoValueError,
|
||||
)
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator, TF
|
||||
|
@ -64,7 +66,6 @@ from app.scodoc import sco_formsemestre_inscriptions
|
|||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_groups_view
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_news
|
||||
from app.scodoc import sco_permissions_check
|
||||
from app.scodoc import sco_undo_notes
|
||||
from app.scodoc import sco_etud
|
||||
|
@ -274,11 +275,12 @@ def do_evaluation_upload_xls():
|
|||
moduleimpl_id=mod["moduleimpl_id"],
|
||||
_external=True,
|
||||
)
|
||||
sco_news.add(
|
||||
typ=sco_news.NEWS_NOTE,
|
||||
object=M["moduleimpl_id"],
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_NOTE,
|
||||
obj=M["moduleimpl_id"],
|
||||
text='Chargement notes dans <a href="%(url)s">%(titre)s</a>' % mod,
|
||||
url=mod["url"],
|
||||
max_frequency=30 * 60, # 30 minutes
|
||||
)
|
||||
|
||||
msg = (
|
||||
|
@ -359,11 +361,12 @@ def do_evaluation_set_missing(evaluation_id, value, dialog_confirmed=False):
|
|||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=mod["moduleimpl_id"],
|
||||
)
|
||||
sco_news.add(
|
||||
typ=sco_news.NEWS_NOTE,
|
||||
object=M["moduleimpl_id"],
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_NOTE,
|
||||
obj=M["moduleimpl_id"],
|
||||
text='Initialisation notes dans <a href="%(url)s">%(titre)s</a>' % mod,
|
||||
url=mod["url"],
|
||||
max_frequency=30 * 60,
|
||||
)
|
||||
return (
|
||||
html_sco_header.sco_header()
|
||||
|
@ -451,9 +454,9 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
|
|||
mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
||||
mod["moduleimpl_id"] = M["moduleimpl_id"]
|
||||
mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
|
||||
sco_news.add(
|
||||
typ=sco_news.NEWS_NOTE,
|
||||
object=M["moduleimpl_id"],
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_NOTE,
|
||||
obj=M["moduleimpl_id"],
|
||||
text='Suppression des notes d\'une évaluation dans <a href="%(url)s">%(titre)s</a>'
|
||||
% mod,
|
||||
url=mod["url"],
|
||||
|
@ -893,10 +896,12 @@ def has_existing_decision(M, E, etudid):
|
|||
|
||||
def saisie_notes(evaluation_id, group_ids=[]):
|
||||
"""Formulaire saisie notes d'une évaluation pour un groupe"""
|
||||
if not isinstance(evaluation_id, int):
|
||||
raise ScoInvalidParamError()
|
||||
group_ids = [int(group_id) for group_id in group_ids]
|
||||
evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})
|
||||
if not evals:
|
||||
raise ScoValueError("invalid evaluation_id")
|
||||
raise ScoValueError("évaluation inexistante")
|
||||
E = evals[0]
|
||||
M = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=E["moduleimpl_id"])[0]
|
||||
formsemestre_id = M["formsemestre_id"]
|
||||
|
@ -1283,9 +1288,9 @@ def save_note(etudid=None, evaluation_id=None, value=None, comment=""):
|
|||
nbchanged, _, existing_decisions = notes_add(
|
||||
authuser, evaluation_id, L, comment=comment, do_it=True
|
||||
)
|
||||
sco_news.add(
|
||||
typ=sco_news.NEWS_NOTE,
|
||||
object=M["moduleimpl_id"],
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_NOTE,
|
||||
obj=M["moduleimpl_id"],
|
||||
text='Chargement notes dans <a href="%(url)s">%(titre)s</a>' % Mod,
|
||||
url=Mod["url"],
|
||||
max_frequency=30 * 60, # 30 minutes
|
||||
|
|
|
@ -29,12 +29,14 @@
|
|||
"""
|
||||
|
||||
import time
|
||||
import pprint
|
||||
from operator import itemgetter
|
||||
|
||||
from flask import g, url_for
|
||||
from flask_login import current_user
|
||||
|
||||
from app import log
|
||||
from app.models import ScolarNews
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc import html_sco_header
|
||||
|
@ -43,11 +45,8 @@ from app.scodoc import sco_formsemestre
|
|||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_inscr_passage
|
||||
from app.scodoc import sco_news
|
||||
from app.scodoc import sco_excel
|
||||
from app.scodoc import sco_portal_apogee
|
||||
from app.scodoc import sco_etud
|
||||
from app import log
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
|
@ -701,10 +700,11 @@ def do_import_etuds_from_portal(sem, a_importer, etudsapo_ident):
|
|||
sco_cache.invalidate_formsemestre()
|
||||
raise
|
||||
|
||||
sco_news.add(
|
||||
typ=sco_news.NEWS_INSCR,
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_INSCR,
|
||||
text="Import Apogée de %d étudiants en " % len(created_etudids),
|
||||
object=sem["formsemestre_id"],
|
||||
obj=sem["formsemestre_id"],
|
||||
max_frequency=10 * 60, # 10'
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -55,19 +55,18 @@ from app.scodoc.sco_pdf import SU
|
|||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import htmlutils
|
||||
from app.scodoc import sco_import_etuds
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_excel
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_groups_view
|
||||
from app.scodoc import sco_pdf
|
||||
from app.scodoc import sco_photos
|
||||
from app.scodoc import sco_portal_apogee
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_trombino_doc
|
||||
|
||||
|
||||
def trombino(
|
||||
group_ids=[], # liste des groupes à afficher
|
||||
group_ids=(), # liste des groupes à afficher
|
||||
formsemestre_id=None, # utilisé si pas de groupes selectionné
|
||||
etat=None,
|
||||
format="html",
|
||||
|
@ -93,6 +92,8 @@ def trombino(
|
|||
return _trombino_pdf(groups_infos)
|
||||
elif format == "pdflist":
|
||||
return _listeappel_photos_pdf(groups_infos)
|
||||
elif format == "doc":
|
||||
return sco_trombino_doc.trombino_doc(groups_infos)
|
||||
else:
|
||||
raise Exception("invalid format")
|
||||
# return _trombino_html_header() + trombino_html( group, members) + html_sco_header.sco_footer()
|
||||
|
@ -176,8 +177,13 @@ def trombino_html(groups_infos):
|
|||
|
||||
H.append("</div>")
|
||||
H.append(
|
||||
'<div style="margin-bottom:15px;"><a class="stdlink" href="trombino?format=pdf&%s">Version PDF</a></div>'
|
||||
% groups_infos.groups_query_args
|
||||
f"""<div style="margin-bottom:15px;">
|
||||
<a class="stdlink" href="{url_for('scolar.trombino', scodoc_dept=g.scodoc_dept,
|
||||
format='pdf', group_ids=groups_infos.group_ids)}">Version PDF</a>
|
||||
|
||||
<a class="stdlink" href="{url_for('scolar.trombino', scodoc_dept=g.scodoc_dept,
|
||||
format='doc', group_ids=groups_infos.group_ids)}">Version doc</a>
|
||||
</div>"""
|
||||
)
|
||||
return "\n".join(H)
|
||||
|
||||
|
@ -234,7 +240,7 @@ def _trombino_zip(groups_infos):
|
|||
Z.writestr(filename, img)
|
||||
Z.close()
|
||||
size = data.tell()
|
||||
log("trombino_zip: %d bytes" % size)
|
||||
log(f"trombino_zip: {size} bytes")
|
||||
data.seek(0)
|
||||
return send_file(
|
||||
data,
|
||||
|
@ -470,7 +476,7 @@ def _listeappel_photos_pdf(groups_infos):
|
|||
|
||||
|
||||
# --------------------- Upload des photos de tout un groupe
|
||||
def photos_generate_excel_sample(group_ids=[]):
|
||||
def photos_generate_excel_sample(group_ids=()):
|
||||
"""Feuille excel pour import fichiers photos"""
|
||||
fmt = sco_import_etuds.sco_import_format()
|
||||
data = sco_import_etuds.sco_import_generate_excel_sample(
|
||||
|
@ -492,31 +498,33 @@ def photos_generate_excel_sample(group_ids=[]):
|
|||
# return sco_excel.send_excel_file(data, "ImportPhotos" + scu.XLSX_SUFFIX)
|
||||
|
||||
|
||||
def photos_import_files_form(group_ids=[]):
|
||||
def photos_import_files_form(group_ids=()):
|
||||
"""Formulaire pour importation photos"""
|
||||
if not group_ids:
|
||||
raise ScoValueError("paramètre manquant !")
|
||||
groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)
|
||||
back_url = "groups_view?%s&curtab=tab-photos" % groups_infos.groups_query_args
|
||||
back_url = f"groups_view?{groups_infos.groups_query_args}&curtab=tab-photos"
|
||||
|
||||
H = [
|
||||
html_sco_header.sco_header(page_title="Import des photos des étudiants"),
|
||||
"""<h2 class="formsemestre">Téléchargement des photos des étudiants</h2>
|
||||
<p><b>Vous pouvez aussi charger les photos individuellement via la fiche de chaque étudiant (menu "Etudiant" / "Changer la photo").</b></p>
|
||||
<p class="help">Cette page permet de charger en une seule fois les photos de plusieurs étudiants.<br/>
|
||||
Il faut d'abord remplir une feuille excel donnant les noms
|
||||
f"""<h2 class="formsemestre">Téléchargement des photos des étudiants</h2>
|
||||
<p><b>Vous pouvez aussi charger les photos individuellement via la fiche
|
||||
de chaque étudiant (menu "Etudiant" / "Changer la photo").</b>
|
||||
</p>
|
||||
<p class="help">Cette page permet de charger en une seule fois les photos
|
||||
de plusieurs étudiants.<br/>
|
||||
Il faut d'abord remplir une feuille excel donnant les noms
|
||||
des fichiers images (une image par étudiant).
|
||||
</p>
|
||||
<p class="help">Ensuite, réunir vos images dans un fichier zip, puis télécharger
|
||||
<p class="help">Ensuite, réunir vos images dans un fichier zip, puis télécharger
|
||||
simultanément le fichier excel et le fichier zip.
|
||||
</p>
|
||||
<ol>
|
||||
<li><a class="stdlink" href="photos_generate_excel_sample?%s">
|
||||
<li><a class="stdlink" href="photos_generate_excel_sample?{groups_infos.groups_query_args}">
|
||||
Obtenir la feuille excel à remplir</a>
|
||||
</li>
|
||||
<li style="padding-top: 2em;">
|
||||
"""
|
||||
% groups_infos.groups_query_args,
|
||||
""",
|
||||
]
|
||||
F = html_sco_header.sco_footer()
|
||||
vals = scu.get_request_args()
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""Génération d'un trombinoscope en doc
|
||||
"""
|
||||
|
||||
import docx
|
||||
from docx.shared import Mm
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from docx.enum.table import WD_ALIGN_VERTICAL
|
||||
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_photos
|
||||
import app.scodoc.sco_utils as scu
|
||||
import sco_version
|
||||
|
||||
|
||||
def trombino_doc(groups_infos):
|
||||
"Send photos as docx document"
|
||||
filename = f"trombino_{groups_infos.groups_filename}.docx"
|
||||
sem = groups_infos.formsemestre # suppose 1 seul semestre
|
||||
PHOTO_WIDTH = Mm(25)
|
||||
N_PER_ROW = 5 # XXX should be in ScoDoc preferences
|
||||
|
||||
document = docx.Document()
|
||||
document.add_heading(
|
||||
f"Trombinoscope {sem['titreannee']} {groups_infos.groups_titles}", 1
|
||||
)
|
||||
section = document.sections[0]
|
||||
footer = section.footer
|
||||
footer.paragraphs[
|
||||
0
|
||||
].text = f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}"
|
||||
|
||||
nb_images = len(groups_infos.members)
|
||||
table = document.add_table(rows=2 * (nb_images // N_PER_ROW + 1), cols=N_PER_ROW)
|
||||
table.allow_autofit = False
|
||||
|
||||
for i, t in enumerate(groups_infos.members):
|
||||
li = i // N_PER_ROW
|
||||
co = i % N_PER_ROW
|
||||
img_path = (
|
||||
sco_photos.photo_pathname(t["photo_filename"], size="small")
|
||||
or sco_photos.UNKNOWN_IMAGE_PATH
|
||||
)
|
||||
cell = table.rows[2 * li].cells[co]
|
||||
cell.vertical_alignment = WD_ALIGN_VERTICAL.TOP
|
||||
cell_p, cell_f, cell_r = _paragraph_format_run(cell)
|
||||
cell_r.add_picture(img_path, width=PHOTO_WIDTH)
|
||||
|
||||
# le nom de l'étudiant: cellules de lignes impaires
|
||||
cell = table.rows[2 * li + 1].cells[co]
|
||||
cell.vertical_alignment = WD_ALIGN_VERTICAL.TOP
|
||||
cell_p, cell_f, cell_r = _paragraph_format_run(cell)
|
||||
cell_r.add_text(sco_etud.format_nomprenom(t))
|
||||
cell_f.space_after = Mm(8)
|
||||
|
||||
return scu.send_docx(document, filename)
|
||||
|
||||
|
||||
def _paragraph_format_run(cell):
|
||||
"parag. dans cellule tableau"
|
||||
# inspired by https://stackoverflow.com/questions/64218305/problem-with-python-docx-putting-pictures-in-a-table
|
||||
paragraph = cell.paragraphs[0]
|
||||
fmt = paragraph.paragraph_format
|
||||
run = paragraph.add_run()
|
||||
|
||||
fmt.space_before = Mm(0)
|
||||
fmt.space_after = Mm(0)
|
||||
fmt.line_spacing = 1.0
|
||||
fmt.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
|
||||
return paragraph, fmt, run
|
|
@ -227,7 +227,7 @@ def _user_list(user_name):
|
|||
|
||||
|
||||
@cache.memoize(timeout=50) # seconds
|
||||
def user_info(user_name_or_id=None, user=None):
|
||||
def user_info(user_name_or_id=None, user: User = None):
|
||||
"""Dict avec infos sur l'utilisateur (qui peut ne pas etre dans notre base).
|
||||
Si user_name est specifie (string ou id), interroge la BD. Sinon, user doit etre une instance
|
||||
de User.
|
||||
|
|
|
@ -33,6 +33,7 @@ import bisect
|
|||
import copy
|
||||
import datetime
|
||||
from enum import IntEnum
|
||||
import io
|
||||
import json
|
||||
from hashlib import md5
|
||||
import numbers
|
||||
|
@ -49,6 +50,7 @@ from PIL import Image as PILImage
|
|||
import pydot
|
||||
import requests
|
||||
|
||||
import flask
|
||||
from flask import g, request
|
||||
from flask import flash, url_for, make_response, jsonify
|
||||
|
||||
|
@ -176,6 +178,7 @@ MONTH_NAMES = (
|
|||
"novembre",
|
||||
"décembre",
|
||||
)
|
||||
DAY_NAMES = ("lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche")
|
||||
|
||||
|
||||
def fmt_note(val, note_max=None, keep_numeric=False):
|
||||
|
@ -191,7 +194,7 @@ def fmt_note(val, note_max=None, keep_numeric=False):
|
|||
if isinstance(val, float) or isinstance(val, int):
|
||||
if np.isnan(val):
|
||||
return "~"
|
||||
if note_max != None and note_max > 0:
|
||||
if (note_max is not None) and note_max > 0:
|
||||
val = val * 20.0 / note_max
|
||||
if keep_numeric:
|
||||
return val
|
||||
|
@ -379,6 +382,10 @@ CSV_FIELDSEP = ";"
|
|||
CSV_LINESEP = "\n"
|
||||
CSV_MIMETYPE = "text/comma-separated-values"
|
||||
CSV_SUFFIX = ".csv"
|
||||
DOCX_MIMETYPE = (
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||
)
|
||||
DOCX_SUFFIX = ".docx"
|
||||
JSON_MIMETYPE = "application/json"
|
||||
JSON_SUFFIX = ".json"
|
||||
PDF_MIMETYPE = "application/pdf"
|
||||
|
@ -398,6 +405,7 @@ def get_mime_suffix(format_code: str) -> tuple[str, str]:
|
|||
"""
|
||||
d = {
|
||||
"csv": (CSV_MIMETYPE, CSV_SUFFIX),
|
||||
"docx": (DOCX_MIMETYPE, DOCX_SUFFIX),
|
||||
"xls": (XLSX_MIMETYPE, XLSX_SUFFIX),
|
||||
"xlsx": (XLSX_MIMETYPE, XLSX_SUFFIX),
|
||||
"pdf": (PDF_MIMETYPE, PDF_SUFFIX),
|
||||
|
@ -720,15 +728,13 @@ def sendResult(
|
|||
|
||||
def send_file(data, filename="", suffix="", mime=None, attached=None):
|
||||
"""Build Flask Response for file download of given type
|
||||
By default (attached is None), json and xml are inlined and otrher types are attached.
|
||||
By default (attached is None), json and xml are inlined and other types are attached.
|
||||
"""
|
||||
if attached is None:
|
||||
if mime == XML_MIMETYPE or mime == JSON_MIMETYPE:
|
||||
attached = False
|
||||
else:
|
||||
attached = True
|
||||
# if attached and not filename:
|
||||
# raise ValueError("send_file: missing attachement filename")
|
||||
if filename:
|
||||
if suffix:
|
||||
filename += suffix
|
||||
|
@ -740,6 +746,18 @@ def send_file(data, filename="", suffix="", mime=None, attached=None):
|
|||
return response
|
||||
|
||||
|
||||
def send_docx(document, filename):
|
||||
"Send a python-docx document"
|
||||
buffer = io.BytesIO() # in-memory document, no disk file
|
||||
document.save(buffer)
|
||||
buffer.seek(0)
|
||||
return flask.send_file(
|
||||
buffer,
|
||||
download_name=sanitize_filename(filename),
|
||||
mimetype=DOCX_MIMETYPE,
|
||||
)
|
||||
|
||||
|
||||
def get_request_args():
|
||||
"""returns a dict with request (POST or GET) arguments
|
||||
converted to suit legacy Zope style (scodoc7) functions.
|
||||
|
@ -853,6 +871,20 @@ def annee_scolaire_debut(year, month):
|
|||
return int(year) - 1
|
||||
|
||||
|
||||
def date_debut_anne_scolaire(annee_scolaire: int) -> datetime:
|
||||
"""La date de début de l'année scolaire
|
||||
= 1er aout
|
||||
"""
|
||||
return datetime.datetime(year=annee_scolaire, month=8, day=1)
|
||||
|
||||
|
||||
def date_fin_anne_scolaire(annee_scolaire: int) -> datetime:
|
||||
"""La date de fin de l'année scolaire
|
||||
= 31 juillet de l'année suivante
|
||||
"""
|
||||
return datetime.datetime(year=annee_scolaire + 1, month=7, day=31)
|
||||
|
||||
|
||||
def sem_decale_str(sem):
|
||||
"""'D' si semestre decalé, ou ''"""
|
||||
# considère "décalé" les semestre impairs commençant entre janvier et juin
|
||||
|
@ -931,6 +963,10 @@ def icontag(name, file_format="png", no_size=False, **attrs):
|
|||
ICON_PDF = icontag("pdficon16x20_img", title="Version PDF")
|
||||
ICON_XLS = icontag("xlsicon_img", title="Version tableur")
|
||||
|
||||
# HTML emojis
|
||||
EMO_WARNING = "⚠️" # warning /!\
|
||||
EMO_RED_TRIANGLE_DOWN = "🔻" # red triangle pointed down
|
||||
|
||||
|
||||
def sort_dates(L, reverse=False):
|
||||
"""Return sorted list of dates, allowing None items (they are put at the beginning)"""
|
||||
|
@ -1053,6 +1089,36 @@ def objects_renumber(db, obj_list) -> None:
|
|||
db.session.commit()
|
||||
|
||||
|
||||
def gen_cell(key: str, row: dict, elt="td", with_col_class=False):
|
||||
"html table cell"
|
||||
klass = row.get(f"_{key}_class", "")
|
||||
if with_col_class:
|
||||
klass = key + " " + klass
|
||||
attrs = f'class="{klass}"' if klass else ""
|
||||
order = row.get(f"_{key}_order")
|
||||
if order:
|
||||
attrs += f' data-order="{order}"'
|
||||
content = row.get(key, "")
|
||||
target = row.get(f"_{key}_target")
|
||||
target_attrs = row.get(f"_{key}_target_attrs", "")
|
||||
if target or target_attrs: # avec lien
|
||||
href = f'href="{target}"' if target else ""
|
||||
content = f"<a {href} {target_attrs}>{content}</a>"
|
||||
return f"<{elt} {attrs}>{content}</{elt}>"
|
||||
|
||||
|
||||
def gen_row(
|
||||
keys: list[str], row, elt="td", selected_etudid=None, with_col_classes=False
|
||||
):
|
||||
"html table row"
|
||||
klass = row.get("_tr_class")
|
||||
tr_class = f'class="{klass}"' if klass else ""
|
||||
tr_id = (
|
||||
f"""id="row_selected" """ if (row.get("etudid", "") == selected_etudid) else ""
|
||||
)
|
||||
return f"""<tr {tr_id} {tr_class}>{"".join([gen_cell(key, row, elt, with_col_class=with_col_classes) for key in keys if not key.startswith('_')])}</tr>"""
|
||||
|
||||
|
||||
# Pour accès depuis les templates jinja
|
||||
def is_entreprises_enabled():
|
||||
from app.models import ScoDocSiteConfig
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -52,6 +52,9 @@ div.title_STANDARD, .champs_STANDARD {
|
|||
div.title_MALUS {
|
||||
background-color: #ff4700;
|
||||
}
|
||||
.sums {
|
||||
background: #ddd;
|
||||
}
|
||||
/***************************/
|
||||
/* Statut des cellules */
|
||||
/***************************/
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue