forked from ScoDoc/ScoDoc
Compare commits
24 Commits
master
...
bac_a_sabl
Author | SHA1 | Date | |
---|---|---|---|
|
580293207d | ||
|
0c6a21425a | ||
|
ed05c1f7fe | ||
|
793dfc4353 | ||
|
0c215565e8 | ||
|
7bf00f1ad9 | ||
|
48453aab86 | ||
|
e9d5e14f16 | ||
|
395dca1f32 | ||
|
b7b91fa415 | ||
|
e00fc8dd09 | ||
|
f26ecb1f8a | ||
|
f3b540b4c1 | ||
|
5a997dddf4 | ||
|
dae7486d3f | ||
|
c7300ccf0d | ||
|
71aa49b1b1 | ||
|
70a52e7ce1 | ||
|
5a9d65788f | ||
|
650deff2c6 | ||
|
d57a3ba1db | ||
|
68a35864d1 | ||
|
33855cd38d | ||
|
4691ed8f36 |
4
app/__init__.py
Normal file → Executable file
4
app/__init__.py
Normal file → Executable file
@ -322,6 +322,7 @@ def create_app(config_class=DevConfig):
|
|||||||
from app.views import notes_bp
|
from app.views import notes_bp
|
||||||
from app.views import users_bp
|
from app.views import users_bp
|
||||||
from app.views import absences_bp
|
from app.views import absences_bp
|
||||||
|
from app.views import assiduites_bp
|
||||||
from app.api import api_bp
|
from app.api import api_bp
|
||||||
from app.api import api_web_bp
|
from app.api import api_web_bp
|
||||||
|
|
||||||
@ -340,6 +341,9 @@ def create_app(config_class=DevConfig):
|
|||||||
app.register_blueprint(
|
app.register_blueprint(
|
||||||
absences_bp, url_prefix="/ScoDoc/<scodoc_dept>/Scolarite/Absences"
|
absences_bp, url_prefix="/ScoDoc/<scodoc_dept>/Scolarite/Absences"
|
||||||
)
|
)
|
||||||
|
app.register_blueprint(
|
||||||
|
assiduites_bp, url_prefix="/ScoDoc/<scodoc_dept>/Scolarite/Assiduites"
|
||||||
|
)
|
||||||
app.register_blueprint(api_bp, url_prefix="/ScoDoc/api")
|
app.register_blueprint(api_bp, url_prefix="/ScoDoc/api")
|
||||||
app.register_blueprint(api_web_bp, url_prefix="/ScoDoc/<scodoc_dept>/api")
|
app.register_blueprint(api_web_bp, url_prefix="/ScoDoc/<scodoc_dept>/api")
|
||||||
|
|
||||||
|
@ -2,7 +2,8 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
from flask import request
|
from flask import request, g, jsonify
|
||||||
|
from app import db
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
from app.scodoc.sco_exceptions import ScoException
|
from app.scodoc.sco_exceptions import ScoException
|
||||||
|
|
||||||
@ -34,9 +35,26 @@ def requested_format(default_format="json", allowed_formats=None):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_model_api_object(model_cls: db.Model, model_id: int, join_cls: db.Model = None):
|
||||||
|
"""
|
||||||
|
Retourne une réponse contenant la représentation api de l'objet "Model[model_id]"
|
||||||
|
|
||||||
|
Filtrage du département en fonction d'une classe de jointure (eg: Identite, Formsemstre) -> join_cls
|
||||||
|
|
||||||
|
exemple d'utilisation : fonction "justificatif()" -> app/api/justificatifs.py
|
||||||
|
"""
|
||||||
|
query = model_cls.query.filter_by(id=model_id)
|
||||||
|
if g.scodoc_dept and join_cls is not None:
|
||||||
|
query = query.join(join_cls).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
unique: model_cls = query.first_or_404()
|
||||||
|
|
||||||
|
return jsonify(unique.to_dict(format_api=True))
|
||||||
|
|
||||||
|
|
||||||
from app.api import tokens
|
from app.api import tokens
|
||||||
from app.api import (
|
from app.api import (
|
||||||
absences,
|
absences,
|
||||||
|
assiduites,
|
||||||
billets_absences,
|
billets_absences,
|
||||||
departements,
|
departements,
|
||||||
etudiants,
|
etudiants,
|
||||||
@ -44,6 +62,7 @@ from app.api import (
|
|||||||
formations,
|
formations,
|
||||||
formsemestres,
|
formsemestres,
|
||||||
jury,
|
jury,
|
||||||
|
justificatifs,
|
||||||
logos,
|
logos,
|
||||||
partitions,
|
partitions,
|
||||||
semset,
|
semset,
|
||||||
|
@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
from flask_json import as_json
|
from flask_json import as_json
|
||||||
|
|
||||||
from app import db
|
|
||||||
from app.api import api_bp as bp, API_CLIENT_ERROR
|
from app.api import api_bp as bp, API_CLIENT_ERROR
|
||||||
from app.scodoc.sco_utils import json_error
|
from app.scodoc.sco_utils import json_error
|
||||||
from app.decorators import scodoc, permission_required
|
from app.decorators import scodoc, permission_required
|
||||||
@ -52,7 +51,7 @@ def absences(etudid: int = None):
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
"""
|
"""
|
||||||
etud = db.session.get(Identite, etudid)
|
etud = Identite.query.get(etudid)
|
||||||
if etud is None:
|
if etud is None:
|
||||||
return json_error(404, message="etudiant inexistant")
|
return json_error(404, message="etudiant inexistant")
|
||||||
# Absences de l'étudiant
|
# Absences de l'étudiant
|
||||||
@ -97,7 +96,7 @@ def absences_just(etudid: int = None):
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
"""
|
"""
|
||||||
etud = db.session.get(Identite, etudid)
|
etud = Identite.query.get(etudid)
|
||||||
if etud is None:
|
if etud is None:
|
||||||
return json_error(404, message="etudiant inexistant")
|
return json_error(404, message="etudiant inexistant")
|
||||||
|
|
||||||
|
868
app/api/assiduites.py
Normal file
868
app/api/assiduites.py
Normal file
@ -0,0 +1,868 @@
|
|||||||
|
##############################################################################
|
||||||
|
# ScoDoc
|
||||||
|
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||||
|
# See LICENSE
|
||||||
|
##############################################################################
|
||||||
|
"""ScoDoc 9 API : Assiduités
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
from flask import g, jsonify, request
|
||||||
|
from flask_login import login_required, current_user
|
||||||
|
|
||||||
|
import app.scodoc.sco_assiduites as scass
|
||||||
|
import app.scodoc.sco_utils as scu
|
||||||
|
from app import db
|
||||||
|
from app.api import api_bp as bp
|
||||||
|
from app.api import api_web_bp
|
||||||
|
from app.api import get_model_api_object
|
||||||
|
from app.decorators import permission_required, scodoc
|
||||||
|
from app.models import Assiduite, FormSemestre, Identite, ModuleImpl
|
||||||
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
|
from app.scodoc.sco_permissions import Permission
|
||||||
|
from app.scodoc.sco_utils import json_error
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/assiduite/<int:assiduite_id>")
|
||||||
|
@api_web_bp.route("/assiduite/<int:assiduite_id>")
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def assiduite(assiduite_id: int = None):
|
||||||
|
"""Retourne un objet assiduité à partir de son id
|
||||||
|
|
||||||
|
Exemple de résultat:
|
||||||
|
{
|
||||||
|
"assiduite_id": 1,
|
||||||
|
"etudid": 2,
|
||||||
|
"moduleimpl_id": 3,
|
||||||
|
"date_debut": "2022-10-31T08:00+01:00",
|
||||||
|
"date_fin": "2022-10-31T10:00+01:00",
|
||||||
|
"etat": "retard",
|
||||||
|
"desc": "une description",
|
||||||
|
"user_id: 1 or null,
|
||||||
|
"est_just": False or True,
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
return get_model_api_object(Assiduite, assiduite_id, Identite)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/assiduites/<int:etudid>/count", defaults={"with_query": False})
|
||||||
|
@bp.route("/assiduites/<int:etudid>/count/query", defaults={"with_query": True})
|
||||||
|
@api_web_bp.route("/assiduites/<int:etudid>/count", defaults={"with_query": False})
|
||||||
|
@api_web_bp.route("/assiduites/<int:etudid>/count/query", defaults={"with_query": True})
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def count_assiduites(etudid: int = None, with_query: bool = False):
|
||||||
|
"""
|
||||||
|
Retourne le nombre d'assiduités d'un étudiant
|
||||||
|
chemin : /assiduites/<int:etudid>/count
|
||||||
|
|
||||||
|
Un filtrage peut être donné avec une query
|
||||||
|
chemin : /assiduites/<int:etudid>/count/query?
|
||||||
|
|
||||||
|
Les différents filtres :
|
||||||
|
Type (type de comptage -> journee, demi, heure, nombre d'assiduite):
|
||||||
|
query?type=(journee, demi, heure) -> une seule valeur parmis les trois
|
||||||
|
ex: .../query?type=heure
|
||||||
|
Comportement par défaut : compte le nombre d'assiduité enregistrée
|
||||||
|
|
||||||
|
Etat (etat de l'étudiant -> absent, present ou retard):
|
||||||
|
query?etat=[- liste des états séparé par une virgule -]
|
||||||
|
ex: .../query?etat=present,retard
|
||||||
|
Date debut
|
||||||
|
(date de début de l'assiduité, sont affichés les assiduités
|
||||||
|
dont la date de début est supérieur ou égale à la valeur donnée):
|
||||||
|
query?date_debut=[- date au format iso -]
|
||||||
|
ex: query?date_debut=2022-11-03T08:00+01:00
|
||||||
|
Date fin
|
||||||
|
(date de fin de l'assiduité, sont affichés les assiduités
|
||||||
|
dont la date de fin est inférieure ou égale à la valeur donnée):
|
||||||
|
query?date_fin=[- date au format iso -]
|
||||||
|
ex: query?date_fin=2022-11-03T10:00+01:00
|
||||||
|
Moduleimpl_id (l'id du module concerné par l'assiduité):
|
||||||
|
query?moduleimpl_id=[- int ou vide -]
|
||||||
|
ex: query?moduleimpl_id=1234
|
||||||
|
query?moduleimpl_od=
|
||||||
|
Formsemstre_id (l'id du formsemestre concerné par l'assiduité)
|
||||||
|
query?formsemestre_id=[int]
|
||||||
|
ex query?formsemestre_id=3
|
||||||
|
user_id (l'id de l'auteur de l'assiduité)
|
||||||
|
query?user_id=[int]
|
||||||
|
ex query?user_id=3
|
||||||
|
est_just (si l'assiduité est justifié (fait aussi filtre par abs/retard))
|
||||||
|
query?est_just=[bool]
|
||||||
|
query?est_just=f
|
||||||
|
query?est_just=t
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
query = Identite.query.filter_by(id=etudid)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
|
||||||
|
etud: Identite = query.first_or_404(etudid)
|
||||||
|
filtered: dict[str, object] = {}
|
||||||
|
metric: str = "all"
|
||||||
|
|
||||||
|
if with_query:
|
||||||
|
metric, filtered = _count_manager(request)
|
||||||
|
|
||||||
|
return jsonify(
|
||||||
|
scass.get_assiduites_stats(
|
||||||
|
assiduites=etud.assiduites, metric=metric, filtered=filtered
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/assiduites/<int:etudid>", defaults={"with_query": False})
|
||||||
|
@bp.route("/assiduites/<int:etudid>/query", defaults={"with_query": True})
|
||||||
|
@api_web_bp.route("/assiduites/<int:etudid>", defaults={"with_query": False})
|
||||||
|
@api_web_bp.route("/assiduites/<int:etudid>/query", defaults={"with_query": True})
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def assiduites(etudid: int = None, with_query: bool = False):
|
||||||
|
"""
|
||||||
|
Retourne toutes les assiduités d'un étudiant
|
||||||
|
chemin : /assiduites/<int:etudid>
|
||||||
|
|
||||||
|
Un filtrage peut être donné avec une query
|
||||||
|
chemin : /assiduites/<int:etudid>/query?
|
||||||
|
|
||||||
|
Les différents filtres :
|
||||||
|
Etat (etat de l'étudiant -> absent, present ou retard):
|
||||||
|
query?etat=[- liste des états séparé par une virgule -]
|
||||||
|
ex: .../query?etat=present,retard
|
||||||
|
Date debut
|
||||||
|
(date de début de l'assiduité, sont affichés les assiduités
|
||||||
|
dont la date de début est supérieur ou égale à la valeur donnée):
|
||||||
|
query?date_debut=[- date au format iso -]
|
||||||
|
ex: query?date_debut=2022-11-03T08:00+01:00
|
||||||
|
Date fin
|
||||||
|
(date de fin de l'assiduité, sont affichés les assiduités
|
||||||
|
dont la date de fin est inférieure ou égale à la valeur donnée):
|
||||||
|
query?date_fin=[- date au format iso -]
|
||||||
|
ex: query?date_fin=2022-11-03T10:00+01:00
|
||||||
|
Moduleimpl_id (l'id du module concerné par l'assiduité):
|
||||||
|
query?moduleimpl_id=[- int ou vide -]
|
||||||
|
ex: query?moduleimpl_id=1234
|
||||||
|
query?moduleimpl_od=
|
||||||
|
Formsemstre_id (l'id du formsemestre concerné par l'assiduité)
|
||||||
|
query?formsemstre_id=[int]
|
||||||
|
ex query?formsemestre_id=3
|
||||||
|
user_id (l'id de l'auteur de l'assiduité)
|
||||||
|
query?user_id=[int]
|
||||||
|
ex query?user_id=3
|
||||||
|
est_just (si l'assiduité est justifié (fait aussi filtre par abs/retard))
|
||||||
|
query?est_just=[bool]
|
||||||
|
query?est_just=f
|
||||||
|
query?est_just=t
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
query = Identite.query.filter_by(id=etudid)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
|
||||||
|
etud: Identite = query.first_or_404(etudid)
|
||||||
|
assiduites_query = etud.assiduites
|
||||||
|
|
||||||
|
if with_query:
|
||||||
|
assiduites_query = _filter_manager(request, assiduites_query)
|
||||||
|
|
||||||
|
data_set: list[dict] = []
|
||||||
|
for ass in assiduites_query.all():
|
||||||
|
data = ass.to_dict(format_api=True)
|
||||||
|
data_set.append(data)
|
||||||
|
|
||||||
|
return jsonify(data_set)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/assiduites/group/query", defaults={"with_query": True})
|
||||||
|
@api_web_bp.route("/assiduites/group/query", defaults={"with_query": True})
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def assiduites_group(with_query: bool = False):
|
||||||
|
"""
|
||||||
|
Retourne toutes les assiduités d'un groupe d'étudiants
|
||||||
|
chemin : /assiduites/group/query?etudids=1,2,3
|
||||||
|
|
||||||
|
Un filtrage peut être donné avec une query
|
||||||
|
chemin : /assiduites/group/query?etudids=1,2,3
|
||||||
|
|
||||||
|
Les différents filtres :
|
||||||
|
Etat (etat de l'étudiant -> absent, present ou retard):
|
||||||
|
query?etat=[- liste des états séparé par une virgule -]
|
||||||
|
ex: .../query?etat=present,retard
|
||||||
|
Date debut
|
||||||
|
(date de début de l'assiduité, sont affichés les assiduités
|
||||||
|
dont la date de début est supérieur ou égale à la valeur donnée):
|
||||||
|
query?date_debut=[- date au format iso -]
|
||||||
|
ex: query?date_debut=2022-11-03T08:00+01:00
|
||||||
|
Date fin
|
||||||
|
(date de fin de l'assiduité, sont affichés les assiduités
|
||||||
|
dont la date de fin est inférieure ou égale à la valeur donnée):
|
||||||
|
query?date_fin=[- date au format iso -]
|
||||||
|
ex: query?date_fin=2022-11-03T10:00+01:00
|
||||||
|
Moduleimpl_id (l'id du module concerné par l'assiduité):
|
||||||
|
query?moduleimpl_id=[- int ou vide -]
|
||||||
|
ex: query?moduleimpl_id=1234
|
||||||
|
query?moduleimpl_od=
|
||||||
|
Formsemstre_id (l'id du formsemestre concerné par l'assiduité)
|
||||||
|
query?formsemstre_id=[int]
|
||||||
|
ex query?formsemestre_id=3
|
||||||
|
user_id (l'id de l'auteur de l'assiduité)
|
||||||
|
query?user_id=[int]
|
||||||
|
ex query?user_id=3
|
||||||
|
est_just (si l'assiduité est justifié (fait aussi filtre par abs/retard))
|
||||||
|
query?est_just=[bool]
|
||||||
|
query?est_just=f
|
||||||
|
query?est_just=t
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
etuds = request.args.get("etudids", "")
|
||||||
|
etuds = etuds.split(",")
|
||||||
|
try:
|
||||||
|
etuds = [int(etu) for etu in etuds]
|
||||||
|
except ValueError:
|
||||||
|
return json_error(404, "Le champs etudids n'est pas correctement formé")
|
||||||
|
|
||||||
|
query = Identite.query.filter(Identite.id.in_(etuds))
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
|
||||||
|
if len(etuds) != query.count() or len(etuds) == 0:
|
||||||
|
return json_error(
|
||||||
|
404,
|
||||||
|
"Tous les étudiants ne sont pas dans le même département et/ou n'existe pas.",
|
||||||
|
)
|
||||||
|
assiduites_query = Assiduite.query.filter(Assiduite.etudid.in_(etuds))
|
||||||
|
|
||||||
|
if with_query:
|
||||||
|
assiduites_query = _filter_manager(request, assiduites_query)
|
||||||
|
|
||||||
|
data_set: dict[list[dict]] = {key: [] for key in etuds}
|
||||||
|
for ass in assiduites_query.all():
|
||||||
|
data = ass.to_dict(format_api=True)
|
||||||
|
data_set.get(data["etudid"]).append(data)
|
||||||
|
|
||||||
|
return jsonify(data_set)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route(
|
||||||
|
"/assiduites/formsemestre/<int:formsemestre_id>", defaults={"with_query": False}
|
||||||
|
)
|
||||||
|
@api_web_bp.route(
|
||||||
|
"/assiduites/formsemestre/<int:formsemestre_id>", defaults={"with_query": False}
|
||||||
|
)
|
||||||
|
@bp.route(
|
||||||
|
"/assiduites/formsemestre/<int:formsemestre_id>/query",
|
||||||
|
defaults={"with_query": True},
|
||||||
|
)
|
||||||
|
@api_web_bp.route(
|
||||||
|
"/assiduites/formsemestre/<int:formsemestre_id>/query",
|
||||||
|
defaults={"with_query": True},
|
||||||
|
)
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
|
||||||
|
"""Retourne toutes les assiduités du formsemestre"""
|
||||||
|
formsemestre: FormSemestre = None
|
||||||
|
formsemestre_id = int(formsemestre_id)
|
||||||
|
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
|
||||||
|
|
||||||
|
if formsemestre is None:
|
||||||
|
return json_error(404, "le paramètre 'formsemestre_id' n'existe pas")
|
||||||
|
|
||||||
|
assiduites_query = scass.filter_by_formsemestre(Assiduite.query, formsemestre)
|
||||||
|
|
||||||
|
if with_query:
|
||||||
|
assiduites_query = _filter_manager(request, assiduites_query)
|
||||||
|
|
||||||
|
data_set: list[dict] = []
|
||||||
|
for ass in assiduites_query.all():
|
||||||
|
data = ass.to_dict(format_api=True)
|
||||||
|
data_set.append(data)
|
||||||
|
|
||||||
|
return jsonify(data_set)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route(
|
||||||
|
"/assiduites/formsemestre/<int:formsemestre_id>/count",
|
||||||
|
defaults={"with_query": False},
|
||||||
|
)
|
||||||
|
@api_web_bp.route(
|
||||||
|
"/assiduites/formsemestre/<int:formsemestre_id>/count",
|
||||||
|
defaults={"with_query": False},
|
||||||
|
)
|
||||||
|
@bp.route(
|
||||||
|
"/assiduites/formsemestre/<int:formsemestre_id>/count/query",
|
||||||
|
defaults={"with_query": True},
|
||||||
|
)
|
||||||
|
@api_web_bp.route(
|
||||||
|
"/assiduites/formsemestre/<int:formsemestre_id>/count/query",
|
||||||
|
defaults={"with_query": True},
|
||||||
|
)
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def count_assiduites_formsemestre(
|
||||||
|
formsemestre_id: int = None, with_query: bool = False
|
||||||
|
):
|
||||||
|
"""Comptage des assiduités du formsemestre"""
|
||||||
|
formsemestre: FormSemestre = None
|
||||||
|
formsemestre_id = int(formsemestre_id)
|
||||||
|
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
|
||||||
|
|
||||||
|
if formsemestre is None:
|
||||||
|
return json_error(404, "le paramètre 'formsemestre_id' n'existe pas")
|
||||||
|
|
||||||
|
etuds = formsemestre.etuds.all()
|
||||||
|
etuds_id = [etud.id for etud in etuds]
|
||||||
|
|
||||||
|
assiduites_query = Assiduite.query.filter(Assiduite.etudid.in_(etuds_id))
|
||||||
|
assiduites_query = scass.filter_by_formsemestre(assiduites_query, formsemestre)
|
||||||
|
metric: str = "all"
|
||||||
|
filtered: dict = {}
|
||||||
|
if with_query:
|
||||||
|
metric, filtered = _count_manager(request)
|
||||||
|
|
||||||
|
return jsonify(scass.get_assiduites_stats(assiduites_query, metric, filtered))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/assiduite/<int:etudid>/create", methods=["POST"])
|
||||||
|
@api_web_bp.route("/assiduite/<int:etudid>/create", methods=["POST"])
|
||||||
|
@scodoc
|
||||||
|
@login_required
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
# @permission_required(Permission.ScoAssiduiteChange)
|
||||||
|
def assiduite_create(etudid: int = None):
|
||||||
|
"""
|
||||||
|
Création d'une assiduité pour l'étudiant (etudid)
|
||||||
|
La requête doit avoir un content type "application/json":
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"date_debut": str,
|
||||||
|
"date_fin": str,
|
||||||
|
"etat": str,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date_debut": str,
|
||||||
|
"date_fin": str,
|
||||||
|
"etat": str,
|
||||||
|
"moduleimpl_id": int,
|
||||||
|
"desc":str,
|
||||||
|
}
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
"""
|
||||||
|
etud: Identite = Identite.query.filter_by(id=etudid).first_or_404()
|
||||||
|
|
||||||
|
create_list: list[object] = request.get_json(force=True)
|
||||||
|
|
||||||
|
if not isinstance(create_list, list):
|
||||||
|
return json_error(404, "Le contenu envoyé n'est pas une liste")
|
||||||
|
|
||||||
|
errors: dict[int, str] = {}
|
||||||
|
success: dict[int, object] = {}
|
||||||
|
for i, data in enumerate(create_list):
|
||||||
|
code, obj = _create_singular(data, etud)
|
||||||
|
if code == 404:
|
||||||
|
errors[i] = obj
|
||||||
|
else:
|
||||||
|
success[i] = obj
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify({"errors": errors, "success": success})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/assiduites/create", methods=["POST"])
|
||||||
|
@api_web_bp.route("/assiduites/create", methods=["POST"])
|
||||||
|
@scodoc
|
||||||
|
@login_required
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
# @permission_required(Permission.ScoAssiduiteChange)
|
||||||
|
def assiduites_create():
|
||||||
|
"""
|
||||||
|
Création d'une assiduité ou plusieurs assiduites
|
||||||
|
La requête doit avoir un content type "application/json":
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"date_debut": str,
|
||||||
|
"date_fin": str,
|
||||||
|
"etat": str,
|
||||||
|
"etudid":int,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date_debut": str,
|
||||||
|
"date_fin": str,
|
||||||
|
"etat": str,
|
||||||
|
"etudid":int,
|
||||||
|
|
||||||
|
"moduleimpl_id": int,
|
||||||
|
"desc":str,
|
||||||
|
}
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
create_list: list[object] = request.get_json(force=True)
|
||||||
|
|
||||||
|
if not isinstance(create_list, list):
|
||||||
|
return json_error(404, "Le contenu envoyé n'est pas une liste")
|
||||||
|
|
||||||
|
errors: dict[int, str] = {}
|
||||||
|
success: dict[int, object] = {}
|
||||||
|
for i, data in enumerate(create_list):
|
||||||
|
etud: Identite = Identite.query.filter_by(id=data["etudid"]).first()
|
||||||
|
if etud is None:
|
||||||
|
errors[i] = "Cet étudiant n'existe pas."
|
||||||
|
continue
|
||||||
|
|
||||||
|
code, obj = _create_singular(data, etud)
|
||||||
|
if code == 404:
|
||||||
|
errors[i] = obj
|
||||||
|
else:
|
||||||
|
success[i] = obj
|
||||||
|
|
||||||
|
return jsonify({"errors": errors, "success": success})
|
||||||
|
|
||||||
|
|
||||||
|
def _create_singular(
|
||||||
|
data: dict,
|
||||||
|
etud: Identite,
|
||||||
|
) -> tuple[int, object]:
|
||||||
|
errors: list[str] = []
|
||||||
|
|
||||||
|
# -- vérifications de l'objet json --
|
||||||
|
# cas 1 : ETAT
|
||||||
|
etat = data.get("etat", None)
|
||||||
|
if etat is None:
|
||||||
|
errors.append("param 'etat': manquant")
|
||||||
|
elif not scu.EtatAssiduite.contains(etat):
|
||||||
|
errors.append("param 'etat': invalide")
|
||||||
|
|
||||||
|
etat = scu.EtatAssiduite.get(etat)
|
||||||
|
|
||||||
|
# cas 2 : date_debut
|
||||||
|
date_debut = data.get("date_debut", None)
|
||||||
|
if date_debut is None:
|
||||||
|
errors.append("param 'date_debut': manquant")
|
||||||
|
deb = scu.is_iso_formated(date_debut, convert=True)
|
||||||
|
if deb is None:
|
||||||
|
errors.append("param 'date_debut': format invalide")
|
||||||
|
|
||||||
|
# cas 3 : date_fin
|
||||||
|
date_fin = data.get("date_fin", None)
|
||||||
|
if date_fin is None:
|
||||||
|
errors.append("param 'date_fin': manquant")
|
||||||
|
fin = scu.is_iso_formated(date_fin, convert=True)
|
||||||
|
if fin is None:
|
||||||
|
errors.append("param 'date_fin': format invalide")
|
||||||
|
|
||||||
|
# cas 4 : moduleimpl_id
|
||||||
|
|
||||||
|
moduleimpl_id = data.get("moduleimpl_id", False)
|
||||||
|
moduleimpl: ModuleImpl = None
|
||||||
|
|
||||||
|
if moduleimpl_id is not False:
|
||||||
|
moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first()
|
||||||
|
if moduleimpl is None:
|
||||||
|
errors.append("param 'moduleimpl_id': invalide")
|
||||||
|
|
||||||
|
# cas 5 : desc
|
||||||
|
|
||||||
|
desc: str = data.get("desc", None)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
err: str = ", ".join(errors)
|
||||||
|
return (404, err)
|
||||||
|
|
||||||
|
# TOUT EST OK
|
||||||
|
try:
|
||||||
|
nouv_assiduite: Assiduite = Assiduite.create_assiduite(
|
||||||
|
date_debut=deb,
|
||||||
|
date_fin=fin,
|
||||||
|
etat=etat,
|
||||||
|
etud=etud,
|
||||||
|
moduleimpl=moduleimpl,
|
||||||
|
description=desc,
|
||||||
|
user_id=current_user.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(nouv_assiduite)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return (200, {"assiduite_id": nouv_assiduite.id})
|
||||||
|
except ScoValueError as excp:
|
||||||
|
return (
|
||||||
|
404,
|
||||||
|
excp.args[0],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/assiduite/delete", methods=["POST"])
|
||||||
|
@api_web_bp.route("/assiduite/delete", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
# @permission_required(Permission.ScoAssiduiteChange)
|
||||||
|
def assiduite_delete():
|
||||||
|
"""
|
||||||
|
Suppression d'une assiduité à partir de son id
|
||||||
|
|
||||||
|
Forme des données envoyées :
|
||||||
|
|
||||||
|
[
|
||||||
|
<assiduite_id:int>,
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
assiduites_list: list[int] = request.get_json(force=True)
|
||||||
|
if not isinstance(assiduites_list, list):
|
||||||
|
return json_error(404, "Le contenu envoyé n'est pas une liste")
|
||||||
|
|
||||||
|
output = {"errors": {}, "success": {}}
|
||||||
|
|
||||||
|
for i, ass in enumerate(assiduites_list):
|
||||||
|
code, msg = _delete_singular(ass, db)
|
||||||
|
if code == 404:
|
||||||
|
output["errors"][f"{i}"] = msg
|
||||||
|
else:
|
||||||
|
output["success"][f"{i}"] = {"OK": True}
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify(output)
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_singular(assiduite_id: int, database):
|
||||||
|
assiduite_unique: Assiduite = Assiduite.query.filter_by(id=assiduite_id).first()
|
||||||
|
if assiduite_unique is None:
|
||||||
|
return (404, "Assiduite non existante")
|
||||||
|
database.session.delete(assiduite_unique)
|
||||||
|
return (200, "OK")
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/assiduite/<int:assiduite_id>/edit", methods=["POST"])
|
||||||
|
@api_web_bp.route("/assiduite/<int:assiduite_id>/edit", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
# @permission_required(Permission.ScoAssiduiteChange)
|
||||||
|
def assiduite_edit(assiduite_id: int):
|
||||||
|
"""
|
||||||
|
Edition d'une assiduité à partir de son id
|
||||||
|
La requête doit avoir un content type "application/json":
|
||||||
|
{
|
||||||
|
"etat"?: str,
|
||||||
|
"moduleimpl_id"?: int
|
||||||
|
"desc"?: str
|
||||||
|
"est_just"?: bool
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
assiduite_unique: Assiduite = Assiduite.query.filter_by(
|
||||||
|
id=assiduite_id
|
||||||
|
).first_or_404()
|
||||||
|
errors: list[str] = []
|
||||||
|
data = request.get_json(force=True)
|
||||||
|
|
||||||
|
# Vérifications de data
|
||||||
|
|
||||||
|
# Cas 1 : Etat
|
||||||
|
if data.get("etat") is not None:
|
||||||
|
etat = scu.EtatAssiduite.get(data.get("etat"))
|
||||||
|
if etat is None:
|
||||||
|
errors.append("param 'etat': invalide")
|
||||||
|
else:
|
||||||
|
assiduite_unique.etat = etat
|
||||||
|
|
||||||
|
# Cas 2 : Moduleimpl_id
|
||||||
|
moduleimpl_id = data.get("moduleimpl_id", False)
|
||||||
|
moduleimpl: ModuleImpl = None
|
||||||
|
|
||||||
|
if moduleimpl_id is not False:
|
||||||
|
if moduleimpl_id is not None:
|
||||||
|
moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first()
|
||||||
|
if moduleimpl is None:
|
||||||
|
errors.append("param 'moduleimpl_id': invalide")
|
||||||
|
else:
|
||||||
|
if not moduleimpl.est_inscrit(
|
||||||
|
Identite.query.filter_by(id=assiduite_unique.etudid).first()
|
||||||
|
):
|
||||||
|
errors.append("param 'moduleimpl_id': etud non inscrit")
|
||||||
|
else:
|
||||||
|
assiduite_unique.moduleimpl_id = moduleimpl_id
|
||||||
|
else:
|
||||||
|
assiduite_unique.moduleimpl_id = moduleimpl_id
|
||||||
|
|
||||||
|
# Cas 3 : desc
|
||||||
|
desc = data.get("desc", False)
|
||||||
|
if desc is not False:
|
||||||
|
assiduite_unique.desc = desc
|
||||||
|
|
||||||
|
# Cas 4 : est_just
|
||||||
|
est_just = data.get("est_just")
|
||||||
|
if est_just is not None:
|
||||||
|
if not isinstance(est_just, bool):
|
||||||
|
errors.append("param 'est_just' : booléen non reconnu")
|
||||||
|
else:
|
||||||
|
assiduite_unique.est_just = est_just
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
err: str = ", ".join(errors)
|
||||||
|
return json_error(404, err)
|
||||||
|
|
||||||
|
db.session.add(assiduite_unique)
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({"OK": True})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/assiduites/edit", methods=["POST"])
|
||||||
|
@api_web_bp.route("/assiduites/edit", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
# @permission_required(Permission.ScoAssiduiteChange)
|
||||||
|
def assiduites_edit():
|
||||||
|
"""
|
||||||
|
Edition d'une assiduité à partir de son id
|
||||||
|
La requête doit avoir un content type "application/json":
|
||||||
|
{
|
||||||
|
"etat"?: str,
|
||||||
|
"moduleimpl_id"?: int
|
||||||
|
"desc"?: str
|
||||||
|
"est_just"?: bool
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
edit_list: list[object] = request.get_json(force=True)
|
||||||
|
|
||||||
|
if not isinstance(edit_list, list):
|
||||||
|
return json_error(404, "Le contenu envoyé n'est pas une liste")
|
||||||
|
|
||||||
|
errors: dict[int, str] = {}
|
||||||
|
success: dict[int, object] = {}
|
||||||
|
for i, data in enumerate(edit_list):
|
||||||
|
assi: Identite = Assiduite.query.filter_by(id=data["assiduite_id"]).first()
|
||||||
|
if assi is None:
|
||||||
|
errors[i] = "Cet assiduité n'existe pas."
|
||||||
|
continue
|
||||||
|
|
||||||
|
code, obj = _edit_singular(assi, data)
|
||||||
|
if code == 404:
|
||||||
|
errors[i] = obj
|
||||||
|
else:
|
||||||
|
success[i] = obj
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify({"errors": errors, "success": success})
|
||||||
|
|
||||||
|
|
||||||
|
def _edit_singular(assiduite_unique, data):
|
||||||
|
errors: list[str] = []
|
||||||
|
|
||||||
|
# Vérifications de data
|
||||||
|
|
||||||
|
# Cas 1 : Etat
|
||||||
|
if data.get("etat") is not None:
|
||||||
|
etat = scu.EtatAssiduite.get(data.get("etat"))
|
||||||
|
if etat is None:
|
||||||
|
errors.append("param 'etat': invalide")
|
||||||
|
else:
|
||||||
|
assiduite_unique.etat = etat
|
||||||
|
|
||||||
|
# Cas 2 : Moduleimpl_id
|
||||||
|
moduleimpl_id = data.get("moduleimpl_id", False)
|
||||||
|
moduleimpl: ModuleImpl = None
|
||||||
|
|
||||||
|
if moduleimpl_id is not False:
|
||||||
|
if moduleimpl_id is not None:
|
||||||
|
moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first()
|
||||||
|
if moduleimpl is None:
|
||||||
|
errors.append("param 'moduleimpl_id': invalide")
|
||||||
|
else:
|
||||||
|
if not moduleimpl.est_inscrit(
|
||||||
|
Identite.query.filter_by(id=assiduite_unique.etudid).first()
|
||||||
|
):
|
||||||
|
errors.append("param 'moduleimpl_id': etud non inscrit")
|
||||||
|
else:
|
||||||
|
assiduite_unique.moduleimpl_id = moduleimpl_id
|
||||||
|
else:
|
||||||
|
assiduite_unique.moduleimpl_id = moduleimpl_id
|
||||||
|
|
||||||
|
# Cas 3 : desc
|
||||||
|
desc = data.get("desc", False)
|
||||||
|
if desc is not False:
|
||||||
|
assiduite_unique.desc = desc
|
||||||
|
|
||||||
|
# Cas 4 : est_just
|
||||||
|
est_just = data.get("est_just")
|
||||||
|
if est_just is not None:
|
||||||
|
if not isinstance(est_just, bool):
|
||||||
|
errors.append("param 'est_just' : booléen non reconnu")
|
||||||
|
else:
|
||||||
|
assiduite_unique.est_just = est_just
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
err: str = ", ".join(errors)
|
||||||
|
return (404, err)
|
||||||
|
|
||||||
|
db.session.add(assiduite_unique)
|
||||||
|
|
||||||
|
return (200, "OK")
|
||||||
|
|
||||||
|
|
||||||
|
# -- Utils --
|
||||||
|
|
||||||
|
|
||||||
|
def _count_manager(requested) -> tuple[str, dict]:
|
||||||
|
"""
|
||||||
|
Retourne la/les métriques à utiliser ainsi que le filtre donnés en query de la requête
|
||||||
|
"""
|
||||||
|
filtered: dict = {}
|
||||||
|
# cas 1 : etat assiduite
|
||||||
|
etat = requested.args.get("etat")
|
||||||
|
if etat is not None:
|
||||||
|
filtered["etat"] = etat
|
||||||
|
|
||||||
|
# cas 2 : date de début
|
||||||
|
deb = requested.args.get("date_debut", "").replace(" ", "+")
|
||||||
|
deb: datetime = scu.is_iso_formated(deb, True)
|
||||||
|
if deb is not None:
|
||||||
|
filtered["date_debut"] = deb
|
||||||
|
|
||||||
|
# cas 3 : date de fin
|
||||||
|
fin = requested.args.get("date_fin", "").replace(" ", "+")
|
||||||
|
fin = scu.is_iso_formated(fin, True)
|
||||||
|
|
||||||
|
if fin is not None:
|
||||||
|
filtered["date_fin"] = fin
|
||||||
|
|
||||||
|
# cas 4 : moduleimpl_id
|
||||||
|
module = requested.args.get("moduleimpl_id", False)
|
||||||
|
try:
|
||||||
|
if module is False:
|
||||||
|
raise ValueError
|
||||||
|
if module != "":
|
||||||
|
module = int(module)
|
||||||
|
else:
|
||||||
|
module = None
|
||||||
|
except ValueError:
|
||||||
|
module = False
|
||||||
|
|
||||||
|
if module is not False:
|
||||||
|
filtered["moduleimpl_id"] = module
|
||||||
|
|
||||||
|
# cas 5 : formsemestre_id
|
||||||
|
formsemestre_id = requested.args.get("formsemestre_id")
|
||||||
|
|
||||||
|
if formsemestre_id is not None:
|
||||||
|
formsemestre: FormSemestre = None
|
||||||
|
formsemestre_id = int(formsemestre_id)
|
||||||
|
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
|
||||||
|
filtered["formsemestre"] = formsemestre
|
||||||
|
|
||||||
|
# cas 6 : type
|
||||||
|
metric = requested.args.get("metric", "all")
|
||||||
|
|
||||||
|
# cas 7 : est_just
|
||||||
|
|
||||||
|
est_just: str = requested.args.get("est_just")
|
||||||
|
if est_just is not None:
|
||||||
|
trues: tuple[str] = ("v", "t", "vrai", "true")
|
||||||
|
falses: tuple[str] = ("f", "faux", "false")
|
||||||
|
|
||||||
|
if est_just.lower() in trues:
|
||||||
|
filtered["est_just"] = True
|
||||||
|
elif est_just.lower() in falses:
|
||||||
|
filtered["est_just"] = False
|
||||||
|
|
||||||
|
# cas 8 : user_id
|
||||||
|
|
||||||
|
user_id = requested.args.get("user_id", False)
|
||||||
|
if user_id is not False:
|
||||||
|
filtered["user_id"] = user_id
|
||||||
|
|
||||||
|
return (metric, filtered)
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_manager(requested, assiduites_query: Assiduite):
|
||||||
|
"""
|
||||||
|
Retourne les assiduites entrées filtrées en fonction de la request
|
||||||
|
"""
|
||||||
|
# cas 1 : etat assiduite
|
||||||
|
etat = requested.args.get("etat")
|
||||||
|
if etat is not None:
|
||||||
|
assiduites_query = scass.filter_assiduites_by_etat(assiduites_query, etat)
|
||||||
|
|
||||||
|
# cas 2 : date de début
|
||||||
|
deb = requested.args.get("date_debut", "").replace(" ", "+")
|
||||||
|
deb: datetime = scu.is_iso_formated(deb, True)
|
||||||
|
|
||||||
|
# cas 3 : date de fin
|
||||||
|
fin = requested.args.get("date_fin", "").replace(" ", "+")
|
||||||
|
fin = scu.is_iso_formated(fin, True)
|
||||||
|
|
||||||
|
if (deb, fin) != (None, None):
|
||||||
|
assiduites_query: Assiduite = scass.filter_by_date(
|
||||||
|
assiduites_query, Assiduite, deb, fin
|
||||||
|
)
|
||||||
|
|
||||||
|
# cas 4 : moduleimpl_id
|
||||||
|
module = requested.args.get("moduleimpl_id", False)
|
||||||
|
try:
|
||||||
|
if module is False:
|
||||||
|
raise ValueError
|
||||||
|
if module != "":
|
||||||
|
module = int(module)
|
||||||
|
else:
|
||||||
|
module = None
|
||||||
|
except ValueError:
|
||||||
|
module = False
|
||||||
|
|
||||||
|
if module is not False:
|
||||||
|
assiduites_query = scass.filter_by_module_impl(assiduites_query, module)
|
||||||
|
|
||||||
|
# cas 5 : formsemestre_id
|
||||||
|
formsemestre_id = requested.args.get("formsemestre_id")
|
||||||
|
|
||||||
|
if formsemestre_id is not None:
|
||||||
|
formsemestre: FormSemestre = None
|
||||||
|
formsemestre_id = int(formsemestre_id)
|
||||||
|
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
|
||||||
|
assiduites_query = scass.filter_by_formsemestre(assiduites_query, formsemestre)
|
||||||
|
|
||||||
|
# cas 6 : est_just
|
||||||
|
|
||||||
|
est_just: str = requested.args.get("est_just")
|
||||||
|
if est_just is not None:
|
||||||
|
trues: tuple[str] = ("v", "t", "vrai", "true")
|
||||||
|
falses: tuple[str] = ("f", "faux", "false")
|
||||||
|
|
||||||
|
if est_just.lower() in trues:
|
||||||
|
assiduites_query: Assiduite = scass.filter_assiduites_by_est_just(
|
||||||
|
assiduites_query, True
|
||||||
|
)
|
||||||
|
elif est_just.lower() in falses:
|
||||||
|
assiduites_query: Assiduite = scass.filter_assiduites_by_est_just(
|
||||||
|
assiduites_query, False
|
||||||
|
)
|
||||||
|
|
||||||
|
# cas 8 : user_id
|
||||||
|
|
||||||
|
user_id = requested.args.get("user_id", False)
|
||||||
|
if user_id is not False:
|
||||||
|
assiduites_query: Assiduite = scass.filter_by_user_id(assiduites_query, user_id)
|
||||||
|
|
||||||
|
return assiduites_query
|
73
app/api/etudiants.py
Normal file → Executable file
73
app/api/etudiants.py
Normal file → Executable file
@ -8,17 +8,16 @@
|
|||||||
API : accès aux étudiants
|
API : accès aux étudiants
|
||||||
"""
|
"""
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from operator import attrgetter
|
|
||||||
|
|
||||||
from flask import g, request
|
from flask import g, request
|
||||||
from flask_json import as_json
|
from flask_json import as_json
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
from sqlalchemy import desc, func, or_
|
from sqlalchemy import desc, or_
|
||||||
from sqlalchemy.dialects.postgresql import VARCHAR
|
|
||||||
|
|
||||||
import app
|
import app
|
||||||
from app.api import api_bp as bp, api_web_bp
|
from app.api import api_bp as bp, api_web_bp
|
||||||
|
from app.scodoc.sco_utils import json_error
|
||||||
from app.api import tools
|
from app.api import tools
|
||||||
from app.decorators import scodoc, permission_required
|
from app.decorators import scodoc, permission_required
|
||||||
from app.models import (
|
from app.models import (
|
||||||
@ -32,8 +31,7 @@ from app.scodoc import sco_bulletins
|
|||||||
from app.scodoc import sco_groups
|
from app.scodoc import sco_groups
|
||||||
from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud
|
from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
from app.scodoc.sco_utils import json_error, suppress_accents
|
import app.scodoc.sco_photos as sco_photos
|
||||||
|
|
||||||
|
|
||||||
# Un exemple:
|
# Un exemple:
|
||||||
# @bp.route("/api_function/<int:arg>")
|
# @bp.route("/api_function/<int:arg>")
|
||||||
@ -136,6 +134,42 @@ def etudiant(etudid: int = None, nip: str = None, ine: str = None):
|
|||||||
return etud.to_dict_api()
|
return etud.to_dict_api()
|
||||||
|
|
||||||
|
|
||||||
|
@api_web_bp.route("/etudiant/etudid/<int:etudid>/photo")
|
||||||
|
@api_web_bp.route("/etudiant/nip/<string:nip>/photo")
|
||||||
|
@api_web_bp.route("/etudiant/ine/<string:ine>/photo")
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def get_photo_image(etudid: int = None, nip: str = None, ine: str = None):
|
||||||
|
"""
|
||||||
|
Retourne la photo de l'étudiant
|
||||||
|
correspondant ou un placeholder si non existant.
|
||||||
|
|
||||||
|
etudid : l'etudid de l'étudiant
|
||||||
|
nip : le code nip de l'étudiant
|
||||||
|
ine : le code ine de l'étudiant
|
||||||
|
|
||||||
|
Attention : Ne peut être qu'utilisée en tant que route de département
|
||||||
|
"""
|
||||||
|
|
||||||
|
etud = tools.get_etud(etudid, nip, ine)
|
||||||
|
|
||||||
|
if etud is None:
|
||||||
|
return json_error(
|
||||||
|
404,
|
||||||
|
message="étudiant inconnu",
|
||||||
|
)
|
||||||
|
if not etudid:
|
||||||
|
filename = sco_photos.UNKNOWN_IMAGE_PATH
|
||||||
|
|
||||||
|
size = request.args.get("size", "orig")
|
||||||
|
filename = sco_photos.photo_pathname(etud.photo_filename, size=size)
|
||||||
|
if not filename:
|
||||||
|
filename = sco_photos.UNKNOWN_IMAGE_PATH
|
||||||
|
res = sco_photos.build_image_response(filename)
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/etudiants/etudid/<int:etudid>", methods=["GET"])
|
@bp.route("/etudiants/etudid/<int:etudid>", methods=["GET"])
|
||||||
@bp.route("/etudiants/nip/<string:nip>", methods=["GET"])
|
@bp.route("/etudiants/nip/<string:nip>", methods=["GET"])
|
||||||
@bp.route("/etudiants/ine/<string:ine>", methods=["GET"])
|
@bp.route("/etudiants/ine/<string:ine>", methods=["GET"])
|
||||||
@ -167,39 +201,12 @@ def etudiants(etudid: int = None, nip: str = None, ine: str = None):
|
|||||||
)
|
)
|
||||||
if not None in allowed_depts:
|
if not None in allowed_depts:
|
||||||
# restreint aux départements autorisés:
|
# restreint aux départements autorisés:
|
||||||
query = query.join(Departement).filter(
|
etuds = etuds.join(Departement).filter(
|
||||||
or_(Departement.acronym == acronym for acronym in allowed_depts)
|
or_(Departement.acronym == acronym for acronym in allowed_depts)
|
||||||
)
|
)
|
||||||
return [etud.to_dict_api() for etud in query]
|
return [etud.to_dict_api() for etud in query]
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/etudiants/name/<string:start>")
|
|
||||||
@api_web_bp.route("/etudiants/name/<string:start>")
|
|
||||||
@scodoc
|
|
||||||
@permission_required(Permission.ScoView)
|
|
||||||
@as_json
|
|
||||||
def etudiants_by_name(start: str = "", min_len=3, limit=32):
|
|
||||||
"""Liste des étudiants dont le nom débute par start.
|
|
||||||
Si start fait moins de min_len=3 caractères, liste vide.
|
|
||||||
La casse et les accents sont ignorés.
|
|
||||||
"""
|
|
||||||
if len(start) < min_len:
|
|
||||||
return []
|
|
||||||
start = suppress_accents(start).lower()
|
|
||||||
query = Identite.query.filter(
|
|
||||||
func.lower(func.unaccent(Identite.nom, type_=VARCHAR)).ilike(start + "%")
|
|
||||||
)
|
|
||||||
allowed_depts = current_user.get_depts_with_permission(Permission.ScoView)
|
|
||||||
if not None in allowed_depts:
|
|
||||||
# restreint aux départements autorisés:
|
|
||||||
query = query.join(Departement).filter(
|
|
||||||
or_(Departement.acronym == acronym for acronym in allowed_depts)
|
|
||||||
)
|
|
||||||
etuds = query.order_by(Identite.nom, Identite.prenom).limit(limit)
|
|
||||||
# Note: on raffine le tri pour les caractères spéciaux et nom usuel ici:
|
|
||||||
return [etud.to_dict_api() for etud in sorted(etuds, key=attrgetter("sort_key"))]
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/etudiant/etudid/<int:etudid>/formsemestres")
|
@bp.route("/etudiant/etudid/<int:etudid>/formsemestres")
|
||||||
@bp.route("/etudiant/nip/<string:nip>/formsemestres")
|
@bp.route("/etudiant/nip/<string:nip>/formsemestres")
|
||||||
@bp.route("/etudiant/ine/<string:ine>/formsemestres")
|
@bp.route("/etudiant/ine/<string:ine>/formsemestres")
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
ScoDoc 9 API : accès aux évaluations
|
ScoDoc 9 API : accès aux évaluations
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import g, request
|
from flask import g
|
||||||
from flask_json import as_json
|
from flask_json import as_json
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ import app
|
|||||||
from app.api import api_bp as bp, api_web_bp
|
from app.api import api_bp as bp, api_web_bp
|
||||||
from app.decorators import scodoc, permission_required
|
from app.decorators import scodoc, permission_required
|
||||||
from app.models import Evaluation, ModuleImpl, FormSemestre
|
from app.models import Evaluation, ModuleImpl, FormSemestre
|
||||||
from app.scodoc import sco_evaluation_db, sco_saisie_notes
|
from app.scodoc import sco_evaluation_db
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
|
|
||||||
@ -28,7 +28,7 @@ import app.scodoc.sco_utils as scu
|
|||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@as_json
|
||||||
def evaluation(evaluation_id: int):
|
def the_eval(evaluation_id: int):
|
||||||
"""Description d'une évaluation.
|
"""Description d'une évaluation.
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -93,22 +93,24 @@ def evaluations(moduleimpl_id: int):
|
|||||||
@as_json
|
@as_json
|
||||||
def evaluation_notes(evaluation_id: int):
|
def evaluation_notes(evaluation_id: int):
|
||||||
"""
|
"""
|
||||||
Retourne la liste des notes de l'évaluation
|
Retourne la liste des notes à partir de l'id d'une évaluation donnée
|
||||||
|
|
||||||
evaluation_id : l'id de l'évaluation
|
evaluation_id : l'id d'une évaluation
|
||||||
|
|
||||||
Exemple de résultat :
|
Exemple de résultat :
|
||||||
{
|
{
|
||||||
"11": {
|
"1": {
|
||||||
"etudid": 11,
|
"id": 1,
|
||||||
|
"etudid": 10,
|
||||||
"evaluation_id": 1,
|
"evaluation_id": 1,
|
||||||
"value": 15.0,
|
"value": 15.0,
|
||||||
"comment": "",
|
"comment": "",
|
||||||
"date": "Wed, 20 Apr 2022 06:49:05 GMT",
|
"date": "Wed, 20 Apr 2022 06:49:05 GMT",
|
||||||
"uid": 2
|
"uid": 2
|
||||||
},
|
},
|
||||||
"12": {
|
"2": {
|
||||||
"etudid": 12,
|
"id": 2,
|
||||||
|
"etudid": 1,
|
||||||
"evaluation_id": 1,
|
"evaluation_id": 1,
|
||||||
"value": 12.0,
|
"value": 12.0,
|
||||||
"comment": "",
|
"comment": "",
|
||||||
@ -126,8 +128,8 @@ def evaluation_notes(evaluation_id: int):
|
|||||||
.filter_by(dept_id=g.scodoc_dept_id)
|
.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
evaluation = query.first_or_404()
|
the_eval = query.first_or_404()
|
||||||
dept = evaluation.moduleimpl.formsemestre.departement
|
dept = the_eval.moduleimpl.formsemestre.departement
|
||||||
app.set_sco_dept(dept.acronym)
|
app.set_sco_dept(dept.acronym)
|
||||||
|
|
||||||
notes = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
|
notes = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
|
||||||
@ -135,49 +137,7 @@ def evaluation_notes(evaluation_id: int):
|
|||||||
# "ABS", "EXC", etc mais laisse les notes sur le barème de l'éval.
|
# "ABS", "EXC", etc mais laisse les notes sur le barème de l'éval.
|
||||||
note = notes[etudid]
|
note = notes[etudid]
|
||||||
note["value"] = scu.fmt_note(note["value"], keep_numeric=True)
|
note["value"] = scu.fmt_note(note["value"], keep_numeric=True)
|
||||||
note["note_max"] = evaluation.note_max
|
note["note_max"] = the_eval.note_max
|
||||||
del note["id"]
|
del note["id"]
|
||||||
|
|
||||||
# in JS, keys must be string, not integers
|
return notes
|
||||||
return {str(etudid): note for etudid, note in notes.items()}
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/evaluation/<int:evaluation_id>/notes/set", methods=["POST"])
|
|
||||||
@api_web_bp.route("/evaluation/<int:evaluation_id>/notes/set", methods=["POST"])
|
|
||||||
@login_required
|
|
||||||
@scodoc
|
|
||||||
@permission_required(Permission.ScoEnsView)
|
|
||||||
@as_json
|
|
||||||
def evaluation_set_notes(evaluation_id: int):
|
|
||||||
"""Écriture de notes dans une évaluation.
|
|
||||||
The request content type should be "application/json",
|
|
||||||
and contains:
|
|
||||||
{
|
|
||||||
'notes' : [ [etudid, value], ... ],
|
|
||||||
'comment' : optional string
|
|
||||||
}
|
|
||||||
Result:
|
|
||||||
- nb_changed: nombre de notes changées
|
|
||||||
- nb_suppress: nombre de notes effacées
|
|
||||||
- etudids_with_decision: liste des etudiants dont la note a changé
|
|
||||||
alors qu'ils ont une décision de jury enregistrée.
|
|
||||||
"""
|
|
||||||
query = Evaluation.query.filter_by(id=evaluation_id)
|
|
||||||
if g.scodoc_dept:
|
|
||||||
query = (
|
|
||||||
query.join(ModuleImpl)
|
|
||||||
.join(FormSemestre)
|
|
||||||
.filter_by(dept_id=g.scodoc_dept_id)
|
|
||||||
)
|
|
||||||
evaluation = query.first_or_404()
|
|
||||||
dept = evaluation.moduleimpl.formsemestre.departement
|
|
||||||
app.set_sco_dept(dept.acronym)
|
|
||||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
|
||||||
notes = data.get("notes")
|
|
||||||
if notes is None:
|
|
||||||
return scu.json_error(404, "no notes")
|
|
||||||
if not isinstance(notes, list):
|
|
||||||
return scu.json_error(404, "invalid notes argument (must be a list)")
|
|
||||||
return sco_saisie_notes.save_notes(
|
|
||||||
evaluation, notes, comment=data.get("comment", "")
|
|
||||||
)
|
|
||||||
|
323
app/api/jury.py
323
app/api/jury.py
@ -5,38 +5,19 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
"""
|
"""
|
||||||
ScoDoc 9 API : jury WIP à compléter avec enregistrement décisions
|
ScoDoc 9 API : jury WIP
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
from flask import flash, g, request, url_for
|
|
||||||
from flask_json import as_json
|
from flask_json import as_json
|
||||||
from flask_login import current_user, login_required
|
from flask_login import login_required
|
||||||
|
|
||||||
import app
|
import app
|
||||||
from app import db, log
|
from app.api import api_bp as bp, api_web_bp
|
||||||
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR, tools
|
|
||||||
from app.decorators import scodoc, permission_required
|
from app.decorators import scodoc, permission_required
|
||||||
from app.scodoc.sco_exceptions import ScoException
|
from app.scodoc.sco_exceptions import ScoException
|
||||||
from app.but import jury_but_results
|
from app.but import jury_but_results
|
||||||
from app.models import (
|
from app.models import FormSemestre
|
||||||
ApcParcours,
|
|
||||||
ApcValidationAnnee,
|
|
||||||
ApcValidationRCUE,
|
|
||||||
Formation,
|
|
||||||
FormSemestre,
|
|
||||||
Identite,
|
|
||||||
ScolarAutorisationInscription,
|
|
||||||
ScolarFormSemestreValidation,
|
|
||||||
ScolarNews,
|
|
||||||
Scolog,
|
|
||||||
UniteEns,
|
|
||||||
)
|
|
||||||
from app.scodoc import codes_cursus
|
|
||||||
from app.scodoc import sco_cache
|
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
from app.scodoc.sco_utils import json_error
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/formsemestre/<int:formsemestre_id>/decisions_jury")
|
@bp.route("/formsemestre/<int:formsemestre_id>/decisions_jury")
|
||||||
@ -48,304 +29,10 @@ from app.scodoc.sco_utils import json_error
|
|||||||
def decisions_jury(formsemestre_id: int):
|
def decisions_jury(formsemestre_id: int):
|
||||||
"""Décisions du jury des étudiants du formsemestre."""
|
"""Décisions du jury des étudiants du formsemestre."""
|
||||||
# APC, pair:
|
# APC, pair:
|
||||||
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
|
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||||
if formsemestre.formation.is_apc():
|
if formsemestre.formation.is_apc():
|
||||||
app.set_sco_dept(formsemestre.departement.acronym)
|
app.set_sco_dept(formsemestre.departement.acronym)
|
||||||
rows = jury_but_results.get_jury_but_results(formsemestre)
|
rows = jury_but_results.get_jury_but_results(formsemestre)
|
||||||
return rows
|
return rows
|
||||||
else:
|
else:
|
||||||
raise ScoException("non implemente")
|
raise ScoException("non implemente")
|
||||||
|
|
||||||
|
|
||||||
def _news_delete_jury_etud(etud: Identite):
|
|
||||||
"génère news sur effacement décision"
|
|
||||||
# n'utilise pas g.scodoc_dept, pas toujours dispo en mode API
|
|
||||||
url = url_for(
|
|
||||||
"scolar.ficheEtud", scodoc_dept=etud.departement.acronym, etudid=etud.id
|
|
||||||
)
|
|
||||||
ScolarNews.add(
|
|
||||||
typ=ScolarNews.NEWS_JURY,
|
|
||||||
obj=etud.id,
|
|
||||||
text=f"""Suppression décision jury pour <a href="{url}">{etud.nomprenom}</a>""",
|
|
||||||
url=url,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route(
|
|
||||||
"/etudiant/<int:etudid>/jury/validation_ue/<int:validation_id>/delete",
|
|
||||||
methods=["POST"],
|
|
||||||
)
|
|
||||||
@api_web_bp.route(
|
|
||||||
"/etudiant/<int:etudid>/jury/validation_ue/<int:validation_id>/delete",
|
|
||||||
methods=["POST"],
|
|
||||||
)
|
|
||||||
@login_required
|
|
||||||
@scodoc
|
|
||||||
@permission_required(Permission.ScoView)
|
|
||||||
@as_json
|
|
||||||
def validation_ue_delete(etudid: int, validation_id: int):
|
|
||||||
"Efface cette validation"
|
|
||||||
return _validation_ue_delete(etudid, validation_id)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route(
|
|
||||||
"/etudiant/<int:etudid>/jury/validation_formsemestre/<int:validation_id>/delete",
|
|
||||||
methods=["POST"],
|
|
||||||
)
|
|
||||||
@api_web_bp.route(
|
|
||||||
"/etudiant/<int:etudid>/jury/validation_formsemestre/<int:validation_id>/delete",
|
|
||||||
methods=["POST"],
|
|
||||||
)
|
|
||||||
@login_required
|
|
||||||
@scodoc
|
|
||||||
@permission_required(Permission.ScoView)
|
|
||||||
@as_json
|
|
||||||
def validation_formsemestre_delete(etudid: int, validation_id: int):
|
|
||||||
"Efface cette validation"
|
|
||||||
# c'est la même chose (formations classiques)
|
|
||||||
return _validation_ue_delete(etudid, validation_id)
|
|
||||||
|
|
||||||
|
|
||||||
def _validation_ue_delete(etudid: int, validation_id: int):
|
|
||||||
"Efface cette validation (semestres classiques ou UEs)"
|
|
||||||
etud = tools.get_etud(etudid)
|
|
||||||
if etud is None:
|
|
||||||
return "étudiant inconnu", 404
|
|
||||||
validation = ScolarFormSemestreValidation.query.filter_by(
|
|
||||||
id=validation_id, etudid=etudid
|
|
||||||
).first_or_404()
|
|
||||||
# Vérification de la permission:
|
|
||||||
# A le droit de supprimer cette validation: le chef de dept ou quelqu'un ayant
|
|
||||||
# le droit de saisir des décisions de jury dans le formsemestre concerné s'il y en a un
|
|
||||||
# (c'est le cas pour les validations de jury, mais pas pour les "antérieures" non
|
|
||||||
# rattachées à un formsemestre)
|
|
||||||
if not g.scodoc_dept: # accès API
|
|
||||||
if not current_user.has_permission(Permission.ScoEtudInscrit):
|
|
||||||
return json_error(403, "opération non autorisée (117)")
|
|
||||||
else:
|
|
||||||
if validation.formsemestre:
|
|
||||||
if (
|
|
||||||
validation.formsemestre.dept_id != g.scodoc_dept_id
|
|
||||||
) or not validation.formsemestre.can_edit_jury():
|
|
||||||
return json_error(403, "opération non autorisée (123)")
|
|
||||||
elif not current_user.has_permission(Permission.ScoEtudInscrit):
|
|
||||||
# Validation non rattachée à un semestre: on doit être chef
|
|
||||||
return json_error(403, "opération non autorisée (126)")
|
|
||||||
|
|
||||||
log(f"validation_ue_delete: etuid={etudid} {validation}")
|
|
||||||
db.session.delete(validation)
|
|
||||||
sco_cache.invalidate_formsemestre_etud(etud)
|
|
||||||
db.session.commit()
|
|
||||||
_news_delete_jury_etud(etud)
|
|
||||||
return "ok"
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route(
|
|
||||||
"/etudiant/<int:etudid>/jury/autorisation_inscription/<int:validation_id>/delete",
|
|
||||||
methods=["POST"],
|
|
||||||
)
|
|
||||||
@api_web_bp.route(
|
|
||||||
"/etudiant/<int:etudid>/jury/autorisation_inscription/<int:validation_id>/delete",
|
|
||||||
methods=["POST"],
|
|
||||||
)
|
|
||||||
@login_required
|
|
||||||
@scodoc
|
|
||||||
@permission_required(Permission.ScoEtudInscrit)
|
|
||||||
@as_json
|
|
||||||
def autorisation_inscription_delete(etudid: int, validation_id: int):
|
|
||||||
"Efface cette validation"
|
|
||||||
etud = tools.get_etud(etudid)
|
|
||||||
if etud is None:
|
|
||||||
return "étudiant inconnu", 404
|
|
||||||
validation = ScolarAutorisationInscription.query.filter_by(
|
|
||||||
id=validation_id, etudid=etudid
|
|
||||||
).first_or_404()
|
|
||||||
log(f"autorisation_inscription_delete: etuid={etudid} {validation}")
|
|
||||||
db.session.delete(validation)
|
|
||||||
sco_cache.invalidate_formsemestre_etud(etud)
|
|
||||||
db.session.commit()
|
|
||||||
_news_delete_jury_etud(etud)
|
|
||||||
return "ok"
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route(
|
|
||||||
"/etudiant/<int:etudid>/jury/validation_rcue/record",
|
|
||||||
methods=["POST"],
|
|
||||||
)
|
|
||||||
@api_web_bp.route(
|
|
||||||
"/etudiant/<int:etudid>/jury/validation_rcue/record",
|
|
||||||
methods=["POST"],
|
|
||||||
)
|
|
||||||
@login_required
|
|
||||||
@scodoc
|
|
||||||
@permission_required(Permission.ScoEtudInscrit)
|
|
||||||
@as_json
|
|
||||||
def validation_rcue_record(etudid: int):
|
|
||||||
"""Enregistre une validation de RCUE.
|
|
||||||
Si une validation existe déjà pour ce RCUE, la remplace.
|
|
||||||
The request content type should be "application/json":
|
|
||||||
{
|
|
||||||
"code" : str,
|
|
||||||
"ue1_id" : int,
|
|
||||||
"ue2_id" : int,
|
|
||||||
// Optionnel:
|
|
||||||
"formsemestre_id" : int,
|
|
||||||
"date" : date_iso, // si non spécifié, now()
|
|
||||||
"parcours_id" :int,
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
etud = tools.get_etud(etudid)
|
|
||||||
if etud is None:
|
|
||||||
return json_error(404, "étudiant inconnu")
|
|
||||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
|
||||||
code = data.get("code")
|
|
||||||
if code is None:
|
|
||||||
return json_error(API_CLIENT_ERROR, "missing argument: code")
|
|
||||||
if code not in codes_cursus.CODES_JURY_RCUE:
|
|
||||||
return json_error(API_CLIENT_ERROR, "invalid code value")
|
|
||||||
ue1_id = data.get("ue1_id")
|
|
||||||
if ue1_id is None:
|
|
||||||
return json_error(API_CLIENT_ERROR, "missing argument: ue1_id")
|
|
||||||
try:
|
|
||||||
ue1_id = int(ue1_id)
|
|
||||||
except ValueError:
|
|
||||||
return json_error(API_CLIENT_ERROR, "invalid value for ue1_id")
|
|
||||||
ue2_id = data.get("ue2_id")
|
|
||||||
if ue2_id is None:
|
|
||||||
return json_error(API_CLIENT_ERROR, "missing argument: ue2_id")
|
|
||||||
try:
|
|
||||||
ue2_id = int(ue2_id)
|
|
||||||
except ValueError:
|
|
||||||
return json_error(API_CLIENT_ERROR, "invalid value for ue2_id")
|
|
||||||
formsemestre_id = data.get("formsemestre_id")
|
|
||||||
date_validation_str = data.get("date", datetime.datetime.now().isoformat())
|
|
||||||
parcours_id = data.get("parcours_id")
|
|
||||||
#
|
|
||||||
query = UniteEns.query.filter_by(id=ue1_id)
|
|
||||||
if g.scodoc_dept:
|
|
||||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
|
||||||
ue1: UniteEns = query.first_or_404()
|
|
||||||
query = UniteEns.query.filter_by(id=ue2_id)
|
|
||||||
if g.scodoc_dept:
|
|
||||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
|
||||||
ue2: UniteEns = query.first_or_404()
|
|
||||||
if ue1.niveau_competence_id != ue2.niveau_competence_id:
|
|
||||||
return json_error(
|
|
||||||
API_CLIENT_ERROR, "UEs non associees au meme niveau de competence"
|
|
||||||
)
|
|
||||||
if formsemestre_id is not None:
|
|
||||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
|
||||||
if g.scodoc_dept:
|
|
||||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
|
||||||
formsemestre: FormSemestre = query.first_or_404()
|
|
||||||
if (formsemestre.formation_id != ue1.formation_id) or (
|
|
||||||
formsemestre.formation_id != ue2.formation_id
|
|
||||||
):
|
|
||||||
return json_error(
|
|
||||||
API_CLIENT_ERROR, "ues et semestre ne sont pas de la meme formation"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
formsemestre = None
|
|
||||||
try:
|
|
||||||
date_validation = datetime.datetime.fromisoformat(date_validation_str)
|
|
||||||
except ValueError:
|
|
||||||
return json_error(API_CLIENT_ERROR, "invalid date string")
|
|
||||||
if parcours_id is not None:
|
|
||||||
parcours: ApcParcours = ApcParcours.query.get_or_404(parcours_id)
|
|
||||||
if parcours.referentiel_id != ue1.niveau_competence.competence.referentiel_id:
|
|
||||||
return json_error(API_CLIENT_ERROR, "niveau et parcours incompatibles")
|
|
||||||
|
|
||||||
# Une validation pour ce niveau de compétence existe-elle ?
|
|
||||||
validation = (
|
|
||||||
ApcValidationRCUE.query.filter_by(etudid=etudid)
|
|
||||||
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue2_id)
|
|
||||||
.filter_by(niveau_competence_id=ue2.niveau_competence_id)
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
if validation:
|
|
||||||
validation.code = code
|
|
||||||
validation.date = date_validation
|
|
||||||
validation.formsemestre_id = formsemestre_id
|
|
||||||
validation.parcours_id = parcours_id
|
|
||||||
validation.ue1_id = ue1_id
|
|
||||||
validation.ue2_id = ue2_id
|
|
||||||
operation = "update"
|
|
||||||
else:
|
|
||||||
validation = ApcValidationRCUE(
|
|
||||||
code=code,
|
|
||||||
date=date_validation,
|
|
||||||
etudid=etudid,
|
|
||||||
formsemestre_id=formsemestre_id,
|
|
||||||
parcours_id=parcours_id,
|
|
||||||
ue1_id=ue1_id,
|
|
||||||
ue2_id=ue2_id,
|
|
||||||
)
|
|
||||||
operation = "record"
|
|
||||||
db.session.add(validation)
|
|
||||||
# invalider bulletins (les autres résultats ne dépendent pas des RCUEs):
|
|
||||||
sco_cache.invalidate_formsemestre_etud(etud)
|
|
||||||
db.session.commit()
|
|
||||||
Scolog.logdb(
|
|
||||||
method="validation_rcue_record",
|
|
||||||
etudid=etudid,
|
|
||||||
msg=f"Enregistrement {validation}",
|
|
||||||
commit=True,
|
|
||||||
)
|
|
||||||
log(f"{operation} {validation}")
|
|
||||||
return validation.to_dict()
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route(
|
|
||||||
"/etudiant/<int:etudid>/jury/validation_rcue/<int:validation_id>/delete",
|
|
||||||
methods=["POST"],
|
|
||||||
)
|
|
||||||
@api_web_bp.route(
|
|
||||||
"/etudiant/<int:etudid>/jury/validation_rcue/<int:validation_id>/delete",
|
|
||||||
methods=["POST"],
|
|
||||||
)
|
|
||||||
@login_required
|
|
||||||
@scodoc
|
|
||||||
@permission_required(Permission.ScoEtudInscrit)
|
|
||||||
@as_json
|
|
||||||
def validation_rcue_delete(etudid: int, validation_id: int):
|
|
||||||
"Efface cette validation"
|
|
||||||
etud = tools.get_etud(etudid)
|
|
||||||
if etud is None:
|
|
||||||
return "étudiant inconnu", 404
|
|
||||||
validation = ApcValidationRCUE.query.filter_by(
|
|
||||||
id=validation_id, etudid=etudid
|
|
||||||
).first_or_404()
|
|
||||||
log(f"validation_ue_delete: etuid={etudid} {validation}")
|
|
||||||
db.session.delete(validation)
|
|
||||||
sco_cache.invalidate_formsemestre_etud(etud)
|
|
||||||
db.session.commit()
|
|
||||||
_news_delete_jury_etud(etud)
|
|
||||||
return "ok"
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route(
|
|
||||||
"/etudiant/<int:etudid>/jury/validation_annee_but/<int:validation_id>/delete",
|
|
||||||
methods=["POST"],
|
|
||||||
)
|
|
||||||
@api_web_bp.route(
|
|
||||||
"/etudiant/<int:etudid>/jury/validation_annee_but/<int:validation_id>/delete",
|
|
||||||
methods=["POST"],
|
|
||||||
)
|
|
||||||
@login_required
|
|
||||||
@scodoc
|
|
||||||
@permission_required(Permission.ScoEtudInscrit)
|
|
||||||
@as_json
|
|
||||||
def validation_annee_but_delete(etudid: int, validation_id: int):
|
|
||||||
"Efface cette validation"
|
|
||||||
etud = tools.get_etud(etudid)
|
|
||||||
if etud is None:
|
|
||||||
return "étudiant inconnu", 404
|
|
||||||
validation = ApcValidationAnnee.query.filter_by(
|
|
||||||
id=validation_id, etudid=etudid
|
|
||||||
).first_or_404()
|
|
||||||
log(f"validation_annee_but: etuid={etudid} {validation}")
|
|
||||||
db.session.delete(validation)
|
|
||||||
sco_cache.invalidate_formsemestre_etud(etud)
|
|
||||||
db.session.commit()
|
|
||||||
_news_delete_jury_etud(etud)
|
|
||||||
return "ok"
|
|
||||||
|
596
app/api/justificatifs.py
Normal file
596
app/api/justificatifs.py
Normal file
@ -0,0 +1,596 @@
|
|||||||
|
##############################################################################
|
||||||
|
# ScoDoc
|
||||||
|
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||||
|
# See LICENSE
|
||||||
|
##############################################################################
|
||||||
|
"""ScoDoc 9 API : Assiduités
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from flask import g, jsonify, request
|
||||||
|
from flask_login import login_required, current_user
|
||||||
|
|
||||||
|
import app.scodoc.sco_assiduites as scass
|
||||||
|
import app.scodoc.sco_utils as scu
|
||||||
|
from app import db
|
||||||
|
from app.api import api_bp as bp
|
||||||
|
from app.api import api_web_bp
|
||||||
|
from app.api import get_model_api_object
|
||||||
|
from app.decorators import permission_required, scodoc
|
||||||
|
from app.models import Identite, Justificatif
|
||||||
|
from app.models.assiduites import compute_assiduites_justified
|
||||||
|
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
|
||||||
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
|
from app.scodoc.sco_permissions import Permission
|
||||||
|
from app.scodoc.sco_utils import json_error
|
||||||
|
|
||||||
|
|
||||||
|
# Partie Modèle
|
||||||
|
@bp.route("/justificatif/<int:justif_id>")
|
||||||
|
@api_web_bp.route("/justificatif/<int:justif_id>")
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def justificatif(justif_id: int = None):
|
||||||
|
"""Retourne un objet justificatif à partir de son id
|
||||||
|
|
||||||
|
Exemple de résultat:
|
||||||
|
{
|
||||||
|
"justif_id": 1,
|
||||||
|
"etudid": 2,
|
||||||
|
"date_debut": "2022-10-31T08:00+01:00",
|
||||||
|
"date_fin": "2022-10-31T10:00+01:00",
|
||||||
|
"etat": "valide",
|
||||||
|
"fichier": "archive_id",
|
||||||
|
"raison": "une raison",
|
||||||
|
"entry_date": "2022-10-31T08:00+01:00",
|
||||||
|
"user_id": 1 or null,
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
return get_model_api_object(Justificatif, justif_id, Identite)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/justificatifs/<int:etudid>", defaults={"with_query": False})
|
||||||
|
@bp.route("/justificatifs/<int:etudid>/query", defaults={"with_query": True})
|
||||||
|
@api_web_bp.route("/justificatifs/<int:etudid>", defaults={"with_query": False})
|
||||||
|
@api_web_bp.route("/justificatifs/<int:etudid>/query", defaults={"with_query": True})
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def justificatifs(etudid: int = None, with_query: bool = False):
|
||||||
|
"""
|
||||||
|
Retourne toutes les assiduités d'un étudiant
|
||||||
|
chemin : /justificatifs/<int:etudid>
|
||||||
|
|
||||||
|
Un filtrage peut être donné avec une query
|
||||||
|
chemin : /justificatifs/<int:etudid>/query?
|
||||||
|
|
||||||
|
Les différents filtres :
|
||||||
|
Etat (etat du justificatif -> validé, non validé, modifé, en attente):
|
||||||
|
query?etat=[- liste des états séparé par une virgule -]
|
||||||
|
ex: .../query?etat=validé,modifié
|
||||||
|
Date debut
|
||||||
|
(date de début du justificatif, sont affichés les justificatifs
|
||||||
|
dont la date de début est supérieur ou égale à la valeur donnée):
|
||||||
|
query?date_debut=[- date au format iso -]
|
||||||
|
ex: query?date_debut=2022-11-03T08:00+01:00
|
||||||
|
Date fin
|
||||||
|
(date de fin du justificatif, sont affichés les justificatifs
|
||||||
|
dont la date de fin est inférieure ou égale à la valeur donnée):
|
||||||
|
query?date_fin=[- date au format iso -]
|
||||||
|
ex: query?date_fin=2022-11-03T10:00+01:00
|
||||||
|
user_id (l'id de l'auteur du justificatif)
|
||||||
|
query?user_id=[int]
|
||||||
|
ex query?user_id=3
|
||||||
|
"""
|
||||||
|
|
||||||
|
query = Identite.query.filter_by(id=etudid)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
|
||||||
|
etud: Identite = query.first_or_404(etudid)
|
||||||
|
justificatifs_query = etud.justificatifs
|
||||||
|
|
||||||
|
if with_query:
|
||||||
|
justificatifs_query = _filter_manager(request, justificatifs_query)
|
||||||
|
|
||||||
|
data_set: list[dict] = []
|
||||||
|
for just in justificatifs_query.all():
|
||||||
|
data = just.to_dict(format_api=True)
|
||||||
|
data_set.append(data)
|
||||||
|
|
||||||
|
return jsonify(data_set)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/justificatif/<int:etudid>/create", methods=["POST"])
|
||||||
|
@api_web_bp.route("/justificatif/<int:etudid>/create", methods=["POST"])
|
||||||
|
@scodoc
|
||||||
|
@login_required
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
# @permission_required(Permission.ScoAssiduiteChange)
|
||||||
|
def justif_create(etudid: int = None):
|
||||||
|
"""
|
||||||
|
Création d'un justificatif pour l'étudiant (etudid)
|
||||||
|
La requête doit avoir un content type "application/json":
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"date_debut": str,
|
||||||
|
"date_fin": str,
|
||||||
|
"etat": str,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date_debut": str,
|
||||||
|
"date_fin": str,
|
||||||
|
"etat": str,
|
||||||
|
"raison":str,
|
||||||
|
}
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
"""
|
||||||
|
etud: Identite = Identite.query.filter_by(id=etudid).first_or_404()
|
||||||
|
|
||||||
|
create_list: list[object] = request.get_json(force=True)
|
||||||
|
|
||||||
|
if not isinstance(create_list, list):
|
||||||
|
return json_error(404, "Le contenu envoyé n'est pas une liste")
|
||||||
|
|
||||||
|
errors: dict[int, str] = {}
|
||||||
|
success: dict[int, object] = {}
|
||||||
|
for i, data in enumerate(create_list):
|
||||||
|
code, obj = _create_singular(data, etud)
|
||||||
|
if code == 404:
|
||||||
|
errors[i] = obj
|
||||||
|
else:
|
||||||
|
success[i] = obj
|
||||||
|
compute_assiduites_justified(Justificatif.query.filter_by(etudid=etudid), True)
|
||||||
|
return jsonify({"errors": errors, "success": success})
|
||||||
|
|
||||||
|
|
||||||
|
def _create_singular(
|
||||||
|
data: dict,
|
||||||
|
etud: Identite,
|
||||||
|
) -> tuple[int, object]:
|
||||||
|
errors: list[str] = []
|
||||||
|
|
||||||
|
# -- vérifications de l'objet json --
|
||||||
|
# cas 1 : ETAT
|
||||||
|
etat = data.get("etat", None)
|
||||||
|
if etat is None:
|
||||||
|
errors.append("param 'etat': manquant")
|
||||||
|
elif not scu.EtatJustificatif.contains(etat):
|
||||||
|
errors.append("param 'etat': invalide")
|
||||||
|
|
||||||
|
etat = scu.EtatJustificatif.get(etat)
|
||||||
|
|
||||||
|
# cas 2 : date_debut
|
||||||
|
date_debut = data.get("date_debut", None)
|
||||||
|
if date_debut is None:
|
||||||
|
errors.append("param 'date_debut': manquant")
|
||||||
|
deb = scu.is_iso_formated(date_debut, convert=True)
|
||||||
|
if deb is None:
|
||||||
|
errors.append("param 'date_debut': format invalide")
|
||||||
|
|
||||||
|
# cas 3 : date_fin
|
||||||
|
date_fin = data.get("date_fin", None)
|
||||||
|
if date_fin is None:
|
||||||
|
errors.append("param 'date_fin': manquant")
|
||||||
|
fin = scu.is_iso_formated(date_fin, convert=True)
|
||||||
|
if fin is None:
|
||||||
|
errors.append("param 'date_fin': format invalide")
|
||||||
|
|
||||||
|
# cas 4 : raison
|
||||||
|
|
||||||
|
raison: str = data.get("raison", None)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
err: str = ", ".join(errors)
|
||||||
|
return (404, err)
|
||||||
|
|
||||||
|
# TOUT EST OK
|
||||||
|
|
||||||
|
try:
|
||||||
|
nouv_justificatif: Justificatif = Justificatif.create_justificatif(
|
||||||
|
date_debut=deb,
|
||||||
|
date_fin=fin,
|
||||||
|
etat=etat,
|
||||||
|
etud=etud,
|
||||||
|
raison=raison,
|
||||||
|
user_id=current_user.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(nouv_justificatif)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return (
|
||||||
|
200,
|
||||||
|
{
|
||||||
|
"justif_id": nouv_justificatif.id,
|
||||||
|
"couverture": scass.justifies(nouv_justificatif),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except ScoValueError as excp:
|
||||||
|
return (
|
||||||
|
404,
|
||||||
|
excp.args[0],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/justificatif/<int:justif_id>/edit", methods=["POST"])
|
||||||
|
@api_web_bp.route("/justificatif/<int:justif_id>/edit", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
# @permission_required(Permission.ScoAssiduiteChange)
|
||||||
|
def justif_edit(justif_id: int):
|
||||||
|
"""
|
||||||
|
Edition d'un justificatif à partir de son id
|
||||||
|
La requête doit avoir un content type "application/json":
|
||||||
|
|
||||||
|
{
|
||||||
|
"etat"?: str,
|
||||||
|
"raison"?: str
|
||||||
|
"date_debut"?: str
|
||||||
|
"date_fin"?: str
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
justificatif_unique: Justificatif = Justificatif.query.filter_by(
|
||||||
|
id=justif_id
|
||||||
|
).first_or_404()
|
||||||
|
|
||||||
|
errors: list[str] = []
|
||||||
|
data = request.get_json(force=True)
|
||||||
|
avant_ids: list[int] = scass.justifies(justificatif_unique)
|
||||||
|
# Vérifications de data
|
||||||
|
|
||||||
|
# Cas 1 : Etat
|
||||||
|
if data.get("etat") is not None:
|
||||||
|
etat = scu.EtatJustificatif.get(data.get("etat"))
|
||||||
|
if etat is None:
|
||||||
|
errors.append("param 'etat': invalide")
|
||||||
|
else:
|
||||||
|
justificatif_unique.etat = etat
|
||||||
|
|
||||||
|
# Cas 2 : raison
|
||||||
|
raison = data.get("raison", False)
|
||||||
|
if raison is not False:
|
||||||
|
justificatif_unique.raison = raison
|
||||||
|
|
||||||
|
deb, fin = None, None
|
||||||
|
|
||||||
|
# cas 3 : date_debut
|
||||||
|
date_debut = data.get("date_debut", False)
|
||||||
|
if date_debut is not False:
|
||||||
|
if date_debut is None:
|
||||||
|
errors.append("param 'date_debut': manquant")
|
||||||
|
deb = scu.is_iso_formated(date_debut.replace(" ", "+"), convert=True)
|
||||||
|
if deb is None:
|
||||||
|
errors.append("param 'date_debut': format invalide")
|
||||||
|
|
||||||
|
if justificatif_unique.date_fin >= deb:
|
||||||
|
errors.append("param 'date_debut': date de début située après date de fin ")
|
||||||
|
|
||||||
|
# cas 4 : date_fin
|
||||||
|
date_fin = data.get("date_fin", False)
|
||||||
|
if date_fin is not False:
|
||||||
|
if date_fin is None:
|
||||||
|
errors.append("param 'date_fin': manquant")
|
||||||
|
fin = scu.is_iso_formated(date_fin.replace(" ", "+"), convert=True)
|
||||||
|
if fin is None:
|
||||||
|
errors.append("param 'date_fin': format invalide")
|
||||||
|
if justificatif_unique.date_debut <= fin:
|
||||||
|
errors.append("param 'date_fin': date de fin située avant date de début ")
|
||||||
|
|
||||||
|
# Mise à jour des dates
|
||||||
|
deb = deb if deb is not None else justificatif_unique.date_debut
|
||||||
|
fin = fin if fin is not None else justificatif_unique.date_fin
|
||||||
|
|
||||||
|
justificatif_unique.date_debut = deb
|
||||||
|
justificatif_unique.date_fin = fin
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
err: str = ", ".join(errors)
|
||||||
|
return json_error(404, err)
|
||||||
|
|
||||||
|
db.session.add(justificatif_unique)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"couverture": {
|
||||||
|
"avant": avant_ids,
|
||||||
|
"après": compute_assiduites_justified(
|
||||||
|
Justificatif.query.filter_by(etudid=justificatif_unique.etudid),
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/justificatif/delete", methods=["POST"])
|
||||||
|
@api_web_bp.route("/justificatif/delete", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
# @permission_required(Permission.ScoAssiduiteChange)
|
||||||
|
def justif_delete():
|
||||||
|
"""
|
||||||
|
Suppression d'un justificatif à partir de son id
|
||||||
|
|
||||||
|
Forme des données envoyées :
|
||||||
|
|
||||||
|
[
|
||||||
|
<justif_id:int>,
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
justificatifs_list: list[int] = request.get_json(force=True)
|
||||||
|
if not isinstance(justificatifs_list, list):
|
||||||
|
return json_error(404, "Le contenu envoyé n'est pas une liste")
|
||||||
|
|
||||||
|
output = {"errors": {}, "success": {}}
|
||||||
|
|
||||||
|
for i, ass in enumerate(justificatifs_list):
|
||||||
|
code, msg = _delete_singular(ass, db)
|
||||||
|
if code == 404:
|
||||||
|
output["errors"][f"{i}"] = msg
|
||||||
|
else:
|
||||||
|
output["success"][f"{i}"] = {"OK": True}
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify(output)
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_singular(justif_id: int, database):
|
||||||
|
justificatif_unique: Justificatif = Justificatif.query.filter_by(
|
||||||
|
id=justif_id
|
||||||
|
).first()
|
||||||
|
if justificatif_unique is None:
|
||||||
|
return (404, "Justificatif non existant")
|
||||||
|
|
||||||
|
archive_name: str = justificatif_unique.fichier
|
||||||
|
|
||||||
|
if archive_name is not None:
|
||||||
|
archiver: JustificatifArchiver = JustificatifArchiver()
|
||||||
|
archiver.delete_justificatif(justificatif_unique.etudid, archive_name)
|
||||||
|
database.session.delete(justificatif_unique)
|
||||||
|
|
||||||
|
compute_assiduites_justified(
|
||||||
|
Justificatif.query.filter_by(etudid=justificatif_unique.etudid), True
|
||||||
|
)
|
||||||
|
|
||||||
|
return (200, "OK")
|
||||||
|
|
||||||
|
|
||||||
|
# Partie archivage
|
||||||
|
@bp.route("/justificatif/<int:justif_id>/import", methods=["POST"])
|
||||||
|
@api_web_bp.route("/justificatif/<int:justif_id>/import", methods=["POST"])
|
||||||
|
@scodoc
|
||||||
|
@login_required
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
# @permission_required(Permission.ScoAssiduiteChange)
|
||||||
|
def justif_import(justif_id: int = None):
|
||||||
|
"""
|
||||||
|
Importation d'un fichier (création d'archive)
|
||||||
|
"""
|
||||||
|
if len(request.files) == 0:
|
||||||
|
return json_error(404, "Il n'y a pas de fichier joint")
|
||||||
|
|
||||||
|
file = list(request.files.values())[0]
|
||||||
|
if file.filename == "":
|
||||||
|
return json_error(404, "Il n'y a pas de fichier joint")
|
||||||
|
|
||||||
|
query = Justificatif.query.filter_by(id=justif_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
|
||||||
|
justificatif_unique: Justificatif = query.first_or_404()
|
||||||
|
|
||||||
|
archive_name: str = justificatif_unique.fichier
|
||||||
|
|
||||||
|
archiver: JustificatifArchiver = JustificatifArchiver()
|
||||||
|
try:
|
||||||
|
fname: str
|
||||||
|
archive_name, fname = archiver.save_justificatif(
|
||||||
|
etudid=justificatif_unique.etudid,
|
||||||
|
filename=file.filename,
|
||||||
|
data=file.stream.read(),
|
||||||
|
archive_name=archive_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
justificatif_unique.fichier = archive_name
|
||||||
|
|
||||||
|
db.session.add(justificatif_unique)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify({"filename": fname})
|
||||||
|
except ScoValueError as err:
|
||||||
|
return json_error(404, err.args[0])
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/justificatif/<int:justif_id>/export/<filename>", methods=["POST"])
|
||||||
|
@api_web_bp.route("/justificatif/<int:justif_id>/export/<filename>", methods=["POST"])
|
||||||
|
@scodoc
|
||||||
|
@login_required
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
# @permission_required(Permission.ScoAssiduiteChange)
|
||||||
|
def justif_export(justif_id: int = None, filename: str = None):
|
||||||
|
"""
|
||||||
|
Retourne un fichier d'une archive d'un justificatif
|
||||||
|
"""
|
||||||
|
|
||||||
|
query = Justificatif.query.filter_by(id=justif_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
|
||||||
|
justificatif_unique: Justificatif = query.first_or_404()
|
||||||
|
|
||||||
|
archive_name: str = justificatif_unique.fichier
|
||||||
|
if archive_name is None:
|
||||||
|
return json_error(404, "le justificatif ne possède pas de fichier")
|
||||||
|
|
||||||
|
archiver: JustificatifArchiver = JustificatifArchiver()
|
||||||
|
|
||||||
|
try:
|
||||||
|
return archiver.get_justificatif_file(
|
||||||
|
archive_name, justificatif_unique.etudid, filename
|
||||||
|
)
|
||||||
|
except ScoValueError as err:
|
||||||
|
return json_error(404, err.args[0])
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/justificatif/<int:justif_id>/remove", methods=["POST"])
|
||||||
|
@api_web_bp.route("/justificatif/<int:justif_id>/remove", methods=["POST"])
|
||||||
|
@scodoc
|
||||||
|
@login_required
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
# @permission_required(Permission.ScoAssiduiteChange)
|
||||||
|
def justif_remove(justif_id: int = None):
|
||||||
|
"""
|
||||||
|
Supression d'un fichier ou d'une archive
|
||||||
|
# TOTALK: Doc, expliquer les noms coté server
|
||||||
|
{
|
||||||
|
"remove": <"all"/"list">
|
||||||
|
|
||||||
|
"filenames"?: [
|
||||||
|
<filename:str>,
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
data: dict = request.get_json(force=True)
|
||||||
|
|
||||||
|
query = Justificatif.query.filter_by(id=justif_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
|
||||||
|
justificatif_unique: Justificatif = query.first_or_404()
|
||||||
|
|
||||||
|
archive_name: str = justificatif_unique.fichier
|
||||||
|
if archive_name is None:
|
||||||
|
return json_error(404, "le justificatif ne possède pas de fichier")
|
||||||
|
|
||||||
|
remove: str = data.get("remove")
|
||||||
|
if remove is None or remove not in ("all", "list"):
|
||||||
|
return json_error(404, "param 'remove': Valeur invalide")
|
||||||
|
archiver: JustificatifArchiver = JustificatifArchiver()
|
||||||
|
etudid: int = justificatif_unique.etudid
|
||||||
|
try:
|
||||||
|
if remove == "all":
|
||||||
|
archiver.delete_justificatif(etudid=etudid, archive_name=archive_name)
|
||||||
|
justificatif_unique.fichier = None
|
||||||
|
db.session.add(justificatif_unique)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
else:
|
||||||
|
for fname in data.get("filenames", []):
|
||||||
|
archiver.delete_justificatif(
|
||||||
|
etudid=etudid,
|
||||||
|
archive_name=archive_name,
|
||||||
|
filename=fname,
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(archiver.list_justificatifs(archive_name, etudid)) == 0:
|
||||||
|
archiver.delete_justificatif(etudid, archive_name)
|
||||||
|
justificatif_unique.fichier = None
|
||||||
|
db.session.add(justificatif_unique)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
except ScoValueError as err:
|
||||||
|
return json_error(404, err.args[0])
|
||||||
|
|
||||||
|
return jsonify({"response": "removed"})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/justificatif/<int:justif_id>/list", methods=["GET"])
|
||||||
|
@api_web_bp.route("/justificatif/<int:justif_id>/list", methods=["GET"])
|
||||||
|
@scodoc
|
||||||
|
@login_required
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
# @permission_required(Permission.ScoAssiduiteChange)
|
||||||
|
def justif_list(justif_id: int = None):
|
||||||
|
"""
|
||||||
|
Liste les fichiers du justificatif
|
||||||
|
"""
|
||||||
|
|
||||||
|
query = Justificatif.query.filter_by(id=justif_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
|
||||||
|
justificatif_unique: Justificatif = query.first_or_404()
|
||||||
|
|
||||||
|
archive_name: str = justificatif_unique.fichier
|
||||||
|
|
||||||
|
filenames: list[str] = []
|
||||||
|
|
||||||
|
archiver: JustificatifArchiver = JustificatifArchiver()
|
||||||
|
if archive_name is not None:
|
||||||
|
filenames = archiver.list_justificatifs(
|
||||||
|
archive_name, justificatif_unique.etudid
|
||||||
|
)
|
||||||
|
|
||||||
|
return jsonify(filenames)
|
||||||
|
|
||||||
|
|
||||||
|
# Partie justification
|
||||||
|
@bp.route("/justificatif/<int:justif_id>/justifies", methods=["GET"])
|
||||||
|
@api_web_bp.route("/justificatif/<int:justif_id>/justifies", methods=["GET"])
|
||||||
|
@scodoc
|
||||||
|
@login_required
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
# @permission_required(Permission.ScoAssiduiteChange)
|
||||||
|
def justif_justifies(justif_id: int = None):
|
||||||
|
"""
|
||||||
|
Liste assiduite_id justifiées par le justificatif
|
||||||
|
"""
|
||||||
|
|
||||||
|
query = Justificatif.query.filter_by(id=justif_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
|
||||||
|
justificatif_unique: Justificatif = query.first_or_404()
|
||||||
|
|
||||||
|
assiduites_list: list[int] = scass.justifies(justificatif_unique)
|
||||||
|
|
||||||
|
return jsonify(assiduites_list)
|
||||||
|
|
||||||
|
|
||||||
|
# -- Utils --
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_manager(requested, justificatifs_query):
|
||||||
|
"""
|
||||||
|
Retourne les justificatifs entrés filtrés en fonction de la request
|
||||||
|
"""
|
||||||
|
# cas 1 : etat justificatif
|
||||||
|
etat = requested.args.get("etat")
|
||||||
|
if etat is not None:
|
||||||
|
justificatifs_query = scass.filter_justificatifs_by_etat(
|
||||||
|
justificatifs_query, etat
|
||||||
|
)
|
||||||
|
|
||||||
|
# cas 2 : date de début
|
||||||
|
deb = requested.args.get("date_debut", "").replace(" ", "+")
|
||||||
|
deb: datetime = scu.is_iso_formated(deb, True)
|
||||||
|
|
||||||
|
# cas 3 : date de fin
|
||||||
|
fin = requested.args.get("date_fin", "").replace(" ", "+")
|
||||||
|
fin = scu.is_iso_formated(fin, True)
|
||||||
|
|
||||||
|
if (deb, fin) != (None, None):
|
||||||
|
justificatifs_query: Justificatif = scass.filter_by_date(
|
||||||
|
justificatifs_query, Justificatif, deb, fin
|
||||||
|
)
|
||||||
|
|
||||||
|
user_id = requested.args.get("user_id", False)
|
||||||
|
if user_id is not False:
|
||||||
|
justificatif_query: Justificatif = scass.filter_by_user_id(
|
||||||
|
justificatif_query, user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return justificatifs_query
|
@ -12,8 +12,6 @@ from operator import attrgetter
|
|||||||
from flask import g, request
|
from flask import g, request
|
||||||
from flask_json import as_json
|
from flask_json import as_json
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
import sqlalchemy as sa
|
|
||||||
from sqlalchemy.exc import IntegrityError
|
|
||||||
|
|
||||||
import app
|
import app
|
||||||
from app import db, log
|
from app import db, log
|
||||||
@ -25,7 +23,6 @@ from app.models import GroupDescr, Partition, Scolog
|
|||||||
from app.models.groups import group_membership
|
from app.models.groups import group_membership
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
from app.scodoc import sco_groups
|
from app.scodoc import sco_groups
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
@ -185,12 +182,10 @@ def set_etud_group(etudid: int, group_id: int):
|
|||||||
if etud.id not in {e.id for e in group.partition.formsemestre.etuds}:
|
if etud.id not in {e.id for e in group.partition.formsemestre.etuds}:
|
||||||
return json_error(404, "etud non inscrit au formsemestre du groupe")
|
return json_error(404, "etud non inscrit au formsemestre du groupe")
|
||||||
|
|
||||||
try:
|
sco_groups.change_etud_group_in_partition(
|
||||||
sco_groups.change_etud_group_in_partition(etudid, group)
|
etudid, group_id, group.partition.to_dict()
|
||||||
except ScoValueError as exc:
|
)
|
||||||
return json_error(404, exc.args[0])
|
|
||||||
except IntegrityError:
|
|
||||||
return json_error(404, "échec de l'enregistrement")
|
|
||||||
return {"group_id": group_id, "etudid": etudid}
|
return {"group_id": group_id, "etudid": etudid}
|
||||||
|
|
||||||
|
|
||||||
@ -249,25 +244,19 @@ def partition_remove_etud(partition_id: int, etudid: int):
|
|||||||
partition = query.first_or_404()
|
partition = query.first_or_404()
|
||||||
if not partition.formsemestre.etat:
|
if not partition.formsemestre.etat:
|
||||||
return json_error(403, "formsemestre verrouillé")
|
return json_error(403, "formsemestre verrouillé")
|
||||||
|
groups = (
|
||||||
db.session.execute(
|
GroupDescr.query.filter_by(partition_id=partition_id)
|
||||||
sa.text(
|
.join(group_membership)
|
||||||
"""DELETE FROM group_membership
|
.filter_by(etudid=etudid)
|
||||||
WHERE etudid=:etudid
|
|
||||||
and group_id IN (
|
|
||||||
SELECT id FROM group_descr WHERE partition_id = :partition_id
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
),
|
|
||||||
{"etudid": etudid, "partition_id": partition_id},
|
|
||||||
)
|
|
||||||
|
|
||||||
Scolog.logdb(
|
|
||||||
method="partition_remove_etud",
|
|
||||||
etudid=etud.id,
|
|
||||||
msg=f"Retrait de la partition {partition.partition_name}",
|
|
||||||
commit=False,
|
|
||||||
)
|
)
|
||||||
|
for group in groups:
|
||||||
|
group.etuds.remove(etud)
|
||||||
|
Scolog.logdb(
|
||||||
|
method="partition_remove_etud",
|
||||||
|
etudid=etud.id,
|
||||||
|
msg=f"Retrait du groupe {group.group_name} de {group.partition.partition_name}",
|
||||||
|
commit=True,
|
||||||
|
)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
# Update parcours
|
# Update parcours
|
||||||
partition.formsemestre.update_inscriptions_parcours_from_groups()
|
partition.formsemestre.update_inscriptions_parcours_from_groups()
|
||||||
@ -282,7 +271,7 @@ def partition_remove_etud(partition_id: int, etudid: int):
|
|||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoEtudChangeGroups)
|
@permission_required(Permission.ScoEtudChangeGroups)
|
||||||
@as_json
|
@as_json
|
||||||
def group_create(partition_id: int): # partition-group-create
|
def group_create(partition_id: int):
|
||||||
"""Création d'un groupe dans une partition
|
"""Création d'un groupe dans une partition
|
||||||
|
|
||||||
The request content type should be "application/json":
|
The request content type should be "application/json":
|
||||||
|
@ -35,7 +35,7 @@ def user_info(uid: int):
|
|||||||
"""
|
"""
|
||||||
Info sur un compte utilisateur scodoc
|
Info sur un compte utilisateur scodoc
|
||||||
"""
|
"""
|
||||||
user: User = db.session.get(User, uid)
|
user: User = User.query.get(uid)
|
||||||
if user is None:
|
if user is None:
|
||||||
return json_error(404, "user not found")
|
return json_error(404, "user not found")
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
|
@ -9,7 +9,7 @@ from flask import current_app, g, redirect, request, url_for
|
|||||||
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth
|
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth
|
||||||
import flask_login
|
import flask_login
|
||||||
|
|
||||||
from app import db, login
|
from app import login
|
||||||
from app.auth.models import User
|
from app.auth.models import User
|
||||||
from app.models.config import ScoDocSiteConfig
|
from app.models.config import ScoDocSiteConfig
|
||||||
from app.scodoc.sco_utils import json_error
|
from app.scodoc.sco_utils import json_error
|
||||||
@ -39,7 +39,7 @@ def basic_auth_error(status):
|
|||||||
@login.user_loader
|
@login.user_loader
|
||||||
def load_user(uid: str) -> User:
|
def load_user(uid: str) -> User:
|
||||||
"flask-login: accès à un utilisateur"
|
"flask-login: accès à un utilisateur"
|
||||||
return db.session.get(User, int(uid))
|
return User.query.get(int(uid))
|
||||||
|
|
||||||
|
|
||||||
@token_auth.verify_token
|
@token_auth.verify_token
|
||||||
|
@ -225,7 +225,7 @@ class User(UserMixin, db.Model):
|
|||||||
return None
|
return None
|
||||||
except (TypeError, KeyError):
|
except (TypeError, KeyError):
|
||||||
return None
|
return None
|
||||||
return db.session.get(User, user_id)
|
return User.query.get(user_id)
|
||||||
|
|
||||||
def to_dict(self, include_email=True):
|
def to_dict(self, include_email=True):
|
||||||
"""l'utilisateur comme un dict, avec des champs supplémentaires"""
|
"""l'utilisateur comme un dict, avec des champs supplémentaires"""
|
||||||
@ -376,9 +376,7 @@ class User(UserMixin, db.Model):
|
|||||||
"""
|
"""
|
||||||
if not isinstance(role, Role):
|
if not isinstance(role, Role):
|
||||||
raise ScoValueError("add_role: rôle invalide")
|
raise ScoValueError("add_role: rôle invalide")
|
||||||
user_role = UserRole(user=self, role=role, dept=dept)
|
self.user_roles.append(UserRole(user=self, role=role, dept=dept))
|
||||||
db.session.add(user_role)
|
|
||||||
self.user_roles.append(user_role)
|
|
||||||
|
|
||||||
def add_roles(self, roles: "list[Role]", dept: str):
|
def add_roles(self, roles: "list[Role]", dept: str):
|
||||||
"""Add roles to this user.
|
"""Add roles to this user.
|
||||||
|
@ -12,7 +12,6 @@ import datetime
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from flask import g, has_request_context, url_for
|
from flask import g, has_request_context, url_for
|
||||||
|
|
||||||
from app import db
|
|
||||||
from app.comp.res_but import ResultatsSemestreBUT
|
from app.comp.res_but import ResultatsSemestreBUT
|
||||||
from app.models import Evaluation, FormSemestre, Identite
|
from app.models import Evaluation, FormSemestre, Identite
|
||||||
from app.models.groups import GroupDescr
|
from app.models.groups import GroupDescr
|
||||||
@ -159,7 +158,7 @@ class BulletinBUT:
|
|||||||
[etud.id]
|
[etud.id]
|
||||||
].iterrows():
|
].iterrows():
|
||||||
if codes_cursus.code_ue_validant(ue_capitalisee.code):
|
if codes_cursus.code_ue_validant(ue_capitalisee.code):
|
||||||
ue = db.session.get(UniteEns, ue_capitalisee.ue_id) # XXX cacher ?
|
ue = UniteEns.query.get(ue_capitalisee.ue_id) # XXX cacher ?
|
||||||
# déjà capitalisé ? montre la meilleure
|
# déjà capitalisé ? montre la meilleure
|
||||||
if ue.acronyme in d:
|
if ue.acronyme in d:
|
||||||
moy_cap = d[ue.acronyme]["moyenne_num"] or 0.0
|
moy_cap = d[ue.acronyme]["moyenne_num"] or 0.0
|
||||||
|
@ -189,9 +189,7 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
|||||||
moy_ue = moy_ue.get("value", "-") if moy_ue is not None else "-"
|
moy_ue = moy_ue.get("value", "-") if moy_ue is not None else "-"
|
||||||
t = {
|
t = {
|
||||||
"titre": f"{ue_acronym} - {ue['titre']}",
|
"titre": f"{ue_acronym} - {ue['titre']}",
|
||||||
"moyenne": Paragraph(
|
"moyenne": Paragraph(f"""<para align=right><b>{moy_ue}</b></para>"""),
|
||||||
f"""<para align=right><b>{moy_ue or "-"}</b></para>"""
|
|
||||||
),
|
|
||||||
"_css_row_class": "note_bold",
|
"_css_row_class": "note_bold",
|
||||||
"_pdf_row_markup": ["b"],
|
"_pdf_row_markup": ["b"],
|
||||||
"_pdf_style": [
|
"_pdf_style": [
|
||||||
|
@ -14,14 +14,17 @@ Classe raccordant avec ScoDoc 7:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import collections
|
import collections
|
||||||
from operator import attrgetter
|
from typing import Union
|
||||||
|
|
||||||
from flask import g, url_for
|
from flask import g, url_for
|
||||||
|
|
||||||
from app import db, log
|
from app import db
|
||||||
|
from app import log
|
||||||
from app.comp.res_but import ResultatsSemestreBUT
|
from app.comp.res_but import ResultatsSemestreBUT
|
||||||
from app.comp.res_compat import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
|
|
||||||
|
from app.comp import res_sem
|
||||||
|
|
||||||
from app.models.but_refcomp import (
|
from app.models.but_refcomp import (
|
||||||
ApcAnneeParcours,
|
ApcAnneeParcours,
|
||||||
ApcCompetence,
|
ApcCompetence,
|
||||||
@ -34,6 +37,7 @@ from app.models import Scolog, ScolarAutorisationInscription
|
|||||||
from app.models.but_validations import (
|
from app.models.but_validations import (
|
||||||
ApcValidationAnnee,
|
ApcValidationAnnee,
|
||||||
ApcValidationRCUE,
|
ApcValidationRCUE,
|
||||||
|
RegroupementCoherentUE,
|
||||||
)
|
)
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
from app.models.formations import Formation
|
from app.models.formations import Formation
|
||||||
@ -41,8 +45,7 @@ from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
|||||||
from app.models.ues import UniteEns
|
from app.models.ues import UniteEns
|
||||||
from app.models.validations import ScolarFormSemestreValidation
|
from app.models.validations import ScolarFormSemestreValidation
|
||||||
from app.scodoc import codes_cursus as sco_codes
|
from app.scodoc import codes_cursus as sco_codes
|
||||||
from app.scodoc.codes_cursus import code_ue_validant, CODES_UE_VALIDES, UE_STANDARD
|
from app.scodoc.codes_cursus import RED, UE_STANDARD
|
||||||
|
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
|
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
|
||||||
|
|
||||||
@ -69,7 +72,6 @@ class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
|
|||||||
class EtudCursusBUT:
|
class EtudCursusBUT:
|
||||||
"""L'état de l'étudiant dans son cursus BUT
|
"""L'état de l'étudiant dans son cursus BUT
|
||||||
Liste des niveaux validés/à valider
|
Liste des niveaux validés/à valider
|
||||||
(utilisé pour le résumé sur la fiche étudiant)
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, etud: Identite, formation: Formation):
|
def __init__(self, etud: Identite, formation: Formation):
|
||||||
@ -101,8 +103,8 @@ class EtudCursusBUT:
|
|||||||
"Liste des inscriptions aux sem. de la formation, triées par indice et chronologie"
|
"Liste des inscriptions aux sem. de la formation, triées par indice et chronologie"
|
||||||
self.parcour: ApcParcours = self.inscriptions[-1].parcour
|
self.parcour: ApcParcours = self.inscriptions[-1].parcour
|
||||||
"Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
|
"Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
|
||||||
self.niveaux_by_annee: dict[int, list[ApcNiveau]] = {}
|
self.niveaux_by_annee = {}
|
||||||
"{ annee:int : liste des niveaux à valider }"
|
"{ annee : liste des niveaux à valider }"
|
||||||
self.niveaux: dict[int, ApcNiveau] = {}
|
self.niveaux: dict[int, ApcNiveau] = {}
|
||||||
"cache les niveaux"
|
"cache les niveaux"
|
||||||
for annee in (1, 2, 3):
|
for annee in (1, 2, 3):
|
||||||
@ -116,6 +118,21 @@ class EtudCursusBUT:
|
|||||||
self.niveaux.update(
|
self.niveaux.update(
|
||||||
{niveau.id: niveau for niveau in self.niveaux_by_annee[annee]}
|
{niveau.id: niveau for niveau in self.niveaux_by_annee[annee]}
|
||||||
)
|
)
|
||||||
|
# Probablement inutile:
|
||||||
|
# # Cherche les validations de jury enregistrées pour chaque niveau
|
||||||
|
# self.validations_by_niveau = collections.defaultdict(lambda: [])
|
||||||
|
# " { niveau_id : [ ApcValidationRCUE ] }"
|
||||||
|
# for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
||||||
|
# self.validations_by_niveau[validation_rcue.niveau().id].append(
|
||||||
|
# validation_rcue
|
||||||
|
# )
|
||||||
|
# self.validation_by_niveau = {
|
||||||
|
# niveau_id: sorted(
|
||||||
|
# validations, key=lambda v: sco_codes.BUT_CODES_ORDERED[v.code]
|
||||||
|
# )[0]
|
||||||
|
# for niveau_id, validations in self.validations_by_niveau.items()
|
||||||
|
# }
|
||||||
|
# "{ niveau_id : meilleure validation pour ce niveau }"
|
||||||
|
|
||||||
self.validation_par_competence_et_annee = {}
|
self.validation_par_competence_et_annee = {}
|
||||||
"""{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
|
"""{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
|
||||||
@ -128,8 +145,8 @@ class EtudCursusBUT:
|
|||||||
).get(validation_rcue.annee())
|
).get(validation_rcue.annee())
|
||||||
# prend la "meilleure" validation
|
# prend la "meilleure" validation
|
||||||
if (not previous_validation) or (
|
if (not previous_validation) or (
|
||||||
sco_codes.BUT_CODES_ORDER[validation_rcue.code]
|
sco_codes.BUT_CODES_ORDERED[validation_rcue.code]
|
||||||
> sco_codes.BUT_CODES_ORDER[previous_validation.code]
|
> sco_codes.BUT_CODES_ORDERED[previous_validation["code"]]
|
||||||
):
|
):
|
||||||
self.validation_par_competence_et_annee[niveau.competence.id][
|
self.validation_par_competence_et_annee[niveau.competence.id][
|
||||||
niveau.annee
|
niveau.annee
|
||||||
@ -189,28 +206,6 @@ class EtudCursusBUT:
|
|||||||
)
|
)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def competence_annee_has_niveau(self, competence_id: int, annee: str) -> bool:
|
|
||||||
"vrai si la compétence à un niveau dans cette annee ('BUT1') pour le parcour de cet etud"
|
|
||||||
# slow, utile pour affichage fiche
|
|
||||||
return annee in [n.annee for n in self.competences[competence_id].niveaux]
|
|
||||||
|
|
||||||
def load_validation_by_niveau(self) -> dict[int, list[ApcValidationRCUE]]:
|
|
||||||
"""Cherche les validations de jury enregistrées pour chaque niveau
|
|
||||||
Résultat: { niveau_id : [ ApcValidationRCUE ] }
|
|
||||||
meilleure validation pour ce niveau
|
|
||||||
"""
|
|
||||||
validations_by_niveau = collections.defaultdict(lambda: [])
|
|
||||||
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=self.etud):
|
|
||||||
validations_by_niveau[validation_rcue.niveau().id].append(validation_rcue)
|
|
||||||
validation_by_niveau = {
|
|
||||||
niveau_id: sorted(
|
|
||||||
validations, key=lambda v: sco_codes.BUT_CODES_ORDER[v.code]
|
|
||||||
)[0]
|
|
||||||
for niveau_id, validations in validations_by_niveau.items()
|
|
||||||
if validations
|
|
||||||
}
|
|
||||||
return validation_by_niveau
|
|
||||||
|
|
||||||
|
|
||||||
class FormSemestreCursusBUT:
|
class FormSemestreCursusBUT:
|
||||||
"""L'état des étudiants d'un formsemestre dans leur cursus BUT
|
"""L'état des étudiants d'un formsemestre dans leur cursus BUT
|
||||||
@ -251,9 +246,7 @@ class FormSemestreCursusBUT:
|
|||||||
parcour = None
|
parcour = None
|
||||||
else:
|
else:
|
||||||
if parcour_id not in self.parcours_by_id:
|
if parcour_id not in self.parcours_by_id:
|
||||||
self.parcours_by_id[parcour_id] = db.session.get(
|
self.parcours_by_id[parcour_id] = ApcParcours.query.get(parcour_id)
|
||||||
ApcParcours, parcour_id
|
|
||||||
)
|
|
||||||
parcour = self.parcours_by_id[parcour_id]
|
parcour = self.parcours_by_id[parcour_id]
|
||||||
|
|
||||||
return self.get_niveaux_parcours_by_annee(parcour)
|
return self.get_niveaux_parcours_by_annee(parcour)
|
||||||
@ -310,8 +303,8 @@ class FormSemestreCursusBUT:
|
|||||||
).get(validation_rcue.annee())
|
).get(validation_rcue.annee())
|
||||||
# prend la "meilleure" validation
|
# prend la "meilleure" validation
|
||||||
if (not previous_validation) or (
|
if (not previous_validation) or (
|
||||||
sco_codes.BUT_CODES_ORDER[validation_rcue.code]
|
sco_codes.BUT_CODES_ORDERED[validation_rcue.code]
|
||||||
> sco_codes.BUT_CODES_ORDER[previous_validation["code"]]
|
> sco_codes.BUT_CODES_ORDERED[previous_validation["code"]]
|
||||||
):
|
):
|
||||||
self.validation_par_competence_et_annee[niveau.competence.id][
|
self.validation_par_competence_et_annee[niveau.competence.id][
|
||||||
niveau.annee
|
niveau.annee
|
||||||
@ -347,8 +340,8 @@ class FormSemestreCursusBUT:
|
|||||||
).get(validation_rcue.annee())
|
).get(validation_rcue.annee())
|
||||||
# prend la "meilleure" validation
|
# prend la "meilleure" validation
|
||||||
if (not previous_validation) or (
|
if (not previous_validation) or (
|
||||||
sco_codes.BUT_CODES_ORDER[validation_rcue.code]
|
sco_codes.BUT_CODES_ORDERED[validation_rcue.code]
|
||||||
> sco_codes.BUT_CODES_ORDER[previous_validation["code"]]
|
> sco_codes.BUT_CODES_ORDERED[previous_validation["code"]]
|
||||||
):
|
):
|
||||||
self.validation_par_competence_et_annee[niveau.competence.id][
|
self.validation_par_competence_et_annee[niveau.competence.id][
|
||||||
niveau.annee
|
niveau.annee
|
||||||
@ -365,66 +358,6 @@ class FormSemestreCursusBUT:
|
|||||||
"cache { competence_id : competence }"
|
"cache { competence_id : competence }"
|
||||||
|
|
||||||
|
|
||||||
def but_ects_valides(etud: Identite, referentiel_competence_id: int) -> float:
|
|
||||||
"""Nombre d'ECTS validés par etud dans le BUT de référentiel indiqué.
|
|
||||||
Ne prend que les UE associées à des niveaux de compétences,
|
|
||||||
et ne les compte qu'une fois même en cas de redoublement avec re-validation.
|
|
||||||
"""
|
|
||||||
validations = (
|
|
||||||
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
|
|
||||||
.filter(ScolarFormSemestreValidation.ue_id != None)
|
|
||||||
.join(UniteEns)
|
|
||||||
.join(ApcNiveau)
|
|
||||||
.join(ApcCompetence)
|
|
||||||
.filter_by(referentiel_id=referentiel_competence_id)
|
|
||||||
)
|
|
||||||
|
|
||||||
ects_dict = {}
|
|
||||||
for v in validations:
|
|
||||||
key = (v.ue.semestre_idx, v.ue.niveau_competence.id)
|
|
||||||
if v.code in CODES_UE_VALIDES:
|
|
||||||
ects_dict[key] = v.ue.ects
|
|
||||||
|
|
||||||
return sum(ects_dict.values()) if ects_dict else 0.0
|
|
||||||
|
|
||||||
|
|
||||||
def etud_ues_de_but1_non_validees(
|
|
||||||
etud: Identite, formation: Formation, parcour: ApcParcours
|
|
||||||
) -> list[UniteEns]:
|
|
||||||
"""Liste des UEs de S1 et S2 non validées, dans son parcours"""
|
|
||||||
# Les UEs avec décisions, dans les S1 ou S2 d'une formation de même code:
|
|
||||||
validations = (
|
|
||||||
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
|
|
||||||
.filter(ScolarFormSemestreValidation.ue_id != None)
|
|
||||||
.join(UniteEns)
|
|
||||||
.filter(db.or_(UniteEns.semestre_idx == 1, UniteEns.semestre_idx == 2))
|
|
||||||
.join(Formation)
|
|
||||||
.filter_by(formation_code=formation.formation_code)
|
|
||||||
)
|
|
||||||
codes_validations_by_ue_code = collections.defaultdict(list)
|
|
||||||
for v in validations:
|
|
||||||
codes_validations_by_ue_code[v.ue.ue_code].append(v.code)
|
|
||||||
|
|
||||||
# Les UEs du parcours en S1 et S2:
|
|
||||||
ues = formation.query_ues_parcour(parcour).filter(
|
|
||||||
db.or_(UniteEns.semestre_idx == 1, UniteEns.semestre_idx == 2)
|
|
||||||
)
|
|
||||||
# Liste triée des ues non validées
|
|
||||||
return sorted(
|
|
||||||
[
|
|
||||||
ue
|
|
||||||
for ue in ues
|
|
||||||
if not any(
|
|
||||||
(
|
|
||||||
code_ue_validant(code)
|
|
||||||
for code in codes_validations_by_ue_code[ue.ue_code]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
],
|
|
||||||
key=attrgetter("numero", "acronyme"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def formsemestre_warning_apc_setup(
|
def formsemestre_warning_apc_setup(
|
||||||
formsemestre: FormSemestre, res: ResultatsSemestreBUT
|
formsemestre: FormSemestre, res: ResultatsSemestreBUT
|
||||||
) -> str:
|
) -> str:
|
||||||
@ -480,122 +413,3 @@ def formsemestre_warning_apc_setup(
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def ue_associee_au_niveau_du_parcours(
|
|
||||||
ues_possibles: list[UniteEns], niveau: ApcNiveau, sem_name: str = "S"
|
|
||||||
) -> UniteEns:
|
|
||||||
"L'UE associée à ce niveau, ou None"
|
|
||||||
ues = [ue for ue in ues_possibles if ue.niveau_competence_id == niveau.id]
|
|
||||||
if len(ues) > 1:
|
|
||||||
# plusieurs UEs associées à ce niveau: élimine celles sans parcours
|
|
||||||
ues_pair_avec_parcours = [ue for ue in ues if ue.parcours]
|
|
||||||
if ues_pair_avec_parcours:
|
|
||||||
ues = ues_pair_avec_parcours
|
|
||||||
if len(ues) > 1:
|
|
||||||
log(f"_niveau_ues: {len(ues)} associées au niveau {niveau} / {sem_name}")
|
|
||||||
return ues[0] if ues else None
|
|
||||||
|
|
||||||
|
|
||||||
def parcour_formation_competences(parcour: ApcParcours, formation: Formation) -> list:
|
|
||||||
"""
|
|
||||||
[
|
|
||||||
{
|
|
||||||
'competence' : ApcCompetence,
|
|
||||||
'niveaux' : {
|
|
||||||
1 : { ... },
|
|
||||||
2 : { ... },
|
|
||||||
3 : {
|
|
||||||
'niveau' : ApcNiveau,
|
|
||||||
'ue_impair' : UniteEns, # actuellement associée
|
|
||||||
'ues_impair' : list[UniteEns], # choix possibles
|
|
||||||
'ue_pair' : UniteEns,
|
|
||||||
'ues_pair' : list[UniteEns],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
"""
|
|
||||||
refcomp: ApcReferentielCompetences = formation.referentiel_competence
|
|
||||||
|
|
||||||
def _niveau_ues(competence: ApcCompetence, annee: int) -> dict:
|
|
||||||
"""niveau et ues pour cette compétence de cette année du parcours.
|
|
||||||
Si parcour est None, les niveaux du tronc commun
|
|
||||||
"""
|
|
||||||
if parcour is not None:
|
|
||||||
# L'étudiant est inscrit à un parcours: cherche les niveaux
|
|
||||||
niveaux = ApcNiveau.niveaux_annee_de_parcours(
|
|
||||||
parcour, annee, competence=competence
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# sans parcours, on cherche les niveaux du Tronc Commun de cette année
|
|
||||||
niveaux = [
|
|
||||||
niveau
|
|
||||||
for niveau in refcomp.get_niveaux_by_parcours(annee)[1]["TC"]
|
|
||||||
if niveau.competence_id == competence.id
|
|
||||||
]
|
|
||||||
|
|
||||||
if len(niveaux) > 0:
|
|
||||||
if len(niveaux) > 1:
|
|
||||||
log(
|
|
||||||
f"""_niveau_ues: plus d'un niveau pour {competence}
|
|
||||||
annee {annee} {("parcours " + parcour.code) if parcour else ""}"""
|
|
||||||
)
|
|
||||||
niveau = niveaux[0]
|
|
||||||
elif len(niveaux) == 0:
|
|
||||||
return {
|
|
||||||
"niveau": None,
|
|
||||||
"ue_pair": None,
|
|
||||||
"ue_impair": None,
|
|
||||||
"ues_pair": [],
|
|
||||||
"ues_impair": [],
|
|
||||||
}
|
|
||||||
# Toutes les UEs de la formation dans ce parcours ou tronc commun
|
|
||||||
ues = [
|
|
||||||
ue
|
|
||||||
for ue in formation.ues
|
|
||||||
if (
|
|
||||||
(not ue.parcours)
|
|
||||||
or (parcour is not None and (parcour.id in (p.id for p in ue.parcours)))
|
|
||||||
)
|
|
||||||
and ue.type == UE_STANDARD
|
|
||||||
]
|
|
||||||
ues_pair_possibles = [ue for ue in ues if ue.semestre_idx == (2 * annee)]
|
|
||||||
ues_impair_possibles = [ue for ue in ues if ue.semestre_idx == (2 * annee - 1)]
|
|
||||||
|
|
||||||
# UE associée au niveau dans ce parcours
|
|
||||||
ue_pair = ue_associee_au_niveau_du_parcours(
|
|
||||||
ues_pair_possibles, niveau, f"S{2*annee}"
|
|
||||||
)
|
|
||||||
ue_impair = ue_associee_au_niveau_du_parcours(
|
|
||||||
ues_impair_possibles, niveau, f"S{2*annee-1}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"niveau": niveau,
|
|
||||||
"ue_pair": ue_pair,
|
|
||||||
"ues_pair": [
|
|
||||||
ue
|
|
||||||
for ue in ues_pair_possibles
|
|
||||||
if (not ue.niveau_competence) or ue.niveau_competence.id == niveau.id
|
|
||||||
],
|
|
||||||
"ue_impair": ue_impair,
|
|
||||||
"ues_impair": [
|
|
||||||
ue
|
|
||||||
for ue in ues_impair_possibles
|
|
||||||
if (not ue.niveau_competence) or ue.niveau_competence.id == niveau.id
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
competences = [
|
|
||||||
{
|
|
||||||
"competence": competence,
|
|
||||||
"niveaux": {annee: _niveau_ues(competence, annee) for annee in (1, 2, 3)},
|
|
||||||
}
|
|
||||||
for competence in (
|
|
||||||
parcour.query_competences()
|
|
||||||
if parcour
|
|
||||||
else refcomp.competences.order_by(ApcCompetence.numero)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
return competences
|
|
||||||
|
1140
app/but/jury_but.py
1140
app/but/jury_but.py
File diff suppressed because it is too large
Load Diff
@ -12,7 +12,6 @@ from openpyxl.styles import Font, Border, Side, Alignment, PatternFill
|
|||||||
|
|
||||||
from app import log
|
from app import log
|
||||||
from app.but import jury_but
|
from app.but import jury_but
|
||||||
from app.but.cursus_but import but_ects_valides
|
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
from app.models.formsemestre import FormSemestre
|
from app.models.formsemestre import FormSemestre
|
||||||
from app.scodoc.gen_tables import GenTable
|
from app.scodoc.gen_tables import GenTable
|
||||||
@ -110,11 +109,6 @@ def pvjury_table_but(
|
|||||||
"""
|
"""
|
||||||
# remplace pour le BUT la fonction sco_pv_forms.pvjury_table
|
# remplace pour le BUT la fonction sco_pv_forms.pvjury_table
|
||||||
annee_but = (formsemestre.semestre_id + 1) // 2
|
annee_but = (formsemestre.semestre_id + 1) // 2
|
||||||
referentiel_competence_id = formsemestre.formation.referentiel_competence_id
|
|
||||||
if referentiel_competence_id is None:
|
|
||||||
raise ScoValueError(
|
|
||||||
"pas de référentiel de compétences associé à la formation de ce semestre !"
|
|
||||||
)
|
|
||||||
titles = {
|
titles = {
|
||||||
"nom": "Code" if anonymous else "Nom",
|
"nom": "Code" if anonymous else "Nom",
|
||||||
"cursus": "Cursus",
|
"cursus": "Cursus",
|
||||||
@ -159,7 +153,7 @@ def pvjury_table_but(
|
|||||||
etudid=etud.id,
|
etudid=etud.id,
|
||||||
),
|
),
|
||||||
"cursus": _descr_cursus_but(etud),
|
"cursus": _descr_cursus_but(etud),
|
||||||
"ects": f"""{deca.ects_annee():g}<br><br>Tot. {but_ects_valides(etud, referentiel_competence_id ):g}""",
|
"ects": f"{deca.formsemestre_ects():g}",
|
||||||
"ues": deca.descr_ues_validation(line_sep=line_sep) if deca else "-",
|
"ues": deca.descr_ues_validation(line_sep=line_sep) if deca else "-",
|
||||||
"niveaux": deca.descr_niveaux_validation(line_sep=line_sep)
|
"niveaux": deca.descr_niveaux_validation(line_sep=line_sep)
|
||||||
if deca
|
if deca
|
||||||
|
@ -48,9 +48,9 @@ def _get_jury_but_etud_result(
|
|||||||
# --- Les RCUEs
|
# --- Les RCUEs
|
||||||
rcue_list = []
|
rcue_list = []
|
||||||
if deca:
|
if deca:
|
||||||
for dec_rcue in deca.get_decisions_rcues_annee():
|
for rcue in deca.rcues_annee:
|
||||||
rcue = dec_rcue.rcue
|
dec_rcue = deca.dec_rcue_by_ue.get(rcue.ue_1.id)
|
||||||
if rcue.complete: # n'exporte que les RCUEs complets
|
if dec_rcue is not None: # None si l'UE n'est pas associée à un niveau
|
||||||
dec_ue1 = deca.decisions_ues[rcue.ue_1.id]
|
dec_ue1 = deca.decisions_ues[rcue.ue_1.id]
|
||||||
dec_ue2 = deca.decisions_ues[rcue.ue_2.id]
|
dec_ue2 = deca.decisions_ues[rcue.ue_2.id]
|
||||||
rcue_dict = {
|
rcue_dict = {
|
||||||
|
@ -6,22 +6,24 @@
|
|||||||
|
|
||||||
"""Jury BUT: calcul des décisions de jury annuelles "automatiques"
|
"""Jury BUT: calcul des décisions de jury annuelles "automatiques"
|
||||||
"""
|
"""
|
||||||
from flask import g, url_for
|
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.but import jury_but
|
from app.but import jury_but
|
||||||
from app.models import Identite, FormSemestre, ScolarNews
|
from app.models.etudiants import Identite
|
||||||
|
from app.models.formsemestre import FormSemestre
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
|
|
||||||
|
|
||||||
def formsemestre_validation_auto_but(
|
def formsemestre_validation_auto_but(
|
||||||
formsemestre: FormSemestre, only_adm: bool = True
|
formsemestre: FormSemestre, only_adm: bool = True, no_overwrite: bool = True
|
||||||
) -> int:
|
) -> int:
|
||||||
"""Calcul automatique des décisions de jury sur une "année" BUT.
|
"""Calcul automatique des décisions de jury sur une "année" BUT.
|
||||||
|
|
||||||
- N'enregistre jamais de décisions de l'année scolaire précédente, même
|
- N'enregistre jamais de décisions de l'année scolaire précédente, même
|
||||||
si on a des RCUE "à cheval".
|
si on a des RCUE "à cheval".
|
||||||
|
- Ne modifie jamais de décisions déjà enregistrées (sauf si no_overwrite est faux,
|
||||||
|
ce qui est utilisé pour certains tests unitaires).
|
||||||
- Normalement, only_adm est True et on n'enregistre que les décisions validantes
|
- Normalement, only_adm est True et on n'enregistre que les décisions validantes
|
||||||
de droit: ADM ou CMP.
|
de droit: ADM ou CMP.
|
||||||
En revanche, si only_adm est faux, on enregistre la première décision proposée par ScoDoc
|
En revanche, si only_adm est faux, on enregistre la première décision proposée par ScoDoc
|
||||||
@ -36,17 +38,9 @@ def formsemestre_validation_auto_but(
|
|||||||
for etudid in formsemestre.etuds_inscriptions:
|
for etudid in formsemestre.etuds_inscriptions:
|
||||||
etud = Identite.get_etud(etudid)
|
etud = Identite.get_etud(etudid)
|
||||||
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
|
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
|
||||||
nb_etud_modif += deca.record_all(only_validantes=only_adm)
|
nb_etud_modif += deca.record_all(
|
||||||
|
no_overwrite=no_overwrite, only_validantes=only_adm
|
||||||
|
)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
ScolarNews.add(
|
|
||||||
typ=ScolarNews.NEWS_JURY,
|
|
||||||
obj=formsemestre.id,
|
|
||||||
text=f"""Calcul jury automatique du semestre {formsemestre.html_link_status()}""",
|
|
||||||
url=url_for(
|
|
||||||
"notes.formsemestre_status",
|
|
||||||
scodoc_dept=g.scodoc_dept,
|
|
||||||
formsemestre_id=formsemestre.id,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return nb_etud_modif
|
return nb_etud_modif
|
||||||
|
@ -31,11 +31,9 @@ from app.models import (
|
|||||||
UniteEns,
|
UniteEns,
|
||||||
ScolarAutorisationInscription,
|
ScolarAutorisationInscription,
|
||||||
ScolarFormSemestreValidation,
|
ScolarFormSemestreValidation,
|
||||||
ScolarNews,
|
|
||||||
)
|
)
|
||||||
from app.models.config import ScoDocSiteConfig
|
from app.models.config import ScoDocSiteConfig
|
||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
from app.scodoc import codes_cursus as sco_codes
|
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
@ -93,25 +91,35 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
|
|||||||
<div class="titre">RCUE</div>
|
<div class="titre">RCUE</div>
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
for dec_rcue in deca.get_decisions_rcues_annee():
|
for niveau in deca.niveaux_competences:
|
||||||
rcue = dec_rcue.rcue
|
|
||||||
niveau = rcue.niveau
|
|
||||||
H.append(
|
H.append(
|
||||||
f"""<div class="but_niveau_titre">
|
f"""<div class="but_niveau_titre">
|
||||||
<div title="{niveau.competence.titre_long}">{niveau.competence.titre}</div>
|
<div title="{niveau.competence.titre_long}">{niveau.competence.titre}</div>
|
||||||
</div>"""
|
</div>"""
|
||||||
)
|
)
|
||||||
ue_impair, ue_pair = rcue.ue_1, rcue.ue_2
|
dec_rcue = deca.decisions_rcue_by_niveau.get(niveau.id) # peut être None
|
||||||
|
ues = [
|
||||||
|
ue
|
||||||
|
for ue in deca.ues_impair
|
||||||
|
if ue.niveau_competence and ue.niveau_competence.id == niveau.id
|
||||||
|
]
|
||||||
|
ue_impair = ues[0] if ues else None
|
||||||
|
ues = [
|
||||||
|
ue
|
||||||
|
for ue in deca.ues_pair
|
||||||
|
if ue.niveau_competence and ue.niveau_competence.id == niveau.id
|
||||||
|
]
|
||||||
|
ue_pair = ues[0] if ues else None
|
||||||
# Les UEs à afficher,
|
# Les UEs à afficher,
|
||||||
# qui
|
# qui seront toujours en readonly sur le formsemestre de l'année précédente du redoublant
|
||||||
ues_ro = [
|
ues_ro = [
|
||||||
(
|
(
|
||||||
ue_impair,
|
ue_impair,
|
||||||
rcue.ue_cur_impair is None,
|
(deca.a_cheval and deca.formsemestre_id != deca.formsemestre_impair.id),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
ue_pair,
|
ue_pair,
|
||||||
rcue.ue_cur_pair is None,
|
deca.a_cheval and deca.formsemestre_id != deca.formsemestre_pair.id,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
# Ordonne selon les dates des 2 semestres considérés:
|
# Ordonne selon les dates des 2 semestres considérés:
|
||||||
@ -145,22 +153,17 @@ def _gen_but_select(
|
|||||||
code_valide: str,
|
code_valide: str,
|
||||||
disabled: bool = False,
|
disabled: bool = False,
|
||||||
klass: str = "",
|
klass: str = "",
|
||||||
data: dict = None,
|
data: dict = {},
|
||||||
code_valide_label: str = "",
|
|
||||||
) -> str:
|
) -> str:
|
||||||
"Le menu html select avec les codes"
|
"Le menu html select avec les codes"
|
||||||
# if disabled: # mauvaise idée car le disabled est traité en JS
|
# if disabled: # mauvaise idée car le disabled est traité en JS
|
||||||
# return f"""<div class="but_code {klass}">{code_valide}</div>"""
|
# return f"""<div class="but_code {klass}">{code_valide}</div>"""
|
||||||
data = data or {}
|
|
||||||
options_htm = "\n".join(
|
options_htm = "\n".join(
|
||||||
[
|
[
|
||||||
f"""<option value="{code}"
|
f"""<option value="{code}"
|
||||||
{'selected' if code == code_valide else ''}
|
{'selected' if code == code_valide else ''}
|
||||||
class="{'recorded' if code == code_valide else ''}"
|
class="{'recorded' if code == code_valide else ''}"
|
||||||
>{code
|
>{code}</option>"""
|
||||||
if ((code != code_valide) or not code_valide_label)
|
|
||||||
else code_valide_label
|
|
||||||
}</option>"""
|
|
||||||
for code in codes
|
for code in codes
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@ -199,54 +202,20 @@ def _gen_but_niveau_ue(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
elif dec_ue.formsemestre is None:
|
|
||||||
# Validation d'UE antérieure (semestre hors année scolaire courante)
|
|
||||||
if dec_ue.validation:
|
|
||||||
moy_ue_str = f"""<span>{scu.fmt_note(dec_ue.validation.moy_ue)}</span>"""
|
|
||||||
scoplement = f"""<div class="scoplement">
|
|
||||||
<div>
|
|
||||||
<b>UE {ue.acronyme} antérieure </b>
|
|
||||||
<span>validée {dec_ue.validation.code}
|
|
||||||
le {dec_ue.validation.event_date.strftime("%d/%m/%Y")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>Non reprise dans l'année en cours</div>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
else:
|
|
||||||
moy_ue_str = """<span>-</span>"""
|
|
||||||
scoplement = """<div class="scoplement">
|
|
||||||
<div>
|
|
||||||
<b>Pas d'UE en cours ou validée dans cette compétence de ce côté.</b>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
else:
|
else:
|
||||||
moy_ue_str = f"""<span>{scu.fmt_note(dec_ue.moy_ue)}</span>"""
|
moy_ue_str = f"""<span>{scu.fmt_note(dec_ue.moy_ue)}</span>"""
|
||||||
if dec_ue.code_valide:
|
if dec_ue.code_valide:
|
||||||
date_str = (
|
|
||||||
f"""enregistré le {dec_ue.validation.event_date.strftime("%d/%m/%Y")}
|
|
||||||
à {dec_ue.validation.event_date.strftime("%Hh%M")}
|
|
||||||
"""
|
|
||||||
if dec_ue.validation and dec_ue.validation.event_date
|
|
||||||
else ""
|
|
||||||
)
|
|
||||||
scoplement = f"""<div class="scoplement">
|
scoplement = f"""<div class="scoplement">
|
||||||
<div>Code {dec_ue.code_valide} {date_str}
|
<div>Code {dec_ue.code_valide} enregistré le {dec_ue.validation.event_date.strftime("%d/%m/%Y")}
|
||||||
|
à {dec_ue.validation.event_date.strftime("%Hh%M")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
else:
|
else:
|
||||||
scoplement = ""
|
scoplement = ""
|
||||||
|
|
||||||
ue_class = "" # 'recorded' if dec_ue.code_valide is not None else ''
|
return f"""<div class="but_niveau_ue {
|
||||||
if dec_ue.code_valide is not None and dec_ue.codes:
|
'recorded' if dec_ue.code_valide is not None else ''}
|
||||||
if dec_ue.code_valide == dec_ue.codes[0]:
|
|
||||||
ue_class = "recorded"
|
|
||||||
else:
|
|
||||||
ue_class = "recorded_different"
|
|
||||||
|
|
||||||
return f"""<div class="but_niveau_ue {ue_class}
|
|
||||||
{'annee_prec' if annee_prec else ''}
|
{'annee_prec' if annee_prec else ''}
|
||||||
">
|
">
|
||||||
<div title="{ue.titre}">{ue.acronyme}</div>
|
<div title="{ue.titre}">{ue.acronyme}</div>
|
||||||
@ -267,7 +236,7 @@ def _gen_but_niveau_ue(
|
|||||||
|
|
||||||
|
|
||||||
def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str:
|
def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str:
|
||||||
if dec_rcue is None or not dec_rcue.rcue.complete:
|
if dec_rcue is None:
|
||||||
return """
|
return """
|
||||||
<div class="but_niveau_rcue niveau_vide with_scoplement">
|
<div class="but_niveau_rcue niveau_vide with_scoplement">
|
||||||
<div></div>
|
<div></div>
|
||||||
@ -275,25 +244,13 @@ def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str:
|
|||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
code_propose_menu = dec_rcue.code_valide # le code enregistré
|
scoplement = (
|
||||||
code_valide_label = code_propose_menu
|
f"""<div class="scoplement">{
|
||||||
if dec_rcue.validation:
|
dec_rcue.validation.to_html()
|
||||||
if dec_rcue.code_valide == dec_rcue.codes[0]:
|
}</div>"""
|
||||||
descr_validation = dec_rcue.validation.html()
|
if dec_rcue.validation
|
||||||
else: # on une validation enregistrée différence de celle proposée
|
else ""
|
||||||
descr_validation = f"""Décision recommandée: <b>{dec_rcue.codes[0]}.</b>
|
)
|
||||||
Il y avait {dec_rcue.validation.html()}"""
|
|
||||||
if (
|
|
||||||
sco_codes.BUT_CODES_ORDER[dec_rcue.codes[0]]
|
|
||||||
> sco_codes.BUT_CODES_ORDER[dec_rcue.code_valide]
|
|
||||||
):
|
|
||||||
code_propose_menu = dec_rcue.codes[0]
|
|
||||||
code_valide_label = (
|
|
||||||
f"{dec_rcue.codes[0]} (actuel {dec_rcue.code_valide})"
|
|
||||||
)
|
|
||||||
scoplement = f"""<div class="scoplement">{descr_validation}</div>"""
|
|
||||||
else:
|
|
||||||
scoplement = "" # "pas de validation"
|
|
||||||
|
|
||||||
# Déjà enregistré ?
|
# Déjà enregistré ?
|
||||||
niveau_rcue_class = ""
|
niveau_rcue_class = ""
|
||||||
@ -313,11 +270,10 @@ def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str:
|
|||||||
<div class="but_code">
|
<div class="but_code">
|
||||||
{_gen_but_select("code_rcue_"+str(niveau.id),
|
{_gen_but_select("code_rcue_"+str(niveau.id),
|
||||||
dec_rcue.codes,
|
dec_rcue.codes,
|
||||||
code_propose_menu,
|
dec_rcue.code_valide,
|
||||||
disabled=True,
|
disabled=True,
|
||||||
klass="manual code_rcue",
|
klass="manual code_rcue",
|
||||||
data = { "niveau_id" : str(niveau.id)},
|
data = { "niveau_id" : str(niveau.id)}
|
||||||
code_valide_label = code_valide_label,
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -395,16 +351,6 @@ def jury_but_semestriel(
|
|||||||
flash(
|
flash(
|
||||||
f"autorisation de passage en S{formsemestre.semestre_id + 1} annulée"
|
f"autorisation de passage en S{formsemestre.semestre_id + 1} annulée"
|
||||||
)
|
)
|
||||||
ScolarNews.add(
|
|
||||||
typ=ScolarNews.NEWS_JURY,
|
|
||||||
obj=formsemestre.id,
|
|
||||||
text=f"""Saisie décision jury dans {formsemestre.html_link_status()}""",
|
|
||||||
url=url_for(
|
|
||||||
"notes.formsemestre_status",
|
|
||||||
scodoc_dept=g.scodoc_dept,
|
|
||||||
formsemestre_id=formsemestre.id,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return flask.redirect(
|
return flask.redirect(
|
||||||
url_for(
|
url_for(
|
||||||
"notes.formsemestre_validation_but",
|
"notes.formsemestre_validation_but",
|
||||||
@ -448,7 +394,7 @@ def jury_but_semestriel(
|
|||||||
{warning}
|
{warning}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="post" class="jury_but_box" id="jury_but">
|
<form method="post" id="jury_but">
|
||||||
""",
|
""",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1,67 +0,0 @@
|
|||||||
##############################################################################
|
|
||||||
# ScoDoc
|
|
||||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
|
||||||
# See LICENSE
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
"""Jury édition manuelle des décisions (correction d'erreurs, parcours hors normes)
|
|
||||||
|
|
||||||
Non spécifique au BUT.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from flask import render_template
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
from app.models import (
|
|
||||||
ApcValidationAnnee,
|
|
||||||
ApcValidationRCUE,
|
|
||||||
Identite,
|
|
||||||
UniteEns,
|
|
||||||
ScolarAutorisationInscription,
|
|
||||||
ScolarFormSemestreValidation,
|
|
||||||
)
|
|
||||||
from app.views import ScoData
|
|
||||||
|
|
||||||
|
|
||||||
def jury_delete_manual(etud: Identite):
|
|
||||||
"""Vue (réservée au chef de dept.)
|
|
||||||
présentant *toutes* les décisions de jury concernant cet étudiant
|
|
||||||
et permettant de les supprimer une à une.
|
|
||||||
"""
|
|
||||||
sem_vals = ScolarFormSemestreValidation.query.filter_by(
|
|
||||||
etudid=etud.id, ue_id=None
|
|
||||||
).order_by(ScolarFormSemestreValidation.event_date)
|
|
||||||
ue_vals = (
|
|
||||||
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
|
|
||||||
.join(UniteEns)
|
|
||||||
.order_by(
|
|
||||||
sa.extract("year", ScolarFormSemestreValidation.event_date),
|
|
||||||
UniteEns.semestre_idx,
|
|
||||||
UniteEns.numero,
|
|
||||||
UniteEns.acronyme,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
autorisations = ScolarAutorisationInscription.query.filter_by(
|
|
||||||
etudid=etud.id
|
|
||||||
).order_by(
|
|
||||||
ScolarAutorisationInscription.semestre_id, ScolarAutorisationInscription.date
|
|
||||||
)
|
|
||||||
rcue_vals = (
|
|
||||||
ApcValidationRCUE.query.filter_by(etudid=etud.id)
|
|
||||||
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id)
|
|
||||||
.order_by(UniteEns.semestre_idx, UniteEns.numero, ApcValidationRCUE.date)
|
|
||||||
)
|
|
||||||
annee_but_vals = ApcValidationAnnee.query.filter_by(etudid=etud.id).order_by(
|
|
||||||
ApcValidationAnnee.ordre, ApcValidationAnnee.date
|
|
||||||
)
|
|
||||||
return render_template(
|
|
||||||
"jury/jury_delete_manual.j2",
|
|
||||||
etud=etud,
|
|
||||||
sem_vals=sem_vals,
|
|
||||||
ue_vals=ue_vals,
|
|
||||||
autorisations=autorisations,
|
|
||||||
rcue_vals=rcue_vals,
|
|
||||||
annee_but_vals=annee_but_vals,
|
|
||||||
sco=ScoData(),
|
|
||||||
title=f"Toutes les décisions de jury enregistrées pour {etud.html_link_fiche()}",
|
|
||||||
)
|
|
253
app/but/rcue.py
253
app/but/rcue.py
@ -1,253 +0,0 @@
|
|||||||
##############################################################################
|
|
||||||
# ScoDoc
|
|
||||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
|
||||||
# See LICENSE
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
"""Jury BUT: un RCUE, ou Regroupe Cohérent d'UEs
|
|
||||||
"""
|
|
||||||
from typing import Union
|
|
||||||
from flask_sqlalchemy.query import Query
|
|
||||||
|
|
||||||
from app.comp.res_but import ResultatsSemestreBUT
|
|
||||||
from app.models import (
|
|
||||||
ApcNiveau,
|
|
||||||
ApcValidationRCUE,
|
|
||||||
Identite,
|
|
||||||
ScolarFormSemestreValidation,
|
|
||||||
UniteEns,
|
|
||||||
)
|
|
||||||
from app.scodoc import codes_cursus
|
|
||||||
from app.scodoc.codes_cursus import BUT_CODES_ORDER
|
|
||||||
|
|
||||||
|
|
||||||
class RegroupementCoherentUE:
|
|
||||||
"""Le regroupement cohérent d'UE, dans la terminologie du BUT, est le couple d'UEs
|
|
||||||
de la même année (BUT1,2,3) liées au *même niveau de compétence*.
|
|
||||||
|
|
||||||
La moyenne (10/20) au RCUE déclenche la compensation des UEs.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
etud: Identite,
|
|
||||||
niveau: ApcNiveau,
|
|
||||||
res_pair: ResultatsSemestreBUT,
|
|
||||||
res_impair: ResultatsSemestreBUT,
|
|
||||||
semestre_id_impair: int,
|
|
||||||
cur_ues_pair: list[UniteEns],
|
|
||||||
cur_ues_impair: list[UniteEns],
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
res_pair, res_impair: résultats des formsemestre de l'année en cours, ou None
|
|
||||||
cur_ues_pair, cur_ues_impair: ues auxquelles l'étudiant est inscrit cette année
|
|
||||||
"""
|
|
||||||
self.semestre_id_impair = semestre_id_impair
|
|
||||||
self.semestre_id_pair = semestre_id_impair + 1
|
|
||||||
self.etud: Identite = etud
|
|
||||||
self.niveau: ApcNiveau = niveau
|
|
||||||
"Le niveau de compétences de ce RCUE"
|
|
||||||
# Chercher l'UE en cours pour pair, impair
|
|
||||||
# une UE à laquelle l'étudiant est inscrit (non dispensé)
|
|
||||||
# dans l'un des formsemestre en cours
|
|
||||||
ues = [ue for ue in cur_ues_pair if ue.niveau_competence_id == niveau.id]
|
|
||||||
self.ue_cur_pair = ues[0] if ues else None
|
|
||||||
"UE paire en cours"
|
|
||||||
ues = [ue for ue in cur_ues_impair if ue.niveau_competence_id == niveau.id]
|
|
||||||
self.ue_cur_impair = ues[0] if ues else None
|
|
||||||
"UE impaire en cours"
|
|
||||||
|
|
||||||
self.validation_ue_cur_pair = (
|
|
||||||
ScolarFormSemestreValidation.query.filter_by(
|
|
||||||
etudid=etud.id,
|
|
||||||
formsemestre_id=res_pair.formsemestre.id,
|
|
||||||
ue_id=self.ue_cur_pair.id,
|
|
||||||
).first()
|
|
||||||
if self.ue_cur_pair
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
self.validation_ue_cur_impair = (
|
|
||||||
ScolarFormSemestreValidation.query.filter_by(
|
|
||||||
etudid=etud.id,
|
|
||||||
formsemestre_id=res_impair.formsemestre.id,
|
|
||||||
ue_id=self.ue_cur_impair.id,
|
|
||||||
).first()
|
|
||||||
if self.ue_cur_impair
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
|
|
||||||
# Autres validations pour l'UE paire
|
|
||||||
self.validation_ue_best_pair = best_autre_ue_validation(
|
|
||||||
etud.id,
|
|
||||||
niveau.id,
|
|
||||||
semestre_id_impair + 1,
|
|
||||||
res_pair.formsemestre.id if (res_pair and self.ue_cur_pair) else None,
|
|
||||||
)
|
|
||||||
self.validation_ue_best_impair = best_autre_ue_validation(
|
|
||||||
etud.id,
|
|
||||||
niveau.id,
|
|
||||||
semestre_id_impair,
|
|
||||||
res_impair.formsemestre.id if (res_impair and self.ue_cur_impair) else None,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Suis-je complet ? (= en cours ou validé sur les deux moitiés)
|
|
||||||
self.complete = (self.ue_cur_pair or self.validation_ue_best_pair) and (
|
|
||||||
self.ue_cur_impair or self.validation_ue_best_impair
|
|
||||||
)
|
|
||||||
if not self.complete:
|
|
||||||
self.moy_rcue = None
|
|
||||||
|
|
||||||
# Stocke les moyennes d'UE
|
|
||||||
self.res_impair = None
|
|
||||||
"résultats formsemestre de l'UE si elle est courante, None sinon"
|
|
||||||
self.ue_status_impair = None
|
|
||||||
if self.ue_cur_impair:
|
|
||||||
ue_status = res_impair.get_etud_ue_status(etud.id, self.ue_cur_impair.id)
|
|
||||||
self.moy_ue_1 = ue_status["moy"] if ue_status else None # avec capitalisée
|
|
||||||
self.ue_1 = self.ue_cur_impair
|
|
||||||
self.res_impair = res_impair
|
|
||||||
self.ue_status_impair = ue_status
|
|
||||||
elif self.validation_ue_best_impair:
|
|
||||||
self.moy_ue_1 = self.validation_ue_best_impair.moy_ue
|
|
||||||
self.ue_1 = self.validation_ue_best_impair.ue
|
|
||||||
else:
|
|
||||||
self.moy_ue_1, self.ue_1 = None, None
|
|
||||||
self.moy_ue_1_val = self.moy_ue_1 if self.moy_ue_1 is not None else 0.0
|
|
||||||
|
|
||||||
self.res_pair = None
|
|
||||||
"résultats formsemestre de l'UE si elle est courante, None sinon"
|
|
||||||
self.ue_status_pair = None
|
|
||||||
if self.ue_cur_pair:
|
|
||||||
ue_status = res_pair.get_etud_ue_status(etud.id, self.ue_cur_pair.id)
|
|
||||||
self.moy_ue_2 = ue_status["moy"] if ue_status else None # avec capitalisée
|
|
||||||
self.ue_2 = self.ue_cur_pair
|
|
||||||
self.res_pair = res_pair
|
|
||||||
self.ue_status_pair = ue_status
|
|
||||||
elif self.validation_ue_best_pair:
|
|
||||||
self.moy_ue_2 = self.validation_ue_best_pair.moy_ue
|
|
||||||
self.ue_2 = self.validation_ue_best_pair.ue
|
|
||||||
else:
|
|
||||||
self.moy_ue_2, self.ue_2 = None, None
|
|
||||||
self.moy_ue_2_val = self.moy_ue_2 if self.moy_ue_2 is not None else 0.0
|
|
||||||
|
|
||||||
# Calcul de la moyenne au RCUE (utilise les moy d'UE capitalisées ou antérieures)
|
|
||||||
if (self.moy_ue_1 is not None) and (self.moy_ue_2 is not None):
|
|
||||||
# Moyenne RCUE (les pondérations par défaut sont 1.)
|
|
||||||
self.moy_rcue = (
|
|
||||||
self.moy_ue_1 * self.ue_1.coef_rcue
|
|
||||||
+ self.moy_ue_2 * self.ue_2.coef_rcue
|
|
||||||
) / (self.ue_1.coef_rcue + self.ue_2.coef_rcue)
|
|
||||||
else:
|
|
||||||
self.moy_rcue = None
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"""<{self.__class__.__name__} {
|
|
||||||
self.ue_1.acronyme if self.ue_1 else "?"}({self.moy_ue_1}) {
|
|
||||||
self.ue_2.acronyme if self.ue_2 else "?"}({self.moy_ue_2})>"""
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"""RCUE {
|
|
||||||
self.ue_1.acronyme if self.ue_1 else "?"}({self.moy_ue_1}) + {
|
|
||||||
self.ue_2.acronyme if self.ue_2 else "?"}({self.moy_ue_2})"""
|
|
||||||
|
|
||||||
def query_validations(
|
|
||||||
self,
|
|
||||||
) -> Query: # list[ApcValidationRCUE]
|
|
||||||
"""Les validations de jury enregistrées pour ce RCUE"""
|
|
||||||
return (
|
|
||||||
ApcValidationRCUE.query.filter_by(
|
|
||||||
etudid=self.etud.id,
|
|
||||||
)
|
|
||||||
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue2_id)
|
|
||||||
.join(ApcNiveau, UniteEns.niveau_competence_id == ApcNiveau.id)
|
|
||||||
.filter(ApcNiveau.id == self.niveau.id)
|
|
||||||
)
|
|
||||||
|
|
||||||
def other_ue(self, ue: UniteEns) -> UniteEns:
|
|
||||||
"""L'autre UE du regroupement. Si ue ne fait pas partie du regroupement, ValueError"""
|
|
||||||
if ue.id == self.ue_1.id:
|
|
||||||
return self.ue_2
|
|
||||||
elif ue.id == self.ue_2.id:
|
|
||||||
return self.ue_1
|
|
||||||
raise ValueError(f"ue {ue} hors RCUE {self}")
|
|
||||||
|
|
||||||
def est_enregistre(self) -> bool:
|
|
||||||
"""Vrai si ce RCUE, donc le niveau de compétences correspondant
|
|
||||||
a une décision jury enregistrée
|
|
||||||
"""
|
|
||||||
return self.query_validations().count() > 0
|
|
||||||
|
|
||||||
def est_compensable(self):
|
|
||||||
"""Vrai si ce RCUE est validable (uniquement) par compensation
|
|
||||||
c'est à dire que sa moyenne est > 10 avec une UE < 10.
|
|
||||||
Note: si ADM, est_compensable est faux.
|
|
||||||
"""
|
|
||||||
return (
|
|
||||||
(self.moy_rcue is not None)
|
|
||||||
and (self.moy_rcue > codes_cursus.BUT_BARRE_RCUE)
|
|
||||||
and (
|
|
||||||
(self.moy_ue_1_val < codes_cursus.NOTES_BARRE_GEN)
|
|
||||||
or (self.moy_ue_2_val < codes_cursus.NOTES_BARRE_GEN)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def est_suffisant(self) -> bool:
|
|
||||||
"""Vrai si ce RCUE est > 8"""
|
|
||||||
return (self.moy_rcue is not None) and (
|
|
||||||
self.moy_rcue > codes_cursus.BUT_RCUE_SUFFISANT
|
|
||||||
)
|
|
||||||
|
|
||||||
def est_validable(self) -> bool:
|
|
||||||
"""Vrai si ce RCUE satisfait les conditions pour être validé,
|
|
||||||
c'est à dire que la moyenne des UE qui le constituent soit > 10
|
|
||||||
"""
|
|
||||||
return (self.moy_rcue is not None) and (
|
|
||||||
self.moy_rcue > codes_cursus.BUT_BARRE_RCUE
|
|
||||||
)
|
|
||||||
|
|
||||||
def code_valide(self) -> Union[ApcValidationRCUE, None]:
|
|
||||||
"Si ce RCUE est ADM, CMP ou ADJ, la validation. Sinon, None"
|
|
||||||
validation = self.query_validations().first()
|
|
||||||
if (validation is not None) and (
|
|
||||||
validation.code in codes_cursus.CODES_RCUE_VALIDES
|
|
||||||
):
|
|
||||||
return validation
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def best_autre_ue_validation(
|
|
||||||
etudid: int, niveau_id: int, semestre_id: int, formsemestre_id: int
|
|
||||||
) -> ScolarFormSemestreValidation:
|
|
||||||
"""La "meilleure" validation validante d'UE pour ce niveau/semestre"""
|
|
||||||
validations = (
|
|
||||||
ScolarFormSemestreValidation.query.filter_by(etudid=etudid)
|
|
||||||
.join(UniteEns)
|
|
||||||
.filter_by(semestre_idx=semestre_id)
|
|
||||||
.join(ApcNiveau)
|
|
||||||
.filter(ApcNiveau.id == niveau_id)
|
|
||||||
)
|
|
||||||
validations = [v for v in validations if codes_cursus.code_ue_validant(v.code)]
|
|
||||||
# Elimine l'UE en cours si elle existe
|
|
||||||
if formsemestre_id is not None:
|
|
||||||
validations = [v for v in validations if v.formsemestre_id != formsemestre_id]
|
|
||||||
validations = sorted(validations, key=lambda v: BUT_CODES_ORDER.get(v.code, 0))
|
|
||||||
return validations[-1] if validations else None
|
|
||||||
|
|
||||||
|
|
||||||
# def compute_ues_by_niveau(
|
|
||||||
# niveaux: list[ApcNiveau],
|
|
||||||
# ) -> dict[int, tuple[list[UniteEns], list[UniteEns]]]:
|
|
||||||
# """UEs à valider cette année pour cet étudiant, selon son parcours.
|
|
||||||
# Considérer les UEs associées aux niveaux et non celles des formsemestres
|
|
||||||
# en cours. Notez que même si l'étudiant n'est pas inscrit ("dispensé") à une UE
|
|
||||||
# dans le formsemestre origine, elle doit apparaitre sur la page jury.
|
|
||||||
# Return: { niveau_id : ( [ues impair], [ues pair]) }
|
|
||||||
# """
|
|
||||||
# # Les UEs associées à ce niveau, toutes formations confondues
|
|
||||||
# return {
|
|
||||||
# niveau.id: (
|
|
||||||
# [ue for ue in niveau.ues if ue.semestre_idx % 2],
|
|
||||||
# [ue for ue in niveau.ues if not (ue.semestre_idx % 2)],
|
|
||||||
# )
|
|
||||||
# for niveau in niveaux
|
|
||||||
# }
|
|
@ -1,117 +0,0 @@
|
|||||||
##############################################################################
|
|
||||||
# ScoDoc
|
|
||||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
|
||||||
# See LICENSE
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
"""Jury édition manuelle des décisions (correction d'erreurs, parcours hors normes)
|
|
||||||
|
|
||||||
Non spécifique au BUT.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from flask import render_template
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
from app import log
|
|
||||||
from app.but import cursus_but
|
|
||||||
from app.models import (
|
|
||||||
ApcCompetence,
|
|
||||||
ApcNiveau,
|
|
||||||
ApcReferentielCompetences,
|
|
||||||
# ApcValidationAnnee, # TODO
|
|
||||||
ApcValidationRCUE,
|
|
||||||
Formation,
|
|
||||||
FormSemestre,
|
|
||||||
Identite,
|
|
||||||
UniteEns,
|
|
||||||
# ScolarAutorisationInscription,
|
|
||||||
ScolarFormSemestreValidation,
|
|
||||||
)
|
|
||||||
from app.scodoc import codes_cursus
|
|
||||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences
|
|
||||||
from app.views import ScoData
|
|
||||||
|
|
||||||
|
|
||||||
def validation_rcues(etud: Identite, formsemestre: FormSemestre, edit: bool = False):
|
|
||||||
"""Page de saisie des décisions de RCUEs "antérieures"
|
|
||||||
On peut l'utiliser pour saisir la validation de n'importe quel RCUE
|
|
||||||
d'une année antérieure et de la formation du formsemestre indiqué.
|
|
||||||
"""
|
|
||||||
formation: Formation = formsemestre.formation
|
|
||||||
refcomp = formation.referentiel_competence
|
|
||||||
if refcomp is None:
|
|
||||||
raise ScoNoReferentielCompetences(formation=formation)
|
|
||||||
parcour = formsemestre.etuds_inscriptions[etud.id].parcour
|
|
||||||
# Si non inscrit à un parcours, prend toutes les compétences
|
|
||||||
competences_parcour = cursus_but.parcour_formation_competences(parcour, formation)
|
|
||||||
|
|
||||||
ue_validation_by_niveau = get_ue_validation_by_niveau(refcomp, etud)
|
|
||||||
rcue_validation_by_niveau = get_rcue_validation_by_niveau(refcomp, etud)
|
|
||||||
ects_total = sum((v.ects() for v in ue_validation_by_niveau.values()))
|
|
||||||
return render_template(
|
|
||||||
"but/validation_rcues.j2",
|
|
||||||
competences_parcour=competences_parcour,
|
|
||||||
edit=edit,
|
|
||||||
ects_total=ects_total,
|
|
||||||
formation=formation,
|
|
||||||
parcour=parcour,
|
|
||||||
rcue_validation_by_niveau=rcue_validation_by_niveau,
|
|
||||||
rcue_codes=sorted(codes_cursus.CODES_JURY_RCUE),
|
|
||||||
sco=ScoData(formsemestre=formsemestre, etud=etud),
|
|
||||||
title=f"{formation.acronyme} - Niveaux et UEs",
|
|
||||||
ue_validation_by_niveau=ue_validation_by_niveau,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_ue_validation_by_niveau(
|
|
||||||
refcomp: ApcReferentielCompetences, etud: Identite
|
|
||||||
) -> dict[tuple[int, str], ScolarFormSemestreValidation]:
|
|
||||||
"""Les validations d'UEs de cet étudiant liées à ce référentiel de compétences.
|
|
||||||
Pour chaque niveau / pair ou impair, choisi la "meilleure" validation
|
|
||||||
"""
|
|
||||||
validations: list[ScolarFormSemestreValidation] = (
|
|
||||||
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
|
|
||||||
.join(UniteEns)
|
|
||||||
.join(ApcNiveau)
|
|
||||||
.join(ApcCompetence)
|
|
||||||
.filter_by(referentiel_id=refcomp.id)
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
# La meilleure validation pour chaque UE
|
|
||||||
ue_validation_by_niveau = {} # { (niveau_id, pair|impair) : validation }
|
|
||||||
for validation in validations:
|
|
||||||
if validation.ue.niveau_competence is None:
|
|
||||||
log(
|
|
||||||
f"""validation_rcues: ignore validation d'UE {
|
|
||||||
validation.ue.id} pas de niveau de competence"""
|
|
||||||
)
|
|
||||||
key = (
|
|
||||||
validation.ue.niveau_competence.id,
|
|
||||||
"impair" if validation.ue.semestre_idx % 2 else "pair",
|
|
||||||
)
|
|
||||||
existing = ue_validation_by_niveau.get(key, None)
|
|
||||||
if (not existing) or (
|
|
||||||
codes_cursus.BUT_CODES_ORDER[existing.code]
|
|
||||||
< codes_cursus.BUT_CODES_ORDER[validation.code]
|
|
||||||
):
|
|
||||||
ue_validation_by_niveau[key] = validation
|
|
||||||
return ue_validation_by_niveau
|
|
||||||
|
|
||||||
|
|
||||||
def get_rcue_validation_by_niveau(
|
|
||||||
refcomp: ApcReferentielCompetences, etud: Identite
|
|
||||||
) -> dict[int, ApcValidationRCUE]:
|
|
||||||
"""Les validations d'UEs de cet étudiant liées à ce référentiel de compétences.
|
|
||||||
Pour chaque niveau / pair ou impair, choisi la "meilleure" validation
|
|
||||||
"""
|
|
||||||
validations: list[ApcValidationRCUE] = (
|
|
||||||
ApcValidationRCUE.query.filter_by(etudid=etud.id)
|
|
||||||
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue2_id)
|
|
||||||
.join(ApcNiveau, UniteEns.niveau_competence_id == ApcNiveau.id)
|
|
||||||
.join(ApcCompetence)
|
|
||||||
.filter_by(referentiel_id=refcomp.id)
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
validation.ue2.niveau_competence.id: validation for validation in validations
|
|
||||||
}
|
|
@ -18,7 +18,7 @@ import pandas as pd
|
|||||||
|
|
||||||
from flask import g
|
from flask import g
|
||||||
|
|
||||||
from app.scodoc.codes_cursus import UE_SPORT, UE_STANDARD
|
from app.scodoc.codes_cursus import UE_SPORT
|
||||||
from app.scodoc.codes_cursus import CursusDUT, CursusDUTMono
|
from app.scodoc.codes_cursus import CursusDUT, CursusDUTMono
|
||||||
from app.scodoc.sco_utils import ModuleType
|
from app.scodoc.sco_utils import ModuleType
|
||||||
|
|
||||||
@ -194,7 +194,7 @@ class BonusSportAdditif(BonusSport):
|
|||||||
# les points au dessus du seuil sont comptés (defaut: seuil_moy_gen):
|
# les points au dessus du seuil sont comptés (defaut: seuil_moy_gen):
|
||||||
seuil_comptage = None
|
seuil_comptage = None
|
||||||
proportion_point = 0.05 # multiplie les points au dessus du seuil
|
proportion_point = 0.05 # multiplie les points au dessus du seuil
|
||||||
bonus_max = 20.0 # le bonus ne peut dépasser 20 points
|
bonux_max = 20.0 # le bonus ne peut dépasser 20 points
|
||||||
bonus_min = 0.0 # et ne peut pas être négatif
|
bonus_min = 0.0 # et ne peut pas être négatif
|
||||||
|
|
||||||
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||||||
@ -435,11 +435,8 @@ class BonusAmiens(BonusSportAdditif):
|
|||||||
class BonusBesanconVesoul(BonusSportAdditif):
|
class BonusBesanconVesoul(BonusSportAdditif):
|
||||||
"""Bonus IUT Besançon - Vesoul pour les UE libres
|
"""Bonus IUT Besançon - Vesoul pour les UE libres
|
||||||
|
|
||||||
<p>Le bonus est compris entre 0 et 0,2 points.
|
<p>Toute note non nulle, peu importe sa valeur, entraine un bonus de 0,2 point
|
||||||
et est reporté sur les moyennes d'UE.
|
sur toutes les moyennes d'UE.
|
||||||
</p>
|
|
||||||
<p>La valeur saisie doit être entre 0 et 0,2: toute valeur
|
|
||||||
supérieure à 0,2 entraine un bonus de 0,2.
|
|
||||||
</p>
|
</p>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -447,7 +444,7 @@ class BonusBesanconVesoul(BonusSportAdditif):
|
|||||||
displayed_name = "IUT de Besançon - Vesoul"
|
displayed_name = "IUT de Besançon - Vesoul"
|
||||||
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
|
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
|
||||||
seuil_moy_gen = 0.0 # tous les points sont comptés
|
seuil_moy_gen = 0.0 # tous les points sont comptés
|
||||||
proportion_point = 1
|
proportion_point = 1e10 # infini
|
||||||
bonus_max = 0.2
|
bonus_max = 0.2
|
||||||
|
|
||||||
|
|
||||||
@ -743,7 +740,6 @@ class BonusGrenobleIUT1(BonusSportMultiplicatif):
|
|||||||
|
|
||||||
name = "bonus_iut1grenoble_2017"
|
name = "bonus_iut1grenoble_2017"
|
||||||
displayed_name = "IUT de Grenoble 1"
|
displayed_name = "IUT de Grenoble 1"
|
||||||
|
|
||||||
# C'est un bonus "multiplicatif": on l'exprime en additif,
|
# C'est un bonus "multiplicatif": on l'exprime en additif,
|
||||||
# sur chaque moyenne d'UE m_0
|
# sur chaque moyenne d'UE m_0
|
||||||
# Augmenter de 5% correspond à multiplier par a=1.05
|
# Augmenter de 5% correspond à multiplier par a=1.05
|
||||||
@ -786,7 +782,6 @@ class BonusIUTRennes1(BonusSportAdditif):
|
|||||||
seuil_moy_gen = 10.0
|
seuil_moy_gen = 10.0
|
||||||
proportion_point = 1 / 20.0
|
proportion_point = 1 / 20.0
|
||||||
classic_use_bonus_ues = False
|
classic_use_bonus_ues = False
|
||||||
|
|
||||||
# S'applique aussi en classic, sur la moy. gen.
|
# S'applique aussi en classic, sur la moy. gen.
|
||||||
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||||||
"""calcul du bonus"""
|
"""calcul du bonus"""
|
||||||
@ -827,32 +822,16 @@ class BonusStMalo(BonusIUTRennes1):
|
|||||||
class BonusLaRocheSurYon(BonusSportAdditif):
|
class BonusLaRocheSurYon(BonusSportAdditif):
|
||||||
"""Bonus IUT de La Roche-sur-Yon
|
"""Bonus IUT de La Roche-sur-Yon
|
||||||
|
|
||||||
<p>
|
Si une note de bonus est saisie, l'étudiant est gratifié de 0,2 points
|
||||||
<b>La note saisie s'applique directement</b>: si on saisit 0,2, un bonus de 0,2 points est appliqué
|
sur sa moyenne générale ou, en BUT, sur la moyenne de chaque UE.
|
||||||
aux moyennes.
|
|
||||||
La valeur maximale du bonus est 1 point. Il est appliqué sur les moyennes d'UEs en BUT,
|
|
||||||
ou sur la moyenne générale dans les autres formations.
|
|
||||||
</p>
|
|
||||||
<p>Pour les <b>semestres antérieurs à janvier 2023</b>: si une note de bonus est saisie,
|
|
||||||
l'étudiant est gratifié de 0,2 points sur sa moyenne générale ou, en BUT, sur la
|
|
||||||
moyenne de chaque UE.
|
|
||||||
</p>
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "bonus_larochesuryon"
|
name = "bonus_larochesuryon"
|
||||||
displayed_name = "IUT de La Roche-sur-Yon"
|
displayed_name = "IUT de La Roche-sur-Yon"
|
||||||
seuil_moy_gen = 0.0
|
seuil_moy_gen = 0.0
|
||||||
seuil_comptage = 0.0
|
seuil_comptage = 0.0
|
||||||
|
proportion_point = 1e10 # le moindre point sature le bonus
|
||||||
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
bonus_max = 0.2 # à 0.2
|
||||||
"""calcul du bonus, avec réglage différent suivant la date"""
|
|
||||||
if self.formsemestre.date_debut > datetime.date(2022, 12, 31):
|
|
||||||
self.proportion_point = 1.0
|
|
||||||
self.bonus_max = 1
|
|
||||||
else: # ancienne règle
|
|
||||||
self.proportion_point = 1e10 # le moindre point sature le bonus
|
|
||||||
self.bonus_max = 0.2 # à 0.2
|
|
||||||
super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
|
|
||||||
|
|
||||||
|
|
||||||
class BonusLaRochelle(BonusSportAdditif):
|
class BonusLaRochelle(BonusSportAdditif):
|
||||||
@ -1076,36 +1055,6 @@ class BonusLyon(BonusSportAdditif):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BonusLyon3(BonusSportAdditif):
|
|
||||||
"""IUT de Lyon 3 (septembre 2022)
|
|
||||||
|
|
||||||
<p>Nous avons deux types de bonifications : sport et/ou culture
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Pour chaque point au-dessus de 10 obtenu en sport ou en culture nous
|
|
||||||
ajoutons 0,03 points à toutes les moyennes d’UE du semestre. Exemple : 16 en
|
|
||||||
sport ajoute 6*0,03 = 0,18 points à toutes les moyennes d’UE du semestre.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Les bonifications sport et culture peuvent se cumuler dans la limite de 0,3
|
|
||||||
points ajoutés aux moyennes des UE. Exemple : 17 en sport et 16 en culture
|
|
||||||
conduisent au calcul (7 + 6)*0,03 = 0,39 qui dépasse 0,3. La bonification
|
|
||||||
dans ce cas ne sera que de 0,3 points ajoutés à toutes les moyennes d’UE du
|
|
||||||
semestre.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Dans Scodoc on déclarera une UE Sport&Culture dans laquelle on aura un
|
|
||||||
module pour le Sport et un autre pour la Culture avec pour chaque module la
|
|
||||||
note sur 20 obtenue en sport ou en culture par l’étudiant.
|
|
||||||
</p>
|
|
||||||
"""
|
|
||||||
|
|
||||||
name = "bonus_lyon3"
|
|
||||||
displayed_name = "IUT de Lyon 3"
|
|
||||||
proportion_point = 0.03
|
|
||||||
bonus_max = 0.3
|
|
||||||
|
|
||||||
|
|
||||||
class BonusMantes(BonusSportAdditif):
|
class BonusMantes(BonusSportAdditif):
|
||||||
"""Calcul bonus modules optionnels (investissement, ...), IUT de Mantes en Yvelines.
|
"""Calcul bonus modules optionnels (investissement, ...), IUT de Mantes en Yvelines.
|
||||||
|
|
||||||
@ -1387,7 +1336,6 @@ class BonusStNazaire(BonusSport):
|
|||||||
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
|
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
|
||||||
amplitude = 0.01 / 4 # 4pt => 1%
|
amplitude = 0.01 / 4 # 4pt => 1%
|
||||||
factor_max = 0.1 # 10% max
|
factor_max = 0.1 # 10% max
|
||||||
|
|
||||||
# Modifié 2022-11-29: calculer chaque bonus
|
# Modifié 2022-11-29: calculer chaque bonus
|
||||||
# (de 1 à 3 modules) séparément.
|
# (de 1 à 3 modules) séparément.
|
||||||
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||||||
@ -1585,63 +1533,6 @@ class BonusIUTV(BonusSportAdditif):
|
|||||||
# c'est le bonus par défaut: aucune méthode à surcharger
|
# c'est le bonus par défaut: aucune méthode à surcharger
|
||||||
|
|
||||||
|
|
||||||
# Finalement inutile: un bonus direct est mieux adapté à leurs besoins.
|
|
||||||
# # class BonusMastersUSPNIG(BonusSportAdditif):
|
|
||||||
# """Calcul bonus modules optionnels (sport, culture), règle Masters de l'Institut Galilée (USPN)
|
|
||||||
|
|
||||||
# Les étudiants peuvent suivre des enseignements optionnels
|
|
||||||
# de l'USPN (sports, musique, deuxième langue, culture, etc) dans une
|
|
||||||
# UE libre. Les points au-dessus de 10 sur 20 obtenus dans cette UE
|
|
||||||
# libre sont ajoutés au total des points obtenus pour les UE obligatoires
|
|
||||||
# du semestre concerné.
|
|
||||||
# """
|
|
||||||
|
|
||||||
# name = "bonus_masters__uspn_ig"
|
|
||||||
# displayed_name = "Masters de l'Institut Galilée (USPN)"
|
|
||||||
# proportion_point = 1.0
|
|
||||||
# seuil_moy_gen = 10.0
|
|
||||||
|
|
||||||
# def __init__(
|
|
||||||
# self,
|
|
||||||
# formsemestre: "FormSemestre",
|
|
||||||
# sem_modimpl_moys: np.array,
|
|
||||||
# ues: list,
|
|
||||||
# modimpl_inscr_df: pd.DataFrame,
|
|
||||||
# modimpl_coefs: np.array,
|
|
||||||
# etud_moy_gen,
|
|
||||||
# etud_moy_ue,
|
|
||||||
# ):
|
|
||||||
# # Pour ce bonus, il nous faut la somme des coefs des modules non bonus
|
|
||||||
# # du formsemestre (et non auxquels les étudiants sont inscrits !)
|
|
||||||
# self.sum_coefs = sum(
|
|
||||||
# [
|
|
||||||
# m.module.coefficient
|
|
||||||
# for m in formsemestre.modimpls_sorted
|
|
||||||
# if (m.module.module_type == ModuleType.STANDARD)
|
|
||||||
# and (m.module.ue.type == UE_STANDARD)
|
|
||||||
# ]
|
|
||||||
# )
|
|
||||||
# super().__init__(
|
|
||||||
# formsemestre,
|
|
||||||
# sem_modimpl_moys,
|
|
||||||
# ues,
|
|
||||||
# modimpl_inscr_df,
|
|
||||||
# modimpl_coefs,
|
|
||||||
# etud_moy_gen,
|
|
||||||
# etud_moy_ue,
|
|
||||||
# )
|
|
||||||
# # Bonus sur la moyenne générale seulement
|
|
||||||
# # On a dans bonus_moy_arr le bonus additif classique
|
|
||||||
# # Sa valeur sera appliquée comme moy_gen += bonus_moy_gen
|
|
||||||
# # or ici on veut
|
|
||||||
# # moy_gen = (somme des notes + bonus_moy_arr) / somme des coefs
|
|
||||||
# # moy_gen += bonus_moy_arr / somme des coefs
|
|
||||||
|
|
||||||
# self.bonus_moy_gen = (
|
|
||||||
# None if self.bonus_moy_gen is None else self.bonus_moy_gen / self.sum_coefs
|
|
||||||
# )
|
|
||||||
|
|
||||||
|
|
||||||
def get_bonus_class_dict(start=BonusSport, d=None):
|
def get_bonus_class_dict(start=BonusSport, d=None):
|
||||||
"""Dictionnaire des classes de bonus
|
"""Dictionnaire des classes de bonus
|
||||||
(liste les sous-classes de BonusSport ayant un nom)
|
(liste les sous-classes de BonusSport ayant un nom)
|
||||||
|
@ -10,17 +10,8 @@ import pandas as pd
|
|||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
|
from app.models import FormSemestre, Identite, ScolarFormSemestreValidation, UniteEns
|
||||||
from app.comp.res_cache import ResultatsCache
|
from app.comp.res_cache import ResultatsCache
|
||||||
from app.models import (
|
|
||||||
ApcValidationAnnee,
|
|
||||||
ApcValidationRCUE,
|
|
||||||
Formation,
|
|
||||||
FormSemestre,
|
|
||||||
Identite,
|
|
||||||
ScolarAutorisationInscription,
|
|
||||||
ScolarFormSemestreValidation,
|
|
||||||
UniteEns,
|
|
||||||
)
|
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
from app.scodoc import codes_cursus
|
from app.scodoc import codes_cursus
|
||||||
|
|
||||||
@ -90,7 +81,7 @@ class ValidationsSemestre(ResultatsCache):
|
|||||||
|
|
||||||
# UEs: { etudid : { ue_id : {"code":, "ects":, "event_date":} }}
|
# UEs: { etudid : { ue_id : {"code":, "ects":, "event_date":} }}
|
||||||
decisions_jury_ues = {}
|
decisions_jury_ues = {}
|
||||||
# Parcoure les décisions d'UE:
|
# Parcours les décisions d'UE:
|
||||||
for decision in (
|
for decision in (
|
||||||
decisions_jury_q.filter(db.text("ue_id is not NULL"))
|
decisions_jury_q.filter(db.text("ue_id is not NULL"))
|
||||||
.join(UniteEns)
|
.join(UniteEns)
|
||||||
@ -181,79 +172,3 @@ def formsemestre_get_ue_capitalisees(formsemestre: FormSemestre) -> pd.DataFrame
|
|||||||
with db.engine.begin() as connection:
|
with db.engine.begin() as connection:
|
||||||
df = pd.read_sql_query(query, connection, params=params, index_col="etudid")
|
df = pd.read_sql_query(query, connection, params=params, index_col="etudid")
|
||||||
return df
|
return df
|
||||||
|
|
||||||
|
|
||||||
def erase_decisions_annee_formation(
|
|
||||||
etud: Identite, formation: Formation, annee: int, delete=False
|
|
||||||
) -> list:
|
|
||||||
"""Efface toutes les décisions de jury de l'étudiant dans les formations de même code
|
|
||||||
que celle donnée pour cette année de la formation:
|
|
||||||
UEs, RCUEs de l'année BUT, année BUT, passage vers l'année suivante.
|
|
||||||
Ne considère pas l'origine de la décision.
|
|
||||||
annee: entier, 1, 2, 3, ...
|
|
||||||
Si delete est faux, renvoie la liste des validations qu'il faudrait effacer, sans y toucher.
|
|
||||||
"""
|
|
||||||
sem1, sem2 = annee * 2 - 1, annee * 2
|
|
||||||
# UEs
|
|
||||||
validations = (
|
|
||||||
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
|
|
||||||
.join(UniteEns)
|
|
||||||
.filter(db.or_(UniteEns.semestre_idx == sem1, UniteEns.semestre_idx == sem2))
|
|
||||||
.join(Formation)
|
|
||||||
.filter_by(formation_code=formation.formation_code)
|
|
||||||
.order_by(
|
|
||||||
UniteEns.acronyme, UniteEns.numero
|
|
||||||
) # acronyme d'abord car 2 semestres
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
# RCUEs (a priori inutile de matcher sur l'ue2_id)
|
|
||||||
validations += (
|
|
||||||
ApcValidationRCUE.query.filter_by(etudid=etud.id)
|
|
||||||
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id)
|
|
||||||
.filter_by(semestre_idx=sem1)
|
|
||||||
.join(Formation)
|
|
||||||
.filter_by(formation_code=formation.formation_code)
|
|
||||||
.order_by(UniteEns.acronyme, UniteEns.numero)
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
# Validation de semestres classiques
|
|
||||||
validations += (
|
|
||||||
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id, ue_id=None)
|
|
||||||
.join(
|
|
||||||
FormSemestre,
|
|
||||||
FormSemestre.id == ScolarFormSemestreValidation.formsemestre_id,
|
|
||||||
)
|
|
||||||
.filter(
|
|
||||||
db.or_(FormSemestre.semestre_id == sem1, FormSemestre.semestre_id == sem2)
|
|
||||||
)
|
|
||||||
.join(Formation)
|
|
||||||
.filter_by(formation_code=formation.formation_code)
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
# Année BUT
|
|
||||||
validations += ApcValidationAnnee.query.filter_by(
|
|
||||||
etudid=etud.id,
|
|
||||||
ordre=annee,
|
|
||||||
referentiel_competence_id=formation.referentiel_competence_id,
|
|
||||||
).all()
|
|
||||||
# Autorisations vers les semestres suivants ceux de l'année:
|
|
||||||
validations += (
|
|
||||||
ScolarAutorisationInscription.query.filter_by(
|
|
||||||
etudid=etud.id, formation_code=formation.formation_code
|
|
||||||
)
|
|
||||||
.filter(
|
|
||||||
db.or_(
|
|
||||||
ScolarAutorisationInscription.semestre_id == sem1 + 1,
|
|
||||||
ScolarAutorisationInscription.semestre_id == sem2 + 1,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
|
|
||||||
if delete:
|
|
||||||
for validation in validations:
|
|
||||||
db.session.delete(validation)
|
|
||||||
db.session.commit()
|
|
||||||
sco_cache.invalidate_formsemestre_etud(etud)
|
|
||||||
return []
|
|
||||||
return validations
|
|
||||||
|
@ -134,7 +134,7 @@ class ModuleImplResults:
|
|||||||
manque des notes) ssi il y a des étudiants inscrits au semestre et au module
|
manque des notes) ssi il y a des étudiants inscrits au semestre et au module
|
||||||
qui ont des notes ATT.
|
qui ont des notes ATT.
|
||||||
"""
|
"""
|
||||||
moduleimpl = db.session.get(ModuleImpl, self.moduleimpl_id)
|
moduleimpl = ModuleImpl.query.get(self.moduleimpl_id)
|
||||||
self.etudids = self._etudids()
|
self.etudids = self._etudids()
|
||||||
|
|
||||||
# --- Calcul nombre d'inscrits pour déterminer les évaluations "completes":
|
# --- Calcul nombre d'inscrits pour déterminer les évaluations "completes":
|
||||||
@ -225,8 +225,8 @@ class ModuleImplResults:
|
|||||||
"""
|
"""
|
||||||
return [
|
return [
|
||||||
inscr.etudid
|
inscr.etudid
|
||||||
for inscr in db.session.get(
|
for inscr in ModuleImpl.query.get(
|
||||||
ModuleImpl, self.moduleimpl_id
|
self.moduleimpl_id
|
||||||
).formsemestre.inscriptions
|
).formsemestre.inscriptions
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -319,16 +319,10 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
|||||||
ou NaN si les évaluations (dans lesquelles l'étudiant a des notes)
|
ou NaN si les évaluations (dans lesquelles l'étudiant a des notes)
|
||||||
ne donnent pas de coef vers cette UE.
|
ne donnent pas de coef vers cette UE.
|
||||||
"""
|
"""
|
||||||
modimpl = db.session.get(ModuleImpl, self.moduleimpl_id)
|
modimpl = ModuleImpl.query.get(self.moduleimpl_id)
|
||||||
nb_etuds, nb_evals = self.evals_notes.shape
|
nb_etuds, nb_evals = self.evals_notes.shape
|
||||||
nb_ues = evals_poids_df.shape[1]
|
nb_ues = evals_poids_df.shape[1]
|
||||||
if evals_poids_df.shape[0] != nb_evals:
|
assert evals_poids_df.shape[0] == nb_evals # compat notes/poids
|
||||||
# compat notes/poids: race condition ?
|
|
||||||
app.critical_error(
|
|
||||||
f"""compute_module_moy: evals_poids_df.shape[0] != nb_evals ({
|
|
||||||
evals_poids_df.shape[0]} != {nb_evals})
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
if nb_etuds == 0:
|
if nb_etuds == 0:
|
||||||
return pd.DataFrame(index=[], columns=evals_poids_df.columns)
|
return pd.DataFrame(index=[], columns=evals_poids_df.columns)
|
||||||
if nb_ues == 0:
|
if nb_ues == 0:
|
||||||
@ -419,7 +413,7 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
|
|||||||
|
|
||||||
Résultat: (evals_poids, liste de UEs du semestre sauf le sport)
|
Résultat: (evals_poids, liste de UEs du semestre sauf le sport)
|
||||||
"""
|
"""
|
||||||
modimpl: ModuleImpl = db.session.get(ModuleImpl, moduleimpl_id)
|
modimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id)
|
||||||
evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all()
|
evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all()
|
||||||
ues = modimpl.formsemestre.get_ues(with_sport=False)
|
ues = modimpl.formsemestre.get_ues(with_sport=False)
|
||||||
ue_ids = [ue.id for ue in ues]
|
ue_ids = [ue.id for ue in ues]
|
||||||
@ -498,7 +492,7 @@ class ModuleImplResultsClassic(ModuleImplResults):
|
|||||||
ou NaN si les évaluations (dans lesquelles l'étudiant a des notes)
|
ou NaN si les évaluations (dans lesquelles l'étudiant a des notes)
|
||||||
ne donnent pas de coef.
|
ne donnent pas de coef.
|
||||||
"""
|
"""
|
||||||
modimpl = db.session.get(ModuleImpl, self.moduleimpl_id)
|
modimpl = ModuleImpl.query.get(self.moduleimpl_id)
|
||||||
nb_etuds, nb_evals = self.evals_notes.shape
|
nb_etuds, nb_evals = self.evals_notes.shape
|
||||||
if nb_etuds == 0:
|
if nb_etuds == 0:
|
||||||
return pd.Series()
|
return pd.Series()
|
||||||
|
@ -30,10 +30,7 @@
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from flask import flash, g, url_for
|
from flask import flash, g, Markup, url_for
|
||||||
from markupsafe import Markup
|
|
||||||
|
|
||||||
from app import db
|
|
||||||
from app.models.formations import Formation
|
from app.models.formations import Formation
|
||||||
|
|
||||||
|
|
||||||
@ -81,7 +78,7 @@ def compute_sem_moys_apc_using_ects(
|
|||||||
moy_gen = (etud_moy_ue_df * ects).sum(axis=1) / ects.sum(axis=1)
|
moy_gen = (etud_moy_ue_df * ects).sum(axis=1) / ects.sum(axis=1)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
if None in ects:
|
if None in ects:
|
||||||
formation = db.session.get(Formation, formation_id)
|
formation = Formation.query.get(formation_id)
|
||||||
flash(
|
flash(
|
||||||
Markup(
|
Markup(
|
||||||
f"""Calcul moyenne générale impossible: ECTS des UE manquants !<br>
|
f"""Calcul moyenne générale impossible: ECTS des UE manquants !<br>
|
||||||
@ -95,7 +92,7 @@ def compute_sem_moys_apc_using_ects(
|
|||||||
return moy_gen
|
return moy_gen
|
||||||
|
|
||||||
|
|
||||||
def comp_ranks_series(notes: pd.Series) -> tuple[pd.Series, pd.Series]:
|
def comp_ranks_series(notes: pd.Series) -> (pd.Series, pd.Series):
|
||||||
"""Calcul rangs à partir d'une séries ("vecteur") de notes (index etudid, valeur
|
"""Calcul rangs à partir d'une séries ("vecteur") de notes (index etudid, valeur
|
||||||
numérique) en tenant compte des ex-aequos.
|
numérique) en tenant compte des ex-aequos.
|
||||||
|
|
||||||
|
@ -30,7 +30,6 @@
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
import app
|
|
||||||
from app import db
|
from app import db
|
||||||
from app import models
|
from app import models
|
||||||
from app.models import (
|
from app.models import (
|
||||||
@ -168,14 +167,8 @@ def notes_sem_assemble_cube(modimpls_notes: list[pd.DataFrame]) -> np.ndarray:
|
|||||||
"""
|
"""
|
||||||
assert len(modimpls_notes)
|
assert len(modimpls_notes)
|
||||||
modimpls_notes_arr = [df.values for df in modimpls_notes]
|
modimpls_notes_arr = [df.values for df in modimpls_notes]
|
||||||
try:
|
modimpls_notes = np.stack(modimpls_notes_arr)
|
||||||
modimpls_notes = np.stack(modimpls_notes_arr)
|
# passe de (mod x etud x ue) à (etud x mod x ue)
|
||||||
# passe de (mod x etud x ue) à (etud x mod x ue)
|
|
||||||
except ValueError:
|
|
||||||
app.critical_error(
|
|
||||||
f"""notes_sem_assemble_cube: shapes {
|
|
||||||
", ".join([x.shape for x in modimpls_notes_arr])}"""
|
|
||||||
)
|
|
||||||
return modimpls_notes.swapaxes(0, 1)
|
return modimpls_notes.swapaxes(0, 1)
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,17 +10,17 @@ import time
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from app import db, log
|
from app import log
|
||||||
from app.comp import moy_ue, moy_sem, inscr_mod
|
from app.comp import moy_ue, moy_sem, inscr_mod
|
||||||
from app.comp.res_compat import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
from app.comp.bonus_spo import BonusSport
|
from app.comp.bonus_spo import BonusSport
|
||||||
from app.models import FormSemestreInscription, ScoDocSiteConfig
|
from app.models import ScoDocSiteConfig
|
||||||
from app.models.moduleimpls import ModuleImpl
|
from app.models.moduleimpls import ModuleImpl
|
||||||
from app.models.but_refcomp import ApcParcours, ApcNiveau
|
from app.models.but_refcomp import ApcParcours, ApcNiveau
|
||||||
from app.models.ues import DispenseUE, UniteEns
|
from app.models.ues import DispenseUE, UniteEns
|
||||||
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
from app.scodoc.codes_cursus import BUT_CODES_ORDER, UE_SPORT
|
from app.scodoc.codes_cursus import UE_SPORT
|
||||||
from app.scodoc.sco_utils import ModuleType
|
from app.scodoc.sco_utils import ModuleType
|
||||||
|
|
||||||
|
|
||||||
@ -44,8 +44,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
"""Parcours de chaque étudiant { etudid : parcour_id }"""
|
"""Parcours de chaque étudiant { etudid : parcour_id }"""
|
||||||
self.ues_ids_by_parcour: dict[set[int]] = {}
|
self.ues_ids_by_parcour: dict[set[int]] = {}
|
||||||
"""{ parcour_id : set }, ue_id de chaque parcours"""
|
"""{ parcour_id : set }, ue_id de chaque parcours"""
|
||||||
self.validations_annee: dict[int, ApcValidationAnnee] = {}
|
|
||||||
"""chargé par get_validations_annee: jury annuel BUT"""
|
|
||||||
if not self.load_cached():
|
if not self.load_cached():
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
self.compute()
|
self.compute()
|
||||||
@ -289,9 +288,9 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
if ref_comp is None:
|
if ref_comp is None:
|
||||||
return set()
|
return set()
|
||||||
if parcour_id is None:
|
if parcour_id is None:
|
||||||
ues_ids = {ue.id for ue in self.ues if ue.type != UE_SPORT}
|
ues_ids = {ue.id for ue in self.ues}
|
||||||
else:
|
else:
|
||||||
parcour: ApcParcours = db.session.get(ApcParcours, parcour_id)
|
parcour: ApcParcours = ApcParcours.query.get(parcour_id)
|
||||||
annee = (self.formsemestre.semestre_id + 1) // 2
|
annee = (self.formsemestre.semestre_id + 1) // 2
|
||||||
niveaux = ApcNiveau.niveaux_annee_de_parcours(parcour, annee, ref_comp)
|
niveaux = ApcNiveau.niveaux_annee_de_parcours(parcour, annee, ref_comp)
|
||||||
# Les UEs du formsemestre associées à ces niveaux:
|
# Les UEs du formsemestre associées à ces niveaux:
|
||||||
@ -307,13 +306,12 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
|
|
||||||
return ues_ids
|
return ues_ids
|
||||||
|
|
||||||
def etud_has_decision(self, etudid) -> bool:
|
def etud_has_decision(self, etudid):
|
||||||
"""True s'il y a une décision (quelconque) de jury
|
"""True s'il y a une décision de jury pour cet étudiant émanant de ce formsemestre.
|
||||||
émanant de ce formsemestre pour cet étudiant.
|
|
||||||
prend aussi en compte les autorisations de passage.
|
prend aussi en compte les autorisations de passage.
|
||||||
Ici sous-classée (BUT) pour les RCUEs et années.
|
Sous-classée en BUT pour les RCUEs et années.
|
||||||
"""
|
"""
|
||||||
return bool(
|
return (
|
||||||
super().etud_has_decision(etudid)
|
super().etud_has_decision(etudid)
|
||||||
or ApcValidationAnnee.query.filter_by(
|
or ApcValidationAnnee.query.filter_by(
|
||||||
formsemestre_id=self.formsemestre.id, etudid=etudid
|
formsemestre_id=self.formsemestre.id, etudid=etudid
|
||||||
@ -322,40 +320,3 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
formsemestre_id=self.formsemestre.id, etudid=etudid
|
formsemestre_id=self.formsemestre.id, etudid=etudid
|
||||||
).count()
|
).count()
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_validations_annee(self) -> dict[int, ApcValidationAnnee]:
|
|
||||||
"""Les validations des étudiants de ce semestre
|
|
||||||
pour l'année BUT d'une formation compatible avec celle de ce semestre.
|
|
||||||
Attention:
|
|
||||||
1) la validation ne provient pas nécessairement de ce semestre
|
|
||||||
(redoublants, pair/impair, extérieurs).
|
|
||||||
2) l'étudiant a pu démissionner ou défaillir.
|
|
||||||
3) S'il y a plusieurs validations pour le même étudiant, prend la "meilleure".
|
|
||||||
|
|
||||||
Mémorise le résultat (dans l'instance, pas en cache: TODO voir au profiler)
|
|
||||||
"""
|
|
||||||
if self.validations_annee:
|
|
||||||
return self.validations_annee
|
|
||||||
annee_but = (self.formsemestre.semestre_id + 1) // 2
|
|
||||||
validations = ApcValidationAnnee.query.filter_by(
|
|
||||||
ordre=annee_but,
|
|
||||||
referentiel_competence_id=self.formsemestre.formation.referentiel_competence_id,
|
|
||||||
).join(
|
|
||||||
FormSemestreInscription,
|
|
||||||
db.and_(
|
|
||||||
FormSemestreInscription.etudid == ApcValidationAnnee.etudid,
|
|
||||||
FormSemestreInscription.formsemestre_id == self.formsemestre.id,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
validation_by_etud = {}
|
|
||||||
for validation in validations:
|
|
||||||
if validation.etudid in validation_by_etud:
|
|
||||||
# keep the "best"
|
|
||||||
if BUT_CODES_ORDER.get(validation.code, 0) > BUT_CODES_ORDER.get(
|
|
||||||
validation_by_etud[validation.etudid].code, 0
|
|
||||||
):
|
|
||||||
validation_by_etud[validation.etudid] = validation
|
|
||||||
else:
|
|
||||||
validation_by_etud[validation.etudid] = validation
|
|
||||||
self.validations_annee = validation_by_etud
|
|
||||||
return self.validations_annee
|
|
||||||
|
@ -17,7 +17,6 @@ import pandas as pd
|
|||||||
|
|
||||||
from flask import g, url_for
|
from flask import g, url_for
|
||||||
|
|
||||||
from app import db
|
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
from app.comp.res_cache import ResultatsCache
|
from app.comp.res_cache import ResultatsCache
|
||||||
from app.comp.jury import ValidationsSemestre
|
from app.comp.jury import ValidationsSemestre
|
||||||
@ -32,7 +31,6 @@ from app.scodoc.codes_cursus import UE_SPORT
|
|||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
# Il faut bien distinguer
|
# Il faut bien distinguer
|
||||||
# - ce qui est caché de façon persistente (via redis):
|
# - ce qui est caché de façon persistente (via redis):
|
||||||
# ce sont les attributs listés dans `_cached_attrs`
|
# ce sont les attributs listés dans `_cached_attrs`
|
||||||
@ -139,7 +137,7 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
def etud_ues(self, etudid: int) -> Generator[UniteEns]:
|
def etud_ues(self, etudid: int) -> Generator[UniteEns]:
|
||||||
"""Liste des UE auxquelles l'étudiant est inscrit
|
"""Liste des UE auxquelles l'étudiant est inscrit
|
||||||
(sans bonus, en BUT prend en compte le parcours de l'étudiant)."""
|
(sans bonus, en BUT prend en compte le parcours de l'étudiant)."""
|
||||||
return (db.session.get(UniteEns, ue_id) for ue_id in self.etud_ues_ids(etudid))
|
return (UniteEns.query.get(ue_id) for ue_id in self.etud_ues_ids(etudid))
|
||||||
|
|
||||||
def etud_ects_tot_sem(self, etudid: int) -> float:
|
def etud_ects_tot_sem(self, etudid: int) -> float:
|
||||||
"""Le total des ECTS associées à ce semestre (que l'étudiant peut ou non valider)"""
|
"""Le total des ECTS associées à ce semestre (que l'étudiant peut ou non valider)"""
|
||||||
@ -353,7 +351,7 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
"""L'état de l'UE pour cet étudiant.
|
"""L'état de l'UE pour cet étudiant.
|
||||||
Result: dict, ou None si l'UE n'est pas dans ce semestre.
|
Result: dict, ou None si l'UE n'est pas dans ce semestre.
|
||||||
"""
|
"""
|
||||||
ue: UniteEns = db.session.get(UniteEns, ue_id)
|
ue: UniteEns = UniteEns.query.get(ue_id)
|
||||||
ue_dict = ue.to_dict()
|
ue_dict = ue.to_dict()
|
||||||
|
|
||||||
if ue.type == UE_SPORT:
|
if ue.type == UE_SPORT:
|
||||||
@ -383,11 +381,7 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
was_capitalized = False
|
was_capitalized = False
|
||||||
if etudid in self.validations.ue_capitalisees.index:
|
if etudid in self.validations.ue_capitalisees.index:
|
||||||
ue_cap = self._get_etud_ue_cap(etudid, ue)
|
ue_cap = self._get_etud_ue_cap(etudid, ue)
|
||||||
if (
|
if ue_cap and not np.isnan(ue_cap["moy_ue"]):
|
||||||
ue_cap
|
|
||||||
and (ue_cap["moy_ue"] is not None)
|
|
||||||
and not np.isnan(ue_cap["moy_ue"])
|
|
||||||
):
|
|
||||||
was_capitalized = True
|
was_capitalized = True
|
||||||
if ue_cap["moy_ue"] > cur_moy_ue or np.isnan(cur_moy_ue):
|
if ue_cap["moy_ue"] > cur_moy_ue or np.isnan(cur_moy_ue):
|
||||||
moy_ue = ue_cap["moy_ue"]
|
moy_ue = ue_cap["moy_ue"]
|
||||||
@ -403,7 +397,7 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
if (not coef_ue) and is_capitalized: # étudiant non inscrit dans l'UE courante
|
if (not coef_ue) and is_capitalized: # étudiant non inscrit dans l'UE courante
|
||||||
if self.is_apc:
|
if self.is_apc:
|
||||||
# Coefs de l'UE capitalisée en formation APC: donné par ses ECTS
|
# Coefs de l'UE capitalisée en formation APC: donné par ses ECTS
|
||||||
ue_capitalized = db.session.get(UniteEns, ue_cap["ue_id"])
|
ue_capitalized = UniteEns.query.get(ue_cap["ue_id"])
|
||||||
coef_ue = ue_capitalized.ects
|
coef_ue = ue_capitalized.ects
|
||||||
if coef_ue is None:
|
if coef_ue is None:
|
||||||
orig_sem = FormSemestre.get_formsemestre(ue_cap["formsemestre_id"])
|
orig_sem = FormSemestre.get_formsemestre(ue_cap["formsemestre_id"])
|
||||||
|
@ -9,10 +9,9 @@
|
|||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from flask import flash, g, url_for
|
from flask import flash, g, Markup, url_for
|
||||||
from markupsafe import Markup
|
|
||||||
|
|
||||||
from app import db, log
|
from app import log
|
||||||
from app.comp import moy_sem
|
from app.comp import moy_sem
|
||||||
from app.comp.aux_stats import StatsMoyenne
|
from app.comp.aux_stats import StatsMoyenne
|
||||||
from app.comp.res_common import ResultatsSemestre
|
from app.comp.res_common import ResultatsSemestre
|
||||||
@ -284,12 +283,12 @@ class NotesTableCompat(ResultatsSemestre):
|
|||||||
]
|
]
|
||||||
return etudids
|
return etudids
|
||||||
|
|
||||||
def etud_has_decision(self, etudid) -> bool:
|
def etud_has_decision(self, etudid):
|
||||||
"""True s'il y a une décision de jury pour cet étudiant émanant de ce formsemestre.
|
"""True s'il y a une décision de jury pour cet étudiant émanant de ce formsemestre.
|
||||||
prend aussi en compte les autorisations de passage.
|
prend aussi en compte les autorisations de passage.
|
||||||
Sous-classée en BUT pour les RCUEs et années.
|
Sous-classée en BUT pour les RCUEs et années.
|
||||||
"""
|
"""
|
||||||
return bool(
|
return (
|
||||||
self.get_etud_decisions_ue(etudid)
|
self.get_etud_decisions_ue(etudid)
|
||||||
or self.get_etud_decision_sem(etudid)
|
or self.get_etud_decision_sem(etudid)
|
||||||
or ScolarAutorisationInscription.query.filter_by(
|
or ScolarAutorisationInscription.query.filter_by(
|
||||||
@ -394,7 +393,7 @@ class NotesTableCompat(ResultatsSemestre):
|
|||||||
de ce module.
|
de ce module.
|
||||||
Évaluation "complete" ssi toutes notes saisies ou en attente.
|
Évaluation "complete" ssi toutes notes saisies ou en attente.
|
||||||
"""
|
"""
|
||||||
modimpl = db.session.get(ModuleImpl, moduleimpl_id)
|
modimpl = ModuleImpl.query.get(moduleimpl_id)
|
||||||
modimpl_results = self.modimpls_results.get(moduleimpl_id)
|
modimpl_results = self.modimpls_results.get(moduleimpl_id)
|
||||||
if not modimpl_results:
|
if not modimpl_results:
|
||||||
return [] # safeguard
|
return [] # safeguard
|
||||||
|
@ -55,9 +55,6 @@ from wtforms.validators import (
|
|||||||
)
|
)
|
||||||
from wtforms.widgets import ListWidget, CheckboxInput
|
from wtforms.widgets import ListWidget, CheckboxInput
|
||||||
|
|
||||||
from app import db
|
|
||||||
from app.auth.models import User
|
|
||||||
from app.entreprises import SIRET_PROVISOIRE_START
|
|
||||||
from app.entreprises.models import (
|
from app.entreprises.models import (
|
||||||
Entreprise,
|
Entreprise,
|
||||||
EntrepriseCorrespondant,
|
EntrepriseCorrespondant,
|
||||||
@ -65,6 +62,9 @@ from app.entreprises.models import (
|
|||||||
EntrepriseSite,
|
EntrepriseSite,
|
||||||
EntrepriseTaxeApprentissage,
|
EntrepriseTaxeApprentissage,
|
||||||
)
|
)
|
||||||
|
from app import db
|
||||||
|
from app.auth.models import User
|
||||||
|
from app.entreprises import SIRET_PROVISOIRE_START
|
||||||
from app.models import Identite, Departement
|
from app.models import Identite, Departement
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
@ -122,13 +122,13 @@ class EntrepriseCreationForm(FlaskForm):
|
|||||||
origine = _build_string_field("Origine du correspondant", required=False)
|
origine = _build_string_field("Origine du correspondant", required=False)
|
||||||
notes = _build_string_field("Notes sur le correspondant", required=False)
|
notes = _build_string_field("Notes sur le correspondant", required=False)
|
||||||
|
|
||||||
submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
|
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||||
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
||||||
|
|
||||||
def validate(self, extra_validators=None):
|
def validate(self):
|
||||||
validate = True
|
validate = True
|
||||||
if not super().validate(extra_validators):
|
if not FlaskForm.validate(self):
|
||||||
return False
|
validate = False
|
||||||
|
|
||||||
if EntreprisePreferences.get_check_siret() and self.siret.data != "":
|
if EntreprisePreferences.get_check_siret() and self.siret.data != "":
|
||||||
siret_data = self.siret.data.strip().replace(" ", "")
|
siret_data = self.siret.data.strip().replace(" ", "")
|
||||||
@ -248,13 +248,13 @@ class SiteCreationForm(FlaskForm):
|
|||||||
codepostal = _build_string_field("Code postal (*)")
|
codepostal = _build_string_field("Code postal (*)")
|
||||||
ville = _build_string_field("Ville (*)")
|
ville = _build_string_field("Ville (*)")
|
||||||
pays = _build_string_field("Pays", required=False)
|
pays = _build_string_field("Pays", required=False)
|
||||||
submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
|
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||||
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
||||||
|
|
||||||
def validate(self, extra_validators=None):
|
def validate(self):
|
||||||
validate = True
|
validate = True
|
||||||
if not super().validate(extra_validators):
|
if not FlaskForm.validate(self):
|
||||||
return False
|
validate = False
|
||||||
|
|
||||||
site = EntrepriseSite.query.filter_by(
|
site = EntrepriseSite.query.filter_by(
|
||||||
entreprise_id=self.hidden_entreprise_id.data, nom=self.nom.data
|
entreprise_id=self.hidden_entreprise_id.data, nom=self.nom.data
|
||||||
@ -278,10 +278,10 @@ class SiteModificationForm(FlaskForm):
|
|||||||
submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE)
|
submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE)
|
||||||
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
||||||
|
|
||||||
def validate(self, extra_validators=None):
|
def validate(self):
|
||||||
validate = True
|
validate = True
|
||||||
if not super().validate(extra_validators):
|
if not FlaskForm.validate(self):
|
||||||
return False
|
validate = False
|
||||||
|
|
||||||
site = EntrepriseSite.query.filter(
|
site = EntrepriseSite.query.filter(
|
||||||
EntrepriseSite.entreprise_id == self.hidden_entreprise_id.data,
|
EntrepriseSite.entreprise_id == self.hidden_entreprise_id.data,
|
||||||
@ -326,7 +326,7 @@ class OffreCreationForm(FlaskForm):
|
|||||||
FileAllowed(["pdf", "docx"], "Fichier .pdf ou .docx uniquement"),
|
FileAllowed(["pdf", "docx"], "Fichier .pdf ou .docx uniquement"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
|
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||||
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -344,10 +344,10 @@ class OffreCreationForm(FlaskForm):
|
|||||||
(dept.id, dept.acronym) for dept in Departement.query.all()
|
(dept.id, dept.acronym) for dept in Departement.query.all()
|
||||||
]
|
]
|
||||||
|
|
||||||
def validate(self, extra_validators=None):
|
def validate(self):
|
||||||
validate = True
|
validate = True
|
||||||
if not super().validate(extra_validators):
|
if not FlaskForm.validate(self):
|
||||||
return False
|
validate = False
|
||||||
|
|
||||||
if len(self.depts.data) < 1:
|
if len(self.depts.data) < 1:
|
||||||
self.depts.errors.append("Choisir au moins un département")
|
self.depts.errors.append("Choisir au moins un département")
|
||||||
@ -392,10 +392,10 @@ class OffreModificationForm(FlaskForm):
|
|||||||
(dept.id, dept.acronym) for dept in Departement.query.all()
|
(dept.id, dept.acronym) for dept in Departement.query.all()
|
||||||
]
|
]
|
||||||
|
|
||||||
def validate(self, extra_validators=None):
|
def validate(self):
|
||||||
validate = True
|
validate = True
|
||||||
if not super().validate(extra_validators):
|
if not FlaskForm.validate(self):
|
||||||
return False
|
validate = False
|
||||||
|
|
||||||
if len(self.depts.data) < 1:
|
if len(self.depts.data) < 1:
|
||||||
self.depts.errors.append("Choisir au moins un département")
|
self.depts.errors.append("Choisir au moins un département")
|
||||||
@ -442,10 +442,10 @@ class CorrespondantCreationForm(FlaskForm):
|
|||||||
"Notes", required=False, render_kw={"class": "form-control"}
|
"Notes", required=False, render_kw={"class": "form-control"}
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate(self, extra_validators=None):
|
def validate(self):
|
||||||
validate = True
|
validate = True
|
||||||
if not super().validate(extra_validators):
|
if not FlaskForm.validate(self):
|
||||||
return False
|
validate = False
|
||||||
|
|
||||||
if not self.telephone.data and not self.mail.data:
|
if not self.telephone.data and not self.mail.data:
|
||||||
msg = "Saisir un moyen de contact (mail ou téléphone)"
|
msg = "Saisir un moyen de contact (mail ou téléphone)"
|
||||||
@ -458,13 +458,13 @@ class CorrespondantCreationForm(FlaskForm):
|
|||||||
class CorrespondantsCreationForm(FlaskForm):
|
class CorrespondantsCreationForm(FlaskForm):
|
||||||
hidden_site_id = HiddenField()
|
hidden_site_id = HiddenField()
|
||||||
correspondants = FieldList(FormField(CorrespondantCreationForm), min_entries=1)
|
correspondants = FieldList(FormField(CorrespondantCreationForm), min_entries=1)
|
||||||
submit = SubmitField("Enregistrer")
|
submit = SubmitField("Envoyer")
|
||||||
cancel = SubmitField("Annuler")
|
cancel = SubmitField("Annuler")
|
||||||
|
|
||||||
def validate(self, extra_validators=None):
|
def validate(self):
|
||||||
validate = True
|
validate = True
|
||||||
if not super().validate(extra_validators):
|
if not FlaskForm.validate(self):
|
||||||
return False
|
validate = False
|
||||||
|
|
||||||
correspondant_list = []
|
correspondant_list = []
|
||||||
for entry in self.correspondants.entries:
|
for entry in self.correspondants.entries:
|
||||||
@ -531,10 +531,10 @@ class CorrespondantModificationForm(FlaskForm):
|
|||||||
.all()
|
.all()
|
||||||
]
|
]
|
||||||
|
|
||||||
def validate(self, extra_validators=None):
|
def validate(self):
|
||||||
validate = True
|
validate = True
|
||||||
if not super().validate(extra_validators):
|
if not FlaskForm.validate(self):
|
||||||
return False
|
validate = False
|
||||||
|
|
||||||
correspondant = EntrepriseCorrespondant.query.filter(
|
correspondant = EntrepriseCorrespondant.query.filter(
|
||||||
EntrepriseCorrespondant.id != self.hidden_correspondant_id.data,
|
EntrepriseCorrespondant.id != self.hidden_correspondant_id.data,
|
||||||
@ -566,7 +566,7 @@ class ContactCreationForm(FlaskForm):
|
|||||||
render_kw={"placeholder": "Tapez le nom de l'utilisateur"},
|
render_kw={"placeholder": "Tapez le nom de l'utilisateur"},
|
||||||
)
|
)
|
||||||
notes = TextAreaField("Notes (*)", validators=[DataRequired(message=CHAMP_REQUIS)])
|
notes = TextAreaField("Notes (*)", validators=[DataRequired(message=CHAMP_REQUIS)])
|
||||||
submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
|
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||||
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
||||||
|
|
||||||
def validate_utilisateur(self, utilisateur):
|
def validate_utilisateur(self, utilisateur):
|
||||||
@ -613,9 +613,8 @@ class ContactModificationForm(FlaskForm):
|
|||||||
class StageApprentissageCreationForm(FlaskForm):
|
class StageApprentissageCreationForm(FlaskForm):
|
||||||
etudiant = _build_string_field(
|
etudiant = _build_string_field(
|
||||||
"Étudiant (*)",
|
"Étudiant (*)",
|
||||||
render_kw={"placeholder": "Tapez le nom de l'étudiant", "autocomplete": "off"},
|
render_kw={"placeholder": "Tapez le nom de l'étudiant"},
|
||||||
)
|
)
|
||||||
etudid = HiddenField()
|
|
||||||
type_offre = SelectField(
|
type_offre = SelectField(
|
||||||
"Type de l'offre (*)",
|
"Type de l'offre (*)",
|
||||||
choices=[("Stage"), ("Alternance")],
|
choices=[("Stage"), ("Alternance")],
|
||||||
@ -628,12 +627,12 @@ class StageApprentissageCreationForm(FlaskForm):
|
|||||||
"Date fin (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
|
"Date fin (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
|
||||||
)
|
)
|
||||||
notes = TextAreaField("Notes")
|
notes = TextAreaField("Notes")
|
||||||
submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
|
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||||
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
||||||
|
|
||||||
def validate(self, extra_validators=None):
|
def validate(self):
|
||||||
validate = True
|
validate = True
|
||||||
if not super().validate(extra_validators):
|
if not FlaskForm.validate(self):
|
||||||
validate = False
|
validate = False
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -647,27 +646,64 @@ class StageApprentissageCreationForm(FlaskForm):
|
|||||||
|
|
||||||
return validate
|
return validate
|
||||||
|
|
||||||
def validate_etudid(self, field):
|
def validate_etudiant(self, etudiant):
|
||||||
"L'etudid doit avoit été placé par le JS"
|
etudiant_data = etudiant.data.upper().strip()
|
||||||
etudid = int(field.data) if field.data else None
|
stm = text(
|
||||||
etudiant = db.session.get(Identite, etudid) if etudid is not None else None
|
"SELECT id, CONCAT(nom, ' ', prenom) as nom_prenom FROM Identite WHERE CONCAT(nom, ' ', prenom)=:nom_prenom"
|
||||||
|
)
|
||||||
|
etudiant = (
|
||||||
|
Identite.query.from_statement(stm).params(nom_prenom=etudiant_data).first()
|
||||||
|
)
|
||||||
if etudiant is None:
|
if etudiant is None:
|
||||||
raise ValidationError("Étudiant introuvable (sélectionnez dans la liste)")
|
raise ValidationError("Champ incorrect (selectionnez dans la liste)")
|
||||||
|
|
||||||
|
|
||||||
class FrenchFloatField(StringField):
|
class StageApprentissageModificationForm(FlaskForm):
|
||||||
"A field allowing to enter . or ,"
|
etudiant = _build_string_field(
|
||||||
|
"Étudiant (*)",
|
||||||
|
render_kw={"placeholder": "Tapez le nom de l'étudiant"},
|
||||||
|
)
|
||||||
|
type_offre = SelectField(
|
||||||
|
"Type de l'offre (*)",
|
||||||
|
choices=[("Stage"), ("Alternance")],
|
||||||
|
validators=[DataRequired(message=CHAMP_REQUIS)],
|
||||||
|
)
|
||||||
|
date_debut = DateField(
|
||||||
|
"Date début (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
|
||||||
|
)
|
||||||
|
date_fin = DateField(
|
||||||
|
"Date fin (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
|
||||||
|
)
|
||||||
|
notes = TextAreaField("Notes")
|
||||||
|
submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE)
|
||||||
|
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
||||||
|
|
||||||
def process_formdata(self, valuelist):
|
def validate(self):
|
||||||
"catch incoming data"
|
validate = True
|
||||||
if not valuelist:
|
if not FlaskForm.validate(self):
|
||||||
return
|
validate = False
|
||||||
try:
|
|
||||||
value = valuelist[0].replace(",", ".")
|
if (
|
||||||
self.data = float(value)
|
self.date_debut.data
|
||||||
except ValueError as exc:
|
and self.date_fin.data
|
||||||
self.data = None
|
and self.date_debut.data > self.date_fin.data
|
||||||
raise ValueError(self.gettext("Not a valid decimal value.")) from exc
|
):
|
||||||
|
self.date_debut.errors.append("Les dates sont incompatibles")
|
||||||
|
self.date_fin.errors.append("Les dates sont incompatibles")
|
||||||
|
validate = False
|
||||||
|
|
||||||
|
return validate
|
||||||
|
|
||||||
|
def validate_etudiant(self, etudiant):
|
||||||
|
etudiant_data = etudiant.data.upper().strip()
|
||||||
|
stm = text(
|
||||||
|
"SELECT id, CONCAT(nom, ' ', prenom) as nom_prenom FROM Identite WHERE CONCAT(nom, ' ', prenom)=:nom_prenom"
|
||||||
|
)
|
||||||
|
etudiant = (
|
||||||
|
Identite.query.from_statement(stm).params(nom_prenom=etudiant_data).first()
|
||||||
|
)
|
||||||
|
if etudiant is None:
|
||||||
|
raise ValidationError("Champ incorrect (selectionnez dans la liste)")
|
||||||
|
|
||||||
|
|
||||||
class TaxeApprentissageForm(FlaskForm):
|
class TaxeApprentissageForm(FlaskForm):
|
||||||
@ -684,26 +720,25 @@ class TaxeApprentissageForm(FlaskForm):
|
|||||||
],
|
],
|
||||||
default=int(datetime.now().strftime("%Y")),
|
default=int(datetime.now().strftime("%Y")),
|
||||||
)
|
)
|
||||||
montant = FrenchFloatField(
|
montant = IntegerField(
|
||||||
"Montant (*)",
|
"Montant (*)",
|
||||||
validators=[
|
validators=[
|
||||||
DataRequired(message=CHAMP_REQUIS),
|
DataRequired(message=CHAMP_REQUIS),
|
||||||
# NumberRange(
|
NumberRange(
|
||||||
# min=0.1,
|
min=1,
|
||||||
# max=1e8,
|
message="Le montant doit être supérieur à 0",
|
||||||
# message="Le montant doit être supérieur à 0",
|
),
|
||||||
# ),
|
|
||||||
],
|
],
|
||||||
default=1,
|
default=1,
|
||||||
)
|
)
|
||||||
notes = TextAreaField("Notes")
|
notes = TextAreaField("Notes")
|
||||||
submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
|
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||||
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
||||||
|
|
||||||
def validate(self, extra_validators=None):
|
def validate(self):
|
||||||
validate = True
|
validate = True
|
||||||
if not super().validate(extra_validators):
|
if not FlaskForm.validate(self):
|
||||||
return False
|
validate = False
|
||||||
|
|
||||||
taxe = EntrepriseTaxeApprentissage.query.filter_by(
|
taxe = EntrepriseTaxeApprentissage.query.filter_by(
|
||||||
entreprise_id=self.hidden_entreprise_id.data, annee=self.annee.data
|
entreprise_id=self.hidden_entreprise_id.data, annee=self.annee.data
|
||||||
@ -753,12 +788,12 @@ class EnvoiOffreForm(FlaskForm):
|
|||||||
submit = SubmitField("Envoyer")
|
submit = SubmitField("Envoyer")
|
||||||
cancel = SubmitField("Annuler")
|
cancel = SubmitField("Annuler")
|
||||||
|
|
||||||
def validate(self, extra_validators=None):
|
def validate(self):
|
||||||
validate = True
|
validate = True
|
||||||
list_select = True
|
list_select = True
|
||||||
|
|
||||||
if not super().validate(extra_validators):
|
if not FlaskForm.validate(self):
|
||||||
return False
|
validate = False
|
||||||
|
|
||||||
for entry in self.responsables.entries:
|
for entry in self.responsables.entries:
|
||||||
if entry.data:
|
if entry.data:
|
||||||
|
@ -164,10 +164,7 @@ class EntrepriseStageApprentissage(db.Model):
|
|||||||
entreprise_id = db.Column(
|
entreprise_id = db.Column(
|
||||||
db.Integer, db.ForeignKey("are_entreprises.id", ondelete="cascade")
|
db.Integer, db.ForeignKey("are_entreprises.id", ondelete="cascade")
|
||||||
)
|
)
|
||||||
etudid = db.Column(
|
etudid = db.Column(db.Integer)
|
||||||
db.Integer,
|
|
||||||
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
|
||||||
)
|
|
||||||
type_offre = db.Column(db.Text)
|
type_offre = db.Column(db.Text)
|
||||||
date_debut = db.Column(db.Date)
|
date_debut = db.Column(db.Date)
|
||||||
date_fin = db.Column(db.Date)
|
date_fin = db.Column(db.Date)
|
||||||
@ -183,7 +180,7 @@ class EntrepriseTaxeApprentissage(db.Model):
|
|||||||
db.Integer, db.ForeignKey("are_entreprises.id", ondelete="cascade")
|
db.Integer, db.ForeignKey("are_entreprises.id", ondelete="cascade")
|
||||||
)
|
)
|
||||||
annee = db.Column(db.Integer)
|
annee = db.Column(db.Integer)
|
||||||
montant = db.Column(db.Float)
|
montant = db.Column(db.Integer)
|
||||||
notes = db.Column(db.Text)
|
notes = db.Column(db.Text)
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@ from app.entreprises.forms import (
|
|||||||
ContactCreationForm,
|
ContactCreationForm,
|
||||||
ContactModificationForm,
|
ContactModificationForm,
|
||||||
StageApprentissageCreationForm,
|
StageApprentissageCreationForm,
|
||||||
|
StageApprentissageModificationForm,
|
||||||
EnvoiOffreForm,
|
EnvoiOffreForm,
|
||||||
AjoutFichierForm,
|
AjoutFichierForm,
|
||||||
TaxeApprentissageForm,
|
TaxeApprentissageForm,
|
||||||
@ -238,7 +239,7 @@ def delete_validation_entreprise(entreprise_id):
|
|||||||
text=f"Non validation de la fiche entreprise ({entreprise.nom})",
|
text=f"Non validation de la fiche entreprise ({entreprise.nom})",
|
||||||
)
|
)
|
||||||
db.session.add(log)
|
db.session.add(log)
|
||||||
flash("L'entreprise a été supprimée de la liste des entreprises à valider.")
|
flash("L'entreprise a été supprimé de la liste des entreprise à valider.")
|
||||||
return redirect(url_for("entreprises.validation"))
|
return redirect(url_for("entreprises.validation"))
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/form_confirmation.j2",
|
"entreprises/form_confirmation.j2",
|
||||||
@ -769,7 +770,7 @@ def delete_taxe_apprentissage(entreprise_id, taxe_id):
|
|||||||
)
|
)
|
||||||
db.session.add(log)
|
db.session.add(log)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash("La taxe d'apprentissage a été supprimée de la liste.")
|
flash("La taxe d'apprentissage a été supprimé de la liste.")
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for("entreprises.fiche_entreprise", entreprise_id=taxe.entreprise_id)
|
url_for("entreprises.fiche_entreprise", entreprise_id=taxe.entreprise_id)
|
||||||
)
|
)
|
||||||
@ -965,7 +966,7 @@ def delete_offre(entreprise_id, offre_id):
|
|||||||
)
|
)
|
||||||
db.session.add(log)
|
db.session.add(log)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash("L'offre a été supprimée de la fiche entreprise.")
|
flash("L'offre a été supprimé de la fiche entreprise.")
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for("entreprises.fiche_entreprise", entreprise_id=offre.entreprise_id)
|
url_for("entreprises.fiche_entreprise", entreprise_id=offre.entreprise_id)
|
||||||
)
|
)
|
||||||
@ -1472,8 +1473,7 @@ def delete_contact(entreprise_id, contact_id):
|
|||||||
@permission_required(Permission.RelationsEntreprisesChange)
|
@permission_required(Permission.RelationsEntreprisesChange)
|
||||||
def add_stage_apprentissage(entreprise_id):
|
def add_stage_apprentissage(entreprise_id):
|
||||||
"""
|
"""
|
||||||
Permet d'ajouter un étudiant ayant réalisé un stage ou alternance
|
Permet d'ajouter un étudiant ayant réalisé un stage ou une alternance sur la fiche entreprise de l'entreprise
|
||||||
sur la fiche de l'entreprise
|
|
||||||
"""
|
"""
|
||||||
entreprise = Entreprise.query.filter_by(
|
entreprise = Entreprise.query.filter_by(
|
||||||
id=entreprise_id, visible=True
|
id=entreprise_id, visible=True
|
||||||
@ -1484,8 +1484,15 @@ def add_stage_apprentissage(entreprise_id):
|
|||||||
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise_id)
|
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise_id)
|
||||||
)
|
)
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
etudid = form.etudid.data
|
etudiant_nomcomplet = form.etudiant.data.upper().strip()
|
||||||
etudiant = Identite.query.get_or_404(etudid)
|
stm = text(
|
||||||
|
"SELECT id, CONCAT(nom, ' ', prenom) as nom_prenom FROM Identite WHERE CONCAT(nom, ' ', prenom)=:nom_prenom"
|
||||||
|
)
|
||||||
|
etudiant = (
|
||||||
|
Identite.query.from_statement(stm)
|
||||||
|
.params(nom_prenom=etudiant_nomcomplet)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
formation = etudiant.inscription_courante_date(
|
formation = etudiant.inscription_courante_date(
|
||||||
form.date_debut.data, form.date_fin.data
|
form.date_debut.data, form.date_fin.data
|
||||||
)
|
)
|
||||||
@ -1531,7 +1538,7 @@ def add_stage_apprentissage(entreprise_id):
|
|||||||
@permission_required(Permission.RelationsEntreprisesChange)
|
@permission_required(Permission.RelationsEntreprisesChange)
|
||||||
def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
|
def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
|
||||||
"""
|
"""
|
||||||
Permet de modifier un étudiant ayant réalisé un stage ou alternance sur la fiche de l'entreprise
|
Permet de modifier un étudiant ayant réalisé un stage ou une alternance sur la fiche entreprise de l'entreprise
|
||||||
"""
|
"""
|
||||||
stage_apprentissage = EntrepriseStageApprentissage.query.filter_by(
|
stage_apprentissage = EntrepriseStageApprentissage.query.filter_by(
|
||||||
id=stage_apprentissage_id, entreprise_id=entreprise_id
|
id=stage_apprentissage_id, entreprise_id=entreprise_id
|
||||||
@ -1541,14 +1548,21 @@ def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
|
|||||||
etudiant = Identite.query.filter_by(id=stage_apprentissage.etudid).first_or_404(
|
etudiant = Identite.query.filter_by(id=stage_apprentissage.etudid).first_or_404(
|
||||||
description=f"etudiant {stage_apprentissage.etudid} inconnue"
|
description=f"etudiant {stage_apprentissage.etudid} inconnue"
|
||||||
)
|
)
|
||||||
form = StageApprentissageCreationForm()
|
form = StageApprentissageModificationForm()
|
||||||
if request.method == "POST" and form.cancel.data:
|
if request.method == "POST" and form.cancel.data:
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise_id)
|
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise_id)
|
||||||
)
|
)
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
etudid = form.etudid.data
|
etudiant_nomcomplet = form.etudiant.data.upper().strip()
|
||||||
etudiant = Identite.query.get_or_404(etudid)
|
stm = text(
|
||||||
|
"SELECT id, CONCAT(nom, ' ', prenom) as nom_prenom FROM Identite WHERE CONCAT(nom, ' ', prenom)=:nom_prenom"
|
||||||
|
)
|
||||||
|
etudiant = (
|
||||||
|
Identite.query.from_statement(stm)
|
||||||
|
.params(nom_prenom=etudiant_nomcomplet)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
formation = etudiant.inscription_courante_date(
|
formation = etudiant.inscription_courante_date(
|
||||||
form.date_debut.data, form.date_fin.data
|
form.date_debut.data, form.date_fin.data
|
||||||
)
|
)
|
||||||
@ -1563,7 +1577,6 @@ def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
|
|||||||
formation.formsemestre.formsemestre_id if formation else None,
|
formation.formsemestre.formsemestre_id if formation else None,
|
||||||
)
|
)
|
||||||
stage_apprentissage.notes = form.notes.data.strip()
|
stage_apprentissage.notes = form.notes.data.strip()
|
||||||
db.session.add(stage_apprentissage)
|
|
||||||
log = EntrepriseHistorique(
|
log = EntrepriseHistorique(
|
||||||
authenticated_user=current_user.user_name,
|
authenticated_user=current_user.user_name,
|
||||||
entreprise_id=stage_apprentissage.entreprise_id,
|
entreprise_id=stage_apprentissage.entreprise_id,
|
||||||
@ -1580,9 +1593,7 @@ def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
elif request.method == "GET":
|
elif request.method == "GET":
|
||||||
form.etudiant.data = f"""{sco_etud.format_nom(etudiant.nom)} {
|
form.etudiant.data = f"{sco_etud.format_nom(etudiant.nom)} {sco_etud.format_prenom(etudiant.prenom)}"
|
||||||
sco_etud.format_prenom(etudiant.prenom)}"""
|
|
||||||
form.etudid.data = etudiant.id
|
|
||||||
form.type_offre.data = stage_apprentissage.type_offre
|
form.type_offre.data = stage_apprentissage.type_offre
|
||||||
form.date_debut.data = stage_apprentissage.date_debut
|
form.date_debut.data = stage_apprentissage.date_debut
|
||||||
form.date_fin.data = stage_apprentissage.date_fin
|
form.date_fin.data = stage_apprentissage.date_fin
|
||||||
|
@ -65,7 +65,6 @@ class CodesDecisionsForm(FlaskForm):
|
|||||||
ADJ = _build_code_field("ADJ")
|
ADJ = _build_code_field("ADJ")
|
||||||
ADJR = _build_code_field("ADJR")
|
ADJR = _build_code_field("ADJR")
|
||||||
ADM = _build_code_field("ADM")
|
ADM = _build_code_field("ADM")
|
||||||
ADSUP = _build_code_field("ADSUP")
|
|
||||||
AJ = _build_code_field("AJ")
|
AJ = _build_code_field("AJ")
|
||||||
ATB = _build_code_field("ATB")
|
ATB = _build_code_field("ATB")
|
||||||
ATJ = _build_code_field("ATJ")
|
ATJ = _build_code_field("ATJ")
|
||||||
@ -82,8 +81,7 @@ class CodesDecisionsForm(FlaskForm):
|
|||||||
|
|
||||||
NOTES_FMT = StringField(
|
NOTES_FMT = StringField(
|
||||||
label="Format notes exportées",
|
label="Format notes exportées",
|
||||||
description="""Format des notes. Par défaut
|
description="""Format des notes. Par défaut <tt style="font-family: monotype;">%3.2f</tt> (deux chiffres après la virgule)""",
|
||||||
<tt style="font-family: monotype;">%3.2f</tt> (deux chiffres après la virgule)""",
|
|
||||||
validators=[
|
validators=[
|
||||||
validators.Length(
|
validators.Length(
|
||||||
max=SHORT_STR_LEN,
|
max=SHORT_STR_LEN,
|
||||||
|
@ -81,3 +81,5 @@ from app.models.but_refcomp import (
|
|||||||
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
||||||
|
|
||||||
from app.models.config import ScoDocSiteConfig
|
from app.models.config import ScoDocSiteConfig
|
||||||
|
|
||||||
|
from app.models.assiduites import Assiduite, Justificatif
|
||||||
|
341
app/models/assiduites.py
Normal file
341
app/models/assiduites.py
Normal file
@ -0,0 +1,341 @@
|
|||||||
|
# -*- coding: UTF-8 -*
|
||||||
|
"""Gestion de l'assiduité (assiduités + justificatifs)
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from app import db
|
||||||
|
from app.models import ModuleImpl
|
||||||
|
from app.models.etudiants import Identite
|
||||||
|
from app.scodoc.sco_utils import (
|
||||||
|
EtatAssiduite,
|
||||||
|
EtatJustificatif,
|
||||||
|
localize_datetime,
|
||||||
|
is_period_overlapping,
|
||||||
|
)
|
||||||
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
|
from app.scodoc.sco_utils import (
|
||||||
|
EtatAssiduite,
|
||||||
|
EtatJustificatif,
|
||||||
|
localize_datetime,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Assiduite(db.Model):
|
||||||
|
"""
|
||||||
|
Représente une assiduité:
|
||||||
|
- une plage horaire lié à un état et un étudiant
|
||||||
|
- un module si spécifiée
|
||||||
|
- une description si spécifiée
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "assiduites"
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True, nullable=False)
|
||||||
|
assiduite_id = db.synonym("id")
|
||||||
|
|
||||||
|
date_debut = db.Column(
|
||||||
|
db.DateTime(timezone=True), server_default=db.func.now(), nullable=False
|
||||||
|
)
|
||||||
|
date_fin = db.Column(
|
||||||
|
db.DateTime(timezone=True), server_default=db.func.now(), nullable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
moduleimpl_id = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey("notes_moduleimpl.id", ondelete="SET NULL"),
|
||||||
|
)
|
||||||
|
etudid = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||||
|
index=True,
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
etat = db.Column(db.Integer, nullable=False)
|
||||||
|
|
||||||
|
desc = db.Column(db.Text)
|
||||||
|
|
||||||
|
entry_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||||
|
|
||||||
|
user_id = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey("user.id", ondelete="SET NULL"),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
est_just = db.Column(db.Boolean, server_default="false", nullable=False)
|
||||||
|
|
||||||
|
def to_dict(self, format_api=True) -> dict:
|
||||||
|
"""Retourne la représentation json de l'assiduité"""
|
||||||
|
etat = self.etat
|
||||||
|
|
||||||
|
if format_api:
|
||||||
|
etat = EtatAssiduite.inverse().get(self.etat).name
|
||||||
|
data = {
|
||||||
|
"assiduite_id": self.id,
|
||||||
|
"etudid": self.etudid,
|
||||||
|
"moduleimpl_id": self.moduleimpl_id,
|
||||||
|
"date_debut": self.date_debut,
|
||||||
|
"date_fin": self.date_fin,
|
||||||
|
"etat": etat,
|
||||||
|
"desc": self.desc,
|
||||||
|
"entry_date": self.entry_date,
|
||||||
|
"user_id": self.user_id,
|
||||||
|
"est_just": self.est_just,
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_assiduite(
|
||||||
|
cls,
|
||||||
|
etud: Identite,
|
||||||
|
date_debut: datetime,
|
||||||
|
date_fin: datetime,
|
||||||
|
etat: EtatAssiduite,
|
||||||
|
moduleimpl: ModuleImpl = None,
|
||||||
|
description: str = None,
|
||||||
|
entry_date: datetime = None,
|
||||||
|
user_id: int = None,
|
||||||
|
est_just: bool = False,
|
||||||
|
) -> object or int:
|
||||||
|
"""Créer une nouvelle assiduité pour l'étudiant"""
|
||||||
|
# Vérification de non duplication des périodes
|
||||||
|
assiduites: list[Assiduite] = etud.assiduites
|
||||||
|
if is_period_conflicting(date_debut, date_fin, assiduites, Assiduite):
|
||||||
|
raise ScoValueError(
|
||||||
|
"Duplication des assiduités (la période rentrée rentre en conflit avec une assiduité enregistrée)"
|
||||||
|
)
|
||||||
|
if moduleimpl is not None:
|
||||||
|
# Vérification de l'existence du module pour l'étudiant
|
||||||
|
if moduleimpl.est_inscrit(etud):
|
||||||
|
nouv_assiduite = Assiduite(
|
||||||
|
date_debut=date_debut,
|
||||||
|
date_fin=date_fin,
|
||||||
|
etat=etat,
|
||||||
|
etudiant=etud,
|
||||||
|
moduleimpl_id=moduleimpl.id,
|
||||||
|
desc=description,
|
||||||
|
entry_date=entry_date,
|
||||||
|
user_id=user_id,
|
||||||
|
est_just=est_just,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ScoValueError("L'étudiant n'est pas inscrit au moduleimpl")
|
||||||
|
else:
|
||||||
|
nouv_assiduite = Assiduite(
|
||||||
|
date_debut=date_debut,
|
||||||
|
date_fin=date_fin,
|
||||||
|
etat=etat,
|
||||||
|
etudiant=etud,
|
||||||
|
desc=description,
|
||||||
|
entry_date=entry_date,
|
||||||
|
user_id=user_id,
|
||||||
|
est_just=est_just,
|
||||||
|
)
|
||||||
|
|
||||||
|
return nouv_assiduite
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fast_create_assiduite(
|
||||||
|
cls,
|
||||||
|
etudid: int,
|
||||||
|
date_debut: datetime,
|
||||||
|
date_fin: datetime,
|
||||||
|
etat: EtatAssiduite,
|
||||||
|
moduleimpl_id: int = None,
|
||||||
|
description: str = None,
|
||||||
|
entry_date: datetime = None,
|
||||||
|
est_just: bool = False,
|
||||||
|
) -> object or int:
|
||||||
|
"""Créer une nouvelle assiduité pour l'étudiant"""
|
||||||
|
# Vérification de non duplication des périodes
|
||||||
|
|
||||||
|
nouv_assiduite = Assiduite(
|
||||||
|
date_debut=date_debut,
|
||||||
|
date_fin=date_fin,
|
||||||
|
etat=etat,
|
||||||
|
etudid=etudid,
|
||||||
|
moduleimpl_id=moduleimpl_id,
|
||||||
|
desc=description,
|
||||||
|
entry_date=entry_date,
|
||||||
|
est_just=est_just,
|
||||||
|
)
|
||||||
|
|
||||||
|
return nouv_assiduite
|
||||||
|
|
||||||
|
|
||||||
|
class Justificatif(db.Model):
|
||||||
|
"""
|
||||||
|
Représente un justificatif:
|
||||||
|
- une plage horaire lié à un état et un étudiant
|
||||||
|
- une raison si spécifiée
|
||||||
|
- un fichier si spécifié
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "justificatifs"
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
justif_id = db.synonym("id")
|
||||||
|
|
||||||
|
date_debut = db.Column(
|
||||||
|
db.DateTime(timezone=True), server_default=db.func.now(), nullable=False
|
||||||
|
)
|
||||||
|
date_fin = db.Column(
|
||||||
|
db.DateTime(timezone=True), server_default=db.func.now(), nullable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
etudid = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||||
|
index=True,
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
etat = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
entry_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||||
|
|
||||||
|
user_id = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey("user.id", ondelete="SET NULL"),
|
||||||
|
nullable=True,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
raison = db.Column(db.Text())
|
||||||
|
|
||||||
|
# Archive_id -> sco_archives_justificatifs.py
|
||||||
|
fichier = db.Column(db.Text())
|
||||||
|
|
||||||
|
def to_dict(self, format_api: bool = False) -> dict:
|
||||||
|
"""transformation de l'objet en dictionnaire sérialisable"""
|
||||||
|
|
||||||
|
etat = self.etat
|
||||||
|
|
||||||
|
if format_api:
|
||||||
|
etat = EtatJustificatif.inverse().get(self.etat).name
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"justif_id": self.justif_id,
|
||||||
|
"etudid": self.etudid,
|
||||||
|
"date_debut": self.date_debut,
|
||||||
|
"date_fin": self.date_fin,
|
||||||
|
"etat": etat,
|
||||||
|
"raison": self.raison,
|
||||||
|
"fichier": self.fichier,
|
||||||
|
"entry_date": self.entry_date,
|
||||||
|
"user_id": self.user_id,
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_justificatif(
|
||||||
|
cls,
|
||||||
|
etud: Identite,
|
||||||
|
date_debut: datetime,
|
||||||
|
date_fin: datetime,
|
||||||
|
etat: EtatJustificatif,
|
||||||
|
raison: str = None,
|
||||||
|
entry_date: datetime = None,
|
||||||
|
user_id: int = None,
|
||||||
|
) -> object or int:
|
||||||
|
"""Créer un nouveau justificatif pour l'étudiant"""
|
||||||
|
nouv_justificatif = Justificatif(
|
||||||
|
date_debut=date_debut,
|
||||||
|
date_fin=date_fin,
|
||||||
|
etat=etat,
|
||||||
|
etudiant=etud,
|
||||||
|
raison=raison,
|
||||||
|
entry_date=entry_date,
|
||||||
|
user_id=user_id,
|
||||||
|
)
|
||||||
|
return nouv_justificatif
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fast_create_justificatif(
|
||||||
|
cls,
|
||||||
|
etudid: int,
|
||||||
|
date_debut: datetime,
|
||||||
|
date_fin: datetime,
|
||||||
|
etat: EtatJustificatif,
|
||||||
|
raison: str = None,
|
||||||
|
entry_date: datetime = None,
|
||||||
|
) -> object or int:
|
||||||
|
"""Créer un nouveau justificatif pour l'étudiant"""
|
||||||
|
|
||||||
|
nouv_justificatif = Justificatif(
|
||||||
|
date_debut=date_debut,
|
||||||
|
date_fin=date_fin,
|
||||||
|
etat=etat,
|
||||||
|
etudid=etudid,
|
||||||
|
raison=raison,
|
||||||
|
entry_date=entry_date,
|
||||||
|
)
|
||||||
|
|
||||||
|
return nouv_justificatif
|
||||||
|
|
||||||
|
|
||||||
|
def is_period_conflicting(
|
||||||
|
date_debut: datetime,
|
||||||
|
date_fin: datetime,
|
||||||
|
collection: list[Assiduite or Justificatif],
|
||||||
|
collection_cls: Assiduite or Justificatif,
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Vérifie si une date n'entre pas en collision
|
||||||
|
avec les justificatifs ou assiduites déjà présentes
|
||||||
|
"""
|
||||||
|
|
||||||
|
date_debut = localize_datetime(date_debut)
|
||||||
|
date_fin = localize_datetime(date_fin)
|
||||||
|
|
||||||
|
if (
|
||||||
|
collection.filter_by(date_debut=date_debut, date_fin=date_fin).first()
|
||||||
|
is not None
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
|
||||||
|
count: int = collection.filter(
|
||||||
|
collection_cls.date_debut < date_fin, collection_cls.date_fin > date_debut
|
||||||
|
).count()
|
||||||
|
|
||||||
|
return count > 0
|
||||||
|
|
||||||
|
|
||||||
|
def compute_assiduites_justified(
|
||||||
|
justificatifs: Justificatif = Justificatif, reset: bool = False
|
||||||
|
) -> list[int]:
|
||||||
|
"""Calcule et modifie les champs "est_just" de chaque assiduité lié à l'étud
|
||||||
|
retourne la liste des assiduite_id justifiées
|
||||||
|
|
||||||
|
Si reset alors : met à false toutes les assiduités non justifiées par les justificatifs donnés
|
||||||
|
"""
|
||||||
|
|
||||||
|
list_assiduites_id: set[int] = set()
|
||||||
|
for justi in justificatifs:
|
||||||
|
assiduites: Assiduite = (
|
||||||
|
Assiduite.query.join(Justificatif, Justificatif.etudid == Assiduite.etudid)
|
||||||
|
.filter(justi.etat == EtatJustificatif.VALIDE)
|
||||||
|
.filter(
|
||||||
|
Assiduite.date_debut < justi.date_fin,
|
||||||
|
Assiduite.date_fin > justi.date_debut,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for assi in assiduites:
|
||||||
|
assi.est_just = True
|
||||||
|
list_assiduites_id.add(assi.id)
|
||||||
|
db.session.add(assi)
|
||||||
|
|
||||||
|
if reset:
|
||||||
|
un_justified: Assiduite = Assiduite.query.filter(
|
||||||
|
Assiduite.id.not_in(list_assiduites_id)
|
||||||
|
).join(Justificatif, Justificatif.etudid == Assiduite.etudid)
|
||||||
|
|
||||||
|
for assi in un_justified:
|
||||||
|
assi.est_just = False
|
||||||
|
db.session.add(assi)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return list(list_assiduites_id)
|
@ -9,7 +9,6 @@ from datetime import datetime
|
|||||||
import functools
|
import functools
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
from flask import g
|
|
||||||
from flask_sqlalchemy.query import Query
|
from flask_sqlalchemy.query import Query
|
||||||
from sqlalchemy.orm import class_mapper
|
from sqlalchemy.orm import class_mapper
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
@ -94,11 +93,6 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
|||||||
backref="referentiel_competence",
|
backref="referentiel_competence",
|
||||||
order_by="Formation.acronyme, Formation.version",
|
order_by="Formation.acronyme, Formation.version",
|
||||||
)
|
)
|
||||||
validations_annee = db.relationship(
|
|
||||||
"ApcValidationAnnee",
|
|
||||||
backref="referentiel_competence",
|
|
||||||
lazy="dynamic",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<ApcReferentielCompetences {self.id} {self.specialite!r} {self.departement!r}>"
|
return f"<ApcReferentielCompetences {self.id} {self.specialite!r} {self.departement!r}>"
|
||||||
@ -364,9 +358,6 @@ class ApcNiveau(db.Model, XMLModel):
|
|||||||
return f"""<{self.__class__.__name__} {self.id} ordre={self.ordre!r} annee={
|
return f"""<{self.__class__.__name__} {self.id} ordre={self.ordre!r} annee={
|
||||||
self.annee!r} {self.competence!r}>"""
|
self.annee!r} {self.competence!r}>"""
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"""{self.competence.titre} niveau {self.ordre}"""
|
|
||||||
|
|
||||||
def to_dict(self, with_app_critiques=True):
|
def to_dict(self, with_app_critiques=True):
|
||||||
"as a dict, recursif (ou non) sur les AC"
|
"as a dict, recursif (ou non) sur les AC"
|
||||||
return {
|
return {
|
||||||
@ -397,9 +388,7 @@ class ApcNiveau(db.Model, XMLModel):
|
|||||||
return (
|
return (
|
||||||
ApcParcours.query.join(ApcAnneeParcours)
|
ApcParcours.query.join(ApcAnneeParcours)
|
||||||
.filter_by(ordre=annee)
|
.filter_by(ordre=annee)
|
||||||
.join(ApcParcoursNiveauCompetence)
|
.join(ApcParcoursNiveauCompetence, ApcCompetence, ApcNiveau)
|
||||||
.join(ApcCompetence)
|
|
||||||
.join(ApcNiveau)
|
|
||||||
.filter_by(id=self.id)
|
.filter_by(id=self.id)
|
||||||
.order_by(ApcParcours.numero, ApcParcours.code)
|
.order_by(ApcParcours.numero, ApcParcours.code)
|
||||||
.all()
|
.all()
|
||||||
@ -423,20 +412,6 @@ class ApcNiveau(db.Model, XMLModel):
|
|||||||
(dans ce cas, spécifier referentiel_competence)
|
(dans ce cas, spécifier referentiel_competence)
|
||||||
Si competence est indiquée, filtre les niveaux de cette compétence.
|
Si competence est indiquée, filtre les niveaux de cette compétence.
|
||||||
"""
|
"""
|
||||||
key = (
|
|
||||||
parcour.id if parcour else None,
|
|
||||||
annee,
|
|
||||||
referentiel_competence.id if referentiel_competence else None,
|
|
||||||
competence.id if competence else None,
|
|
||||||
)
|
|
||||||
_cache = getattr(g, "_niveaux_annee_de_parcours_cache", None)
|
|
||||||
if _cache:
|
|
||||||
result = g._niveaux_annee_de_parcours_cache.get(key, False)
|
|
||||||
if result is not False:
|
|
||||||
return result
|
|
||||||
else:
|
|
||||||
g._niveaux_annee_de_parcours_cache = {}
|
|
||||||
_cache = g._niveaux_annee_de_parcours_cache
|
|
||||||
if annee not in {1, 2, 3}:
|
if annee not in {1, 2, 3}:
|
||||||
raise ValueError("annee invalide pour un parcours BUT")
|
raise ValueError("annee invalide pour un parcours BUT")
|
||||||
referentiel_competence = (
|
referentiel_competence = (
|
||||||
@ -453,13 +428,10 @@ class ApcNiveau(db.Model, XMLModel):
|
|||||||
)
|
)
|
||||||
if competence is not None:
|
if competence is not None:
|
||||||
query = query.filter(ApcCompetence.id == competence.id)
|
query = query.filter(ApcCompetence.id == competence.id)
|
||||||
result = query.all()
|
return query.all()
|
||||||
_cache[key] = result
|
|
||||||
return result
|
|
||||||
|
|
||||||
annee_parcour: ApcAnneeParcours = parcour.annees.filter_by(ordre=annee).first()
|
annee_parcour: ApcAnneeParcours = parcour.annees.filter_by(ordre=annee).first()
|
||||||
if not annee_parcour:
|
if not annee_parcour:
|
||||||
_cache[key] = []
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
if competence is None:
|
if competence is None:
|
||||||
@ -471,17 +443,9 @@ class ApcNiveau(db.Model, XMLModel):
|
|||||||
for pn in parcour_niveaux
|
for pn in parcour_niveaux
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
niveaux: list[ApcNiveau] = (
|
niveaux: list[ApcNiveau] = competence.niveaux.filter_by(
|
||||||
ApcNiveau.query.filter_by(annee=f"BUT{int(annee)}")
|
annee=f"BUT{int(annee)}"
|
||||||
.join(ApcCompetence)
|
).all()
|
||||||
.filter_by(id=competence.id)
|
|
||||||
.join(ApcParcoursNiveauCompetence)
|
|
||||||
.filter(ApcParcoursNiveauCompetence.niveau == ApcNiveau.ordre)
|
|
||||||
.join(ApcAnneeParcours)
|
|
||||||
.filter_by(parcours_id=parcour.id)
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
_cache[key] = niveaux
|
|
||||||
return niveaux
|
return niveaux
|
||||||
|
|
||||||
|
|
||||||
@ -623,8 +587,7 @@ class ApcParcours(db.Model, XMLModel):
|
|||||||
def query_competences(self) -> Query:
|
def query_competences(self) -> Query:
|
||||||
"Les compétences associées à ce parcours"
|
"Les compétences associées à ce parcours"
|
||||||
return (
|
return (
|
||||||
ApcCompetence.query.join(ApcParcoursNiveauCompetence)
|
ApcCompetence.query.join(ApcParcoursNiveauCompetence, ApcAnneeParcours)
|
||||||
.join(ApcAnneeParcours)
|
|
||||||
.filter_by(parcours_id=self.id)
|
.filter_by(parcours_id=self.id)
|
||||||
.order_by(ApcCompetence.numero)
|
.order_by(ApcCompetence.numero)
|
||||||
)
|
)
|
||||||
@ -633,8 +596,7 @@ class ApcParcours(db.Model, XMLModel):
|
|||||||
"La compétence de titre donné dans ce parcours, ou None"
|
"La compétence de titre donné dans ce parcours, ou None"
|
||||||
return (
|
return (
|
||||||
ApcCompetence.query.filter_by(titre=titre)
|
ApcCompetence.query.filter_by(titre=titre)
|
||||||
.join(ApcParcoursNiveauCompetence)
|
.join(ApcParcoursNiveauCompetence, ApcAnneeParcours)
|
||||||
.join(ApcAnneeParcours)
|
|
||||||
.filter_by(parcours_id=self.id)
|
.filter_by(parcours_id=self.id)
|
||||||
.order_by(ApcCompetence.numero)
|
.order_by(ApcCompetence.numero)
|
||||||
.first()
|
.first()
|
||||||
|
@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
"""Décisions de jury (validations) des RCUE et années du BUT
|
"""Décisions de jury (validations) des RCUE et années du BUT
|
||||||
"""
|
"""
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from flask_sqlalchemy.query import Query
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import CODE_STR_LEN
|
from app.models import CODE_STR_LEN
|
||||||
@ -10,6 +13,8 @@ from app.models.etudiants import Identite
|
|||||||
from app.models.formations import Formation
|
from app.models.formations import Formation
|
||||||
from app.models.formsemestre import FormSemestre
|
from app.models.formsemestre import FormSemestre
|
||||||
from app.models.ues import UniteEns
|
from app.models.ues import UniteEns
|
||||||
|
from app.scodoc import codes_cursus as sco_codes
|
||||||
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
class ApcValidationRCUE(db.Model):
|
class ApcValidationRCUE(db.Model):
|
||||||
@ -17,7 +22,7 @@ class ApcValidationRCUE(db.Model):
|
|||||||
|
|
||||||
aka "regroupements cohérents d'UE" dans le jargon BUT.
|
aka "regroupements cohérents d'UE" dans le jargon BUT.
|
||||||
|
|
||||||
Le formsemestre est l'origine, utilisé pour effacer
|
Le formsemestre est celui du semestre PAIR du niveau de compétence
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__tablename__ = "apc_validation_rcue"
|
__tablename__ = "apc_validation_rcue"
|
||||||
@ -36,7 +41,7 @@ class ApcValidationRCUE(db.Model):
|
|||||||
formsemestre_id = db.Column(
|
formsemestre_id = db.Column(
|
||||||
db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, nullable=True
|
db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, nullable=True
|
||||||
)
|
)
|
||||||
"formsemestre origine du RCUE (celui d'où a été émis la validation)"
|
"formsemestre pair du RCUE"
|
||||||
# Les deux UE associées à ce niveau:
|
# Les deux UE associées à ce niveau:
|
||||||
ue1_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False)
|
ue1_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False)
|
||||||
ue2_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False)
|
ue2_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False)
|
||||||
@ -61,7 +66,7 @@ class ApcValidationRCUE(db.Model):
|
|||||||
return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}: {
|
return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}: {
|
||||||
self.code} enregistrée le {self.date.strftime("%d/%m/%Y")}"""
|
self.code} enregistrée le {self.date.strftime("%d/%m/%Y")}"""
|
||||||
|
|
||||||
def html(self) -> str:
|
def to_html(self) -> str:
|
||||||
"description en HTML"
|
"description en HTML"
|
||||||
return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}:
|
return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}:
|
||||||
<b>{self.code}</b>
|
<b>{self.code}</b>
|
||||||
@ -82,10 +87,6 @@ class ApcValidationRCUE(db.Model):
|
|||||||
"as a dict"
|
"as a dict"
|
||||||
d = dict(self.__dict__)
|
d = dict(self.__dict__)
|
||||||
d.pop("_sa_instance_state", None)
|
d.pop("_sa_instance_state", None)
|
||||||
d["etud"] = self.etud.to_dict_short()
|
|
||||||
d["ue1"] = self.ue1.to_dict()
|
|
||||||
d["ue2"] = self.ue2.to_dict()
|
|
||||||
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def to_dict_bul(self) -> dict:
|
def to_dict_bul(self) -> dict:
|
||||||
@ -108,14 +109,204 @@ class ApcValidationRCUE(db.Model):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Attention: ce n'est pas un modèle mais une classe ordinaire:
|
||||||
|
class RegroupementCoherentUE:
|
||||||
|
"""Le regroupement cohérent d'UE, dans la terminologie du BUT, est le couple d'UEs
|
||||||
|
de la même année (BUT1,2,3) liées au *même niveau de compétence*.
|
||||||
|
|
||||||
|
La moyenne (10/20) au RCUE déclenche la compensation des UE.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
etud: Identite,
|
||||||
|
formsemestre_1: FormSemestre,
|
||||||
|
dec_ue_1: "DecisionsProposeesUE",
|
||||||
|
formsemestre_2: FormSemestre,
|
||||||
|
dec_ue_2: "DecisionsProposeesUE",
|
||||||
|
inscription_etat: str,
|
||||||
|
):
|
||||||
|
ue_1 = dec_ue_1.ue
|
||||||
|
ue_2 = dec_ue_2.ue
|
||||||
|
# Ordonne les UE dans le sens croissant (S1,S2) ou (S3,S4)...
|
||||||
|
if formsemestre_1.semestre_id > formsemestre_2.semestre_id:
|
||||||
|
(ue_1, formsemestre_1), (ue_2, formsemestre_2) = (
|
||||||
|
(ue_2, formsemestre_2),
|
||||||
|
(ue_1, formsemestre_1),
|
||||||
|
)
|
||||||
|
assert formsemestre_1.semestre_id % 2 == 1
|
||||||
|
assert formsemestre_2.semestre_id % 2 == 0
|
||||||
|
assert abs(formsemestre_1.semestre_id - formsemestre_2.semestre_id) == 1
|
||||||
|
assert ue_1.niveau_competence_id == ue_2.niveau_competence_id
|
||||||
|
self.etud = etud
|
||||||
|
self.formsemestre_1 = formsemestre_1
|
||||||
|
"semestre impair"
|
||||||
|
self.ue_1 = ue_1
|
||||||
|
self.formsemestre_2 = formsemestre_2
|
||||||
|
"semestre pair"
|
||||||
|
self.ue_2 = ue_2
|
||||||
|
# Stocke les moyennes d'UE
|
||||||
|
if inscription_etat != scu.INSCRIT:
|
||||||
|
self.moy_rcue = None
|
||||||
|
self.moy_ue_1 = self.moy_ue_2 = "-"
|
||||||
|
self.moy_ue_1_val = self.moy_ue_2_val = 0.0
|
||||||
|
return
|
||||||
|
self.moy_ue_1 = dec_ue_1.moy_ue_with_cap
|
||||||
|
self.moy_ue_1_val = self.moy_ue_1 if self.moy_ue_1 is not None else 0.0
|
||||||
|
self.moy_ue_2 = dec_ue_2.moy_ue_with_cap
|
||||||
|
self.moy_ue_2_val = self.moy_ue_2 if self.moy_ue_2 is not None else 0.0
|
||||||
|
|
||||||
|
# Calcul de la moyenne au RCUE (utilise les moy d'UE capitalisées)
|
||||||
|
if (self.moy_ue_1 is not None) and (self.moy_ue_2 is not None):
|
||||||
|
# Moyenne RCUE (les pondérations par défaut sont 1.)
|
||||||
|
self.moy_rcue = (
|
||||||
|
self.moy_ue_1 * ue_1.coef_rcue + self.moy_ue_2 * ue_2.coef_rcue
|
||||||
|
) / (ue_1.coef_rcue + ue_2.coef_rcue)
|
||||||
|
else:
|
||||||
|
self.moy_rcue = None
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"""<{self.__class__.__name__} {
|
||||||
|
self.ue_1.acronyme}({self.moy_ue_1}) {
|
||||||
|
self.ue_2.acronyme}({self.moy_ue_2})>"""
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"""RCUE {
|
||||||
|
self.ue_1.acronyme}({self.moy_ue_1}) + {
|
||||||
|
self.ue_2.acronyme}({self.moy_ue_2})"""
|
||||||
|
|
||||||
|
def query_validations(
|
||||||
|
self,
|
||||||
|
) -> Query: # list[ApcValidationRCUE]
|
||||||
|
"""Les validations de jury enregistrées pour ce RCUE"""
|
||||||
|
niveau = self.ue_2.niveau_competence
|
||||||
|
|
||||||
|
return (
|
||||||
|
ApcValidationRCUE.query.filter_by(
|
||||||
|
etudid=self.etud.id,
|
||||||
|
)
|
||||||
|
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue2_id)
|
||||||
|
.join(ApcNiveau, UniteEns.niveau_competence_id == ApcNiveau.id)
|
||||||
|
.filter(ApcNiveau.id == niveau.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
def other_ue(self, ue: UniteEns) -> UniteEns:
|
||||||
|
"""L'autre UE du regroupement. Si ue ne fait pas partie du regroupement, ValueError"""
|
||||||
|
if ue.id == self.ue_1.id:
|
||||||
|
return self.ue_2
|
||||||
|
elif ue.id == self.ue_2.id:
|
||||||
|
return self.ue_1
|
||||||
|
raise ValueError(f"ue {ue} hors RCUE {self}")
|
||||||
|
|
||||||
|
def est_enregistre(self) -> bool:
|
||||||
|
"""Vrai si ce RCUE, donc le niveau de compétences correspondant
|
||||||
|
a une décision jury enregistrée
|
||||||
|
"""
|
||||||
|
return self.query_validations().count() > 0
|
||||||
|
|
||||||
|
def est_compensable(self):
|
||||||
|
"""Vrai si ce RCUE est validable (uniquement) par compensation
|
||||||
|
c'est à dire que sa moyenne est > 10 avec une UE < 10.
|
||||||
|
Note: si ADM, est_compensable est faux.
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
(self.moy_rcue is not None)
|
||||||
|
and (self.moy_rcue > sco_codes.BUT_BARRE_RCUE)
|
||||||
|
and (
|
||||||
|
(self.moy_ue_1_val < sco_codes.NOTES_BARRE_GEN)
|
||||||
|
or (self.moy_ue_2_val < sco_codes.NOTES_BARRE_GEN)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def est_suffisant(self) -> bool:
|
||||||
|
"""Vrai si ce RCUE est > 8"""
|
||||||
|
return (self.moy_rcue is not None) and (
|
||||||
|
self.moy_rcue > sco_codes.BUT_RCUE_SUFFISANT
|
||||||
|
)
|
||||||
|
|
||||||
|
def est_validable(self) -> bool:
|
||||||
|
"""Vrai si ce RCUE satisfait les conditions pour être validé,
|
||||||
|
c'est à dire que la moyenne des UE qui le constituent soit > 10
|
||||||
|
"""
|
||||||
|
return (self.moy_rcue is not None) and (
|
||||||
|
self.moy_rcue > sco_codes.BUT_BARRE_RCUE
|
||||||
|
)
|
||||||
|
|
||||||
|
def code_valide(self) -> Union[ApcValidationRCUE, None]:
|
||||||
|
"Si ce RCUE est ADM, CMP ou ADJ, la validation. Sinon, None"
|
||||||
|
validation = self.query_validations().first()
|
||||||
|
if (validation is not None) and (
|
||||||
|
validation.code in sco_codes.CODES_RCUE_VALIDES
|
||||||
|
):
|
||||||
|
return validation
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# unused
|
||||||
|
# def find_rcues(
|
||||||
|
# formsemestre: FormSemestre, ue: UniteEns, etud: Identite, inscription_etat: str
|
||||||
|
# ) -> list[RegroupementCoherentUE]:
|
||||||
|
# """Les RCUE (niveau de compétence) à considérer pour cet étudiant dans
|
||||||
|
# ce semestre pour cette UE.
|
||||||
|
|
||||||
|
# Cherche les UEs du même niveau de compétence auxquelles l'étudiant est inscrit.
|
||||||
|
# En cas de redoublement, il peut y en avoir plusieurs, donc plusieurs RCUEs.
|
||||||
|
|
||||||
|
# Résultat: la liste peut être vide.
|
||||||
|
# """
|
||||||
|
# if (ue.niveau_competence is None) or (ue.semestre_idx is None):
|
||||||
|
# return []
|
||||||
|
|
||||||
|
# if ue.semestre_idx % 2: # S1, S3, S5
|
||||||
|
# other_semestre_idx = ue.semestre_idx + 1
|
||||||
|
# else:
|
||||||
|
# other_semestre_idx = ue.semestre_idx - 1
|
||||||
|
|
||||||
|
# cursor = db.session.execute(
|
||||||
|
# text(
|
||||||
|
# """SELECT
|
||||||
|
# ue.id, formsemestre.id
|
||||||
|
# FROM
|
||||||
|
# notes_ue ue,
|
||||||
|
# notes_formsemestre_inscription inscr,
|
||||||
|
# notes_formsemestre formsemestre
|
||||||
|
|
||||||
|
# WHERE
|
||||||
|
# inscr.etudid = :etudid
|
||||||
|
# AND inscr.formsemestre_id = formsemestre.id
|
||||||
|
|
||||||
|
# AND formsemestre.semestre_id = :other_semestre_idx
|
||||||
|
# AND ue.formation_id = formsemestre.formation_id
|
||||||
|
# AND ue.niveau_competence_id = :ue_niveau_competence_id
|
||||||
|
# AND ue.semestre_idx = :other_semestre_idx
|
||||||
|
# """
|
||||||
|
# ),
|
||||||
|
# {
|
||||||
|
# "etudid": etud.id,
|
||||||
|
# "other_semestre_idx": other_semestre_idx,
|
||||||
|
# "ue_niveau_competence_id": ue.niveau_competence_id,
|
||||||
|
# },
|
||||||
|
# )
|
||||||
|
# rcues = []
|
||||||
|
# for ue_id, formsemestre_id in cursor:
|
||||||
|
# other_ue = UniteEns.query.get(ue_id)
|
||||||
|
# other_formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||||
|
# rcues.append(
|
||||||
|
# RegroupementCoherentUE(
|
||||||
|
# etud, formsemestre, ue, other_formsemestre, other_ue, inscription_etat
|
||||||
|
# )
|
||||||
|
# )
|
||||||
|
# # safety check: 1 seul niveau de comp. concerné:
|
||||||
|
# assert len({rcue.ue_1.niveau_competence_id for rcue in rcues}) == 1
|
||||||
|
# return rcues
|
||||||
|
|
||||||
|
|
||||||
class ApcValidationAnnee(db.Model):
|
class ApcValidationAnnee(db.Model):
|
||||||
"""Validation des années du BUT"""
|
"""Validation des années du BUT"""
|
||||||
|
|
||||||
__tablename__ = "apc_validation_annee"
|
__tablename__ = "apc_validation_annee"
|
||||||
# Assure unicité de la décision:
|
# Assure unicité de la décision:
|
||||||
__table_args__ = (
|
__table_args__ = (db.UniqueConstraint("etudid", "annee_scolaire", "ordre"),)
|
||||||
db.UniqueConstraint("etudid", "ordre", "referentiel_competence_id"),
|
|
||||||
)
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
etudid = db.Column(
|
etudid = db.Column(
|
||||||
db.Integer,
|
db.Integer,
|
||||||
@ -128,11 +319,8 @@ class ApcValidationAnnee(db.Model):
|
|||||||
formsemestre_id = db.Column(
|
formsemestre_id = db.Column(
|
||||||
db.Integer, db.ForeignKey("notes_formsemestre.id"), nullable=True
|
db.Integer, db.ForeignKey("notes_formsemestre.id"), nullable=True
|
||||||
)
|
)
|
||||||
"le semestre origine, normalement l'IMPAIR (le 1er) de l'année"
|
"le semestre IMPAIR (le 1er) de l'année"
|
||||||
referentiel_competence_id = db.Column(
|
annee_scolaire = db.Column(db.Integer, nullable=False) # 2021
|
||||||
db.Integer, db.ForeignKey("apc_referentiel_competences.id"), nullable=False
|
|
||||||
)
|
|
||||||
annee_scolaire = db.Column(db.Integer, nullable=False) # eg 2021
|
|
||||||
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||||
code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True)
|
code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True)
|
||||||
|
|
||||||
@ -150,50 +338,25 @@ class ApcValidationAnnee(db.Model):
|
|||||||
"dict pour bulletins"
|
"dict pour bulletins"
|
||||||
return {
|
return {
|
||||||
"annee_scolaire": self.annee_scolaire,
|
"annee_scolaire": self.annee_scolaire,
|
||||||
"date": self.date.isoformat() if self.date else "",
|
"date": self.date.isoformat(),
|
||||||
"code": self.code,
|
"code": self.code,
|
||||||
"ordre": self.ordre,
|
"ordre": self.ordre,
|
||||||
}
|
}
|
||||||
|
|
||||||
def html(self) -> str:
|
|
||||||
"Affichage html"
|
|
||||||
date_str = (
|
|
||||||
f"""le {self.date.strftime("%d/%m/%Y")} à {self.date.strftime("%Hh%M")}"""
|
|
||||||
if self.date
|
|
||||||
else "(sans date)"
|
|
||||||
)
|
|
||||||
link = (
|
|
||||||
self.formsemestre.html_link_status(
|
|
||||||
label=f"{self.formsemestre.titre_formation(with_sem_idx=1)}",
|
|
||||||
title=self.formsemestre.titre_annee(),
|
|
||||||
)
|
|
||||||
if self.formsemestre
|
|
||||||
else "externe/antérieure"
|
|
||||||
)
|
|
||||||
return f"""Validation <b>année BUT{self.ordre}</b> émise par
|
|
||||||
{link}
|
|
||||||
: <b>{self.code}</b>
|
|
||||||
{date_str}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
|
def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
|
||||||
"""
|
"""
|
||||||
Un dict avec les décisions de jury BUT enregistrées:
|
Un dict avec les décisions de jury BUT enregistrées:
|
||||||
- decision_rcue : list[dict]
|
- decision_rcue : list[dict]
|
||||||
- decision_annee : dict (décision issue de ce semestre seulement (à confirmer ?))
|
- decision_annee : dict
|
||||||
Ne reprend pas les décisions d'UE, non spécifiques au BUT.
|
Ne reprend pas les décisions d'UE, non spécifiques au BUT.
|
||||||
"""
|
"""
|
||||||
decisions = {}
|
decisions = {}
|
||||||
# --- RCUEs: seulement sur semestres pairs XXX à améliorer
|
# --- RCUEs: seulement sur semestres pairs XXX à améliorer
|
||||||
if formsemestre.semestre_id % 2 == 0:
|
if formsemestre.semestre_id % 2 == 0:
|
||||||
# validations émises depuis ce formsemestre:
|
# validations émises depuis ce formsemestre:
|
||||||
validations_rcues = (
|
validations_rcues = ApcValidationRCUE.query.filter_by(
|
||||||
ApcValidationRCUE.query.filter_by(
|
etudid=etud.id, formsemestre_id=formsemestre.id
|
||||||
etudid=etud.id, formsemestre_id=formsemestre.id
|
|
||||||
)
|
|
||||||
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id)
|
|
||||||
.order_by(UniteEns.numero, UniteEns.acronyme)
|
|
||||||
)
|
)
|
||||||
decisions["decision_rcue"] = [v.to_dict_bul() for v in validations_rcues]
|
decisions["decision_rcue"] = [v.to_dict_bul() for v in validations_rcues]
|
||||||
titres_rcues = []
|
titres_rcues = []
|
||||||
@ -215,11 +378,16 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
|
|||||||
decisions["descr_decisions_rcue"] = ""
|
decisions["descr_decisions_rcue"] = ""
|
||||||
decisions["descr_decisions_niveaux"] = ""
|
decisions["descr_decisions_niveaux"] = ""
|
||||||
# --- Année: prend la validation pour l'année scolaire de ce semestre
|
# --- Année: prend la validation pour l'année scolaire de ce semestre
|
||||||
validation = ApcValidationAnnee.query.filter_by(
|
validation = (
|
||||||
etudid=etud.id,
|
ApcValidationAnnee.query.filter_by(
|
||||||
annee_scolaire=formsemestre.annee_scolaire(),
|
etudid=etud.id,
|
||||||
referentiel_competence_id=formsemestre.formation.referentiel_competence_id,
|
annee_scolaire=formsemestre.annee_scolaire(),
|
||||||
).first()
|
)
|
||||||
|
.join(ApcValidationAnnee.formsemestre)
|
||||||
|
.join(FormSemestre.formation)
|
||||||
|
.filter(Formation.formation_code == formsemestre.formation.formation_code)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
if validation:
|
if validation:
|
||||||
decisions["decision_annee"] = validation.to_dict_bul()
|
decisions["decision_annee"] = validation.to_dict_bul()
|
||||||
else:
|
else:
|
||||||
|
@ -15,7 +15,6 @@ from app.scodoc.codes_cursus import (
|
|||||||
ADJ,
|
ADJ,
|
||||||
ADJR,
|
ADJR,
|
||||||
ADM,
|
ADM,
|
||||||
ADSUP,
|
|
||||||
AJ,
|
AJ,
|
||||||
ATB,
|
ATB,
|
||||||
ATJ,
|
ATJ,
|
||||||
@ -38,7 +37,6 @@ CODES_SCODOC_TO_APO = {
|
|||||||
ADJ: "ADM",
|
ADJ: "ADM",
|
||||||
ADJR: "ADM",
|
ADJR: "ADM",
|
||||||
ADM: "ADM",
|
ADM: "ADM",
|
||||||
ADSUP: "ADM",
|
|
||||||
AJ: "AJ",
|
AJ: "AJ",
|
||||||
ATB: "AJAC",
|
ATB: "AJAC",
|
||||||
ATJ: "AJAC",
|
ATJ: "AJAC",
|
||||||
|
@ -43,8 +43,8 @@ class Identite(db.Model):
|
|||||||
"optionnel (si present, affiché à la place du nom)"
|
"optionnel (si present, affiché à la place du nom)"
|
||||||
civilite = db.Column(db.String(1), nullable=False)
|
civilite = db.Column(db.String(1), nullable=False)
|
||||||
|
|
||||||
# données d'état-civil. Si présent remplace les données d'usage dans les documents
|
# données d'état-civil. Si présent remplace les données d'usage dans les documents officiels (bulletins, PV)
|
||||||
# officiels (bulletins, PV): voir nomprenom_etat_civil()
|
# cf nomprenom_etat_civil()
|
||||||
civilite_etat_civil = db.Column(db.String(1), nullable=False, server_default="X")
|
civilite_etat_civil = db.Column(db.String(1), nullable=False, server_default="X")
|
||||||
prenom_etat_civil = db.Column(db.Text(), nullable=False, server_default="")
|
prenom_etat_civil = db.Column(db.Text(), nullable=False, server_default="")
|
||||||
|
|
||||||
@ -73,17 +73,15 @@ class Identite(db.Model):
|
|||||||
passive_deletes=True,
|
passive_deletes=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Relations avec les assiduites et les justificatifs
|
||||||
|
assiduites = db.relationship("Assiduite", backref="etudiant", lazy="dynamic")
|
||||||
|
justificatifs = db.relationship("Justificatif", backref="etudiant", lazy="dynamic")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return (
|
return (
|
||||||
f"<Etud {self.id}/{self.departement.acronym} {self.nom!r} {self.prenom!r}>"
|
f"<Etud {self.id}/{self.departement.acronym} {self.nom!r} {self.prenom!r}>"
|
||||||
)
|
)
|
||||||
|
|
||||||
def html_link_fiche(self) -> str:
|
|
||||||
"lien vers la fiche"
|
|
||||||
return f"""<a class="stdlink" href="{
|
|
||||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=self.id)
|
|
||||||
}">{self.nomprenom}</a>"""
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_request(cls, etudid=None, code_nip=None) -> "Identite":
|
def from_request(cls, etudid=None, code_nip=None) -> "Identite":
|
||||||
"""Étudiant à partir de l'etudid ou du code_nip, soit
|
"""Étudiant à partir de l'etudid ou du code_nip, soit
|
||||||
@ -226,7 +224,7 @@ class Identite(db.Model):
|
|||||||
}
|
}
|
||||||
args_dict = {}
|
args_dict = {}
|
||||||
for key, value in args.items():
|
for key, value in args.items():
|
||||||
if hasattr(cls, key) and not isinstance(getattr(cls, key, None), property):
|
if hasattr(cls, key):
|
||||||
# compat scodoc7 (mauvaise idée de l'époque)
|
# compat scodoc7 (mauvaise idée de l'époque)
|
||||||
if key in fs_empty_stored_as_nulls and value == "":
|
if key in fs_empty_stored_as_nulls and value == "":
|
||||||
value = None
|
value = None
|
||||||
|
@ -145,18 +145,6 @@ class Evaluation(db.Model):
|
|||||||
db.session.add(copy)
|
db.session.add(copy)
|
||||||
return copy
|
return copy
|
||||||
|
|
||||||
def is_matin(self) -> bool:
|
|
||||||
"Evaluation ayant lieu le matin (faux si pas de date)"
|
|
||||||
heure_debut_dt = self.heure_debut or datetime.time(8, 00)
|
|
||||||
# 8:00 au cas ou pas d'heure (note externe?)
|
|
||||||
return bool(self.jour) and heure_debut_dt < datetime.time(12, 00)
|
|
||||||
|
|
||||||
def is_apresmidi(self) -> bool:
|
|
||||||
"Evaluation ayant lieu l'après midi (faux si pas de date)"
|
|
||||||
heure_debut_dt = self.heure_debut or datetime.time(8, 00)
|
|
||||||
# 8:00 au cas ou pas d'heure (note externe?)
|
|
||||||
return bool(self.jour) and heure_debut_dt >= datetime.time(12, 00)
|
|
||||||
|
|
||||||
def set_default_poids(self) -> bool:
|
def set_default_poids(self) -> bool:
|
||||||
"""Initialize les poids bvers les UE à leurs valeurs par défaut
|
"""Initialize les poids bvers les UE à leurs valeurs par défaut
|
||||||
C'est à dire à 1 si le coef. module/UE est non nul, 0 sinon.
|
C'est à dire à 1 si le coef. module/UE est non nul, 0 sinon.
|
||||||
@ -190,10 +178,8 @@ class Evaluation(db.Model):
|
|||||||
"""
|
"""
|
||||||
L = []
|
L = []
|
||||||
for ue_id, poids in ue_poids_dict.items():
|
for ue_id, poids in ue_poids_dict.items():
|
||||||
ue = db.session.get(UniteEns, ue_id)
|
ue = UniteEns.query.get(ue_id)
|
||||||
ue_poids = EvaluationUEPoids(evaluation=self, ue=ue, poids=poids)
|
L.append(EvaluationUEPoids(evaluation=self, ue=ue, poids=poids))
|
||||||
L.append(ue_poids)
|
|
||||||
db.session.add(ue_poids)
|
|
||||||
self.ue_poids = L # backref # pylint:disable=attribute-defined-outside-init
|
self.ue_poids = L # backref # pylint:disable=attribute-defined-outside-init
|
||||||
self.moduleimpl.invalidate_evaluations_poids() # inval cache
|
self.moduleimpl.invalidate_evaluations_poids() # inval cache
|
||||||
|
|
||||||
@ -340,7 +326,7 @@ def check_evaluation_args(args):
|
|||||||
jour = args.get("jour", None)
|
jour = args.get("jour", None)
|
||||||
args["jour"] = jour
|
args["jour"] = jour
|
||||||
if jour:
|
if jour:
|
||||||
modimpl = db.session.get(ModuleImpl, moduleimpl_id)
|
modimpl = ModuleImpl.query.get(moduleimpl_id)
|
||||||
formsemestre = modimpl.formsemestre
|
formsemestre = modimpl.formsemestre
|
||||||
y, m, d = [int(x) for x in ndb.DateDMYtoISO(jour).split("-")]
|
y, m, d = [int(x) for x in ndb.DateDMYtoISO(jour).split("-")]
|
||||||
jour = datetime.date(y, m, d)
|
jour = datetime.date(y, m, d)
|
||||||
|
@ -54,17 +54,14 @@ class ScolarNews(db.Model):
|
|||||||
NEWS_APO = "APO" # changements de codes APO
|
NEWS_APO = "APO" # changements de codes APO
|
||||||
NEWS_FORM = "FORM" # modification formation (object=formation_id)
|
NEWS_FORM = "FORM" # modification formation (object=formation_id)
|
||||||
NEWS_INSCR = "INSCR" # inscription d'étudiants (object=None ou formsemestre_id)
|
NEWS_INSCR = "INSCR" # inscription d'étudiants (object=None ou formsemestre_id)
|
||||||
NEWS_JURY = "JURY" # saisie jury
|
|
||||||
NEWS_MISC = "MISC" # unused
|
NEWS_MISC = "MISC" # unused
|
||||||
NEWS_NOTE = "NOTES" # saisie note (object=moduleimpl_id)
|
NEWS_NOTE = "NOTES" # saisie note (object=moduleimpl_id)
|
||||||
NEWS_SEM = "SEM" # creation semestre (object=None)
|
NEWS_SEM = "SEM" # creation semestre (object=None)
|
||||||
|
|
||||||
NEWS_MAP = {
|
NEWS_MAP = {
|
||||||
NEWS_ABS: "saisie absence",
|
NEWS_ABS: "saisie absence",
|
||||||
NEWS_APO: "modif. code Apogée",
|
NEWS_APO: "modif. code Apogée",
|
||||||
NEWS_FORM: "modification formation",
|
NEWS_FORM: "modification formation",
|
||||||
NEWS_INSCR: "inscription d'étudiants",
|
NEWS_INSCR: "inscription d'étudiants",
|
||||||
NEWS_JURY: "saisie jury",
|
|
||||||
NEWS_MISC: "opération", # unused
|
NEWS_MISC: "opération", # unused
|
||||||
NEWS_NOTE: "saisie note",
|
NEWS_NOTE: "saisie note",
|
||||||
NEWS_SEM: "création semestre",
|
NEWS_SEM: "création semestre",
|
||||||
@ -133,10 +130,10 @@ class ScolarNews(db.Model):
|
|||||||
return query.order_by(cls.date.desc()).limit(n).all()
|
return query.order_by(cls.date.desc()).limit(n).all()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add(cls, typ, obj=None, text="", url=None, max_frequency=600):
|
def add(cls, typ, obj=None, text="", url=None, max_frequency=0):
|
||||||
"""Enregistre une nouvelle
|
"""Enregistre une nouvelle
|
||||||
Si max_frequency, ne génère pas 2 nouvelles "identiques"
|
Si max_frequency, ne génère pas 2 nouvelles "identiques"
|
||||||
à moins de max_frequency secondes d'intervalle (10 minutes par défaut).
|
à moins de max_frequency secondes d'intervalle.
|
||||||
Deux nouvelles sont considérées comme "identiques" si elles ont
|
Deux nouvelles sont considérées comme "identiques" si elles ont
|
||||||
même (obj, typ, user).
|
même (obj, typ, user).
|
||||||
La nouvelle enregistrée est aussi envoyée par mail.
|
La nouvelle enregistrée est aussi envoyée par mail.
|
||||||
@ -156,10 +153,7 @@ class ScolarNews(db.Model):
|
|||||||
if last_news:
|
if last_news:
|
||||||
now = datetime.datetime.now(tz=last_news.date.tzinfo)
|
now = datetime.datetime.now(tz=last_news.date.tzinfo)
|
||||||
if (now - last_news.date) < datetime.timedelta(seconds=max_frequency):
|
if (now - last_news.date) < datetime.timedelta(seconds=max_frequency):
|
||||||
# pas de nouvel event, mais met à jour l'heure
|
# on n'enregistre pas
|
||||||
last_news.date = datetime.datetime.now()
|
|
||||||
db.session.add(last_news)
|
|
||||||
db.session.commit()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
news = ScolarNews(
|
news = ScolarNews(
|
||||||
@ -187,14 +181,14 @@ class ScolarNews(db.Model):
|
|||||||
elif self.type == self.NEWS_NOTE:
|
elif self.type == self.NEWS_NOTE:
|
||||||
moduleimpl_id = self.object
|
moduleimpl_id = self.object
|
||||||
if moduleimpl_id:
|
if moduleimpl_id:
|
||||||
modimpl = db.session.get(ModuleImpl, moduleimpl_id)
|
modimpl = ModuleImpl.query.get(moduleimpl_id)
|
||||||
if modimpl is None:
|
if modimpl is None:
|
||||||
return None # module does not exists anymore
|
return None # module does not exists anymore
|
||||||
formsemestre_id = modimpl.formsemestre_id
|
formsemestre_id = modimpl.formsemestre_id
|
||||||
|
|
||||||
if not formsemestre_id:
|
if not formsemestre_id:
|
||||||
return None
|
return None
|
||||||
formsemestre = db.session.get(FormSemestre, formsemestre_id)
|
formsemestre = FormSemestre.query.get(formsemestre_id)
|
||||||
return formsemestre
|
return formsemestre
|
||||||
|
|
||||||
def notify_by_mail(self):
|
def notify_by_mail(self):
|
||||||
@ -265,8 +259,11 @@ class ScolarNews(db.Model):
|
|||||||
|
|
||||||
# Informations générales
|
# Informations générales
|
||||||
H.append(
|
H.append(
|
||||||
f"""<div><a class="discretelink" href="{scu.SCO_ANNONCES_WEBSITE}">
|
f"""<div>
|
||||||
Pour en savoir plus sur ScoDoc voir le site scodoc.org</a>.
|
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>
|
</div>
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
@ -60,7 +60,7 @@ class Formation(db.Model):
|
|||||||
return f"""<{self.__class__.__name__}(id={self.id}, dept_id={
|
return f"""<{self.__class__.__name__}(id={self.id}, dept_id={
|
||||||
self.dept_id}, acronyme={self.acronyme!r}, version={self.version})>"""
|
self.dept_id}, acronyme={self.acronyme!r}, version={self.version})>"""
|
||||||
|
|
||||||
def html(self) -> str:
|
def to_html(self) -> str:
|
||||||
"titre complet pour affichage"
|
"titre complet pour affichage"
|
||||||
return f"""Formation {self.titre} ({self.acronyme}) [version {self.version}] code {self.formation_code}"""
|
return f"""Formation {self.titre} ({self.acronyme}) [version {self.version}] code {self.formation_code}"""
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ from operator import attrgetter
|
|||||||
|
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
|
||||||
from flask import flash, g, url_for
|
from flask import flash, g
|
||||||
from sqlalchemy.sql import text
|
from sqlalchemy.sql import text
|
||||||
|
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
@ -163,14 +163,6 @@ class FormSemestre(db.Model):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__name__} {self.id} {self.titre_annee()}>"
|
return f"<{self.__class__.__name__} {self.id} {self.titre_annee()}>"
|
||||||
|
|
||||||
def html_link_status(self, label=None, title=None) -> str:
|
|
||||||
"html link to status page"
|
|
||||||
return f"""<a class="stdlink" href="{
|
|
||||||
url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept,
|
|
||||||
formsemestre_id=self.id,)
|
|
||||||
}" title="{title or ''}">{label or self.titre_mois()}</a>
|
|
||||||
"""
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_formsemestre(cls, formsemestre_id: int) -> "FormSemestre":
|
def get_formsemestre(cls, formsemestre_id: int) -> "FormSemestre":
|
||||||
""" "FormSemestre ou 404, cherche uniquement dans le département courant"""
|
""" "FormSemestre ou 404, cherche uniquement dans le département courant"""
|
||||||
@ -305,17 +297,6 @@ class FormSemestre(db.Model):
|
|||||||
- et sont associées à l'un des parcours de ce formsemestre
|
- et sont associées à l'un des parcours de ce formsemestre
|
||||||
(ou à aucun, donc tronc commun).
|
(ou à aucun, donc tronc commun).
|
||||||
"""
|
"""
|
||||||
# per-request caching
|
|
||||||
key = (self.id, with_sport)
|
|
||||||
_cache = getattr(g, "_formsemestre_get_ues_cache", None)
|
|
||||||
if _cache:
|
|
||||||
result = _cache.get(key, False)
|
|
||||||
if result is not False:
|
|
||||||
return result
|
|
||||||
else:
|
|
||||||
g._formsemestre_get_ues_cache = {}
|
|
||||||
_cache = g._formsemestre_get_ues_cache
|
|
||||||
|
|
||||||
formation: Formation = self.formation
|
formation: Formation = self.formation
|
||||||
if formation.is_apc():
|
if formation.is_apc():
|
||||||
# UEs de tronc commun (sans parcours indiqué)
|
# UEs de tronc commun (sans parcours indiqué)
|
||||||
@ -335,7 +316,8 @@ class FormSemestre(db.Model):
|
|||||||
).filter(UniteEns.semestre_idx == self.semestre_id)
|
).filter(UniteEns.semestre_idx == self.semestre_id)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
ues = sorted(sem_ues.values(), key=attrgetter("numero", "acronyme"))
|
ues = sem_ues.values()
|
||||||
|
return sorted(ues, key=attrgetter("numero"))
|
||||||
else:
|
else:
|
||||||
sem_ues = db.session.query(UniteEns).filter(
|
sem_ues = db.session.query(UniteEns).filter(
|
||||||
ModuleImpl.formsemestre_id == self.id,
|
ModuleImpl.formsemestre_id == self.id,
|
||||||
@ -344,9 +326,7 @@ class FormSemestre(db.Model):
|
|||||||
)
|
)
|
||||||
if not with_sport:
|
if not with_sport:
|
||||||
sem_ues = sem_ues.filter(UniteEns.type != codes_cursus.UE_SPORT)
|
sem_ues = sem_ues.filter(UniteEns.type != codes_cursus.UE_SPORT)
|
||||||
ues = sem_ues.order_by(UniteEns.numero).all()
|
return sem_ues.order_by(UniteEns.numero).all()
|
||||||
_cache[key] = ues
|
|
||||||
return ues
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def modimpls_sorted(self) -> list[ModuleImpl]:
|
def modimpls_sorted(self) -> list[ModuleImpl]:
|
||||||
@ -394,7 +374,7 @@ class FormSemestre(db.Model):
|
|||||||
),
|
),
|
||||||
{"formsemestre_id": self.id, "parcours_id": parcours.id},
|
{"formsemestre_id": self.id, "parcours_id": parcours.id},
|
||||||
)
|
)
|
||||||
return [db.session.get(ModuleImpl, modimpl_id) for modimpl_id in cursor]
|
return [ModuleImpl.query.get(modimpl_id) for modimpl_id in cursor]
|
||||||
|
|
||||||
def can_be_edited_by(self, user):
|
def can_be_edited_by(self, user):
|
||||||
"""Vrai si user peut modifier ce semestre (est chef ou l'un des responsables)"""
|
"""Vrai si user peut modifier ce semestre (est chef ou l'un des responsables)"""
|
||||||
@ -538,11 +518,6 @@ class FormSemestre(db.Model):
|
|||||||
return ""
|
return ""
|
||||||
return ", ".join(sorted([etape.etape_apo for etape in self.etapes if etape]))
|
return ", ".join(sorted([etape.etape_apo for etape in self.etapes if etape]))
|
||||||
|
|
||||||
def add_etape(self, etape_apo: str):
|
|
||||||
"Ajoute une étape"
|
|
||||||
etape = FormSemestreEtape(formsemestre_id=self.id, etape_apo=etape_apo)
|
|
||||||
db.session.add(etape)
|
|
||||||
|
|
||||||
def regroupements_coherents_etud(self) -> list[tuple[UniteEns, UniteEns]]:
|
def regroupements_coherents_etud(self) -> list[tuple[UniteEns, UniteEns]]:
|
||||||
"""Calcule la liste des regroupements cohérents d'UE impliquant ce
|
"""Calcule la liste des regroupements cohérents d'UE impliquant ce
|
||||||
formsemestre.
|
formsemestre.
|
||||||
@ -585,17 +560,6 @@ class FormSemestre(db.Model):
|
|||||||
user
|
user
|
||||||
)
|
)
|
||||||
|
|
||||||
def can_change_groups(self, user: User = None) -> bool:
|
|
||||||
"""Vrai si l'utilisateur (par def. current) peut changer les groupes dans
|
|
||||||
ce semestre: vérifie permission et verrouillage.
|
|
||||||
"""
|
|
||||||
if not self.etat:
|
|
||||||
return False # semestre verrouillé
|
|
||||||
user = user or current_user
|
|
||||||
if user.has_permission(Permission.ScoEtudChangeGroups):
|
|
||||||
return True # typiquement admin, chef dept
|
|
||||||
return self.est_responsable(user)
|
|
||||||
|
|
||||||
def can_edit_jury(self, user: User = None):
|
def can_edit_jury(self, user: User = None):
|
||||||
"""Vrai si utilisateur (par def. current) peut saisir decision de jury
|
"""Vrai si utilisateur (par def. current) peut saisir decision de jury
|
||||||
dans ce semestre: vérifie permission et verrouillage.
|
dans ce semestre: vérifie permission et verrouillage.
|
||||||
@ -818,8 +782,6 @@ class FormSemestre(db.Model):
|
|||||||
Les groupes de parcours sont ceux de la partition scu.PARTITION_PARCOURS
|
Les groupes de parcours sont ceux de la partition scu.PARTITION_PARCOURS
|
||||||
et leur nom est le code du parcours (eg "Cyber").
|
et leur nom est le code du parcours (eg "Cyber").
|
||||||
"""
|
"""
|
||||||
if self.formation.referentiel_competence_id is None:
|
|
||||||
return # safety net
|
|
||||||
partition = Partition.query.filter_by(
|
partition = Partition.query.filter_by(
|
||||||
formsemestre_id=self.id, partition_name=scu.PARTITION_PARCOURS
|
formsemestre_id=self.id, partition_name=scu.PARTITION_PARCOURS
|
||||||
).first()
|
).first()
|
||||||
@ -843,10 +805,7 @@ class FormSemestre(db.Model):
|
|||||||
query = (
|
query = (
|
||||||
ApcParcours.query.filter_by(code=group.group_name)
|
ApcParcours.query.filter_by(code=group.group_name)
|
||||||
.join(ApcReferentielCompetences)
|
.join(ApcReferentielCompetences)
|
||||||
.filter_by(
|
.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
dept_id=g.scodoc_dept_id,
|
|
||||||
id=self.formation.referentiel_competence_id,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
if query.count() != 1:
|
if query.count() != 1:
|
||||||
log(
|
log(
|
||||||
@ -895,12 +854,15 @@ class FormSemestre(db.Model):
|
|||||||
.order_by(UniteEns.numero)
|
.order_by(UniteEns.numero)
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
vals_annee = ( # issues de cette année scolaire seulement
|
vals_annee = (
|
||||||
ApcValidationAnnee.query.filter_by(
|
ApcValidationAnnee.query.filter_by(
|
||||||
etudid=etudid,
|
etudid=etudid,
|
||||||
annee_scolaire=self.annee_scolaire(),
|
annee_scolaire=self.annee_scolaire(),
|
||||||
referentiel_competence_id=self.formation.referentiel_competence_id,
|
)
|
||||||
).all()
|
.join(ApcValidationAnnee.formsemestre)
|
||||||
|
.join(FormSemestre.formation)
|
||||||
|
.filter(Formation.formation_code == self.formation.formation_code)
|
||||||
|
.all()
|
||||||
)
|
)
|
||||||
H = []
|
H = []
|
||||||
for vals in (vals_sem, vals_ues, vals_rcues, vals_annee):
|
for vals in (vals_sem, vals_ues, vals_rcues, vals_annee):
|
||||||
|
@ -8,13 +8,11 @@
|
|||||||
"""ScoDoc models: Groups & partitions
|
"""ScoDoc models: Groups & partitions
|
||||||
"""
|
"""
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
from sqlalchemy.exc import IntegrityError
|
|
||||||
|
|
||||||
from app import db, log
|
from app import db
|
||||||
from app.models import SHORT_STR_LEN
|
from app.models import SHORT_STR_LEN
|
||||||
from app.models import GROUPNAME_STR_LEN
|
from app.models import GROUPNAME_STR_LEN
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
|
||||||
|
|
||||||
|
|
||||||
class Partition(db.Model):
|
class Partition(db.Model):
|
||||||
@ -119,81 +117,6 @@ class Partition(db.Model):
|
|||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_etud_group(self, etud: "Identite", group: "GroupDescr") -> bool:
|
|
||||||
"""Affect etudid to group_id in given partition.
|
|
||||||
Raises IntegrityError si conflit,
|
|
||||||
or ValueError si ce group_id n'est pas dans cette partition
|
|
||||||
ou que l'étudiant n'est pas inscrit au semestre.
|
|
||||||
Return True si changement, False s'il était déjà dans ce groupe.
|
|
||||||
"""
|
|
||||||
if not group.id in (g.id for g in self.groups):
|
|
||||||
raise ScoValueError(
|
|
||||||
f"""Le groupe {group.id} n'est pas dans la partition {
|
|
||||||
self.partition_name or "tous"}"""
|
|
||||||
)
|
|
||||||
if etud.id not in (e.id for e in self.formsemestre.etuds):
|
|
||||||
raise ScoValueError(
|
|
||||||
f"""étudiant {etud.nomprenom} non inscrit au formsemestre du groupe {
|
|
||||||
group.group_name}"""
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
existing_row = (
|
|
||||||
db.session.query(group_membership)
|
|
||||||
.filter_by(etudid=etud.id)
|
|
||||||
.join(GroupDescr)
|
|
||||||
.filter_by(partition_id=self.id)
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
if existing_row:
|
|
||||||
existing_group_id = existing_row[1]
|
|
||||||
if group.id == existing_group_id:
|
|
||||||
return False
|
|
||||||
# Fait le changement avec l'ORM sinon risque élevé de blocage
|
|
||||||
existing_group = db.session.get(GroupDescr, existing_group_id)
|
|
||||||
db.session.commit()
|
|
||||||
group.etuds.append(etud)
|
|
||||||
existing_group.etuds.remove(etud)
|
|
||||||
db.session.add(etud)
|
|
||||||
db.session.add(existing_group)
|
|
||||||
db.session.add(group)
|
|
||||||
else:
|
|
||||||
new_row = group_membership.insert().values(
|
|
||||||
etudid=etud.id, group_id=group.id
|
|
||||||
)
|
|
||||||
db.session.execute(new_row)
|
|
||||||
db.session.commit()
|
|
||||||
except IntegrityError:
|
|
||||||
db.session.rollback()
|
|
||||||
raise
|
|
||||||
return True
|
|
||||||
|
|
||||||
def create_group(self, group_name="", default=False) -> "GroupDescr":
|
|
||||||
"Crée un groupe dans cette partition"
|
|
||||||
if not self.formsemestre.can_change_groups():
|
|
||||||
raise AccessDenied(
|
|
||||||
"""Vous n'avez pas le droit d'effectuer cette opération,
|
|
||||||
ou bien le semestre est verrouillé !"""
|
|
||||||
)
|
|
||||||
if group_name:
|
|
||||||
group_name = group_name.strip()
|
|
||||||
if not group_name and not default:
|
|
||||||
raise ValueError("invalid group name: ()")
|
|
||||||
if not GroupDescr.check_name(self, group_name, default=default):
|
|
||||||
raise ScoValueError(
|
|
||||||
f"Le groupe {group_name} existe déjà dans cette partition"
|
|
||||||
)
|
|
||||||
numeros = [g.numero if g.numero is not None else 0 for g in self.groups]
|
|
||||||
if len(numeros) > 0:
|
|
||||||
new_numero = max(numeros) + 1
|
|
||||||
else:
|
|
||||||
new_numero = 0
|
|
||||||
group = GroupDescr(partition=self, group_name=group_name, numero=new_numero)
|
|
||||||
db.session.add(group)
|
|
||||||
db.session.commit()
|
|
||||||
log(f"create_group: created group_id={group.id}")
|
|
||||||
#
|
|
||||||
return group
|
|
||||||
|
|
||||||
|
|
||||||
class GroupDescr(db.Model):
|
class GroupDescr(db.Model):
|
||||||
"""Description d'un groupe d'une partition"""
|
"""Description d'un groupe d'une partition"""
|
||||||
|
@ -122,6 +122,22 @@ class ModuleImpl(db.Model):
|
|||||||
raise AccessDenied(f"Modification impossible pour {user}")
|
raise AccessDenied(f"Modification impossible pour {user}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def est_inscrit(self, etud: Identite) -> bool:
|
||||||
|
"""
|
||||||
|
Vérifie si l'étudiant est bien inscrit au moduleimpl
|
||||||
|
|
||||||
|
Retourne Vrai si c'est le cas, faux sinon
|
||||||
|
"""
|
||||||
|
|
||||||
|
is_module: int = (
|
||||||
|
ModuleImplInscription.query.filter_by(
|
||||||
|
etudid=etud.id, moduleimpl_id=self.id
|
||||||
|
).count()
|
||||||
|
> 0
|
||||||
|
)
|
||||||
|
|
||||||
|
return is_module
|
||||||
|
|
||||||
|
|
||||||
# Enseignants (chargés de TD ou TP) d'un moduleimpl
|
# Enseignants (chargés de TD ou TP) d'un moduleimpl
|
||||||
notes_modules_enseignants = db.Table(
|
notes_modules_enseignants = db.Table(
|
||||||
|
@ -55,7 +55,7 @@ class Module(db.Model):
|
|||||||
secondary=parcours_modules,
|
secondary=parcours_modules,
|
||||||
lazy="subquery",
|
lazy="subquery",
|
||||||
backref=db.backref("modules", lazy=True),
|
backref=db.backref("modules", lazy=True),
|
||||||
order_by="ApcParcours.numero, ApcParcours.code",
|
order_by="ApcParcours.numero",
|
||||||
)
|
)
|
||||||
|
|
||||||
app_critiques = db.relationship(
|
app_critiques = db.relationship(
|
||||||
@ -198,7 +198,7 @@ class Module(db.Model):
|
|||||||
else:
|
else:
|
||||||
# crée nouveau coef:
|
# crée nouveau coef:
|
||||||
if coef != 0.0:
|
if coef != 0.0:
|
||||||
ue = db.session.get(UniteEns, ue_id)
|
ue = UniteEns.query.get(ue_id)
|
||||||
ue_coef = ModuleUECoef(module=self, ue=ue, coef=coef)
|
ue_coef = ModuleUECoef(module=self, ue=ue, coef=coef)
|
||||||
db.session.add(ue_coef)
|
db.session.add(ue_coef)
|
||||||
self.ue_coefs.append(ue_coef)
|
self.ue_coefs.append(ue_coef)
|
||||||
@ -229,19 +229,19 @@ class Module(db.Model):
|
|||||||
"""delete coef"""
|
"""delete coef"""
|
||||||
if self.formation.has_locked_sems(self.ue.semestre_idx):
|
if self.formation.has_locked_sems(self.ue.semestre_idx):
|
||||||
current_app.logguer.info(
|
current_app.logguer.info(
|
||||||
"delete_ue_coef: locked formation, ignoring request"
|
f"delete_ue_coef: locked formation, ignoring request"
|
||||||
)
|
)
|
||||||
raise ScoValueError("Formation verrouillée")
|
raise ScoValueError("Formation verrouillée")
|
||||||
ue_coef = db.session.get(ModuleUECoef, (self.id, ue.id))
|
ue_coef = ModuleUECoef.query.get((self.id, ue.id))
|
||||||
if ue_coef:
|
if ue_coef:
|
||||||
db.session.delete(ue_coef)
|
db.session.delete(ue_coef)
|
||||||
self.formation.invalidate_module_coefs()
|
self.formation.invalidate_module_coefs()
|
||||||
|
|
||||||
def get_ue_coefs_sorted(self):
|
def get_ue_coefs_sorted(self):
|
||||||
"les coefs d'UE, trié par numéro et acronyme d'UE"
|
"les coefs d'UE, trié par numéro d'UE"
|
||||||
# je n'ai pas su mettre un order_by sur le backref sans avoir
|
# je n'ai pas su mettre un order_by sur le backref sans avoir
|
||||||
# à redéfinir les relationships...
|
# à redéfinir les relationships...
|
||||||
return sorted(self.ue_coefs, key=lambda uc: (uc.ue.numero, uc.ue.acronyme))
|
return sorted(self.ue_coefs, key=lambda x: x.ue.numero)
|
||||||
|
|
||||||
def ue_coefs_list(
|
def ue_coefs_list(
|
||||||
self, include_zeros=True, ues: list["UniteEns"] = None
|
self, include_zeros=True, ues: list["UniteEns"] = None
|
||||||
|
@ -56,8 +56,8 @@ class NotesNotes(db.Model):
|
|||||||
"pour debug"
|
"pour debug"
|
||||||
from app.models.evaluations import Evaluation
|
from app.models.evaluations import Evaluation
|
||||||
|
|
||||||
return f"""<{self.__class__.__name__} {self.id} etudid={self.etudid} v={self.value} {self.date.isoformat()
|
return f"""<{self.__class__.__name__} {self.id} v={self.value} {self.date.isoformat()
|
||||||
} {db.session.get(Evaluation, self.evaluation_id) if self.evaluation_id else "X" }>"""
|
} {Evaluation.query.get(self.evaluation_id) if self.evaluation_id else "X" }>"""
|
||||||
|
|
||||||
|
|
||||||
class NotesNotesLog(db.Model):
|
class NotesNotesLog(db.Model):
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
"""ScoDoc 9 models : Unités d'Enseignement (UE)
|
"""ScoDoc 9 models : Unités d'Enseignement (UE)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import g
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from app import db, log
|
from app import db, log
|
||||||
@ -9,6 +8,7 @@ from app.models import APO_CODE_STR_LEN
|
|||||||
from app.models import SHORT_STR_LEN
|
from app.models import SHORT_STR_LEN
|
||||||
from app.models.but_refcomp import ApcNiveau, ApcParcours
|
from app.models.but_refcomp import ApcNiveau, ApcParcours
|
||||||
from app.models.modules import Module
|
from app.models.modules import Module
|
||||||
|
from app.scodoc.sco_exceptions import ScoFormationConflict
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
@ -58,10 +58,7 @@ class UniteEns(db.Model):
|
|||||||
|
|
||||||
# Une UE appartient soit à tous les parcours (tronc commun), soit à un sous-ensemble
|
# Une UE appartient soit à tous les parcours (tronc commun), soit à un sous-ensemble
|
||||||
parcours = db.relationship(
|
parcours = db.relationship(
|
||||||
ApcParcours,
|
ApcParcours, secondary="ue_parcours", backref=db.backref("ues", lazy=True)
|
||||||
secondary="ue_parcours",
|
|
||||||
backref=db.backref("ues", lazy=True),
|
|
||||||
order_by="ApcParcours.numero, ApcParcours.code",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# relations
|
# relations
|
||||||
@ -107,17 +104,6 @@ class UniteEns(db.Model):
|
|||||||
If convert_objects, convert all attributes to native types
|
If convert_objects, convert all attributes to native types
|
||||||
(suitable for json encoding).
|
(suitable for json encoding).
|
||||||
"""
|
"""
|
||||||
# cache car très utilisé par anciens codes
|
|
||||||
key = (self.id, convert_objects, with_module_ue_coefs)
|
|
||||||
_cache = getattr(g, "_ue_to_dict_cache", None)
|
|
||||||
if _cache:
|
|
||||||
result = g._ue_to_dict_cache.get(key, False)
|
|
||||||
if result is not False:
|
|
||||||
return result
|
|
||||||
else:
|
|
||||||
g._ue_to_dict_cache = {}
|
|
||||||
_cache = g._ue_to_dict_cache
|
|
||||||
|
|
||||||
e = dict(self.__dict__)
|
e = dict(self.__dict__)
|
||||||
e.pop("_sa_instance_state", None)
|
e.pop("_sa_instance_state", None)
|
||||||
e.pop("evaluation_ue_poids", None)
|
e.pop("evaluation_ue_poids", None)
|
||||||
@ -144,7 +130,6 @@ class UniteEns(db.Model):
|
|||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
e.pop("module_ue_coefs", None)
|
e.pop("module_ue_coefs", None)
|
||||||
_cache[key] = e
|
|
||||||
return e
|
return e
|
||||||
|
|
||||||
def annee(self) -> int:
|
def annee(self) -> int:
|
||||||
@ -192,23 +177,12 @@ class UniteEns(db.Model):
|
|||||||
le parcours indiqué.
|
le parcours indiqué.
|
||||||
"""
|
"""
|
||||||
if parcour is not None:
|
if parcour is not None:
|
||||||
key = (parcour.id, self.id, only_parcours)
|
|
||||||
ue_ects_cache = getattr(g, "_ue_ects_cache", None)
|
|
||||||
if ue_ects_cache:
|
|
||||||
ects = g._ue_ects_cache.get(key, False)
|
|
||||||
if ects is not False:
|
|
||||||
return ects
|
|
||||||
else:
|
|
||||||
g._ue_ects_cache = {}
|
|
||||||
ue_ects_cache = g._ue_ects_cache
|
|
||||||
ue_parcour = UEParcours.query.filter_by(
|
ue_parcour = UEParcours.query.filter_by(
|
||||||
ue_id=self.id, parcours_id=parcour.id
|
ue_id=self.id, parcours_id=parcour.id
|
||||||
).first()
|
).first()
|
||||||
if ue_parcour is not None and ue_parcour.ects is not None:
|
if ue_parcour is not None and ue_parcour.ects is not None:
|
||||||
ue_ects_cache[key] = ue_parcour.ects
|
|
||||||
return ue_parcour.ects
|
return ue_parcour.ects
|
||||||
if only_parcours:
|
if only_parcours:
|
||||||
ue_ects_cache[key] = None
|
|
||||||
return None
|
return None
|
||||||
return self.ects
|
return self.ects
|
||||||
|
|
||||||
|
@ -8,13 +8,10 @@ from app import log
|
|||||||
from app.models import SHORT_STR_LEN
|
from app.models import SHORT_STR_LEN
|
||||||
from app.models import CODE_STR_LEN
|
from app.models import CODE_STR_LEN
|
||||||
from app.models.events import Scolog
|
from app.models.events import Scolog
|
||||||
from app.scodoc import sco_cache
|
|
||||||
from app.scodoc import sco_utils as scu
|
|
||||||
from app.scodoc.codes_cursus import CODES_UE_VALIDES
|
|
||||||
|
|
||||||
|
|
||||||
class ScolarFormSemestreValidation(db.Model):
|
class ScolarFormSemestreValidation(db.Model):
|
||||||
"""Décisions de jury (sur semestre ou UEs)"""
|
"""Décisions de jury"""
|
||||||
|
|
||||||
__tablename__ = "scolar_formsemestre_validation"
|
__tablename__ = "scolar_formsemestre_validation"
|
||||||
# Assure unicité de la décision:
|
# Assure unicité de la décision:
|
||||||
@ -57,30 +54,18 @@ class ScolarFormSemestreValidation(db.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
ue = db.relationship("UniteEns", lazy="select", uselist=False)
|
ue = db.relationship("UniteEns", lazy="select", uselist=False)
|
||||||
etud = db.relationship("Identite", backref="validations")
|
|
||||||
formsemestre = db.relationship(
|
formsemestre = db.relationship(
|
||||||
"FormSemestre", lazy="select", uselist=False, foreign_keys=[formsemestre_id]
|
"FormSemestre", lazy="select", uselist=False, foreign_keys=[formsemestre_id]
|
||||||
)
|
)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"""{self.__class__.__name__}(sem={self.formsemestre_id}, etuid={
|
return f"{self.__class__.__name__}(sem={self.formsemestre_id}, etuid={self.etudid}, code={self.code}, ue={self.ue}, moy_ue={self.moy_ue})"
|
||||||
self.etudid}, code={self.code}, ue={self.ue}, moy_ue={self.moy_ue})"""
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.ue_id:
|
if self.ue_id:
|
||||||
# Note: si l'objet vient d'être créé, ue_id peut exister mais pas ue !
|
# Note: si l'objet vient d'être créé, ue_id peut exister mais pas ue !
|
||||||
return f"""décision sur UE {self.ue.acronyme if self.ue else self.ue_id
|
return f"""décision sur UE {self.ue.acronyme if self.ue else self.ue_id}: {self.code}"""
|
||||||
} ({self.ue_id}): {self.code}"""
|
return f"""décision sur semestre {self.formsemestre.titre_mois()} du {self.event_date.strftime("%d/%m/%Y")}"""
|
||||||
return f"""décision sur semestre {self.formsemestre.titre_mois()} du {
|
|
||||||
self.event_date.strftime("%d/%m/%Y")}"""
|
|
||||||
|
|
||||||
def delete(self):
|
|
||||||
"Efface cette validation"
|
|
||||||
log(f"{self.__class__.__name__}.delete({self})")
|
|
||||||
etud = self.etud
|
|
||||||
db.session.delete(self)
|
|
||||||
db.session.commit()
|
|
||||||
sco_cache.invalidate_formsemestre_etud(etud)
|
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
"as a dict"
|
"as a dict"
|
||||||
@ -88,49 +73,6 @@ class ScolarFormSemestreValidation(db.Model):
|
|||||||
d.pop("_sa_instance_state", None)
|
d.pop("_sa_instance_state", None)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def html(self, detail=False) -> str:
|
|
||||||
"Affichage html"
|
|
||||||
if self.ue_id is not None:
|
|
||||||
moyenne = (
|
|
||||||
f", moyenne {scu.fmt_note(self.moy_ue)}/20 "
|
|
||||||
if self.moy_ue is not None
|
|
||||||
else ""
|
|
||||||
)
|
|
||||||
link = (
|
|
||||||
self.formsemestre.html_link_status(
|
|
||||||
label=f"{self.formsemestre.titre_formation(with_sem_idx=1)}",
|
|
||||||
title=self.formsemestre.titre_annee(),
|
|
||||||
)
|
|
||||||
if self.formsemestre
|
|
||||||
else "externe/antérieure"
|
|
||||||
)
|
|
||||||
return f"""Validation
|
|
||||||
{'<span class="redboldtext">externe</span>' if self.is_external else ""}
|
|
||||||
de l'UE <b>{self.ue.acronyme}</b>
|
|
||||||
{('parcours <span class="parcours">'
|
|
||||||
+ ", ".join([p.code for p in self.ue.parcours]))
|
|
||||||
+ "</span>"
|
|
||||||
if self.ue.parcours else ""}
|
|
||||||
{("émise par " + link)}
|
|
||||||
: <b>{self.code}</b>{moyenne}
|
|
||||||
le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")}
|
|
||||||
"""
|
|
||||||
else:
|
|
||||||
return f"""Validation du semestre S{
|
|
||||||
self.formsemestre.semestre_id if self.formsemestre else "?"}
|
|
||||||
{self.formsemestre.html_link_status() if self.formsemestre else ""}
|
|
||||||
: <b>{self.code}</b>
|
|
||||||
le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")}
|
|
||||||
"""
|
|
||||||
|
|
||||||
def ects(self) -> float:
|
|
||||||
"Les ECTS acquis par cette validation. (0 si ce n'est pas une validation d'UE)"
|
|
||||||
return (
|
|
||||||
self.ue.ects
|
|
||||||
if (self.ue is not None) and (self.code in CODES_UE_VALIDES)
|
|
||||||
else 0.0
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ScolarAutorisationInscription(db.Model):
|
class ScolarAutorisationInscription(db.Model):
|
||||||
"""Autorisation d'inscription dans un semestre"""
|
"""Autorisation d'inscription dans un semestre"""
|
||||||
@ -151,7 +93,6 @@ class ScolarAutorisationInscription(db.Model):
|
|||||||
db.Integer,
|
db.Integer,
|
||||||
db.ForeignKey("notes_formsemestre.id"),
|
db.ForeignKey("notes_formsemestre.id"),
|
||||||
)
|
)
|
||||||
origin_formsemestre = db.relationship("FormSemestre", lazy="select", uselist=False)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"""{self.__class__.__name__}(id={self.id}, etudid={
|
return f"""{self.__class__.__name__}(id={self.id}, etudid={
|
||||||
@ -163,21 +104,6 @@ class ScolarAutorisationInscription(db.Model):
|
|||||||
d.pop("_sa_instance_state", None)
|
d.pop("_sa_instance_state", None)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def html(self) -> str:
|
|
||||||
"Affichage html"
|
|
||||||
link = (
|
|
||||||
self.origin_formsemestre.html_link_status(
|
|
||||||
label=f"{self.origin_formsemestre.titre_formation(with_sem_idx=1)}",
|
|
||||||
title=self.origin_formsemestre.titre_annee(),
|
|
||||||
)
|
|
||||||
if self.origin_formsemestre
|
|
||||||
else "externe/antérieure"
|
|
||||||
)
|
|
||||||
return f"""Autorisation de passage vers <b>S{self.semestre_id}</b> émise par
|
|
||||||
{link}
|
|
||||||
le {self.date.strftime("%d/%m/%Y")} à {self.date.strftime("%Hh%M")}
|
|
||||||
"""
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def autorise_etud(
|
def autorise_etud(
|
||||||
cls,
|
cls,
|
||||||
|
@ -36,7 +36,7 @@ Created on Fri Sep 9 09:15:05 2016
|
|||||||
@author: barasc
|
@author: barasc
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from app import db, log
|
from app import log
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
from app.comp.res_compat import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
from app.models import FormSemestre
|
from app.models import FormSemestre
|
||||||
@ -487,7 +487,7 @@ def comp_coeff_pond(coeffs, ponderations):
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
def get_moduleimpl(modimpl_id) -> dict:
|
def get_moduleimpl(modimpl_id) -> dict:
|
||||||
"""Renvoie l'objet modimpl dont l'id est modimpl_id"""
|
"""Renvoie l'objet modimpl dont l'id est modimpl_id"""
|
||||||
modimpl = db.session.get(ModuleImpl, modimpl_id)
|
modimpl = ModuleImpl.query.get(modimpl_id)
|
||||||
if modimpl:
|
if modimpl:
|
||||||
return modimpl
|
return modimpl
|
||||||
if SemestreTag.DEBUG:
|
if SemestreTag.DEBUG:
|
||||||
|
43
app/profiler.py
Normal file
43
app/profiler.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
from time import time
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class Profiler:
|
||||||
|
OUTPUT: str = "/tmp/scodoc.profiler.csv"
|
||||||
|
|
||||||
|
def __init__(self, tag: str) -> None:
|
||||||
|
self.tag: str = tag
|
||||||
|
self.start_time: time = None
|
||||||
|
self.stop_time: time = None
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self.start_time = time()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.stop_time = time()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def elapsed(self) -> float:
|
||||||
|
return self.stop_time - self.start_time
|
||||||
|
|
||||||
|
def dates(self) -> tuple[datetime, datetime]:
|
||||||
|
return datetime.fromtimestamp(self.start_time), datetime.fromtimestamp(
|
||||||
|
self.stop_time
|
||||||
|
)
|
||||||
|
|
||||||
|
def write(self):
|
||||||
|
with open(Profiler.OUTPUT, "a") as file:
|
||||||
|
dates: tuple = self.dates()
|
||||||
|
date_str = (dates[0].isoformat(), dates[1].isoformat())
|
||||||
|
file.write(f"\n{self.tag},{self.elapsed() : .2}")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def write_in(cls, msg: str):
|
||||||
|
with open(cls.OUTPUT, "a") as file:
|
||||||
|
file.write(f"\n# {msg}")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def clear(cls):
|
||||||
|
with open(cls.OUTPUT, "w") as file:
|
||||||
|
file.write("")
|
@ -122,7 +122,6 @@ ABAN = "ABAN"
|
|||||||
ABL = "ABL"
|
ABL = "ABL"
|
||||||
ADM = "ADM" # moyenne gen., barres UE, assiduité: sem. validé
|
ADM = "ADM" # moyenne gen., barres UE, assiduité: sem. validé
|
||||||
ADC = "ADC" # admis par compensation (eg moy(S1, S2) > 10)
|
ADC = "ADC" # admis par compensation (eg moy(S1, S2) > 10)
|
||||||
ADSUP = "ADSUP" # BUT: UE ou RCUE validé par niveau supérieur
|
|
||||||
ADJ = "ADJ" # admis par le jury
|
ADJ = "ADJ" # admis par le jury
|
||||||
ADJR = "ADJR" # UE admise car son RCUE est ADJ
|
ADJR = "ADJR" # UE admise car son RCUE est ADJ
|
||||||
ATT = "ATT" #
|
ATT = "ATT" #
|
||||||
@ -163,7 +162,6 @@ CODES_EXPL = {
|
|||||||
ADJ: "Validé par le Jury",
|
ADJ: "Validé par le Jury",
|
||||||
ADJR: "UE validée car son RCUE est validé ADJ par le jury",
|
ADJR: "UE validée car son RCUE est validé ADJ par le jury",
|
||||||
ADM: "Validé",
|
ADM: "Validé",
|
||||||
ADSUP: "UE ou RCUE validé car le niveau supérieur est validé",
|
|
||||||
AJ: "Ajourné (ou UE/BC de BUT en attente pour problème de moyenne)",
|
AJ: "Ajourné (ou UE/BC de BUT en attente pour problème de moyenne)",
|
||||||
ATB: "Décision en attente d'un autre semestre (au moins une UE sous la barre)",
|
ATB: "Décision en attente d'un autre semestre (au moins une UE sous la barre)",
|
||||||
ATJ: "Décision en attente d'un autre semestre (assiduité insuffisante)",
|
ATJ: "Décision en attente d'un autre semestre (assiduité insuffisante)",
|
||||||
@ -196,23 +194,18 @@ CODES_SEM_ATTENTES = {ATT, ATB, ATJ} # semestre en attente
|
|||||||
|
|
||||||
CODES_SEM_REO = {NAR} # reorientation
|
CODES_SEM_REO = {NAR} # reorientation
|
||||||
|
|
||||||
# Les codes d'UEs
|
|
||||||
CODES_JURY_UE = {ADM, CMP, ADJ, ADJR, ADSUP, AJ, ATJ, RAT, DEF, ABAN, DEM, UEBSL}
|
|
||||||
CODES_UE_VALIDES_DE_DROIT = {ADM, CMP} # validation "de droit"
|
CODES_UE_VALIDES_DE_DROIT = {ADM, CMP} # validation "de droit"
|
||||||
CODES_UE_VALIDES = CODES_UE_VALIDES_DE_DROIT | {ADJ, ADJR, ADSUP}
|
CODES_UE_VALIDES = CODES_UE_VALIDES_DE_DROIT | {ADJ, ADJR}
|
||||||
"UE validée"
|
"UE validée"
|
||||||
CODES_UE_CAPITALISANTS = {ADM}
|
CODES_UE_CAPITALISANTS = {ADM}
|
||||||
"UE capitalisée"
|
"UE capitalisée"
|
||||||
|
|
||||||
CODES_JURY_RCUE = {ADM, ADJ, ADSUP, CMP, AJ, ATJ, RAT, DEF, ABAN}
|
|
||||||
"codes de jury utilisables sur les RCUEs"
|
|
||||||
CODES_RCUE_VALIDES_DE_DROIT = {ADM, CMP}
|
CODES_RCUE_VALIDES_DE_DROIT = {ADM, CMP}
|
||||||
CODES_RCUE_VALIDES = CODES_RCUE_VALIDES_DE_DROIT | {ADJ, ADSUP}
|
CODES_RCUE_VALIDES = CODES_RCUE_VALIDES_DE_DROIT | {ADJ}
|
||||||
"Niveau RCUE validé"
|
"Niveau RCUE validé"
|
||||||
|
|
||||||
# Pour le BUT:
|
# Pour le BUT:
|
||||||
CODES_ANNEE_BUT_VALIDES_DE_DROIT = {ADM, PASD} # PASD pour enregistrement auto
|
CODES_ANNEE_BUT_VALIDES_DE_DROIT = {ADM, PASD}
|
||||||
CODES_ANNEE_BUT_VALIDES = {ADM, ADSUP}
|
|
||||||
CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL}
|
CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL}
|
||||||
BUT_BARRE_UE8 = 8.0 - NOTES_TOLERANCE
|
BUT_BARRE_UE8 = 8.0 - NOTES_TOLERANCE
|
||||||
BUT_BARRE_UE = BUT_BARRE_RCUE = 10.0 - NOTES_TOLERANCE
|
BUT_BARRE_UE = BUT_BARRE_RCUE = 10.0 - NOTES_TOLERANCE
|
||||||
@ -226,25 +219,17 @@ BUT_CODES_PASSAGE = {
|
|||||||
}
|
}
|
||||||
# les codes, du plus "défavorable" à l'étudiant au plus favorable:
|
# les codes, du plus "défavorable" à l'étudiant au plus favorable:
|
||||||
# (valeur par défaut 0)
|
# (valeur par défaut 0)
|
||||||
BUT_CODES_ORDER = {
|
BUT_CODES_ORDERED = {
|
||||||
ABAN: 0,
|
|
||||||
ABL: 0,
|
|
||||||
DEM: 0,
|
|
||||||
DEF: 0,
|
|
||||||
EXCLU: 0,
|
|
||||||
NAR: 0,
|
NAR: 0,
|
||||||
UEBSL: 0,
|
DEF: 0,
|
||||||
RAT: 5,
|
|
||||||
RED: 6,
|
|
||||||
AJ: 10,
|
AJ: 10,
|
||||||
ATJ: 20,
|
ATJ: 20,
|
||||||
CMP: 50,
|
CMP: 50,
|
||||||
ADC: 50,
|
ADC: 50,
|
||||||
PAS1NCI: 50,
|
PASD: 50,
|
||||||
PASD: 60,
|
PAS1NCI: 60,
|
||||||
ADJR: 90,
|
ADJR: 90,
|
||||||
ADSUP: 90,
|
ADJ: 100,
|
||||||
ADJ: 90,
|
|
||||||
ADM: 100,
|
ADM: 100,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -264,16 +249,6 @@ def code_ue_validant(code: str) -> bool:
|
|||||||
return code in CODES_UE_VALIDES
|
return code in CODES_UE_VALIDES
|
||||||
|
|
||||||
|
|
||||||
def code_rcue_validant(code: str) -> bool:
|
|
||||||
"Vrai si ce code d'RCUE est validant"
|
|
||||||
return code in CODES_RCUE_VALIDES
|
|
||||||
|
|
||||||
|
|
||||||
def code_annee_validant(code: str) -> bool:
|
|
||||||
"Vrai si code d'année BUT validant"
|
|
||||||
return code in CODES_ANNEE_BUT_VALIDES
|
|
||||||
|
|
||||||
|
|
||||||
DEVENIR_EXPL = {
|
DEVENIR_EXPL = {
|
||||||
NEXT: "Passage au semestre suivant",
|
NEXT: "Passage au semestre suivant",
|
||||||
REDOANNEE: "Redoublement année",
|
REDOANNEE: "Redoublement année",
|
||||||
|
@ -88,7 +88,7 @@ class DEFAULT_TABLE_PREFERENCES(object):
|
|||||||
return self.values[k]
|
return self.values[k]
|
||||||
|
|
||||||
|
|
||||||
class GenTable:
|
class GenTable(object):
|
||||||
"""Simple 2D tables with export to HTML, PDF, Excel, CSV.
|
"""Simple 2D tables with export to HTML, PDF, Excel, CSV.
|
||||||
Can be sub-classed to generate fancy formats.
|
Can be sub-classed to generate fancy formats.
|
||||||
"""
|
"""
|
||||||
@ -197,9 +197,6 @@ class GenTable:
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<gen_table( nrows={self.get_nb_rows()}, ncols={self.get_nb_cols()} )>"
|
return f"<gen_table( nrows={self.get_nb_rows()}, ncols={self.get_nb_cols()} )>"
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return len(self.rows)
|
|
||||||
|
|
||||||
def get_nb_cols(self):
|
def get_nb_cols(self):
|
||||||
return len(self.columns_ids)
|
return len(self.columns_ids)
|
||||||
|
|
||||||
|
4
app/scodoc/html_sidebar.py
Normal file → Executable file
4
app/scodoc/html_sidebar.py
Normal file → Executable file
@ -126,7 +126,7 @@ def sidebar(etudid: int = None):
|
|||||||
if current_user.has_permission(Permission.ScoAbsChange):
|
if current_user.has_permission(Permission.ScoAbsChange):
|
||||||
H.append(
|
H.append(
|
||||||
f"""
|
f"""
|
||||||
<li><a href="{ url_for('absences.SignaleAbsenceEtud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Ajouter</a></li>
|
<li><a href="{ url_for('assiduites.signal_assiduites_etud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Ajouter</a></li>
|
||||||
<li><a href="{ url_for('absences.JustifAbsenceEtud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Justifier</a></li>
|
<li><a href="{ url_for('absences.JustifAbsenceEtud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Justifier</a></li>
|
||||||
<li><a href="{ url_for('absences.AnnuleAbsenceEtud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Supprimer</a></li>
|
<li><a href="{ url_for('absences.AnnuleAbsenceEtud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Supprimer</a></li>
|
||||||
"""
|
"""
|
||||||
@ -138,7 +138,7 @@ def sidebar(etudid: int = None):
|
|||||||
H.append(
|
H.append(
|
||||||
f"""
|
f"""
|
||||||
<li><a href="{ url_for('absences.CalAbs', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Calendrier</a></li>
|
<li><a href="{ url_for('absences.CalAbs', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Calendrier</a></li>
|
||||||
<li><a href="{ url_for('absences.ListeAbsEtud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Liste</a></li>
|
<li><a href="{ url_for('assiduites.liste_assiduites_etud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Liste</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
41
app/scodoc/sco_abs.py
Normal file → Executable file
41
app/scodoc/sco_abs.py
Normal file → Executable file
@ -42,6 +42,8 @@ from app.scodoc import sco_cache
|
|||||||
from app.scodoc import sco_etud
|
from app.scodoc import sco_etud
|
||||||
from app.scodoc import sco_formsemestre_inscriptions
|
from app.scodoc import sco_formsemestre_inscriptions
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
|
from app.models import Assiduite, Justificatif
|
||||||
|
import app.scodoc.sco_assiduites as scass
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
|
|
||||||
# --- Misc tools.... ------------------
|
# --- Misc tools.... ------------------
|
||||||
@ -1052,6 +1054,45 @@ def get_abs_count_in_interval(etudid, date_debut_iso, date_fin_iso):
|
|||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def get_assiduites_count_in_interval(etudid, date_debut_iso, date_fin_iso):
|
||||||
|
"""Les comptes d'absences de cet étudiant entre ces deux dates, incluses:
|
||||||
|
tuple (nb abs, nb abs justifiées)
|
||||||
|
Utilise un cache.
|
||||||
|
"""
|
||||||
|
key = str(etudid) + "_" + date_debut_iso + "_" + date_fin_iso + "_assiduites"
|
||||||
|
r = sco_cache.AbsSemEtudCache.get(key)
|
||||||
|
if not r:
|
||||||
|
|
||||||
|
date_debut: datetime.datetime = scu.is_iso_formated(date_debut_iso, True)
|
||||||
|
date_fin: datetime.datetime = scu.is_iso_formated(date_fin_iso, True)
|
||||||
|
|
||||||
|
assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid)
|
||||||
|
justificatifs: Justificatif = Justificatif.query.filter_by(etudid=etudid)
|
||||||
|
|
||||||
|
assiduites = scass.filter_by_date(assiduites, Assiduite, date_debut, date_fin)
|
||||||
|
justificatifs = scass.filter_by_date(
|
||||||
|
justificatifs, Justificatif, date_debut, date_fin
|
||||||
|
)
|
||||||
|
|
||||||
|
calculator: scass.CountCalculator = scass.CountCalculator()
|
||||||
|
calculator.compute_assiduites(assiduites)
|
||||||
|
nb_abs: dict = calculator.to_dict()["demi"]
|
||||||
|
|
||||||
|
abs_just: list[Assiduite] = scass.get_all_justified(
|
||||||
|
etudid, date_debut, date_fin
|
||||||
|
)
|
||||||
|
|
||||||
|
calculator.reset()
|
||||||
|
calculator.compute_assiduites(abs_just)
|
||||||
|
nb_abs_just: dict = calculator.to_dict()["demi"]
|
||||||
|
|
||||||
|
r = (nb_abs, nb_abs_just)
|
||||||
|
ans = sco_cache.AbsSemEtudCache.set(key, r)
|
||||||
|
if not ans:
|
||||||
|
log("warning: get_assiduites_count failed to cache")
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
def invalidate_abs_count(etudid, sem):
|
def invalidate_abs_count(etudid, sem):
|
||||||
"""Invalidate (clear) cached counts"""
|
"""Invalidate (clear) cached counts"""
|
||||||
date_debut = sem["date_debut_iso"]
|
date_debut = sem["date_debut_iso"]
|
||||||
|
@ -51,14 +51,7 @@ from app import log
|
|||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
from app.comp.res_compat import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
from app.comp.res_but import ResultatsSemestreBUT
|
from app.comp.res_but import ResultatsSemestreBUT
|
||||||
from app.models import (
|
from app.models import FormSemestre, Identite, ApcValidationAnnee
|
||||||
ApcValidationAnnee,
|
|
||||||
ApcValidationRCUE,
|
|
||||||
FormSemestre,
|
|
||||||
Identite,
|
|
||||||
ScolarFormSemestreValidation,
|
|
||||||
)
|
|
||||||
|
|
||||||
from app.models.config import ScoDocSiteConfig
|
from app.models.config import ScoDocSiteConfig
|
||||||
from app.scodoc.sco_apogee_reader import (
|
from app.scodoc.sco_apogee_reader import (
|
||||||
APO_DECIMAL_SEP,
|
APO_DECIMAL_SEP,
|
||||||
@ -71,7 +64,6 @@ from app.scodoc.gen_tables import GenTable
|
|||||||
from app.scodoc.sco_vdi import ApoEtapeVDI
|
from app.scodoc.sco_vdi import ApoEtapeVDI
|
||||||
from app.scodoc.codes_cursus import code_semestre_validant
|
from app.scodoc.codes_cursus import code_semestre_validant
|
||||||
from app.scodoc.codes_cursus import (
|
from app.scodoc.codes_cursus import (
|
||||||
ADSUP,
|
|
||||||
DEF,
|
DEF,
|
||||||
DEM,
|
DEM,
|
||||||
NAR,
|
NAR,
|
||||||
@ -224,12 +216,7 @@ class ApoEtud(dict):
|
|||||||
break
|
break
|
||||||
self.col_elts[code] = elt
|
self.col_elts[code] = elt
|
||||||
if elt is None:
|
if elt is None:
|
||||||
try:
|
self.new_cols[col_id] = self.cols[col_id]
|
||||||
self.new_cols[col_id] = self.cols[col_id]
|
|
||||||
except KeyError as exc:
|
|
||||||
raise ScoFormatError(
|
|
||||||
f"""Fichier Apogee invalide : ligne mal formatée ? <br>colonne <tt>{col_id}</tt> non déclarée ?"""
|
|
||||||
) from exc
|
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
self.new_cols[col_id] = sco_elts[code][
|
self.new_cols[col_id] = sco_elts[code][
|
||||||
@ -336,22 +323,14 @@ class ApoEtud(dict):
|
|||||||
x.strip() for x in ue["code_apogee"].split(",")
|
x.strip() for x in ue["code_apogee"].split(",")
|
||||||
}:
|
}:
|
||||||
if self.export_res_ues:
|
if self.export_res_ues:
|
||||||
if (
|
if decisions_ue and ue["ue_id"] in decisions_ue:
|
||||||
decisions_ue and ue["ue_id"] in decisions_ue
|
|
||||||
) or self.export_res_sdj:
|
|
||||||
ue_status = res.get_etud_ue_status(etudid, ue["ue_id"])
|
ue_status = res.get_etud_ue_status(etudid, ue["ue_id"])
|
||||||
if decisions_ue and ue["ue_id"] in decisions_ue:
|
code_decision_ue = decisions_ue[ue["ue_id"]]["code"]
|
||||||
code_decision_ue = decisions_ue[ue["ue_id"]]["code"]
|
|
||||||
code_decision_ue_apo = ScoDocSiteConfig.get_code_apo(
|
|
||||||
code_decision_ue
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
code_decision_ue_apo = ""
|
|
||||||
return dict(
|
return dict(
|
||||||
N=self.fmt_note(ue_status["moy"] if ue_status else ""),
|
N=self.fmt_note(ue_status["moy"] if ue_status else ""),
|
||||||
B=20,
|
B=20,
|
||||||
J="",
|
J="",
|
||||||
R=code_decision_ue_apo,
|
R=ScoDocSiteConfig.get_code_apo(code_decision_ue),
|
||||||
M="",
|
M="",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -364,17 +343,14 @@ class ApoEtud(dict):
|
|||||||
module_code_found = False
|
module_code_found = False
|
||||||
for modimpl in modimpls:
|
for modimpl in modimpls:
|
||||||
module = modimpl["module"]
|
module = modimpl["module"]
|
||||||
if (
|
if module["code_apogee"] and code in {
|
||||||
res.modimpl_inscr_df[modimpl["moduleimpl_id"]][etudid]
|
x.strip() for x in module["code_apogee"].split(",")
|
||||||
and module["code_apogee"]
|
}:
|
||||||
and code in {x.strip() for x in module["code_apogee"].split(",")}
|
|
||||||
):
|
|
||||||
n = res.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid)
|
n = res.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid)
|
||||||
if n != "NI" and self.export_res_modules:
|
if n != "NI" and self.export_res_modules:
|
||||||
return dict(N=self.fmt_note(n), B=20, J="", R="")
|
return dict(N=self.fmt_note(n), B=20, J="", R="")
|
||||||
else:
|
else:
|
||||||
module_code_found = True
|
module_code_found = True
|
||||||
|
|
||||||
if module_code_found:
|
if module_code_found:
|
||||||
return VOID_APO_RES
|
return VOID_APO_RES
|
||||||
#
|
#
|
||||||
@ -497,10 +473,7 @@ class ApoEtud(dict):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _but_load_validation_annuelle(self):
|
def _but_load_validation_annuelle(self):
|
||||||
"""charge la validation de jury BUT annuelle.
|
"charge la validation de jury BUT annuelle"
|
||||||
Ici impose qu'elle soit issue d'un semestre de l'année en cours
|
|
||||||
(pas forcément nécessaire, voir selon les retours des équipes ?)
|
|
||||||
"""
|
|
||||||
# le semestre impair de l'année scolaire
|
# le semestre impair de l'année scolaire
|
||||||
if self.cur_res.formsemestre.semestre_id % 2:
|
if self.cur_res.formsemestre.semestre_id % 2:
|
||||||
formsemestre = self.cur_res.formsemestre
|
formsemestre = self.cur_res.formsemestre
|
||||||
@ -515,11 +488,11 @@ class ApoEtud(dict):
|
|||||||
# ne trouve pas de semestre impair
|
# ne trouve pas de semestre impair
|
||||||
self.validation_annee_but = None
|
self.validation_annee_but = None
|
||||||
return
|
return
|
||||||
self.validation_annee_but: ApcValidationAnnee = ApcValidationAnnee.query.filter_by(
|
self.validation_annee_but: ApcValidationAnnee = (
|
||||||
formsemestre_id=formsemestre.id,
|
ApcValidationAnnee.query.filter_by(
|
||||||
etudid=self.etud["etudid"],
|
formsemestre_id=formsemestre.id, etudid=self.etud["etudid"]
|
||||||
referentiel_competence_id=self.cur_res.formsemestre.formation.referentiel_competence_id,
|
).first()
|
||||||
).first()
|
)
|
||||||
self.is_nar = (
|
self.is_nar = (
|
||||||
self.validation_annee_but and self.validation_annee_but.code == NAR
|
self.validation_annee_but and self.validation_annee_but.code == NAR
|
||||||
)
|
)
|
||||||
@ -919,75 +892,6 @@ class ApoData:
|
|||||||
)
|
)
|
||||||
return T
|
return T
|
||||||
|
|
||||||
def build_adsup_table(self):
|
|
||||||
"""Construit une table listant les ADSUP émis depuis les formsemestres
|
|
||||||
NIP nom prenom nom_formsemestre etape UE
|
|
||||||
"""
|
|
||||||
validations_ues, validations_rcue = self.list_adsup()
|
|
||||||
rows = [
|
|
||||||
{
|
|
||||||
"code_nip": v.etud.code_nip,
|
|
||||||
"nom": v.etud.nom,
|
|
||||||
"prenom": v.etud.prenom,
|
|
||||||
"formsemestre": v.formsemestre.titre_formation(with_sem_idx=1),
|
|
||||||
"etape": v.formsemestre.etapes_apo_str(),
|
|
||||||
"ue": v.ue.acronyme,
|
|
||||||
}
|
|
||||||
for v in validations_ues
|
|
||||||
]
|
|
||||||
rows += [
|
|
||||||
{
|
|
||||||
"code_nip": v.etud.code_nip,
|
|
||||||
"nom": v.etud.nom,
|
|
||||||
"prenom": v.etud.prenom,
|
|
||||||
"formsemestre": v.formsemestre.titre_formation(with_sem_idx=1),
|
|
||||||
"etape": "", # on ne sait pas à quel étape rattacher le RCUE
|
|
||||||
"rcue": f"{v.ue1.acronyme}/{v.ue2.acronyme}",
|
|
||||||
}
|
|
||||||
for v in validations_rcue
|
|
||||||
]
|
|
||||||
|
|
||||||
return GenTable(
|
|
||||||
columns_ids=(
|
|
||||||
"code_nip",
|
|
||||||
"nom",
|
|
||||||
"prenom",
|
|
||||||
"formsemestre",
|
|
||||||
"etape",
|
|
||||||
"ue",
|
|
||||||
"rcue",
|
|
||||||
),
|
|
||||||
titles={
|
|
||||||
"code_nip": "NIP",
|
|
||||||
"nom": "Nom",
|
|
||||||
"prenom": "Prénom",
|
|
||||||
"formsemestre": "Semestre",
|
|
||||||
"etape": "Etape",
|
|
||||||
"ue": "UE",
|
|
||||||
"rcue": "RCUE",
|
|
||||||
},
|
|
||||||
rows=rows,
|
|
||||||
xls_sheet_name="ADSUPs",
|
|
||||||
)
|
|
||||||
|
|
||||||
def list_adsup(
|
|
||||||
self,
|
|
||||||
) -> tuple[list[ScolarFormSemestreValidation], list[ApcValidationRCUE]]:
|
|
||||||
"""Liste les validations ADSUP émises par des formsemestres de cet ensemble"""
|
|
||||||
validations_ues = (
|
|
||||||
ScolarFormSemestreValidation.query.filter_by(code=ADSUP)
|
|
||||||
.filter(ScolarFormSemestreValidation.ue_id != None)
|
|
||||||
.filter(
|
|
||||||
ScolarFormSemestreValidation.formsemestre_id.in_(
|
|
||||||
self.etape_formsemestre_ids
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
validations_rcue = ApcValidationRCUE.query.filter_by(code=ADSUP).filter(
|
|
||||||
ApcValidationRCUE.formsemestre_id.in_(self.etape_formsemestre_ids)
|
|
||||||
)
|
|
||||||
return validations_ues, validations_rcue
|
|
||||||
|
|
||||||
|
|
||||||
def comp_apo_sems(etape_apogee, annee_scolaire: int) -> list[dict]:
|
def comp_apo_sems(etape_apogee, annee_scolaire: int) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
@ -1114,10 +1018,6 @@ def export_csv_to_apogee(
|
|||||||
cr_table = apo_data.build_cr_table()
|
cr_table = apo_data.build_cr_table()
|
||||||
cr_xls = cr_table.excel()
|
cr_xls = cr_table.excel()
|
||||||
|
|
||||||
# ADSUPs
|
|
||||||
adsup_table = apo_data.build_adsup_table()
|
|
||||||
adsup_xls = adsup_table.excel() if len(adsup_table) else None
|
|
||||||
|
|
||||||
# Create ZIP
|
# Create ZIP
|
||||||
if not dest_zip:
|
if not dest_zip:
|
||||||
data = io.BytesIO()
|
data = io.BytesIO()
|
||||||
@ -1143,7 +1043,6 @@ def export_csv_to_apogee(
|
|||||||
log_filename = "scodoc-" + basename + ".log.txt"
|
log_filename = "scodoc-" + basename + ".log.txt"
|
||||||
nar_filename = basename + "-nar" + scu.XLSX_SUFFIX
|
nar_filename = basename + "-nar" + scu.XLSX_SUFFIX
|
||||||
cr_filename = basename + "-decisions" + scu.XLSX_SUFFIX
|
cr_filename = basename + "-decisions" + scu.XLSX_SUFFIX
|
||||||
adsup_filename = f"{basename}-adsups{scu.XLSX_SUFFIX}"
|
|
||||||
|
|
||||||
logf = io.StringIO()
|
logf = io.StringIO()
|
||||||
logf.write(f"export_to_apogee du {time.ctime()}\n\n")
|
logf.write(f"export_to_apogee du {time.ctime()}\n\n")
|
||||||
@ -1180,8 +1079,6 @@ def export_csv_to_apogee(
|
|||||||
"\n\nElements Apogee inconnus dans ces semestres ScoDoc:\n"
|
"\n\nElements Apogee inconnus dans ces semestres ScoDoc:\n"
|
||||||
+ "\n".join(apo_data.list_unknown_elements())
|
+ "\n".join(apo_data.list_unknown_elements())
|
||||||
)
|
)
|
||||||
if adsup_xls:
|
|
||||||
logf.write(f"\n\nADSUP générés: {len(adsup_table)}\n")
|
|
||||||
log(logf.getvalue()) # sortie aussi sur le log ScoDoc
|
log(logf.getvalue()) # sortie aussi sur le log ScoDoc
|
||||||
|
|
||||||
# Write data to ZIP
|
# Write data to ZIP
|
||||||
@ -1190,8 +1087,6 @@ def export_csv_to_apogee(
|
|||||||
if nar_xls:
|
if nar_xls:
|
||||||
dest_zip.writestr(nar_filename, nar_xls)
|
dest_zip.writestr(nar_filename, nar_xls)
|
||||||
dest_zip.writestr(cr_filename, cr_xls)
|
dest_zip.writestr(cr_filename, cr_xls)
|
||||||
if adsup_xls:
|
|
||||||
dest_zip.writestr(adsup_filename, adsup_xls)
|
|
||||||
|
|
||||||
if my_zip:
|
if my_zip:
|
||||||
dest_zip.close()
|
dest_zip.close()
|
||||||
|
@ -295,15 +295,8 @@ class ApoCSVReadWrite:
|
|||||||
filename=self.get_filename(),
|
filename=self.get_filename(),
|
||||||
)
|
)
|
||||||
cols = {} # { col_id : value }
|
cols = {} # { col_id : value }
|
||||||
try:
|
for i, field in enumerate(fields):
|
||||||
for i, field in enumerate(fields):
|
cols[self.col_ids[i]] = field
|
||||||
cols[self.col_ids[i]] = field
|
|
||||||
except IndexError as exc:
|
|
||||||
raise
|
|
||||||
raise ScoFormatError(
|
|
||||||
f"Fichier Apogee incorrect (colonnes excédentaires ? (<tt>{i}/{field}</tt>))",
|
|
||||||
filename=self.get_filename(),
|
|
||||||
) from exc
|
|
||||||
etud_tuples.append(
|
etud_tuples.append(
|
||||||
ApoEtudTuple(
|
ApoEtudTuple(
|
||||||
nip=fields[0], # id etudiant
|
nip=fields[0], # id etudiant
|
||||||
|
@ -68,9 +68,9 @@ from app import log, ScoDocJSONEncoder
|
|||||||
from app.but import jury_but_pv
|
from app.but import jury_but_pv
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
from app.comp.res_compat import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
from app.models import Departement, FormSemestre
|
from app.models import FormSemestre
|
||||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||||
from app.scodoc.sco_exceptions import ScoException, ScoPermissionDenied
|
from app.scodoc.sco_exceptions import ScoPermissionDenied
|
||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
from app.scodoc import sco_bulletins_pdf
|
from app.scodoc import sco_bulletins_pdf
|
||||||
from app.scodoc import sco_groups
|
from app.scodoc import sco_groups
|
||||||
@ -86,6 +86,11 @@ class BaseArchiver(object):
|
|||||||
self.archive_type = archive_type
|
self.archive_type = archive_type
|
||||||
self.initialized = False
|
self.initialized = False
|
||||||
self.root = None
|
self.root = None
|
||||||
|
self.dept_id = None
|
||||||
|
|
||||||
|
def set_dept_id(self, dept_id: int):
|
||||||
|
"set dept"
|
||||||
|
self.dept_id = dept_id
|
||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
if self.initialized:
|
if self.initialized:
|
||||||
@ -107,6 +112,8 @@ class BaseArchiver(object):
|
|||||||
finally:
|
finally:
|
||||||
scu.GSL.release()
|
scu.GSL.release()
|
||||||
self.initialized = True
|
self.initialized = True
|
||||||
|
if self.dept_id is None:
|
||||||
|
self.dept_id = getattr(g, "scodoc_dept_id")
|
||||||
|
|
||||||
def get_obj_dir(self, oid: int):
|
def get_obj_dir(self, oid: int):
|
||||||
"""
|
"""
|
||||||
@ -114,8 +121,7 @@ class BaseArchiver(object):
|
|||||||
If directory does not yet exist, create it.
|
If directory does not yet exist, create it.
|
||||||
"""
|
"""
|
||||||
self.initialize()
|
self.initialize()
|
||||||
dept = Departement.query.filter_by(acronym=g.scodoc_dept).first()
|
dept_dir = os.path.join(self.root, str(self.dept_id))
|
||||||
dept_dir = os.path.join(self.root, str(dept.id))
|
|
||||||
try:
|
try:
|
||||||
scu.GSL.acquire()
|
scu.GSL.acquire()
|
||||||
if not os.path.isdir(dept_dir):
|
if not os.path.isdir(dept_dir):
|
||||||
@ -125,12 +131,6 @@ class BaseArchiver(object):
|
|||||||
if not os.path.isdir(obj_dir):
|
if not os.path.isdir(obj_dir):
|
||||||
log(f"creating directory {obj_dir}")
|
log(f"creating directory {obj_dir}")
|
||||||
os.mkdir(obj_dir)
|
os.mkdir(obj_dir)
|
||||||
except FileExistsError as exc:
|
|
||||||
raise ScoException(
|
|
||||||
f"""BaseArchiver error: obj_dir={obj_dir} exists={
|
|
||||||
os.path.exists(obj_dir)
|
|
||||||
} isdir={os.path.isdir(obj_dir)}"""
|
|
||||||
) from exc
|
|
||||||
finally:
|
finally:
|
||||||
scu.GSL.release()
|
scu.GSL.release()
|
||||||
return obj_dir
|
return obj_dir
|
||||||
@ -140,8 +140,7 @@ class BaseArchiver(object):
|
|||||||
:return: list of archive oids
|
:return: list of archive oids
|
||||||
"""
|
"""
|
||||||
self.initialize()
|
self.initialize()
|
||||||
dept = Departement.query.filter_by(acronym=g.scodoc_dept).first()
|
base = os.path.join(self.root, str(self.dept_id)) + os.path.sep
|
||||||
base = os.path.join(self.root, str(dept.id)) + os.path.sep
|
|
||||||
dirs = glob.glob(base + "*")
|
dirs = glob.glob(base + "*")
|
||||||
return [os.path.split(x)[1] for x in dirs]
|
return [os.path.split(x)[1] for x in dirs]
|
||||||
|
|
||||||
@ -344,7 +343,7 @@ def do_formsemestre_archive(
|
|||||||
if data:
|
if data:
|
||||||
PVArchive.store(archive_id, "Tableau_moyennes" + scu.XLSX_SUFFIX, 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)
|
# Tableau recap notes en HTML (pour tous les etudiants, n'utilise pas les groupes)
|
||||||
table_html, _, _ = gen_formsemestre_recapcomplet_html_table(
|
table_html, _ = gen_formsemestre_recapcomplet_html_table(
|
||||||
formsemestre, res, include_evaluations=True
|
formsemestre, res, include_evaluations=True
|
||||||
)
|
)
|
||||||
if table_html:
|
if table_html:
|
||||||
|
215
app/scodoc/sco_archives_justificatifs.py
Normal file
215
app/scodoc/sco_archives_justificatifs.py
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
"""
|
||||||
|
Gestion de l'archivage des justificatifs
|
||||||
|
|
||||||
|
Ecrit par Matthias HARTMANN
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from shutil import rmtree
|
||||||
|
|
||||||
|
from app.models import Identite
|
||||||
|
from app.scodoc.sco_archives import BaseArchiver
|
||||||
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
|
from app.scodoc.sco_utils import is_iso_formated
|
||||||
|
|
||||||
|
|
||||||
|
class Trace:
|
||||||
|
"""gestionnaire de la trace des fichiers justificatifs"""
|
||||||
|
|
||||||
|
def __init__(self, path: str) -> None:
|
||||||
|
self.path: str = path + "/_trace.csv"
|
||||||
|
self.content: dict[str, list[datetime, datetime]] = {}
|
||||||
|
self.import_from_file()
|
||||||
|
|
||||||
|
def import_from_file(self):
|
||||||
|
"""import trace from file"""
|
||||||
|
if os.path.isfile(self.path):
|
||||||
|
with open(self.path, "r", encoding="utf-8") as file:
|
||||||
|
for line in file.readlines():
|
||||||
|
csv = line.split(",")
|
||||||
|
fname: str = csv[0]
|
||||||
|
entry_date: datetime = is_iso_formated(csv[1], True)
|
||||||
|
delete_date: datetime = is_iso_formated(csv[2], True)
|
||||||
|
|
||||||
|
self.content[fname] = [entry_date, delete_date]
|
||||||
|
|
||||||
|
def set_trace(self, *fnames: str, mode: str = "entry"):
|
||||||
|
"""Ajoute une trace du fichier donné
|
||||||
|
mode : entry / delete
|
||||||
|
"""
|
||||||
|
modes: list[str] = ["entry", "delete"]
|
||||||
|
for fname in fnames:
|
||||||
|
if fname in modes:
|
||||||
|
continue
|
||||||
|
traced: list[datetime, datetime] = self.content.get(fname, False)
|
||||||
|
if not traced:
|
||||||
|
self.content[fname] = [None, None]
|
||||||
|
traced = self.content[fname]
|
||||||
|
|
||||||
|
traced[modes.index(mode)] = datetime.now()
|
||||||
|
self.save_trace()
|
||||||
|
|
||||||
|
def save_trace(self):
|
||||||
|
"""Enregistre la trace dans le fichier _trace.csv"""
|
||||||
|
lines: list[str] = []
|
||||||
|
for fname, traced in self.content.items():
|
||||||
|
date_fin: datetime or None = traced[1].isoformat() if traced[1] else "None"
|
||||||
|
|
||||||
|
lines.append(f"{fname},{traced[0].isoformat()},{date_fin}")
|
||||||
|
with open(self.path, "w", encoding="utf-8") as file:
|
||||||
|
file.write("\n".join(lines))
|
||||||
|
|
||||||
|
def get_trace(self, fnames: list[str] = ()) -> dict[str, list[datetime, datetime]]:
|
||||||
|
"""Récupère la trace pour les noms de fichiers.
|
||||||
|
si aucun nom n'est donné, récupère tous les fichiers"""
|
||||||
|
|
||||||
|
if fnames is None or len(fnames) == 0:
|
||||||
|
return self.content
|
||||||
|
|
||||||
|
traced: dict = {}
|
||||||
|
for fname in fnames:
|
||||||
|
traced[fname] = self.content.get(fname, None)
|
||||||
|
|
||||||
|
return traced
|
||||||
|
|
||||||
|
|
||||||
|
class JustificatifArchiver(BaseArchiver):
|
||||||
|
"""
|
||||||
|
|
||||||
|
TOTALK:
|
||||||
|
- oid -> etudid
|
||||||
|
- archive_id -> date de création de l'archive (une archive par dépot de document)
|
||||||
|
|
||||||
|
justificatif
|
||||||
|
└── <dept_id>
|
||||||
|
└── <etudid/oid>
|
||||||
|
├── [_trace.csv]
|
||||||
|
└── <archive_id>
|
||||||
|
├── [_description.txt]
|
||||||
|
└── [<filename.ext>]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
BaseArchiver.__init__(self, archive_type="justificatifs")
|
||||||
|
|
||||||
|
def save_justificatif(
|
||||||
|
self,
|
||||||
|
etudid: int,
|
||||||
|
filename: str,
|
||||||
|
data: bytes or str,
|
||||||
|
archive_name: str = None,
|
||||||
|
description: str = "",
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Ajoute un fichier dans une archive "justificatif" pour l'etudid donné
|
||||||
|
Retourne l'archive_name utilisé
|
||||||
|
"""
|
||||||
|
self._set_dept(etudid)
|
||||||
|
if archive_name is None:
|
||||||
|
archive_id: str = self.create_obj_archive(
|
||||||
|
oid=etudid, description=description
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
archive_id: str = self.get_id_from_name(etudid, archive_name)
|
||||||
|
|
||||||
|
fname: str = self.store(archive_id, filename, data)
|
||||||
|
|
||||||
|
trace = Trace(self.get_obj_dir(etudid))
|
||||||
|
trace.set_trace(fname, "entry")
|
||||||
|
|
||||||
|
return self.get_archive_name(archive_id), fname
|
||||||
|
|
||||||
|
def delete_justificatif(
|
||||||
|
self,
|
||||||
|
etudid: int,
|
||||||
|
archive_name: str,
|
||||||
|
filename: str = None,
|
||||||
|
has_trace: bool = True,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Supprime une archive ou un fichier particulier de l'archivage de l'étudiant donné
|
||||||
|
|
||||||
|
Si trace == True : sauvegarde le nom du/des fichier(s) supprimé(s) dans la trace de l'étudiant
|
||||||
|
"""
|
||||||
|
self._set_dept(etudid)
|
||||||
|
if str(etudid) not in self.list_oids():
|
||||||
|
raise ValueError(f"Aucune archive pour etudid[{etudid}]")
|
||||||
|
|
||||||
|
archive_id = self.get_id_from_name(etudid, archive_name)
|
||||||
|
|
||||||
|
if filename is not None:
|
||||||
|
if filename not in self.list_archive(archive_id):
|
||||||
|
raise ValueError(
|
||||||
|
f"filename {filename} inconnu dans l'archive archive_id[{archive_id}] -> etudid[{etudid}]"
|
||||||
|
)
|
||||||
|
|
||||||
|
path: str = os.path.join(self.get_obj_dir(etudid), archive_id, filename)
|
||||||
|
|
||||||
|
if os.path.isfile(path):
|
||||||
|
if has_trace:
|
||||||
|
trace = Trace(self.get_obj_dir(etudid))
|
||||||
|
trace.set_trace(filename, "delete")
|
||||||
|
os.remove(path)
|
||||||
|
|
||||||
|
else:
|
||||||
|
if has_trace:
|
||||||
|
trace = Trace(self.get_obj_dir(etudid))
|
||||||
|
trace.set_trace(*self.list_archive(archive_id), mode="delete")
|
||||||
|
|
||||||
|
self.delete_archive(
|
||||||
|
os.path.join(
|
||||||
|
self.get_obj_dir(etudid),
|
||||||
|
archive_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def list_justificatifs(self, archive_name: str, etudid: int) -> list[str]:
|
||||||
|
"""
|
||||||
|
Retourne la liste des noms de fichiers dans l'archive donnée
|
||||||
|
"""
|
||||||
|
self._set_dept(etudid)
|
||||||
|
filenames: list[str] = []
|
||||||
|
archive_id = self.get_id_from_name(etudid, archive_name)
|
||||||
|
|
||||||
|
filenames = self.list_archive(archive_id)
|
||||||
|
return filenames
|
||||||
|
|
||||||
|
def get_justificatif_file(self, archive_name: str, etudid: int, filename: str):
|
||||||
|
"""
|
||||||
|
Retourne une réponse de téléchargement de fichier si le fichier existe
|
||||||
|
"""
|
||||||
|
self._set_dept(etudid)
|
||||||
|
archive_id: str = self.get_id_from_name(etudid, archive_name)
|
||||||
|
if filename in self.list_archive(archive_id):
|
||||||
|
return self.get_archived_file(etudid, archive_name, filename)
|
||||||
|
raise ScoValueError(
|
||||||
|
f"Fichier {filename} introuvable dans l'archive {archive_name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _set_dept(self, etudid: int):
|
||||||
|
"""
|
||||||
|
Mets à jour le dept_id de l'archiver en fonction du département de l'étudiant
|
||||||
|
"""
|
||||||
|
etud: Identite = Identite.query.filter_by(id=etudid).first()
|
||||||
|
self.set_dept_id(etud.dept_id)
|
||||||
|
|
||||||
|
def remove_dept_archive(self, dept_id: int = None):
|
||||||
|
"""
|
||||||
|
Supprime toutes les archives d'un département (ou de tous les départements)
|
||||||
|
⚠ Supprime aussi les fichiers de trace ⚠
|
||||||
|
"""
|
||||||
|
self.set_dept_id(1)
|
||||||
|
self.initialize()
|
||||||
|
|
||||||
|
if dept_id is None:
|
||||||
|
rmtree(self.root, ignore_errors=True)
|
||||||
|
else:
|
||||||
|
rmtree(os.path.join(self.root, str(dept_id)), ignore_errors=True)
|
||||||
|
|
||||||
|
def get_trace(
|
||||||
|
self, etudid: int, *fnames: str
|
||||||
|
) -> dict[str, list[datetime, datetime]]:
|
||||||
|
"""Récupère la trace des justificatifs de l'étudiant"""
|
||||||
|
trace = Trace(self.get_obj_dir(etudid))
|
||||||
|
return trace.get_trace(fnames)
|
352
app/scodoc/sco_assiduites.py
Normal file
352
app/scodoc/sco_assiduites.py
Normal file
@ -0,0 +1,352 @@
|
|||||||
|
"""
|
||||||
|
Ecrit par Matthias Hartmann.
|
||||||
|
"""
|
||||||
|
from datetime import date, datetime, time, timedelta
|
||||||
|
|
||||||
|
import app.scodoc.sco_utils as scu
|
||||||
|
from app.models.assiduites import Assiduite, Justificatif
|
||||||
|
from app.models.etudiants import Identite
|
||||||
|
from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
||||||
|
|
||||||
|
|
||||||
|
class CountCalculator:
|
||||||
|
"""Classe qui gére le comptage des assiduités"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
morning: time = time(8, 0),
|
||||||
|
noon: time = time(12, 0),
|
||||||
|
after_noon: time = time(14, 00),
|
||||||
|
evening: time = time(18, 0),
|
||||||
|
skip_saturday: bool = True,
|
||||||
|
) -> None:
|
||||||
|
self.morning: time = morning
|
||||||
|
self.noon: time = noon
|
||||||
|
self.after_noon: time = after_noon
|
||||||
|
self.evening: time = evening
|
||||||
|
self.skip_saturday: bool = skip_saturday
|
||||||
|
|
||||||
|
delta_total: timedelta = datetime.combine(date.min, evening) - datetime.combine(
|
||||||
|
date.min, morning
|
||||||
|
)
|
||||||
|
delta_lunch: timedelta = datetime.combine(
|
||||||
|
date.min, after_noon
|
||||||
|
) - datetime.combine(date.min, noon)
|
||||||
|
|
||||||
|
self.hour_per_day: float = (delta_total - delta_lunch).total_seconds() / 3600
|
||||||
|
|
||||||
|
self.days: list[date] = []
|
||||||
|
self.half_days: list[tuple[date, bool]] = [] # tuple -> (date, morning:bool)
|
||||||
|
self.hours: float = 0.0
|
||||||
|
|
||||||
|
self.count: int = 0
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
"""Remet à zero le compteur"""
|
||||||
|
self.days = []
|
||||||
|
self.half_days = []
|
||||||
|
self.hours = 0.0
|
||||||
|
self.count = 0
|
||||||
|
|
||||||
|
def add_half_day(self, day: date, is_morning: bool = True):
|
||||||
|
"""Ajoute une demi journée dans le comptage"""
|
||||||
|
key: tuple[date, bool] = (day, is_morning)
|
||||||
|
if key not in self.half_days:
|
||||||
|
self.half_days.append(key)
|
||||||
|
|
||||||
|
def add_day(self, day: date):
|
||||||
|
"""Ajoute un jour dans le comptage"""
|
||||||
|
if day not in self.days:
|
||||||
|
self.days.append(day)
|
||||||
|
|
||||||
|
def check_in_morning(self, period: tuple[datetime, datetime]) -> bool:
|
||||||
|
"""Vérifiée si la période donnée fait partie du matin
|
||||||
|
(Test sur la date de début)
|
||||||
|
"""
|
||||||
|
|
||||||
|
interval_morning: tuple[datetime, datetime] = (
|
||||||
|
scu.localize_datetime(datetime.combine(period[0].date(), self.morning)),
|
||||||
|
scu.localize_datetime(datetime.combine(period[0].date(), self.noon)),
|
||||||
|
)
|
||||||
|
|
||||||
|
in_morning: bool = scu.is_period_overlapping(
|
||||||
|
period, interval_morning, bornes=False
|
||||||
|
)
|
||||||
|
return in_morning
|
||||||
|
|
||||||
|
def check_in_evening(self, period: tuple[datetime, datetime]) -> bool:
|
||||||
|
"""Vérifie si la période fait partie de l'aprèm
|
||||||
|
(test sur la date de début)
|
||||||
|
"""
|
||||||
|
|
||||||
|
interval_evening: tuple[datetime, datetime] = (
|
||||||
|
scu.localize_datetime(datetime.combine(period[0].date(), self.after_noon)),
|
||||||
|
scu.localize_datetime(datetime.combine(period[0].date(), self.evening)),
|
||||||
|
)
|
||||||
|
|
||||||
|
in_evening: bool = scu.is_period_overlapping(period, interval_evening)
|
||||||
|
|
||||||
|
return in_evening
|
||||||
|
|
||||||
|
def compute_long_assiduite(self, assi: Assiduite):
|
||||||
|
"""Calcule les métriques sur une assiduité longue (plus d'un jour)"""
|
||||||
|
|
||||||
|
pointer_date: date = assi.date_debut.date() + timedelta(days=1)
|
||||||
|
start_hours: timedelta = assi.date_debut - scu.localize_datetime(
|
||||||
|
datetime.combine(assi.date_debut, self.morning)
|
||||||
|
)
|
||||||
|
finish_hours: timedelta = assi.date_fin - scu.localize_datetime(
|
||||||
|
datetime.combine(assi.date_fin, self.morning)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.add_day(assi.date_debut.date())
|
||||||
|
self.add_day(assi.date_fin.date())
|
||||||
|
|
||||||
|
start_period: tuple[datetime, datetime] = (
|
||||||
|
assi.date_debut,
|
||||||
|
scu.localize_datetime(
|
||||||
|
datetime.combine(assi.date_debut.date(), self.evening)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
finish_period: tuple[datetime, datetime] = (
|
||||||
|
scu.localize_datetime(datetime.combine(assi.date_fin.date(), self.morning)),
|
||||||
|
assi.date_fin,
|
||||||
|
)
|
||||||
|
hours = 0.0
|
||||||
|
for period in (start_period, finish_period):
|
||||||
|
if self.check_in_evening(period):
|
||||||
|
self.add_half_day(period[0].date(), False)
|
||||||
|
if self.check_in_morning(period):
|
||||||
|
self.add_half_day(period[0].date())
|
||||||
|
|
||||||
|
while pointer_date < assi.date_fin.date():
|
||||||
|
if pointer_date.weekday() < (6 - self.skip_saturday):
|
||||||
|
self.add_day(pointer_date)
|
||||||
|
self.add_half_day(pointer_date)
|
||||||
|
self.add_half_day(pointer_date, False)
|
||||||
|
self.hours += self.hour_per_day
|
||||||
|
hours += self.hour_per_day
|
||||||
|
|
||||||
|
pointer_date += timedelta(days=1)
|
||||||
|
|
||||||
|
self.hours += finish_hours.total_seconds() / 3600
|
||||||
|
self.hours += self.hour_per_day - (start_hours.total_seconds() / 3600)
|
||||||
|
|
||||||
|
def compute_assiduites(self, assiduites: Assiduite):
|
||||||
|
"""Calcule les métriques pour la collection d'assiduité donnée"""
|
||||||
|
assi: Assiduite
|
||||||
|
assiduites: list[Assiduite] = (
|
||||||
|
assiduites.all() if isinstance(assiduites, Assiduite) else assiduites
|
||||||
|
)
|
||||||
|
for assi in assiduites:
|
||||||
|
self.count += 1
|
||||||
|
delta: timedelta = assi.date_fin - assi.date_debut
|
||||||
|
|
||||||
|
if delta.days > 0:
|
||||||
|
# raise Exception(self.hours)
|
||||||
|
self.compute_long_assiduite(assi)
|
||||||
|
|
||||||
|
continue
|
||||||
|
|
||||||
|
period: tuple[datetime, datetime] = (assi.date_debut, assi.date_fin)
|
||||||
|
deb_date: date = assi.date_debut.date()
|
||||||
|
if self.check_in_morning(period):
|
||||||
|
self.add_half_day(deb_date)
|
||||||
|
if self.check_in_evening(period):
|
||||||
|
self.add_half_day(deb_date, False)
|
||||||
|
|
||||||
|
self.add_day(deb_date)
|
||||||
|
|
||||||
|
self.hours += delta.total_seconds() / 3600
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, object]:
|
||||||
|
"""Retourne les métriques sous la forme d'un dictionnaire"""
|
||||||
|
return {
|
||||||
|
"compte": self.count,
|
||||||
|
"journee": len(self.days),
|
||||||
|
"demi": len(self.half_days),
|
||||||
|
"heure": round(self.hours, 2),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_assiduites_stats(
|
||||||
|
assiduites: Assiduite, metric: str = "all", filtered: dict[str, object] = None
|
||||||
|
) -> Assiduite:
|
||||||
|
"""Compte les assiduités en fonction des filtres"""
|
||||||
|
|
||||||
|
if filtered is not None:
|
||||||
|
deb, fin = None, None
|
||||||
|
for key in filtered:
|
||||||
|
if key == "etat":
|
||||||
|
assiduites = filter_assiduites_by_etat(assiduites, filtered[key])
|
||||||
|
elif key == "date_fin":
|
||||||
|
fin = filtered[key]
|
||||||
|
elif key == "date_debut":
|
||||||
|
deb = filtered[key]
|
||||||
|
elif key == "moduleimpl_id":
|
||||||
|
assiduites = filter_by_module_impl(assiduites, filtered[key])
|
||||||
|
elif key == "formsemestre":
|
||||||
|
assiduites = filter_by_formsemestre(assiduites, filtered[key])
|
||||||
|
elif key == "est_just":
|
||||||
|
assiduites = filter_assiduites_by_est_just(assiduites, filtered[key])
|
||||||
|
elif key == "user_id":
|
||||||
|
assiduites = filter_by_user_id(assiduites, filtered[key])
|
||||||
|
if (deb, fin) != (None, None):
|
||||||
|
assiduites = filter_by_date(assiduites, Assiduite, deb, fin)
|
||||||
|
|
||||||
|
calculator: CountCalculator = CountCalculator()
|
||||||
|
calculator.compute_assiduites(assiduites)
|
||||||
|
count: dict = calculator.to_dict()
|
||||||
|
|
||||||
|
metrics: list[str] = metric.split(",")
|
||||||
|
|
||||||
|
output: dict = {}
|
||||||
|
|
||||||
|
for key, val in count.items():
|
||||||
|
if key in metrics:
|
||||||
|
output[key] = val
|
||||||
|
return output if output else count
|
||||||
|
|
||||||
|
|
||||||
|
def filter_assiduites_by_etat(assiduites: Assiduite, etat: str) -> Assiduite:
|
||||||
|
"""
|
||||||
|
Filtrage d'une collection d'assiduites en fonction de leur état
|
||||||
|
"""
|
||||||
|
etats: list[str] = list(etat.split(","))
|
||||||
|
etats = [scu.EtatAssiduite.get(e, -1) for e in etats]
|
||||||
|
return assiduites.filter(Assiduite.etat.in_(etats))
|
||||||
|
|
||||||
|
|
||||||
|
def filter_assiduites_by_est_just(
|
||||||
|
assiduites: Assiduite, est_just: bool
|
||||||
|
) -> Justificatif:
|
||||||
|
"""
|
||||||
|
Filtrage d'une collection d'assiduites en fonction de s'ils sont justifiés
|
||||||
|
"""
|
||||||
|
return assiduites.filter_by(est_just=est_just)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_by_user_id(
|
||||||
|
collection: Assiduite or Justificatif,
|
||||||
|
user_id: int,
|
||||||
|
) -> Justificatif:
|
||||||
|
"""
|
||||||
|
Filtrage d'une collection en fonction de l'user_id
|
||||||
|
"""
|
||||||
|
return collection.filter_by(user_id=user_id)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_by_date(
|
||||||
|
collection: Assiduite or Justificatif,
|
||||||
|
collection_cls: Assiduite or Justificatif,
|
||||||
|
date_deb: datetime = None,
|
||||||
|
date_fin: datetime = None,
|
||||||
|
strict: bool = False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Filtrage d'une collection d'assiduites en fonction d'une date
|
||||||
|
"""
|
||||||
|
if date_deb is None:
|
||||||
|
date_deb = datetime.min
|
||||||
|
if date_fin is None:
|
||||||
|
date_fin = datetime.max
|
||||||
|
|
||||||
|
date_deb = scu.localize_datetime(date_deb)
|
||||||
|
date_fin = scu.localize_datetime(date_fin)
|
||||||
|
if not strict:
|
||||||
|
return collection.filter(
|
||||||
|
collection_cls.date_debut <= date_fin, collection_cls.date_fin >= date_deb
|
||||||
|
)
|
||||||
|
return collection.filter(
|
||||||
|
collection_cls.date_debut < date_fin, collection_cls.date_fin > date_deb
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_justificatifs_by_etat(
|
||||||
|
justificatifs: Justificatif, etat: str
|
||||||
|
) -> Justificatif:
|
||||||
|
"""
|
||||||
|
Filtrage d'une collection de justificatifs en fonction de leur état
|
||||||
|
"""
|
||||||
|
etats: list[str] = list(etat.split(","))
|
||||||
|
etats = [scu.EtatJustificatif.get(e, -1) for e in etats]
|
||||||
|
return justificatifs.filter(Justificatif.etat.in_(etats))
|
||||||
|
|
||||||
|
|
||||||
|
def filter_by_module_impl(
|
||||||
|
assiduites: Assiduite, module_impl_id: int or None
|
||||||
|
) -> Assiduite:
|
||||||
|
"""
|
||||||
|
Filtrage d'une collection d'assiduites en fonction de l'ID du module_impl
|
||||||
|
"""
|
||||||
|
return assiduites.filter(Assiduite.moduleimpl_id == module_impl_id)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_by_formsemestre(assiduites_query: Assiduite, formsemestre: FormSemestre):
|
||||||
|
"""
|
||||||
|
Filtrage d'une collection d'assiduites en fonction d'un formsemestre
|
||||||
|
"""
|
||||||
|
|
||||||
|
if formsemestre is None:
|
||||||
|
return assiduites_query.filter(False)
|
||||||
|
|
||||||
|
assiduites_query = (
|
||||||
|
assiduites_query.join(Identite, Assiduite.etudid == Identite.id)
|
||||||
|
.join(
|
||||||
|
FormSemestreInscription,
|
||||||
|
Identite.id == FormSemestreInscription.etudid,
|
||||||
|
)
|
||||||
|
.filter(FormSemestreInscription.formsemestre_id == formsemestre.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
assiduites_query = assiduites_query.filter(
|
||||||
|
Assiduite.date_debut >= formsemestre.date_debut
|
||||||
|
)
|
||||||
|
return assiduites_query.filter(Assiduite.date_fin <= formsemestre.date_fin)
|
||||||
|
|
||||||
|
|
||||||
|
def justifies(justi: Justificatif, obj: bool = False) -> list[int]:
|
||||||
|
"""
|
||||||
|
Retourne la liste des assiduite_id qui sont justifié par la justification
|
||||||
|
Une assiduité est justifiée si elle est COMPLETEMENT ou PARTIELLEMENT comprise dans la plage du justificatif
|
||||||
|
et que l'état du justificatif est "valide"
|
||||||
|
renvoie des id si obj == False, sinon les Assiduités
|
||||||
|
"""
|
||||||
|
|
||||||
|
if justi.etat != scu.EtatJustificatif.VALIDE:
|
||||||
|
return []
|
||||||
|
|
||||||
|
assiduites_query: Assiduite = Assiduite.query.join(
|
||||||
|
Justificatif, Assiduite.etudid == Justificatif.etudid
|
||||||
|
).filter(
|
||||||
|
Assiduite.date_debut <= justi.date_fin,
|
||||||
|
Assiduite.date_fin >= justi.date_debut,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not obj:
|
||||||
|
return [assi.id for assi in assiduites_query.all()]
|
||||||
|
|
||||||
|
return assiduites_query
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_justified(
|
||||||
|
etudid: int, date_deb: datetime = None, date_fin: datetime = None
|
||||||
|
) -> list[Assiduite]:
|
||||||
|
"""Retourne toutes les assiduités justifiées sur une période"""
|
||||||
|
|
||||||
|
if date_deb is None:
|
||||||
|
date_deb = datetime.min
|
||||||
|
if date_fin is None:
|
||||||
|
date_fin = datetime.max
|
||||||
|
|
||||||
|
date_deb = scu.localize_datetime(date_deb)
|
||||||
|
date_fin = scu.localize_datetime(date_fin)
|
||||||
|
justified = Assiduite.query.filter_by(est_just=True, etudid=etudid)
|
||||||
|
after = filter_by_date(
|
||||||
|
justified,
|
||||||
|
Assiduite,
|
||||||
|
date_deb,
|
||||||
|
date_fin,
|
||||||
|
)
|
||||||
|
return after
|
@ -38,7 +38,7 @@ from flask import flash, render_template, url_for
|
|||||||
from flask_json import json_response
|
from flask_json import json_response
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
|
||||||
from app import db, email
|
from app import email
|
||||||
from app import log
|
from app import log
|
||||||
from app.scodoc.sco_utils import json_error
|
from app.scodoc.sco_utils import json_error
|
||||||
from app.but import bulletin_but
|
from app.but import bulletin_but
|
||||||
@ -354,7 +354,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
|
|||||||
"modules_capitalized"
|
"modules_capitalized"
|
||||||
] = [] # modules de l'UE capitalisée (liste vide si pas capitalisée)
|
] = [] # modules de l'UE capitalisée (liste vide si pas capitalisée)
|
||||||
if ue_status["is_capitalized"] and ue_status["formsemestre_id"] is not None:
|
if ue_status["is_capitalized"] and ue_status["formsemestre_id"] is not None:
|
||||||
sem_origin = db.session.get(FormSemestre, ue_status["formsemestre_id"])
|
sem_origin = FormSemestre.query.get(ue_status["formsemestre_id"])
|
||||||
u[
|
u[
|
||||||
"ue_descr_txt"
|
"ue_descr_txt"
|
||||||
] = f'capitalisée le {ndb.DateISOtoDMY(ue_status["event_date"])}'
|
] = f'capitalisée le {ndb.DateISOtoDMY(ue_status["event_date"])}'
|
||||||
@ -369,9 +369,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
|
|||||||
)
|
)
|
||||||
if ue_status["moy"] != "NA":
|
if ue_status["moy"] != "NA":
|
||||||
# détail des modules de l'UE capitalisée
|
# détail des modules de l'UE capitalisée
|
||||||
formsemestre_cap = db.session.get(
|
formsemestre_cap = FormSemestre.query.get(ue_status["formsemestre_id"])
|
||||||
FormSemestre, ue_status["formsemestre_id"]
|
|
||||||
)
|
|
||||||
nt_cap: NotesTableCompat = res_sem.load_formsemestre_results(
|
nt_cap: NotesTableCompat = res_sem.load_formsemestre_results(
|
||||||
formsemestre_cap
|
formsemestre_cap
|
||||||
)
|
)
|
||||||
@ -751,7 +749,7 @@ def etud_descr_situation_semestre(
|
|||||||
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
|
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
|
||||||
parcour_id = res.etuds_parcour_id[etudid]
|
parcour_id = res.etuds_parcour_id[etudid]
|
||||||
parcour: ApcParcours = (
|
parcour: ApcParcours = (
|
||||||
db.session.get(ApcParcours, parcour_id) if parcour_id is not None else None
|
ApcParcours.query.get(parcour_id) if parcour_id is not None else None
|
||||||
)
|
)
|
||||||
if parcour:
|
if parcour:
|
||||||
infos["parcours_titre"] = parcour.libelle or ""
|
infos["parcours_titre"] = parcour.libelle or ""
|
||||||
@ -930,7 +928,7 @@ def formsemestre_bulletinetud(
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
format = format or "html"
|
format = format or "html"
|
||||||
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
|
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||||
if not formsemestre:
|
if not formsemestre:
|
||||||
raise ScoValueError(f"semestre {formsemestre_id} inconnu !")
|
raise ScoValueError(f"semestre {formsemestre_id} inconnu !")
|
||||||
|
|
||||||
@ -945,7 +943,7 @@ def formsemestre_bulletinetud(
|
|||||||
)[0]
|
)[0]
|
||||||
|
|
||||||
if format not in {"html", "pdfmail"}:
|
if format not in {"html", "pdfmail"}:
|
||||||
filename = scu.bul_filename(formsemestre, etud)
|
filename = scu.bul_filename(formsemestre, etud, format)
|
||||||
mime, suffix = scu.get_mime_suffix(format)
|
mime, suffix = scu.get_mime_suffix(format)
|
||||||
return scu.send_file(bulletin, filename, mime=mime, suffix=suffix)
|
return scu.send_file(bulletin, filename, mime=mime, suffix=suffix)
|
||||||
elif format == "pdfmail":
|
elif format == "pdfmail":
|
||||||
@ -1240,7 +1238,7 @@ def make_menu_autres_operations(
|
|||||||
"enabled": current_user.has_permission(Permission.ScoImplement),
|
"enabled": current_user.has_permission(Permission.ScoImplement),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Gérer les validations d'UEs antérieures",
|
"title": "Enregistrer une validation d'UE antérieure",
|
||||||
"endpoint": "notes.formsemestre_validate_previous_ue",
|
"endpoint": "notes.formsemestre_validate_previous_ue",
|
||||||
"args": {
|
"args": {
|
||||||
"formsemestre_id": formsemestre.id,
|
"formsemestre_id": formsemestre.id,
|
||||||
|
@ -33,7 +33,7 @@ import json
|
|||||||
|
|
||||||
from flask import abort
|
from flask import abort
|
||||||
|
|
||||||
from app import db, ScoDocJSONEncoder
|
from app import ScoDocJSONEncoder
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
from app.comp.res_compat import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
from app.models import but_validations
|
from app.models import but_validations
|
||||||
@ -245,7 +245,7 @@ def formsemestre_bulletinetud_published_dict(
|
|||||||
u["module"] = []
|
u["module"] = []
|
||||||
# Structure UE/Matière/Module
|
# Structure UE/Matière/Module
|
||||||
# Recodé en 2022
|
# Recodé en 2022
|
||||||
ue = db.session.get(UniteEns, ue_id)
|
ue = UniteEns.query.get(ue_id)
|
||||||
u["matiere"] = [
|
u["matiere"] = [
|
||||||
{
|
{
|
||||||
"matiere_id": mat.id,
|
"matiere_id": mat.id,
|
||||||
|
@ -54,7 +54,7 @@ import traceback
|
|||||||
from flask import g
|
from flask import g
|
||||||
|
|
||||||
import app
|
import app
|
||||||
from app import db, log
|
from app import log
|
||||||
from app.scodoc import notesdb as ndb
|
from app.scodoc import notesdb as ndb
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
from app.scodoc.sco_exceptions import ScoException
|
from app.scodoc.sco_exceptions import ScoException
|
||||||
@ -266,7 +266,7 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
|
|||||||
# appel via API ou tests sans dept:
|
# appel via API ou tests sans dept:
|
||||||
formsemestre = None
|
formsemestre = None
|
||||||
if formsemestre_id:
|
if formsemestre_id:
|
||||||
formsemestre = db.session.get(FormSemestre, formsemestre_id)
|
formsemestre = FormSemestre.query.get(formsemestre_id)
|
||||||
if formsemestre is None:
|
if formsemestre is None:
|
||||||
raise ScoException("invalidate_formsemestre: departement must be set")
|
raise ScoException("invalidate_formsemestre: departement must be set")
|
||||||
app.set_sco_dept(formsemestre.departement.acronym, open_cnx=False)
|
app.set_sco_dept(formsemestre.departement.acronym, open_cnx=False)
|
||||||
@ -315,19 +315,6 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
|
|||||||
SemBulletinsPDFCache.invalidate_sems(formsemestre_ids)
|
SemBulletinsPDFCache.invalidate_sems(formsemestre_ids)
|
||||||
|
|
||||||
|
|
||||||
def invalidate_formsemestre_etud(etud: "Identite"):
|
|
||||||
"""Invalide tous les formsemestres auxquels l'étudiant est inscrit"""
|
|
||||||
from app.models import FormSemestre, FormSemestreInscription
|
|
||||||
|
|
||||||
inscriptions = (
|
|
||||||
FormSemestreInscription.query.filter_by(etudid=etud.id)
|
|
||||||
.join(FormSemestre)
|
|
||||||
.filter_by(dept_id=g.scodoc_dept_id)
|
|
||||||
)
|
|
||||||
for inscription in inscriptions:
|
|
||||||
invalidate_formsemestre(inscription.formsemestre_id)
|
|
||||||
|
|
||||||
|
|
||||||
class DeferredSemCacheManager:
|
class DeferredSemCacheManager:
|
||||||
"""Contexte pour effectuer des opérations indépendantes dans la
|
"""Contexte pour effectuer des opérations indépendantes dans la
|
||||||
même requete qui invalident le cache. Par exemple, quand on inscrit
|
même requete qui invalident le cache. Par exemple, quand on inscrit
|
||||||
|
@ -949,7 +949,6 @@ def do_formsemestre_validate_ue(
|
|||||||
"ue_id": ue_id,
|
"ue_id": ue_id,
|
||||||
"semestre_id": semestre_id,
|
"semestre_id": semestre_id,
|
||||||
"is_external": is_external,
|
"is_external": is_external,
|
||||||
"moy_ue": moy_ue,
|
|
||||||
}
|
}
|
||||||
if date:
|
if date:
|
||||||
args["event_date"] = date
|
args["event_date"] = date
|
||||||
@ -966,13 +965,14 @@ def do_formsemestre_validate_ue(
|
|||||||
cursor.execute("delete from scolar_formsemestre_validation where " + cond, args)
|
cursor.execute("delete from scolar_formsemestre_validation where " + cond, args)
|
||||||
# insert
|
# insert
|
||||||
args["code"] = code
|
args["code"] = code
|
||||||
if (code == ADM) and (moy_ue is None):
|
if code == ADM:
|
||||||
# stocke la moyenne d'UE capitalisée:
|
if moy_ue is None:
|
||||||
ue_status = nt.get_etud_ue_status(etudid, ue_id)
|
# stocke la moyenne d'UE capitalisée:
|
||||||
args["moy_ue"] = ue_status["moy"] if ue_status else ""
|
ue_status = nt.get_etud_ue_status(etudid, ue_id)
|
||||||
|
moy_ue = ue_status["moy"] if ue_status else ""
|
||||||
|
args["moy_ue"] = moy_ue
|
||||||
log("formsemestre_validate_ue: create %s" % args)
|
log("formsemestre_validate_ue: create %s" % args)
|
||||||
if code is not None:
|
if code != None:
|
||||||
scolar_formsemestre_validation_create(cnx, args)
|
scolar_formsemestre_validation_create(cnx, args)
|
||||||
else:
|
else:
|
||||||
log("formsemestre_validate_ue: code is None, not recording validation")
|
log("formsemestre_validate_ue: code is None, not recording validation")
|
||||||
|
@ -82,7 +82,7 @@ def html_edit_formation_apc(
|
|||||||
if None in ects:
|
if None in ects:
|
||||||
ects_by_sem[semestre_idx] = '<span class="missing_ue_ects">manquant</span>'
|
ects_by_sem[semestre_idx] = '<span class="missing_ue_ects">manquant</span>'
|
||||||
else:
|
else:
|
||||||
ects_by_sem[semestre_idx] = f"{sum(ects):g}"
|
ects_by_sem[semestre_idx] = sum(ects)
|
||||||
|
|
||||||
arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags()
|
arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags()
|
||||||
|
|
||||||
|
@ -103,7 +103,7 @@ def do_formation_delete(formation_id):
|
|||||||
"""delete a formation (and all its UE, matieres, modules)
|
"""delete a formation (and all its UE, matieres, modules)
|
||||||
Warning: delete all ues, will ask if there are validations !
|
Warning: delete all ues, will ask if there are validations !
|
||||||
"""
|
"""
|
||||||
formation: Formation = db.session.get(Formation, formation_id)
|
formation: Formation = Formation.query.get(formation_id)
|
||||||
if formation is None:
|
if formation is None:
|
||||||
return
|
return
|
||||||
acronyme = formation.acronyme
|
acronyme = formation.acronyme
|
||||||
@ -132,7 +132,6 @@ def do_formation_delete(formation_id):
|
|||||||
typ=ScolarNews.NEWS_FORM,
|
typ=ScolarNews.NEWS_FORM,
|
||||||
obj=formation_id,
|
obj=formation_id,
|
||||||
text=f"Suppression de la formation {acronyme}",
|
text=f"Suppression de la formation {acronyme}",
|
||||||
max_frequency=0,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -330,7 +329,6 @@ def do_formation_create(args: dict) -> Formation:
|
|||||||
typ=ScolarNews.NEWS_FORM,
|
typ=ScolarNews.NEWS_FORM,
|
||||||
text=f"""Création de la formation {
|
text=f"""Création de la formation {
|
||||||
formation.titre} ({formation.acronyme}) version {formation.version}""",
|
formation.titre} ({formation.acronyme}) version {formation.version}""",
|
||||||
max_frequency=0,
|
|
||||||
)
|
)
|
||||||
return formation
|
return formation
|
||||||
|
|
||||||
|
@ -30,13 +30,13 @@
|
|||||||
"""
|
"""
|
||||||
import flask
|
import flask
|
||||||
from flask import g, url_for, request
|
from flask import g, url_for, request
|
||||||
|
from app.models.events import ScolarNews
|
||||||
from app import db, log
|
from app.models.formations import Matiere
|
||||||
from app.models import Formation, Matiere, UniteEns, ScolarNews
|
|
||||||
|
|
||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
|
from app import log
|
||||||
|
from app.models import Formation
|
||||||
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
|
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
|
||||||
from app.scodoc.sco_exceptions import (
|
from app.scodoc.sco_exceptions import (
|
||||||
ScoValueError,
|
ScoValueError,
|
||||||
@ -73,7 +73,7 @@ def do_matiere_edit(*args, **kw):
|
|||||||
# edit
|
# edit
|
||||||
_matiereEditor.edit(cnx, *args, **kw)
|
_matiereEditor.edit(cnx, *args, **kw)
|
||||||
formation_id = sco_edit_ue.ue_list({"ue_id": mat["ue_id"]})[0]["formation_id"]
|
formation_id = sco_edit_ue.ue_list({"ue_id": mat["ue_id"]})[0]["formation_id"]
|
||||||
db.session.get(Formation, formation_id).invalidate_cached_sems()
|
Formation.query.get(formation_id).invalidate_cached_sems()
|
||||||
|
|
||||||
|
|
||||||
def do_matiere_create(args):
|
def do_matiere_create(args):
|
||||||
@ -88,11 +88,12 @@ def do_matiere_create(args):
|
|||||||
r = _matiereEditor.create(cnx, args)
|
r = _matiereEditor.create(cnx, args)
|
||||||
|
|
||||||
# news
|
# news
|
||||||
formation = db.session.get(Formation, ue["formation_id"])
|
formation = Formation.query.get(ue["formation_id"])
|
||||||
ScolarNews.add(
|
ScolarNews.add(
|
||||||
typ=ScolarNews.NEWS_FORM,
|
typ=ScolarNews.NEWS_FORM,
|
||||||
obj=ue["formation_id"],
|
obj=ue["formation_id"],
|
||||||
text=f"Modification de la formation {formation.acronyme}",
|
text=f"Modification de la formation {formation.acronyme}",
|
||||||
|
max_frequency=10 * 60,
|
||||||
)
|
)
|
||||||
formation.invalidate_cached_sems()
|
formation.invalidate_cached_sems()
|
||||||
return r
|
return r
|
||||||
@ -100,12 +101,13 @@ def do_matiere_create(args):
|
|||||||
|
|
||||||
def matiere_create(ue_id=None):
|
def matiere_create(ue_id=None):
|
||||||
"""Creation d'une matiere"""
|
"""Creation d'une matiere"""
|
||||||
ue: UniteEns = UniteEns.query.get_or_404(ue_id)
|
from app.scodoc import sco_edit_ue
|
||||||
default_numero = max([mat.numero for mat in ue.matieres] or [9]) + 1
|
|
||||||
|
UE = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0]
|
||||||
H = [
|
H = [
|
||||||
html_sco_header.sco_header(page_title="Création d'une matière"),
|
html_sco_header.sco_header(page_title="Création d'une matière"),
|
||||||
f"""<h2>Création d'une matière dans l'UE {ue.titre} ({ue.acronyme})</h2>
|
"""<h2>Création d'une matière dans l'UE %(titre)s (%(acronyme)s)</h2>""" % UE,
|
||||||
<p class="help">Les matières sont des groupes de modules dans une UE
|
"""<p class="help">Les matières sont des groupes de modules dans une UE
|
||||||
d'une formation donnée. Les matières servent surtout pour la
|
d'une formation donnée. Les matières servent surtout pour la
|
||||||
présentation (bulletins, etc) mais <em>n'ont pas de rôle dans le calcul
|
présentation (bulletins, etc) mais <em>n'ont pas de rôle dans le calcul
|
||||||
des notes.</em>
|
des notes.</em>
|
||||||
@ -125,21 +127,13 @@ associé.
|
|||||||
scu.get_request_args(),
|
scu.get_request_args(),
|
||||||
(
|
(
|
||||||
("ue_id", {"input_type": "hidden", "default": ue_id}),
|
("ue_id", {"input_type": "hidden", "default": ue_id}),
|
||||||
(
|
("titre", {"size": 30, "explanation": "nom de la matière."}),
|
||||||
"titre",
|
|
||||||
{
|
|
||||||
"size": 30,
|
|
||||||
"explanation": "nom de la matière.",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
"numero",
|
"numero",
|
||||||
{
|
{
|
||||||
"size": 2,
|
"size": 2,
|
||||||
"explanation": "numéro (1,2,3,4...) pour affichage",
|
"explanation": "numéro (1,2,3,4...) pour affichage",
|
||||||
"type": "int",
|
"type": "int",
|
||||||
"default": default_numero,
|
|
||||||
"allow_null": False,
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -147,7 +141,7 @@ associé.
|
|||||||
)
|
)
|
||||||
|
|
||||||
dest_url = url_for(
|
dest_url = url_for(
|
||||||
"notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=ue.formation_id
|
"notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=UE["formation_id"]
|
||||||
)
|
)
|
||||||
|
|
||||||
if tf[0] == 0:
|
if tf[0] == 0:
|
||||||
@ -200,11 +194,12 @@ def do_matiere_delete(oid):
|
|||||||
_matiereEditor.delete(cnx, oid)
|
_matiereEditor.delete(cnx, oid)
|
||||||
|
|
||||||
# news
|
# news
|
||||||
formation = db.session.get(Formation, ue["formation_id"])
|
formation = Formation.query.get(ue["formation_id"])
|
||||||
ScolarNews.add(
|
ScolarNews.add(
|
||||||
typ=ScolarNews.NEWS_FORM,
|
typ=ScolarNews.NEWS_FORM,
|
||||||
obj=ue["formation_id"],
|
obj=ue["formation_id"],
|
||||||
text=f"Modification de la formation {formation.acronyme}",
|
text=f"Modification de la formation {formation.acronyme}",
|
||||||
|
max_frequency=10 * 60,
|
||||||
)
|
)
|
||||||
formation.invalidate_cached_sems()
|
formation.invalidate_cached_sems()
|
||||||
|
|
||||||
|
@ -98,10 +98,10 @@ def module_list(*args, **kw):
|
|||||||
|
|
||||||
def do_module_create(args) -> int:
|
def do_module_create(args) -> int:
|
||||||
"Create a module. Returns id of new object."
|
"Create a module. Returns id of new object."
|
||||||
formation = db.session.get(Formation, args["formation_id"])
|
formation = Formation.query.get(args["formation_id"])
|
||||||
# refuse de créer un module APC avec semestres incohérents:
|
# refuse de créer un module APC avec semestres incohérents:
|
||||||
if formation.is_apc():
|
if formation.is_apc():
|
||||||
ue = db.session.get(UniteEns, args["ue_id"])
|
ue = UniteEns.query.get(args["ue_id"])
|
||||||
if int(args.get("semestre_id", 0)) != ue.semestre_idx:
|
if int(args.get("semestre_id", 0)) != ue.semestre_idx:
|
||||||
raise ScoValueError("Formation incompatible: contacter le support ScoDoc")
|
raise ScoValueError("Formation incompatible: contacter le support ScoDoc")
|
||||||
# create
|
# create
|
||||||
@ -114,6 +114,7 @@ def do_module_create(args) -> int:
|
|||||||
typ=ScolarNews.NEWS_FORM,
|
typ=ScolarNews.NEWS_FORM,
|
||||||
obj=formation.id,
|
obj=formation.id,
|
||||||
text=f"Modification de la formation {formation.acronyme}",
|
text=f"Modification de la formation {formation.acronyme}",
|
||||||
|
max_frequency=10 * 60,
|
||||||
)
|
)
|
||||||
formation.invalidate_cached_sems()
|
formation.invalidate_cached_sems()
|
||||||
return module_id
|
return module_id
|
||||||
@ -185,6 +186,7 @@ def do_module_delete(oid):
|
|||||||
typ=ScolarNews.NEWS_FORM,
|
typ=ScolarNews.NEWS_FORM,
|
||||||
obj=mod["formation_id"],
|
obj=mod["formation_id"],
|
||||||
text=f"Modification de la formation {formation.acronyme}",
|
text=f"Modification de la formation {formation.acronyme}",
|
||||||
|
max_frequency=10 * 60,
|
||||||
)
|
)
|
||||||
formation.invalidate_cached_sems()
|
formation.invalidate_cached_sems()
|
||||||
|
|
||||||
@ -248,7 +250,7 @@ def do_module_edit(vals: dict) -> None:
|
|||||||
# edit
|
# edit
|
||||||
cnx = ndb.GetDBConnexion()
|
cnx = ndb.GetDBConnexion()
|
||||||
_moduleEditor.edit(cnx, vals)
|
_moduleEditor.edit(cnx, vals)
|
||||||
db.session.get(Formation, mod["formation_id"]).invalidate_cached_sems()
|
Formation.query.get(mod["formation_id"]).invalidate_cached_sems()
|
||||||
|
|
||||||
|
|
||||||
def check_module_code_unicity(code, field, formation_id, module_id=None):
|
def check_module_code_unicity(code, field, formation_id, module_id=None):
|
||||||
@ -659,7 +661,6 @@ def module_edit(
|
|||||||
"explanation": "numéro (1, 2, 3, 4, ...) pour ordre d'affichage",
|
"explanation": "numéro (1, 2, 3, 4, ...) pour ordre d'affichage",
|
||||||
"type": "int",
|
"type": "int",
|
||||||
"default": default_num,
|
"default": default_num,
|
||||||
"allow_null": False,
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@ -805,7 +806,7 @@ def module_edit(
|
|||||||
if create:
|
if create:
|
||||||
if not matiere_id:
|
if not matiere_id:
|
||||||
# formulaire avec choix UE de rattachement
|
# formulaire avec choix UE de rattachement
|
||||||
ue = db.session.get(UniteEns, tf[2]["ue_id"])
|
ue = UniteEns.query.get(tf[2]["ue_id"])
|
||||||
if ue is None:
|
if ue is None:
|
||||||
raise ValueError("UE invalide")
|
raise ValueError("UE invalide")
|
||||||
matiere = ue.matieres.first()
|
matiere = ue.matieres.first()
|
||||||
@ -819,7 +820,7 @@ def module_edit(
|
|||||||
|
|
||||||
tf[2]["semestre_id"] = ue.semestre_idx
|
tf[2]["semestre_id"] = ue.semestre_idx
|
||||||
module_id = do_module_create(tf[2])
|
module_id = do_module_create(tf[2])
|
||||||
module = db.session.get(Module, module_id)
|
module = Module.query.get(module_id)
|
||||||
else: # EDITION MODULE
|
else: # EDITION MODULE
|
||||||
# l'UE de rattachement peut changer
|
# l'UE de rattachement peut changer
|
||||||
tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!")
|
tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!")
|
||||||
@ -837,7 +838,7 @@ def module_edit(
|
|||||||
)
|
)
|
||||||
# En APC, force le semestre égal à celui de l'UE
|
# En APC, force le semestre égal à celui de l'UE
|
||||||
if is_apc:
|
if is_apc:
|
||||||
selected_ue = db.session.get(UniteEns, tf[2]["ue_id"])
|
selected_ue = UniteEns.query.get(tf[2]["ue_id"])
|
||||||
if selected_ue is None:
|
if selected_ue is None:
|
||||||
raise ValueError("UE invalide")
|
raise ValueError("UE invalide")
|
||||||
tf[2]["semestre_id"] = selected_ue.semestre_idx
|
tf[2]["semestre_id"] = selected_ue.semestre_idx
|
||||||
@ -853,13 +854,13 @@ def module_edit(
|
|||||||
module.parcours = formation.referentiel_competence.parcours.all()
|
module.parcours = formation.referentiel_competence.parcours.all()
|
||||||
else:
|
else:
|
||||||
module.parcours = [
|
module.parcours = [
|
||||||
db.session.get(ApcParcours, int(parcour_id_str))
|
ApcParcours.query.get(int(parcour_id_str))
|
||||||
for parcour_id_str in tf[2]["parcours"]
|
for parcour_id_str in tf[2]["parcours"]
|
||||||
]
|
]
|
||||||
# Modifie les AC
|
# Modifie les AC
|
||||||
if "app_critiques" in tf[2]:
|
if "app_critiques" in tf[2]:
|
||||||
module.app_critiques = [
|
module.app_critiques = [
|
||||||
db.session.get(ApcAppCritique, int(ac_id_str))
|
ApcAppCritique.query.get(int(ac_id_str))
|
||||||
for ac_id_str in tf[2]["app_critiques"]
|
for ac_id_str in tf[2]["app_critiques"]
|
||||||
]
|
]
|
||||||
db.session.add(module)
|
db.session.add(module)
|
||||||
|
@ -36,7 +36,8 @@ from flask import flash, render_template, url_for
|
|||||||
from flask import g, request
|
from flask import g, request
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
|
||||||
from app import db, log
|
from app import db
|
||||||
|
from app import log
|
||||||
from app.but import apc_edit_ue
|
from app.but import apc_edit_ue
|
||||||
from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN
|
from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN
|
||||||
from app.models import (
|
from app.models import (
|
||||||
@ -136,14 +137,15 @@ def do_ue_create(args):
|
|||||||
ue_id = _ueEditor.create(cnx, args)
|
ue_id = _ueEditor.create(cnx, args)
|
||||||
log(f"do_ue_create: created {ue_id} with {args}")
|
log(f"do_ue_create: created {ue_id} with {args}")
|
||||||
|
|
||||||
formation: Formation = db.session.get(Formation, args["formation_id"])
|
formation: Formation = Formation.query.get(args["formation_id"])
|
||||||
formation.invalidate_module_coefs()
|
formation.invalidate_module_coefs()
|
||||||
# news
|
# news
|
||||||
formation = db.session.get(Formation, args["formation_id"])
|
formation = Formation.query.get(args["formation_id"])
|
||||||
ScolarNews.add(
|
ScolarNews.add(
|
||||||
typ=ScolarNews.NEWS_FORM,
|
typ=ScolarNews.NEWS_FORM,
|
||||||
obj=args["formation_id"],
|
obj=args["formation_id"],
|
||||||
text=f"Modification de la formation {formation.acronyme}",
|
text=f"Modification de la formation {formation.acronyme}",
|
||||||
|
max_frequency=10 * 60,
|
||||||
)
|
)
|
||||||
formation.invalidate_cached_sems()
|
formation.invalidate_cached_sems()
|
||||||
return ue_id
|
return ue_id
|
||||||
@ -228,6 +230,7 @@ def do_ue_delete(ue: UniteEns, delete_validations=False, force=False):
|
|||||||
typ=ScolarNews.NEWS_FORM,
|
typ=ScolarNews.NEWS_FORM,
|
||||||
obj=formation.id,
|
obj=formation.id,
|
||||||
text=f"Modification de la formation {formation.acronyme}",
|
text=f"Modification de la formation {formation.acronyme}",
|
||||||
|
max_frequency=10 * 60,
|
||||||
)
|
)
|
||||||
#
|
#
|
||||||
if not force:
|
if not force:
|
||||||
@ -283,7 +286,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
|||||||
}
|
}
|
||||||
submitlabel = "Créer cette UE"
|
submitlabel = "Créer cette UE"
|
||||||
can_change_semestre_id = True
|
can_change_semestre_id = True
|
||||||
formation = db.session.get(Formation, formation_id)
|
formation = Formation.query.get(formation_id)
|
||||||
if not formation:
|
if not formation:
|
||||||
raise ScoValueError(f"Formation inexistante ! (id={formation_id})")
|
raise ScoValueError(f"Formation inexistante ! (id={formation_id})")
|
||||||
cursus = formation.get_cursus()
|
cursus = formation.get_cursus()
|
||||||
@ -440,7 +443,6 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
|||||||
{
|
{
|
||||||
"input_type": "boolcheckbox",
|
"input_type": "boolcheckbox",
|
||||||
"title": "UE externe",
|
"title": "UE externe",
|
||||||
"readonly": not create, # ne permet pas de transformer une UE existante en externe
|
|
||||||
"explanation": "réservé pour les capitalisations d'UE effectuées à l'extérieur de l'établissement",
|
"explanation": "réservé pour les capitalisations d'UE effectuées à l'extérieur de l'établissement",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -501,7 +503,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
|||||||
else:
|
else:
|
||||||
clone_form = ""
|
clone_form = ""
|
||||||
bonus_div = """<div id="bonus_description"></div>"""
|
bonus_div = """<div id="bonus_description"></div>"""
|
||||||
ue_div = """<div id="ue_list_code" class="sco_box sco_green_bg"></div>"""
|
ue_div = """<div id="ue_list_code"></div>"""
|
||||||
return (
|
return (
|
||||||
"\n".join(H)
|
"\n".join(H)
|
||||||
+ tf[1]
|
+ tf[1]
|
||||||
@ -542,11 +544,9 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
|||||||
"semestre_id": tf[2]["semestre_idx"],
|
"semestre_id": tf[2]["semestre_idx"],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
ue = db.session.get(UniteEns, ue_id)
|
ue = UniteEns.query.get(ue_id)
|
||||||
flash(f"UE créée (code {ue.ue_code})")
|
flash(f"UE créée (code {ue.ue_code})")
|
||||||
else:
|
else:
|
||||||
if not tf[2]["numero"]:
|
|
||||||
tf[2]["numero"] = 0
|
|
||||||
do_ue_edit(tf[2])
|
do_ue_edit(tf[2])
|
||||||
flash("UE modifiée")
|
flash("UE modifiée")
|
||||||
|
|
||||||
@ -596,7 +596,7 @@ def next_ue_numero(formation_id, semestre_id=None):
|
|||||||
"""Numero d'une nouvelle UE dans cette formation.
|
"""Numero d'une nouvelle UE dans cette formation.
|
||||||
Si le semestre est specifie, cherche les UE ayant des modules de ce semestre
|
Si le semestre est specifie, cherche les UE ayant des modules de ce semestre
|
||||||
"""
|
"""
|
||||||
formation = db.session.get(Formation, formation_id)
|
formation = Formation.query.get(formation_id)
|
||||||
ues = ue_list(args={"formation_id": formation_id})
|
ues = ue_list(args={"formation_id": formation_id})
|
||||||
if not ues:
|
if not ues:
|
||||||
return 0
|
return 0
|
||||||
@ -660,7 +660,7 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
|
|||||||
"""
|
"""
|
||||||
from app.scodoc import sco_formsemestre_validation
|
from app.scodoc import sco_formsemestre_validation
|
||||||
|
|
||||||
formation: Formation = db.session.get(Formation, formation_id)
|
formation: Formation = Formation.query.get(formation_id)
|
||||||
if not formation:
|
if not formation:
|
||||||
raise ScoValueError("invalid formation_id")
|
raise ScoValueError("invalid formation_id")
|
||||||
parcours = formation.get_cursus()
|
parcours = formation.get_cursus()
|
||||||
@ -756,7 +756,7 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
|
|||||||
],
|
],
|
||||||
page_title=f"Programme {formation.acronyme} v{formation.version}",
|
page_title=f"Programme {formation.acronyme} v{formation.version}",
|
||||||
),
|
),
|
||||||
f"""<h2>{formation.html()} {lockicon}
|
f"""<h2>{formation.to_html()} {lockicon}
|
||||||
</h2>
|
</h2>
|
||||||
""",
|
""",
|
||||||
]
|
]
|
||||||
@ -1009,7 +1009,12 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
|
|||||||
<p><ul>"""
|
<p><ul>"""
|
||||||
)
|
)
|
||||||
for formsemestre in formsemestres:
|
for formsemestre in formsemestres:
|
||||||
H.append(f"""<li>{formsemestre.html_link_status()}""")
|
H.append(
|
||||||
|
f"""<li><a class="stdlink" href="{
|
||||||
|
url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept,
|
||||||
|
formsemestre_id=formsemestre.id
|
||||||
|
)}">{formsemestre.titre_mois()}</a>"""
|
||||||
|
)
|
||||||
if not formsemestre.etat:
|
if not formsemestre.etat:
|
||||||
H.append(" [verrouillé]")
|
H.append(" [verrouillé]")
|
||||||
else:
|
else:
|
||||||
@ -1376,12 +1381,13 @@ def _ue_table_modules(
|
|||||||
return "\n".join(H)
|
return "\n".join(H)
|
||||||
|
|
||||||
|
|
||||||
def ue_sharing_code(ue_code: str = "", ue_id: int = None, hide_ue_id: int = None):
|
def ue_sharing_code(ue_code=None, ue_id=None, hide_ue_id=None):
|
||||||
"""HTML list of UE sharing this code
|
"""HTML list of UE sharing this code
|
||||||
Either ue_code or ue_id may be specified.
|
Either ue_code or ue_id may be specified.
|
||||||
hide_ue_id spécifie un id à retirer de la liste.
|
hide_ue_id spécifie un id à retirer de la liste.
|
||||||
"""
|
"""
|
||||||
if ue_id is not None:
|
ue_code = str(ue_code)
|
||||||
|
if ue_id:
|
||||||
ue = UniteEns.query.get_or_404(ue_id)
|
ue = UniteEns.query.get_or_404(ue_id)
|
||||||
if not ue_code:
|
if not ue_code:
|
||||||
ue_code = ue.ue_code
|
ue_code = ue.ue_code
|
||||||
@ -1400,36 +1406,29 @@ def ue_sharing_code(ue_code: str = "", ue_id: int = None, hide_ue_id: int = None
|
|||||||
.filter_by(dept_id=g.scodoc_dept_id)
|
.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
if hide_ue_id is not None: # enlève l'ue de depart
|
if hide_ue_id: # enlève l'ue de depart
|
||||||
q_ues = q_ues.filter(UniteEns.id != hide_ue_id)
|
q_ues = q_ues.filter(UniteEns.id != hide_ue_id)
|
||||||
|
|
||||||
ues = q_ues.all()
|
ues = q_ues.all()
|
||||||
msg = " dans les formations du département "
|
|
||||||
if not ues:
|
if not ues:
|
||||||
if ue_id is not None:
|
if ue_id:
|
||||||
return f"""<span class="ue_share">Seule UE avec code {
|
return (
|
||||||
ue_code if ue_code is not None else '-'}{msg}</span>"""
|
f"""<span class="ue_share">Seule UE avec code {ue_code or '-'}</span>"""
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return f"""<span class="ue_share">Aucune UE avec code {
|
return f"""<span class="ue_share">Aucune UE avec code {ue_code or '-'}</span>"""
|
||||||
ue_code if ue_code is not None else '-'}{msg}</span>"""
|
|
||||||
H = []
|
H = []
|
||||||
if ue_id:
|
if ue_id:
|
||||||
H.append(
|
H.append(
|
||||||
f"""<span class="ue_share">Pour information, autres UEs avec le code {
|
f"""<span class="ue_share">Autres UE avec le code {ue_code or '-'}:</span>"""
|
||||||
ue_code if ue_code is not None else '-'}{msg}:</span>"""
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
H.append(
|
H.append(f"""<span class="ue_share">UE avec le code {ue_code or '-'}:</span>""")
|
||||||
f"""<span class="ue_share">UE avec le code {
|
|
||||||
ue_code if ue_code is not None else '-'}{msg}:</span>"""
|
|
||||||
)
|
|
||||||
H.append("<ul>")
|
H.append("<ul>")
|
||||||
for ue in ues:
|
for ue in ues:
|
||||||
H.append(
|
H.append(
|
||||||
f"""<li>{ue.acronyme} ({ue.titre}) dans
|
f"""<li>{ue.acronyme} ({ue.titre}) dans <a class="stdlink"
|
||||||
<a class="stdlink" href="{
|
href="{url_for("notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=ue.formation.id)}"
|
||||||
url_for("notes.ue_table",
|
|
||||||
scodoc_dept=g.scodoc_dept, formation_id=ue.formation.id)}"
|
|
||||||
>{ue.formation.acronyme} ({ue.formation.titre})</a>, version {ue.formation.version}
|
>{ue.formation.acronyme} ({ue.formation.titre})</a>, version {ue.formation.version}
|
||||||
</li>
|
</li>
|
||||||
"""
|
"""
|
||||||
@ -1461,7 +1460,7 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False):
|
|||||||
cnx = ndb.GetDBConnexion()
|
cnx = ndb.GetDBConnexion()
|
||||||
_ueEditor.edit(cnx, args)
|
_ueEditor.edit(cnx, args)
|
||||||
|
|
||||||
formation = db.session.get(Formation, ue["formation_id"])
|
formation = Formation.query.get(ue["formation_id"])
|
||||||
if not dont_invalidate_cache:
|
if not dont_invalidate_cache:
|
||||||
# Invalide les semestres utilisant cette formation
|
# Invalide les semestres utilisant cette formation
|
||||||
# ainsi que les poids et coefs
|
# ainsi que les poids et coefs
|
||||||
|
@ -62,9 +62,7 @@ def format_etud_ident(etud):
|
|||||||
else:
|
else:
|
||||||
etud["prenom_etat_civil"] = ""
|
etud["prenom_etat_civil"] = ""
|
||||||
etud["civilite_str"] = format_civilite(etud["civilite"])
|
etud["civilite_str"] = format_civilite(etud["civilite"])
|
||||||
etud["civilite_etat_civil_str"] = format_civilite(
|
etud["civilite_etat_civil_str"] = format_civilite(etud["civilite_etat_civil"])
|
||||||
etud.get("civilite_etat_civil", "X")
|
|
||||||
)
|
|
||||||
# Nom à afficher:
|
# Nom à afficher:
|
||||||
if etud["nom_usuel"]:
|
if etud["nom_usuel"]:
|
||||||
etud["nom_disp"] = etud["nom_usuel"]
|
etud["nom_disp"] = etud["nom_usuel"]
|
||||||
@ -147,7 +145,7 @@ def format_civilite(civilite):
|
|||||||
|
|
||||||
def format_etat_civil(etud: dict):
|
def format_etat_civil(etud: dict):
|
||||||
if etud["prenom_etat_civil"]:
|
if etud["prenom_etat_civil"]:
|
||||||
civ = {"M": "M.", "F": "Mme", "X": ""}[etud.get("civilite_etat_civil", "X")]
|
civ = {"M": "M.", "F": "Mme", "X": ""}[etud["civilite_etat_civil"]]
|
||||||
return f'{civ} {etud["prenom_etat_civil"]} {etud["nom"]}'
|
return f'{civ} {etud["prenom_etat_civil"]} {etud["nom"]}'
|
||||||
else:
|
else:
|
||||||
return etud["nomprenom"]
|
return etud["nomprenom"]
|
||||||
@ -262,7 +260,7 @@ def identite_list(cnx, *a, **kw):
|
|||||||
|
|
||||||
def identite_edit_nocheck(cnx, args):
|
def identite_edit_nocheck(cnx, args):
|
||||||
"""Modifie les champs mentionnes dans args, sans verification ni notification."""
|
"""Modifie les champs mentionnes dans args, sans verification ni notification."""
|
||||||
etud = db.session.get(Identite, args["etudid"])
|
etud = Identite.query.get(args["etudid"])
|
||||||
etud.from_dict(args)
|
etud.from_dict(args)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
@ -671,7 +669,6 @@ def create_etud(cnx, args: dict = None):
|
|||||||
typ=ScolarNews.NEWS_INSCR,
|
typ=ScolarNews.NEWS_INSCR,
|
||||||
text='Nouvel étudiant <a href="%(url)s">%(nomprenom)s</a>' % etud,
|
text='Nouvel étudiant <a href="%(url)s">%(nomprenom)s</a>' % etud,
|
||||||
url=etud["url"],
|
url=etud["url"],
|
||||||
max_frequency=0,
|
|
||||||
)
|
)
|
||||||
return etud
|
return etud
|
||||||
|
|
||||||
|
@ -129,7 +129,7 @@ def do_evaluation_create(
|
|||||||
)
|
)
|
||||||
args = locals()
|
args = locals()
|
||||||
log("do_evaluation_create: args=" + str(args))
|
log("do_evaluation_create: args=" + str(args))
|
||||||
modimpl: ModuleImpl = db.session.get(ModuleImpl, moduleimpl_id)
|
modimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id)
|
||||||
if modimpl is None:
|
if modimpl is None:
|
||||||
raise ValueError("module not found")
|
raise ValueError("module not found")
|
||||||
check_evaluation_args(args)
|
check_evaluation_args(args)
|
||||||
@ -252,11 +252,12 @@ def do_evaluation_delete(evaluation_id):
|
|||||||
def do_evaluation_get_all_notes(
|
def do_evaluation_get_all_notes(
|
||||||
evaluation_id, table="notes_notes", filter_suppressed=True, by_uid=None
|
evaluation_id, table="notes_notes", filter_suppressed=True, by_uid=None
|
||||||
):
|
):
|
||||||
"""Toutes les notes pour une évaluation: { etudid : { 'value' : value, 'date' : date ... }}
|
"""Toutes les notes pour une evaluation: { etudid : { 'value' : value, 'date' : date ... }}
|
||||||
Attention: inclut aussi les notes des étudiants qui ne sont plus inscrits au module.
|
Attention: inclut aussi les notes des étudiants qui ne sont plus inscrits au module.
|
||||||
"""
|
"""
|
||||||
# pas de cache pour (rares) appels via undo_notes ou specifiant un enseignant
|
do_cache = (
|
||||||
do_cache = filter_suppressed and table == "notes_notes" and (by_uid is None)
|
filter_suppressed and table == "notes_notes" and (by_uid is None)
|
||||||
|
) # pas de cache pour (rares) appels via undo_notes ou specifiant un enseignant
|
||||||
if do_cache:
|
if do_cache:
|
||||||
r = sco_cache.EvaluationCache.get(evaluation_id)
|
r = sco_cache.EvaluationCache.get(evaluation_id)
|
||||||
if r is not None:
|
if r is not None:
|
||||||
|
@ -37,8 +37,11 @@ from flask_login import current_user
|
|||||||
from flask import request
|
from flask import request
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import Evaluation, FormSemestre, ModuleImpl
|
from app import log
|
||||||
|
from app import models
|
||||||
|
from app.models.evaluations import Evaluation
|
||||||
|
from app.models.formsemestre import FormSemestre
|
||||||
|
from app.models.moduleimpls import ModuleImpl
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app.scodoc.sco_utils import ModuleType
|
from app.scodoc.sco_utils import ModuleType
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
@ -59,7 +62,7 @@ def evaluation_create_form(
|
|||||||
):
|
):
|
||||||
"Formulaire création/édition d'une évaluation (pas de ses notes)"
|
"Formulaire création/édition d'une évaluation (pas de ses notes)"
|
||||||
if evaluation_id is not None:
|
if evaluation_id is not None:
|
||||||
evaluation: Evaluation = db.session.get(Evaluation, evaluation_id)
|
evaluation: Evaluation = models.Evaluation.query.get(evaluation_id)
|
||||||
if evaluation is None:
|
if evaluation is None:
|
||||||
raise ScoValueError("Cette évaluation n'existe pas ou plus !")
|
raise ScoValueError("Cette évaluation n'existe pas ou plus !")
|
||||||
moduleimpl_id = evaluation.moduleimpl_id
|
moduleimpl_id = evaluation.moduleimpl_id
|
||||||
@ -360,7 +363,7 @@ def evaluation_create_form(
|
|||||||
evaluation_id = sco_evaluation_db.do_evaluation_create(**tf[2])
|
evaluation_id = sco_evaluation_db.do_evaluation_create(**tf[2])
|
||||||
if is_apc:
|
if is_apc:
|
||||||
# Set poids
|
# Set poids
|
||||||
evaluation = db.session.get(Evaluation, evaluation_id)
|
evaluation = models.Evaluation.query.get(evaluation_id)
|
||||||
for ue in sem_ues:
|
for ue in sem_ues:
|
||||||
evaluation.set_ue_poids(ue, tf[2][f"poids_{ue.id}"])
|
evaluation.set_ue_poids(ue, tf[2][f"poids_{ue.id}"])
|
||||||
db.session.add(evaluation)
|
db.session.add(evaluation)
|
||||||
|
@ -12,7 +12,6 @@ Sur une idée de Pascal Bouron, de Lyon.
|
|||||||
import time
|
import time
|
||||||
from flask import g, url_for
|
from flask import g, url_for
|
||||||
|
|
||||||
from app import db
|
|
||||||
from app.models import Evaluation, FormSemestre
|
from app.models import Evaluation, FormSemestre
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
from app.comp.res_compat import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
@ -114,7 +113,7 @@ def evaluations_recap_table(formsemestre: FormSemestre) -> list[dict]:
|
|||||||
rows.append(row)
|
rows.append(row)
|
||||||
line_idx += 1
|
line_idx += 1
|
||||||
for evaluation_id in modimpl_results.evals_notes:
|
for evaluation_id in modimpl_results.evals_notes:
|
||||||
e = db.session.get(Evaluation, evaluation_id)
|
e = Evaluation.query.get(evaluation_id)
|
||||||
eval_etat = modimpl_results.evaluations_etat[evaluation_id]
|
eval_etat = modimpl_results.evaluations_etat[evaluation_id]
|
||||||
row = {
|
row = {
|
||||||
"type": "",
|
"type": "",
|
||||||
|
@ -433,7 +433,7 @@ def excel_simple_table(
|
|||||||
return ws.generate()
|
return ws.generate()
|
||||||
|
|
||||||
|
|
||||||
def excel_feuille_saisie(evaluation: "Evaluation", titreannee, description, lines):
|
def excel_feuille_saisie(e, titreannee, description, lines):
|
||||||
"""Genere feuille excel pour saisie des notes.
|
"""Genere feuille excel pour saisie des notes.
|
||||||
E: evaluation (dict)
|
E: evaluation (dict)
|
||||||
lines: liste de tuples
|
lines: liste de tuples
|
||||||
@ -512,20 +512,18 @@ def excel_feuille_saisie(evaluation: "Evaluation", titreannee, description, line
|
|||||||
# description evaluation
|
# description evaluation
|
||||||
ws.append_single_cell_row(scu.unescape_html(description), style_titres)
|
ws.append_single_cell_row(scu.unescape_html(description), style_titres)
|
||||||
ws.append_single_cell_row(
|
ws.append_single_cell_row(
|
||||||
"Evaluation du %s (coef. %g)"
|
"Evaluation du %s (coef. %g)" % (e["jour"], e["coefficient"]), style
|
||||||
% (evaluation.jour or "sans date", evaluation.coefficient or 0.0),
|
|
||||||
style,
|
|
||||||
)
|
)
|
||||||
# ligne blanche
|
# ligne blanche
|
||||||
ws.append_blank_row()
|
ws.append_blank_row()
|
||||||
# code et titres colonnes
|
# code et titres colonnes
|
||||||
ws.append_row(
|
ws.append_row(
|
||||||
[
|
[
|
||||||
ws.make_cell("!%s" % evaluation.id, style_ro),
|
ws.make_cell("!%s" % e["evaluation_id"], style_ro),
|
||||||
ws.make_cell("Nom", style_titres),
|
ws.make_cell("Nom", style_titres),
|
||||||
ws.make_cell("Prénom", style_titres),
|
ws.make_cell("Prénom", style_titres),
|
||||||
ws.make_cell("Groupe", style_titres),
|
ws.make_cell("Groupe", style_titres),
|
||||||
ws.make_cell("Note sur %g" % (evaluation.note_max or 0.0), style_titres),
|
ws.make_cell("Note sur %g" % e["note_max"], style_titres),
|
||||||
ws.make_cell("Remarque", style_titres),
|
ws.make_cell("Remarque", style_titres),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
@ -28,15 +28,15 @@
|
|||||||
"""Table recap formation (avec champs éditables)
|
"""Table recap formation (avec champs éditables)
|
||||||
"""
|
"""
|
||||||
import io
|
import io
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile, BadZipfile
|
||||||
|
|
||||||
from flask import Response
|
from flask import Response
|
||||||
from flask import send_file, url_for
|
from flask import send_file, url_for
|
||||||
from flask import g, request
|
from flask import g, request
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
|
||||||
from app import db
|
from app.models import Formation, FormSemestre, UniteEns, Module
|
||||||
from app.models import Formation, FormSemestre, Matiere, Module, UniteEns
|
from app.models.formations import Matiere
|
||||||
|
|
||||||
from app.scodoc.gen_tables import GenTable
|
from app.scodoc.gen_tables import GenTable
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
@ -178,7 +178,7 @@ def export_recap_formations_annee_scolaire(annee_scolaire):
|
|||||||
)
|
)
|
||||||
formation_ids = {formsemestre.formation.id for formsemestre in formsemestres}
|
formation_ids = {formsemestre.formation.id for formsemestre in formsemestres}
|
||||||
for formation_id in formation_ids:
|
for formation_id in formation_ids:
|
||||||
formation = db.session.get(Formation, formation_id)
|
formation = Formation.query.get(formation_id)
|
||||||
xls = formation_table_recap(formation_id, format="xlsx").data
|
xls = formation_table_recap(formation_id, format="xlsx").data
|
||||||
filename = (
|
filename = (
|
||||||
scu.sanitize_filename(formation.get_titre_version()) + scu.XLSX_SUFFIX
|
scu.sanitize_filename(formation.get_titre_version()) + scu.XLSX_SUFFIX
|
||||||
|
@ -200,31 +200,31 @@ def do_formsemestres_associate_new_version(
|
|||||||
|
|
||||||
# New formation:
|
# New formation:
|
||||||
(
|
(
|
||||||
new_formation_id,
|
formation_id,
|
||||||
modules_old2new,
|
modules_old2new,
|
||||||
ues_old2new,
|
ues_old2new,
|
||||||
) = sco_formations.formation_create_new_version(formation_id, redirect=False)
|
) = sco_formations.formation_create_new_version(formation_id, redirect=False)
|
||||||
# Log new ues:
|
# Log new ues:
|
||||||
for ue_id in ues_old2new:
|
for ue_id in ues_old2new:
|
||||||
ue = db.session.get(UniteEns, ue_id)
|
ue = UniteEns.query.get(ue_id)
|
||||||
new_ue = db.session.get(UniteEns, ues_old2new[ue_id])
|
new_ue = UniteEns.query.get(ues_old2new[ue_id])
|
||||||
assert ue.semestre_idx == new_ue.semestre_idx
|
assert ue.semestre_idx == new_ue.semestre_idx
|
||||||
log(f"{ue} -> {new_ue}")
|
log(f"{ue} -> {new_ue}")
|
||||||
# Log new modules
|
# Log new modules
|
||||||
for module_id in modules_old2new:
|
for module_id in modules_old2new:
|
||||||
mod = db.session.get(Module, module_id)
|
mod = Module.query.get(module_id)
|
||||||
new_mod = db.session.get(Module, modules_old2new[module_id])
|
new_mod = Module.query.get(modules_old2new[module_id])
|
||||||
assert mod.semestre_id == new_mod.semestre_id
|
assert mod.semestre_id == new_mod.semestre_id
|
||||||
log(f"{mod} -> {new_mod}")
|
log(f"{mod} -> {new_mod}")
|
||||||
# re-associate
|
# re-associate
|
||||||
for formsemestre_id in formsemestre_ids:
|
for formsemestre_id in formsemestre_ids:
|
||||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||||
formsemestre.formation_id = new_formation_id
|
formsemestre.formation_id = formation_id
|
||||||
db.session.add(formsemestre)
|
db.session.add(formsemestre)
|
||||||
_reassociate_moduleimpls(formsemestre, ues_old2new, modules_old2new)
|
_reassociate_moduleimpls(formsemestre, ues_old2new, modules_old2new)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return new_formation_id
|
return formation_id
|
||||||
|
|
||||||
|
|
||||||
def _reassociate_moduleimpls(
|
def _reassociate_moduleimpls(
|
||||||
@ -246,12 +246,8 @@ def _reassociate_moduleimpls(
|
|||||||
Evaluation.moduleimpl_id == ModuleImpl.id,
|
Evaluation.moduleimpl_id == ModuleImpl.id,
|
||||||
ModuleImpl.formsemestre_id == formsemestre.id,
|
ModuleImpl.formsemestre_id == formsemestre.id,
|
||||||
):
|
):
|
||||||
if poids.ue_id in ues_old2new:
|
poids.ue_id = ues_old2new[poids.ue_id]
|
||||||
poids.ue_id = ues_old2new[poids.ue_id]
|
db.session.add(poids)
|
||||||
db.session.add(poids)
|
|
||||||
else:
|
|
||||||
# poids vers une UE qui n'est pas ou plus dans notre formation
|
|
||||||
db.session.delete(poids)
|
|
||||||
|
|
||||||
# update decisions:
|
# update decisions:
|
||||||
for event in ScolarEvent.query.filter_by(formsemestre_id=formsemestre.id):
|
for event in ScolarEvent.query.filter_by(formsemestre_id=formsemestre.id):
|
||||||
@ -262,9 +258,8 @@ def _reassociate_moduleimpls(
|
|||||||
for validation in ScolarFormSemestreValidation.query.filter_by(
|
for validation in ScolarFormSemestreValidation.query.filter_by(
|
||||||
formsemestre_id=formsemestre.id
|
formsemestre_id=formsemestre.id
|
||||||
):
|
):
|
||||||
if (validation.ue_id is not None) and validation.ue_id in ues_old2new:
|
if validation.ue_id is not None:
|
||||||
validation.ue_id = ues_old2new[validation.ue_id]
|
validation.ue_id = ues_old2new[validation.ue_id]
|
||||||
# si l'UE n'est pas ou plus dans notre formation, laisse.
|
|
||||||
db.session.add(validation)
|
db.session.add(validation)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -163,7 +163,7 @@ def formation_export_dict(
|
|||||||
if tags:
|
if tags:
|
||||||
mod["tags"] = [{"name": x} for x in tags]
|
mod["tags"] = [{"name": x} for x in tags]
|
||||||
#
|
#
|
||||||
module: Module = db.session.get(Module, module_id)
|
module: Module = Module.query.get(module_id)
|
||||||
if module.is_apc():
|
if module.is_apc():
|
||||||
# Exporte les coefficients
|
# Exporte les coefficients
|
||||||
if ue_reference_style == "id":
|
if ue_reference_style == "id":
|
||||||
@ -359,7 +359,7 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
|
|||||||
referentiel_competence_id, ue_info[1]
|
referentiel_competence_id, ue_info[1]
|
||||||
)
|
)
|
||||||
ue_id = sco_edit_ue.do_ue_create(ue_info[1])
|
ue_id = sco_edit_ue.do_ue_create(ue_info[1])
|
||||||
ue: UniteEns = db.session.get(UniteEns, ue_id)
|
ue: UniteEns = UniteEns.query.get(ue_id)
|
||||||
assert ue
|
assert ue
|
||||||
if xml_ue_id:
|
if xml_ue_id:
|
||||||
ues_old2new[xml_ue_id] = ue_id
|
ues_old2new[xml_ue_id] = ue_id
|
||||||
@ -424,7 +424,7 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
|
|||||||
if xml_module_id:
|
if xml_module_id:
|
||||||
modules_old2new[int(xml_module_id)] = mod_id
|
modules_old2new[int(xml_module_id)] = mod_id
|
||||||
if len(mod_info) > 2:
|
if len(mod_info) > 2:
|
||||||
module: Module = db.session.get(Module, mod_id)
|
module: Module = Module.query.get(mod_id)
|
||||||
tag_names = []
|
tag_names = []
|
||||||
ue_coef_dict = {}
|
ue_coef_dict = {}
|
||||||
for child in mod_info[2]:
|
for child in mod_info[2]:
|
||||||
@ -626,9 +626,7 @@ def formation_list_table() -> GenTable:
|
|||||||
def formation_create_new_version(formation_id, redirect=True):
|
def formation_create_new_version(formation_id, redirect=True):
|
||||||
"duplicate formation, with new version number"
|
"duplicate formation, with new version number"
|
||||||
formation = Formation.query.get_or_404(formation_id)
|
formation = Formation.query.get_or_404(formation_id)
|
||||||
resp = formation_export(
|
resp = formation_export(formation_id, export_ids=True, format="xml")
|
||||||
formation_id, export_ids=True, export_external_ues=True, format="xml"
|
|
||||||
)
|
|
||||||
xml_data = resp.get_data(as_text=True)
|
xml_data = resp.get_data(as_text=True)
|
||||||
new_id, modules_old2new, ues_old2new = formation_import_xml(
|
new_id, modules_old2new, ues_old2new = formation_import_xml(
|
||||||
xml_data, use_local_refcomp=True
|
xml_data, use_local_refcomp=True
|
||||||
@ -638,7 +636,6 @@ def formation_create_new_version(formation_id, redirect=True):
|
|||||||
typ=ScolarNews.NEWS_FORM,
|
typ=ScolarNews.NEWS_FORM,
|
||||||
obj=new_id,
|
obj=new_id,
|
||||||
text=f"Nouvelle version de la formation {formation.acronyme}",
|
text=f"Nouvelle version de la formation {formation.acronyme}",
|
||||||
max_frequency=0,
|
|
||||||
)
|
)
|
||||||
if redirect:
|
if redirect:
|
||||||
flash("Nouvelle version !")
|
flash("Nouvelle version !")
|
||||||
|
@ -261,7 +261,6 @@ def do_formsemestre_create(args, silent=False):
|
|||||||
typ=ScolarNews.NEWS_SEM,
|
typ=ScolarNews.NEWS_SEM,
|
||||||
text='Création du semestre <a href="%(url)s">%(titre)s</a>' % args,
|
text='Création du semestre <a href="%(url)s">%(titre)s</a>' % args,
|
||||||
url=args["url"],
|
url=args["url"],
|
||||||
max_frequency=0,
|
|
||||||
)
|
)
|
||||||
return formsemestre_id
|
return formsemestre_id
|
||||||
|
|
||||||
|
@ -793,16 +793,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
|
|||||||
{tf[1]}
|
{tf[1]}
|
||||||
"""
|
"""
|
||||||
elif tf[0] == -1:
|
elif tf[0] == -1:
|
||||||
if formsemestre:
|
return "<h4>annulation</h4>"
|
||||||
return redirect(
|
|
||||||
url_for(
|
|
||||||
"notes.formsemestre_status",
|
|
||||||
scodoc_dept=g.scodoc_dept,
|
|
||||||
formsemestre_id=formsemestre.id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return redirect(url_for("notes.index_html", scodoc_dept=g.scodoc_dept))
|
|
||||||
else:
|
else:
|
||||||
if tf[2]["gestion_compensation_lst"]:
|
if tf[2]["gestion_compensation_lst"]:
|
||||||
tf[2]["gestion_compensation"] = True
|
tf[2]["gestion_compensation"] = True
|
||||||
@ -950,7 +941,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
|
|||||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||||
if "parcours" in tf[2]:
|
if "parcours" in tf[2]:
|
||||||
formsemestre.parcours = [
|
formsemestre.parcours = [
|
||||||
db.session.get(ApcParcours, int(parcour_id_str))
|
ApcParcours.query.get(int(parcour_id_str))
|
||||||
for parcour_id_str in tf[2]["parcours"]
|
for parcour_id_str in tf[2]["parcours"]
|
||||||
]
|
]
|
||||||
db.session.add(formsemestre)
|
db.session.add(formsemestre)
|
||||||
@ -1044,7 +1035,7 @@ def formsemestre_delete_moduleimpls(formsemestre_id, module_ids_to_del):
|
|||||||
ok = True
|
ok = True
|
||||||
msg = []
|
msg = []
|
||||||
for module_id in module_ids_to_del:
|
for module_id in module_ids_to_del:
|
||||||
module = db.session.get(Module, module_id)
|
module = Module.query.get(module_id)
|
||||||
if module is None:
|
if module is None:
|
||||||
continue # ignore invalid ids
|
continue # ignore invalid ids
|
||||||
modimpls = ModuleImpl.query.filter_by(
|
modimpls = ModuleImpl.query.filter_by(
|
||||||
@ -1224,7 +1215,7 @@ def do_formsemestre_clone(
|
|||||||
args["etat"] = 1 # non verrouillé
|
args["etat"] = 1 # non verrouillé
|
||||||
formsemestre_id = sco_formsemestre.do_formsemestre_create(args)
|
formsemestre_id = sco_formsemestre.do_formsemestre_create(args)
|
||||||
log(f"created formsemestre {formsemestre_id}")
|
log(f"created formsemestre {formsemestre_id}")
|
||||||
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
|
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||||
# 2- create moduleimpls
|
# 2- create moduleimpls
|
||||||
mods_orig = sco_moduleimpl.moduleimpl_list(formsemestre_id=orig_formsemestre_id)
|
mods_orig = sco_moduleimpl.moduleimpl_list(formsemestre_id=orig_formsemestre_id)
|
||||||
for mod_orig in mods_orig:
|
for mod_orig in mods_orig:
|
||||||
@ -1342,18 +1333,11 @@ Ceci n'est possible que si :
|
|||||||
cancelbutton="Annuler",
|
cancelbutton="Annuler",
|
||||||
)
|
)
|
||||||
if tf[0] == 0:
|
if tf[0] == 0:
|
||||||
has_decisions, message = formsemestre_has_decisions_or_compensations(
|
if formsemestre_has_decisions_or_compensations(formsemestre):
|
||||||
formsemestre
|
|
||||||
)
|
|
||||||
if has_decisions:
|
|
||||||
H.append(
|
H.append(
|
||||||
f"""<p><b>Ce semestre ne peut pas être supprimé !</b></p>
|
"""<p><b>Ce semestre ne peut pas être supprimé !
|
||||||
<p>il y a des décisions de jury ou des compensations par d'autres semestres:
|
(il y a des décisions de jury ou des compensations par d'autres semestres)</b>
|
||||||
</p>
|
</p>"""
|
||||||
<ul>
|
|
||||||
<li>{message}</li>
|
|
||||||
</ul>
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
H.append(tf[1])
|
H.append(tf[1])
|
||||||
@ -1388,46 +1372,32 @@ def formsemestre_delete2(formsemestre_id, dialog_confirmed=False):
|
|||||||
return flask.redirect(scu.ScoURL())
|
return flask.redirect(scu.ScoURL())
|
||||||
|
|
||||||
|
|
||||||
def formsemestre_has_decisions_or_compensations(
|
def formsemestre_has_decisions_or_compensations(formsemestre: FormSemestre):
|
||||||
formsemestre: FormSemestre,
|
|
||||||
) -> tuple[bool, str]:
|
|
||||||
"""True if decision de jury (sem. UE, RCUE, année) émanant de ce semestre
|
"""True if decision de jury (sem. UE, RCUE, année) émanant de ce semestre
|
||||||
ou compensation de ce semestre par d'autres semestres
|
ou compensation de ce semestre par d'autres semestres
|
||||||
ou autorisations de passage.
|
ou autorisations de passage.
|
||||||
"""
|
"""
|
||||||
# Validations de semestre ou d'UEs
|
# Validations de semestre ou d'UEs
|
||||||
nb_validations = ScolarFormSemestreValidation.query.filter_by(
|
if ScolarFormSemestreValidation.query.filter_by(
|
||||||
formsemestre_id=formsemestre.id
|
formsemestre_id=formsemestre.id
|
||||||
).count()
|
).count():
|
||||||
if nb_validations:
|
return True
|
||||||
return True, f"{nb_validations} validations de semestre ou d'UE"
|
if ScolarFormSemestreValidation.query.filter_by(
|
||||||
nb_validations = ScolarFormSemestreValidation.query.filter_by(
|
|
||||||
compense_formsemestre_id=formsemestre.id
|
compense_formsemestre_id=formsemestre.id
|
||||||
).count()
|
).count():
|
||||||
if nb_validations:
|
return True
|
||||||
return True, f"{nb_validations} compensations utilisées dans d'autres semestres"
|
|
||||||
# Autorisations d'inscription:
|
# Autorisations d'inscription:
|
||||||
nb_validations = ScolarAutorisationInscription.query.filter_by(
|
if ScolarAutorisationInscription.query.filter_by(
|
||||||
origin_formsemestre_id=formsemestre.id
|
origin_formsemestre_id=formsemestre.id
|
||||||
).count()
|
).count():
|
||||||
if nb_validations:
|
return True
|
||||||
return (
|
|
||||||
True,
|
|
||||||
f"{nb_validations} autorisations d'inscriptions émanant de ce semestre",
|
|
||||||
)
|
|
||||||
# Validations d'années BUT
|
# Validations d'années BUT
|
||||||
nb_validations = ApcValidationAnnee.query.filter_by(
|
if ApcValidationAnnee.query.filter_by(formsemestre_id=formsemestre.id).count():
|
||||||
formsemestre_id=formsemestre.id
|
return True
|
||||||
).count()
|
|
||||||
if nb_validations:
|
|
||||||
return True, f"{nb_validations} validations d'année BUT utilisant ce semestre"
|
|
||||||
# Validations de RCUEs
|
# Validations de RCUEs
|
||||||
nb_validations = ApcValidationRCUE.query.filter_by(
|
if ApcValidationRCUE.query.filter_by(formsemestre_id=formsemestre.id).count():
|
||||||
formsemestre_id=formsemestre.id
|
return True
|
||||||
).count()
|
return False
|
||||||
if nb_validations:
|
|
||||||
return True, f"{nb_validations} validations de RCUE utilisant ce semestre"
|
|
||||||
return False, ""
|
|
||||||
|
|
||||||
|
|
||||||
def do_formsemestre_delete(formsemestre_id):
|
def do_formsemestre_delete(formsemestre_id):
|
||||||
@ -1530,7 +1500,6 @@ def do_formsemestre_delete(formsemestre_id):
|
|||||||
typ=ScolarNews.NEWS_SEM,
|
typ=ScolarNews.NEWS_SEM,
|
||||||
obj=formsemestre_id,
|
obj=formsemestre_id,
|
||||||
text="Suppression du semestre %(titre)s" % sem,
|
text="Suppression du semestre %(titre)s" % sem,
|
||||||
max_frequency=0,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -517,7 +517,7 @@ def _record_ue_validations_and_coefs(
|
|||||||
)
|
)
|
||||||
assert code is None or (note) # si code validant, il faut une note
|
assert code is None or (note) # si code validant, il faut une note
|
||||||
sco_formsemestre_validation.do_formsemestre_validate_previous_ue(
|
sco_formsemestre_validation.do_formsemestre_validate_previous_ue(
|
||||||
formsemestre,
|
formsemestre.id,
|
||||||
etud.id,
|
etud.id,
|
||||||
ue.id,
|
ue.id,
|
||||||
note,
|
note,
|
||||||
|
@ -175,7 +175,9 @@ def do_formsemestre_demission(
|
|||||||
)
|
)
|
||||||
db.session.add(event)
|
db.session.add(event)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id)
|
sco_cache.invalidate_formsemestre(
|
||||||
|
formsemestre_id=formsemestre_id
|
||||||
|
) # > démission ou défaillance
|
||||||
if etat_new == scu.DEMISSION:
|
if etat_new == scu.DEMISSION:
|
||||||
flash("Démission enregistrée")
|
flash("Démission enregistrée")
|
||||||
elif etat_new == scu.DEF:
|
elif etat_new == scu.DEF:
|
||||||
@ -208,7 +210,7 @@ def do_formsemestre_desinscription(etudid, formsemestre_id):
|
|||||||
|
|
||||||
if nt.etud_has_decision(etudid):
|
if nt.etud_has_decision(etudid):
|
||||||
raise ScoValueError(
|
raise ScoValueError(
|
||||||
f"""désinscription impossible: l'étudiant {etud.nomprenom} a
|
"""désinscription impossible: l'étudiant {etud.nomprenom} a
|
||||||
une décision de jury (la supprimer avant si nécessaire)"""
|
une décision de jury (la supprimer avant si nécessaire)"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
62
app/scodoc/sco_formsemestre_status.py
Normal file → Executable file
62
app/scodoc/sco_formsemestre_status.py
Normal file → Executable file
@ -36,20 +36,14 @@ from flask import request
|
|||||||
from flask import flash, redirect, render_template, url_for
|
from flask import flash, redirect, render_template, url_for
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
|
||||||
from app import db, log
|
from app import log
|
||||||
from app.but.cursus_but import formsemestre_warning_apc_setup
|
from app.but.cursus_but import formsemestre_warning_apc_setup
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
from app.comp.res_common import ResultatsSemestre
|
from app.comp.res_common import ResultatsSemestre
|
||||||
from app.comp.res_compat import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
from app.models import (
|
from app.models import Evaluation, Formation, Module, ModuleImpl, NotesNotes
|
||||||
Evaluation,
|
from app.models.etudiants import Identite
|
||||||
Formation,
|
from app.models.formsemestre import FormSemestre
|
||||||
FormSemestre,
|
|
||||||
Identite,
|
|
||||||
Module,
|
|
||||||
ModuleImpl,
|
|
||||||
NotesNotes,
|
|
||||||
)
|
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app.scodoc.sco_utils import ModuleType
|
from app.scodoc.sco_utils import ModuleType
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
@ -260,7 +254,7 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
# debug :
|
# debug :
|
||||||
if current_app.config["DEBUG"]:
|
if current_app.config["ENV"] == "development":
|
||||||
menu_semestre.append(
|
menu_semestre.append(
|
||||||
{
|
{
|
||||||
"title": "Vérifier l'intégrité",
|
"title": "Vérifier l'intégrité",
|
||||||
@ -600,7 +594,6 @@ def formsemestre_description_table(
|
|||||||
formsemestre: FormSemestre = FormSemestre.query.filter_by(
|
formsemestre: FormSemestre = FormSemestre.query.filter_by(
|
||||||
id=formsemestre_id, dept_id=g.scodoc_dept_id
|
id=formsemestre_id, dept_id=g.scodoc_dept_id
|
||||||
).first_or_404()
|
).first_or_404()
|
||||||
is_apc = formsemestre.formation.is_apc()
|
|
||||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id)
|
use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id)
|
||||||
parcours = codes_cursus.get_cursus_from_code(formsemestre.formation.type_parcours)
|
parcours = codes_cursus.get_cursus_from_code(formsemestre.formation.type_parcours)
|
||||||
@ -614,7 +607,7 @@ def formsemestre_description_table(
|
|||||||
else:
|
else:
|
||||||
ues = formsemestre.get_ues()
|
ues = formsemestre.get_ues()
|
||||||
columns_ids += [f"ue_{ue.id}" for ue in ues]
|
columns_ids += [f"ue_{ue.id}" for ue in ues]
|
||||||
if sco_preferences.get_preference("bul_show_ects", formsemestre_id) and not is_apc:
|
if sco_preferences.get_preference("bul_show_ects", formsemestre_id):
|
||||||
columns_ids += ["ects"]
|
columns_ids += ["ects"]
|
||||||
columns_ids += ["Inscrits", "Responsable", "Enseignants"]
|
columns_ids += ["Inscrits", "Responsable", "Enseignants"]
|
||||||
if with_evals:
|
if with_evals:
|
||||||
@ -641,7 +634,6 @@ def formsemestre_description_table(
|
|||||||
sum_coef = 0
|
sum_coef = 0
|
||||||
sum_ects = 0
|
sum_ects = 0
|
||||||
last_ue_id = None
|
last_ue_id = None
|
||||||
formsemestre_parcours_ids = {p.id for p in formsemestre.parcours}
|
|
||||||
for modimpl in formsemestre.modimpls_sorted:
|
for modimpl in formsemestre.modimpls_sorted:
|
||||||
# Ligne UE avec ECTS:
|
# Ligne UE avec ECTS:
|
||||||
ue = modimpl.module.ue
|
ue = modimpl.module.ue
|
||||||
@ -668,7 +660,7 @@ def formsemestre_description_table(
|
|||||||
ue_info[
|
ue_info[
|
||||||
f"_{k}_td_attrs"
|
f"_{k}_td_attrs"
|
||||||
] = f'style="background-color: {ue.color} !important;"'
|
] = f'style="background-color: {ue.color} !important;"'
|
||||||
if not is_apc:
|
if not formsemestre.formation.is_apc():
|
||||||
# n'affiche la ligne UE qu'en formation classique
|
# n'affiche la ligne UE qu'en formation classique
|
||||||
# car l'UE de rattachement n'a pas d'intérêt en BUT
|
# car l'UE de rattachement n'a pas d'intérêt en BUT
|
||||||
rows.append(ue_info)
|
rows.append(ue_info)
|
||||||
@ -709,17 +701,8 @@ def formsemestre_description_table(
|
|||||||
for ue in ues:
|
for ue in ues:
|
||||||
row[f"ue_{ue.id}"] = coef_dict.get(ue.id, 0.0) or ""
|
row[f"ue_{ue.id}"] = coef_dict.get(ue.id, 0.0) or ""
|
||||||
if with_parcours:
|
if with_parcours:
|
||||||
# Intersection des parcours du module avec ceux du formsemestre
|
|
||||||
row["parcours"] = ", ".join(
|
row["parcours"] = ", ".join(
|
||||||
[
|
sorted([pa.code for pa in modimpl.module.parcours])
|
||||||
pa.code
|
|
||||||
for pa in (
|
|
||||||
modimpl.module.parcours
|
|
||||||
if modimpl.module.parcours
|
|
||||||
else modimpl.formsemestre.parcours
|
|
||||||
)
|
|
||||||
if pa.id in formsemestre_parcours_ids
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
@ -759,7 +742,7 @@ def formsemestre_description_table(
|
|||||||
e["publish_incomplete_str"] = "Non"
|
e["publish_incomplete_str"] = "Non"
|
||||||
e["_publish_incomplete_str_td_attrs"] = 'style="color: red;"'
|
e["_publish_incomplete_str_td_attrs"] = 'style="color: red;"'
|
||||||
# Poids vers UEs (en APC)
|
# Poids vers UEs (en APC)
|
||||||
evaluation: Evaluation = db.session.get(Evaluation, e["evaluation_id"])
|
evaluation: Evaluation = Evaluation.query.get(e["evaluation_id"])
|
||||||
for ue_id, poids in evaluation.get_ue_poids_dict().items():
|
for ue_id, poids in evaluation.get_ue_poids_dict().items():
|
||||||
e[f"ue_{ue_id}"] = poids or ""
|
e[f"ue_{ue_id}"] = poids or ""
|
||||||
e[f"_ue_{ue_id}_class"] = "poids"
|
e[f"_ue_{ue_id}_class"] = "poids"
|
||||||
@ -846,9 +829,9 @@ def _make_listes_sem(formsemestre: FormSemestre, with_absences=True):
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<form action="{url_for(
|
<form action="{url_for(
|
||||||
"absences.SignaleAbsenceGrSemestre", scodoc_dept=g.scodoc_dept
|
"assiduites.signal_assiduites_group", scodoc_dept=g.scodoc_dept
|
||||||
)}" method="get">
|
)}" method="get">
|
||||||
<input type="hidden" name="datefin" value="{
|
<input type="hidden" name="date" value="{
|
||||||
formsemestre.date_fin.strftime("%d/%m/%Y")}"/>
|
formsemestre.date_fin.strftime("%d/%m/%Y")}"/>
|
||||||
<input type="hidden" name="group_ids" value="%(group_id)s"/>
|
<input type="hidden" name="group_ids" value="%(group_id)s"/>
|
||||||
<input type="hidden" name="destination" value="{destination}"/>
|
<input type="hidden" name="destination" value="{destination}"/>
|
||||||
@ -865,8 +848,8 @@ def _make_listes_sem(formsemestre: FormSemestre, with_absences=True):
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
<a href="{
|
<a href="{
|
||||||
url_for("absences.choix_semaine", scodoc_dept=g.scodoc_dept)
|
url_for("assiduites.signal_assiduites_group", scodoc_dept=g.scodoc_dept)
|
||||||
}?group_id=%(group_id)s">saisie par semaine</a>
|
}?group_ids=%(group_id)s&jour={datetime.date.today().isoformat()}&formsemestre_id={formsemestre.id}">saisie par semaine</a>
|
||||||
</form></td>
|
</form></td>
|
||||||
"""
|
"""
|
||||||
else:
|
else:
|
||||||
@ -881,15 +864,11 @@ def _make_listes_sem(formsemestre: FormSemestre, with_absences=True):
|
|||||||
H.append("<h4>Tous les étudiants</h4>")
|
H.append("<h4>Tous les étudiants</h4>")
|
||||||
else:
|
else:
|
||||||
H.append("<h4>Groupes de %(partition_name)s</h4>" % partition)
|
H.append("<h4>Groupes de %(partition_name)s</h4>" % partition)
|
||||||
partition_is_empty = True
|
|
||||||
groups = sco_groups.get_partition_groups(partition)
|
groups = sco_groups.get_partition_groups(partition)
|
||||||
if groups:
|
if groups:
|
||||||
H.append("<table>")
|
H.append("<table>")
|
||||||
for group in groups:
|
for group in groups:
|
||||||
n_members = len(sco_groups.get_group_members(group["group_id"]))
|
n_members = len(sco_groups.get_group_members(group["group_id"]))
|
||||||
if n_members == 0:
|
|
||||||
continue # skip empty groups
|
|
||||||
partition_is_empty = False
|
|
||||||
group["url_etat"] = url_for(
|
group["url_etat"] = url_for(
|
||||||
"absences.EtatAbsencesGr",
|
"absences.EtatAbsencesGr",
|
||||||
group_ids=group["group_id"],
|
group_ids=group["group_id"],
|
||||||
@ -922,14 +901,13 @@ def _make_listes_sem(formsemestre: FormSemestre, with_absences=True):
|
|||||||
|
|
||||||
H.append("</tr>")
|
H.append("</tr>")
|
||||||
H.append("</table>")
|
H.append("</table>")
|
||||||
if partition_is_empty:
|
else:
|
||||||
H.append('<p class="help indent">Aucun groupe peuplé dans cette partition')
|
H.append('<p class="help indent">Aucun groupe dans cette partition')
|
||||||
if sco_groups.sco_permissions_check.can_change_groups(formsemestre.id):
|
if sco_groups.sco_permissions_check.can_change_groups(formsemestre.id):
|
||||||
H.append(
|
H.append(
|
||||||
f""" (<a href="{url_for("scolar.partition_editor",
|
f""" (<a href="{url_for("scolar.affect_groups",
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
formsemestre_id=formsemestre.id,
|
partition_id=partition["partition_id"])
|
||||||
edit_partition=1)
|
|
||||||
}" class="stdlink">créer</a>)"""
|
}" class="stdlink">créer</a>)"""
|
||||||
)
|
)
|
||||||
H.append("</p>")
|
H.append("</p>")
|
||||||
@ -981,7 +959,7 @@ def html_expr_diagnostic(diagnostics):
|
|||||||
|
|
||||||
def formsemestre_status_head(formsemestre_id: int = None, page_title: str = None):
|
def formsemestre_status_head(formsemestre_id: int = None, page_title: str = None):
|
||||||
"""En-tête HTML des pages "semestre" """
|
"""En-tête HTML des pages "semestre" """
|
||||||
sem: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
|
sem: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||||
if not sem:
|
if not sem:
|
||||||
raise ScoValueError("Semestre inexistant (il a peut être été supprimé ?)")
|
raise ScoValueError("Semestre inexistant (il a peut être été supprimé ?)")
|
||||||
formation: Formation = sem.formation
|
formation: Formation = sem.formation
|
||||||
@ -1232,7 +1210,7 @@ def formsemestre_tableau_modules(
|
|||||||
H = []
|
H = []
|
||||||
prev_ue_id = None
|
prev_ue_id = None
|
||||||
for modimpl in modimpls:
|
for modimpl in modimpls:
|
||||||
mod: Module = db.session.get(Module, modimpl["module_id"])
|
mod: Module = Module.query.get(modimpl["module_id"])
|
||||||
moduleimpl_status_url = url_for(
|
moduleimpl_status_url = url_for(
|
||||||
"notes.moduleimpl_status",
|
"notes.moduleimpl_status",
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
@ -31,17 +31,15 @@ import time
|
|||||||
|
|
||||||
import flask
|
import flask
|
||||||
from flask import url_for, flash, g, request
|
from flask import url_for, flash, g, request
|
||||||
from flask_login import current_user
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
|
|
||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app import db, log
|
from app import db, log
|
||||||
|
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
from app.comp.res_compat import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
from app.models import Formation, FormSemestre, UniteEns, ScolarNews
|
from app.models import Formation, FormSemestre, UniteEns
|
||||||
from app.models.notes import etud_has_notes_attente
|
from app.models.notes import etud_has_notes_attente
|
||||||
from app.models.validations import (
|
from app.models.validations import (
|
||||||
ScolarAutorisationInscription,
|
ScolarAutorisationInscription,
|
||||||
@ -67,8 +65,6 @@ from app.scodoc.sco_cursus_dut import etud_est_inscrit_ue
|
|||||||
from app.scodoc import sco_photos
|
from app.scodoc import sco_photos
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
from app.scodoc import sco_pv_dict
|
from app.scodoc import sco_pv_dict
|
||||||
from app.scodoc.sco_permissions import Permission
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------------
|
||||||
def formsemestre_validation_etud_form(
|
def formsemestre_validation_etud_form(
|
||||||
@ -400,7 +396,7 @@ def formsemestre_validation_etud(
|
|||||||
selected_choice = choice
|
selected_choice = choice
|
||||||
break
|
break
|
||||||
if not selected_choice:
|
if not selected_choice:
|
||||||
raise ValueError(f"code choix invalide ! ({codechoice})")
|
raise ValueError("code choix invalide ! (%s)" % codechoice)
|
||||||
#
|
#
|
||||||
Se.valide_decision(selected_choice) # enregistre
|
Se.valide_decision(selected_choice) # enregistre
|
||||||
return _redirect_valid_choice(
|
return _redirect_valid_choice(
|
||||||
@ -515,7 +511,7 @@ def decisions_possible_rows(Se, assiduite, subtitle="", trclass=""):
|
|||||||
|
|
||||||
|
|
||||||
def formsemestre_recap_parcours_table(
|
def formsemestre_recap_parcours_table(
|
||||||
situation_etud_cursus: sco_cursus_dut.SituationEtudCursus,
|
Se,
|
||||||
etudid,
|
etudid,
|
||||||
with_links=False,
|
with_links=False,
|
||||||
with_all_columns=True,
|
with_all_columns=True,
|
||||||
@ -553,18 +549,16 @@ def formsemestre_recap_parcours_table(
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
# titres des UE
|
# titres des UE
|
||||||
H.append("<th></th>" * situation_etud_cursus.nb_max_ue)
|
H.append("<th></th>" * Se.nb_max_ue)
|
||||||
#
|
#
|
||||||
if with_links:
|
if with_links:
|
||||||
H.append("<th></th>")
|
H.append("<th></th>")
|
||||||
H.append("<th></th></tr>")
|
H.append("<th></th></tr>")
|
||||||
|
|
||||||
num_sem = 0
|
num_sem = 0
|
||||||
for sem in situation_etud_cursus.get_semestres():
|
for sem in Se.get_semestres():
|
||||||
is_prev = situation_etud_cursus.prev and (
|
is_prev = Se.prev and (Se.prev["formsemestre_id"] == sem["formsemestre_id"])
|
||||||
situation_etud_cursus.prev["formsemestre_id"] == sem["formsemestre_id"]
|
is_cur = Se.formsemestre_id == sem["formsemestre_id"]
|
||||||
)
|
|
||||||
is_cur = situation_etud_cursus.formsemestre_id == sem["formsemestre_id"]
|
|
||||||
num_sem += 1
|
num_sem += 1
|
||||||
|
|
||||||
dpv = sco_pv_dict.dict_pvjury(sem["formsemestre_id"], etudids=[etudid])
|
dpv = sco_pv_dict.dict_pvjury(sem["formsemestre_id"], etudids=[etudid])
|
||||||
@ -576,7 +570,7 @@ def formsemestre_recap_parcours_table(
|
|||||||
else:
|
else:
|
||||||
ass = ""
|
ass = ""
|
||||||
|
|
||||||
formsemestre = db.session.get(FormSemestre, sem["formsemestre_id"])
|
formsemestre = FormSemestre.query.get(sem["formsemestre_id"])
|
||||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
if is_cur:
|
if is_cur:
|
||||||
type_sem = "*" # now unused
|
type_sem = "*" # now unused
|
||||||
@ -587,7 +581,7 @@ def formsemestre_recap_parcours_table(
|
|||||||
else:
|
else:
|
||||||
type_sem = ""
|
type_sem = ""
|
||||||
class_sem = "sem_autre"
|
class_sem = "sem_autre"
|
||||||
if sem["formation_code"] != situation_etud_cursus.formation.formation_code:
|
if sem["formation_code"] != Se.formation.formation_code:
|
||||||
class_sem += " sem_autre_formation"
|
class_sem += " sem_autre_formation"
|
||||||
if sem["bul_bgcolor"]:
|
if sem["bul_bgcolor"]:
|
||||||
bgcolor = sem["bul_bgcolor"]
|
bgcolor = sem["bul_bgcolor"]
|
||||||
@ -651,7 +645,7 @@ def formsemestre_recap_parcours_table(
|
|||||||
H.append("<td><em>en cours</em></td>")
|
H.append("<td><em>en cours</em></td>")
|
||||||
H.append(f"""<td class="rcp_nonass">{ass}</td>""") # abs
|
H.append(f"""<td class="rcp_nonass">{ass}</td>""") # abs
|
||||||
# acronymes UEs auxquelles l'étudiant est inscrit (ou capitalisé)
|
# acronymes UEs auxquelles l'étudiant est inscrit (ou capitalisé)
|
||||||
ues = list(nt.etud_ues(etudid)) # nb: en BUT, les UE "dispensées" sont incluses
|
ues = list(nt.etud_ues(etudid))
|
||||||
cnx = ndb.GetDBConnexion()
|
cnx = ndb.GetDBConnexion()
|
||||||
etud_ue_status = {ue.id: nt.get_etud_ue_status(etudid, ue.id) for ue in ues}
|
etud_ue_status = {ue.id: nt.get_etud_ue_status(etudid, ue.id) for ue in ues}
|
||||||
if not nt.is_apc:
|
if not nt.is_apc:
|
||||||
@ -665,10 +659,8 @@ def formsemestre_recap_parcours_table(
|
|||||||
|
|
||||||
for ue in ues:
|
for ue in ues:
|
||||||
H.append(f"""<td class="ue_acro"><span>{ue.acronyme}</span></td>""")
|
H.append(f"""<td class="ue_acro"><span>{ue.acronyme}</span></td>""")
|
||||||
if len(ues) < situation_etud_cursus.nb_max_ue:
|
if len(ues) < Se.nb_max_ue:
|
||||||
H.append(
|
H.append(f"""<td colspan="{Se.nb_max_ue - len(ues)}"></td>""")
|
||||||
f"""<td colspan="{situation_etud_cursus.nb_max_ue - len(ues)}"></td>"""
|
|
||||||
)
|
|
||||||
# indique le semestre compensé par celui ci:
|
# indique le semestre compensé par celui ci:
|
||||||
if decision_sem and decision_sem["compense_formsemestre_id"]:
|
if decision_sem and decision_sem["compense_formsemestre_id"]:
|
||||||
csem = sco_formsemestre.get_formsemestre(
|
csem = sco_formsemestre.get_formsemestre(
|
||||||
@ -693,7 +685,7 @@ def formsemestre_recap_parcours_table(
|
|||||||
if not sem["etat"]: # locked
|
if not sem["etat"]: # locked
|
||||||
lockicon = scu.icontag("lock32_img", title="verrouillé", border="0")
|
lockicon = scu.icontag("lock32_img", title="verrouillé", border="0")
|
||||||
default_sem_info += lockicon
|
default_sem_info += lockicon
|
||||||
if sem["formation_code"] != situation_etud_cursus.formation.formation_code:
|
if sem["formation_code"] != Se.formation.formation_code:
|
||||||
default_sem_info += f"""Autre formation: {sem["formation_code"]}"""
|
default_sem_info += f"""Autre formation: {sem["formation_code"]}"""
|
||||||
H.append(
|
H.append(
|
||||||
'<td class="datefin">%s</td><td class="sem_info">%s</td>'
|
'<td class="datefin">%s</td><td class="sem_info">%s</td>'
|
||||||
@ -730,21 +722,14 @@ def formsemestre_recap_parcours_table(
|
|||||||
explanation_ue.append(
|
explanation_ue.append(
|
||||||
f"""Capitalisée le {ue_status["event_date"] or "?"}."""
|
f"""Capitalisée le {ue_status["event_date"] or "?"}."""
|
||||||
)
|
)
|
||||||
# Dispense BUT ?
|
|
||||||
if (etudid, ue.id) in nt.dispense_ues:
|
|
||||||
moy_ue_txt = "❎" if (ue_status and ue_status["is_capitalized"]) else "⭕"
|
|
||||||
explanation_ue.append("non inscrit (dispense)")
|
|
||||||
else:
|
|
||||||
moy_ue_txt = scu.fmt_note(moy_ue)
|
|
||||||
H.append(
|
H.append(
|
||||||
f"""<td class="{class_ue}" title="{
|
f"""<td class="{class_ue}" title="{
|
||||||
" ".join(explanation_ue)
|
" ".join(explanation_ue)
|
||||||
}">{moy_ue_txt}</td>"""
|
}">{scu.fmt_note(moy_ue)}</td>"""
|
||||||
)
|
|
||||||
if len(ues) < situation_etud_cursus.nb_max_ue:
|
|
||||||
H.append(
|
|
||||||
f"""<td colspan="{situation_etud_cursus.nb_max_ue - len(ues)}"></td>"""
|
|
||||||
)
|
)
|
||||||
|
if len(ues) < Se.nb_max_ue:
|
||||||
|
H.append(f"""<td colspan="{Se.nb_max_ue - len(ues)}"></td>""")
|
||||||
|
|
||||||
H.append("<td></td>")
|
H.append("<td></td>")
|
||||||
if with_links:
|
if with_links:
|
||||||
@ -1006,26 +991,16 @@ def do_formsemestre_validation_auto(formsemestre_id):
|
|||||||
)
|
)
|
||||||
nb_valid += 1
|
nb_valid += 1
|
||||||
log(
|
log(
|
||||||
f"do_formsemestre_validation_auto: {nb_valid} validations, {len(conflicts)} conflicts"
|
"do_formsemestre_validation_auto: %d validations, %d conflicts"
|
||||||
|
% (nb_valid, len(conflicts))
|
||||||
)
|
)
|
||||||
ScolarNews.add(
|
H = [html_sco_header.sco_header(page_title="Saisie automatique")]
|
||||||
typ=ScolarNews.NEWS_JURY,
|
H.append(
|
||||||
obj=formsemestre.id,
|
"""<h2>Saisie automatique des décisions du semestre %s</h2>
|
||||||
text=f"""Calcul jury automatique du semestre {formsemestre.html_link_status()
|
|
||||||
} ({nb_valid} décisions)""",
|
|
||||||
url=url_for(
|
|
||||||
"notes.formsemestre_status",
|
|
||||||
scodoc_dept=g.scodoc_dept,
|
|
||||||
formsemestre_id=formsemestre.id,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
H = [
|
|
||||||
f"""{html_sco_header.sco_header(page_title="Saisie automatique")}
|
|
||||||
<h2>Saisie automatique des décisions du semestre {formsemestre.titre_annee()}</h2>
|
|
||||||
<p>Opération effectuée.</p>
|
<p>Opération effectuée.</p>
|
||||||
<p>{nb_valid} étudiants validés sur {len(etudids)}</p>
|
<p>%d étudiants validés (sur %s)</p>"""
|
||||||
"""
|
% (sem["titreannee"], nb_valid, len(etudids))
|
||||||
]
|
)
|
||||||
if conflicts:
|
if conflicts:
|
||||||
H.append(
|
H.append(
|
||||||
f"""<p><b>Attention:</b> {len(conflicts)} étudiants non modifiés
|
f"""<p><b>Attention:</b> {len(conflicts)} étudiants non modifiés
|
||||||
@ -1084,44 +1059,64 @@ def formsemestre_validation_suppress_etud(formsemestre_id, etudid):
|
|||||||
) # > suppr. decision jury (peut affecter de plusieurs semestres utilisant UE capitalisée)
|
) # > suppr. decision jury (peut affecter de plusieurs semestres utilisant UE capitalisée)
|
||||||
|
|
||||||
|
|
||||||
def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite):
|
def formsemestre_validate_previous_ue(formsemestre_id, etudid):
|
||||||
"""Form. saisie UE validée hors ScoDoc
|
"""Form. saisie UE validée hors ScoDoc
|
||||||
(pour étudiants arrivant avec un UE antérieurement validée).
|
(pour étudiants arrivant avec un UE antérieurement validée).
|
||||||
"""
|
"""
|
||||||
formation: Formation = formsemestre.formation
|
from app.scodoc import sco_formations
|
||||||
|
|
||||||
# Toutes les UEs non bonus de cette formation sont présentées
|
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||||
# avec indice de semestre <= semestre courant ou NULL
|
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||||
ues = formation.ues.filter(
|
formation: Formation = Formation.query.get_or_404(sem["formation_id"])
|
||||||
UniteEns.type != UE_SPORT,
|
H = [
|
||||||
db.or_(
|
html_sco_header.sco_header(
|
||||||
UniteEns.semestre_idx == None,
|
page_title="Validation UE",
|
||||||
UniteEns.semestre_idx <= formsemestre.semestre_id,
|
javascripts=["js/validate_previous_ue.js"],
|
||||||
),
|
),
|
||||||
).order_by(UniteEns.semestre_idx, UniteEns.numero)
|
'<table style="width: 100%"><tr><td>',
|
||||||
|
"""<h2 class="formsemestre">%s: validation d'une UE antérieure</h2>"""
|
||||||
ue_names = ["Choisir..."] + [
|
% etud["nomprenom"],
|
||||||
f"""{('S'+str(ue.semestre_idx)+' : ') if ue.semestre_idx is not None else ''
|
|
||||||
}{ue.acronyme} {ue.titre} ({ue.ue_code or ""})"""
|
|
||||||
for ue in ues
|
|
||||||
]
|
|
||||||
ue_ids = [""] + [ue.id for ue in ues]
|
|
||||||
form_descr = [
|
|
||||||
("etudid", {"input_type": "hidden"}),
|
|
||||||
("formsemestre_id", {"input_type": "hidden"}),
|
|
||||||
(
|
(
|
||||||
"ue_id",
|
'</td><td style="text-align: right;"><a href="%s">%s</a></td></tr></table>'
|
||||||
{
|
% (
|
||||||
"input_type": "menu",
|
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
|
||||||
"title": "Unité d'Enseignement (UE)",
|
sco_photos.etud_photo_html(etud, title="fiche de %s" % etud["nom"]),
|
||||||
"allow_null": False,
|
)
|
||||||
"allowed_values": ue_ids,
|
|
||||||
"labels": ue_names,
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
f"""<p class="help">Utiliser cette page pour enregistrer une UE validée antérieurement,
|
||||||
|
<em>dans un semestre hors ScoDoc</em>.</p>
|
||||||
|
<p><b>Les UE validées dans ScoDoc sont déjà
|
||||||
|
automatiquement prises en compte</b>. Cette page n'est utile que pour les étudiants ayant
|
||||||
|
suivi un début de cursus dans <b>un autre établissement</b>, ou bien dans un semestre géré <b>sans
|
||||||
|
ScoDoc</b> et qui <b>redouble</b> ce semestre
|
||||||
|
(<em>ne pas utiliser pour les semestres précédents !</em>).
|
||||||
|
</p>
|
||||||
|
<p>Notez que l'UE est validée, avec enregistrement immédiat de la décision et
|
||||||
|
l'attribution des ECTS.</p>
|
||||||
|
<p>On ne peut prendre en compte ici que les UE du cursus <b>{formation.titre}</b></p>
|
||||||
|
""",
|
||||||
]
|
]
|
||||||
if not formation.is_apc():
|
|
||||||
form_descr.append(
|
# Toutes les UE de cette formation sont présentées (même celles des autres semestres)
|
||||||
|
ues = formation.ues.order_by(UniteEns.numero)
|
||||||
|
ue_names = ["Choisir..."] + [f"{ue.acronyme} {ue.titre}" for ue in ues]
|
||||||
|
ue_ids = [""] + [ue.id for ue in ues]
|
||||||
|
tf = TrivialFormulator(
|
||||||
|
request.base_url,
|
||||||
|
scu.get_request_args(),
|
||||||
|
(
|
||||||
|
("etudid", {"input_type": "hidden"}),
|
||||||
|
("formsemestre_id", {"input_type": "hidden"}),
|
||||||
|
(
|
||||||
|
"ue_id",
|
||||||
|
{
|
||||||
|
"input_type": "menu",
|
||||||
|
"title": "Unité d'Enseignement (UE)",
|
||||||
|
"allow_null": False,
|
||||||
|
"allowed_values": ue_ids,
|
||||||
|
"labels": ue_names,
|
||||||
|
},
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"semestre_id",
|
"semestre_id",
|
||||||
{
|
{
|
||||||
@ -1132,185 +1127,69 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
|
|||||||
"allowed_values": [""] + [x for x in range(11)],
|
"allowed_values": [""] + [x for x in range(11)],
|
||||||
"labels": ["-"] + list(range(11)),
|
"labels": ["-"] + list(range(11)),
|
||||||
},
|
},
|
||||||
)
|
),
|
||||||
)
|
(
|
||||||
ue_codes = sorted(codes_cursus.CODES_JURY_UE)
|
"date",
|
||||||
form_descr += [
|
{
|
||||||
(
|
"input_type": "date",
|
||||||
"date",
|
"size": 9,
|
||||||
{
|
"explanation": "j/m/a",
|
||||||
"input_type": "date",
|
"default": time.strftime("%d/%m/%Y"),
|
||||||
"size": 9,
|
},
|
||||||
"explanation": "j/m/a",
|
),
|
||||||
"default": time.strftime("%d/%m/%Y"),
|
(
|
||||||
},
|
"moy_ue",
|
||||||
|
{
|
||||||
|
"type": "float",
|
||||||
|
"allow_null": False,
|
||||||
|
"min_value": 0,
|
||||||
|
"max_value": 20,
|
||||||
|
"title": "Moyenne (/20) obtenue dans cette UE:",
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
(
|
cancelbutton="Annuler",
|
||||||
"moy_ue",
|
|
||||||
{
|
|
||||||
"type": "float",
|
|
||||||
"allow_null": False,
|
|
||||||
"min_value": 0,
|
|
||||||
"max_value": 20,
|
|
||||||
"title": "Moyenne (/20) obtenue dans cette UE:",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"code_jury",
|
|
||||||
{
|
|
||||||
"input_type": "menu",
|
|
||||||
"title": "Code jury",
|
|
||||||
"explanation": " code donné par le jury (ADM si validée normalement)",
|
|
||||||
"allow_null": True,
|
|
||||||
"allowed_values": [""] + ue_codes,
|
|
||||||
"labels": ["-"] + ue_codes,
|
|
||||||
"default": ADM,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
tf = TrivialFormulator(
|
|
||||||
request.base_url,
|
|
||||||
scu.get_request_args(),
|
|
||||||
form_descr,
|
|
||||||
cancelbutton="Revenir au bulletin",
|
|
||||||
submitlabel="Enregistrer validation d'UE",
|
submitlabel="Enregistrer validation d'UE",
|
||||||
)
|
)
|
||||||
if tf[0] == 0:
|
if tf[0] == 0:
|
||||||
return f"""
|
X = """
|
||||||
{html_sco_header.sco_header(
|
<div id="ue_list_etud_validations"><!-- filled by get_etud_ue_cap_html --></div>
|
||||||
page_title="Validation UE antérieure",
|
<div id="ue_list_code"><!-- filled by ue_sharing_code --></div>
|
||||||
javascripts=["js/validate_previous_ue.js"],
|
"""
|
||||||
cssstyles=["css/jury_delete_manual.css"],
|
warn, ue_multiples = check_formation_ues(formation.id)
|
||||||
etudid=etud.id,
|
return "\n".join(H) + tf[1] + X + warn + html_sco_header.sco_footer()
|
||||||
formsemestre_id=formsemestre.id,
|
elif tf[0] == -1:
|
||||||
)}
|
return flask.redirect(
|
||||||
<h2 class="formsemestre">Gestion des validations d'UEs antérieures
|
scu.NotesURL()
|
||||||
de {etud.html_link_fiche()}
|
+ "/formsemestre_status?formsemestre_id="
|
||||||
</h2>
|
+ str(formsemestre_id)
|
||||||
|
)
|
||||||
<p class="help">Utiliser cette page pour enregistrer des UEs validées antérieurement,
|
else:
|
||||||
<em>dans un semestre hors ScoDoc</em>.</p>
|
if tf[2]["semestre_id"]:
|
||||||
<p class="expl"><b>Les UE validées dans ScoDoc sont
|
semestre_id = int(tf[2]["semestre_id"])
|
||||||
automatiquement prises en compte</b>.
|
else:
|
||||||
</p>
|
semestre_id = None
|
||||||
<p>Cette page est surtout utile pour les étudiants ayant
|
do_formsemestre_validate_previous_ue(
|
||||||
suivi un début de cursus dans <b>un autre établissement</b>, ou qui
|
formsemestre_id,
|
||||||
ont suivi une UE à l'étranger ou dans un semestre géré <b>sans ScoDoc</b>.
|
etudid,
|
||||||
</p>
|
tf[2]["ue_id"],
|
||||||
<p>Pour les semestres précédents gérés avec ScoDoc, passer par la page jury normale.
|
tf[2]["moy_ue"],
|
||||||
</p>
|
tf[2]["date"],
|
||||||
<p>Notez que l'UE est validée, avec enregistrement immédiat de la décision et
|
semestre_id=semestre_id,
|
||||||
l'attribution des ECTS si le code jury est validant (ADM).
|
)
|
||||||
</p>
|
flash("Validation d'UE enregistrée")
|
||||||
<p>On ne peut valider ici que les UEs du cursus <b>{formation.titre}</b></p>
|
|
||||||
|
|
||||||
{_get_etud_ue_cap_html(etud, formsemestre)}
|
|
||||||
|
|
||||||
<div class="sco_box">
|
|
||||||
<div class="sco_box_title">
|
|
||||||
Enregistrer une UE antérieure
|
|
||||||
</div>
|
|
||||||
{tf[1]}
|
|
||||||
</div>
|
|
||||||
<div id="ue_list_code" class="sco_box sco_green_bg">
|
|
||||||
<!-- filled by ue_sharing_code -->
|
|
||||||
</div>
|
|
||||||
{check_formation_ues(formation.id)[0]}
|
|
||||||
{html_sco_header.sco_footer()}
|
|
||||||
"""
|
|
||||||
|
|
||||||
dest_url = url_for(
|
|
||||||
"notes.formsemestre_validate_previous_ue",
|
|
||||||
scodoc_dept=g.scodoc_dept,
|
|
||||||
formsemestre_id=formsemestre.id,
|
|
||||||
etudid=etud.id,
|
|
||||||
)
|
|
||||||
if tf[0] == -1:
|
|
||||||
return flask.redirect(
|
return flask.redirect(
|
||||||
url_for(
|
url_for(
|
||||||
"notes.formsemestre_bulletinetud",
|
"notes.formsemestre_bulletinetud",
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
formsemestre_id=formsemestre.id,
|
formsemestre_id=formsemestre_id,
|
||||||
etudid=etud.id,
|
etudid=etudid,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if tf[2].get("semestre_id"):
|
|
||||||
semestre_id = int(tf[2]["semestre_id"])
|
|
||||||
else:
|
|
||||||
semestre_id = None
|
|
||||||
|
|
||||||
if tf[2]["code_jury"] not in CODES_JURY_UE:
|
|
||||||
flash("Code UE invalide")
|
|
||||||
return flask.redirect(dest_url)
|
|
||||||
do_formsemestre_validate_previous_ue(
|
|
||||||
formsemestre,
|
|
||||||
etud.id,
|
|
||||||
tf[2]["ue_id"],
|
|
||||||
tf[2]["moy_ue"],
|
|
||||||
tf[2]["date"],
|
|
||||||
code=tf[2]["code_jury"],
|
|
||||||
semestre_id=semestre_id,
|
|
||||||
)
|
|
||||||
flash("Validation d'UE enregistrée")
|
|
||||||
return flask.redirect(dest_url)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_etud_ue_cap_html(etud: Identite, formsemestre: FormSemestre) -> str:
|
|
||||||
"""HTML listant les validations d'UEs pour cet étudiant dans des formations de même
|
|
||||||
code que celle du formsemestre indiqué.
|
|
||||||
"""
|
|
||||||
validations: list[ScolarFormSemestreValidation] = (
|
|
||||||
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
|
|
||||||
.join(UniteEns)
|
|
||||||
.join(Formation)
|
|
||||||
.filter_by(formation_code=formsemestre.formation.formation_code)
|
|
||||||
.order_by(
|
|
||||||
sa.desc(UniteEns.semestre_idx),
|
|
||||||
UniteEns.acronyme,
|
|
||||||
sa.desc(ScolarFormSemestreValidation.event_date),
|
|
||||||
)
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
|
|
||||||
if not validations:
|
|
||||||
return ""
|
|
||||||
H = [
|
|
||||||
f"""<div class="sco_box sco_lightgreen_bg ue_list_etud_validations">
|
|
||||||
<div class="sco_box_title">Validations d'UEs dans cette formation</div>
|
|
||||||
<div class="help">Liste de toutes les UEs validées par {etud.html_link_fiche()},
|
|
||||||
sur des semestres ou déclarées comme "antérieures" (externes).
|
|
||||||
</div>
|
|
||||||
<ul class="liste_validations">"""
|
|
||||||
]
|
|
||||||
for validation in validations:
|
|
||||||
if validation.formsemestre_id is None:
|
|
||||||
origine = " enregistrée d'un parcours antérieur (hors ScoDoc)"
|
|
||||||
else:
|
|
||||||
origine = f", du semestre {formsemestre.html_link_status()}"
|
|
||||||
if validation.semestre_id is not None:
|
|
||||||
origine += f" (<b>S{validation.semestre_id}</b>)"
|
|
||||||
H.append(f"""<li>{validation.html()}""")
|
|
||||||
if (validation.formsemestre and validation.formsemestre.can_edit_jury()) or (
|
|
||||||
current_user and current_user.has_permission(Permission.ScoEtudInscrit)
|
|
||||||
):
|
|
||||||
H.append(
|
|
||||||
f"""
|
|
||||||
<form class="inline-form">
|
|
||||||
<button
|
|
||||||
data-v_id="{validation.id}" data-type="validation_ue" data-etudid="{etud.id}"
|
|
||||||
>effacer</button>
|
|
||||||
</form>
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
H.append(scu.icontag("lock_img", border="0", title="Semestre verrouillé"))
|
|
||||||
H.append("</li>")
|
|
||||||
H.append("</ul></div>")
|
|
||||||
return "\n".join(H)
|
|
||||||
|
|
||||||
|
|
||||||
def do_formsemestre_validate_previous_ue(
|
def do_formsemestre_validate_previous_ue(
|
||||||
formsemestre: FormSemestre,
|
formsemestre_id,
|
||||||
etudid,
|
etudid,
|
||||||
ue_id,
|
ue_id,
|
||||||
moy_ue,
|
moy_ue,
|
||||||
@ -1323,20 +1202,21 @@ def do_formsemestre_validate_previous_ue(
|
|||||||
Si le coefficient est spécifié, modifie le coefficient de
|
Si le coefficient est spécifié, modifie le coefficient de
|
||||||
cette UE (utile seulement pour les semestres extérieurs).
|
cette UE (utile seulement pour les semestres extérieurs).
|
||||||
"""
|
"""
|
||||||
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
ue: UniteEns = UniteEns.query.get_or_404(ue_id)
|
ue: UniteEns = UniteEns.query.get_or_404(ue_id)
|
||||||
|
|
||||||
cnx = ndb.GetDBConnexion()
|
cnx = ndb.GetDBConnexion()
|
||||||
if ue_coefficient is not None:
|
if ue_coefficient != None:
|
||||||
sco_formsemestre.do_formsemestre_uecoef_edit_or_create(
|
sco_formsemestre.do_formsemestre_uecoef_edit_or_create(
|
||||||
cnx, formsemestre.id, ue_id, ue_coefficient
|
cnx, formsemestre_id, ue_id, ue_coefficient
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
sco_formsemestre.do_formsemestre_uecoef_delete(cnx, formsemestre.id, ue_id)
|
sco_formsemestre.do_formsemestre_uecoef_delete(cnx, formsemestre_id, ue_id)
|
||||||
sco_cursus_dut.do_formsemestre_validate_ue(
|
sco_cursus_dut.do_formsemestre_validate_ue(
|
||||||
cnx,
|
cnx,
|
||||||
nt,
|
nt,
|
||||||
formsemestre.id, # "importe" cette UE dans le semestre (new 3/2015)
|
formsemestre_id, # "importe" cette UE dans le semestre (new 3/2015)
|
||||||
etudid,
|
etudid,
|
||||||
ue_id,
|
ue_id,
|
||||||
code,
|
code,
|
||||||
@ -1374,6 +1254,62 @@ def _invalidate_etud_formation_caches(etudid, formation_id):
|
|||||||
) # > modif decision UE (inval tous semestres avec cet etudiant, ok mais conservatif)
|
) # > modif decision UE (inval tous semestres avec cet etudiant, ok mais conservatif)
|
||||||
|
|
||||||
|
|
||||||
|
def get_etud_ue_cap_html(etudid, formsemestre_id, ue_id):
|
||||||
|
"""Ramene bout de HTML pour pouvoir supprimer une validation de cette UE"""
|
||||||
|
valids = ndb.SimpleDictFetch(
|
||||||
|
"""SELECT SFV.*
|
||||||
|
FROM scolar_formsemestre_validation SFV
|
||||||
|
WHERE ue_id=%(ue_id)s
|
||||||
|
AND etudid=%(etudid)s""",
|
||||||
|
{"etudid": etudid, "ue_id": ue_id},
|
||||||
|
)
|
||||||
|
if not valids:
|
||||||
|
return ""
|
||||||
|
H = [
|
||||||
|
'<div class="existing_valids"><span>Validations existantes pour cette UE:</span><ul>'
|
||||||
|
]
|
||||||
|
for valid in valids:
|
||||||
|
valid["event_date"] = ndb.DateISOtoDMY(valid["event_date"])
|
||||||
|
if valid["moy_ue"] != None:
|
||||||
|
valid["m"] = ", moyenne %(moy_ue)g/20" % valid
|
||||||
|
else:
|
||||||
|
valid["m"] = ""
|
||||||
|
if valid["formsemestre_id"]:
|
||||||
|
sem = sco_formsemestre.get_formsemestre(valid["formsemestre_id"])
|
||||||
|
valid["s"] = ", du semestre %s" % sem["titreannee"]
|
||||||
|
else:
|
||||||
|
valid["s"] = " enregistrée d'un parcours antérieur (hors ScoDoc)"
|
||||||
|
if valid["semestre_id"]:
|
||||||
|
valid["s"] += " (<b>S%d</b>)" % valid["semestre_id"]
|
||||||
|
valid["ds"] = formsemestre_id
|
||||||
|
H.append(
|
||||||
|
'<li>%(code)s%(m)s%(s)s, le %(event_date)s <a class="stdlink" href="etud_ue_suppress_validation?etudid=%(etudid)s&ue_id=%(ue_id)s&formsemestre_id=%(ds)s" title="supprime cette validation">effacer</a></li>'
|
||||||
|
% valid
|
||||||
|
)
|
||||||
|
H.append("</ul></div>")
|
||||||
|
return "\n".join(H)
|
||||||
|
|
||||||
|
|
||||||
|
def etud_ue_suppress_validation(etudid, formsemestre_id, ue_id):
|
||||||
|
"""Suppress a validation (ue_id, etudid) and redirect to formsemestre"""
|
||||||
|
log("etud_ue_suppress_validation( %s, %s, %s)" % (etudid, formsemestre_id, ue_id))
|
||||||
|
cnx = ndb.GetDBConnexion()
|
||||||
|
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||||
|
cursor.execute(
|
||||||
|
"DELETE FROM scolar_formsemestre_validation WHERE etudid=%(etudid)s and ue_id=%(ue_id)s",
|
||||||
|
{"etudid": etudid, "ue_id": ue_id},
|
||||||
|
)
|
||||||
|
|
||||||
|
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||||
|
_invalidate_etud_formation_caches(etudid, sem["formation_id"])
|
||||||
|
|
||||||
|
return flask.redirect(
|
||||||
|
scu.NotesURL()
|
||||||
|
+ "/formsemestre_validate_previous_ue?etudid=%s&formsemestre_id=%s"
|
||||||
|
% (etudid, formsemestre_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def check_formation_ues(formation_id):
|
def check_formation_ues(formation_id):
|
||||||
"""Verifie que les UE d'une formation sont chacune utilisée dans un seul semestre_id
|
"""Verifie que les UE d'une formation sont chacune utilisée dans un seul semestre_id
|
||||||
Si ce n'est pas le cas, c'est probablement (mais pas forcément) une erreur de
|
Si ce n'est pas le cas, c'est probablement (mais pas forcément) une erreur de
|
||||||
|
@ -34,6 +34,7 @@ Optimisation possible:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import collections
|
import collections
|
||||||
|
import operator
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from xml.etree import ElementTree
|
from xml.etree import ElementTree
|
||||||
@ -44,14 +45,15 @@ from flask import g, request
|
|||||||
from flask import url_for, make_response
|
from flask import url_for, make_response
|
||||||
from sqlalchemy.sql import text
|
from sqlalchemy.sql import text
|
||||||
|
|
||||||
from app import cache, db, log
|
from app import db
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
from app.comp.res_compat import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
from app.models import FormSemestre, Identite, Scolog
|
from app.models import FormSemestre, Identite
|
||||||
from app.models import GROUPNAME_STR_LEN, SHORT_STR_LEN
|
from app.models import GROUPNAME_STR_LEN, SHORT_STR_LEN
|
||||||
from app.models.groups import GroupDescr, Partition, group_membership
|
from app.models.groups import GroupDescr, Partition
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
|
from app import log, cache
|
||||||
from app.scodoc.scolog import logdb
|
from app.scodoc.scolog import logdb
|
||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
@ -92,7 +94,7 @@ groupEditor = ndb.EditableTable(
|
|||||||
group_list = groupEditor.list
|
group_list = groupEditor.list
|
||||||
|
|
||||||
|
|
||||||
def get_group(group_id: int) -> dict: # OBSOLETE !
|
def get_group(group_id: int) -> dict:
|
||||||
"""Returns group object, with partition"""
|
"""Returns group object, with partition"""
|
||||||
r = ndb.SimpleDictFetch(
|
r = ndb.SimpleDictFetch(
|
||||||
"""SELECT gd.id AS group_id, gd.*, p.id AS partition_id, p.*
|
"""SELECT gd.id AS group_id, gd.*, p.id AS partition_id, p.*
|
||||||
@ -122,7 +124,7 @@ def group_delete(group_id: int):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_partition(partition_id): # OBSOLETE
|
def get_partition(partition_id):
|
||||||
r = ndb.SimpleDictFetch(
|
r = ndb.SimpleDictFetch(
|
||||||
"""SELECT p.id AS partition_id, p.*
|
"""SELECT p.id AS partition_id, p.*
|
||||||
FROM partition p
|
FROM partition p
|
||||||
@ -198,7 +200,7 @@ def get_formsemestre_etuds_groups(formsemestre_id: int) -> dict:
|
|||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
def get_partition_groups(partition): # OBSOLETE !
|
def get_partition_groups(partition):
|
||||||
"""List of groups in this partition (list of dicts).
|
"""List of groups in this partition (list of dicts).
|
||||||
Some groups may be empty."""
|
Some groups may be empty."""
|
||||||
return ndb.SimpleDictFetch(
|
return ndb.SimpleDictFetch(
|
||||||
@ -241,7 +243,7 @@ def get_default_group(formsemestre_id, fix_if_missing=False):
|
|||||||
return group.id
|
return group.id
|
||||||
# debug check
|
# debug check
|
||||||
if len(r) != 1:
|
if len(r) != 1:
|
||||||
log(f"invalid group structure for {formsemestre_id}: {len(r)}")
|
raise ScoException(f"invalid group structure for {formsemestre_id}")
|
||||||
group_id = r[0]["group_id"]
|
group_id = r[0]["group_id"]
|
||||||
return group_id
|
return group_id
|
||||||
|
|
||||||
@ -450,7 +452,7 @@ def get_etud_formsemestre_groups(
|
|||||||
),
|
),
|
||||||
{"etudid": etud.id, "formsemestre_id": formsemestre.id},
|
{"etudid": etud.id, "formsemestre_id": formsemestre.id},
|
||||||
)
|
)
|
||||||
return [db.session.get(GroupDescr, group_id) for group_id in cursor]
|
return [GroupDescr.query.get(group_id) for group_id in cursor]
|
||||||
|
|
||||||
|
|
||||||
# Ancienne fonction:
|
# Ancienne fonction:
|
||||||
@ -560,10 +562,10 @@ def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
|
|||||||
x_group = Element(
|
x_group = Element(
|
||||||
"group",
|
"group",
|
||||||
partition_id=str(partition_id),
|
partition_id=str(partition_id),
|
||||||
partition_name=partition["partition_name"] or "",
|
partition_name=partition["partition_name"],
|
||||||
groups_editable=str(int(partition["groups_editable"])),
|
groups_editable=str(int(partition["groups_editable"])),
|
||||||
group_id=str(group["group_id"]),
|
group_id=str(group["group_id"]),
|
||||||
group_name=group["group_name"] or "",
|
group_name=group["group_name"],
|
||||||
)
|
)
|
||||||
x_response.append(x_group)
|
x_response.append(x_group)
|
||||||
for e in get_group_members(group["group_id"]):
|
for e in get_group_members(group["group_id"]):
|
||||||
@ -572,10 +574,10 @@ def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
|
|||||||
Element(
|
Element(
|
||||||
"etud",
|
"etud",
|
||||||
etudid=str(e["etudid"]),
|
etudid=str(e["etudid"]),
|
||||||
civilite=etud["civilite_str"] or "",
|
civilite=etud["civilite_str"],
|
||||||
sexe=etud["civilite_str"] or "", # compat
|
sexe=etud["civilite_str"], # compat
|
||||||
nom=sco_etud.format_nom(etud["nom"] or ""),
|
nom=sco_etud.format_nom(etud["nom"]),
|
||||||
prenom=sco_etud.format_prenom(etud["prenom"] or ""),
|
prenom=sco_etud.format_prenom(etud["prenom"]),
|
||||||
origin=_comp_etud_origin(etud, formsemestre),
|
origin=_comp_etud_origin(etud, formsemestre),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -587,7 +589,7 @@ def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
|
|||||||
x_group = Element(
|
x_group = Element(
|
||||||
"group",
|
"group",
|
||||||
partition_id=str(partition_id),
|
partition_id=str(partition_id),
|
||||||
partition_name=partition["partition_name"] or "",
|
partition_name=partition["partition_name"],
|
||||||
groups_editable=str(int(partition["groups_editable"])),
|
groups_editable=str(int(partition["groups_editable"])),
|
||||||
group_id="_none_",
|
group_id="_none_",
|
||||||
group_name="",
|
group_name="",
|
||||||
@ -599,9 +601,9 @@ def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
|
|||||||
Element(
|
Element(
|
||||||
"etud",
|
"etud",
|
||||||
etudid=str(etud["etudid"]),
|
etudid=str(etud["etudid"]),
|
||||||
sexe=etud["civilite_str"] or "",
|
sexe=etud["civilite_str"],
|
||||||
nom=sco_etud.format_nom(etud["nom"] or ""),
|
nom=sco_etud.format_nom(etud["nom"]),
|
||||||
prenom=sco_etud.format_prenom(etud["prenom"] or ""),
|
prenom=sco_etud.format_prenom(etud["prenom"]),
|
||||||
origin=_comp_etud_origin(etud, formsemestre),
|
origin=_comp_etud_origin(etud, formsemestre),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -635,7 +637,7 @@ def _comp_etud_origin(etud: dict, cur_formsemestre: FormSemestre):
|
|||||||
return "" # parcours normal, ne le signale pas
|
return "" # parcours normal, ne le signale pas
|
||||||
|
|
||||||
|
|
||||||
def set_group(etudid: int, group_id: int) -> bool: # OBSOLETE !
|
def set_group(etudid: int, group_id: int) -> bool:
|
||||||
"""Inscrit l'étudiant au groupe.
|
"""Inscrit l'étudiant au groupe.
|
||||||
Return True if ok, False si deja inscrit.
|
Return True if ok, False si deja inscrit.
|
||||||
Warning:
|
Warning:
|
||||||
@ -662,33 +664,55 @@ def set_group(etudid: int, group_id: int) -> bool: # OBSOLETE !
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def change_etud_group_in_partition(etudid: int, group: GroupDescr) -> bool:
|
def change_etud_group_in_partition(etudid: int, group_id: int, partition: dict = None):
|
||||||
"""Inscrit etud au groupe
|
"""Inscrit etud au groupe de cette partition,
|
||||||
(et le désinscrit d'autres groupes de cette partition)
|
et le desinscrit d'autres groupes de cette partition.
|
||||||
Return True si changement, False s'il était déjà dans ce groupe.
|
|
||||||
"""
|
"""
|
||||||
etud: Identite = Identite.query.get_or_404(etudid)
|
log("change_etud_group_in_partition: etudid=%s group_id=%s" % (etudid, group_id))
|
||||||
if not group.partition.set_etud_group(etud, group):
|
# 0- La partition
|
||||||
return # pas de changement
|
group = get_group(group_id)
|
||||||
|
if partition:
|
||||||
|
# verifie que le groupe est bien dans cette partition:
|
||||||
|
if group["partition_id"] != partition["partition_id"]:
|
||||||
|
raise ValueError(
|
||||||
|
"inconsistent group/partition (group_id=%s, partition_id=%s)"
|
||||||
|
% (group_id, partition["partition_id"])
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
partition = get_partition(group["partition_id"])
|
||||||
|
# 1- Supprime membership dans cette partition
|
||||||
|
ndb.SimpleQuery(
|
||||||
|
"""DELETE FROM group_membership gm
|
||||||
|
WHERE EXISTS
|
||||||
|
(SELECT 1 FROM group_descr gd
|
||||||
|
WHERE gm.etudid = %(etudid)s
|
||||||
|
AND gm.group_id = gd.id
|
||||||
|
AND gd.partition_id = %(partition_id)s)
|
||||||
|
""",
|
||||||
|
{"etudid": etudid, "partition_id": partition["partition_id"]},
|
||||||
|
)
|
||||||
|
# 2- associe au nouveau groupe
|
||||||
|
set_group(etudid, group_id)
|
||||||
|
|
||||||
# - log
|
# 3- log
|
||||||
formsemestre: FormSemestre = group.partition.formsemestre
|
formsemestre_id = partition["formsemestre_id"]
|
||||||
log(f"change_etud_group_in_partition: etudid={etudid} group={group}")
|
cnx = ndb.GetDBConnexion()
|
||||||
Scolog.logdb(
|
logdb(
|
||||||
|
cnx,
|
||||||
method="changeGroup",
|
method="changeGroup",
|
||||||
etudid=etudid,
|
etudid=etudid,
|
||||||
msg=f"""formsemestre_id={formsemestre.id}, partition_name={
|
msg="formsemestre_id=%s,partition_name=%s, group_name=%s"
|
||||||
group.partition.partition_name or ""}, group_name={group.group_name or ""}""",
|
% (formsemestre_id, partition["partition_name"], group["group_name"]),
|
||||||
commit=True,
|
|
||||||
)
|
)
|
||||||
|
cnx.commit()
|
||||||
|
|
||||||
# - Update parcours
|
# 5- Update parcours
|
||||||
if group.partition.partition_name == scu.PARTITION_PARCOURS:
|
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||||
formsemestre.update_inscriptions_parcours_from_groups()
|
formsemestre.update_inscriptions_parcours_from_groups()
|
||||||
|
|
||||||
# - invalidate cache
|
# 6- invalidate cache
|
||||||
sco_cache.invalidate_formsemestre(
|
sco_cache.invalidate_formsemestre(
|
||||||
formsemestre_id=formsemestre.id
|
formsemestre_id=formsemestre_id
|
||||||
) # > change etud group
|
) # > change etud group
|
||||||
|
|
||||||
|
|
||||||
@ -705,6 +729,7 @@ def setGroups(
|
|||||||
|
|
||||||
Ne peux pas modifier les groupes des partitions non éditables.
|
Ne peux pas modifier les groupes des partitions non éditables.
|
||||||
"""
|
"""
|
||||||
|
from app.scodoc import sco_formsemestre
|
||||||
|
|
||||||
def xml_error(msg, code=404):
|
def xml_error(msg, code=404):
|
||||||
data = (
|
data = (
|
||||||
@ -714,27 +739,26 @@ def setGroups(
|
|||||||
response.headers["Content-Type"] = scu.XML_MIMETYPE
|
response.headers["Content-Type"] = scu.XML_MIMETYPE
|
||||||
return response
|
return response
|
||||||
|
|
||||||
partition: Partition = db.session.get(Partition, partition_id)
|
partition = get_partition(partition_id)
|
||||||
if not partition.groups_editable and (groupsToCreate or groupsToDelete):
|
if not partition["groups_editable"] and (groupsToCreate or groupsToDelete):
|
||||||
msg = "setGroups: partition non editable"
|
msg = "setGroups: partition non editable"
|
||||||
log(msg)
|
log(msg)
|
||||||
return xml_error(msg, code=403)
|
return xml_error(msg, code=403)
|
||||||
|
formsemestre_id = partition["formsemestre_id"]
|
||||||
if not sco_permissions_check.can_change_groups(partition.formsemestre.id):
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||||
|
if not sco_permissions_check.can_change_groups(formsemestre_id):
|
||||||
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
||||||
log("***setGroups: partition_id=%s" % partition_id)
|
log("***setGroups: partition_id=%s" % partition_id)
|
||||||
log("groupsLists=%s" % groupsLists)
|
log("groupsLists=%s" % groupsLists)
|
||||||
log("groupsToCreate=%s" % groupsToCreate)
|
log("groupsToCreate=%s" % groupsToCreate)
|
||||||
log("groupsToDelete=%s" % groupsToDelete)
|
log("groupsToDelete=%s" % groupsToDelete)
|
||||||
|
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||||
if not partition.formsemestre.etat:
|
if not sem["etat"]:
|
||||||
raise AccessDenied("Modification impossible: semestre verrouillé")
|
raise AccessDenied("Modification impossible: semestre verrouillé")
|
||||||
|
|
||||||
groupsToDelete = [g for g in groupsToDelete.split(";") if g]
|
groupsToDelete = [g for g in groupsToDelete.split(";") if g]
|
||||||
|
|
||||||
etud_groups = formsemestre_get_etud_groupnames(
|
etud_groups = formsemestre_get_etud_groupnames(formsemestre_id, attr="group_id")
|
||||||
partition.formsemestre.id, attr="group_id"
|
|
||||||
)
|
|
||||||
for line in groupsLists.split("\n"): # for each group_id (one per line)
|
for line in groupsLists.split("\n"): # for each group_id (one per line)
|
||||||
fs = line.split(";")
|
fs = line.split(";")
|
||||||
group_id = fs[0].strip()
|
group_id = fs[0].strip()
|
||||||
@ -745,23 +769,26 @@ def setGroups(
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
log(f"setGroups: ignoring invalid group_id={group_id}")
|
log(f"setGroups: ignoring invalid group_id={group_id}")
|
||||||
continue
|
continue
|
||||||
group: GroupDescr = GroupDescr.query.get_or_404(group_id)
|
group = get_group(group_id)
|
||||||
# Anciens membres du groupe:
|
# Anciens membres du groupe:
|
||||||
old_members_set = {etud.id for etud in group.etuds}
|
old_members = get_group_members(group_id)
|
||||||
|
old_members_set = set([x["etudid"] for x in old_members])
|
||||||
# Place dans ce groupe les etudiants indiqués:
|
# Place dans ce groupe les etudiants indiqués:
|
||||||
for etudid_str in fs[1:-1]:
|
for etudid_str in fs[1:-1]:
|
||||||
etudid = int(etudid_str)
|
etudid = int(etudid_str)
|
||||||
if etudid in old_members_set:
|
if etudid in old_members_set:
|
||||||
# était dans ce groupe, l'enlever
|
old_members_set.remove(
|
||||||
old_members_set.remove(etudid)
|
etudid
|
||||||
|
) # a nouveau dans ce groupe, pas besoin de l'enlever
|
||||||
if (etudid not in etud_groups) or (
|
if (etudid not in etud_groups) or (
|
||||||
group_id != etud_groups[etudid].get(partition_id, "")
|
group_id != etud_groups[etudid].get(partition_id, "")
|
||||||
): # pas le meme groupe qu'actuel
|
): # pas le meme groupe qu'actuel
|
||||||
change_etud_group_in_partition(etudid, group)
|
change_etud_group_in_partition(etudid, group_id, partition)
|
||||||
# Retire les anciens membres:
|
# Retire les anciens membres:
|
||||||
cnx = ndb.GetDBConnexion()
|
cnx = ndb.GetDBConnexion()
|
||||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||||
for etudid in old_members_set:
|
for etudid in old_members_set:
|
||||||
|
log("removing %s from group %s" % (etudid, group_id))
|
||||||
ndb.SimpleQuery(
|
ndb.SimpleQuery(
|
||||||
"DELETE FROM group_membership WHERE etudid=%(etudid)s and group_id=%(group_id)s",
|
"DELETE FROM group_membership WHERE etudid=%(etudid)s and group_id=%(group_id)s",
|
||||||
{"etudid": etudid, "group_id": group_id},
|
{"etudid": etudid, "group_id": group_id},
|
||||||
@ -771,8 +798,8 @@ def setGroups(
|
|||||||
cnx,
|
cnx,
|
||||||
method="removeFromGroup",
|
method="removeFromGroup",
|
||||||
etudid=etudid,
|
etudid=etudid,
|
||||||
msg=f"""formsemestre_id={partition.formsemestre.id},partition_name={
|
msg="formsemestre_id=%s,partition_name=%s, group_name=%s"
|
||||||
partition.partition_name}, group_name={group.group_name}""",
|
% (formsemestre_id, partition["partition_name"], group["group_name"]),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Supprime les groupes indiqués comme supprimés:
|
# Supprime les groupes indiqués comme supprimés:
|
||||||
@ -792,10 +819,10 @@ def setGroups(
|
|||||||
return xml_error(msg, code=404)
|
return xml_error(msg, code=404)
|
||||||
# Place dans ce groupe les etudiants indiqués:
|
# Place dans ce groupe les etudiants indiqués:
|
||||||
for etudid in fs[1:-1]:
|
for etudid in fs[1:-1]:
|
||||||
change_etud_group_in_partition(etudid, group)
|
change_etud_group_in_partition(etudid, group.id, partition)
|
||||||
|
|
||||||
# Update parcours
|
# Update parcours
|
||||||
partition.formsemestre.update_inscriptions_parcours_from_groups()
|
formsemestre.update_inscriptions_parcours_from_groups()
|
||||||
|
|
||||||
data = (
|
data = (
|
||||||
'<?xml version="1.0" encoding="utf-8"?><response>Groupes enregistrés</response>'
|
'<?xml version="1.0" encoding="utf-8"?><response>Groupes enregistrés</response>'
|
||||||
@ -808,7 +835,6 @@ def setGroups(
|
|||||||
def create_group(partition_id, group_name="", default=False) -> GroupDescr:
|
def create_group(partition_id, group_name="", default=False) -> GroupDescr:
|
||||||
"""Create a new group in this partition.
|
"""Create a new group in this partition.
|
||||||
If default, create default partition (with no name)
|
If default, create default partition (with no name)
|
||||||
Obsolete: utiliser Partition.create_group
|
|
||||||
"""
|
"""
|
||||||
partition = Partition.query.get_or_404(partition_id)
|
partition = Partition.query.get_or_404(partition_id)
|
||||||
if not sco_permissions_check.can_change_groups(partition.formsemestre_id):
|
if not sco_permissions_check.can_change_groups(partition.formsemestre_id):
|
||||||
@ -830,7 +856,7 @@ def create_group(partition_id, group_name="", default=False) -> GroupDescr:
|
|||||||
group = GroupDescr(partition=partition, group_name=group_name, numero=new_numero)
|
group = GroupDescr(partition=partition, group_name=group_name, numero=new_numero)
|
||||||
db.session.add(group)
|
db.session.add(group)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
log(f"create_group: created group_id={group.id}")
|
log("create_group: created group_id={group.id}")
|
||||||
#
|
#
|
||||||
return group
|
return group
|
||||||
|
|
||||||
@ -950,20 +976,10 @@ def edit_partition_form(formsemestre_id=None):
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
""",
|
""",
|
||||||
f"""<h2>Partitions du semestre</h2>
|
r"""<h2>Partitions du semestre</h2>
|
||||||
<p class="help">
|
|
||||||
👉💡 vous pourriez essayer <a href="{
|
|
||||||
url_for("scolar.partition_editor",
|
|
||||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
|
|
||||||
}" class="stdlink">le nouvel éditeur</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<form name="editpart" id="editpart" method="POST" action="partition_create">
|
<form name="editpart" id="editpart" method="POST" action="partition_create">
|
||||||
<div id="epmsg"></div>
|
<div id="epmsg"></div>
|
||||||
<table>
|
<table><tr class="eptit"><th></th><th></th><th></th><th>Partition</th><th>Groupes</th><th></th><th></th><th></th></tr>
|
||||||
<tr class="eptit">
|
|
||||||
<th></th><th></th><th></th><th>Partition</th><th>Groupes</th><th></th><th></th><th></th>
|
|
||||||
</tr>
|
|
||||||
""",
|
""",
|
||||||
]
|
]
|
||||||
i = 0
|
i = 0
|
||||||
@ -1384,16 +1400,14 @@ def groups_auto_repartition(partition_id=None):
|
|||||||
"""Reparti les etudiants dans des groupes dans une partition, en respectant le niveau
|
"""Reparti les etudiants dans des groupes dans une partition, en respectant le niveau
|
||||||
et la mixité.
|
et la mixité.
|
||||||
"""
|
"""
|
||||||
partition: Partition = Partition.query.get_or_404(partition_id)
|
partition = get_partition(partition_id)
|
||||||
if not partition.groups_editable:
|
if not partition["groups_editable"]:
|
||||||
raise AccessDenied("Partition non éditable")
|
raise AccessDenied("Partition non éditable")
|
||||||
formsemestre_id = partition.formsemestre_id
|
formsemestre_id = partition["formsemestre_id"]
|
||||||
formsemestre = partition.formsemestre
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||||
# renvoie sur page édition partitions et groupes
|
# renvoie sur page édition groupes
|
||||||
dest_url = url_for(
|
dest_url = url_for(
|
||||||
"scolar.partition_editor",
|
"scolar.affect_groups", scodoc_dept=g.scodoc_dept, partition_id=partition_id
|
||||||
scodoc_dept=g.scodoc_dept,
|
|
||||||
formsemestre_id=formsemestre_id,
|
|
||||||
)
|
)
|
||||||
if not sco_permissions_check.can_change_groups(formsemestre_id):
|
if not sco_permissions_check.can_change_groups(formsemestre_id):
|
||||||
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
||||||
@ -1413,14 +1427,12 @@ def groups_auto_repartition(partition_id=None):
|
|||||||
|
|
||||||
H = [
|
H = [
|
||||||
html_sco_header.sco_header(page_title="Répartition des groupes"),
|
html_sco_header.sco_header(page_title="Répartition des groupes"),
|
||||||
f"""<h2>Répartition des groupes de {partition.partition_name}</h2>
|
"<h2>Répartition des groupes de %s</h2>" % partition["partition_name"],
|
||||||
<p>Semestre {formsemestre.titre_annee()}</p>
|
f"<p>Semestre {formsemestre.titre_annee()}</p>",
|
||||||
<p class="help">Les groupes existants seront <b>effacés</b> et remplacés par
|
"""<p class="help">Les groupes existants seront <b>effacés</b> et remplacés par
|
||||||
ceux créés ici. La répartition aléatoire tente d'uniformiser le niveau
|
ceux créés ici. La répartition aléatoire tente d'uniformiser le niveau
|
||||||
des groupes (en utilisant la dernière moyenne générale disponible pour
|
des groupes (en utilisant la dernière moyenne générale disponible pour
|
||||||
chaque étudiant) et de maximiser la mixité de chaque groupe.
|
chaque étudiant) et de maximiser la mixité de chaque groupe.</p>""",
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
tf = TrivialFormulator(
|
tf = TrivialFormulator(
|
||||||
@ -1438,23 +1450,25 @@ def groups_auto_repartition(partition_id=None):
|
|||||||
return flask.redirect(dest_url)
|
return flask.redirect(dest_url)
|
||||||
else:
|
else:
|
||||||
# form submission
|
# form submission
|
||||||
log(f"groups_auto_repartition({partition})")
|
log(
|
||||||
group_names = tf[2]["groupNames"]
|
"groups_auto_repartition( partition_id=%s partition_name=%s"
|
||||||
group_names = sorted({x.strip() for x in group_names.split(",")})
|
% (partition_id, partition["partition_name"])
|
||||||
|
)
|
||||||
|
groupNames = tf[2]["groupNames"]
|
||||||
|
group_names = sorted(set([x.strip() for x in groupNames.split(",")]))
|
||||||
# Détruit les groupes existant de cette partition
|
# Détruit les groupes existant de cette partition
|
||||||
for group in partition.groups:
|
for old_group in get_partition_groups(partition):
|
||||||
db.session.delete(group)
|
group_delete(old_group["group_id"])
|
||||||
db.session.commit()
|
|
||||||
# Crée les nouveaux groupes
|
# Crée les nouveaux groupes
|
||||||
groups = []
|
group_ids = []
|
||||||
for group_name in group_names:
|
for group_name in group_names:
|
||||||
if group_name.strip():
|
if group_name.strip():
|
||||||
groups.append(partition.create_group(group_name))
|
group_ids.append(create_group(partition_id, group_name).id)
|
||||||
#
|
#
|
||||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
identdict = nt.identdict
|
identdict = nt.identdict
|
||||||
# build: { civilite : liste etudids trie par niveau croissant }
|
# build: { civilite : liste etudids trie par niveau croissant }
|
||||||
civilites = {x["civilite"] for x in identdict.values()}
|
civilites = set([x["civilite"] for x in identdict.values()])
|
||||||
listes = {}
|
listes = {}
|
||||||
for civilite in civilites:
|
for civilite in civilites:
|
||||||
listes[civilite] = [
|
listes[civilite] = [
|
||||||
@ -1467,19 +1481,16 @@ def groups_auto_repartition(partition_id=None):
|
|||||||
# affect aux groupes:
|
# affect aux groupes:
|
||||||
n = len(identdict)
|
n = len(identdict)
|
||||||
igroup = 0
|
igroup = 0
|
||||||
nbgroups = len(groups)
|
nbgroups = len(group_ids)
|
||||||
while n > 0:
|
while n > 0:
|
||||||
log(f"n={n}")
|
|
||||||
for civilite in civilites:
|
for civilite in civilites:
|
||||||
log(f"civilite={civilite}")
|
|
||||||
if len(listes[civilite]):
|
if len(listes[civilite]):
|
||||||
n -= 1
|
n -= 1
|
||||||
etudid = listes[civilite].pop()[1]
|
etudid = listes[civilite].pop()[1]
|
||||||
group = groups[igroup]
|
group_id = group_ids[igroup]
|
||||||
igroup = (igroup + 1) % nbgroups
|
igroup = (igroup + 1) % nbgroups
|
||||||
log(f"in {etudid} in group {group.id}")
|
change_etud_group_in_partition(etudid, group_id, partition)
|
||||||
change_etud_group_in_partition(etudid, group)
|
log("%s in group %s" % (etudid, group_id))
|
||||||
log(f"{etudid} in group {group.id}")
|
|
||||||
return flask.redirect(dest_url)
|
return flask.redirect(dest_url)
|
||||||
|
|
||||||
|
|
||||||
@ -1487,13 +1498,15 @@ def _get_prev_moy(etudid, formsemestre_id):
|
|||||||
"""Donne la derniere moyenne generale calculee pour cette étudiant,
|
"""Donne la derniere moyenne generale calculee pour cette étudiant,
|
||||||
ou 0 si on n'en trouve pas (nouvel inscrit,...).
|
ou 0 si on n'en trouve pas (nouvel inscrit,...).
|
||||||
"""
|
"""
|
||||||
|
from app.scodoc import sco_cursus_dut
|
||||||
|
|
||||||
info = sco_etud.get_etud_info(etudid=etudid, filled=True)
|
info = sco_etud.get_etud_info(etudid=etudid, filled=True)
|
||||||
if not info:
|
if not info:
|
||||||
raise ScoValueError("etudiant invalide: etudid=%s" % etudid)
|
raise ScoValueError("etudiant invalide: etudid=%s" % etudid)
|
||||||
etud = info[0]
|
etud = info[0]
|
||||||
Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
|
Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
|
||||||
if Se.prev:
|
if Se.prev:
|
||||||
prev_sem = db.session.get(FormSemestre, Se.prev["formsemestre_id"])
|
prev_sem = FormSemestre.query.get(Se.prev["formsemestre_id"])
|
||||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(prev_sem)
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(prev_sem)
|
||||||
return nt.get_etud_moy_gen(etudid)
|
return nt.get_etud_moy_gen(etudid)
|
||||||
else:
|
else:
|
||||||
@ -1507,11 +1520,10 @@ def create_etapes_partition(formsemestre_id, partition_name="apo_etapes"):
|
|||||||
Si la partition existe déjà, ses groupes sont mis à jour (les groupes devenant
|
Si la partition existe déjà, ses groupes sont mis à jour (les groupes devenant
|
||||||
vides ne sont pas supprimés).
|
vides ne sont pas supprimés).
|
||||||
"""
|
"""
|
||||||
# A RE-ECRIRE pour utiliser les modèles.
|
|
||||||
from app.scodoc import sco_formsemestre_inscriptions
|
from app.scodoc import sco_formsemestre_inscriptions
|
||||||
|
|
||||||
partition_name = str(partition_name)
|
partition_name = str(partition_name)
|
||||||
log(f"create_etapes_partition({formsemestre_id})")
|
log("create_etapes_partition(%s)" % formsemestre_id)
|
||||||
ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
|
ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
|
||||||
args={"formsemestre_id": formsemestre_id}
|
args={"formsemestre_id": formsemestre_id}
|
||||||
)
|
)
|
||||||
@ -1530,17 +1542,20 @@ def create_etapes_partition(formsemestre_id, partition_name="apo_etapes"):
|
|||||||
pid = partition_create(
|
pid = partition_create(
|
||||||
formsemestre_id, partition_name=partition_name, redirect=False
|
formsemestre_id, partition_name=partition_name, redirect=False
|
||||||
)
|
)
|
||||||
partition: Partition = db.session.get(Partition, pid)
|
partition = get_partition(pid)
|
||||||
groups = partition.groups
|
groups = get_partition_groups(partition)
|
||||||
groups_by_names = {g.group_name: g for g in groups}
|
groups_by_names = {g["group_name"]: g for g in groups}
|
||||||
for etape in etapes:
|
for etape in etapes:
|
||||||
if etape not in groups_by_names:
|
if not (etape in groups_by_names):
|
||||||
new_group = create_group(pid, etape)
|
new_group = create_group(pid, etape)
|
||||||
groups_by_names[etape] = new_group
|
g = get_group(new_group.id) # XXX transition: recupere old style dict
|
||||||
|
groups_by_names[etape] = g
|
||||||
# Place les etudiants dans les groupes
|
# Place les etudiants dans les groupes
|
||||||
for i in ins:
|
for i in ins:
|
||||||
if i["etape"]:
|
if i["etape"]:
|
||||||
change_etud_group_in_partition(i["etudid"], groups_by_names[i["etape"]])
|
change_etud_group_in_partition(
|
||||||
|
i["etudid"], groups_by_names[i["etape"]]["group_id"], partition
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def do_evaluation_listeetuds_groups(
|
def do_evaluation_listeetuds_groups(
|
||||||
|
@ -36,12 +36,16 @@ import time
|
|||||||
|
|
||||||
from flask import g, url_for
|
from flask import g, url_for
|
||||||
|
|
||||||
from app import db, log
|
import app.scodoc.sco_utils as scu
|
||||||
|
import app.scodoc.notesdb as ndb
|
||||||
|
from app import log
|
||||||
from app.models import ScolarNews, GroupDescr
|
from app.models import ScolarNews, GroupDescr
|
||||||
from app.models.etudiants import input_civilite
|
from app.models.etudiants import input_civilite
|
||||||
|
|
||||||
from app.scodoc.gen_tables import GenTable
|
|
||||||
from app.scodoc.sco_excel import COLORS
|
from app.scodoc.sco_excel import COLORS
|
||||||
|
from app.scodoc.sco_formsemestre_inscriptions import (
|
||||||
|
do_formsemestre_inscription_with_modules,
|
||||||
|
)
|
||||||
|
from app.scodoc.gen_tables import GenTable
|
||||||
from app.scodoc.sco_exceptions import (
|
from app.scodoc.sco_exceptions import (
|
||||||
AccessDenied,
|
AccessDenied,
|
||||||
ScoFormatError,
|
ScoFormatError,
|
||||||
@ -51,6 +55,7 @@ from app.scodoc.sco_exceptions import (
|
|||||||
ScoLockedFormError,
|
ScoLockedFormError,
|
||||||
ScoGenError,
|
ScoGenError,
|
||||||
)
|
)
|
||||||
|
|
||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
from app.scodoc import sco_etud
|
from app.scodoc import sco_etud
|
||||||
@ -58,11 +63,6 @@ from app.scodoc import sco_groups
|
|||||||
from app.scodoc import sco_excel
|
from app.scodoc import sco_excel
|
||||||
from app.scodoc import sco_groups_view
|
from app.scodoc import sco_groups_view
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
import app.scodoc.notesdb as ndb
|
|
||||||
from app.scodoc.sco_formsemestre_inscriptions import (
|
|
||||||
do_formsemestre_inscription_with_modules,
|
|
||||||
)
|
|
||||||
import app.scodoc.sco_utils as scu
|
|
||||||
|
|
||||||
# format description (in tools/)
|
# format description (in tools/)
|
||||||
FORMAT_FILE = "format_import_etudiants.txt"
|
FORMAT_FILE = "format_import_etudiants.txt"
|
||||||
@ -480,7 +480,6 @@ def scolars_import_excel_file(
|
|||||||
text="Inscription de %d étudiants" # peuvent avoir ete inscrits a des semestres differents
|
text="Inscription de %d étudiants" # peuvent avoir ete inscrits a des semestres differents
|
||||||
% len(created_etudids),
|
% len(created_etudids),
|
||||||
obj=formsemestre_id,
|
obj=formsemestre_id,
|
||||||
max_frequency=0,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
log("scolars_import_excel_file: completing transaction")
|
log("scolars_import_excel_file: completing transaction")
|
||||||
@ -639,10 +638,10 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None
|
|||||||
fields = adm_get_fields(titles, formsemestre_id)
|
fields = adm_get_fields(titles, formsemestre_id)
|
||||||
idx_nom = None
|
idx_nom = None
|
||||||
idx_prenom = None
|
idx_prenom = None
|
||||||
for idx, field in fields.items():
|
for idx in fields:
|
||||||
if field[0] == "nom":
|
if fields[idx][0] == "nom":
|
||||||
idx_nom = idx
|
idx_nom = idx
|
||||||
if field[0] == "prenom":
|
if fields[idx][0] == "prenom":
|
||||||
idx_prenom = idx
|
idx_prenom = idx
|
||||||
if (idx_nom is None) or (idx_prenom is None):
|
if (idx_nom is None) or (idx_prenom is None):
|
||||||
log("fields indices=" + ", ".join([str(x) for x in fields]))
|
log("fields indices=" + ", ".join([str(x) for x in fields]))
|
||||||
@ -664,20 +663,21 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None
|
|||||||
# Retrouve l'étudiant parmi ceux du semestre par (nom, prenom)
|
# Retrouve l'étudiant parmi ceux du semestre par (nom, prenom)
|
||||||
nom = adm_normalize_string(line[idx_nom])
|
nom = adm_normalize_string(line[idx_nom])
|
||||||
prenom = adm_normalize_string(line[idx_prenom])
|
prenom = adm_normalize_string(line[idx_prenom])
|
||||||
if (nom, prenom) not in etuds_by_nomprenom:
|
if not (nom, prenom) in etuds_by_nomprenom:
|
||||||
msg = f"""Étudiant <b>{line[idx_nom]} {line[idx_prenom]} inexistant</b>"""
|
log(
|
||||||
diag.append(msg)
|
"unable to find %s %s among members" % (line[idx_nom], line[idx_prenom])
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
etud = etuds_by_nomprenom[(nom, prenom)]
|
etud = etuds_by_nomprenom[(nom, prenom)]
|
||||||
cur_adm = sco_etud.admission_list(cnx, args={"etudid": etud["etudid"]})[0]
|
cur_adm = sco_etud.admission_list(cnx, args={"etudid": etud["etudid"]})[0]
|
||||||
# peuple les champs presents dans le tableau
|
# peuple les champs presents dans le tableau
|
||||||
args = {}
|
args = {}
|
||||||
for idx, field in fields.items():
|
for idx in fields:
|
||||||
field_name, convertor = field
|
field_name, convertor = fields[idx]
|
||||||
if field_name in modifiable_fields:
|
if field_name in modifiable_fields:
|
||||||
try:
|
try:
|
||||||
val = convertor(line[idx])
|
val = convertor(line[idx])
|
||||||
except ValueError as exc:
|
except ValueError:
|
||||||
raise ScoFormatError(
|
raise ScoFormatError(
|
||||||
'scolars_import_admission: valeur invalide, ligne %d colonne %s: "%s"'
|
'scolars_import_admission: valeur invalide, ligne %d colonne %s: "%s"'
|
||||||
% (nline, field_name, line[idx]),
|
% (nline, field_name, line[idx]),
|
||||||
@ -686,7 +686,7 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None
|
|||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
formsemestre_id=formsemestre_id,
|
formsemestre_id=formsemestre_id,
|
||||||
),
|
),
|
||||||
) from exc
|
)
|
||||||
if val is not None: # note: ne peut jamais supprimer une valeur
|
if val is not None: # note: ne peut jamais supprimer une valeur
|
||||||
args[field_name] = val
|
args[field_name] = val
|
||||||
if args:
|
if args:
|
||||||
@ -719,10 +719,10 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None
|
|||||||
)
|
)
|
||||||
|
|
||||||
for group_id in group_ids:
|
for group_id in group_ids:
|
||||||
group = db.session.get(GroupDescr, group_id)
|
group = GroupDescr.query.get(group_id)
|
||||||
if group.partition.groups_editable:
|
if group.partition.groups_editable:
|
||||||
sco_groups.change_etud_group_in_partition(
|
sco_groups.change_etud_group_in_partition(
|
||||||
args["etudid"], group
|
args["etudid"], group_id
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
log("scolars_import_admission: partition non editable")
|
log("scolars_import_admission: partition non editable")
|
||||||
|
@ -35,13 +35,14 @@ from flask import url_for, g, request
|
|||||||
|
|
||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app import db, log
|
from app import log
|
||||||
from app.models import Formation, FormSemestre, GroupDescr
|
from app.models import Formation, FormSemestre
|
||||||
from app.scodoc.gen_tables import GenTable
|
from app.scodoc.gen_tables import GenTable
|
||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
from app.scodoc import codes_cursus
|
from app.scodoc import codes_cursus
|
||||||
from app.scodoc import sco_etud
|
from app.scodoc import sco_etud
|
||||||
|
from app.scodoc import sco_formations
|
||||||
from app.scodoc import sco_formsemestre
|
from app.scodoc import sco_formsemestre
|
||||||
from app.scodoc import sco_formsemestre_inscriptions
|
from app.scodoc import sco_formsemestre_inscriptions
|
||||||
from app.scodoc import sco_groups
|
from app.scodoc import sco_groups
|
||||||
@ -176,8 +177,7 @@ def do_inscrit(sem, etudids, inscrit_groupes=False):
|
|||||||
(la liste doit avoir été vérifiée au préalable)
|
(la liste doit avoir été vérifiée au préalable)
|
||||||
En option: inscrit aux mêmes groupes que dans le semestre origine
|
En option: inscrit aux mêmes groupes que dans le semestre origine
|
||||||
"""
|
"""
|
||||||
# TODO à ré-écrire pour utiliser le smodèle, notamment GroupDescr
|
formsemestre: FormSemestre = FormSemestre.query.get(sem["formsemestre_id"])
|
||||||
formsemestre: FormSemestre = db.session.get(FormSemestre, sem["formsemestre_id"])
|
|
||||||
formsemestre.setup_parcours_groups()
|
formsemestre.setup_parcours_groups()
|
||||||
log(f"do_inscrit (inscrit_groupes={inscrit_groupes}): {etudids}")
|
log(f"do_inscrit (inscrit_groupes={inscrit_groupes}): {etudids}")
|
||||||
for etudid in etudids:
|
for etudid in etudids:
|
||||||
@ -220,10 +220,11 @@ def do_inscrit(sem, etudids, inscrit_groupes=False):
|
|||||||
|
|
||||||
# Inscrit aux groupes
|
# Inscrit aux groupes
|
||||||
for partition_group in partition_groups:
|
for partition_group in partition_groups:
|
||||||
group: GroupDescr = db.session.get(
|
sco_groups.change_etud_group_in_partition(
|
||||||
GroupDescr, partition_group["group_id"]
|
etudid,
|
||||||
|
partition_group["group_id"],
|
||||||
|
partition_group,
|
||||||
)
|
)
|
||||||
sco_groups.change_etud_group_in_partition(etudid, group)
|
|
||||||
|
|
||||||
|
|
||||||
def do_desinscrit(sem, etudids):
|
def do_desinscrit(sem, etudids):
|
||||||
@ -415,10 +416,10 @@ def formsemestre_inscr_passage(
|
|||||||
): # il y a au moins une vraie partition
|
): # il y a au moins une vraie partition
|
||||||
H.append(
|
H.append(
|
||||||
f"""<li><a class="stdlink" href="{
|
f"""<li><a class="stdlink" href="{
|
||||||
url_for("scolar.partition_editor", scodoc_dept=g.scodoc_dept,
|
url_for("scolar.affect_groups",
|
||||||
formsemestre_id=formsemestre_id)
|
scodoc_dept=g.scodoc_dept, partition_id=partition["partition_id"])
|
||||||
}">Répartir les groupes de {partition["partition_name"]}</a></li>
|
}">Répartir les groupes de {partition["partition_name"]}</a></li>
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -435,7 +436,7 @@ def _build_page(
|
|||||||
inscrit_groupes=False,
|
inscrit_groupes=False,
|
||||||
ignore_jury=False,
|
ignore_jury=False,
|
||||||
):
|
):
|
||||||
formsemestre: FormSemestre = db.session.get(FormSemestre, sem["formsemestre_id"])
|
formsemestre: FormSemestre = FormSemestre.query.get(sem["formsemestre_id"])
|
||||||
inscrit_groupes = int(inscrit_groupes)
|
inscrit_groupes = int(inscrit_groupes)
|
||||||
ignore_jury = int(ignore_jury)
|
ignore_jury = int(ignore_jury)
|
||||||
if inscrit_groupes:
|
if inscrit_groupes:
|
||||||
|
@ -33,7 +33,7 @@ import numpy as np
|
|||||||
import flask
|
import flask
|
||||||
from flask import url_for, g, request
|
from flask import url_for, g, request
|
||||||
|
|
||||||
from app import db, log
|
from app import log
|
||||||
from app import models
|
from app import models
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
from app.comp import moy_mod
|
from app.comp import moy_mod
|
||||||
@ -79,7 +79,7 @@ def do_evaluation_listenotes(
|
|||||||
return "<p>Aucune évaluation !</p>", "ScoDoc"
|
return "<p>Aucune évaluation !</p>", "ScoDoc"
|
||||||
|
|
||||||
E = evals[0] # il y a au moins une evaluation
|
E = evals[0] # il y a au moins une evaluation
|
||||||
modimpl = db.session.get(ModuleImpl, E["moduleimpl_id"])
|
modimpl = ModuleImpl.query.get(E["moduleimpl_id"])
|
||||||
# description de l'evaluation
|
# description de l'evaluation
|
||||||
if mode == "eval":
|
if mode == "eval":
|
||||||
H = [sco_evaluations.evaluation_describe(evaluation_id=evaluation_id)]
|
H = [sco_evaluations.evaluation_describe(evaluation_id=evaluation_id)]
|
||||||
@ -624,7 +624,7 @@ def _make_table_notes(
|
|||||||
]
|
]
|
||||||
commentkeys = list(key_mgr.items()) # [ (comment, key), ... ]
|
commentkeys = list(key_mgr.items()) # [ (comment, key), ... ]
|
||||||
commentkeys.sort(key=lambda x: int(x[1]))
|
commentkeys.sort(key=lambda x: int(x[1]))
|
||||||
for comment, key in commentkeys:
|
for (comment, key) in commentkeys:
|
||||||
C.append(
|
C.append(
|
||||||
'<span class="colcomment">(%s)</span> <em>%s</em><br>' % (key, comment)
|
'<span class="colcomment">(%s)</span> <em>%s</em><br>' % (key, comment)
|
||||||
)
|
)
|
||||||
@ -673,7 +673,7 @@ def _add_eval_columns(
|
|||||||
sum_notes = 0
|
sum_notes = 0
|
||||||
notes = [] # liste des notes numeriques, pour calcul histogramme uniquement
|
notes = [] # liste des notes numeriques, pour calcul histogramme uniquement
|
||||||
evaluation_id = e["evaluation_id"]
|
evaluation_id = e["evaluation_id"]
|
||||||
e_o = db.session.get(Evaluation, evaluation_id) # XXX en attendant ré-écriture
|
e_o = Evaluation.query.get(evaluation_id) # XXX en attendant ré-écriture
|
||||||
inscrits = e_o.moduleimpl.formsemestre.etudids_actifs # set d'etudids
|
inscrits = e_o.moduleimpl.formsemestre.etudids_actifs # set d'etudids
|
||||||
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
|
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
|
||||||
|
|
||||||
|
@ -31,16 +31,15 @@
|
|||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
import psycopg2
|
import psycopg2
|
||||||
|
|
||||||
from app import db
|
|
||||||
|
|
||||||
from app.models import Formation
|
|
||||||
from app.scodoc import scolog
|
|
||||||
from app.scodoc import sco_formsemestre
|
|
||||||
from app.scodoc import sco_cache
|
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
from app.scodoc.sco_exceptions import ScoValueError, AccessDenied
|
from app.scodoc.sco_exceptions import ScoValueError, AccessDenied
|
||||||
|
from app import log
|
||||||
|
from app import models
|
||||||
|
from app.scodoc import scolog
|
||||||
|
from app.scodoc import sco_formsemestre
|
||||||
|
from app.scodoc import sco_cache
|
||||||
|
|
||||||
# --- Gestion des "Implémentations de Modules"
|
# --- Gestion des "Implémentations de Modules"
|
||||||
# Un "moduleimpl" correspond a la mise en oeuvre d'un module
|
# Un "moduleimpl" correspond a la mise en oeuvre d'un module
|
||||||
@ -171,7 +170,7 @@ def moduleimpl_withmodule_list(
|
|||||||
mi["matiere"] = matieres[matiere_id]
|
mi["matiere"] = matieres[matiere_id]
|
||||||
|
|
||||||
mod = modimpls[0]["module"]
|
mod = modimpls[0]["module"]
|
||||||
formation = db.session.get(Formation, mod["formation_id"])
|
formation = models.Formation.query.get(mod["formation_id"])
|
||||||
|
|
||||||
if formation.is_apc():
|
if formation.is_apc():
|
||||||
# tri par numero_module
|
# tri par numero_module
|
||||||
|
@ -28,13 +28,12 @@
|
|||||||
"""Opérations d'inscriptions aux modules (interface pour gérer options ou parcours)
|
"""Opérations d'inscriptions aux modules (interface pour gérer options ou parcours)
|
||||||
"""
|
"""
|
||||||
import collections
|
import collections
|
||||||
from operator import attrgetter
|
from operator import itemgetter
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
from flask import url_for, g, request
|
from flask import url_for, g, request
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
|
||||||
from app import db, log
|
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
from app.comp.res_compat import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
from app.models import (
|
from app.models import (
|
||||||
@ -44,6 +43,9 @@ from app.models import (
|
|||||||
ScolarFormSemestreValidation,
|
ScolarFormSemestreValidation,
|
||||||
UniteEns,
|
UniteEns,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from app import log
|
||||||
|
from app.tables import list_etuds
|
||||||
from app.scodoc.scolog import logdb
|
from app.scodoc.scolog import logdb
|
||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
from app.scodoc import htmlutils
|
from app.scodoc import htmlutils
|
||||||
@ -60,7 +62,6 @@ import app.scodoc.notesdb as ndb
|
|||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app.tables import list_etuds
|
|
||||||
|
|
||||||
|
|
||||||
def moduleimpl_inscriptions_edit(moduleimpl_id, etuds=[], submitted=False):
|
def moduleimpl_inscriptions_edit(moduleimpl_id, etuds=[], submitted=False):
|
||||||
@ -519,7 +520,7 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->
|
|||||||
else set()
|
else set()
|
||||||
)
|
)
|
||||||
ues = sorted(
|
ues = sorted(
|
||||||
(db.session.get(UniteEns, ue_id) for ue_id in ue_ids),
|
(UniteEns.query.get(ue_id) for ue_id in ue_ids),
|
||||||
key=lambda u: (u.numero or 0, u.acronyme),
|
key=lambda u: (u.numero or 0, u.acronyme),
|
||||||
)
|
)
|
||||||
H.append(
|
H.append(
|
||||||
@ -552,11 +553,8 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->
|
|||||||
>{etud.nomprenom}</a></td>"""
|
>{etud.nomprenom}</a></td>"""
|
||||||
)
|
)
|
||||||
# Parcours:
|
# Parcours:
|
||||||
if partition_parcours:
|
group = partition_parcours.get_etud_group(etud.id)
|
||||||
group = partition_parcours.get_etud_group(etud.id)
|
parcours_name = group.group_name if group else ""
|
||||||
parcours_name = group.group_name if group else ""
|
|
||||||
else:
|
|
||||||
parcours_name = ""
|
|
||||||
H.append(f"""<td class="parcours">{parcours_name}</td>""")
|
H.append(f"""<td class="parcours">{parcours_name}</td>""")
|
||||||
# UEs:
|
# UEs:
|
||||||
for ue in ues:
|
for ue in ues:
|
||||||
@ -580,7 +578,7 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->
|
|||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
validations_ue.sort(
|
validations_ue.sort(
|
||||||
key=lambda v: codes_cursus.BUT_CODES_ORDER.get(v.code, 0)
|
key=lambda v: codes_cursus.BUT_CODES_ORDERED.get(v.code, 0)
|
||||||
)
|
)
|
||||||
validation = validations_ue[-1] if validations_ue else None
|
validation = validations_ue[-1] if validations_ue else None
|
||||||
expl_validation = (
|
expl_validation = (
|
||||||
@ -670,7 +668,7 @@ def descr_inscrs_module(moduleimpl_id, set_all, partitions):
|
|||||||
gr.append((partition["partition_name"], grp))
|
gr.append((partition["partition_name"], grp))
|
||||||
#
|
#
|
||||||
d = []
|
d = []
|
||||||
for partition_name, grp in gr:
|
for (partition_name, grp) in gr:
|
||||||
if grp:
|
if grp:
|
||||||
d.append("groupes de %s: %s" % (partition_name, ", ".join(grp)))
|
d.append("groupes de %s: %s" % (partition_name, ", ".join(grp)))
|
||||||
r = []
|
r = []
|
||||||
@ -682,25 +680,25 @@ def descr_inscrs_module(moduleimpl_id, set_all, partitions):
|
|||||||
return False, len(ins), " et ".join(r)
|
return False, len(ins), " et ".join(r)
|
||||||
|
|
||||||
|
|
||||||
def _fmt_etud_set(etudids, max_list_size=7) -> str:
|
def _fmt_etud_set(ins, max_list_size=7):
|
||||||
# max_list_size est le nombre max de noms d'etudiants listés
|
# max_list_size est le nombre max de noms d'etudiants listés
|
||||||
# au delà, on indique juste le nombre, sans les noms.
|
# au delà, on indique juste le nombre, sans les noms.
|
||||||
if len(etudids) > max_list_size:
|
if len(ins) > max_list_size:
|
||||||
return f"{len(etudids)} étudiants"
|
return "%d étudiants" % len(ins)
|
||||||
etuds = []
|
etuds = []
|
||||||
for etudid in etudids:
|
for etudid in ins:
|
||||||
etud = db.session.get(Identite, etudid)
|
etuds.append(sco_etud.get_etud_info(etudid=etudid, filled=True)[0])
|
||||||
if etud:
|
etuds.sort(key=itemgetter("nom"))
|
||||||
etuds.append(etud)
|
|
||||||
|
|
||||||
return ", ".join(
|
return ", ".join(
|
||||||
[
|
[
|
||||||
f"""<a class="discretelink" href="{
|
'<a class="discretelink" href="%s">%s</a>'
|
||||||
|
% (
|
||||||
url_for(
|
url_for(
|
||||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id
|
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
|
||||||
)
|
),
|
||||||
}">{etud.nomprenom}</a>"""
|
etud["nomprenom"],
|
||||||
for etud in sorted(etuds, key=attrgetter("sort_key"))
|
)
|
||||||
|
for etud in etuds
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -57,7 +57,6 @@ from app.scodoc import sco_moduleimpl
|
|||||||
from app.scodoc import sco_permissions_check
|
from app.scodoc import sco_permissions_check
|
||||||
from app.tables import list_etuds
|
from app.tables import list_etuds
|
||||||
|
|
||||||
|
|
||||||
# menu evaluation dans moduleimpl
|
# menu evaluation dans moduleimpl
|
||||||
def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str:
|
def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str:
|
||||||
"Menu avec actions sur une evaluation"
|
"Menu avec actions sur une evaluation"
|
||||||
@ -227,7 +226,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||||||
)
|
)
|
||||||
arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags()
|
arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags()
|
||||||
#
|
#
|
||||||
module_resp = db.session.get(User, modimpl.responsable_id)
|
module_resp = User.query.get(modimpl.responsable_id)
|
||||||
mod_type_name = scu.MODULE_TYPE_NAMES[module.module_type]
|
mod_type_name = scu.MODULE_TYPE_NAMES[module.module_type]
|
||||||
H = [
|
H = [
|
||||||
html_sco_header.sco_header(
|
html_sco_header.sco_header(
|
||||||
@ -529,7 +528,7 @@ def _ligne_evaluation(
|
|||||||
) -> str:
|
) -> str:
|
||||||
"""Ligne <tr> décrivant une évaluation dans le tableau de bord moduleimpl."""
|
"""Ligne <tr> décrivant une évaluation dans le tableau de bord moduleimpl."""
|
||||||
H = []
|
H = []
|
||||||
# evaluation: Evaluation = db.session.get(Evaluation, eval_dict["evaluation_id"])
|
# evaluation: Evaluation = Evaluation.query.get(eval_dict["evaluation_id"])
|
||||||
etat = sco_evaluations.do_evaluation_etat(
|
etat = sco_evaluations.do_evaluation_etat(
|
||||||
evaluation.id,
|
evaluation.id,
|
||||||
partition_id=partition_id,
|
partition_id=partition_id,
|
||||||
@ -733,7 +732,7 @@ def _ligne_evaluation(
|
|||||||
)
|
)
|
||||||
if etat["moy"]:
|
if etat["moy"]:
|
||||||
H.append(
|
H.append(
|
||||||
f"""<b>{etat["moy"]} / 20</b>
|
f"""<b>{etat["moy"]} / {evaluation.note_max:g}</b>
|
||||||
(<a class="stdlink" href="{
|
(<a class="stdlink" href="{
|
||||||
url_for('notes.evaluation_listenotes',
|
url_for('notes.evaluation_listenotes',
|
||||||
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id)
|
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id)
|
||||||
@ -838,7 +837,7 @@ def _evaluation_poids_html(evaluation: Evaluation, max_poids: float = 0.0) -> st
|
|||||||
"></div>
|
"></div>
|
||||||
</div>"""
|
</div>"""
|
||||||
for ue, poids in (
|
for ue, poids in (
|
||||||
(db.session.get(UniteEns, ue_id), poids)
|
(UniteEns.query.get(ue_id), poids)
|
||||||
for ue_id, poids in ue_poids.items()
|
for ue_id, poids in ue_poids.items()
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
@ -33,8 +33,10 @@
|
|||||||
from flask import abort, url_for, g, render_template, request
|
from flask import abort, url_for, g, render_template, request
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
|
||||||
from app import db, log
|
import app.scodoc.sco_utils as scu
|
||||||
from app.but import cursus_but
|
import app.scodoc.notesdb as ndb
|
||||||
|
from app import log
|
||||||
|
from app.but import cursus_but, jury_but_view
|
||||||
from app.models.etudiants import Identite, make_etud_args
|
from app.models.etudiants import Identite, make_etud_args
|
||||||
from app.models.formsemestre import FormSemestre
|
from app.models.formsemestre import FormSemestre
|
||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
@ -55,17 +57,13 @@ from app.scodoc.sco_bulletins import etud_descr_situation_semestre
|
|||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
from app.scodoc.sco_formsemestre_validation import formsemestre_recap_parcours_table
|
from app.scodoc.sco_formsemestre_validation import formsemestre_recap_parcours_table
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
import app.scodoc.sco_utils as scu
|
|
||||||
import app.scodoc.notesdb as ndb
|
|
||||||
|
|
||||||
|
|
||||||
def _menu_scolarite(
|
def _menu_scolarite(authuser, sem: dict, etudid: int):
|
||||||
authuser, formsemestre: FormSemestre, etudid: int, etat_inscription: str
|
|
||||||
):
|
|
||||||
"""HTML pour menu "scolarite" pour un etudiant dans un semestre.
|
"""HTML pour menu "scolarite" pour un etudiant dans un semestre.
|
||||||
Le contenu du menu depend des droits de l'utilisateur et de l'état de l'étudiant.
|
Le contenu du menu depend des droits de l'utilisateur et de l'état de l'étudiant.
|
||||||
"""
|
"""
|
||||||
locked = not formsemestre.etat
|
locked = not sem["etat"]
|
||||||
if locked:
|
if locked:
|
||||||
lockicon = scu.icontag("lock32_img", title="verrouillé", border="0")
|
lockicon = scu.icontag("lock32_img", title="verrouillé", border="0")
|
||||||
return lockicon # no menu
|
return lockicon # no menu
|
||||||
@ -73,10 +71,10 @@ def _menu_scolarite(
|
|||||||
Permission.ScoEtudInscrit
|
Permission.ScoEtudInscrit
|
||||||
) and not authuser.has_permission(Permission.ScoEtudChangeGroups):
|
) and not authuser.has_permission(Permission.ScoEtudChangeGroups):
|
||||||
return "" # no menu
|
return "" # no menu
|
||||||
|
ins = sem["ins"]
|
||||||
|
args = {"etudid": etudid, "formsemestre_id": ins["formsemestre_id"]}
|
||||||
|
|
||||||
args = {"etudid": etudid, "formsemestre_id": formsemestre.id}
|
if ins["etat"] != "D":
|
||||||
|
|
||||||
if etat_inscription != scu.DEMISSION:
|
|
||||||
dem_title = "Démission"
|
dem_title = "Démission"
|
||||||
dem_url = "scolar.form_dem"
|
dem_url = "scolar.form_dem"
|
||||||
else:
|
else:
|
||||||
@ -84,14 +82,14 @@ def _menu_scolarite(
|
|||||||
dem_url = "scolar.do_cancel_dem"
|
dem_url = "scolar.do_cancel_dem"
|
||||||
|
|
||||||
# Note: seul un etudiant inscrit (I) peut devenir défaillant.
|
# Note: seul un etudiant inscrit (I) peut devenir défaillant.
|
||||||
if etat_inscription != codes_cursus.DEF:
|
if ins["etat"] != codes_cursus.DEF:
|
||||||
def_title = "Déclarer défaillance"
|
def_title = "Déclarer défaillance"
|
||||||
def_url = "scolar.form_def"
|
def_url = "scolar.form_def"
|
||||||
elif etat_inscription == codes_cursus.DEF:
|
elif ins["etat"] == codes_cursus.DEF:
|
||||||
def_title = "Annuler la défaillance"
|
def_title = "Annuler la défaillance"
|
||||||
def_url = "scolar.do_cancel_def"
|
def_url = "scolar.do_cancel_def"
|
||||||
def_enabled = (
|
def_enabled = (
|
||||||
(etat_inscription != scu.DEMISSION)
|
(ins["etat"] != "D")
|
||||||
and authuser.has_permission(Permission.ScoEtudInscrit)
|
and authuser.has_permission(Permission.ScoEtudInscrit)
|
||||||
and not locked
|
and not locked
|
||||||
)
|
)
|
||||||
@ -130,12 +128,6 @@ def _menu_scolarite(
|
|||||||
"enabled": authuser.has_permission(Permission.ScoEtudInscrit)
|
"enabled": authuser.has_permission(Permission.ScoEtudInscrit)
|
||||||
and not locked,
|
and not locked,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"title": "Gérer les validations d'UEs antérieures",
|
|
||||||
"endpoint": "notes.formsemestre_validate_previous_ue",
|
|
||||||
"args": args,
|
|
||||||
"enabled": formsemestre.can_edit_jury(),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"title": "Inscrire à un autre semestre",
|
"title": "Inscrire à un autre semestre",
|
||||||
"endpoint": "notes.formsemestre_inscription_with_modules_form",
|
"endpoint": "notes.formsemestre_inscription_with_modules_form",
|
||||||
@ -258,10 +250,8 @@ def ficheEtud(etudid=None):
|
|||||||
info["last_formsemestre_id"] = ""
|
info["last_formsemestre_id"] = ""
|
||||||
sem_info = {}
|
sem_info = {}
|
||||||
for sem in info["sems"]:
|
for sem in info["sems"]:
|
||||||
formsemestre: FormSemestre = db.session.get(
|
|
||||||
FormSemestre, sem["formsemestre_id"]
|
|
||||||
)
|
|
||||||
if sem["ins"]["etat"] != scu.INSCRIT:
|
if sem["ins"]["etat"] != scu.INSCRIT:
|
||||||
|
formsemestre: FormSemestre = FormSemestre.query.get(sem["formsemestre_id"])
|
||||||
descr, _ = etud_descr_situation_semestre(
|
descr, _ = etud_descr_situation_semestre(
|
||||||
etudid,
|
etudid,
|
||||||
formsemestre,
|
formsemestre,
|
||||||
@ -293,7 +283,7 @@ def ficheEtud(etudid=None):
|
|||||||
)
|
)
|
||||||
grlink = ", ".join(grlinks)
|
grlink = ", ".join(grlinks)
|
||||||
# infos ajoutées au semestre dans le parcours (groupe, menu)
|
# infos ajoutées au semestre dans le parcours (groupe, menu)
|
||||||
menu = _menu_scolarite(authuser, formsemestre, etudid, sem["ins"]["etat"])
|
menu = _menu_scolarite(authuser, sem, etudid)
|
||||||
if menu:
|
if menu:
|
||||||
sem_info[sem["formsemestre_id"]] = (
|
sem_info[sem["formsemestre_id"]] = (
|
||||||
"<table><tr><td>" + grlink + "</td><td>" + menu + "</td></tr></table>"
|
"<table><tr><td>" + grlink + "</td><td>" + menu + "</td></tr></table>"
|
||||||
@ -313,39 +303,16 @@ def ficheEtud(etudid=None):
|
|||||||
)
|
)
|
||||||
info[
|
info[
|
||||||
"link_bul_pdf"
|
"link_bul_pdf"
|
||||||
] = f"""
|
] = f"""<span class="link_bul_pdf"><a class="stdlink" href="{
|
||||||
<span class="link_bul_pdf">
|
url_for("notes.etud_bulletins_pdf", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||||
<a class="stdlink" href="{
|
}">tous les bulletins</a></span>"""
|
||||||
url_for("notes.etud_bulletins_pdf", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
|
||||||
}">Tous les bulletins</a>
|
|
||||||
</span>
|
|
||||||
"""
|
|
||||||
last_formsemestre: FormSemestre = db.session.get(
|
|
||||||
FormSemestre, info["sems"][0]["formsemestre_id"]
|
|
||||||
)
|
|
||||||
if last_formsemestre.formation.is_apc() and last_formsemestre.semestre_id > 2:
|
|
||||||
info[
|
|
||||||
"link_bul_pdf"
|
|
||||||
] += f"""
|
|
||||||
<span class="link_bul_pdf">
|
|
||||||
<a class="stdlink" href="{
|
|
||||||
url_for("notes.validation_rcues", scodoc_dept=g.scodoc_dept, etudid=etudid, formsemestre_id=last_formsemestre.id)
|
|
||||||
}">Visualiser les compétences BUT</a>
|
|
||||||
</span>
|
|
||||||
"""
|
|
||||||
if authuser.has_permission(Permission.ScoEtudInscrit):
|
if authuser.has_permission(Permission.ScoEtudInscrit):
|
||||||
info[
|
info[
|
||||||
"link_inscrire_ailleurs"
|
"link_inscrire_ailleurs"
|
||||||
] = f"""<span class="link_bul_pdf"><a class="stdlink" href="{
|
] = f"""<span class="link_bul_pdf"><a class="stdlink" href="{
|
||||||
url_for("notes.formsemestre_inscription_with_modules_form",
|
url_for("notes.formsemestre_inscription_with_modules_form",
|
||||||
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||||
}">Inscrire à un autre semestre</a></span>
|
}">inscrire à un autre semestre</a></span>"""
|
||||||
<span class="link_bul_pdf"><a class="stdlink" href="{
|
|
||||||
url_for("notes.jury_delete_manual",
|
|
||||||
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
|
||||||
}">Éditer toutes décisions de jury</a></span>
|
|
||||||
"""
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
info["link_inscrire_ailleurs"] = ""
|
info["link_inscrire_ailleurs"] = ""
|
||||||
else:
|
else:
|
||||||
@ -370,18 +337,17 @@ def ficheEtud(etudid=None):
|
|||||||
if not sco_permissions_check.can_suppress_annotation(a["id"]):
|
if not sco_permissions_check.can_suppress_annotation(a["id"]):
|
||||||
a["dellink"] = ""
|
a["dellink"] = ""
|
||||||
else:
|
else:
|
||||||
a["dellink"] = (
|
a[
|
||||||
'<td class="annodel"><a href="doSuppressAnnotation?etudid=%s&annotation_id=%s">%s</a></td>'
|
"dellink"
|
||||||
% (
|
] = '<td class="annodel"><a href="doSuppressAnnotation?etudid=%s&annotation_id=%s">%s</a></td>' % (
|
||||||
etudid,
|
etudid,
|
||||||
a["id"],
|
a["id"],
|
||||||
scu.icontag(
|
scu.icontag(
|
||||||
"delete_img",
|
"delete_img",
|
||||||
border="0",
|
border="0",
|
||||||
alt="suppress",
|
alt="suppress",
|
||||||
title="Supprimer cette annotation",
|
title="Supprimer cette annotation",
|
||||||
),
|
),
|
||||||
)
|
|
||||||
)
|
)
|
||||||
author = sco_users.user_info(a["author"])
|
author = sco_users.user_info(a["author"])
|
||||||
alist.append(
|
alist.append(
|
||||||
@ -480,7 +446,7 @@ def ficheEtud(etudid=None):
|
|||||||
info[
|
info[
|
||||||
"inscriptions_mkup"
|
"inscriptions_mkup"
|
||||||
] = f"""<div class="ficheinscriptions" id="ficheinscriptions">
|
] = f"""<div class="ficheinscriptions" id="ficheinscriptions">
|
||||||
<div class="fichetitre">Cursus</div>{info["liste_inscriptions"]}
|
<div class="fichetitre">Parcours</div>{info["liste_inscriptions"]}
|
||||||
{info["link_bul_pdf"]} {info["link_inscrire_ailleurs"]}
|
{info["link_bul_pdf"]} {info["link_inscrire_ailleurs"]}
|
||||||
</div>"""
|
</div>"""
|
||||||
|
|
||||||
@ -508,26 +474,11 @@ def ficheEtud(etudid=None):
|
|||||||
last_sem = FormSemestre.query.get_or_404(info["sems"][0]["formsemestre_id"])
|
last_sem = FormSemestre.query.get_or_404(info["sems"][0]["formsemestre_id"])
|
||||||
if last_sem.formation.is_apc():
|
if last_sem.formation.is_apc():
|
||||||
but_cursus = cursus_but.EtudCursusBUT(etud, last_sem.formation)
|
but_cursus = cursus_but.EtudCursusBUT(etud, last_sem.formation)
|
||||||
info[
|
info["but_cursus_mkup"] = render_template(
|
||||||
"but_cursus_mkup"
|
"but/cursus_etud.j2",
|
||||||
] = f"""
|
cursus=but_cursus,
|
||||||
<div class="section_but">
|
scu=scu,
|
||||||
{render_template(
|
)
|
||||||
"but/cursus_etud.j2",
|
|
||||||
cursus=but_cursus,
|
|
||||||
scu=scu,
|
|
||||||
)}
|
|
||||||
<div class="link_validation_rcues">
|
|
||||||
<a href="{url_for("notes.validation_rcues",
|
|
||||||
scodoc_dept=g.scodoc_dept, etudid=etudid,
|
|
||||||
formsemestre_id=last_formsemestre.id)}"
|
|
||||||
title="Visualiser les compétences BUT"
|
|
||||||
>
|
|
||||||
<img src="/ScoDoc/static/icons/parcours-but.png" alt="validation_rcues" height="100px"/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
|
|
||||||
tmpl = """<div class="menus_etud">%(menus_etud)s</div>
|
tmpl = """<div class="menus_etud">%(menus_etud)s</div>
|
||||||
<div class="ficheEtud" id="ficheEtud"><table>
|
<div class="ficheEtud" id="ficheEtud"><table>
|
||||||
|
@ -84,8 +84,6 @@ def SU(s: str) -> str:
|
|||||||
s = html.unescape(s)
|
s = html.unescape(s)
|
||||||
# Remplace les <br> par des <br/>
|
# Remplace les <br> par des <br/>
|
||||||
s = re.sub(r"<br\s*>", "<br/>", s)
|
s = re.sub(r"<br\s*>", "<br/>", s)
|
||||||
# And substitute unicode characters not supported by ReportLab
|
|
||||||
s = s.replace("‐", "-")
|
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
@ -6,7 +6,6 @@
|
|||||||
from flask import g
|
from flask import g
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
|
||||||
from app import db
|
|
||||||
from app.auth.models import User
|
from app.auth.models import User
|
||||||
from app.models import FormSemestre
|
from app.models import FormSemestre
|
||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
@ -132,10 +131,7 @@ def check_access_diretud(formsemestre_id, required_permission=Permission.ScoImpl
|
|||||||
"<h2>Opération non autorisée pour %s</h2>" % current_user,
|
"<h2>Opération non autorisée pour %s</h2>" % current_user,
|
||||||
"<p>Responsable de ce semestre : <b>%s</b></p>"
|
"<p>Responsable de ce semestre : <b>%s</b></p>"
|
||||||
% ", ".join(
|
% ", ".join(
|
||||||
[
|
[User.query.get(i).get_prenomnom() for i in sem["responsables"]]
|
||||||
db.session.get(User, i).get_prenomnom()
|
|
||||||
for i in sem["responsables"]
|
|
||||||
]
|
|
||||||
),
|
),
|
||||||
footer,
|
footer,
|
||||||
]
|
]
|
||||||
@ -146,9 +142,7 @@ def check_access_diretud(formsemestre_id, required_permission=Permission.ScoImpl
|
|||||||
|
|
||||||
|
|
||||||
def can_change_groups(formsemestre_id: int) -> bool:
|
def can_change_groups(formsemestre_id: int) -> bool:
|
||||||
"""Vrai si l'utilisateur peut changer les groupes dans ce semestre
|
"Vrai si l'utilisateur peut changer les groupes dans ce semestre"
|
||||||
Obsolete: utiliser FormSemestre.can_change_groups
|
|
||||||
"""
|
|
||||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||||
if not formsemestre.etat:
|
if not formsemestre.etat:
|
||||||
return False # semestre verrouillé
|
return False # semestre verrouillé
|
||||||
|
4
app/scodoc/sco_photos.py
Normal file → Executable file
4
app/scodoc/sco_photos.py
Normal file → Executable file
@ -148,11 +148,11 @@ def get_photo_image(etudid=None, size="small"):
|
|||||||
filename = photo_pathname(etud.photo_filename, size=size)
|
filename = photo_pathname(etud.photo_filename, size=size)
|
||||||
if not filename:
|
if not filename:
|
||||||
filename = UNKNOWN_IMAGE_PATH
|
filename = UNKNOWN_IMAGE_PATH
|
||||||
r = _http_jpeg_file(filename)
|
r = build_image_response(filename)
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
def _http_jpeg_file(filename):
|
def build_image_response(filename):
|
||||||
"""returns an image as a Flask response"""
|
"""returns an image as a Flask response"""
|
||||||
st = os.stat(filename)
|
st = os.stat(filename)
|
||||||
last_modified = st.st_mtime # float timestamp
|
last_modified = st.st_mtime # float timestamp
|
||||||
|
@ -489,7 +489,6 @@ def _normalize_apo_fields(infolist):
|
|||||||
recode les champs: paiementinscription (-> booleen), datefinalisationinscription (date)
|
recode les champs: paiementinscription (-> booleen), datefinalisationinscription (date)
|
||||||
ajoute le champs 'paiementinscription_str' : 'ok', 'Non' ou '?'
|
ajoute le champs 'paiementinscription_str' : 'ok', 'Non' ou '?'
|
||||||
ajoute les champs 'etape' (= None) et 'prenom' ('') s'ils ne sont pas présents.
|
ajoute les champs 'etape' (= None) et 'prenom' ('') s'ils ne sont pas présents.
|
||||||
ajoute le champ 'civilite_etat_civil' (='X'), et 'prenom_etat_civil' (='') si non présent.
|
|
||||||
"""
|
"""
|
||||||
for infos in infolist:
|
for infos in infolist:
|
||||||
if "paiementinscription" in infos:
|
if "paiementinscription" in infos:
|
||||||
@ -521,15 +520,6 @@ def _normalize_apo_fields(infolist):
|
|||||||
if "prenom" not in infos:
|
if "prenom" not in infos:
|
||||||
infos["prenom"] = ""
|
infos["prenom"] = ""
|
||||||
|
|
||||||
if "civilite_etat_civil" not in infos:
|
|
||||||
infos["civilite_etat_civil"] = "X"
|
|
||||||
|
|
||||||
if "civilite_etat_civil" not in infos:
|
|
||||||
infos["civilite_etat_civil"] = "X"
|
|
||||||
|
|
||||||
if "prenom_etat_civil" not in infos:
|
|
||||||
infos["prenom_etat_civil"] = ""
|
|
||||||
|
|
||||||
return infolist
|
return infolist
|
||||||
|
|
||||||
|
|
||||||
|
@ -111,11 +111,13 @@ get_base_preferences(formsemestre_id)
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import flask
|
import flask
|
||||||
|
|
||||||
from flask import current_app, flash, g, request, url_for
|
from flask import current_app, flash, g, request, url_for
|
||||||
|
|
||||||
from app import db, log
|
|
||||||
from app.models import Departement
|
from app.models import Departement
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
|
from app import log
|
||||||
from app.scodoc.sco_exceptions import ScoValueError, ScoException
|
from app.scodoc.sco_exceptions import ScoValueError, ScoException
|
||||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
@ -272,7 +274,7 @@ class BasePreferences(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, dept_id: int):
|
def __init__(self, dept_id: int):
|
||||||
dept = db.session.get(Departement, dept_id)
|
dept = Departement.query.get(dept_id)
|
||||||
if not dept:
|
if not dept:
|
||||||
raise ScoValueError(f"BasePreferences: Invalid departement: {dept_id}")
|
raise ScoValueError(f"BasePreferences: Invalid departement: {dept_id}")
|
||||||
self.dept_id = dept.id
|
self.dept_id = dept.id
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
"""
|
"""
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
|
|
||||||
from app import db
|
from app import log
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
from app.comp.res_compat import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
from app.models import (
|
from app.models import (
|
||||||
@ -63,32 +63,25 @@ def dict_pvjury(
|
|||||||
Si with_parcours_decisions: ajoute infos sur code decision jury de tous les semestre du parcours
|
Si with_parcours_decisions: ajoute infos sur code decision jury de tous les semestre du parcours
|
||||||
Résultat:
|
Résultat:
|
||||||
{
|
{
|
||||||
'date' : str = date de la decision la plus recente, format dd/mm/yyyy,
|
'date' : date de la decision la plus recente,
|
||||||
'formsemestre' : dict = formsemestre,
|
'formsemestre' : sem,
|
||||||
'is_apc' : bool,
|
'is_apc' : bool,
|
||||||
'formation' : { 'acronyme' :, 'titre': ... }
|
'formation' : { 'acronyme' :, 'titre': ... }
|
||||||
'decisions' : [
|
'decisions' : { [ { 'identite' : {'nom' :, 'prenom':, ...,},
|
||||||
{
|
'etat' : I ou D ou DEF
|
||||||
'identite' : {'nom' :, 'prenom':, ...,},
|
'decision_sem' : {'code':, 'code_prev': },
|
||||||
'etat' : I ou D ou DEF
|
'decisions_ue' : { ue_id : { 'code' : ADM|CMP|AJ, 'event_date' :,
|
||||||
'decision_sem' : {'code':, 'code_prev': },
|
'acronyme', 'numero': } },
|
||||||
'decisions_ue' : {
|
'autorisations' : [ { 'semestre_id' : { ... } } ],
|
||||||
ue_id : {
|
'validation_parcours' : True si parcours validé (diplome obtenu)
|
||||||
'code' : ADM|CMP|AJ,
|
'prev_code' : code (calculé slt si with_prev),
|
||||||
'ects' : float,
|
'mention' : mention (en fct moy gen),
|
||||||
'event_date' :str = "dd/mm/yyyy",
|
'sum_ects' : total ECTS acquis dans ce semestre (incluant les UE capitalisées)
|
||||||
},
|
'sum_ects_capitalises' : somme des ECTS des UE capitalisees
|
||||||
},
|
}
|
||||||
'autorisations' : [ { 'semestre_id' : { ... } } ],
|
]
|
||||||
'validation_parcours' : True si parcours validé (diplome obtenu)
|
},
|
||||||
'prev_code' : code (calculé slt si with_prev),
|
'decisions_dict' : { etudid : decision (comme ci-dessus) },
|
||||||
'mention' : mention (en fct moy gen),
|
|
||||||
'sum_ects' : total ECTS acquis dans ce semestre (incluant les UE capitalisées)
|
|
||||||
'sum_ects_capitalises' : somme des ECTS des UE capitalisees
|
|
||||||
},
|
|
||||||
...
|
|
||||||
],
|
|
||||||
'decisions_dict' : { etudid : decision (comme ci-dessus) },
|
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||||
@ -260,7 +253,7 @@ def _comp_ects_by_ue_code(nt, decisions_ue):
|
|||||||
ects_by_ue_code = {}
|
ects_by_ue_code = {}
|
||||||
for ue_id in decisions_ue:
|
for ue_id in decisions_ue:
|
||||||
d = decisions_ue[ue_id]
|
d = decisions_ue[ue_id]
|
||||||
ue = db.session.get(UniteEns, ue_id)
|
ue = UniteEns.query.get(ue_id)
|
||||||
ects_by_ue_code[ue.ue_code] = d["ects"]
|
ects_by_ue_code[ue.ue_code] = d["ects"]
|
||||||
|
|
||||||
return ects_by_ue_code
|
return ects_by_ue_code
|
||||||
|
@ -42,7 +42,6 @@ from reportlab.platypus import PageBreak, Table, Image
|
|||||||
from reportlab.platypus.doctemplate import BaseDocTemplate
|
from reportlab.platypus.doctemplate import BaseDocTemplate
|
||||||
from reportlab.lib import styles
|
from reportlab.lib import styles
|
||||||
|
|
||||||
from app import db
|
|
||||||
from app.models import FormSemestre, Identite
|
from app.models import FormSemestre, Identite
|
||||||
|
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
@ -71,7 +70,7 @@ def pdf_lettres_individuelles(
|
|||||||
if not dpv:
|
if not dpv:
|
||||||
return ""
|
return ""
|
||||||
#
|
#
|
||||||
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
|
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||||
prefs = sco_preferences.SemPreferences(formsemestre_id)
|
prefs = sco_preferences.SemPreferences(formsemestre_id)
|
||||||
params = {
|
params = {
|
||||||
"date_jury": date_jury,
|
"date_jury": date_jury,
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user