forked from ScoDoc/ScoDoc
master devient 9.6x.
utiliser la branche sco95 pour la série 9.5.x.
This commit is contained in:
commit
98f6ba2dd7
26
.pylintrc
26
.pylintrc
@ -1,10 +1,24 @@
|
|||||||
|
|
||||||
[MASTER]
|
[MASTER]
|
||||||
load-plugins=pylint_flask_sqlalchemy,pylint_flask
|
|
||||||
|
|
||||||
[MESSAGES CONTROL]
|
# List of plugins (as comma separated values of python module names) to load,
|
||||||
# pylint and black disagree...
|
# usually to register additional checkers.
|
||||||
disable=bad-continuation
|
load-plugins=pylint_flask
|
||||||
|
|
||||||
[TYPECHECK]
|
[TYPECHECK]
|
||||||
ignored-classes=Permission,SQLObject,Registrant,scoped_session
|
# List of class names for which member attributes should not be checked (useful
|
||||||
|
# for classes with dynamically set attributes). This supports the use of
|
||||||
|
# qualified names.
|
||||||
|
ignored-classes=Permission,
|
||||||
|
SQLObject,
|
||||||
|
Registrant,
|
||||||
|
scoped_session,
|
||||||
|
func
|
||||||
|
|
||||||
|
# List of module names for which member attributes should not be checked
|
||||||
|
# (useful for modules/projects where namespaces are manipulated during runtime
|
||||||
|
# and thus existing member attributes cannot be deduced by static analysis). It
|
||||||
|
# supports qualified module names, as well as Unix pattern matching.
|
||||||
|
ignored-modules=entreprises
|
||||||
|
|
||||||
|
good-names=d,df,e,f,i,j,k,n,nt,t,u,ue,v,x,y,z,H,F
|
||||||
|
|
||||||
|
@ -176,7 +176,7 @@ puis
|
|||||||
|
|
||||||
snakeviz -s --hostname 0.0.0.0 -p 5555 /opt/scodoc-data/GET.ScoDoc......prof
|
snakeviz -s --hostname 0.0.0.0 -p 5555 /opt/scodoc-data/GET.ScoDoc......prof
|
||||||
|
|
||||||
# Paquet Debian 11
|
## Paquet Debian 12
|
||||||
|
|
||||||
Les scripts associés au paquet Debian (.deb) sont dans `tools/debian`. Le plus
|
Les scripts associés au paquet Debian (.deb) sont dans `tools/debian`. Le plus
|
||||||
important est `postinst`qui se charge de configurer le système (install ou
|
important est `postinst`qui se charge de configurer le système (install ou
|
||||||
|
14
app/__init__.py
Normal file → Executable file
14
app/__init__.py
Normal file → Executable file
@ -148,7 +148,7 @@ def handle_invalid_usage(error):
|
|||||||
|
|
||||||
# JSON ENCODING
|
# JSON ENCODING
|
||||||
# used by some internal finctions
|
# used by some internal finctions
|
||||||
# the API is now using flask_son, NOT THIS ENCODER
|
# the API is now using flask_json, NOT THIS ENCODER
|
||||||
class ScoDocJSONEncoder(json.JSONEncoder):
|
class ScoDocJSONEncoder(json.JSONEncoder):
|
||||||
def default(self, o): # pylint: disable=E0202
|
def default(self, o): # pylint: disable=E0202
|
||||||
if isinstance(o, (datetime.date, datetime.datetime)):
|
if isinstance(o, (datetime.date, datetime.datetime)):
|
||||||
@ -260,7 +260,13 @@ def create_app(config_class=DevConfig):
|
|||||||
|
|
||||||
CAS(app, url_prefix="/cas", configuration_function=cas.set_cas_configuration)
|
CAS(app, url_prefix="/cas", configuration_function=cas.set_cas_configuration)
|
||||||
app.wsgi_app = ReverseProxied(app.wsgi_app)
|
app.wsgi_app = ReverseProxied(app.wsgi_app)
|
||||||
FlaskJSON(app)
|
app_json = FlaskJSON(app)
|
||||||
|
|
||||||
|
@app_json.encoder
|
||||||
|
def scodoc_json_encoder(o):
|
||||||
|
"Overide default date encoding (RFC 822) and use ISO"
|
||||||
|
if isinstance(o, (datetime.date, datetime.datetime)):
|
||||||
|
return o.isoformat()
|
||||||
|
|
||||||
# Pour conserver l'ordre des objets dans les JSON:
|
# Pour conserver l'ordre des objets dans les JSON:
|
||||||
# e.g. l'ordre des UE dans les bulletins
|
# e.g. l'ordre des UE dans les bulletins
|
||||||
@ -322,6 +328,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 +347,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")
|
||||||
|
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
"""api.__init__
|
"""api.__init__
|
||||||
"""
|
"""
|
||||||
|
from flask_json import as_json
|
||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
from flask import request
|
from flask import request, g
|
||||||
|
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 AccessDenied, ScoException
|
||||||
|
|
||||||
api_bp = Blueprint("api", __name__)
|
api_bp = Blueprint("api", __name__)
|
||||||
api_web_bp = Blueprint("apiweb", __name__)
|
api_web_bp = Blueprint("apiweb", __name__)
|
||||||
@ -14,12 +15,24 @@ API_CLIENT_ERROR = 400 # erreur dans les paramètres fournis par le client
|
|||||||
|
|
||||||
|
|
||||||
@api_bp.errorhandler(ScoException)
|
@api_bp.errorhandler(ScoException)
|
||||||
|
@api_web_bp.errorhandler(ScoException)
|
||||||
@api_bp.errorhandler(404)
|
@api_bp.errorhandler(404)
|
||||||
def api_error_handler(e):
|
def api_error_handler(e):
|
||||||
"erreurs API => json"
|
"erreurs API => json"
|
||||||
return scu.json_error(404, message=str(e))
|
return scu.json_error(404, message=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.errorhandler(AccessDenied)
|
||||||
|
@api_web_bp.errorhandler(AccessDenied)
|
||||||
|
def permission_denied_error_handler(exc):
|
||||||
|
"""
|
||||||
|
Renvoie message d'erreur pour l'erreur 403
|
||||||
|
"""
|
||||||
|
return scu.json_error(
|
||||||
|
403, f"operation non autorisee ({exc.args[0] if exc.args else ''})"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def requested_format(default_format="json", allowed_formats=None):
|
def requested_format(default_format="json", allowed_formats=None):
|
||||||
"""Extract required format from query string.
|
"""Extract required format from query string.
|
||||||
* default value is json. A list of allowed formats may be provided
|
* default value is json. A list of allowed formats may be provided
|
||||||
@ -34,9 +47,26 @@ def requested_format(default_format="json", allowed_formats=None):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@as_json
|
||||||
|
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 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,
|
assiduites,
|
||||||
billets_absences,
|
billets_absences,
|
||||||
departements,
|
departements,
|
||||||
etudiants,
|
etudiants,
|
||||||
@ -44,7 +74,9 @@ from app.api import (
|
|||||||
formations,
|
formations,
|
||||||
formsemestres,
|
formsemestres,
|
||||||
jury,
|
jury,
|
||||||
|
justificatifs,
|
||||||
logos,
|
logos,
|
||||||
|
moduleimpl,
|
||||||
partitions,
|
partitions,
|
||||||
semset,
|
semset,
|
||||||
users,
|
users,
|
||||||
|
@ -1,263 +0,0 @@
|
|||||||
##############################################################################
|
|
||||||
# ScoDoc
|
|
||||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
|
||||||
# See LICENSE
|
|
||||||
##############################################################################
|
|
||||||
"""ScoDoc 9 API : Absences
|
|
||||||
"""
|
|
||||||
|
|
||||||
from flask_json import as_json
|
|
||||||
|
|
||||||
from app import db
|
|
||||||
from app.api import api_bp as bp, API_CLIENT_ERROR
|
|
||||||
from app.scodoc.sco_utils import json_error
|
|
||||||
from app.decorators import scodoc, permission_required
|
|
||||||
from app.models import Identite
|
|
||||||
|
|
||||||
from app.scodoc import notesdb as ndb
|
|
||||||
from app.scodoc import sco_abs
|
|
||||||
|
|
||||||
from app.scodoc.sco_groups import get_group_members
|
|
||||||
from app.scodoc.sco_permissions import Permission
|
|
||||||
|
|
||||||
|
|
||||||
# TODO XXX revoir routes web API et calcul des droits
|
|
||||||
@bp.route("/absences/etudid/<int:etudid>", methods=["GET"])
|
|
||||||
@scodoc
|
|
||||||
@permission_required(Permission.ScoView)
|
|
||||||
@as_json
|
|
||||||
def absences(etudid: int = None):
|
|
||||||
"""
|
|
||||||
Liste des absences de cet étudiant
|
|
||||||
|
|
||||||
Exemple de résultat:
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"jour": "2022-04-15",
|
|
||||||
"matin": true,
|
|
||||||
"estabs": true,
|
|
||||||
"estjust": true,
|
|
||||||
"description": "",
|
|
||||||
"begin": "2022-04-15 08:00:00",
|
|
||||||
"end": "2022-04-15 11:59:59"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"jour": "2022-04-15",
|
|
||||||
"matin": false,
|
|
||||||
"estabs": true,
|
|
||||||
"estjust": false,
|
|
||||||
"description": "",
|
|
||||||
"begin": "2022-04-15 12:00:00",
|
|
||||||
"end": "2022-04-15 17:59:59"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
"""
|
|
||||||
etud = db.session.get(Identite, etudid)
|
|
||||||
if etud is None:
|
|
||||||
return json_error(404, message="etudiant inexistant")
|
|
||||||
# Absences de l'étudiant
|
|
||||||
ndb.open_db_connection()
|
|
||||||
abs_list = sco_abs.list_abs_date(etud.id)
|
|
||||||
for absence in abs_list:
|
|
||||||
absence["jour"] = absence["jour"].isoformat()
|
|
||||||
return abs_list
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/absences/etudid/<int:etudid>/just", methods=["GET"])
|
|
||||||
@scodoc
|
|
||||||
@permission_required(Permission.ScoView)
|
|
||||||
@as_json
|
|
||||||
def absences_just(etudid: int = None):
|
|
||||||
"""
|
|
||||||
Retourne la liste des absences justifiées d'un étudiant donné
|
|
||||||
|
|
||||||
etudid : l'etudid d'un étudiant
|
|
||||||
nip: le code nip d'un étudiant
|
|
||||||
ine : le code ine d'un étudiant
|
|
||||||
|
|
||||||
Exemple de résultat :
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"jour": "2022-04-15",
|
|
||||||
"matin": true,
|
|
||||||
"estabs": true,
|
|
||||||
"estjust": true,
|
|
||||||
"description": "",
|
|
||||||
"begin": "2022-04-15 08:00:00",
|
|
||||||
"end": "2022-04-15 11:59:59"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"jour": "Fri, 15 Apr 2022 00:00:00 GMT",
|
|
||||||
"matin": false,
|
|
||||||
"estabs": true,
|
|
||||||
"estjust": true,
|
|
||||||
"description": "",
|
|
||||||
"begin": "2022-04-15 12:00:00",
|
|
||||||
"end": "2022-04-15 17:59:59"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
"""
|
|
||||||
etud = db.session.get(Identite, etudid)
|
|
||||||
if etud is None:
|
|
||||||
return json_error(404, message="etudiant inexistant")
|
|
||||||
|
|
||||||
# Absences justifiées de l'étudiant
|
|
||||||
abs_just = [
|
|
||||||
absence for absence in sco_abs.list_abs_date(etud.id) if absence["estjust"]
|
|
||||||
]
|
|
||||||
for absence in abs_just:
|
|
||||||
absence["jour"] = absence["jour"].isoformat()
|
|
||||||
return abs_just
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route(
|
|
||||||
"/absences/abs_group_etat/<int:group_id>",
|
|
||||||
methods=["GET"],
|
|
||||||
)
|
|
||||||
@bp.route(
|
|
||||||
"/absences/abs_group_etat/group_id/<int:group_id>/date_debut/<string:date_debut>/date_fin/<string:date_fin>",
|
|
||||||
methods=["GET"],
|
|
||||||
)
|
|
||||||
@scodoc
|
|
||||||
@permission_required(Permission.ScoView)
|
|
||||||
@as_json
|
|
||||||
def abs_groupe_etat(group_id: int, date_debut=None, date_fin=None):
|
|
||||||
"""
|
|
||||||
Liste des absences d'un groupe (possibilité de choisir entre deux dates)
|
|
||||||
|
|
||||||
group_id = l'id du groupe
|
|
||||||
date_debut = None par défaut, sinon la date ISO du début de notre filtre
|
|
||||||
date_fin = None par défaut, sinon la date ISO de la fin de notre filtre
|
|
||||||
|
|
||||||
Exemple de résultat :
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"etudid": 1,
|
|
||||||
"list_abs": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"etudid": 2,
|
|
||||||
"list_abs": [
|
|
||||||
{
|
|
||||||
"jour": "Fri, 15 Apr 2022 00:00:00 GMT",
|
|
||||||
"matin": true,
|
|
||||||
"estabs": true,
|
|
||||||
"estjust": true,
|
|
||||||
"description": "",
|
|
||||||
"begin": "2022-04-15 08:00:00",
|
|
||||||
"end": "2022-04-15 11:59:59"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"jour": "Fri, 15 Apr 2022 00:00:00 GMT",
|
|
||||||
"matin": false,
|
|
||||||
"estabs": true,
|
|
||||||
"estjust": false,
|
|
||||||
"description": "",
|
|
||||||
"begin": "2022-04-15 12:00:00",
|
|
||||||
"end": "2022-04-15 17:59:59"
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
...
|
|
||||||
]
|
|
||||||
"""
|
|
||||||
members = get_group_members(group_id)
|
|
||||||
|
|
||||||
data = []
|
|
||||||
# Filtre entre les deux dates renseignées
|
|
||||||
for member in members:
|
|
||||||
absence = {
|
|
||||||
"etudid": member["etudid"],
|
|
||||||
"list_abs": sco_abs.list_abs_date(member["etudid"], date_debut, date_fin),
|
|
||||||
}
|
|
||||||
data.append(absence)
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
# XXX TODO EV: A REVOIR (data json dans le POST + modifier les routes)
|
|
||||||
# @bp.route(
|
|
||||||
# "/absences/etudid/<int:etudid>/list_abs/<string:list_abs>/reset_etud_abs",
|
|
||||||
# methods=["POST"],
|
|
||||||
# defaults={"just_or_not": 0},
|
|
||||||
# )
|
|
||||||
# @bp.route(
|
|
||||||
# "/absences/etudid/<int:etudid>/list_abs/<string:list_abs>/reset_etud_abs/only_not_just",
|
|
||||||
# methods=["POST"],
|
|
||||||
# defaults={"just_or_not": 1},
|
|
||||||
# )
|
|
||||||
# @bp.route(
|
|
||||||
# "/absences/etudid/<int:etudid>/list_abs/<string:list_abs>/reset_etud_abs/only_just",
|
|
||||||
# methods=["POST"],
|
|
||||||
# defaults={"just_or_not": 2},
|
|
||||||
# )
|
|
||||||
# @token_auth.login_required
|
|
||||||
# @token_permission_required(Permission.APIAbsChange)
|
|
||||||
# def reset_etud_abs(etudid: int, list_abs: str, just_or_not: int = 0):
|
|
||||||
# """
|
|
||||||
# Set la liste des absences d'un étudiant sur tout un semestre.
|
|
||||||
# (les absences existant pour cet étudiant sur cette période sont effacées)
|
|
||||||
|
|
||||||
# etudid : l'id d'un étudiant
|
|
||||||
# list_abs : json d'absences
|
|
||||||
# just_or_not : 0 (pour les absences justifiées et non justifiées),
|
|
||||||
# 1 (pour les absences justifiées),
|
|
||||||
# 2 (pour les absences non justifiées)
|
|
||||||
# """
|
|
||||||
# # Toutes les absences
|
|
||||||
# if just_or_not == 0:
|
|
||||||
# # suppression des absences et justificatif déjà existant pour éviter les doublons
|
|
||||||
# for abs in list_abs:
|
|
||||||
# # Récupération de la date au format iso
|
|
||||||
# jour = abs["jour"].isoformat()
|
|
||||||
# if abs["matin"] is True:
|
|
||||||
# annule_absence(etudid, jour, True)
|
|
||||||
# annule_justif(etudid, jour, True)
|
|
||||||
# else:
|
|
||||||
# annule_absence(etudid, jour, False)
|
|
||||||
# annule_justif(etudid, jour, False)
|
|
||||||
|
|
||||||
# # Ajout de la liste d'absences en base
|
|
||||||
# add_abslist(list_abs)
|
|
||||||
|
|
||||||
# # Uniquement les absences justifiées
|
|
||||||
# elif just_or_not == 1:
|
|
||||||
# list_abs_not_just = []
|
|
||||||
# # Trie des absences justifiées
|
|
||||||
# for abs in list_abs:
|
|
||||||
# if abs["estjust"] is False:
|
|
||||||
# list_abs_not_just.append(abs)
|
|
||||||
# # suppression des absences et justificatif déjà existant pour éviter les doublons
|
|
||||||
# for abs in list_abs:
|
|
||||||
# # Récupération de la date au format iso
|
|
||||||
# jour = abs["jour"].isoformat()
|
|
||||||
# if abs["matin"] is True:
|
|
||||||
# annule_absence(etudid, jour, True)
|
|
||||||
# annule_justif(etudid, jour, True)
|
|
||||||
# else:
|
|
||||||
# annule_absence(etudid, jour, False)
|
|
||||||
# annule_justif(etudid, jour, False)
|
|
||||||
|
|
||||||
# # Ajout de la liste d'absences en base
|
|
||||||
# add_abslist(list_abs_not_just)
|
|
||||||
|
|
||||||
# # Uniquement les absences non justifiées
|
|
||||||
# elif just_or_not == 2:
|
|
||||||
# list_abs_just = []
|
|
||||||
# # Trie des absences non justifiées
|
|
||||||
# for abs in list_abs:
|
|
||||||
# if abs["estjust"] is True:
|
|
||||||
# list_abs_just.append(abs)
|
|
||||||
# # suppression des absences et justificatif déjà existant pour éviter les doublons
|
|
||||||
# for abs in list_abs:
|
|
||||||
# # Récupération de la date au format iso
|
|
||||||
# jour = abs["jour"].isoformat()
|
|
||||||
# if abs["matin"] is True:
|
|
||||||
# annule_absence(etudid, jour, True)
|
|
||||||
# annule_justif(etudid, jour, True)
|
|
||||||
# else:
|
|
||||||
# annule_absence(etudid, jour, False)
|
|
||||||
# annule_justif(etudid, jour, False)
|
|
||||||
|
|
||||||
# # Ajout de la liste d'absences en base
|
|
||||||
# add_abslist(list_abs_just)
|
|
1043
app/api/assiduites.py
Normal file
1043
app/api/assiduites.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -281,7 +281,15 @@ def dept_formsemestres_courants(acronym: str):
|
|||||||
FormSemestre.date_debut <= test_date,
|
FormSemestre.date_debut <= test_date,
|
||||||
FormSemestre.date_fin >= test_date,
|
FormSemestre.date_fin >= test_date,
|
||||||
)
|
)
|
||||||
return [d.to_dict_api() for d in formsemestres]
|
return [
|
||||||
|
d.to_dict_api()
|
||||||
|
for d in formsemestres.order_by(
|
||||||
|
FormSemestre.date_debut.desc(),
|
||||||
|
FormSemestre.modalite,
|
||||||
|
FormSemestre.semestre_id,
|
||||||
|
FormSemestre.titre,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/departement/id/<int:dept_id>/formsemestres_courants")
|
@bp.route("/departement/id/<int:dept_id>/formsemestres_courants")
|
||||||
|
76
app/api/etudiants.py
Normal file → Executable file
76
app/api/etudiants.py
Normal file → Executable file
@ -34,6 +34,7 @@ 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
|
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 +137,81 @@ def etudiant(etudid: int = None, nip: str = None, ine: str = None):
|
|||||||
return etud.to_dict_api()
|
return etud.to_dict_api()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/etudiant/etudid/<int:etudid>/photo")
|
||||||
|
@bp.route("/etudiant/nip/<string:nip>/photo")
|
||||||
|
@bp.route("/etudiant/ine/<string:ine>/photo")
|
||||||
|
@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
|
||||||
|
"""
|
||||||
|
|
||||||
|
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("/etudiant/etudid/<int:etudid>/photo", methods=["POST"])
|
||||||
|
@api_web_bp.route("/etudiant/etudid/<int:etudid>/photo", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoEtudChangeAdr)
|
||||||
|
@as_json
|
||||||
|
def set_photo_image(etudid: int = None):
|
||||||
|
"""Enregistre la photo de l'étudiant."""
|
||||||
|
allowed_depts = current_user.get_depts_with_permission(Permission.ScoEtudChangeAdr)
|
||||||
|
query = Identite.query.filter_by(id=etudid)
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
if g.scodoc_dept is not None:
|
||||||
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
etud: Identite = query.first()
|
||||||
|
if etud is None:
|
||||||
|
return json_error(404, message="etudiant inexistant")
|
||||||
|
# Récupère l'image
|
||||||
|
if len(request.files) == 0:
|
||||||
|
return json_error(404, "Il n'y a pas de fichier joint")
|
||||||
|
|
||||||
|
file = list(request.files.values())[0]
|
||||||
|
if not file.filename:
|
||||||
|
return json_error(404, "Il n'y a pas de fichier joint")
|
||||||
|
data = file.stream.read()
|
||||||
|
|
||||||
|
status, err_msg = sco_photos.store_photo(etud, data, file.filename)
|
||||||
|
if status:
|
||||||
|
return {"etudid": etud.id, "message": "recorded photo"}
|
||||||
|
return json_error(
|
||||||
|
404,
|
||||||
|
message=f"Erreur: {err_msg}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@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"])
|
||||||
|
@ -7,17 +7,17 @@
|
|||||||
"""
|
"""
|
||||||
ScoDoc 9 API : accès aux évaluations
|
ScoDoc 9 API : accès aux évaluations
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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 current_user, login_required
|
||||||
|
|
||||||
import app
|
import app
|
||||||
|
from app import log, db
|
||||||
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, sco_saisie_notes
|
||||||
|
from app.scodoc.sco_exceptions import AccessDenied, 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
|
||||||
|
|
||||||
@ -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 get_evaluation(evaluation_id: int):
|
||||||
"""Description d'une évaluation.
|
"""Description d'une évaluation.
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -47,7 +47,7 @@ def evaluation(evaluation_id: int):
|
|||||||
'UE1.3': 1.0
|
'UE1.3': 1.0
|
||||||
},
|
},
|
||||||
'publish_incomplete': False,
|
'publish_incomplete': False,
|
||||||
'visi_bulletin': True
|
'visibulletin': True
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
query = Evaluation.query.filter_by(id=evaluation_id)
|
query = Evaluation.query.filter_by(id=evaluation_id)
|
||||||
@ -181,3 +181,97 @@ def evaluation_set_notes(evaluation_id: int):
|
|||||||
return sco_saisie_notes.save_notes(
|
return sco_saisie_notes.save_notes(
|
||||||
evaluation, notes, comment=data.get("comment", "")
|
evaluation, notes, comment=data.get("comment", "")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/moduleimpl/<int:moduleimpl_id>/evaluation/create", methods=["POST"])
|
||||||
|
@api_web_bp.route("/moduleimpl/<int:moduleimpl_id>/evaluation/create", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoEnsView) # permission gérée dans la fonction
|
||||||
|
@as_json
|
||||||
|
def evaluation_create(moduleimpl_id: int):
|
||||||
|
"""Création d'une évaluation.
|
||||||
|
The request content type should be "application/json",
|
||||||
|
and contains:
|
||||||
|
{
|
||||||
|
"description" : str,
|
||||||
|
"evaluation_type" : int, // {0,1,2} default 0 (normale)
|
||||||
|
"date_debut" : date_iso, // optionnel
|
||||||
|
"date_fin" : date_iso, // optionnel
|
||||||
|
"note_max" : float, // si non spécifié, 20.0
|
||||||
|
"numero" : int, // ordre de présentation, default tri sur date
|
||||||
|
"visibulletin" : boolean , //default true
|
||||||
|
"publish_incomplete" : boolean , //default false
|
||||||
|
"coefficient" : float, // si non spécifié, 1.0
|
||||||
|
"poids" : { ue_id : poids } // optionnel
|
||||||
|
}
|
||||||
|
Result: l'évaluation créée.
|
||||||
|
"""
|
||||||
|
moduleimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id)
|
||||||
|
if not moduleimpl.can_edit_evaluation(current_user):
|
||||||
|
return scu.json_error(403, "opération non autorisée")
|
||||||
|
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||||
|
|
||||||
|
try:
|
||||||
|
evaluation = Evaluation.create(moduleimpl=moduleimpl, **data)
|
||||||
|
except ValueError:
|
||||||
|
return scu.json_error(400, "paramètre incorrect")
|
||||||
|
except ScoValueError as exc:
|
||||||
|
return scu.json_error(
|
||||||
|
400, f"paramètre de type incorrect ({exc.args[0] if exc.args else ''})"
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(evaluation)
|
||||||
|
db.session.commit()
|
||||||
|
# Les poids vers les UEs:
|
||||||
|
poids = data.get("poids")
|
||||||
|
if poids is not None:
|
||||||
|
if not isinstance(poids, dict):
|
||||||
|
log("API error: canceling evaluation creation")
|
||||||
|
db.session.delete(evaluation)
|
||||||
|
db.session.commit()
|
||||||
|
return scu.json_error(
|
||||||
|
400, "paramètre de type incorrect (poids must be a dict)"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
evaluation.set_ue_poids_dict(data["poids"])
|
||||||
|
except ScoValueError as exc:
|
||||||
|
log("API error: canceling evaluation creation")
|
||||||
|
db.session.delete(evaluation)
|
||||||
|
db.session.commit()
|
||||||
|
return scu.json_error(
|
||||||
|
400,
|
||||||
|
f"erreur enregistrement des poids ({exc.args[0] if exc.args else ''})",
|
||||||
|
)
|
||||||
|
db.session.commit()
|
||||||
|
return evaluation.to_dict_api()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/evaluation/<int:evaluation_id>/delete", methods=["POST"])
|
||||||
|
@api_web_bp.route("/evaluation/<int:evaluation_id>/delete", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoEnsView) # permission gérée dans la fonction
|
||||||
|
@as_json
|
||||||
|
def evaluation_delete(evaluation_id: int):
|
||||||
|
"""Suppression d'une évaluation.
|
||||||
|
Efface aussi toutes ses notes
|
||||||
|
"""
|
||||||
|
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)
|
||||||
|
if not evaluation.moduleimpl.can_edit_evaluation(current_user):
|
||||||
|
raise AccessDenied("evaluation_delete")
|
||||||
|
|
||||||
|
sco_saisie_notes.evaluation_suppress_alln(
|
||||||
|
evaluation_id=evaluation_id, dialog_confirmed=True
|
||||||
|
)
|
||||||
|
sco_evaluation_db.do_evaluation_delete(evaluation_id)
|
||||||
|
return "ok"
|
||||||
|
@ -21,8 +21,6 @@ from app.models import (
|
|||||||
ApcNiveau,
|
ApcNiveau,
|
||||||
ApcParcours,
|
ApcParcours,
|
||||||
Formation,
|
Formation,
|
||||||
FormSemestre,
|
|
||||||
ModuleImpl,
|
|
||||||
UniteEns,
|
UniteEns,
|
||||||
)
|
)
|
||||||
from app.scodoc import sco_formations
|
from app.scodoc import sco_formations
|
||||||
@ -249,54 +247,6 @@ def referentiel_competences(formation_id: int):
|
|||||||
return formation.referentiel_competence.to_dict()
|
return formation.referentiel_competence.to_dict()
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/moduleimpl/<int:moduleimpl_id>")
|
|
||||||
@api_web_bp.route("/moduleimpl/<int:moduleimpl_id>")
|
|
||||||
@login_required
|
|
||||||
@scodoc
|
|
||||||
@permission_required(Permission.ScoView)
|
|
||||||
@as_json
|
|
||||||
def moduleimpl(moduleimpl_id: int):
|
|
||||||
"""
|
|
||||||
Retourne un moduleimpl en fonction de son id
|
|
||||||
|
|
||||||
moduleimpl_id : l'id d'un moduleimpl
|
|
||||||
|
|
||||||
Exemple de résultat :
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"formsemestre_id": 1,
|
|
||||||
"module_id": 1,
|
|
||||||
"responsable_id": 2,
|
|
||||||
"moduleimpl_id": 1,
|
|
||||||
"ens": [],
|
|
||||||
"module": {
|
|
||||||
"heures_tp": 0,
|
|
||||||
"code_apogee": "",
|
|
||||||
"titre": "Initiation aux réseaux informatiques",
|
|
||||||
"coefficient": 1,
|
|
||||||
"module_type": 2,
|
|
||||||
"id": 1,
|
|
||||||
"ects": null,
|
|
||||||
"abbrev": "Init aux réseaux informatiques",
|
|
||||||
"ue_id": 1,
|
|
||||||
"code": "R101",
|
|
||||||
"formation_id": 1,
|
|
||||||
"heures_cours": 0,
|
|
||||||
"matiere_id": 1,
|
|
||||||
"heures_td": 0,
|
|
||||||
"semestre_id": 1,
|
|
||||||
"numero": 10,
|
|
||||||
"module_id": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
query = ModuleImpl.query.filter_by(id=moduleimpl_id)
|
|
||||||
if g.scodoc_dept:
|
|
||||||
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
|
||||||
modimpl: ModuleImpl = query.first_or_404()
|
|
||||||
return modimpl.to_dict(convert_objects=True)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/set_ue_parcours/<int:ue_id>", methods=["POST"])
|
@bp.route("/set_ue_parcours/<int:ue_id>", methods=["POST"])
|
||||||
@api_web_bp.route("/set_ue_parcours/<int:ue_id>", methods=["POST"])
|
@api_web_bp.route("/set_ue_parcours/<int:ue_id>", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
|
@ -99,18 +99,20 @@ def formsemestre_infos(formsemestre_id: int):
|
|||||||
def formsemestres_query():
|
def formsemestres_query():
|
||||||
"""
|
"""
|
||||||
Retourne les formsemestres filtrés par
|
Retourne les formsemestres filtrés par
|
||||||
étape Apogée ou année scolaire ou département (acronyme ou id)
|
étape Apogée ou année scolaire ou département (acronyme ou id) ou état ou code étudiant
|
||||||
|
|
||||||
etape_apo : un code étape apogée
|
etape_apo : un code étape apogée
|
||||||
annee_scolaire : année de début de l'année scolaire
|
annee_scolaire : année de début de l'année scolaire
|
||||||
dept_acronym : acronyme du département (eg "RT")
|
dept_acronym : acronyme du département (eg "RT")
|
||||||
dept_id : id du département
|
dept_id : id du département
|
||||||
ine ou nip: code d'un étudiant: ramène alors tous les semestres auxquels il est inscrit.
|
ine ou nip: code d'un étudiant: ramène alors tous les semestres auxquels il est inscrit.
|
||||||
|
etat: 0 si verrouillé, 1 sinon
|
||||||
"""
|
"""
|
||||||
etape_apo = request.args.get("etape_apo")
|
etape_apo = request.args.get("etape_apo")
|
||||||
annee_scolaire = request.args.get("annee_scolaire")
|
annee_scolaire = request.args.get("annee_scolaire")
|
||||||
dept_acronym = request.args.get("dept_acronym")
|
dept_acronym = request.args.get("dept_acronym")
|
||||||
dept_id = request.args.get("dept_id")
|
dept_id = request.args.get("dept_id")
|
||||||
|
etat = request.args.get("etat")
|
||||||
nip = request.args.get("nip")
|
nip = request.args.get("nip")
|
||||||
ine = request.args.get("ine")
|
ine = request.args.get("ine")
|
||||||
formsemestres = FormSemestre.query
|
formsemestres = FormSemestre.query
|
||||||
@ -126,6 +128,12 @@ def formsemestres_query():
|
|||||||
formsemestres = formsemestres.filter(
|
formsemestres = formsemestres.filter(
|
||||||
FormSemestre.date_fin >= debut_annee, FormSemestre.date_debut <= fin_annee
|
FormSemestre.date_fin >= debut_annee, FormSemestre.date_debut <= fin_annee
|
||||||
)
|
)
|
||||||
|
if etat is not None:
|
||||||
|
try:
|
||||||
|
etat = bool(int(etat))
|
||||||
|
except ValueError:
|
||||||
|
return json_error(404, "invalid etat: integer expected")
|
||||||
|
formsemestres = formsemestres.filter_by(etat=etat)
|
||||||
if dept_acronym is not None:
|
if dept_acronym is not None:
|
||||||
formsemestres = formsemestres.join(Departement).filter_by(acronym=dept_acronym)
|
formsemestres = formsemestres.join(Departement).filter_by(acronym=dept_acronym)
|
||||||
if dept_id is not None:
|
if dept_id is not None:
|
||||||
@ -151,7 +159,15 @@ def formsemestres_query():
|
|||||||
formsemestres = formsemestres.join(FormSemestreInscription).join(Identite)
|
formsemestres = formsemestres.join(FormSemestreInscription).join(Identite)
|
||||||
formsemestres = formsemestres.filter_by(code_ine=ine)
|
formsemestres = formsemestres.filter_by(code_ine=ine)
|
||||||
|
|
||||||
return [formsemestre.to_dict_api() for formsemestre in formsemestres]
|
return [
|
||||||
|
formsemestre.to_dict_api()
|
||||||
|
for formsemestre in formsemestres.order_by(
|
||||||
|
FormSemestre.date_debut.desc(),
|
||||||
|
FormSemestre.modalite,
|
||||||
|
FormSemestre.semestre_id,
|
||||||
|
FormSemestre.titre,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
|
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
|
||||||
@ -196,7 +212,7 @@ def bulletins(formsemestre_id: int, version: str = "long"):
|
|||||||
@as_json
|
@as_json
|
||||||
def formsemestre_programme(formsemestre_id: int):
|
def formsemestre_programme(formsemestre_id: int):
|
||||||
"""
|
"""
|
||||||
Retourne la liste des Ues, ressources et SAE d'un semestre
|
Retourne la liste des UEs, ressources et SAEs d'un semestre
|
||||||
|
|
||||||
formsemestre_id : l'id d'un formsemestre
|
formsemestre_id : l'id d'un formsemestre
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from flask import flash, g, request, url_for
|
from flask import 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 current_user, login_required
|
||||||
|
|
||||||
|
699
app/api/justificatifs.py
Normal file
699
app/api/justificatifs.py
Normal file
@ -0,0 +1,699 @@
|
|||||||
|
##############################################################################
|
||||||
|
# ScoDoc
|
||||||
|
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||||
|
# See LICENSE
|
||||||
|
##############################################################################
|
||||||
|
"""ScoDoc 9 API : Assiduités
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from flask_json import as_json
|
||||||
|
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, tools
|
||||||
|
from app.decorators import permission_required, scodoc
|
||||||
|
from app.models import Identite, Justificatif, Departement, FormSemestre
|
||||||
|
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
|
||||||
|
from flask_sqlalchemy.query import Query
|
||||||
|
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
|
||||||
|
# etudid
|
||||||
|
@bp.route("/justificatifs/<etudid>", defaults={"with_query": False})
|
||||||
|
@api_web_bp.route("/justificatifs/<etudid>", defaults={"with_query": False})
|
||||||
|
@bp.route("/justificatifs/<etudid>/query", defaults={"with_query": True})
|
||||||
|
@api_web_bp.route("/justificatifs/<etudid>/query", defaults={"with_query": True})
|
||||||
|
@bp.route("/justificatifs/etudid/<etudid>", defaults={"with_query": False})
|
||||||
|
@api_web_bp.route("/justificatifs/etudid/<etudid>", defaults={"with_query": False})
|
||||||
|
@bp.route("/justificatifs/etudid/<etudid>/query", defaults={"with_query": True})
|
||||||
|
@api_web_bp.route("/justificatifs/etudid/<etudid>/query", defaults={"with_query": True})
|
||||||
|
# nip
|
||||||
|
@bp.route("/justificatifs/nip/<nip>", defaults={"with_query": False})
|
||||||
|
@api_web_bp.route("/justificatifs/nip/<nip>", defaults={"with_query": False})
|
||||||
|
@bp.route("/justificatifs/nip/<nip>/query", defaults={"with_query": True})
|
||||||
|
@api_web_bp.route("/justificatifs/nip/<nip>/query", defaults={"with_query": True})
|
||||||
|
# ine
|
||||||
|
@bp.route("/justificatifs/ine/<ine>", defaults={"with_query": False})
|
||||||
|
@api_web_bp.route("/justificatifs/ine/<ine>", defaults={"with_query": False})
|
||||||
|
@bp.route("/justificatifs/ine/<ine>/query", defaults={"with_query": True})
|
||||||
|
@api_web_bp.route("/justificatifs/ine/<ine>/query", defaults={"with_query": True})
|
||||||
|
#
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@as_json
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = False):
|
||||||
|
"""
|
||||||
|
Retourne toutes les assiduités d'un étudiant
|
||||||
|
chemin : /justificatifs/<int:etudid>
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
|
||||||
|
etud: Identite = tools.get_etud(etudid, nip, ine)
|
||||||
|
|
||||||
|
if etud is None:
|
||||||
|
return json_error(
|
||||||
|
404,
|
||||||
|
message="étudiant inconnu",
|
||||||
|
)
|
||||||
|
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 data_set
|
||||||
|
|
||||||
|
|
||||||
|
@api_web_bp.route("/justificatifs/dept/<int:dept_id>", defaults={"with_query": False})
|
||||||
|
@api_web_bp.route(
|
||||||
|
"/justificatifs/dept/<int:dept_id>/query", defaults={"with_query": True}
|
||||||
|
)
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@as_json
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def justificatifs_dept(dept_id: int = None, with_query: bool = False):
|
||||||
|
""" """
|
||||||
|
dept = Departement.query.get_or_404(dept_id)
|
||||||
|
etuds = [etud.id for etud in dept.etudiants]
|
||||||
|
|
||||||
|
justificatifs_query = Justificatif.query.filter(Justificatif.etudid.in_(etuds))
|
||||||
|
|
||||||
|
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 data_set
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/justificatif/<int:etudid>/create", methods=["POST"])
|
||||||
|
@api_web_bp.route("/justificatif/<int:etudid>/create", methods=["POST"])
|
||||||
|
@bp.route("/justificatif/etudid/<etudid>/create", methods=["POST"])
|
||||||
|
@api_web_bp.route("/justificatif/etudid/<etudid>/create", methods=["POST"])
|
||||||
|
# nip
|
||||||
|
@bp.route("/justificatif/nip/<nip>/create", methods=["POST"])
|
||||||
|
@api_web_bp.route("/justificatif/nip/<nip>/create", methods=["POST"])
|
||||||
|
# ine
|
||||||
|
@bp.route("/justificatif/ine/<ine>/create", methods=["POST"])
|
||||||
|
@api_web_bp.route("/justificatif/ine/<ine>/create", methods=["POST"])
|
||||||
|
@scodoc
|
||||||
|
@login_required
|
||||||
|
@as_json
|
||||||
|
@permission_required(Permission.ScoAbsChange)
|
||||||
|
def justif_create(etudid: int = None, nip=None, ine=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 = tools.get_etud(etudid, nip, ine)
|
||||||
|
|
||||||
|
if etud is None:
|
||||||
|
return json_error(
|
||||||
|
404,
|
||||||
|
message="étudiant inconnu",
|
||||||
|
)
|
||||||
|
|
||||||
|
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: list = []
|
||||||
|
success: list = []
|
||||||
|
justifs: list = []
|
||||||
|
for i, data in enumerate(create_list):
|
||||||
|
code, obj, justi = _create_singular(data, etud)
|
||||||
|
if code == 404:
|
||||||
|
errors.append({"indice": i, "message": obj})
|
||||||
|
else:
|
||||||
|
success.append({"indice": i, "message": obj})
|
||||||
|
justifs.append(justi)
|
||||||
|
scass.simple_invalidate_cache(data, etud.id)
|
||||||
|
|
||||||
|
compute_assiduites_justified(etud.etudid, justifs)
|
||||||
|
return {"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)
|
||||||
|
|
||||||
|
external_data = data.get("external_data")
|
||||||
|
if external_data is not None:
|
||||||
|
if not isinstance(external_data, dict):
|
||||||
|
errors.append("param 'external_data' : n'est pas un objet JSON")
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
err: str = ", ".join(errors)
|
||||||
|
return (404, err, None)
|
||||||
|
|
||||||
|
# TOUT EST OK
|
||||||
|
|
||||||
|
try:
|
||||||
|
nouv_justificatif: Query = Justificatif.create_justificatif(
|
||||||
|
date_debut=deb,
|
||||||
|
date_fin=fin,
|
||||||
|
etat=etat,
|
||||||
|
etud=etud,
|
||||||
|
raison=raison,
|
||||||
|
user_id=current_user.id,
|
||||||
|
external_data=external_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(nouv_justificatif)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return (
|
||||||
|
200,
|
||||||
|
{
|
||||||
|
"justif_id": nouv_justificatif.id,
|
||||||
|
"couverture": scass.justifies(nouv_justificatif),
|
||||||
|
},
|
||||||
|
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
|
||||||
|
@as_json
|
||||||
|
@permission_required(Permission.ScoAbsChange)
|
||||||
|
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: Query = 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")
|
||||||
|
|
||||||
|
# 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")
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
external_data = data.get("external_data")
|
||||||
|
if external_data is not None:
|
||||||
|
if not isinstance(external_data, dict):
|
||||||
|
errors.append("param 'external_data' : n'est pas un objet JSON")
|
||||||
|
else:
|
||||||
|
justificatif_unique.external_data = external_data
|
||||||
|
|
||||||
|
if fin <= deb:
|
||||||
|
errors.append("param 'dates' : Date de début après date de 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()
|
||||||
|
|
||||||
|
retour = {
|
||||||
|
"couverture": {
|
||||||
|
"avant": avant_ids,
|
||||||
|
"après": compute_assiduites_justified(
|
||||||
|
justificatif_unique.etudid,
|
||||||
|
[justificatif_unique],
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scass.simple_invalidate_cache(justificatif_unique.to_dict())
|
||||||
|
return retour
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/justificatif/delete", methods=["POST"])
|
||||||
|
@api_web_bp.route("/justificatif/delete", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@as_json
|
||||||
|
@permission_required(Permission.ScoAbsChange)
|
||||||
|
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"].append({"indice": i, "message": msg})
|
||||||
|
else:
|
||||||
|
output["success"].append({"indice": i, "message": "OK"})
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_singular(justif_id: int, database):
|
||||||
|
justificatif_unique: Query = 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()
|
||||||
|
try:
|
||||||
|
archiver.delete_justificatif(justificatif_unique.etudid, archive_name)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
scass.simple_invalidate_cache(justificatif_unique.to_dict())
|
||||||
|
database.session.delete(justificatif_unique)
|
||||||
|
compute_assiduites_justified(
|
||||||
|
justificatif_unique.etudid,
|
||||||
|
Justificatif.query.filter_by(etudid=justificatif_unique.etudid).all(),
|
||||||
|
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
|
||||||
|
@as_json
|
||||||
|
@permission_required(Permission.ScoAbsChange)
|
||||||
|
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: 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,
|
||||||
|
user_id=current_user.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
justificatif_unique.fichier = archive_name
|
||||||
|
|
||||||
|
db.session.add(justificatif_unique)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return {"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.ScoAbsChange)
|
||||||
|
def justif_export(justif_id: int = None, filename: str = None):
|
||||||
|
"""
|
||||||
|
Retourne un fichier d'une archive d'un justificatif
|
||||||
|
"""
|
||||||
|
|
||||||
|
query: Query = Justificatif.query.filter_by(id=justif_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
|
||||||
|
justificatif_unique: Justificaitf = 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
|
||||||
|
@as_json
|
||||||
|
@permission_required(Permission.ScoAbsChange)
|
||||||
|
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: 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 {"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
|
||||||
|
@as_json
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def justif_list(justif_id: int = None):
|
||||||
|
"""
|
||||||
|
Liste les fichiers du justificatif
|
||||||
|
"""
|
||||||
|
|
||||||
|
query: Query = Justificatif.query.filter_by(id=justif_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
|
||||||
|
justificatif_unique: Justificatif = query.first_or_404()
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
retour = {"total": len(filenames), "filenames": []}
|
||||||
|
|
||||||
|
for fi in filenames:
|
||||||
|
if int(fi[1]) == current_user.id or current_user.has_permission(
|
||||||
|
Permission.ScoJustifView
|
||||||
|
):
|
||||||
|
retour["filenames"].append(fi[0])
|
||||||
|
return retour
|
||||||
|
|
||||||
|
|
||||||
|
# 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
|
||||||
|
@as_json
|
||||||
|
@permission_required(Permission.ScoAbsChange)
|
||||||
|
def justif_justifies(justif_id: int = None):
|
||||||
|
"""
|
||||||
|
Liste assiduite_id justifiées par le justificatif
|
||||||
|
"""
|
||||||
|
|
||||||
|
query: Query = Justificatif.query.filter_by(id=justif_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
|
||||||
|
justificatif_unique: Justificatif = query.first_or_404()
|
||||||
|
|
||||||
|
assiduites_list: list[int] = scass.justifies(justificatif_unique)
|
||||||
|
|
||||||
|
return 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: Query = scass.filter_by_date(
|
||||||
|
justificatifs_query, Justificatif, deb, fin
|
||||||
|
)
|
||||||
|
|
||||||
|
user_id = requested.args.get("user_id", False)
|
||||||
|
if user_id is not False:
|
||||||
|
justificatifs_query: Query = scass.filter_by_user_id(
|
||||||
|
justificatifs_query, user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
justificatifs_query = scass.filter_by_formsemestre(
|
||||||
|
justificatifs_query, Justificatif, formsemestre
|
||||||
|
)
|
||||||
|
|
||||||
|
return justificatifs_query
|
69
app/api/moduleimpl.py
Normal file
69
app/api/moduleimpl.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
##############################################################################
|
||||||
|
# ScoDoc
|
||||||
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
|
# See LICENSE
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
ScoDoc 9 API : accès aux moduleimpl
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import g
|
||||||
|
from flask_json import as_json
|
||||||
|
from flask_login import login_required
|
||||||
|
|
||||||
|
from app.api import api_bp as bp, api_web_bp
|
||||||
|
from app.decorators import scodoc, permission_required
|
||||||
|
from app.models import (
|
||||||
|
FormSemestre,
|
||||||
|
ModuleImpl,
|
||||||
|
)
|
||||||
|
from app.scodoc.sco_permissions import Permission
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/moduleimpl/<int:moduleimpl_id>")
|
||||||
|
@api_web_bp.route("/moduleimpl/<int:moduleimpl_id>")
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
@as_json
|
||||||
|
def moduleimpl(moduleimpl_id: int):
|
||||||
|
"""
|
||||||
|
Retourne un moduleimpl en fonction de son id
|
||||||
|
|
||||||
|
moduleimpl_id : l'id d'un moduleimpl
|
||||||
|
|
||||||
|
Exemple de résultat :
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"formsemestre_id": 1,
|
||||||
|
"module_id": 1,
|
||||||
|
"responsable_id": 2,
|
||||||
|
"moduleimpl_id": 1,
|
||||||
|
"ens": [],
|
||||||
|
"module": {
|
||||||
|
"heures_tp": 0,
|
||||||
|
"code_apogee": "",
|
||||||
|
"titre": "Initiation aux réseaux informatiques",
|
||||||
|
"coefficient": 1,
|
||||||
|
"module_type": 2,
|
||||||
|
"id": 1,
|
||||||
|
"ects": null,
|
||||||
|
"abbrev": "Init aux réseaux informatiques",
|
||||||
|
"ue_id": 1,
|
||||||
|
"code": "R101",
|
||||||
|
"formation_id": 1,
|
||||||
|
"heures_cours": 0,
|
||||||
|
"matiere_id": 1,
|
||||||
|
"heures_td": 0,
|
||||||
|
"semestre_id": 1,
|
||||||
|
"numero": 10,
|
||||||
|
"module_id": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
query = ModuleImpl.query.filter_by(id=moduleimpl_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
modimpl: ModuleImpl = query.first_or_404()
|
||||||
|
return modimpl.to_dict(convert_objects=True)
|
@ -110,7 +110,7 @@ def formsemestre_partitions(formsemestre_id: int):
|
|||||||
def etud_in_group(group_id: int):
|
def etud_in_group(group_id: int):
|
||||||
"""
|
"""
|
||||||
Retourne la liste des étudiants dans un groupe
|
Retourne la liste des étudiants dans un groupe
|
||||||
|
(inscrits au groupe et inscrits au semestre).
|
||||||
group_id : l'id d'un groupe
|
group_id : l'id d'un groupe
|
||||||
|
|
||||||
Exemple de résultat :
|
Exemple de résultat :
|
||||||
@ -133,7 +133,15 @@ def etud_in_group(group_id: int):
|
|||||||
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
)
|
)
|
||||||
group = query.first_or_404()
|
group = query.first_or_404()
|
||||||
return [etud.to_dict_short() for etud in group.etuds]
|
|
||||||
|
query = (
|
||||||
|
Identite.query.join(group_membership)
|
||||||
|
.filter_by(group_id=group_id)
|
||||||
|
.join(FormSemestreInscription)
|
||||||
|
.filter_by(formsemestre_id=group.partition.formsemestre_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
return [etud.to_dict_short() for etud in query]
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/group/<int:group_id>/etudiants/query")
|
@bp.route("/group/<int:group_id>/etudiants/query")
|
||||||
@ -161,7 +169,6 @@ def etud_in_group_query(group_id: int):
|
|||||||
query = query.filter_by(etat=etat)
|
query = query.filter_by(etat=etat)
|
||||||
|
|
||||||
query = query.join(group_membership).filter_by(group_id=group_id)
|
query = query.join(group_membership).filter_by(group_id=group_id)
|
||||||
|
|
||||||
return [etud.to_dict_short() for etud in query]
|
return [etud.to_dict_short() for etud in query]
|
||||||
|
|
||||||
|
|
||||||
@ -223,7 +230,9 @@ def group_remove_etud(group_id: int, etudid: int):
|
|||||||
commit=True,
|
commit=True,
|
||||||
)
|
)
|
||||||
# Update parcours
|
# Update parcours
|
||||||
group.partition.formsemestre.update_inscriptions_parcours_from_groups()
|
group.partition.formsemestre.update_inscriptions_parcours_from_groups(
|
||||||
|
etudid=etudid
|
||||||
|
)
|
||||||
sco_cache.invalidate_formsemestre(group.partition.formsemestre_id)
|
sco_cache.invalidate_formsemestre(group.partition.formsemestre_id)
|
||||||
return {"group_id": group_id, "etudid": etudid}
|
return {"group_id": group_id, "etudid": etudid}
|
||||||
|
|
||||||
@ -270,7 +279,7 @@ def partition_remove_etud(partition_id: int, etudid: int):
|
|||||||
)
|
)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
# Update parcours
|
# Update parcours
|
||||||
partition.formsemestre.update_inscriptions_parcours_from_groups()
|
partition.formsemestre.update_inscriptions_parcours_from_groups(etudid=etudid)
|
||||||
app.set_sco_dept(partition.formsemestre.departement.acronym)
|
app.set_sco_dept(partition.formsemestre.departement.acronym)
|
||||||
sco_cache.invalidate_formsemestre(partition.formsemestre_id)
|
sco_cache.invalidate_formsemestre(partition.formsemestre_id)
|
||||||
return {"partition_id": partition_id, "etudid": etudid}
|
return {"partition_id": partition_id, "etudid": etudid}
|
||||||
|
@ -276,11 +276,10 @@ class BulletinBUT:
|
|||||||
"coef": fmt_note(e.coefficient)
|
"coef": fmt_note(e.coefficient)
|
||||||
if e.evaluation_type == scu.EVALUATION_NORMALE
|
if e.evaluation_type == scu.EVALUATION_NORMALE
|
||||||
else None,
|
else None,
|
||||||
"date": e.jour.isoformat() if e.jour else None,
|
"date_debut": e.date_debut.isoformat() if e.date_debut else None,
|
||||||
|
"date_fin": e.date_fin.isoformat() if e.date_fin else None,
|
||||||
"description": e.description,
|
"description": e.description,
|
||||||
"evaluation_type": e.evaluation_type,
|
"evaluation_type": e.evaluation_type,
|
||||||
"heure_debut": e.heure_debut.strftime("%H:%M") if e.heure_debut else None,
|
|
||||||
"heure_fin": e.heure_fin.strftime("%H:%M") if e.heure_debut else None,
|
|
||||||
"note": {
|
"note": {
|
||||||
"value": fmt_note(
|
"value": fmt_note(
|
||||||
eval_notes[etud.id],
|
eval_notes[etud.id],
|
||||||
@ -298,6 +297,12 @@ class BulletinBUT:
|
|||||||
)
|
)
|
||||||
if has_request_context()
|
if has_request_context()
|
||||||
else "na",
|
else "na",
|
||||||
|
# deprecated (supprimer avant #sco9.7)
|
||||||
|
"date": e.date_debut.isoformat() if e.date_debut else None,
|
||||||
|
"heure_debut": e.date_debut.time().isoformat("minutes")
|
||||||
|
if e.date_debut
|
||||||
|
else None,
|
||||||
|
"heure_fin": e.date_fin.time().isoformat("minutes") if e.date_fin else None,
|
||||||
}
|
}
|
||||||
return d
|
return d
|
||||||
|
|
||||||
@ -387,6 +392,11 @@ class BulletinBUT:
|
|||||||
semestre_infos["absences"] = {
|
semestre_infos["absences"] = {
|
||||||
"injustifie": nbabs - nbabsjust,
|
"injustifie": nbabs - nbabsjust,
|
||||||
"total": nbabs,
|
"total": nbabs,
|
||||||
|
"metrique": {
|
||||||
|
"H.": "Heure(s)",
|
||||||
|
"J.": "Journée(s)",
|
||||||
|
"1/2 J.": "1/2 Jour.",
|
||||||
|
}.get(sco_preferences.get_preference("assi_metrique")),
|
||||||
}
|
}
|
||||||
decisions_ues = self.res.get_etud_decisions_ue(etud.id) or {}
|
decisions_ues = self.res.get_etud_decisions_ue(etud.id) or {}
|
||||||
if self.prefs["bul_show_ects"]:
|
if self.prefs["bul_show_ects"]:
|
||||||
|
@ -212,6 +212,34 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
|||||||
else:
|
else:
|
||||||
self.ue_std_rows(rows, ue, title_bg)
|
self.ue_std_rows(rows, ue, title_bg)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def affichage_bonus_malus(ue: dict) -> list:
|
||||||
|
fields_bmr = []
|
||||||
|
# lecture des bonus sport culture et malus (ou bonus autre) (0 si valeur non numérique)
|
||||||
|
try:
|
||||||
|
bonus_sc = float(ue.get("bonus", 0.0)) or 0
|
||||||
|
except ValueError:
|
||||||
|
bonus_sc = 0
|
||||||
|
try:
|
||||||
|
malus = float(ue.get("malus", 0.0)) or 0
|
||||||
|
except ValueError:
|
||||||
|
malus = 0
|
||||||
|
# Calcul de l affichage
|
||||||
|
if malus < 0:
|
||||||
|
if bonus_sc > 0:
|
||||||
|
fields_bmr.append(f"Bonus sport/culture: {bonus_sc}")
|
||||||
|
fields_bmr.append(f"Bonus autres: {-malus}")
|
||||||
|
else:
|
||||||
|
fields_bmr.append(f"Bonus: {-malus}")
|
||||||
|
elif malus > 0:
|
||||||
|
if bonus_sc > 0:
|
||||||
|
fields_bmr.append(f"Bonus: {bonus_sc}")
|
||||||
|
fields_bmr.append(f"Malus: {malus}")
|
||||||
|
else:
|
||||||
|
if bonus_sc > 0:
|
||||||
|
fields_bmr.append(f"Bonus: {bonus_sc}")
|
||||||
|
return fields_bmr
|
||||||
|
|
||||||
def ue_std_rows(self, rows: list, ue: dict, title_bg: tuple):
|
def ue_std_rows(self, rows: list, ue: dict, title_bg: tuple):
|
||||||
"Lignes décrivant une UE standard dans la table de synthèse"
|
"Lignes décrivant une UE standard dans la table de synthèse"
|
||||||
# 2eme ligne titre UE (bonus/malus/ects)
|
# 2eme ligne titre UE (bonus/malus/ects)
|
||||||
@ -220,20 +248,7 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
|||||||
else:
|
else:
|
||||||
ects_txt = ""
|
ects_txt = ""
|
||||||
# case Bonus/Malus/Rang "bmr"
|
# case Bonus/Malus/Rang "bmr"
|
||||||
fields_bmr = []
|
fields_bmr = BulletinGeneratorStandardBUT.affichage_bonus_malus(ue)
|
||||||
try:
|
|
||||||
value = float(ue.get("bonus", 0.0))
|
|
||||||
if value != 0:
|
|
||||||
fields_bmr.append(f"Bonus: {ue['bonus']}")
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
value = float(ue.get("malus", 0.0))
|
|
||||||
if value != 0:
|
|
||||||
fields_bmr.append(f"Malus: {ue['malus']}")
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
moy_ue = ue.get("moyenne", "-")
|
moy_ue = ue.get("moyenne", "-")
|
||||||
if isinstance(moy_ue, dict): # UE non capitalisées
|
if isinstance(moy_ue, dict): # UE non capitalisées
|
||||||
if self.preferences["bul_show_ue_rangs"]:
|
if self.preferences["bul_show_ue_rangs"]:
|
||||||
|
@ -202,12 +202,11 @@ def bulletin_but_xml_compat(
|
|||||||
if e.visibulletin or version == "long":
|
if e.visibulletin or version == "long":
|
||||||
x_eval = Element(
|
x_eval = Element(
|
||||||
"evaluation",
|
"evaluation",
|
||||||
jour=e.jour.isoformat() if e.jour else "",
|
date_debut=e.date_debut.isoformat()
|
||||||
heure_debut=e.heure_debut.isoformat()
|
if e.date_debut
|
||||||
if e.heure_debut
|
|
||||||
else "",
|
else "",
|
||||||
heure_fin=e.heure_fin.isoformat()
|
date_fin=e.date_fin.isoformat()
|
||||||
if e.heure_debut
|
if e.date_debut
|
||||||
else "",
|
else "",
|
||||||
coefficient=str(e.coefficient),
|
coefficient=str(e.coefficient),
|
||||||
# pas les poids en XML compat
|
# pas les poids en XML compat
|
||||||
@ -215,6 +214,12 @@ def bulletin_but_xml_compat(
|
|||||||
description=quote_xml_attr(e.description),
|
description=quote_xml_attr(e.description),
|
||||||
# notes envoyées sur 20, ceci juste pour garder trace:
|
# notes envoyées sur 20, ceci juste pour garder trace:
|
||||||
note_max_origin=str(e.note_max),
|
note_max_origin=str(e.note_max),
|
||||||
|
# --- deprecated
|
||||||
|
jour=e.date_debut.isoformat()
|
||||||
|
if e.date_debut
|
||||||
|
else "",
|
||||||
|
heure_debut=e.heure_debut(),
|
||||||
|
heure_fin=e.heure_fin(),
|
||||||
)
|
)
|
||||||
x_mod.append(x_eval)
|
x_mod.append(x_eval)
|
||||||
try:
|
try:
|
||||||
|
@ -76,6 +76,13 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
|
|||||||
f"""
|
f"""
|
||||||
<div class="titre_niveaux">
|
<div class="titre_niveaux">
|
||||||
<b>Niveaux de compétences et unités d'enseignement du BUT{deca.annee_but}</b>
|
<b>Niveaux de compétences et unités d'enseignement du BUT{deca.annee_but}</b>
|
||||||
|
<a style="margin-left: 32px;" class="stdlink" target="_blank" rel="noopener noreferrer"
|
||||||
|
href={
|
||||||
|
url_for("notes.validation_rcues", scodoc_dept=g.scodoc_dept,
|
||||||
|
etudid=deca.etud.id,
|
||||||
|
formsemestre_id=formsemestre_2.id if formsemestre_2 else formsemestre_1.id
|
||||||
|
)
|
||||||
|
}>visualiser son cursus</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="but_explanation">{deca.explanation}</div>
|
<div class="but_explanation">{deca.explanation}</div>
|
||||||
<div class="but_annee">
|
<div class="but_annee">
|
||||||
|
@ -250,7 +250,7 @@ class ModuleImplResults:
|
|||||||
).reshape(-1, 1)
|
).reshape(-1, 1)
|
||||||
|
|
||||||
# was _list_notes_evals_titles
|
# was _list_notes_evals_titles
|
||||||
def get_evaluations_completes(self, moduleimpl: ModuleImpl) -> list:
|
def get_evaluations_completes(self, moduleimpl: ModuleImpl) -> list[Evaluation]:
|
||||||
"Liste des évaluations complètes"
|
"Liste des évaluations complètes"
|
||||||
return [
|
return [
|
||||||
e for e in moduleimpl.evaluations if self.evaluations_completes_dict[e.id]
|
e for e in moduleimpl.evaluations if self.evaluations_completes_dict[e.id]
|
||||||
|
@ -78,7 +78,11 @@ def compute_sem_moys_apc_using_ects(
|
|||||||
else:
|
else:
|
||||||
ects = ects_df.to_numpy()
|
ects = ects_df.to_numpy()
|
||||||
# ects est maintenant un array nb_etuds x nb_ues
|
# ects est maintenant un array nb_etuds x nb_ues
|
||||||
|
|
||||||
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 ZeroDivisionError:
|
||||||
|
# peut arriver si aucun module... on ignore
|
||||||
|
moy_gen = pd.Series(np.NaN, index=etud_moy_ue_df.index)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
if None in ects:
|
if None in ects:
|
||||||
formation = db.session.get(Formation, formation_id)
|
formation = db.session.get(Formation, formation_id)
|
||||||
|
@ -394,7 +394,10 @@ def compute_ue_moys_classic(
|
|||||||
if sco_preferences.get_preference("use_ue_coefs", formsemestre.id):
|
if sco_preferences.get_preference("use_ue_coefs", formsemestre.id):
|
||||||
# Cas avec coefficients d'UE forcés: (on met à zéro l'UE bonus)
|
# Cas avec coefficients d'UE forcés: (on met à zéro l'UE bonus)
|
||||||
etud_coef_ue_df = pd.DataFrame(
|
etud_coef_ue_df = pd.DataFrame(
|
||||||
{ue.id: ue.coefficient if ue.type != UE_SPORT else 0.0 for ue in ues},
|
{
|
||||||
|
ue.id: (ue.coefficient or 0.0) if ue.type != UE_SPORT else 0.0
|
||||||
|
for ue in ues
|
||||||
|
},
|
||||||
index=modimpl_inscr_df.index,
|
index=modimpl_inscr_df.index,
|
||||||
columns=[ue.id for ue in ues],
|
columns=[ue.id for ue in ues],
|
||||||
)
|
)
|
||||||
|
@ -53,8 +53,8 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
self.store()
|
self.store()
|
||||||
t2 = time.time()
|
t2 = time.time()
|
||||||
log(
|
log(
|
||||||
f"""ResultatsSemestreBUT: cached formsemestre_id={formsemestre.id
|
f"""+++ ResultatsSemestreBUT: cached [{formsemestre.id
|
||||||
} ({(t1-t0):g}s +{(t2-t1):g}s)"""
|
}] ({(t1-t0):g}s +{(t2-t1):g}s) +++"""
|
||||||
)
|
)
|
||||||
|
|
||||||
def compute(self):
|
def compute(self):
|
||||||
|
@ -50,8 +50,8 @@ class ResultatsSemestreClassic(NotesTableCompat):
|
|||||||
self.store()
|
self.store()
|
||||||
t2 = time.time()
|
t2 = time.time()
|
||||||
log(
|
log(
|
||||||
f"""ResultatsSemestreClassic: cached formsemestre_id={
|
f"""+++ ResultatsSemestreClassic: cached formsemestre_id={
|
||||||
formsemestre.id} ({(t1-t0):g}s +{(t2-t1):g}s)"""
|
formsemestre.id} ({(t1-t0):g}s +{(t2-t1):g}s) +++"""
|
||||||
)
|
)
|
||||||
# recalculé (aussi rapide que de les cacher)
|
# recalculé (aussi rapide que de les cacher)
|
||||||
self.moy_min = self.etud_moy_gen.min()
|
self.moy_min = self.etud_moy_gen.min()
|
||||||
|
@ -80,8 +80,8 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
self.moy_gen_rangs_by_group = None # virtual
|
self.moy_gen_rangs_by_group = None # virtual
|
||||||
self.modimpl_inscr_df: pd.DataFrame = None
|
self.modimpl_inscr_df: pd.DataFrame = None
|
||||||
"Inscriptions: row etudid, col modimlpl_id"
|
"Inscriptions: row etudid, col modimlpl_id"
|
||||||
self.modimpls_results: ModuleImplResults = None
|
self.modimpls_results: dict[int, ModuleImplResults] = None
|
||||||
"Résultats de chaque modimpl: dict { modimpl.id : ModuleImplResults(Classique ou BUT) }"
|
"Résultats de chaque modimpl (classique ou BUT)"
|
||||||
self.etud_coef_ue_df = None
|
self.etud_coef_ue_df = None
|
||||||
"""coefs d'UE effectifs pour chaque étudiant (pour form. classiques)"""
|
"""coefs d'UE effectifs pour chaque étudiant (pour form. classiques)"""
|
||||||
self.modimpl_coefs_df: pd.DataFrame = None
|
self.modimpl_coefs_df: pd.DataFrame = None
|
||||||
@ -192,6 +192,17 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
*[mr.etudids_attente for mr in self.modimpls_results.values()]
|
*[mr.etudids_attente for mr in self.modimpls_results.values()]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# # Etat des évaluations
|
||||||
|
# # (se substitue à do_evaluation_etat, sans les moyennes par groupes)
|
||||||
|
# def get_evaluations_etats(evaluation_id: int) -> dict:
|
||||||
|
# """Renvoie dict avec les clés:
|
||||||
|
# last_modif
|
||||||
|
# nb_evals_completes
|
||||||
|
# nb_evals_en_cours
|
||||||
|
# nb_evals_vides
|
||||||
|
# attente
|
||||||
|
# """
|
||||||
|
|
||||||
# --- JURY...
|
# --- JURY...
|
||||||
def get_formsemestre_validations(self) -> ValidationsSemestre:
|
def get_formsemestre_validations(self) -> ValidationsSemestre:
|
||||||
"""Load validations if not already stored, set attribute and return value"""
|
"""Load validations if not already stored, set attribute and return value"""
|
||||||
|
@ -16,7 +16,13 @@ from app import db, 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
|
||||||
from app.models import Identite, FormSemestre, ModuleImpl, ScolarAutorisationInscription
|
from app.models import (
|
||||||
|
Evaluation,
|
||||||
|
Identite,
|
||||||
|
FormSemestre,
|
||||||
|
ModuleImpl,
|
||||||
|
ScolarAutorisationInscription,
|
||||||
|
)
|
||||||
from app.scodoc.codes_cursus import UE_SPORT, DEF
|
from app.scodoc.codes_cursus import UE_SPORT, DEF
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
@ -389,7 +395,7 @@ class NotesTableCompat(ResultatsSemestre):
|
|||||||
"ects_total": ects_total,
|
"ects_total": ects_total,
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_evals_in_mod(self, moduleimpl_id: int) -> list[dict]:
|
def get_modimpl_evaluations_completes(self, moduleimpl_id: int) -> list[Evaluation]:
|
||||||
"""Liste d'informations (compat NotesTable) sur évaluations completes
|
"""Liste d'informations (compat NotesTable) sur évaluations completes
|
||||||
de ce module.
|
de ce module.
|
||||||
Évaluation "complete" ssi toutes notes saisies ou en attente.
|
Évaluation "complete" ssi toutes notes saisies ou en attente.
|
||||||
@ -398,34 +404,24 @@ class NotesTableCompat(ResultatsSemestre):
|
|||||||
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
|
||||||
evals_results = []
|
evaluations = []
|
||||||
for e in modimpl.evaluations:
|
for e in modimpl.evaluations:
|
||||||
if modimpl_results.evaluations_completes_dict.get(e.id, False):
|
if modimpl_results.evaluations_completes_dict.get(e.id, False):
|
||||||
d = e.to_dict()
|
evaluations.append(e)
|
||||||
d["heure_debut"] = e.heure_debut # datetime.time
|
|
||||||
d["heure_fin"] = e.heure_fin
|
|
||||||
d["jour"] = e.jour # datetime
|
|
||||||
d["notes"] = {
|
|
||||||
etud.id: {
|
|
||||||
"etudid": etud.id,
|
|
||||||
"value": modimpl_results.evals_notes[e.id][etud.id],
|
|
||||||
}
|
|
||||||
for etud in self.etuds
|
|
||||||
}
|
|
||||||
d["etat"] = {
|
|
||||||
"evalattente": modimpl_results.evaluations_etat[e.id].nb_attente,
|
|
||||||
}
|
|
||||||
evals_results.append(d)
|
|
||||||
elif e.id not in modimpl_results.evaluations_completes_dict:
|
elif e.id not in modimpl_results.evaluations_completes_dict:
|
||||||
# ne devrait pas arriver ? XXX
|
# ne devrait pas arriver ? XXX
|
||||||
log(
|
log(
|
||||||
f"Warning: 220213 get_evals_in_mod {e.id} not in mod {moduleimpl_id} ?"
|
f"Warning: 220213 get_modimpl_evaluations_completes {e.id} not in mod {moduleimpl_id} ?"
|
||||||
)
|
)
|
||||||
return evals_results
|
return evaluations
|
||||||
|
|
||||||
|
def get_evaluations_etats(self) -> list[dict]:
|
||||||
|
"""Liste de toutes les évaluations du semestre
|
||||||
|
[ {...evaluation et son etat...} ]"""
|
||||||
|
# TODO: à moderniser (voir dans ResultatsSemestre)
|
||||||
|
# utilisé par
|
||||||
|
# do_evaluation_etat_in_sem
|
||||||
|
|
||||||
def get_evaluations_etats(self):
|
|
||||||
"""[ {...evaluation et son etat...} ]"""
|
|
||||||
# TODO: à moderniser
|
|
||||||
from app.scodoc import sco_evaluations
|
from app.scodoc import sco_evaluations
|
||||||
|
|
||||||
if not hasattr(self, "_evaluations_etats"):
|
if not hasattr(self, "_evaluations_etats"):
|
||||||
|
@ -85,7 +85,9 @@ Adresses d'origine:
|
|||||||
)
|
)
|
||||||
|
|
||||||
current_app.logger.info(
|
current_app.logger.info(
|
||||||
f"""email sent to{' (mode test)' if email_test_mode_address else ''}: {msg.recipients}
|
f"""email sent to{
|
||||||
|
' (mode test)' if email_test_mode_address else ''
|
||||||
|
}: {msg.recipients}
|
||||||
from sender {msg.sender}
|
from sender {msg.sender}
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
@ -98,7 +100,8 @@ def get_from_addr(dept_acronym: str = None):
|
|||||||
"""L'adresse "from" à utiliser pour envoyer un mail
|
"""L'adresse "from" à utiliser pour envoyer un mail
|
||||||
|
|
||||||
Si le departement est spécifié, ou si l'attribut `g.scodoc_dept`existe,
|
Si le departement est spécifié, ou si l'attribut `g.scodoc_dept`existe,
|
||||||
prend le `email_from_addr` des préférences de ce département si ce champ est non vide.
|
prend le `email_from_addr` des préférences de ce département si ce champ
|
||||||
|
est non vide.
|
||||||
Sinon, utilise le paramètre global `email_from_addr`.
|
Sinon, utilise le paramètre global `email_from_addr`.
|
||||||
Sinon, la variable de config `SCODOC_MAIL_FROM`.
|
Sinon, la variable de config `SCODOC_MAIL_FROM`.
|
||||||
"""
|
"""
|
||||||
|
88
app/forms/main/config_assiduites.py
Normal file
88
app/forms/main/config_assiduites.py
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
# -*- mode: python -*-
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# ScoDoc
|
||||||
|
#
|
||||||
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
Formulaire configuration Module Assiduités
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import SubmitField, DecimalField
|
||||||
|
from wtforms.fields.simple import StringField
|
||||||
|
from wtforms.widgets import TimeInput
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class TimeField(StringField):
|
||||||
|
"""HTML5 time input."""
|
||||||
|
|
||||||
|
widget = TimeInput()
|
||||||
|
|
||||||
|
def __init__(self, label=None, validators=None, fmt="%H:%M:%S", **kwargs):
|
||||||
|
super(TimeField, self).__init__(label, validators, **kwargs)
|
||||||
|
self.fmt = fmt
|
||||||
|
self.data = None
|
||||||
|
|
||||||
|
def _value(self):
|
||||||
|
if self.raw_data:
|
||||||
|
return " ".join(self.raw_data)
|
||||||
|
if self.data and isinstance(self.data, str):
|
||||||
|
self.data = datetime.time(*map(int, self.data.split(":")))
|
||||||
|
return self.data and self.data.strftime(self.fmt) or ""
|
||||||
|
|
||||||
|
def process_formdata(self, valuelist):
|
||||||
|
if valuelist:
|
||||||
|
time_str = " ".join(valuelist)
|
||||||
|
try:
|
||||||
|
components = time_str.split(":")
|
||||||
|
hour = 0
|
||||||
|
minutes = 0
|
||||||
|
seconds = 0
|
||||||
|
if len(components) in range(2, 4):
|
||||||
|
hour = int(components[0])
|
||||||
|
minutes = int(components[1])
|
||||||
|
|
||||||
|
if len(components) == 3:
|
||||||
|
seconds = int(components[2])
|
||||||
|
else:
|
||||||
|
raise ValueError
|
||||||
|
self.data = datetime.time(hour, minutes, seconds)
|
||||||
|
except ValueError:
|
||||||
|
self.data = None
|
||||||
|
raise ValueError(self.gettext("Not a valid time string"))
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigAssiduitesForm(FlaskForm):
|
||||||
|
"Formulaire paramétrage Module Assiduités"
|
||||||
|
|
||||||
|
morning_time = TimeField("Début de la journée")
|
||||||
|
lunch_time = TimeField("Heure de midi (date pivot entre Matin et Après Midi)")
|
||||||
|
afternoon_time = TimeField("Fin de la journée")
|
||||||
|
|
||||||
|
tick_time = DecimalField("Granularité de la Time Line (temps en minutes)", places=0)
|
||||||
|
|
||||||
|
submit = SubmitField("Valider")
|
||||||
|
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
72
app/forms/main/config_personalized_links.py
Normal file
72
app/forms/main/config_personalized_links.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
"""
|
||||||
|
Formulaire configuration liens personalisés (menu "Liens")
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import g, url_for
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import FieldList, Form, validators
|
||||||
|
from wtforms.fields.simple import BooleanField, StringField, SubmitField
|
||||||
|
|
||||||
|
from app.models import ScoDocSiteConfig
|
||||||
|
|
||||||
|
|
||||||
|
class _PersonalizedLinksForm(FlaskForm):
|
||||||
|
"form. définition des liens personnalisés"
|
||||||
|
# construit dynamiquement ci-dessous
|
||||||
|
|
||||||
|
|
||||||
|
def PersonalizedLinksForm() -> _PersonalizedLinksForm:
|
||||||
|
"Création d'un formulaire pour éditer les liens"
|
||||||
|
|
||||||
|
# Formulaire dynamique, on créé une classe ad-hoc
|
||||||
|
class F(_PersonalizedLinksForm):
|
||||||
|
pass
|
||||||
|
|
||||||
|
F.links_by_id = dict(enumerate(ScoDocSiteConfig.get_perso_links()))
|
||||||
|
|
||||||
|
def _gen_link_form(idx):
|
||||||
|
setattr(
|
||||||
|
F,
|
||||||
|
f"link_{idx}",
|
||||||
|
StringField(
|
||||||
|
f"Titre",
|
||||||
|
validators=[
|
||||||
|
validators.Optional(),
|
||||||
|
validators.Length(min=1, max=80),
|
||||||
|
],
|
||||||
|
default="",
|
||||||
|
render_kw={"size": 6},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
setattr(
|
||||||
|
F,
|
||||||
|
f"link_url_{idx}",
|
||||||
|
StringField(
|
||||||
|
f"URL",
|
||||||
|
description="adresse, incluant le http.",
|
||||||
|
validators=[
|
||||||
|
validators.Optional(),
|
||||||
|
validators.URL(),
|
||||||
|
validators.Length(min=1, max=256),
|
||||||
|
],
|
||||||
|
default="",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
setattr(
|
||||||
|
F,
|
||||||
|
f"link_with_args_{idx}",
|
||||||
|
BooleanField(
|
||||||
|
f"ajouter arguments",
|
||||||
|
description="query string avec ids",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Initialise un champ de saisie par lien
|
||||||
|
for idx in F.links_by_id:
|
||||||
|
_gen_link_form(idx)
|
||||||
|
_gen_link_form("new")
|
||||||
|
|
||||||
|
F.submit = SubmitField("Valider")
|
||||||
|
F.cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
||||||
|
|
||||||
|
return F()
|
@ -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
|
||||||
|
386
app/models/assiduites.py
Normal file
386
app/models/assiduites.py
Normal file
@ -0,0 +1,386 @@
|
|||||||
|
# -*- coding: UTF-8 -*
|
||||||
|
"""Gestion de l'assiduité (assiduités + justificatifs)
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from app import db, log
|
||||||
|
from app.models import ModuleImpl, Scolog
|
||||||
|
from app.models.etudiants import Identite
|
||||||
|
from app.auth.models import User
|
||||||
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
|
from app.scodoc.sco_utils import (
|
||||||
|
EtatAssiduite,
|
||||||
|
EtatJustificatif,
|
||||||
|
localize_datetime,
|
||||||
|
)
|
||||||
|
|
||||||
|
from flask_sqlalchemy.query import Query
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
description = 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)
|
||||||
|
|
||||||
|
external_data = db.Column(db.JSON, nullable=True)
|
||||||
|
|
||||||
|
# Déclare la relation "joined" car on va très souvent vouloir récupérer
|
||||||
|
# l'étudiant en même tant que l'assiduité (perf.: évite nouvelle requete SQL)
|
||||||
|
etudiant = db.relationship("Identite", back_populates="assiduites", lazy="joined")
|
||||||
|
|
||||||
|
def to_dict(self, format_api=True) -> dict:
|
||||||
|
"""Retourne la représentation json de l'assiduité"""
|
||||||
|
etat = self.etat
|
||||||
|
username = self.user_id
|
||||||
|
if format_api:
|
||||||
|
etat = EtatAssiduite.inverse().get(self.etat).name
|
||||||
|
if self.user_id is not None:
|
||||||
|
user: User = db.session.get(User, self.user_id)
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
username = "Non renseigné"
|
||||||
|
else:
|
||||||
|
username = user.get_prenomnom()
|
||||||
|
data = {
|
||||||
|
"assiduite_id": self.id,
|
||||||
|
"etudid": self.etudid,
|
||||||
|
"code_nip": self.etudiant.code_nip,
|
||||||
|
"moduleimpl_id": self.moduleimpl_id,
|
||||||
|
"date_debut": self.date_debut,
|
||||||
|
"date_fin": self.date_fin,
|
||||||
|
"etat": etat,
|
||||||
|
"desc": self.description,
|
||||||
|
"entry_date": self.entry_date,
|
||||||
|
"user_id": username,
|
||||||
|
"est_just": self.est_just,
|
||||||
|
"external_data": self.external_data,
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"chaine pour journaux et debug (lisible par humain français)"
|
||||||
|
try:
|
||||||
|
etat_str = EtatAssiduite(self.etat).name.lower().capitalize()
|
||||||
|
except ValueError:
|
||||||
|
etat_str = "Invalide"
|
||||||
|
return f"""{etat_str} {
|
||||||
|
"just." if self.est_just else "non just."
|
||||||
|
} de {
|
||||||
|
self.date_debut.strftime("%d/%m/%Y %Hh%M")
|
||||||
|
} à {
|
||||||
|
self.date_fin.strftime("%d/%m/%Y %Hh%M")
|
||||||
|
}"""
|
||||||
|
|
||||||
|
@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,
|
||||||
|
external_data: dict = None,
|
||||||
|
) -> object or int:
|
||||||
|
"""Créer une nouvelle assiduité pour l'étudiant"""
|
||||||
|
# Vérification de non duplication des périodes
|
||||||
|
assiduites: Query = 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 not est_just:
|
||||||
|
est_just = (
|
||||||
|
len(_get_assiduites_justif(etud.etudid, date_debut, date_fin)) > 0
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
description=description,
|
||||||
|
entry_date=entry_date,
|
||||||
|
user_id=user_id,
|
||||||
|
est_just=est_just,
|
||||||
|
external_data=external_data,
|
||||||
|
)
|
||||||
|
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,
|
||||||
|
description=description,
|
||||||
|
entry_date=entry_date,
|
||||||
|
user_id=user_id,
|
||||||
|
est_just=est_just,
|
||||||
|
external_data=external_data,
|
||||||
|
)
|
||||||
|
db.session.add(nouv_assiduite)
|
||||||
|
log(f"create_assiduite: {etud.id} {nouv_assiduite}")
|
||||||
|
Scolog.logdb(
|
||||||
|
method="create_assiduite",
|
||||||
|
etudid=etud.id,
|
||||||
|
msg=f"assiduité: {nouv_assiduite}",
|
||||||
|
)
|
||||||
|
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())
|
||||||
|
|
||||||
|
# Déclare la relation "joined" car on va très souvent vouloir récupérer
|
||||||
|
# l'étudiant en même tant que le justificatif (perf.: évite nouvelle requete SQL)
|
||||||
|
etudiant = db.relationship(
|
||||||
|
"Identite", back_populates="justificatifs", lazy="joined"
|
||||||
|
)
|
||||||
|
|
||||||
|
external_data = db.Column(db.JSON, nullable=True)
|
||||||
|
|
||||||
|
def to_dict(self, format_api: bool = False) -> dict:
|
||||||
|
"""transformation de l'objet en dictionnaire sérialisable"""
|
||||||
|
|
||||||
|
etat = self.etat
|
||||||
|
username = self.user_id
|
||||||
|
|
||||||
|
if format_api:
|
||||||
|
etat = EtatJustificatif.inverse().get(self.etat).name
|
||||||
|
if self.user_id is not None:
|
||||||
|
user: User = db.session.get(User, self.user_id)
|
||||||
|
if user is None:
|
||||||
|
username = "Non renseigné"
|
||||||
|
else:
|
||||||
|
username = user.get_prenomnom()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"justif_id": self.justif_id,
|
||||||
|
"etudid": self.etudid,
|
||||||
|
"code_nip": self.etudiant.code_nip,
|
||||||
|
"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": username,
|
||||||
|
"external_data": self.external_data,
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"chaine pour journaux et debug (lisible par humain français)"
|
||||||
|
try:
|
||||||
|
etat_str = EtatJustificatif(self.etat).name
|
||||||
|
except ValueError:
|
||||||
|
etat_str = "Invalide"
|
||||||
|
return f"""Justificatif {etat_str} de {
|
||||||
|
self.date_debut.strftime("%d/%m/%Y %Hh%M")
|
||||||
|
} à {
|
||||||
|
self.date_fin.strftime("%d/%m/%Y %Hh%M")
|
||||||
|
}"""
|
||||||
|
|
||||||
|
@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,
|
||||||
|
external_data: dict = 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,
|
||||||
|
external_data=external_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(nouv_justificatif)
|
||||||
|
|
||||||
|
log(f"create_justificatif: {etud.id} {nouv_justificatif}")
|
||||||
|
Scolog.logdb(
|
||||||
|
method="create_justificatif",
|
||||||
|
etudid=etud.id,
|
||||||
|
msg=f"justificatif: {nouv_justificatif}",
|
||||||
|
)
|
||||||
|
return nouv_justificatif
|
||||||
|
|
||||||
|
|
||||||
|
def is_period_conflicting(
|
||||||
|
date_debut: datetime,
|
||||||
|
date_fin: datetime,
|
||||||
|
collection: Query,
|
||||||
|
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(
|
||||||
|
etudid: int, justificatifs: list[Justificatif] = None, reset: bool = False
|
||||||
|
) -> list[int]:
|
||||||
|
"""
|
||||||
|
compute_assiduites_justified_faster
|
||||||
|
|
||||||
|
Args:
|
||||||
|
etudid (int): l'identifiant de l'étudiant
|
||||||
|
justificatifs (list[Justificatif]): La liste des justificatifs qui seront utilisés
|
||||||
|
reset (bool, optional): remet à false les assiduites non justifiés. Defaults to False.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[int]: la liste des assiduités qui ont été justifiées.
|
||||||
|
"""
|
||||||
|
if justificatifs is None:
|
||||||
|
justificatifs: Justificatif = Justificatif.query.filter_by(etudid=etudid).all()
|
||||||
|
|
||||||
|
assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid)
|
||||||
|
|
||||||
|
assiduites_justifiees: list[int] = []
|
||||||
|
|
||||||
|
for assi in assiduites:
|
||||||
|
if any(
|
||||||
|
assi.date_debut >= j.date_debut and assi.date_fin <= j.date_fin
|
||||||
|
for j in justificatifs
|
||||||
|
):
|
||||||
|
assi.est_just = True
|
||||||
|
assiduites_justifiees.append(assi.assiduite_id)
|
||||||
|
db.session.add(assi)
|
||||||
|
elif reset:
|
||||||
|
assi.est_just = False
|
||||||
|
db.session.add(assi)
|
||||||
|
db.session.commit()
|
||||||
|
return assiduites_justifiees
|
||||||
|
|
||||||
|
|
||||||
|
def get_assiduites_justif(assiduite_id: int, long: bool):
|
||||||
|
assi: Assiduite = Assiduite.query.get_or_404(assiduite_id)
|
||||||
|
return _get_assiduites_justif(assi.etudid, assi.date_debut, assi.date_fin, long)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_assiduites_justif(
|
||||||
|
etudid: int, date_debut: datetime, date_fin: datetime, long: bool = False
|
||||||
|
):
|
||||||
|
justifs: Justificatif = Justificatif.query.filter(
|
||||||
|
Justificatif.etudid == etudid,
|
||||||
|
Justificatif.date_debut <= date_debut,
|
||||||
|
Justificatif.date_fin >= date_fin,
|
||||||
|
)
|
||||||
|
|
||||||
|
return [j.justif_id if not long else j.to_dict(True) for j in justifs]
|
@ -3,11 +3,17 @@
|
|||||||
"""Model : site config WORK IN PROGRESS #WIP
|
"""Model : site config WORK IN PROGRESS #WIP
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
from flask import flash
|
from flask import flash
|
||||||
from app import current_app, db, log
|
from app import current_app, db, log
|
||||||
from app.comp import bonus_spo
|
from app.comp import bonus_spo
|
||||||
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
from datetime import time
|
||||||
|
|
||||||
from app.scodoc.codes_cursus import (
|
from app.scodoc.codes_cursus import (
|
||||||
ABAN,
|
ABAN,
|
||||||
ABL,
|
ABL,
|
||||||
@ -96,6 +102,10 @@ class ScoDocSiteConfig(db.Model):
|
|||||||
"cas_logout_route": str,
|
"cas_logout_route": str,
|
||||||
"cas_validate_route": str,
|
"cas_validate_route": str,
|
||||||
"cas_attribute_id": str,
|
"cas_attribute_id": str,
|
||||||
|
# Assiduités
|
||||||
|
"morning_time": str,
|
||||||
|
"lunch_time": str,
|
||||||
|
"afternoon_time": str,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, name, value):
|
def __init__(self, name, value):
|
||||||
@ -247,7 +257,7 @@ class ScoDocSiteConfig(db.Model):
|
|||||||
cfg = ScoDocSiteConfig.query.filter_by(name=name).first()
|
cfg = ScoDocSiteConfig.query.filter_by(name=name).first()
|
||||||
if cfg is None:
|
if cfg is None:
|
||||||
return default
|
return default
|
||||||
return cfg.value or ""
|
return cls.NAMES.get(name, lambda x: x)(cfg.value or "")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def set(cls, name: str, value: str) -> bool:
|
def set(cls, name: str, value: str) -> bool:
|
||||||
@ -336,3 +346,47 @@ class ScoDocSiteConfig(db.Model):
|
|||||||
log(f"set_month_debut_periode2({month})")
|
log(f"set_month_debut_periode2({month})")
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_perso_links(cls) -> list["PersonalizedLink"]:
|
||||||
|
"Return links"
|
||||||
|
data_links = cls.get("personalized_links")
|
||||||
|
if not data_links:
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
links_dict = json.loads(data_links)
|
||||||
|
except json.decoder.JSONDecodeError as exc:
|
||||||
|
# Corrupted data ? erase content
|
||||||
|
cls.set("personalized_links", "")
|
||||||
|
raise ScoValueError(
|
||||||
|
"Attention: liens personnalisés erronés: ils ont été effacés."
|
||||||
|
)
|
||||||
|
return [PersonalizedLink(**item) for item in links_dict]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def set_perso_links(cls, links: list["PersonalizedLink"] = None):
|
||||||
|
"Store all links"
|
||||||
|
if not links:
|
||||||
|
links = []
|
||||||
|
links_dict = [link.to_dict() for link in links]
|
||||||
|
data_links = json.dumps(links_dict)
|
||||||
|
cls.set("personalized_links", data_links)
|
||||||
|
|
||||||
|
|
||||||
|
class PersonalizedLink:
|
||||||
|
def __init__(self, title: str = "", url: str = "", with_args: bool = False):
|
||||||
|
self.title = str(title or "")
|
||||||
|
self.url = str(url or "")
|
||||||
|
self.with_args = bool(with_args)
|
||||||
|
|
||||||
|
def get_url(self, params: dict = {}) -> str:
|
||||||
|
if not self.with_args:
|
||||||
|
return self.url
|
||||||
|
query_string = urllib.parse.urlencode(params)
|
||||||
|
if "?" in self.url:
|
||||||
|
return self.url + "&" + query_string
|
||||||
|
return self.url + "?" + query_string
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
"as dict"
|
||||||
|
return {"title": self.title, "url": self.url, "with_args": self.with_args}
|
||||||
|
@ -73,6 +73,12 @@ class Identite(db.Model):
|
|||||||
passive_deletes=True,
|
passive_deletes=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Relations avec les assiduites et les justificatifs
|
||||||
|
assiduites = db.relationship("Assiduite", back_populates="etudiant", lazy="dynamic")
|
||||||
|
justificatifs = db.relationship(
|
||||||
|
"Justificatif", back_populates="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}>"
|
||||||
|
@ -5,17 +5,28 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
from app import db
|
from flask import g, url_for
|
||||||
|
from flask_login import current_user
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from app import db, log
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
|
from app.models.events import ScolarNews
|
||||||
from app.models.moduleimpls import ModuleImpl
|
from app.models.moduleimpls import ModuleImpl
|
||||||
from app.models.notes import NotesNotes
|
from app.models.notes import NotesNotes
|
||||||
from app.models.ues import UniteEns
|
from app.models.ues import UniteEns
|
||||||
|
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc import sco_cache
|
||||||
import app.scodoc.notesdb as ndb
|
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||||
|
import app.scodoc.sco_utils as scu
|
||||||
|
from app.scodoc.sco_xml import quote_xml_attr
|
||||||
|
|
||||||
|
MAX_EVALUATION_DURATION = datetime.timedelta(days=365)
|
||||||
|
NOON = datetime.time(12, 00)
|
||||||
DEFAULT_EVALUATION_TIME = datetime.time(8, 0)
|
DEFAULT_EVALUATION_TIME = datetime.time(8, 0)
|
||||||
|
|
||||||
|
VALID_EVALUATION_TYPES = {0, 1, 2}
|
||||||
|
|
||||||
|
|
||||||
class Evaluation(db.Model):
|
class Evaluation(db.Model):
|
||||||
"""Evaluation (contrôle, examen, ...)"""
|
"""Evaluation (contrôle, examen, ...)"""
|
||||||
@ -27,15 +38,15 @@ class Evaluation(db.Model):
|
|||||||
moduleimpl_id = db.Column(
|
moduleimpl_id = db.Column(
|
||||||
db.Integer, db.ForeignKey("notes_moduleimpl.id"), index=True
|
db.Integer, db.ForeignKey("notes_moduleimpl.id"), index=True
|
||||||
)
|
)
|
||||||
jour = db.Column(db.Date)
|
date_debut = db.Column(db.DateTime(timezone=True), nullable=True)
|
||||||
heure_debut = db.Column(db.Time)
|
date_fin = db.Column(db.DateTime(timezone=True), nullable=True)
|
||||||
heure_fin = db.Column(db.Time)
|
|
||||||
description = db.Column(db.Text)
|
description = db.Column(db.Text)
|
||||||
note_max = db.Column(db.Float)
|
note_max = db.Column(db.Float)
|
||||||
coefficient = db.Column(db.Float)
|
coefficient = db.Column(db.Float)
|
||||||
visibulletin = db.Column(
|
visibulletin = db.Column(
|
||||||
db.Boolean, nullable=False, default=True, server_default="true"
|
db.Boolean, nullable=False, default=True, server_default="true"
|
||||||
)
|
)
|
||||||
|
"visible sur les bulletins version intermédiaire"
|
||||||
publish_incomplete = db.Column(
|
publish_incomplete = db.Column(
|
||||||
db.Boolean, nullable=False, default=False, server_default="false"
|
db.Boolean, nullable=False, default=False, server_default="false"
|
||||||
)
|
)
|
||||||
@ -50,47 +61,108 @@ class Evaluation(db.Model):
|
|||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"""<Evaluation {self.id} {
|
return f"""<Evaluation {self.id} {
|
||||||
self.jour.isoformat() if self.jour else ''} "{
|
self.date_debut.isoformat() if self.date_debut else ''} "{
|
||||||
self.description[:16] if self.description else ''}">"""
|
self.description[:16] if self.description else ''}">"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(
|
||||||
|
cls,
|
||||||
|
moduleimpl: ModuleImpl = None,
|
||||||
|
date_debut: datetime.datetime = None,
|
||||||
|
date_fin: datetime.datetime = None,
|
||||||
|
description=None,
|
||||||
|
note_max=None,
|
||||||
|
coefficient=None,
|
||||||
|
visibulletin=None,
|
||||||
|
publish_incomplete=None,
|
||||||
|
evaluation_type=None,
|
||||||
|
numero=None,
|
||||||
|
**kw, # ceci pour absorber les éventuel arguments excedentaires
|
||||||
|
):
|
||||||
|
"""Create an evaluation. Check permission and all arguments.
|
||||||
|
Ne crée pas les poids vers les UEs.
|
||||||
|
"""
|
||||||
|
if not moduleimpl.can_edit_evaluation(current_user):
|
||||||
|
raise AccessDenied(
|
||||||
|
f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
|
||||||
|
)
|
||||||
|
args = locals()
|
||||||
|
del args["cls"]
|
||||||
|
del args["kw"]
|
||||||
|
check_convert_evaluation_args(moduleimpl, args)
|
||||||
|
# Check numeros
|
||||||
|
Evaluation.moduleimpl_evaluation_renumber(moduleimpl, only_if_unumbered=True)
|
||||||
|
if not "numero" in args or args["numero"] is None:
|
||||||
|
args["numero"] = cls.get_new_numero(moduleimpl, args["date_debut"])
|
||||||
|
#
|
||||||
|
evaluation = Evaluation(**args)
|
||||||
|
sco_cache.invalidate_formsemestre(formsemestre_id=moduleimpl.formsemestre_id)
|
||||||
|
url = url_for(
|
||||||
|
"notes.moduleimpl_status",
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
moduleimpl_id=moduleimpl.id,
|
||||||
|
)
|
||||||
|
log(f"created evaluation in {moduleimpl.module.titre_str()}")
|
||||||
|
ScolarNews.add(
|
||||||
|
typ=ScolarNews.NEWS_NOTE,
|
||||||
|
obj=moduleimpl.id,
|
||||||
|
text=f"""Création d'une évaluation dans <a href="{url}">{
|
||||||
|
moduleimpl.module.titre_str()}</a>""",
|
||||||
|
url=url,
|
||||||
|
)
|
||||||
|
return evaluation
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_new_numero(
|
||||||
|
cls, moduleimpl: ModuleImpl, date_debut: datetime.datetime
|
||||||
|
) -> int:
|
||||||
|
"""Get a new numero for an evaluation in this moduleimpl
|
||||||
|
If necessary, renumber existing evals to make room for a new one.
|
||||||
|
"""
|
||||||
|
n = None
|
||||||
|
# Détermine le numero grâce à la date
|
||||||
|
# Liste des eval existantes triées par date, la plus ancienne en tete
|
||||||
|
evaluations = moduleimpl.evaluations.order_by(Evaluation.date_debut).all()
|
||||||
|
if date_debut is not None:
|
||||||
|
next_eval = None
|
||||||
|
t = date_debut
|
||||||
|
for e in evaluations:
|
||||||
|
if e.date_debut and e.date_debut > t:
|
||||||
|
next_eval = e
|
||||||
|
break
|
||||||
|
if next_eval:
|
||||||
|
n = _moduleimpl_evaluation_insert_before(evaluations, next_eval)
|
||||||
|
else:
|
||||||
|
n = None # à placer en fin
|
||||||
|
if n is None: # pas de date ou en fin:
|
||||||
|
if evaluations:
|
||||||
|
n = evaluations[-1].numero + 1
|
||||||
|
else:
|
||||||
|
n = 0 # the only one
|
||||||
|
return n
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
"Représentation dict (riche, compat ScoDoc 7)"
|
"Représentation dict (riche, compat ScoDoc 7)"
|
||||||
e = dict(self.__dict__)
|
e_dict = dict(self.__dict__)
|
||||||
e.pop("_sa_instance_state", None)
|
e_dict.pop("_sa_instance_state", None)
|
||||||
# ScoDoc7 output_formators
|
# ScoDoc7 output_formators
|
||||||
e["evaluation_id"] = self.id
|
e_dict["evaluation_id"] = self.id
|
||||||
e["jour"] = e["jour"].strftime("%d/%m/%Y") if e["jour"] else ""
|
e_dict["date_debut"] = self.date_debut.isoformat() if self.date_debut else None
|
||||||
if self.jour is None:
|
e_dict["date_fin"] = self.date_debut.isoformat() if self.date_fin else None
|
||||||
e["date_debut"] = None
|
e_dict["numero"] = self.numero or 0
|
||||||
e["date_fin"] = None
|
e_dict["poids"] = self.get_ue_poids_dict() # { ue_id : poids }
|
||||||
else:
|
|
||||||
e["date_debut"] = datetime.datetime.combine(
|
# Deprecated
|
||||||
self.jour, self.heure_debut or datetime.time(0, 0)
|
e_dict["jour"] = self.date_debut.strftime("%d/%m/%Y") if self.date_debut else ""
|
||||||
).isoformat()
|
|
||||||
e["date_fin"] = datetime.datetime.combine(
|
return evaluation_enrich_dict(self, e_dict)
|
||||||
self.jour, self.heure_fin or datetime.time(0, 0)
|
|
||||||
).isoformat()
|
|
||||||
e["numero"] = ndb.int_null_is_zero(e["numero"])
|
|
||||||
e["poids"] = self.get_ue_poids_dict() # { ue_id : poids }
|
|
||||||
return evaluation_enrich_dict(e)
|
|
||||||
|
|
||||||
def to_dict_api(self) -> dict:
|
def to_dict_api(self) -> dict:
|
||||||
"Représentation dict pour API JSON"
|
"Représentation dict pour API JSON"
|
||||||
if self.jour is None:
|
|
||||||
date_debut = None
|
|
||||||
date_fin = None
|
|
||||||
else:
|
|
||||||
date_debut = datetime.datetime.combine(
|
|
||||||
self.jour, self.heure_debut or datetime.time(0, 0)
|
|
||||||
).isoformat()
|
|
||||||
date_fin = datetime.datetime.combine(
|
|
||||||
self.jour, self.heure_fin or datetime.time(0, 0)
|
|
||||||
).isoformat()
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"coefficient": self.coefficient,
|
"coefficient": self.coefficient,
|
||||||
"date_debut": date_debut,
|
"date_debut": self.date_debut.isoformat() if self.date_debut else "",
|
||||||
"date_fin": date_fin,
|
"date_fin": self.date_fin.isoformat() if self.date_fin else "",
|
||||||
"description": self.description,
|
"description": self.description,
|
||||||
"evaluation_type": self.evaluation_type,
|
"evaluation_type": self.evaluation_type,
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
@ -99,39 +171,135 @@ class Evaluation(db.Model):
|
|||||||
"numero": self.numero,
|
"numero": self.numero,
|
||||||
"poids": self.get_ue_poids_dict(),
|
"poids": self.get_ue_poids_dict(),
|
||||||
"publish_incomplete": self.publish_incomplete,
|
"publish_incomplete": self.publish_incomplete,
|
||||||
"visi_bulletin": self.visibulletin,
|
"visibulletin": self.visibulletin,
|
||||||
|
# Deprecated (supprimer avant #sco9.7)
|
||||||
|
"date": self.date_debut.date().isoformat() if self.date_debut else "",
|
||||||
|
"heure_debut": self.date_debut.time().isoformat()
|
||||||
|
if self.date_debut
|
||||||
|
else "",
|
||||||
|
"heure_fin": self.date_fin.time().isoformat() if self.date_fin else "",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def to_dict_bul(self) -> dict:
|
||||||
|
"dict pour les bulletins json"
|
||||||
|
# c'est la version API avec quelques champs legacy en plus
|
||||||
|
e_dict = self.to_dict_api()
|
||||||
|
# Pour les bulletins (json ou xml), quote toujours la description
|
||||||
|
e_dict["description"] = quote_xml_attr(self.description or "")
|
||||||
|
# deprecated fields:
|
||||||
|
e_dict["evaluation_id"] = self.id
|
||||||
|
e_dict["jour"] = e_dict["date_debut"] # chaine iso
|
||||||
|
e_dict["heure_debut"] = (
|
||||||
|
self.date_debut.time().isoformat() if self.date_debut else ""
|
||||||
|
)
|
||||||
|
e_dict["heure_fin"] = self.date_fin.time().isoformat() if self.date_fin else ""
|
||||||
|
|
||||||
|
return e_dict
|
||||||
|
|
||||||
def from_dict(self, data):
|
def from_dict(self, data):
|
||||||
"""Set evaluation attributes from given dict values."""
|
"""Set evaluation attributes from given dict values."""
|
||||||
check_evaluation_args(data)
|
check_convert_evaluation_args(self.moduleimpl, data)
|
||||||
|
if data.get("numero") is None:
|
||||||
|
data["numero"] = Evaluation.get_max_numero(self.moduleimpl.id) + 1
|
||||||
for k in self.__dict__.keys():
|
for k in self.__dict__.keys():
|
||||||
if k != "_sa_instance_state" and k != "id" and k in data:
|
if k != "_sa_instance_state" and k != "id" and k in data:
|
||||||
setattr(self, k, data[k])
|
setattr(self, k, data[k])
|
||||||
|
|
||||||
def descr_heure(self) -> str:
|
@classmethod
|
||||||
"Description de la plage horaire pour affichages"
|
def get_max_numero(cls, moduleimpl_id: int) -> int:
|
||||||
if self.heure_debut and (
|
"""Return max numero among evaluations in this
|
||||||
not self.heure_fin or self.heure_fin == self.heure_debut
|
moduleimpl (0 if None)
|
||||||
|
"""
|
||||||
|
max_num = (
|
||||||
|
db.session.query(sa.sql.functions.max(Evaluation.numero))
|
||||||
|
.filter_by(moduleimpl_id=moduleimpl_id)
|
||||||
|
.first()[0]
|
||||||
|
)
|
||||||
|
return max_num or 0
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def moduleimpl_evaluation_renumber(
|
||||||
|
cls, moduleimpl: ModuleImpl, only_if_unumbered=False
|
||||||
):
|
):
|
||||||
return f"""à {self.heure_debut.strftime("%Hh%M")}"""
|
"""Renumber evaluations in this moduleimpl, according to their date. (numero=0: oldest one)
|
||||||
elif self.heure_debut and self.heure_fin:
|
Needed because previous versions of ScoDoc did not have eval numeros
|
||||||
return f"""de {self.heure_debut.strftime("%Hh%M")} à {self.heure_fin.strftime("%Hh%M")}"""
|
Note: existing numeros are ignored
|
||||||
|
"""
|
||||||
|
# Liste des eval existantes triées par date, la plus ancienne en tete
|
||||||
|
evaluations = moduleimpl.evaluations.order_by(
|
||||||
|
Evaluation.date_debut, Evaluation.numero
|
||||||
|
).all()
|
||||||
|
all_numbered = all(e.numero is not None for e in evaluations)
|
||||||
|
if all_numbered and only_if_unumbered:
|
||||||
|
return # all ok
|
||||||
|
|
||||||
|
# Reset all numeros:
|
||||||
|
i = 1
|
||||||
|
for e in evaluations:
|
||||||
|
e.numero = i
|
||||||
|
db.session.add(e)
|
||||||
|
i += 1
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
def descr_heure(self) -> str:
|
||||||
|
"Description de la plage horaire pour affichages ('de 13h00 à 14h00')"
|
||||||
|
if self.date_debut and (not self.date_fin or self.date_fin == self.date_debut):
|
||||||
|
return f"""à {self.date_debut.strftime("%Hh%M")}"""
|
||||||
|
elif self.date_debut and self.date_fin:
|
||||||
|
return f"""de {self.date_debut.strftime("%Hh%M")
|
||||||
|
} à {self.date_fin.strftime("%Hh%M")}"""
|
||||||
else:
|
else:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def descr_duree(self) -> str:
|
def descr_duree(self) -> str:
|
||||||
"Description de la durée pour affichages"
|
"Description de la durée pour affichages ('3h' ou '2h30')"
|
||||||
if self.heure_debut is None and self.heure_fin is None:
|
if self.date_debut is None or self.date_fin is None:
|
||||||
return ""
|
return ""
|
||||||
debut = self.heure_debut or DEFAULT_EVALUATION_TIME
|
minutes = (self.date_fin - self.date_debut).seconds // 60
|
||||||
fin = self.heure_fin or DEFAULT_EVALUATION_TIME
|
duree = f"{minutes // 60}h"
|
||||||
d = (fin.hour * 60 + fin.minute) - (debut.hour * 60 + debut.minute)
|
minutes = minutes % 60
|
||||||
duree = f"{d//60}h"
|
if minutes != 0:
|
||||||
if d % 60:
|
duree += f"{minutes:02d}"
|
||||||
duree += f"{d%60:02d}"
|
|
||||||
return duree
|
return duree
|
||||||
|
|
||||||
|
def descr_date(self) -> str:
|
||||||
|
"""Description de la date pour affichages
|
||||||
|
'sans date'
|
||||||
|
'le 21/9/2021 à 13h'
|
||||||
|
'le 21/9/2021 de 13h à 14h30'
|
||||||
|
'du 21/9/2021 à 13h30 au 23/9/2021 à 15h'
|
||||||
|
"""
|
||||||
|
if self.date_debut is None:
|
||||||
|
return "sans date"
|
||||||
|
|
||||||
|
def _h(dt: datetime.datetime) -> str:
|
||||||
|
if dt.minute:
|
||||||
|
return dt.strftime("%Hh%M")
|
||||||
|
return f"{dt.hour}h"
|
||||||
|
|
||||||
|
if self.date_fin is None:
|
||||||
|
return f"le {self.date_debut.strftime('%d/%m/%Y')} à {_h(self.date_debut)}"
|
||||||
|
if self.date_debut.date() == self.date_fin.date(): # même jour
|
||||||
|
if self.date_debut.time() == self.date_fin.time():
|
||||||
|
return (
|
||||||
|
f"le {self.date_debut.strftime('%d/%m/%Y')} à {_h(self.date_debut)}"
|
||||||
|
)
|
||||||
|
return f"""le {self.date_debut.strftime('%d/%m/%Y')} de {
|
||||||
|
_h(self.date_debut)} à {_h(self.date_fin)}"""
|
||||||
|
# évaluation sur plus d'une journée
|
||||||
|
return f"""du {self.date_debut.strftime('%d/%m/%Y')} à {
|
||||||
|
_h(self.date_debut)} au {self.date_fin.strftime('%d/%m/%Y')} à {_h(self.date_fin)}"""
|
||||||
|
|
||||||
|
def heure_debut(self) -> str:
|
||||||
|
"""L'heure de début (sans la date), en ISO.
|
||||||
|
Chaine vide si non renseignée."""
|
||||||
|
return self.date_debut.time().isoformat("minutes") if self.date_debut else ""
|
||||||
|
|
||||||
|
def heure_fin(self) -> str:
|
||||||
|
"""L'heure de fin (sans la date), en ISO.
|
||||||
|
Chaine vide si non renseignée."""
|
||||||
|
return self.date_fin.time().isoformat("minutes") if self.date_fin else ""
|
||||||
|
|
||||||
def clone(self, not_copying=()):
|
def clone(self, not_copying=()):
|
||||||
"""Clone, not copying the given attrs
|
"""Clone, not copying the given attrs
|
||||||
Attention: la copie n'a pas d'id avant le prochain commit
|
Attention: la copie n'a pas d'id avant le prochain commit
|
||||||
@ -146,19 +314,19 @@ class Evaluation(db.Model):
|
|||||||
return copy
|
return copy
|
||||||
|
|
||||||
def is_matin(self) -> bool:
|
def is_matin(self) -> bool:
|
||||||
"Evaluation ayant lieu le matin (faux si pas de date)"
|
"Evaluation commençant le matin (faux si pas de date)"
|
||||||
heure_debut_dt = self.heure_debut or datetime.time(8, 00)
|
if not self.date_debut:
|
||||||
# 8:00 au cas ou pas d'heure (note externe?)
|
return False
|
||||||
return bool(self.jour) and heure_debut_dt < datetime.time(12, 00)
|
return self.date_debut.time() < NOON
|
||||||
|
|
||||||
def is_apresmidi(self) -> bool:
|
def is_apresmidi(self) -> bool:
|
||||||
"Evaluation ayant lieu l'après midi (faux si pas de date)"
|
"Evaluation commençant l'après midi (faux si pas de date)"
|
||||||
heure_debut_dt = self.heure_debut or datetime.time(8, 00)
|
if not self.date_debut:
|
||||||
# 8:00 au cas ou pas d'heure (note externe?)
|
return False
|
||||||
return bool(self.jour) and heure_debut_dt >= datetime.time(12, 00)
|
return self.date_debut.time() >= NOON
|
||||||
|
|
||||||
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 vers 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.
|
||||||
Les poids existants ne sont pas modifiés.
|
Les poids existants ne sont pas modifiés.
|
||||||
Return True if (uncommited) modification, False otherwise.
|
Return True if (uncommited) modification, False otherwise.
|
||||||
@ -191,6 +359,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 = db.session.get(UniteEns, ue_id)
|
||||||
|
if ue is None:
|
||||||
|
raise ScoValueError("poids vers une UE inexistante")
|
||||||
ue_poids = EvaluationUEPoids(evaluation=self, ue=ue, poids=poids)
|
ue_poids = EvaluationUEPoids(evaluation=self, ue=ue, poids=poids)
|
||||||
L.append(ue_poids)
|
L.append(ue_poids)
|
||||||
db.session.add(ue_poids)
|
db.session.add(ue_poids)
|
||||||
@ -274,88 +444,158 @@ class EvaluationUEPoids(db.Model):
|
|||||||
return f"<EvaluationUEPoids {self.evaluation} {self.ue} poids={self.poids}>"
|
return f"<EvaluationUEPoids {self.evaluation} {self.ue} poids={self.poids}>"
|
||||||
|
|
||||||
|
|
||||||
# Fonction héritée de ScoDoc7 à refactorer
|
# Fonction héritée de ScoDoc7
|
||||||
def evaluation_enrich_dict(e: dict):
|
def evaluation_enrich_dict(e: Evaluation, e_dict: dict):
|
||||||
"""add or convert some fields in an evaluation dict"""
|
"""add or convert some fields in an evaluation dict"""
|
||||||
# For ScoDoc7 compat
|
# For ScoDoc7 compat
|
||||||
heure_debut_dt = e["heure_debut"] or datetime.time(
|
e_dict["heure_debut"] = e.date_debut.strftime("%Hh%M") if e.date_debut else ""
|
||||||
8, 00
|
e_dict["heure_fin"] = e.date_fin.strftime("%Hh%M") if e.date_fin else ""
|
||||||
) # au cas ou pas d'heure (note externe?)
|
e_dict["jour_iso"] = e.date_debut.isoformat() if e.date_debut else ""
|
||||||
heure_fin_dt = e["heure_fin"] or datetime.time(8, 00)
|
# Calcule durée en minutes
|
||||||
e["heure_debut"] = ndb.TimefromISO8601(e["heure_debut"])
|
e_dict["descrheure"] = e.descr_heure()
|
||||||
e["heure_fin"] = ndb.TimefromISO8601(e["heure_fin"])
|
e_dict["descrduree"] = e.descr_duree()
|
||||||
e["jour_iso"] = ndb.DateDMYtoISO(e["jour"])
|
|
||||||
heure_debut, heure_fin = e["heure_debut"], e["heure_fin"]
|
|
||||||
d = ndb.TimeDuration(heure_debut, heure_fin)
|
|
||||||
if d is not None:
|
|
||||||
m = d % 60
|
|
||||||
e["duree"] = "%dh" % (d / 60)
|
|
||||||
if m != 0:
|
|
||||||
e["duree"] += "%02d" % m
|
|
||||||
else:
|
|
||||||
e["duree"] = ""
|
|
||||||
if heure_debut and (not heure_fin or heure_fin == heure_debut):
|
|
||||||
e["descrheure"] = " à " + heure_debut
|
|
||||||
elif heure_debut and heure_fin:
|
|
||||||
e["descrheure"] = " de %s à %s" % (heure_debut, heure_fin)
|
|
||||||
else:
|
|
||||||
e["descrheure"] = ""
|
|
||||||
# matin, apresmidi: utile pour se referer aux absences:
|
# matin, apresmidi: utile pour se referer aux absences:
|
||||||
|
# note août 2023: si l'évaluation s'étend sur plusieurs jours,
|
||||||
if e["jour"] and heure_debut_dt < datetime.time(12, 00):
|
# cet indicateur n'a pas grand sens
|
||||||
e["matin"] = 1
|
if e.date_debut and e.date_debut.time() < datetime.time(12, 00):
|
||||||
|
e_dict["matin"] = 1
|
||||||
else:
|
else:
|
||||||
e["matin"] = 0
|
e_dict["matin"] = 0
|
||||||
if e["jour"] and heure_fin_dt > datetime.time(12, 00):
|
if e.date_fin and e.date_fin.time() > datetime.time(12, 00):
|
||||||
e["apresmidi"] = 1
|
e_dict["apresmidi"] = 1
|
||||||
else:
|
else:
|
||||||
e["apresmidi"] = 0
|
e_dict["apresmidi"] = 0
|
||||||
return e
|
return e_dict
|
||||||
|
|
||||||
|
|
||||||
def check_evaluation_args(args):
|
def check_convert_evaluation_args(moduleimpl: ModuleImpl, data: dict):
|
||||||
"Check coefficient, dates and duration, raises exception if invalid"
|
"""Check coefficient, dates and duration, raises exception if invalid.
|
||||||
moduleimpl_id = args["moduleimpl_id"]
|
Convert date and time strings to date and time objects.
|
||||||
# check bareme
|
|
||||||
note_max = args.get("note_max", None)
|
Set required default value for unspecified fields.
|
||||||
if note_max is None:
|
May raise ScoValueError.
|
||||||
raise ScoValueError("missing note_max")
|
"""
|
||||||
|
# --- description
|
||||||
|
data["description"] = data.get("description", "") or ""
|
||||||
|
if len(data["description"]) > scu.MAX_TEXT_LEN:
|
||||||
|
raise ScoValueError("description too large")
|
||||||
|
|
||||||
|
# --- evaluation_type
|
||||||
|
try:
|
||||||
|
data["evaluation_type"] = int(data.get("evaluation_type", 0) or 0)
|
||||||
|
if not data["evaluation_type"] in VALID_EVALUATION_TYPES:
|
||||||
|
raise ScoValueError("invalid evaluation_type value")
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ScoValueError("invalid evaluation_type value") from exc
|
||||||
|
|
||||||
|
# --- note_max (bareme)
|
||||||
|
note_max = data.get("note_max", 20.0) or 20.0
|
||||||
try:
|
try:
|
||||||
note_max = float(note_max)
|
note_max = float(note_max)
|
||||||
except ValueError:
|
except ValueError as exc:
|
||||||
raise ScoValueError("Invalid note_max value")
|
raise ScoValueError("invalid note_max value") from exc
|
||||||
if note_max < 0:
|
if note_max < 0:
|
||||||
raise ScoValueError("Invalid note_max value (must be positive or null)")
|
raise ScoValueError("invalid note_max value (must be positive or null)")
|
||||||
# check coefficient
|
data["note_max"] = note_max
|
||||||
coef = args.get("coefficient", None)
|
# --- coefficient
|
||||||
if coef is None:
|
coef = data.get("coefficient", 1.0) or 1.0
|
||||||
raise ScoValueError("missing coefficient")
|
|
||||||
try:
|
try:
|
||||||
coef = float(coef)
|
coef = float(coef)
|
||||||
except ValueError:
|
except ValueError as exc:
|
||||||
raise ScoValueError("Invalid coefficient value")
|
raise ScoValueError("invalid coefficient value") from exc
|
||||||
if coef < 0:
|
if coef < 0:
|
||||||
raise ScoValueError("Invalid coefficient value (must be positive or null)")
|
raise ScoValueError("invalid coefficient value (must be positive or null)")
|
||||||
# check date
|
data["coefficient"] = coef
|
||||||
jour = args.get("jour", None)
|
# --- date de l'évaluation
|
||||||
args["jour"] = jour
|
formsemestre = moduleimpl.formsemestre
|
||||||
if jour:
|
date_debut = data.get("date_debut", None)
|
||||||
modimpl = db.session.get(ModuleImpl, moduleimpl_id)
|
if date_debut:
|
||||||
formsemestre = modimpl.formsemestre
|
if isinstance(date_debut, str):
|
||||||
y, m, d = [int(x) for x in ndb.DateDMYtoISO(jour).split("-")]
|
data["date_debut"] = datetime.datetime.fromisoformat(date_debut)
|
||||||
jour = datetime.date(y, m, d)
|
if data["date_debut"].tzinfo is None:
|
||||||
if (jour > formsemestre.date_fin) or (jour < formsemestre.date_debut):
|
data["date_debut"] = scu.TIME_ZONE.localize(data["date_debut"])
|
||||||
|
if not (
|
||||||
|
formsemestre.date_debut
|
||||||
|
<= data["date_debut"].date()
|
||||||
|
<= formsemestre.date_fin
|
||||||
|
):
|
||||||
raise ScoValueError(
|
raise ScoValueError(
|
||||||
"La date de l'évaluation (%s/%s/%s) n'est pas dans le semestre !"
|
f"""La date de début de l'évaluation ({
|
||||||
% (d, m, y),
|
data["date_debut"].strftime("%d/%m/%Y")
|
||||||
|
}) n'est pas dans le semestre !""",
|
||||||
dest_url="javascript:history.back();",
|
dest_url="javascript:history.back();",
|
||||||
)
|
)
|
||||||
heure_debut = args.get("heure_debut", None)
|
date_fin = data.get("date_fin", None)
|
||||||
args["heure_debut"] = heure_debut
|
if date_fin:
|
||||||
heure_fin = args.get("heure_fin", None)
|
if isinstance(date_fin, str):
|
||||||
args["heure_fin"] = heure_fin
|
data["date_fin"] = datetime.datetime.fromisoformat(date_fin)
|
||||||
if jour and ((not heure_debut) or (not heure_fin)):
|
if data["date_fin"].tzinfo is None:
|
||||||
raise ScoValueError("Les heures doivent être précisées")
|
data["date_fin"] = scu.TIME_ZONE.localize(data["date_fin"])
|
||||||
d = ndb.TimeDuration(heure_debut, heure_fin)
|
if not (
|
||||||
if d and ((d < 0) or (d > 60 * 12)):
|
formsemestre.date_debut <= data["date_fin"].date() <= formsemestre.date_fin
|
||||||
|
):
|
||||||
|
raise ScoValueError(
|
||||||
|
f"""La date de fin de l'évaluation ({
|
||||||
|
data["date_fin"].strftime("%d/%m/%Y")
|
||||||
|
}) n'est pas dans le semestre !""",
|
||||||
|
dest_url="javascript:history.back();",
|
||||||
|
)
|
||||||
|
if date_debut and date_fin:
|
||||||
|
duration = data["date_fin"] - data["date_debut"]
|
||||||
|
if duration.total_seconds() < 0 or duration > MAX_EVALUATION_DURATION:
|
||||||
raise ScoValueError("Heures de l'évaluation incohérentes !")
|
raise ScoValueError("Heures de l'évaluation incohérentes !")
|
||||||
|
# # --- heures
|
||||||
|
# heure_debut = data.get("heure_debut", None)
|
||||||
|
# if heure_debut and not isinstance(heure_debut, datetime.time):
|
||||||
|
# if date_format == "dmy":
|
||||||
|
# data["heure_debut"] = heure_to_time(heure_debut)
|
||||||
|
# else: # ISO
|
||||||
|
# data["heure_debut"] = datetime.time.fromisoformat(heure_debut)
|
||||||
|
# heure_fin = data.get("heure_fin", None)
|
||||||
|
# if heure_fin and not isinstance(heure_fin, datetime.time):
|
||||||
|
# if date_format == "dmy":
|
||||||
|
# data["heure_fin"] = heure_to_time(heure_fin)
|
||||||
|
# else: # ISO
|
||||||
|
# data["heure_fin"] = datetime.time.fromisoformat(heure_fin)
|
||||||
|
|
||||||
|
|
||||||
|
def heure_to_time(heure: str) -> datetime.time:
|
||||||
|
"Convert external heure ('10h22' or '10:22') to a time"
|
||||||
|
t = heure.strip().upper().replace("H", ":")
|
||||||
|
h, m = t.split(":")[:2]
|
||||||
|
return datetime.time(int(h), int(m))
|
||||||
|
|
||||||
|
|
||||||
|
def _time_duration_HhM(heure_debut: str, heure_fin: str) -> int:
|
||||||
|
"""duree (nb entier de minutes) entre deux heures a notre format
|
||||||
|
ie 12h23
|
||||||
|
"""
|
||||||
|
if heure_debut and heure_fin:
|
||||||
|
h0, m0 = [int(x) for x in heure_debut.split("h")]
|
||||||
|
h1, m1 = [int(x) for x in heure_fin.split("h")]
|
||||||
|
d = (h1 - h0) * 60 + (m1 - m0)
|
||||||
|
return d
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _moduleimpl_evaluation_insert_before(
|
||||||
|
evaluations: list[Evaluation], next_eval: Evaluation
|
||||||
|
) -> int:
|
||||||
|
"""Renumber evaluations such that an evaluation with can be inserted before next_eval
|
||||||
|
Returns numero suitable for the inserted evaluation
|
||||||
|
"""
|
||||||
|
if next_eval:
|
||||||
|
n = next_eval.numero
|
||||||
|
if n is None:
|
||||||
|
Evaluation.moduleimpl_evaluation_renumber(next_eval.moduleimpl)
|
||||||
|
n = next_eval.numero
|
||||||
|
else:
|
||||||
|
n = 1
|
||||||
|
# all numeros >= n are incremented
|
||||||
|
for e in evaluations:
|
||||||
|
if e.numero >= n:
|
||||||
|
e.numero += 1
|
||||||
|
db.session.add(e)
|
||||||
|
db.session.commit()
|
||||||
|
return n
|
||||||
|
@ -30,6 +30,7 @@ from app.models.but_refcomp import (
|
|||||||
)
|
)
|
||||||
from app.models.config import ScoDocSiteConfig
|
from app.models.config import ScoDocSiteConfig
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
|
from app.models.evaluations import Evaluation
|
||||||
from app.models.formations import Formation
|
from app.models.formations import Formation
|
||||||
from app.models.groups import GroupDescr, Partition
|
from app.models.groups import GroupDescr, Partition
|
||||||
from app.models.moduleimpls import ModuleImpl, ModuleImplInscription
|
from app.models.moduleimpls import ModuleImpl, ModuleImplInscription
|
||||||
@ -39,9 +40,11 @@ from app.models.validations import ScolarFormSemestreValidation
|
|||||||
from app.scodoc import codes_cursus, sco_preferences
|
from app.scodoc import codes_cursus, sco_preferences
|
||||||
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
|
||||||
from app.scodoc.sco_utils import MONTH_NAMES_ABBREV
|
from app.scodoc.sco_utils import MONTH_NAMES_ABBREV, translate_assiduites_metric
|
||||||
from app.scodoc.sco_vdi import ApoEtapeVDI
|
from app.scodoc.sco_vdi import ApoEtapeVDI
|
||||||
|
|
||||||
|
from app.scodoc.sco_utils import translate_assiduites_metric
|
||||||
|
|
||||||
GROUPS_AUTO_ASSIGNMENT_DATA_MAX = 1024 * 1024 # bytes
|
GROUPS_AUTO_ASSIGNMENT_DATA_MAX = 1024 * 1024 # bytes
|
||||||
|
|
||||||
|
|
||||||
@ -348,6 +351,21 @@ class FormSemestre(db.Model):
|
|||||||
_cache[key] = ues
|
_cache[key] = ues
|
||||||
return ues
|
return ues
|
||||||
|
|
||||||
|
def get_evaluations(self) -> list[Evaluation]:
|
||||||
|
"Liste de toutes les évaluations du semestre, triées par module/numero"
|
||||||
|
return (
|
||||||
|
Evaluation.query.join(ModuleImpl)
|
||||||
|
.filter_by(formsemestre_id=self.id)
|
||||||
|
.join(Module)
|
||||||
|
.order_by(
|
||||||
|
Module.numero,
|
||||||
|
Module.code,
|
||||||
|
Evaluation.numero,
|
||||||
|
Evaluation.date_debut.desc(),
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def modimpls_sorted(self) -> list[ModuleImpl]:
|
def modimpls_sorted(self) -> list[ModuleImpl]:
|
||||||
"""Liste des modimpls du semestre (y compris bonus)
|
"""Liste des modimpls du semestre (y compris bonus)
|
||||||
@ -712,10 +730,14 @@ class FormSemestre(db.Model):
|
|||||||
tuple (nb abs, nb abs justifiées)
|
tuple (nb abs, nb abs justifiées)
|
||||||
Utilise un cache.
|
Utilise un cache.
|
||||||
"""
|
"""
|
||||||
from app.scodoc import sco_abs
|
from app.scodoc import sco_assiduites
|
||||||
|
|
||||||
return sco_abs.get_abs_count_in_interval(
|
metrique = sco_preferences.get_preference("assi_metrique", self.id)
|
||||||
etudid, self.date_debut.isoformat(), self.date_fin.isoformat()
|
return sco_assiduites.get_assiduites_count_in_interval(
|
||||||
|
etudid,
|
||||||
|
self.date_debut.isoformat(),
|
||||||
|
self.date_fin.isoformat(),
|
||||||
|
translate_assiduites_metric(metrique),
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_codes_apogee(self, category=None) -> set[str]:
|
def get_codes_apogee(self, category=None) -> set[str]:
|
||||||
@ -812,11 +834,15 @@ class FormSemestre(db.Model):
|
|||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
def update_inscriptions_parcours_from_groups(self) -> None:
|
def update_inscriptions_parcours_from_groups(self, etudid: int = None) -> None:
|
||||||
"""Met à jour les inscriptions dans les parcours du semestres en
|
"""Met à jour les inscriptions dans les parcours du semestres en
|
||||||
fonction des groupes de parcours.
|
fonction des groupes de parcours.
|
||||||
|
|
||||||
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").
|
||||||
|
|
||||||
|
Si etudid est sépcifié, n'affecte que cet étudiant,
|
||||||
|
sinon traite tous les inscrits du semestre.
|
||||||
"""
|
"""
|
||||||
if self.formation.referentiel_competence_id is None:
|
if self.formation.referentiel_competence_id is None:
|
||||||
return # safety net
|
return # safety net
|
||||||
@ -827,6 +853,21 @@ class FormSemestre(db.Model):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Efface les inscriptions aux parcours:
|
# Efface les inscriptions aux parcours:
|
||||||
|
if etudid:
|
||||||
|
db.session.execute(
|
||||||
|
text(
|
||||||
|
"""UPDATE notes_formsemestre_inscription
|
||||||
|
SET parcour_id=NULL
|
||||||
|
WHERE formsemestre_id=:formsemestre_id
|
||||||
|
AND etudid=:etudid
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"formsemestre_id": self.id,
|
||||||
|
"etudid": etudid,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else:
|
||||||
db.session.execute(
|
db.session.execute(
|
||||||
text(
|
text(
|
||||||
"""UPDATE notes_formsemestre_inscription
|
"""UPDATE notes_formsemestre_inscription
|
||||||
@ -855,6 +896,26 @@ class FormSemestre(db.Model):
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
parcour = query.first()
|
parcour = query.first()
|
||||||
|
if etudid:
|
||||||
|
db.session.execute(
|
||||||
|
text(
|
||||||
|
"""UPDATE notes_formsemestre_inscription ins
|
||||||
|
SET parcour_id=:parcour_id
|
||||||
|
FROM group_membership gm
|
||||||
|
WHERE formsemestre_id=:formsemestre_id
|
||||||
|
AND ins.etudid = :etudid
|
||||||
|
AND gm.etudid = :etudid
|
||||||
|
AND gm.group_id = :group_id
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"etudid": etudid,
|
||||||
|
"formsemestre_id": self.id,
|
||||||
|
"parcour_id": parcour.id,
|
||||||
|
"group_id": group.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else:
|
||||||
db.session.execute(
|
db.session.execute(
|
||||||
text(
|
text(
|
||||||
"""UPDATE notes_formsemestre_inscription ins
|
"""UPDATE notes_formsemestre_inscription ins
|
||||||
|
@ -101,6 +101,23 @@ class ModuleImpl(db.Model):
|
|||||||
d.pop("module", None)
|
d.pop("module", None)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
def can_edit_evaluation(self, user) -> bool:
|
||||||
|
"""True if this user can create, delete or edit and evaluation in this modimpl
|
||||||
|
(nb: n'implique pas le droit de saisir ou modifier des notes)
|
||||||
|
"""
|
||||||
|
# acces pour resp. moduleimpl et resp. form semestre (dir etud)
|
||||||
|
if (
|
||||||
|
user.has_permission(Permission.ScoEditAllEvals)
|
||||||
|
or user.id == self.responsable_id
|
||||||
|
or user.id in (r.id for r in self.formsemestre.responsables)
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
elif self.formsemestre.ens_can_edit_eval:
|
||||||
|
if user.id in (e.id for e in self.enseignants):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def can_change_ens_by(self, user: User, raise_exc=False) -> bool:
|
def can_change_ens_by(self, user: User, raise_exc=False) -> bool:
|
||||||
"""Check if user can modify module resp.
|
"""Check if user can modify module resp.
|
||||||
If raise_exc, raises exception (AccessDenied or ScoLockedSemError) if not.
|
If raise_exc, raises exception (AccessDenied or ScoLockedSemError) if not.
|
||||||
@ -122,6 +139,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(
|
||||||
|
@ -153,6 +153,10 @@ class Module(db.Model):
|
|||||||
"""
|
"""
|
||||||
return scu.ModuleType.get_abbrev(self.module_type)
|
return scu.ModuleType.get_abbrev(self.module_type)
|
||||||
|
|
||||||
|
def titre_str(self) -> str:
|
||||||
|
"Identifiant du module à afficher : abbrev ou titre ou code"
|
||||||
|
return self.abbrev or self.titre or self.code
|
||||||
|
|
||||||
def sort_key_apc(self) -> tuple:
|
def sort_key_apc(self) -> tuple:
|
||||||
"""Clé de tri pour avoir
|
"""Clé de tri pour avoir
|
||||||
présentation par type (res, sae), parcours, type, numéro
|
présentation par type (res, sae), parcours, type, numéro
|
||||||
|
@ -57,8 +57,10 @@ def _pe_view_sem_recap_form(formsemestre_id):
|
|||||||
poursuites d'études.
|
poursuites d'études.
|
||||||
<br>
|
<br>
|
||||||
De nombreux aspects sont paramétrables:
|
De nombreux aspects sont paramétrables:
|
||||||
<a href="https://scodoc.org/AvisPoursuiteEtudes" target="_blank" rel="noopener">
|
<a href="https://scodoc.org/AvisPoursuiteEtudes"
|
||||||
voir la documentation</a>.
|
target="_blank" rel="noopener noreferrer">
|
||||||
|
voir la documentation
|
||||||
|
</a>.
|
||||||
</p>
|
</p>
|
||||||
<form method="post" action="pe_view_sem_recap" id="pe_view_sem_recap_form"
|
<form method="post" action="pe_view_sem_recap" id="pe_view_sem_recap_form"
|
||||||
enctype="multipart/form-data">
|
enctype="multipart/form-data">
|
||||||
|
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("")
|
@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
import html
|
import html
|
||||||
|
|
||||||
from flask import render_template
|
from flask import g, render_template
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
|
||||||
@ -148,6 +148,8 @@ def sco_header(
|
|||||||
"Main HTML page header for ScoDoc"
|
"Main HTML page header for ScoDoc"
|
||||||
from app.scodoc.sco_formsemestre_status import formsemestre_page_title
|
from app.scodoc.sco_formsemestre_status import formsemestre_page_title
|
||||||
|
|
||||||
|
if etudid is not None:
|
||||||
|
g.current_etudid = etudid
|
||||||
scodoc_flash_status_messages()
|
scodoc_flash_status_messages()
|
||||||
|
|
||||||
# Get head message from http request:
|
# Get head message from http request:
|
||||||
|
27
app/scodoc/html_sidebar.py
Normal file → Executable file
27
app/scodoc/html_sidebar.py
Normal file → Executable file
@ -54,9 +54,12 @@ def sidebar_common():
|
|||||||
<h2 class="insidebar">Scolarité</h2>
|
<h2 class="insidebar">Scolarité</h2>
|
||||||
<a href="{scu.ScoURL()}" class="sidebar">Semestres</a> <br>
|
<a href="{scu.ScoURL()}" class="sidebar">Semestres</a> <br>
|
||||||
<a href="{scu.NotesURL()}" class="sidebar">Programmes</a> <br>
|
<a href="{scu.NotesURL()}" class="sidebar">Programmes</a> <br>
|
||||||
<a href="{scu.AbsencesURL()}" class="sidebar">Absences</a> <br>
|
|
||||||
"""
|
"""
|
||||||
]
|
]
|
||||||
|
if current_user.has_permission(Permission.ScoAbsChange):
|
||||||
|
H.append(
|
||||||
|
f""" <a href="{scu.AssiduitesURL()}" class="sidebar">Assiduités</a> <br> """
|
||||||
|
)
|
||||||
if current_user.has_permission(
|
if current_user.has_permission(
|
||||||
Permission.ScoUsersAdmin
|
Permission.ScoUsersAdmin
|
||||||
) or current_user.has_permission(Permission.ScoUsersView):
|
) or current_user.has_permission(Permission.ScoUsersView):
|
||||||
@ -76,7 +79,7 @@ def sidebar_common():
|
|||||||
def sidebar(etudid: int = None):
|
def sidebar(etudid: int = None):
|
||||||
"Main HTML page sidebar"
|
"Main HTML page sidebar"
|
||||||
# rewritten from legacy DTML code
|
# rewritten from legacy DTML code
|
||||||
from app.scodoc import sco_abs
|
from app.scodoc import sco_assiduites
|
||||||
from app.scodoc import sco_etud
|
from app.scodoc import sco_etud
|
||||||
|
|
||||||
params = {}
|
params = {}
|
||||||
@ -116,19 +119,18 @@ def sidebar(etudid: int = None):
|
|||||||
)
|
)
|
||||||
if etud["cursem"]:
|
if etud["cursem"]:
|
||||||
cur_sem = etud["cursem"]
|
cur_sem = etud["cursem"]
|
||||||
nbabs, nbabsjust = sco_abs.get_abs_count(etudid, cur_sem)
|
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, cur_sem)
|
||||||
nbabsnj = nbabs - nbabsjust
|
nbabsnj = nbabs - nbabsjust
|
||||||
H.append(
|
H.append(
|
||||||
f"""<span title="absences du { cur_sem["date_debut"] } au { cur_sem["date_fin"] }">(1/2 j.)
|
f"""<span title="absences du { cur_sem["date_debut"] } au { cur_sem["date_fin"] }">({sco_preferences.get_preference("assi_metrique", None)})
|
||||||
<br>{ nbabsjust } J., { nbabsnj } N.J.</span>"""
|
<br>{ nbabsjust } J., { nbabsnj } N.J.</span>"""
|
||||||
)
|
)
|
||||||
H.append("<ul>")
|
H.append("<ul>")
|
||||||
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('assiduites.ajout_justificatif_etud', 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>
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
if sco_preferences.get_preference("handle_billets_abs"):
|
if sco_preferences.get_preference("handle_billets_abs"):
|
||||||
@ -137,8 +139,9 @@ 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('assiduites.calendrier_etud', 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>
|
||||||
|
<li><a href="{ url_for('assiduites.bilan_etud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Bilan</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
@ -149,8 +152,10 @@ def sidebar(etudid: int = None):
|
|||||||
# Logo
|
# Logo
|
||||||
H.append(
|
H.append(
|
||||||
f"""<div class="logo-insidebar">
|
f"""<div class="logo-insidebar">
|
||||||
<div class="sidebar-bottom"><a href="{ url_for( 'scodoc.about', scodoc_dept=g.scodoc_dept ) }" class="sidebar">À propos</a><br>
|
<div class="sidebar-bottom"><a href="{
|
||||||
<a href="{ scu.SCO_USER_MANUAL }" target="_blank" class="sidebar">Aide</a>
|
url_for( 'scodoc.about', scodoc_dept=g.scodoc_dept )
|
||||||
|
}" class="sidebar">À propos</a><br>
|
||||||
|
<a href="{ scu.SCO_USER_MANUAL }" target="_blank" rel="noopener" class="sidebar">Aide</a>
|
||||||
</div></div>
|
</div></div>
|
||||||
<div class="logo-logo">
|
<div class="logo-logo">
|
||||||
<a href="{ url_for( 'scodoc.about', scodoc_dept=g.scodoc_dept ) }">
|
<a href="{ url_for( 'scodoc.about', scodoc_dept=g.scodoc_dept ) }">
|
||||||
|
@ -6,7 +6,7 @@ from PIL import Image as PILImage
|
|||||||
|
|
||||||
def ImageScale(img_file, maxx, maxy):
|
def ImageScale(img_file, maxx, maxy):
|
||||||
im = PILImage.open(img_file)
|
im = PILImage.open(img_file)
|
||||||
im.thumbnail((maxx, maxy), PILImage.ANTIALIAS)
|
im.thumbnail((maxx, maxy), PILImage.LANCZOS)
|
||||||
out_file_str = io.BytesIO()
|
out_file_str = io.BytesIO()
|
||||||
im.save(out_file_str, im.format)
|
im.save(out_file_str, im.format)
|
||||||
out_file_str.seek(0)
|
out_file_str.seek(0)
|
||||||
@ -20,7 +20,7 @@ def ImageScaleH(img_file, W=None, H=90):
|
|||||||
if W is None:
|
if W is None:
|
||||||
# keep aspect
|
# keep aspect
|
||||||
W = int((im.size[0] * H) / float(im.size[1]))
|
W = int((im.size[0] * H) / float(im.size[1]))
|
||||||
im.thumbnail((W, H), PILImage.ANTIALIAS)
|
im.thumbnail((W, H), PILImage.LANCZOS)
|
||||||
out_file_str = io.BytesIO()
|
out_file_str = io.BytesIO()
|
||||||
im.save(out_file_str, im.format)
|
im.save(out_file_str, im.format)
|
||||||
out_file_str.seek(0)
|
out_file_str.seek(0)
|
||||||
|
@ -1,19 +1,17 @@
|
|||||||
# -*- mode: python -*-
|
# -*- mode: python -*-
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import datetime
|
||||||
import html
|
import html
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
from flask import g, current_app, abort
|
||||||
import psycopg2
|
import psycopg2
|
||||||
import psycopg2.pool
|
import psycopg2.pool
|
||||||
import psycopg2.extras
|
import psycopg2.extras
|
||||||
|
|
||||||
from flask import g, current_app, abort
|
|
||||||
|
|
||||||
import app
|
|
||||||
import app.scodoc.sco_utils as scu
|
|
||||||
from app import log
|
from app import log
|
||||||
from app.scodoc.sco_exceptions import ScoException, ScoValueError, NoteProcessError
|
from app.scodoc.sco_exceptions import ScoException, ScoValueError, NoteProcessError
|
||||||
import datetime
|
|
||||||
|
|
||||||
quote_html = html.escape
|
quote_html = html.escape
|
||||||
|
|
||||||
@ -459,8 +457,10 @@ def dictfilter(d, fields, filter_nulls=True):
|
|||||||
# --- Misc Tools
|
# --- Misc Tools
|
||||||
|
|
||||||
|
|
||||||
def DateDMYtoISO(dmy: str, null_is_empty=False) -> str:
|
def DateDMYtoISO(dmy: str, null_is_empty=False) -> str: # XXX deprecated
|
||||||
"convert date string from french format to ISO"
|
"""Convert date string from french format to ISO.
|
||||||
|
If null_is_empty (default false), returns "" if no input.
|
||||||
|
"""
|
||||||
if not dmy:
|
if not dmy:
|
||||||
if null_is_empty:
|
if null_is_empty:
|
||||||
return ""
|
return ""
|
||||||
@ -506,7 +506,7 @@ def DateISOtoDMY(isodate):
|
|||||||
return "%02d/%02d/%04d" % (day, month, year)
|
return "%02d/%02d/%04d" % (day, month, year)
|
||||||
|
|
||||||
|
|
||||||
def TimetoISO8601(t, null_is_empty=False):
|
def TimetoISO8601(t, null_is_empty=False) -> str:
|
||||||
"convert time string to ISO 8601 (allow 16:03, 16h03, 16)"
|
"convert time string to ISO 8601 (allow 16:03, 16h03, 16)"
|
||||||
if isinstance(t, datetime.time):
|
if isinstance(t, datetime.time):
|
||||||
return t.isoformat()
|
return t.isoformat()
|
||||||
@ -518,7 +518,7 @@ def TimetoISO8601(t, null_is_empty=False):
|
|||||||
return t
|
return t
|
||||||
|
|
||||||
|
|
||||||
def TimefromISO8601(t):
|
def TimefromISO8601(t) -> str:
|
||||||
"convert time string from ISO 8601 to our display format"
|
"convert time string from ISO 8601 to our display format"
|
||||||
if not t:
|
if not t:
|
||||||
return t
|
return t
|
||||||
@ -532,19 +532,6 @@ def TimefromISO8601(t):
|
|||||||
return fs[0] + "h" + fs[1] # discard seconds
|
return fs[0] + "h" + fs[1] # discard seconds
|
||||||
|
|
||||||
|
|
||||||
def TimeDuration(heure_debut, heure_fin):
|
|
||||||
"""duree (nb entier de minutes) entre deux heures a notre format
|
|
||||||
ie 12h23
|
|
||||||
"""
|
|
||||||
if heure_debut and heure_fin:
|
|
||||||
h0, m0 = [int(x) for x in heure_debut.split("h")]
|
|
||||||
h1, m1 = [int(x) for x in heure_fin.split("h")]
|
|
||||||
d = (h1 - h0) * 60 + (m1 - m0)
|
|
||||||
return d
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def float_null_is_zero(x):
|
def float_null_is_zero(x):
|
||||||
if x is None or x == "":
|
if x is None or x == "":
|
||||||
return 0.0
|
return 0.0
|
||||||
|
27
app/scodoc/sco_abs.py
Normal file → Executable file
27
app/scodoc/sco_abs.py
Normal file → Executable file
@ -318,7 +318,7 @@ def list_abs_in_range(
|
|||||||
Returns:
|
Returns:
|
||||||
List of absences
|
List of absences
|
||||||
"""
|
"""
|
||||||
if matin != None:
|
if matin is not None:
|
||||||
matin = _toboolean(matin)
|
matin = _toboolean(matin)
|
||||||
ismatin = " AND A.MATIN = %(matin)s "
|
ismatin = " AND A.MATIN = %(matin)s "
|
||||||
else:
|
else:
|
||||||
@ -387,7 +387,7 @@ def count_abs_just(etudid, debut, fin, matin=None, moduleimpl_id=None) -> int:
|
|||||||
Returns:
|
Returns:
|
||||||
An integer.
|
An integer.
|
||||||
"""
|
"""
|
||||||
if matin != None:
|
if matin is not None:
|
||||||
matin = _toboolean(matin)
|
matin = _toboolean(matin)
|
||||||
ismatin = " AND A.MATIN = %(matin)s "
|
ismatin = " AND A.MATIN = %(matin)s "
|
||||||
else:
|
else:
|
||||||
@ -482,7 +482,9 @@ def _get_abs_description(a, cursor=None):
|
|||||||
else:
|
else:
|
||||||
a["matin"] = False
|
a["matin"] = False
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""select * from absences where etudid=%(etudid)s and jour=%(jour)s and matin=%(matin)s order by entry_date desc""",
|
"""SELECT * FROM absences
|
||||||
|
WHERE etudid=%(etudid)s AND jour=%(jour)s AND matin=%(matin)s
|
||||||
|
ORDER BY entry_date desc""",
|
||||||
a,
|
a,
|
||||||
)
|
)
|
||||||
A = cursor.dictfetchall()
|
A = cursor.dictfetchall()
|
||||||
@ -507,7 +509,7 @@ def _get_abs_description(a, cursor=None):
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def list_abs_jour(date, am=True, pm=True, is_abs=True, is_just=None):
|
def list_abs_jour(date, am=True, pm=True, is_abs=True, is_just=None) -> list[dict]:
|
||||||
"""Liste des absences et/ou justificatifs ce jour.
|
"""Liste des absences et/ou justificatifs ce jour.
|
||||||
is_abs: None (peu importe), True, False
|
is_abs: None (peu importe), True, False
|
||||||
is_just: idem
|
is_just: idem
|
||||||
@ -517,9 +519,9 @@ def list_abs_jour(date, am=True, pm=True, is_abs=True, is_just=None):
|
|||||||
req = """SELECT DISTINCT etudid, jour, matin FROM ABSENCES A
|
req = """SELECT DISTINCT etudid, jour, matin FROM ABSENCES A
|
||||||
WHERE A.jour = %(date)s
|
WHERE A.jour = %(date)s
|
||||||
"""
|
"""
|
||||||
if is_abs != None:
|
if is_abs is not None:
|
||||||
req += " AND A.estabs = %(is_abs)s"
|
req += " AND A.estabs = %(is_abs)s"
|
||||||
if is_just != None:
|
if is_just is not None:
|
||||||
req += " AND A.estjust = %(is_just)s"
|
req += " AND A.estjust = %(is_just)s"
|
||||||
if not am:
|
if not am:
|
||||||
req += " AND NOT matin "
|
req += " AND NOT matin "
|
||||||
@ -533,7 +535,7 @@ WHERE A.jour = %(date)s
|
|||||||
return A
|
return A
|
||||||
|
|
||||||
|
|
||||||
def list_abs_non_just_jour(date, am=True, pm=True):
|
def list_abs_non_just_jour(date, am=True, pm=True) -> list[dict]:
|
||||||
"Liste des absences non justifiees ce jour"
|
"Liste des absences non justifiees ce jour"
|
||||||
cnx = ndb.GetDBConnexion()
|
cnx = ndb.GetDBConnexion()
|
||||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||||
@ -883,7 +885,7 @@ def MonthTableBody(
|
|||||||
descr = ev[4]
|
descr = ev[4]
|
||||||
#
|
#
|
||||||
cc = []
|
cc = []
|
||||||
if color != None:
|
if color is not None:
|
||||||
cc.append('<td bgcolor="%s" class="calcell">' % color)
|
cc.append('<td bgcolor="%s" class="calcell">' % color)
|
||||||
else:
|
else:
|
||||||
cc.append('<td class="calcell">')
|
cc.append('<td class="calcell">')
|
||||||
@ -896,7 +898,7 @@ def MonthTableBody(
|
|||||||
cc.append("<a %s %s>" % (href, descr))
|
cc.append("<a %s %s>" % (href, descr))
|
||||||
|
|
||||||
if legend or d == 1:
|
if legend or d == 1:
|
||||||
if pad_width != None:
|
if pad_width is not None:
|
||||||
n = pad_width - len(legend) # pad to 8 cars
|
n = pad_width - len(legend) # pad to 8 cars
|
||||||
if n > 0:
|
if n > 0:
|
||||||
legend = (
|
legend = (
|
||||||
@ -959,7 +961,7 @@ def MonthTableBody(
|
|||||||
ev_year = int(ev[0][:4])
|
ev_year = int(ev[0][:4])
|
||||||
ev_month = int(ev[0][5:7])
|
ev_month = int(ev[0][5:7])
|
||||||
ev_day = int(ev[0][8:10])
|
ev_day = int(ev[0][8:10])
|
||||||
if ev[4] != None:
|
if ev[4] is not None:
|
||||||
ev_half = int(ev[4])
|
ev_half = int(ev[4])
|
||||||
else:
|
else:
|
||||||
ev_half = 0
|
ev_half = 0
|
||||||
@ -978,7 +980,7 @@ def MonthTableBody(
|
|||||||
if len(ev) > 5 and ev[5]:
|
if len(ev) > 5 and ev[5]:
|
||||||
descr = ev[5]
|
descr = ev[5]
|
||||||
#
|
#
|
||||||
if color != None:
|
if color is not None:
|
||||||
cc.append('<td bgcolor="%s" class="calcell">' % (color))
|
cc.append('<td bgcolor="%s" class="calcell">' % (color))
|
||||||
else:
|
else:
|
||||||
cc.append('<td class="calcell">')
|
cc.append('<td class="calcell">')
|
||||||
@ -1072,7 +1074,8 @@ def invalidate_abs_count_sem(sem):
|
|||||||
|
|
||||||
|
|
||||||
def invalidate_abs_etud_date(etudid, date): # was invalidateAbsEtudDate
|
def invalidate_abs_etud_date(etudid, date): # was invalidateAbsEtudDate
|
||||||
"""Doit etre appelé à chaque modification des absences pour cet étudiant et cette date.
|
"""Doit etre appelé à chaque modification des absences
|
||||||
|
pour cet étudiant et cette date.
|
||||||
Invalide cache absence et caches semestre
|
Invalide cache absence et caches semestre
|
||||||
date: date au format ISO
|
date: date au format ISO
|
||||||
"""
|
"""
|
||||||
|
@ -47,6 +47,7 @@ import app.scodoc.notesdb as ndb
|
|||||||
from app.scodoc import sco_etud
|
from app.scodoc import sco_etud
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
from app.scodoc import sco_users
|
from app.scodoc import sco_users
|
||||||
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
def abs_notify(etudid, date):
|
def abs_notify(etudid, date):
|
||||||
@ -55,14 +56,21 @@ def abs_notify(etudid, date):
|
|||||||
(s'il n'y a pas de semestre courant, ne fait rien,
|
(s'il n'y a pas de semestre courant, ne fait rien,
|
||||||
car l'etudiant n'est pas inscrit au moment de l'absence!).
|
car l'etudiant n'est pas inscrit au moment de l'absence!).
|
||||||
"""
|
"""
|
||||||
from app.scodoc import sco_abs
|
from app.scodoc import sco_assiduites
|
||||||
|
|
||||||
formsemestre = retreive_current_formsemestre(etudid, date)
|
formsemestre = retreive_current_formsemestre(etudid, date)
|
||||||
if not formsemestre:
|
if not formsemestre:
|
||||||
return # non inscrit a la date, pas de notification
|
return # non inscrit a la date, pas de notification
|
||||||
|
|
||||||
nbabs, nbabsjust = sco_abs.get_abs_count_in_interval(
|
nbabs, nbabsjust = sco_assiduites.get_assiduites_count_in_interval(
|
||||||
etudid, formsemestre.date_debut.isoformat(), formsemestre.date_fin.isoformat()
|
etudid,
|
||||||
|
formsemestre.date_debut.isoformat(),
|
||||||
|
formsemestre.date_fin.isoformat(),
|
||||||
|
scu.translate_assiduites_metric(
|
||||||
|
sco_preferences.get_preference(
|
||||||
|
"assi_metrique", formsemestre.formsemestre_id
|
||||||
|
)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
do_abs_notify(formsemestre, etudid, date, nbabs, nbabsjust)
|
do_abs_notify(formsemestre, etudid, date, nbabs, nbabsjust)
|
||||||
|
|
||||||
@ -85,6 +93,7 @@ def do_abs_notify(formsemestre: FormSemestre, etudid, date, nbabs, nbabsjust):
|
|||||||
return # abort
|
return # abort
|
||||||
|
|
||||||
# Vérification fréquence (pour ne pas envoyer de mails trop souvent)
|
# Vérification fréquence (pour ne pas envoyer de mails trop souvent)
|
||||||
|
# TODO Mettre la fréquence dans les préférences assiduités
|
||||||
abs_notify_max_freq = sco_preferences.get_preference("abs_notify_max_freq")
|
abs_notify_max_freq = sco_preferences.get_preference("abs_notify_max_freq")
|
||||||
destinations_filtered = []
|
destinations_filtered = []
|
||||||
for email_addr in destinations:
|
for email_addr in destinations:
|
||||||
@ -174,6 +183,8 @@ def abs_notify_is_above_threshold(etudid, nbabs, nbabsjust, formsemestre_id):
|
|||||||
|
|
||||||
(nbabs > abs_notify_abs_threshold)
|
(nbabs > abs_notify_abs_threshold)
|
||||||
(nbabs - nbabs_last_notified) > abs_notify_abs_increment
|
(nbabs - nbabs_last_notified) > abs_notify_abs_increment
|
||||||
|
|
||||||
|
TODO Mettre à jour avec le module assiduité + fonctionnement métrique
|
||||||
"""
|
"""
|
||||||
abs_notify_abs_threshold = sco_preferences.get_preference(
|
abs_notify_abs_threshold = sco_preferences.get_preference(
|
||||||
"abs_notify_abs_threshold", formsemestre_id
|
"abs_notify_abs_threshold", formsemestre_id
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -68,7 +68,7 @@ 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 ScoException, ScoPermissionDenied
|
||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
@ -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):
|
||||||
@ -140,8 +146,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]
|
||||||
|
|
||||||
|
@ -34,6 +34,7 @@ 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.models import Identite
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app.scodoc import sco_import_etuds
|
from app.scodoc import sco_import_etuds
|
||||||
from app.scodoc import sco_groups
|
from app.scodoc import sco_groups
|
||||||
@ -351,10 +352,8 @@ def etudarchive_import_files(
|
|||||||
):
|
):
|
||||||
"Importe des fichiers"
|
"Importe des fichiers"
|
||||||
|
|
||||||
def callback(etud, data, filename):
|
def callback(etud: Identite, data, filename):
|
||||||
return _store_etud_file_to_new_archive(
|
return _store_etud_file_to_new_archive(etud.id, data, filename, description)
|
||||||
etud["etudid"], data, filename, description
|
|
||||||
)
|
|
||||||
|
|
||||||
# Utilise la fontion developpée au depart pour les photos
|
# Utilise la fontion developpée au depart pour les photos
|
||||||
(
|
(
|
||||||
|
231
app/scodoc/sco_archives_justificatifs.py
Normal file
231
app/scodoc/sco_archives_justificatifs.py
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
"""
|
||||||
|
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, str]] = {}
|
||||||
|
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(",")
|
||||||
|
if len(csv) < 4:
|
||||||
|
continue
|
||||||
|
fname: str = csv[0]
|
||||||
|
entry_date: datetime = is_iso_formated(csv[1], True)
|
||||||
|
delete_date: datetime = is_iso_formated(csv[2], True)
|
||||||
|
user_id = csv[3]
|
||||||
|
|
||||||
|
self.content[fname] = [entry_date, delete_date, user_id]
|
||||||
|
|
||||||
|
def set_trace(self, *fnames: str, mode: str = "entry", current_user: str = None):
|
||||||
|
"""Ajoute une trace du fichier donné
|
||||||
|
mode : entry / delete
|
||||||
|
"""
|
||||||
|
modes: list[str] = ["entry", "delete", "user_id"]
|
||||||
|
for fname in fnames:
|
||||||
|
if fname in modes:
|
||||||
|
continue
|
||||||
|
traced: list[datetime, datetime, str] = self.content.get(fname, False)
|
||||||
|
if not traced:
|
||||||
|
self.content[fname] = [None, None, None]
|
||||||
|
traced = self.content[fname]
|
||||||
|
|
||||||
|
traced[modes.index(mode)] = (
|
||||||
|
datetime.now() if mode != "user_id" else current_user
|
||||||
|
)
|
||||||
|
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"
|
||||||
|
if traced[0] is not None:
|
||||||
|
lines.append(f"{fname},{traced[0].isoformat()},{date_fin}, {traced[2]}")
|
||||||
|
with open(self.path, "w", encoding="utf-8") as file:
|
||||||
|
file.write("\n".join(lines))
|
||||||
|
|
||||||
|
def get_trace(
|
||||||
|
self, fnames: list[str] = None
|
||||||
|
) -> dict[str, list[datetime, datetime, str]]:
|
||||||
|
"""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:
|
||||||
|
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 = "",
|
||||||
|
user_id: str = None,
|
||||||
|
) -> 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, mode="entry")
|
||||||
|
if user_id is not None:
|
||||||
|
trace.set_trace(fname, mode="user_id", current_user=user_id)
|
||||||
|
|
||||||
|
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, mode="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[tuple[str, int]]:
|
||||||
|
"""
|
||||||
|
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)
|
||||||
|
trace: Trace = Trace(self.get_obj_dir(etudid))
|
||||||
|
traced = trace.get_trace(filenames)
|
||||||
|
retour = [(key, value[2]) for key, value in traced.items()]
|
||||||
|
|
||||||
|
return retour
|
||||||
|
|
||||||
|
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)
|
521
app/scodoc/sco_assiduites.py
Normal file
521
app/scodoc/sco_assiduites.py
Normal file
@ -0,0 +1,521 @@
|
|||||||
|
"""
|
||||||
|
Ecrit par Matthias Hartmann.
|
||||||
|
"""
|
||||||
|
from datetime import date, datetime, time, timedelta
|
||||||
|
from pytz import UTC
|
||||||
|
|
||||||
|
from app import log
|
||||||
|
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
|
||||||
|
from app.scodoc import sco_formsemestre_inscriptions
|
||||||
|
from app.scodoc import sco_preferences
|
||||||
|
from app.scodoc import sco_cache
|
||||||
|
from app.scodoc import sco_etud
|
||||||
|
from flask_sqlalchemy.query import Query
|
||||||
|
|
||||||
|
|
||||||
|
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():
|
||||||
|
# TODO : Utiliser la préférence de département : workdays
|
||||||
|
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, int or float]:
|
||||||
|
"""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: Query, metric: str = "all", filtered: dict[str, object] = None
|
||||||
|
) -> dict[str, int or float]:
|
||||||
|
"""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, Assiduite, 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) -> Query:
|
||||||
|
"""
|
||||||
|
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) -> Query:
|
||||||
|
"""
|
||||||
|
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,
|
||||||
|
) -> Query:
|
||||||
|
"""
|
||||||
|
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,
|
||||||
|
) -> Query:
|
||||||
|
"""
|
||||||
|
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) -> Query:
|
||||||
|
"""
|
||||||
|
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) -> Query:
|
||||||
|
"""
|
||||||
|
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(
|
||||||
|
collection_query: Assiduite or Justificatif,
|
||||||
|
collection_class: Assiduite or Justificatif,
|
||||||
|
formsemestre: FormSemestre,
|
||||||
|
) -> Query:
|
||||||
|
"""
|
||||||
|
Filtrage d'une collection en fonction d'un formsemestre
|
||||||
|
"""
|
||||||
|
|
||||||
|
if formsemestre is None:
|
||||||
|
return collection_query.filter(False)
|
||||||
|
|
||||||
|
collection_result = (
|
||||||
|
collection_query.join(Identite, collection_class.etudid == Identite.id)
|
||||||
|
.join(
|
||||||
|
FormSemestreInscription,
|
||||||
|
Identite.id == FormSemestreInscription.etudid,
|
||||||
|
)
|
||||||
|
.filter(FormSemestreInscription.formsemestre_id == formsemestre.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
form_date_debut = formsemestre.date_debut + timedelta(days=1)
|
||||||
|
form_date_fin = formsemestre.date_fin + timedelta(days=1)
|
||||||
|
|
||||||
|
collection_result = collection_result.filter(
|
||||||
|
collection_class.date_debut >= form_date_debut
|
||||||
|
)
|
||||||
|
|
||||||
|
return collection_result.filter(collection_class.date_fin <= form_date_fin)
|
||||||
|
|
||||||
|
|
||||||
|
def justifies(justi: Justificatif, obj: bool = False) -> list[int] or Query:
|
||||||
|
"""
|
||||||
|
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.filter_by(etudid=justi.etudid)
|
||||||
|
assiduites_query = assiduites_query.filter(
|
||||||
|
Assiduite.date_debut >= justi.date_debut, Assiduite.date_fin <= justi.date_fin
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
) -> Query:
|
||||||
|
"""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
|
||||||
|
|
||||||
|
|
||||||
|
# Gestion du cache
|
||||||
|
def get_assiduites_count(etudid: int, sem: dict) -> tuple[int, int]:
|
||||||
|
"""Les comptes d'absences de cet étudiant dans ce semestre:
|
||||||
|
tuple (nb abs non justifiées, nb abs justifiées)
|
||||||
|
Utilise un cache.
|
||||||
|
"""
|
||||||
|
metrique = sco_preferences.get_preference("assi_metrique", sem["formsemestre_id"])
|
||||||
|
return get_assiduites_count_in_interval(
|
||||||
|
etudid,
|
||||||
|
sem["date_debut_iso"],
|
||||||
|
sem["date_fin_iso"],
|
||||||
|
scu.translate_assiduites_metric(metrique),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def formsemestre_get_assiduites_count(
|
||||||
|
etudid: int, formsemestre: FormSemestre
|
||||||
|
) -> tuple[int, int]:
|
||||||
|
"""Les comptes d'absences de cet étudiant dans ce semestre:
|
||||||
|
tuple (nb abs non justifiées, nb abs justifiées)
|
||||||
|
Utilise un cache.
|
||||||
|
"""
|
||||||
|
metrique = sco_preferences.get_preference("assi_metrique", formsemestre.id)
|
||||||
|
return get_assiduites_count_in_interval(
|
||||||
|
etudid,
|
||||||
|
date_debut=formsemestre.date_debut,
|
||||||
|
date_fin=formsemestre.date_fin,
|
||||||
|
metrique=scu.translate_assiduites_metric(metrique),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_assiduites_count_in_interval(
|
||||||
|
etudid,
|
||||||
|
date_debut_iso: str = "",
|
||||||
|
date_fin_iso: str = "",
|
||||||
|
metrique="demi",
|
||||||
|
date_debut: datetime = None,
|
||||||
|
date_fin: datetime = None,
|
||||||
|
):
|
||||||
|
"""Les comptes d'absences de cet étudiant entre ces deux dates, incluses:
|
||||||
|
tuple (nb abs, nb abs justifiées)
|
||||||
|
On peut spécifier les dates comme datetime ou iso.
|
||||||
|
Utilise un cache.
|
||||||
|
"""
|
||||||
|
date_debut_iso = date_debut_iso or date_debut.isoformat()
|
||||||
|
date_fin_iso = date_fin_iso or date_fin.isoformat()
|
||||||
|
key = f"{etudid}_{date_debut_iso}_{date_fin_iso}{metrique}_assiduites"
|
||||||
|
|
||||||
|
r = sco_cache.AbsSemEtudCache.get(key)
|
||||||
|
if not r:
|
||||||
|
date_debut: datetime = date_debut or datetime.fromisoformat(date_debut_iso)
|
||||||
|
date_fin: datetime = date_fin or datetime.fromisoformat(date_fin_iso)
|
||||||
|
|
||||||
|
assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid)
|
||||||
|
assiduites = assiduites.filter(Assiduite.etat == scu.EtatAssiduite.ABSENT)
|
||||||
|
justificatifs: Justificatif = Justificatif.query.filter_by(etudid=etudid)
|
||||||
|
|
||||||
|
assiduites = filter_by_date(assiduites, Assiduite, date_debut, date_fin)
|
||||||
|
justificatifs = filter_by_date(
|
||||||
|
justificatifs, Justificatif, date_debut, date_fin
|
||||||
|
)
|
||||||
|
|
||||||
|
calculator: CountCalculator = CountCalculator()
|
||||||
|
calculator.compute_assiduites(assiduites)
|
||||||
|
nb_abs: dict = calculator.to_dict()[metrique]
|
||||||
|
|
||||||
|
abs_just: list[Assiduite] = get_all_justified(etudid, date_debut, date_fin)
|
||||||
|
|
||||||
|
calculator.reset()
|
||||||
|
calculator.compute_assiduites(abs_just)
|
||||||
|
nb_abs_just: dict = calculator.to_dict()[metrique]
|
||||||
|
|
||||||
|
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_assiduites_count(etudid, sem):
|
||||||
|
"""Invalidate (clear) cached counts"""
|
||||||
|
date_debut = sem["date_debut_iso"]
|
||||||
|
date_fin = sem["date_fin_iso"]
|
||||||
|
for met in scu.AssiduitesMetrics.TAG:
|
||||||
|
key = str(etudid) + "_" + date_debut + "_" + date_fin + f"{met}_assiduites"
|
||||||
|
sco_cache.AbsSemEtudCache.delete(key)
|
||||||
|
|
||||||
|
|
||||||
|
def invalidate_assiduites_count_sem(sem):
|
||||||
|
"""Invalidate (clear) cached abs counts for all the students of this semestre"""
|
||||||
|
inscriptions = (
|
||||||
|
sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits(
|
||||||
|
sem["formsemestre_id"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for ins in inscriptions:
|
||||||
|
invalidate_assiduites_count(ins["etudid"], sem)
|
||||||
|
|
||||||
|
|
||||||
|
def invalidate_assiduites_etud_date(etudid, date: datetime):
|
||||||
|
"""Doit etre appelé à chaque modification des assiduites
|
||||||
|
pour cet étudiant et cette date.
|
||||||
|
Invalide cache absence et caches semestre
|
||||||
|
"""
|
||||||
|
from app.scodoc import sco_compute_moy
|
||||||
|
|
||||||
|
# Semestres a cette date:
|
||||||
|
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)
|
||||||
|
if len(etud) == 0:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
etud = etud[0]
|
||||||
|
sems = [
|
||||||
|
sem
|
||||||
|
for sem in etud["sems"]
|
||||||
|
if scu.is_iso_formated(sem["date_debut_iso"], True).replace(tzinfo=UTC)
|
||||||
|
<= date.replace(tzinfo=UTC)
|
||||||
|
and scu.is_iso_formated(sem["date_fin_iso"], True).replace(tzinfo=UTC)
|
||||||
|
>= date.replace(tzinfo=UTC)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Invalide les PDF et les absences:
|
||||||
|
for sem in sems:
|
||||||
|
# Inval cache bulletin et/ou note_table
|
||||||
|
if sco_compute_moy.formsemestre_expressions_use_abscounts(
|
||||||
|
sem["formsemestre_id"]
|
||||||
|
):
|
||||||
|
# certaines formules utilisent les absences
|
||||||
|
pdfonly = False
|
||||||
|
else:
|
||||||
|
# efface toujours le PDF car il affiche en général les absences
|
||||||
|
pdfonly = True
|
||||||
|
|
||||||
|
sco_cache.invalidate_formsemestre(
|
||||||
|
formsemestre_id=sem["formsemestre_id"], pdfonly=pdfonly
|
||||||
|
)
|
||||||
|
|
||||||
|
# Inval cache compteurs absences:
|
||||||
|
invalidate_assiduites_count(etudid, sem)
|
||||||
|
|
||||||
|
|
||||||
|
def simple_invalidate_cache(obj: dict, etudid: str or int = None):
|
||||||
|
"""Invalide le cache de l'étudiant et du / des semestres"""
|
||||||
|
date_debut = (
|
||||||
|
obj["date_debut"]
|
||||||
|
if isinstance(obj["date_debut"], datetime)
|
||||||
|
else scu.is_iso_formated(obj["date_debut"], True)
|
||||||
|
)
|
||||||
|
date_fin = (
|
||||||
|
obj["date_fin"]
|
||||||
|
if isinstance(obj["date_fin"], datetime)
|
||||||
|
else scu.is_iso_formated(obj["date_fin"], True)
|
||||||
|
)
|
||||||
|
etudid = etudid if etudid is not None else obj["etudid"]
|
||||||
|
invalidate_assiduites_etud_date(etudid, date_debut)
|
||||||
|
invalidate_assiduites_etud_date(etudid, date_fin)
|
@ -47,6 +47,7 @@ from app.comp.res_but import ResultatsSemestreBUT
|
|||||||
from app.comp.res_compat import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
from app.models import (
|
from app.models import (
|
||||||
ApcParcours,
|
ApcParcours,
|
||||||
|
Evaluation,
|
||||||
Formation,
|
Formation,
|
||||||
FormSemestre,
|
FormSemestre,
|
||||||
Identite,
|
Identite,
|
||||||
@ -56,15 +57,13 @@ from app.scodoc.sco_permissions import Permission
|
|||||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
from app.scodoc import htmlutils
|
from app.scodoc import htmlutils
|
||||||
from app.scodoc import sco_abs
|
from app.scodoc import sco_assiduites
|
||||||
from app.scodoc import sco_abs_views
|
|
||||||
from app.scodoc import sco_bulletins_generator
|
from app.scodoc import sco_bulletins_generator
|
||||||
from app.scodoc import sco_bulletins_json
|
from app.scodoc import sco_bulletins_json
|
||||||
from app.scodoc import sco_bulletins_pdf
|
from app.scodoc import sco_bulletins_pdf
|
||||||
from app.scodoc import sco_bulletins_xml
|
from app.scodoc import sco_bulletins_xml
|
||||||
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_evaluation_db
|
|
||||||
from app.scodoc import sco_formsemestre
|
from app.scodoc import sco_formsemestre
|
||||||
from app.scodoc import sco_groups
|
from app.scodoc import sco_groups
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
@ -142,7 +141,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
|
|||||||
Cette fonction est utilisée pour les bulletins CLASSIQUES (DUT, ...)
|
Cette fonction est utilisée pour les bulletins CLASSIQUES (DUT, ...)
|
||||||
en HTML et PDF, mais pas ceux en XML.
|
en HTML et PDF, mais pas ceux en XML.
|
||||||
"""
|
"""
|
||||||
from app.scodoc import sco_abs
|
from app.scodoc import sco_assiduites
|
||||||
|
|
||||||
if version not in scu.BULLETINS_VERSIONS:
|
if version not in scu.BULLETINS_VERSIONS:
|
||||||
raise ValueError("invalid version code !")
|
raise ValueError("invalid version code !")
|
||||||
@ -197,7 +196,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
|
|||||||
pid = partition["partition_id"]
|
pid = partition["partition_id"]
|
||||||
partitions_etud_groups[pid] = sco_groups.get_etud_groups_in_partition(pid)
|
partitions_etud_groups[pid] = sco_groups.get_etud_groups_in_partition(pid)
|
||||||
# --- Absences
|
# --- Absences
|
||||||
I["nbabs"], I["nbabsjust"] = sco_abs.get_abs_count(etudid, nt.sem)
|
I["nbabs"], I["nbabsjust"] = sco_assiduites.get_assiduites_count(etudid, nt.sem)
|
||||||
|
|
||||||
# --- Decision Jury
|
# --- Decision Jury
|
||||||
infos, dpv = etud_descr_situation_semestre(
|
infos, dpv = etud_descr_situation_semestre(
|
||||||
@ -482,6 +481,7 @@ def _ue_mod_bulletin(
|
|||||||
mods = [] # result
|
mods = [] # result
|
||||||
ue_attente = False # true si une eval en attente dans cette UE
|
ue_attente = False # true si une eval en attente dans cette UE
|
||||||
for modimpl in ue_modimpls:
|
for modimpl in ue_modimpls:
|
||||||
|
modimpl_results = nt.modimpls_results.get(modimpl["moduleimpl_id"])
|
||||||
mod_attente = False
|
mod_attente = False
|
||||||
mod = modimpl.copy()
|
mod = modimpl.copy()
|
||||||
mod_moy = nt.get_etud_mod_moy(
|
mod_moy = nt.get_etud_mod_moy(
|
||||||
@ -489,7 +489,7 @@ def _ue_mod_bulletin(
|
|||||||
) # peut etre 'NI'
|
) # peut etre 'NI'
|
||||||
is_malus = mod["module"]["module_type"] == ModuleType.MALUS
|
is_malus = mod["module"]["module_type"] == ModuleType.MALUS
|
||||||
if bul_show_abs_modules:
|
if bul_show_abs_modules:
|
||||||
nbabs, nbabsjust = sco_abs.get_abs_count(etudid, sem)
|
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem)
|
||||||
mod_abs = [nbabs, nbabsjust]
|
mod_abs = [nbabs, nbabsjust]
|
||||||
mod["mod_abs_txt"] = scu.fmt_abs(mod_abs)
|
mod["mod_abs_txt"] = scu.fmt_abs(mod_abs)
|
||||||
else:
|
else:
|
||||||
@ -531,10 +531,13 @@ def _ue_mod_bulletin(
|
|||||||
scu.fmt_coef(modimpl["module"]["coefficient"]),
|
scu.fmt_coef(modimpl["module"]["coefficient"]),
|
||||||
sco_users.user_info(modimpl["responsable_id"])["nomcomplet"],
|
sco_users.user_info(modimpl["responsable_id"])["nomcomplet"],
|
||||||
)
|
)
|
||||||
link_mod = (
|
link_mod = f"""<a class="bull_link" href="{
|
||||||
'<a class="bull_link" href="moduleimpl_status?moduleimpl_id=%s" title="%s">'
|
url_for("notes.moduleimpl_status",
|
||||||
% (modimpl["moduleimpl_id"], mod["mod_descr_txt"])
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
moduleimpl_id=modimpl["moduleimpl_id"]
|
||||||
)
|
)
|
||||||
|
}" title="{mod["mod_descr_txt"]}">"""
|
||||||
|
|
||||||
if sco_preferences.get_preference("bul_show_codemodules", formsemestre_id):
|
if sco_preferences.get_preference("bul_show_codemodules", formsemestre_id):
|
||||||
mod["code"] = modimpl["module"]["code"]
|
mod["code"] = modimpl["module"]["code"]
|
||||||
mod["code_html"] = link_mod + (mod["code"] or "") + "</a>"
|
mod["code_html"] = link_mod + (mod["code"] or "") + "</a>"
|
||||||
@ -561,91 +564,88 @@ def _ue_mod_bulletin(
|
|||||||
mod["code_txt"] = ""
|
mod["code_txt"] = ""
|
||||||
mod["code_html"] = ""
|
mod["code_html"] = ""
|
||||||
# Evaluations: notes de chaque eval
|
# Evaluations: notes de chaque eval
|
||||||
evals = nt.get_evals_in_mod(modimpl["moduleimpl_id"])
|
evaluations_completes = nt.get_modimpl_evaluations_completes(
|
||||||
|
modimpl["moduleimpl_id"]
|
||||||
|
)
|
||||||
|
# On liste séparément les éval. complètes ou non
|
||||||
mod["evaluations"] = []
|
mod["evaluations"] = []
|
||||||
for e in evals:
|
mod["evaluations_incompletes"] = []
|
||||||
e = e.copy()
|
complete_eval_ids = {e.id for e in evaluations_completes}
|
||||||
if e["visibulletin"] or version == "long":
|
all_evals: list[Evaluation] = Evaluation.query.filter_by(
|
||||||
# affiche "bonus" quand les points de malus sont négatifs
|
moduleimpl_id=modimpl["moduleimpl_id"]
|
||||||
|
).order_by(Evaluation.numero, Evaluation.date_debut)
|
||||||
|
# (plus ancienne d'abord)
|
||||||
|
for e in all_evals:
|
||||||
|
if not e.visibulletin and version != "long":
|
||||||
|
continue
|
||||||
|
is_complete = e.id in complete_eval_ids
|
||||||
|
e_dict = e.to_dict_bul()
|
||||||
|
# Note à l'évaluation:
|
||||||
|
val = modimpl_results.evals_notes[e.id].get(etudid, "NP")
|
||||||
|
# Affiche "bonus" quand les points de malus sont négatifs
|
||||||
if is_malus:
|
if is_malus:
|
||||||
val = e["notes"].get(etudid, {"value": "NP"})[
|
if val == "NP":
|
||||||
"value"
|
e_dict["name"] = "Points de bonus/malus sur cette UE"
|
||||||
] # NA si etud demissionnaire
|
elif val > 0:
|
||||||
if val == "NP" or val > 0:
|
e_dict["name"] = "Points de malus sur cette UE"
|
||||||
e["name"] = "Points de malus sur cette UE"
|
|
||||||
else:
|
else:
|
||||||
e["name"] = "Points de bonus sur cette UE"
|
e_dict["name"] = "Points de bonus sur cette UE"
|
||||||
else:
|
else:
|
||||||
e["name"] = e["description"] or f"le {e['jour']}"
|
e_dict[
|
||||||
e["target_html"] = url_for(
|
"name"
|
||||||
|
] = f"""{e.description or ""} {
|
||||||
|
e.descr_date()
|
||||||
|
if e.date_debut and not is_complete
|
||||||
|
else ""}"""
|
||||||
|
e_dict["target_html"] = url_for(
|
||||||
"notes.evaluation_listenotes",
|
"notes.evaluation_listenotes",
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
evaluation_id=e["evaluation_id"],
|
evaluation_id=e.id,
|
||||||
format="html",
|
format="html",
|
||||||
tf_submitted=1,
|
tf_submitted=1,
|
||||||
)
|
)
|
||||||
e[
|
e_dict[
|
||||||
"name_html"
|
"name_html"
|
||||||
] = f"""<a class="bull_link" href="{
|
] = f"""<a class="bull_link" href="{
|
||||||
e['target_html']}">{e['name']}</a>"""
|
e_dict['target_html']}">{e_dict['name']}</a>"""
|
||||||
val = e["notes"].get(etudid, {"value": "NP"})["value"]
|
if is_complete: # évaluation complète
|
||||||
# val est NP si etud demissionnaire
|
# val est NP si etud demissionnaire
|
||||||
if val == "NP":
|
if val == "NP":
|
||||||
e["note_txt"] = "nd"
|
e_dict["note_txt"] = "nd"
|
||||||
e["note_html"] = '<span class="note_nd">nd</span>'
|
e_dict["note_html"] = '<span class="note_nd">nd</span>'
|
||||||
e["coef_txt"] = scu.fmt_coef(e["coefficient"])
|
e_dict["coef_txt"] = scu.fmt_coef(e["coefficient"])
|
||||||
else:
|
else:
|
||||||
# (-0.15) s'affiche "bonus de 0.15"
|
# (-0.15) s'affiche "bonus de 0.15"
|
||||||
if is_malus:
|
if is_malus:
|
||||||
val = abs(val)
|
val = abs(val)
|
||||||
e["note_txt"] = scu.fmt_note(val, note_max=e["note_max"])
|
e_dict["note_txt"] = e_dict["note_html"] = scu.fmt_note(
|
||||||
e["note_html"] = e["note_txt"]
|
val, note_max=e.note_max
|
||||||
if is_malus:
|
|
||||||
e["coef_txt"] = ""
|
|
||||||
else:
|
|
||||||
e["coef_txt"] = scu.fmt_coef(e["coefficient"])
|
|
||||||
if e["evaluation_type"] == scu.EVALUATION_RATTRAPAGE:
|
|
||||||
e["coef_txt"] = "rat."
|
|
||||||
elif e["evaluation_type"] == scu.EVALUATION_SESSION2:
|
|
||||||
e["coef_txt"] = "Ses. 2"
|
|
||||||
if e["etat"]["evalattente"]:
|
|
||||||
mod_attente = True # une eval en attente dans ce module
|
|
||||||
if ((not is_malus) or (val != "NP")) and (
|
|
||||||
(
|
|
||||||
e["evaluation_type"] == scu.EVALUATION_NORMALE
|
|
||||||
or not np.isnan(val)
|
|
||||||
)
|
)
|
||||||
|
else: # évaluation incomplète: pas de note
|
||||||
|
e_dict["note_txt"] = e_dict["note_html"] = ""
|
||||||
|
|
||||||
|
if is_malus:
|
||||||
|
e_dict["coef_txt"] = ""
|
||||||
|
else:
|
||||||
|
e_dict["coef_txt"] = scu.fmt_coef(e.coefficient)
|
||||||
|
if e.evaluation_type == scu.EVALUATION_RATTRAPAGE:
|
||||||
|
e_dict["coef_txt"] = "rat."
|
||||||
|
elif e.evaluation_type == scu.EVALUATION_SESSION2:
|
||||||
|
e_dict["coef_txt"] = "Ses. 2"
|
||||||
|
|
||||||
|
if modimpl_results.evaluations_etat[e.id].nb_attente:
|
||||||
|
mod_attente = True # une eval en attente dans ce module
|
||||||
|
|
||||||
|
if ((not is_malus) or (val != "NP")) and (
|
||||||
|
(e.evaluation_type == scu.EVALUATION_NORMALE or not np.isnan(val))
|
||||||
):
|
):
|
||||||
# ne liste pas les eval malus sans notes
|
# ne liste pas les eval malus sans notes
|
||||||
# ni les rattrapages et sessions 2 si pas de note
|
# ni les rattrapages et sessions 2 si pas de note
|
||||||
mod["evaluations"].append(e)
|
if e.id in complete_eval_ids:
|
||||||
|
mod["evaluations"].append(e_dict)
|
||||||
|
else:
|
||||||
|
mod["evaluations_incompletes"].append(e_dict)
|
||||||
|
|
||||||
# Evaluations incomplètes ou futures:
|
|
||||||
mod["evaluations_incompletes"] = []
|
|
||||||
if sco_preferences.get_preference("bul_show_all_evals", formsemestre_id):
|
|
||||||
complete_eval_ids = set([e["evaluation_id"] for e in evals])
|
|
||||||
all_evals = sco_evaluation_db.do_evaluation_list(
|
|
||||||
args={"moduleimpl_id": modimpl["moduleimpl_id"]}
|
|
||||||
)
|
|
||||||
all_evals.reverse() # plus ancienne d'abord
|
|
||||||
for e in all_evals:
|
|
||||||
if e["evaluation_id"] not in complete_eval_ids:
|
|
||||||
e = e.copy()
|
|
||||||
mod["evaluations_incompletes"].append(e)
|
|
||||||
e["name"] = (e["description"] or "") + " (%s)" % e["jour"]
|
|
||||||
e["target_html"] = url_for(
|
|
||||||
"notes.evaluation_listenotes",
|
|
||||||
scodoc_dept=g.scodoc_dept,
|
|
||||||
evaluation_id=e["evaluation_id"],
|
|
||||||
tf_submitted=1,
|
|
||||||
format="html",
|
|
||||||
)
|
|
||||||
e["name_html"] = '<a class="bull_link" href="%s">%s</a>' % (
|
|
||||||
e["target_html"],
|
|
||||||
e["name"],
|
|
||||||
)
|
|
||||||
e["note_txt"] = e["note_html"] = ""
|
|
||||||
e["coef_txt"] = scu.fmt_coef(e["coefficient"])
|
|
||||||
# Classement
|
# Classement
|
||||||
if (
|
if (
|
||||||
bul_show_mod_rangs
|
bul_show_mod_rangs
|
||||||
@ -1114,9 +1114,10 @@ def mail_bulletin(formsemestre_id, infos, pdfdata, filename, recipient_addr):
|
|||||||
hea = ""
|
hea = ""
|
||||||
|
|
||||||
if sco_preferences.get_preference("bul_mail_list_abs"):
|
if sco_preferences.get_preference("bul_mail_list_abs"):
|
||||||
hea += "\n\n" + sco_abs_views.ListeAbsEtud(
|
hea += "\n\n" + "(LISTE D'ABSENCES NON DISPONIBLE)" # XXX TODO-ASSIDUITE
|
||||||
etud["etudid"], with_evals=False, format="text"
|
# sco_abs_views.ListeAbsEtud(
|
||||||
)
|
# etud["etudid"], with_evals=False, format="text"
|
||||||
|
# )
|
||||||
|
|
||||||
subject = f"""Relevé de notes de {etud["nomprenom"]}"""
|
subject = f"""Relevé de notes de {etud["nomprenom"]}"""
|
||||||
recipients = [recipient_addr]
|
recipients = [recipient_addr]
|
||||||
|
@ -37,16 +37,15 @@ from app import db, 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
|
||||||
from app.models import Matiere, ModuleImpl, UniteEns
|
from app.models import Evaluation, Matiere, UniteEns
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
from app.models.formsemestre import FormSemestre
|
from app.models.formsemestre import FormSemestre
|
||||||
|
|
||||||
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 import sco_abs
|
from app.scodoc import sco_assiduites
|
||||||
from app.scodoc import sco_edit_ue
|
from app.scodoc import sco_edit_ue
|
||||||
from app.scodoc import sco_evaluations
|
from app.scodoc import sco_evaluations
|
||||||
from app.scodoc import sco_evaluation_db
|
|
||||||
from app.scodoc import sco_formsemestre
|
from app.scodoc import sco_formsemestre
|
||||||
from app.scodoc import sco_groups
|
from app.scodoc import sco_groups
|
||||||
from app.scodoc import sco_photos
|
from app.scodoc import sco_photos
|
||||||
@ -112,7 +111,7 @@ def formsemestre_bulletinetud_published_dict(
|
|||||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||||
|
|
||||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
if not etudid in nt.identdict:
|
if etudid not in nt.identdict:
|
||||||
abort(404, "etudiant non inscrit dans ce semestre")
|
abort(404, "etudiant non inscrit dans ce semestre")
|
||||||
d = {"type": "classic", "version": "0"}
|
d = {"type": "classic", "version": "0"}
|
||||||
if (not sem["bul_hide_xml"]) or force_publishing:
|
if (not sem["bul_hide_xml"]) or force_publishing:
|
||||||
@ -297,7 +296,7 @@ def formsemestre_bulletinetud_published_dict(
|
|||||||
|
|
||||||
# --- Absences
|
# --- Absences
|
||||||
if prefs["bul_show_abs"]:
|
if prefs["bul_show_abs"]:
|
||||||
nbabs, nbabsjust = sco_abs.get_abs_count(etudid, sem)
|
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem)
|
||||||
d["absences"] = dict(nbabs=nbabs, nbabsjust=nbabsjust)
|
d["absences"] = dict(nbabs=nbabs, nbabsjust=nbabsjust)
|
||||||
|
|
||||||
# --- Décision Jury
|
# --- Décision Jury
|
||||||
@ -324,7 +323,7 @@ def formsemestre_bulletinetud_published_dict(
|
|||||||
def _list_modimpls(
|
def _list_modimpls(
|
||||||
nt: NotesTableCompat,
|
nt: NotesTableCompat,
|
||||||
etudid: int,
|
etudid: int,
|
||||||
modimpls: list[ModuleImpl],
|
modimpls: list[dict],
|
||||||
prefs: SemPreferences,
|
prefs: SemPreferences,
|
||||||
version: str,
|
version: str,
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
@ -333,6 +332,7 @@ def _list_modimpls(
|
|||||||
mod_moy = scu.fmt_note(nt.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid))
|
mod_moy = scu.fmt_note(nt.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid))
|
||||||
if mod_moy == "NI": # ne mentionne pas les modules ou n'est pas inscrit
|
if mod_moy == "NI": # ne mentionne pas les modules ou n'est pas inscrit
|
||||||
continue
|
continue
|
||||||
|
modimpl_results = nt.modimpls_results.get(modimpl["moduleimpl_id"])
|
||||||
mod = modimpl["module"]
|
mod = modimpl["module"]
|
||||||
# if mod['ects'] is None:
|
# if mod['ects'] is None:
|
||||||
# ects = ''
|
# ects = ''
|
||||||
@ -363,61 +363,42 @@ def _list_modimpls(
|
|||||||
mod_dict["effectif"] = dict(value=nt.mod_rangs[modimpl["moduleimpl_id"]][1])
|
mod_dict["effectif"] = dict(value=nt.mod_rangs[modimpl["moduleimpl_id"]][1])
|
||||||
|
|
||||||
# --- notes de chaque eval:
|
# --- notes de chaque eval:
|
||||||
evals = nt.get_evals_in_mod(modimpl["moduleimpl_id"])
|
evaluations_completes = nt.get_modimpl_evaluations_completes(
|
||||||
|
modimpl["moduleimpl_id"]
|
||||||
|
)
|
||||||
mod_dict["evaluation"] = []
|
mod_dict["evaluation"] = []
|
||||||
if version != "short":
|
if version != "short":
|
||||||
for e in evals:
|
for e in evaluations_completes:
|
||||||
if e["visibulletin"] or version == "long":
|
if e.visibulletin or version == "long":
|
||||||
val = e["notes"].get(etudid, {"value": "NP"})["value"]
|
# Note à l'évaluation:
|
||||||
|
val = modimpl_results.evals_notes[e.id].get(etudid, "NP")
|
||||||
# nb: val est NA si etud démissionnaire
|
# nb: val est NA si etud démissionnaire
|
||||||
val = scu.fmt_note(val, note_max=e["note_max"])
|
e_dict = e.to_dict_bul()
|
||||||
eval_dict = dict(
|
e_dict["note"] = scu.fmt_note(val, note_max=e.note_max)
|
||||||
jour=ndb.DateDMYtoISO(e["jour"], null_is_empty=True),
|
|
||||||
heure_debut=ndb.TimetoISO8601(
|
|
||||||
e["heure_debut"], null_is_empty=True
|
|
||||||
),
|
|
||||||
heure_fin=ndb.TimetoISO8601(e["heure_fin"], null_is_empty=True),
|
|
||||||
coefficient=e["coefficient"],
|
|
||||||
evaluation_type=e["evaluation_type"],
|
|
||||||
# CM : ajout pour permettre de faire le lien sur
|
|
||||||
# les bulletins en ligne avec l'évaluation:
|
|
||||||
evaluation_id=e["evaluation_id"],
|
|
||||||
description=quote_xml_attr(e["description"]),
|
|
||||||
note=val,
|
|
||||||
)
|
|
||||||
if prefs["bul_show_minmax_eval"] or prefs["bul_show_moypromo"]:
|
|
||||||
etat = sco_evaluations.do_evaluation_etat(e["evaluation_id"])
|
|
||||||
if prefs["bul_show_minmax_eval"]:
|
|
||||||
eval_dict["min"] = etat["mini"] # chaine, sur 20
|
|
||||||
eval_dict["max"] = etat["maxi"]
|
|
||||||
if prefs["bul_show_moypromo"]:
|
|
||||||
eval_dict["moy"] = etat["moy"]
|
|
||||||
|
|
||||||
mod_dict["evaluation"].append(eval_dict)
|
if prefs["bul_show_minmax_eval"] or prefs["bul_show_moypromo"]:
|
||||||
|
# XXX à revoir pour utiliser modimplresult
|
||||||
|
etat = sco_evaluations.do_evaluation_etat(e.id)
|
||||||
|
if prefs["bul_show_minmax_eval"]:
|
||||||
|
e_dict["min"] = etat["mini"] # chaine, sur 20
|
||||||
|
e_dict["max"] = etat["maxi"]
|
||||||
|
if prefs["bul_show_moypromo"]:
|
||||||
|
e_dict["moy"] = etat["moy"]
|
||||||
|
|
||||||
|
mod_dict["evaluation"].append(e_dict)
|
||||||
|
|
||||||
# Evaluations incomplètes ou futures:
|
# Evaluations incomplètes ou futures:
|
||||||
complete_eval_ids = set([e["evaluation_id"] for e in evals])
|
complete_eval_ids = {e.id for e in evaluations_completes}
|
||||||
if prefs["bul_show_all_evals"]:
|
if prefs["bul_show_all_evals"]:
|
||||||
all_evals = sco_evaluation_db.do_evaluation_list(
|
evaluations: list[Evaluation] = Evaluation.query.filter_by(
|
||||||
args={"moduleimpl_id": modimpl["moduleimpl_id"]}
|
moduleimpl_id=modimpl["moduleimpl_id"]
|
||||||
)
|
).order_by(Evaluation.date_debut)
|
||||||
all_evals.reverse() # plus ancienne d'abord
|
# plus ancienne d'abord
|
||||||
for e in all_evals:
|
for e in evaluations:
|
||||||
if e["evaluation_id"] not in complete_eval_ids:
|
if e.id not in complete_eval_ids:
|
||||||
mod_dict["evaluation"].append(
|
e_dict = e.to_dict_bul()
|
||||||
dict(
|
e_dict["incomplete"] = 1
|
||||||
jour=ndb.DateDMYtoISO(e["jour"], null_is_empty=True),
|
mod_dict["evaluation"].append(e_dict)
|
||||||
heure_debut=ndb.TimetoISO8601(
|
|
||||||
e["heure_debut"], null_is_empty=True
|
|
||||||
),
|
|
||||||
heure_fin=ndb.TimetoISO8601(
|
|
||||||
e["heure_fin"], null_is_empty=True
|
|
||||||
),
|
|
||||||
coefficient=e["coefficient"],
|
|
||||||
description=quote_xml_attr(e["description"]),
|
|
||||||
incomplete="1",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
modules_dict.append(mod_dict)
|
modules_dict.append(mod_dict)
|
||||||
return modules_dict
|
return modules_dict
|
||||||
|
|
||||||
|
@ -132,11 +132,14 @@ class BulletinGeneratorLegacy(sco_bulletins_generator.BulletinGenerator):
|
|||||||
if sco_preferences.get_preference(
|
if sco_preferences.get_preference(
|
||||||
"bul_show_minmax_mod", formsemestre_id
|
"bul_show_minmax_mod", formsemestre_id
|
||||||
):
|
):
|
||||||
rang_minmax = '%s <span class="bul_minmax" title="[min, max] UE">[%s, %s]</span>' % (
|
rang_minmax = (
|
||||||
|
'%s <span class="bul_minmax" title="[min, max] UE">[%s, %s]</span>'
|
||||||
|
% (
|
||||||
mod["mod_rang_txt"],
|
mod["mod_rang_txt"],
|
||||||
scu.fmt_note(mod["stats"]["min"]),
|
scu.fmt_note(mod["stats"]["min"]),
|
||||||
scu.fmt_note(mod["stats"]["max"]),
|
scu.fmt_note(mod["stats"]["max"]),
|
||||||
)
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
rang_minmax = mod["mod_rang_txt"] # vide si pas option rang
|
rang_minmax = mod["mod_rang_txt"] # vide si pas option rang
|
||||||
H.append(
|
H.append(
|
||||||
@ -301,9 +304,11 @@ class BulletinGeneratorLegacy(sco_bulletins_generator.BulletinGenerator):
|
|||||||
authuser = self.authuser
|
authuser = self.authuser
|
||||||
H = []
|
H = []
|
||||||
# --- Absences
|
# --- Absences
|
||||||
|
# XXX TODO-ASSIDUITE
|
||||||
|
# au passage, utiliser url_for...
|
||||||
H.append(
|
H.append(
|
||||||
"""<p>
|
"""<p>
|
||||||
<a href="../Absences/CalAbs?etudid=%(etudid)s" class="bull_link">
|
XXX <a href="../Absences/CalAbs?etudid=%(etudid)s" class="bull_link">
|
||||||
<b>Absences :</b> %(nbabs)s demi-journées, dont %(nbabsjust)s justifiées
|
<b>Absences :</b> %(nbabs)s demi-journées, dont %(nbabsjust)s justifiées
|
||||||
(pendant ce semestre).
|
(pendant ce semestre).
|
||||||
</a></p>
|
</a></p>
|
||||||
|
@ -124,9 +124,12 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
|||||||
nbabs = self.infos["nbabs"]
|
nbabs = self.infos["nbabs"]
|
||||||
story.append(Spacer(1, 2 * mm))
|
story.append(Spacer(1, 2 * mm))
|
||||||
if nbabs:
|
if nbabs:
|
||||||
|
# XXX TODO-ASSIDUITE
|
||||||
|
# et utiliser url_for...
|
||||||
H.append(
|
H.append(
|
||||||
"""<p class="bul_abs">
|
"""<p class="bul_abs">
|
||||||
<a href="../Absences/CalAbs?etudid=%(etudid)s" class="bull_link">
|
<a href="../Absences/CalAbs?etudid=%(etudid)s" class="bull_link">
|
||||||
|
XXX
|
||||||
<b>Absences :</b> %(nbabs)s demi-journées, dont %(nbabsjust)s justifiées
|
<b>Absences :</b> %(nbabs)s demi-journées, dont %(nbabsjust)s justifiées
|
||||||
(pendant ce semestre).
|
(pendant ce semestre).
|
||||||
</a></p>
|
</a></p>
|
||||||
|
@ -50,11 +50,11 @@ import app.scodoc.sco_utils as scu
|
|||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
from app import log
|
from app import log
|
||||||
from app.but.bulletin_but_xml_compat import bulletin_but_xml_compat
|
from app.but.bulletin_but_xml_compat import bulletin_but_xml_compat
|
||||||
|
from app.models.evaluations import Evaluation
|
||||||
from app.models.formsemestre import FormSemestre
|
from app.models.formsemestre import FormSemestre
|
||||||
from app.scodoc import sco_abs
|
from app.scodoc import sco_assiduites
|
||||||
from app.scodoc import codes_cursus
|
from app.scodoc import codes_cursus
|
||||||
from app.scodoc import sco_edit_ue
|
from app.scodoc import sco_edit_ue
|
||||||
from app.scodoc import sco_evaluation_db
|
|
||||||
from app.scodoc import sco_formsemestre
|
from app.scodoc import sco_formsemestre
|
||||||
from app.scodoc import sco_groups
|
from app.scodoc import sco_groups
|
||||||
from app.scodoc import sco_photos
|
from app.scodoc import sco_photos
|
||||||
@ -63,6 +63,7 @@ from app.scodoc import sco_etud
|
|||||||
from app.scodoc import sco_xml
|
from app.scodoc import sco_xml
|
||||||
from app.scodoc.sco_xml import quote_xml_attr
|
from app.scodoc.sco_xml import quote_xml_attr
|
||||||
|
|
||||||
|
|
||||||
# -------- Bulletin en XML
|
# -------- Bulletin en XML
|
||||||
# (fonction séparée: n'utilise pas formsemestre_bulletinetud_dict()
|
# (fonction séparée: n'utilise pas formsemestre_bulletinetud_dict()
|
||||||
# pour simplifier le code, mais attention a la maintenance !)
|
# pour simplifier le code, mais attention a la maintenance !)
|
||||||
@ -241,6 +242,7 @@ def make_xml_formsemestre_bulletinetud(
|
|||||||
# Liste les modules de l'UE
|
# Liste les modules de l'UE
|
||||||
ue_modimpls = [mod for mod in modimpls if mod["module"]["ue_id"] == ue["ue_id"]]
|
ue_modimpls = [mod for mod in modimpls if mod["module"]["ue_id"] == ue["ue_id"]]
|
||||||
for modimpl in ue_modimpls:
|
for modimpl in ue_modimpls:
|
||||||
|
modimpl_results = nt.modimpls_results.get(modimpl["moduleimpl_id"])
|
||||||
mod_moy = scu.fmt_note(
|
mod_moy = scu.fmt_note(
|
||||||
nt.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid)
|
nt.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid)
|
||||||
)
|
)
|
||||||
@ -289,57 +291,34 @@ def make_xml_formsemestre_bulletinetud(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
# --- notes de chaque eval:
|
# --- notes de chaque eval:
|
||||||
evals = nt.get_evals_in_mod(modimpl["moduleimpl_id"])
|
evaluations_completes = nt.get_modimpl_evaluations_completes(
|
||||||
if version != "short":
|
modimpl["moduleimpl_id"]
|
||||||
for e in evals:
|
|
||||||
if e["visibulletin"] or version == "long":
|
|
||||||
x_eval = Element(
|
|
||||||
"evaluation",
|
|
||||||
jour=ndb.DateDMYtoISO(e["jour"], null_is_empty=True),
|
|
||||||
heure_debut=ndb.TimetoISO8601(
|
|
||||||
e["heure_debut"], null_is_empty=True
|
|
||||||
),
|
|
||||||
heure_fin=ndb.TimetoISO8601(
|
|
||||||
e["heure_fin"], null_is_empty=True
|
|
||||||
),
|
|
||||||
coefficient=str(e["coefficient"]),
|
|
||||||
evaluation_type=str(e["evaluation_type"]),
|
|
||||||
description=quote_xml_attr(e["description"]),
|
|
||||||
# notes envoyées sur 20, ceci juste pour garder trace:
|
|
||||||
note_max_origin=str(e["note_max"]),
|
|
||||||
)
|
)
|
||||||
|
if version != "short":
|
||||||
|
for e in evaluations_completes:
|
||||||
|
if e.visibulletin or version == "long":
|
||||||
|
# pour xml, tout convertir en chaines
|
||||||
|
e_dict = {k: str(v) for k, v in e.to_dict_bul().items()}
|
||||||
|
# notes envoyées sur 20, ceci juste pour garder trace:
|
||||||
|
e_dict["note_max_origin"] = str(e.note_max)
|
||||||
|
x_eval = Element("evaluation", **e_dict)
|
||||||
x_mod.append(x_eval)
|
x_mod.append(x_eval)
|
||||||
val = e["notes"].get(etudid, {"value": "NP"})[
|
# Note à l'évaluation:
|
||||||
"value"
|
val = modimpl_results.evals_notes[e.id].get(etudid, "NP")
|
||||||
] # NA si etud demissionnaire
|
val = scu.fmt_note(val, note_max=e.note_max)
|
||||||
val = scu.fmt_note(val, note_max=e["note_max"])
|
|
||||||
x_eval.append(Element("note", value=val))
|
x_eval.append(Element("note", value=val))
|
||||||
# Evaluations incomplètes ou futures:
|
# Evaluations incomplètes ou futures:
|
||||||
complete_eval_ids = set([e["evaluation_id"] for e in evals])
|
complete_eval_ids = {e.id for e in evaluations_completes}
|
||||||
if sco_preferences.get_preference(
|
if sco_preferences.get_preference(
|
||||||
"bul_show_all_evals", formsemestre_id
|
"bul_show_all_evals", formsemestre_id
|
||||||
):
|
):
|
||||||
all_evals = sco_evaluation_db.do_evaluation_list(
|
evaluations = Evaluation.query.filter_by(
|
||||||
args={"moduleimpl_id": modimpl["moduleimpl_id"]}
|
moduleimpl_id=modimpl["moduleimpl_id"]
|
||||||
)
|
).order_by(Evaluation.date_debut)
|
||||||
all_evals.reverse() # plus ancienne d'abord
|
for e in evaluations:
|
||||||
for e in all_evals:
|
if e.id not in complete_eval_ids:
|
||||||
if e["evaluation_id"] not in complete_eval_ids:
|
e_dict = e.to_dict_bul()
|
||||||
x_eval = Element(
|
x_eval = Element("evaluation", **e_dict)
|
||||||
"evaluation",
|
|
||||||
jour=ndb.DateDMYtoISO(e["jour"], null_is_empty=True),
|
|
||||||
heure_debut=ndb.TimetoISO8601(
|
|
||||||
e["heure_debut"], null_is_empty=True
|
|
||||||
),
|
|
||||||
heure_fin=ndb.TimetoISO8601(
|
|
||||||
e["heure_fin"], null_is_empty=True
|
|
||||||
),
|
|
||||||
coefficient=str(e["coefficient"]),
|
|
||||||
description=quote_xml_attr(e["description"]),
|
|
||||||
incomplete="1",
|
|
||||||
# notes envoyées sur 20, ceci juste pour garder trace:
|
|
||||||
note_max_origin=str(e["note_max"] or ""),
|
|
||||||
)
|
|
||||||
x_mod.append(x_eval)
|
x_mod.append(x_eval)
|
||||||
# UE capitalisee (listee seulement si meilleure que l'UE courante)
|
# UE capitalisee (listee seulement si meilleure que l'UE courante)
|
||||||
if ue_status["is_capitalized"]:
|
if ue_status["is_capitalized"]:
|
||||||
@ -369,7 +348,7 @@ def make_xml_formsemestre_bulletinetud(
|
|||||||
|
|
||||||
# --- Absences
|
# --- Absences
|
||||||
if sco_preferences.get_preference("bul_show_abs", formsemestre_id):
|
if sco_preferences.get_preference("bul_show_abs", formsemestre_id):
|
||||||
nbabs, nbabsjust = sco_abs.get_abs_count(etudid, sem)
|
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem)
|
||||||
doc.append(Element("absences", nbabs=str(nbabs), nbabsjust=str(nbabsjust)))
|
doc.append(Element("absences", nbabs=str(nbabs), nbabsjust=str(nbabsjust)))
|
||||||
# --- Decision Jury
|
# --- Decision Jury
|
||||||
if (
|
if (
|
||||||
|
@ -273,9 +273,7 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
|
|||||||
|
|
||||||
if formsemestre_id is None:
|
if formsemestre_id is None:
|
||||||
# clear all caches
|
# clear all caches
|
||||||
log(
|
log(f"--- invalidate_formsemestre: clearing all caches. pdfonly={pdfonly}---")
|
||||||
f"----- invalidate_formsemestre: clearing all caches. pdfonly={pdfonly}-----"
|
|
||||||
)
|
|
||||||
formsemestre_ids = [
|
formsemestre_ids = [
|
||||||
formsemestre.id
|
formsemestre.id
|
||||||
for formsemestre in FormSemestre.query.filter_by(dept_id=g.scodoc_dept_id)
|
for formsemestre in FormSemestre.query.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
@ -285,7 +283,7 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
|
|||||||
formsemestre_id
|
formsemestre_id
|
||||||
] + sco_cursus.list_formsemestre_utilisateurs_uecap(formsemestre_id)
|
] + sco_cursus.list_formsemestre_utilisateurs_uecap(formsemestre_id)
|
||||||
log(
|
log(
|
||||||
f"----- invalidate_formsemestre: clearing {formsemestre_ids}. pdfonly={pdfonly} -----"
|
f"--- invalidate_formsemestre: clearing {formsemestre_ids}. pdfonly={pdfonly} ---"
|
||||||
)
|
)
|
||||||
|
|
||||||
if not pdfonly:
|
if not pdfonly:
|
||||||
|
511
app/scodoc/sco_cal.py
Normal file
511
app/scodoc/sco_cal.py
Normal file
@ -0,0 +1,511 @@
|
|||||||
|
# -*- mode: python -*-
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gestion scolarite IUT
|
||||||
|
#
|
||||||
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
"""Génération calendrier (ancienne présentation)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import calendar
|
||||||
|
import datetime
|
||||||
|
import html
|
||||||
|
import time
|
||||||
|
|
||||||
|
from app.scodoc.sco_exceptions import ScoValueError, ScoInvalidDateError
|
||||||
|
from app.scodoc import sco_preferences
|
||||||
|
import app.scodoc.sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
|
def is_work_saturday():
|
||||||
|
"Vrai si le samedi est travaillé"
|
||||||
|
return int(sco_preferences.get_preference("work_saturday"))
|
||||||
|
|
||||||
|
|
||||||
|
def MonthNbDays(month, year):
|
||||||
|
"returns nb of days in month"
|
||||||
|
if month > 7:
|
||||||
|
month = month + 1
|
||||||
|
if month % 2:
|
||||||
|
return 31
|
||||||
|
elif month == 2:
|
||||||
|
if calendar.isleap(year):
|
||||||
|
return 29
|
||||||
|
else:
|
||||||
|
return 28
|
||||||
|
else:
|
||||||
|
return 30
|
||||||
|
|
||||||
|
|
||||||
|
class ddmmyyyy(object):
|
||||||
|
"""immutable dates"""
|
||||||
|
|
||||||
|
def __init__(self, date=None, fmt="ddmmyyyy", work_saturday=False):
|
||||||
|
self.work_saturday = work_saturday
|
||||||
|
if date is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
if fmt == "ddmmyyyy":
|
||||||
|
self.day, self.month, self.year = date.split("/")
|
||||||
|
elif fmt == "iso":
|
||||||
|
self.year, self.month, self.day = date.split("-")
|
||||||
|
else:
|
||||||
|
raise ValueError("invalid format spec. (%s)" % fmt)
|
||||||
|
self.year = int(self.year)
|
||||||
|
self.month = int(self.month)
|
||||||
|
self.day = int(self.day)
|
||||||
|
except ValueError:
|
||||||
|
raise ScoValueError("date invalide: %s" % date)
|
||||||
|
# accept years YYYY or YY, uses 1970 as pivot
|
||||||
|
if self.year < 1970:
|
||||||
|
if self.year > 100:
|
||||||
|
raise ScoInvalidDateError("Année invalide: %s" % self.year)
|
||||||
|
if self.year < 70:
|
||||||
|
self.year = self.year + 2000
|
||||||
|
else:
|
||||||
|
self.year = self.year + 1900
|
||||||
|
if self.month < 1 or self.month > 12:
|
||||||
|
raise ScoInvalidDateError("Mois invalide: %s" % self.month)
|
||||||
|
|
||||||
|
if self.day < 1 or self.day > MonthNbDays(self.month, self.year):
|
||||||
|
raise ScoInvalidDateError("Jour invalide: %s" % self.day)
|
||||||
|
|
||||||
|
# weekday in 0-6, where 0 is monday
|
||||||
|
self.weekday = calendar.weekday(self.year, self.month, self.day)
|
||||||
|
|
||||||
|
self.time = time.mktime((self.year, self.month, self.day, 0, 0, 0, 0, 0, 0))
|
||||||
|
|
||||||
|
def iswork(self):
|
||||||
|
"returns true if workable day"
|
||||||
|
if self.work_saturday:
|
||||||
|
nbdays = 6
|
||||||
|
else:
|
||||||
|
nbdays = 5
|
||||||
|
if (
|
||||||
|
self.weekday >= 0 and self.weekday < nbdays
|
||||||
|
): # monday-friday or monday-saturday
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "'%02d/%02d/%04d'" % (self.day, self.month, self.year)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%02d/%02d/%04d" % (self.day, self.month, self.year)
|
||||||
|
|
||||||
|
def ISO(self):
|
||||||
|
"iso8601 representation of the date"
|
||||||
|
return "%04d-%02d-%02d" % (self.year, self.month, self.day)
|
||||||
|
|
||||||
|
def next_day(self, days=1):
|
||||||
|
"date for the next day (nota: may be a non workable day)"
|
||||||
|
day = self.day + days
|
||||||
|
month = self.month
|
||||||
|
year = self.year
|
||||||
|
|
||||||
|
while day > MonthNbDays(month, year):
|
||||||
|
day = day - MonthNbDays(month, year)
|
||||||
|
month = month + 1
|
||||||
|
if month > 12:
|
||||||
|
month = 1
|
||||||
|
year = year + 1
|
||||||
|
return self.__class__(
|
||||||
|
"%02d/%02d/%04d" % (day, month, year), work_saturday=self.work_saturday
|
||||||
|
)
|
||||||
|
|
||||||
|
def prev(self, days=1):
|
||||||
|
"date for previous day"
|
||||||
|
day = self.day - days
|
||||||
|
month = self.month
|
||||||
|
year = self.year
|
||||||
|
while day <= 0:
|
||||||
|
month = month - 1
|
||||||
|
if month == 0:
|
||||||
|
month = 12
|
||||||
|
year = year - 1
|
||||||
|
day = day + MonthNbDays(month, year)
|
||||||
|
|
||||||
|
return self.__class__(
|
||||||
|
"%02d/%02d/%04d" % (day, month, year), work_saturday=self.work_saturday
|
||||||
|
)
|
||||||
|
|
||||||
|
def next_monday(self):
|
||||||
|
"date of next monday"
|
||||||
|
return self.next_day((7 - self.weekday) % 7)
|
||||||
|
|
||||||
|
def prev_monday(self):
|
||||||
|
"date of last monday, but on sunday, pick next monday"
|
||||||
|
if self.weekday == 6:
|
||||||
|
return self.next_monday()
|
||||||
|
else:
|
||||||
|
return self.prev(self.weekday)
|
||||||
|
|
||||||
|
def __cmp__(self, other): # #py3 TODO à supprimer
|
||||||
|
"""return a negative integer if self < other,
|
||||||
|
zero if self == other, a positive integer if self > other"""
|
||||||
|
return int(self.time - other.time)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.time == other.time
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return self.time != other.time
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
return self.time < other.time
|
||||||
|
|
||||||
|
def __le__(self, other):
|
||||||
|
return self.time <= other.time
|
||||||
|
|
||||||
|
def __gt__(self, other):
|
||||||
|
return self.time > other.time
|
||||||
|
|
||||||
|
def __ge__(self, other):
|
||||||
|
return self.time >= other.time
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
"we are immutable !"
|
||||||
|
return hash(self.time) ^ hash(str(self))
|
||||||
|
|
||||||
|
|
||||||
|
# d = ddmmyyyy( '21/12/99' )
|
||||||
|
def DateRangeISO(date_beg, date_end, workable=1):
|
||||||
|
"""returns list of dates in [date_beg,date_end]
|
||||||
|
workable = 1 => keeps only workable days"""
|
||||||
|
if not date_beg:
|
||||||
|
raise ScoValueError("pas de date spécifiée !")
|
||||||
|
if not date_end:
|
||||||
|
date_end = date_beg
|
||||||
|
r = []
|
||||||
|
work_saturday = is_work_saturday()
|
||||||
|
try:
|
||||||
|
cur = ddmmyyyy(date_beg, work_saturday=work_saturday)
|
||||||
|
end = ddmmyyyy(date_end, work_saturday=work_saturday)
|
||||||
|
except (AttributeError, ValueError) as e:
|
||||||
|
raise ScoValueError("date invalide !") from e
|
||||||
|
while cur <= end:
|
||||||
|
if (not workable) or cur.iswork():
|
||||||
|
r.append(cur)
|
||||||
|
cur = cur.next_day()
|
||||||
|
|
||||||
|
return [x.ISO() for x in r]
|
||||||
|
|
||||||
|
|
||||||
|
def day_names():
|
||||||
|
"""Returns week day names.
|
||||||
|
If work_saturday property is set, include saturday
|
||||||
|
"""
|
||||||
|
if is_work_saturday():
|
||||||
|
return scu.DAY_NAMES[:-1]
|
||||||
|
else:
|
||||||
|
return scu.DAY_NAMES[:-2]
|
||||||
|
|
||||||
|
|
||||||
|
def next_iso_day(date):
|
||||||
|
"return date after date"
|
||||||
|
d = ddmmyyyy(date, fmt="iso", work_saturday=is_work_saturday())
|
||||||
|
return d.next_day().ISO()
|
||||||
|
|
||||||
|
|
||||||
|
def YearTable(
|
||||||
|
year,
|
||||||
|
events=[],
|
||||||
|
firstmonth=9,
|
||||||
|
lastmonth=7,
|
||||||
|
halfday=0,
|
||||||
|
dayattributes="",
|
||||||
|
pad_width=8,
|
||||||
|
):
|
||||||
|
"""Generate a calendar table
|
||||||
|
events = list of tuples (date, text, color, href [,halfday])
|
||||||
|
where date is a string in ISO format (yyyy-mm-dd)
|
||||||
|
halfday is boolean (true: morning, false: afternoon)
|
||||||
|
text = text to put in calendar (must be short, 1-5 cars) (optional)
|
||||||
|
if halfday, generate 2 cells per day (morning, afternoon)
|
||||||
|
"""
|
||||||
|
T = [
|
||||||
|
'<table id="maincalendar" class="maincalendar" border="3" cellpadding="1" cellspacing="1" frame="box">'
|
||||||
|
]
|
||||||
|
T.append("<tr>")
|
||||||
|
month = firstmonth
|
||||||
|
while 1:
|
||||||
|
T.append('<td valign="top">')
|
||||||
|
T.append(MonthTableHead(month))
|
||||||
|
T.append(
|
||||||
|
MonthTableBody(
|
||||||
|
month,
|
||||||
|
year,
|
||||||
|
events,
|
||||||
|
halfday,
|
||||||
|
dayattributes,
|
||||||
|
is_work_saturday(),
|
||||||
|
pad_width=pad_width,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
T.append(MonthTableTail())
|
||||||
|
T.append("</td>")
|
||||||
|
if month == lastmonth:
|
||||||
|
break
|
||||||
|
month = month + 1
|
||||||
|
if month > 12:
|
||||||
|
month = 1
|
||||||
|
year = year + 1
|
||||||
|
T.append("</table>")
|
||||||
|
return "\n".join(T)
|
||||||
|
|
||||||
|
|
||||||
|
# ------ HTML Calendar functions (see YearTable function)
|
||||||
|
|
||||||
|
# MONTH/DAY NAMES:
|
||||||
|
|
||||||
|
MONTHNAMES = (
|
||||||
|
"Janvier",
|
||||||
|
"Février",
|
||||||
|
"Mars",
|
||||||
|
"Avril",
|
||||||
|
"Mai",
|
||||||
|
"Juin",
|
||||||
|
"Juillet",
|
||||||
|
"Aout",
|
||||||
|
"Septembre",
|
||||||
|
"Octobre",
|
||||||
|
"Novembre",
|
||||||
|
"Décembre",
|
||||||
|
)
|
||||||
|
|
||||||
|
MONTHNAMES_ABREV = (
|
||||||
|
"Jan.",
|
||||||
|
"Fév.",
|
||||||
|
"Mars",
|
||||||
|
"Avr.",
|
||||||
|
"Mai ",
|
||||||
|
"Juin",
|
||||||
|
"Juil",
|
||||||
|
"Aout",
|
||||||
|
"Sept",
|
||||||
|
"Oct.",
|
||||||
|
"Nov.",
|
||||||
|
"Déc.",
|
||||||
|
)
|
||||||
|
|
||||||
|
DAYNAMES = ("Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche")
|
||||||
|
|
||||||
|
DAYNAMES_ABREV = ("L", "M", "M", "J", "V", "S", "D")
|
||||||
|
|
||||||
|
# COLORS:
|
||||||
|
|
||||||
|
WHITE = "#FFFFFF"
|
||||||
|
GRAY1 = "#EEEEEE"
|
||||||
|
GREEN3 = "#99CC99"
|
||||||
|
WEEKDAYCOLOR = GRAY1
|
||||||
|
WEEKENDCOLOR = GREEN3
|
||||||
|
|
||||||
|
|
||||||
|
def MonthTableHead(month):
|
||||||
|
color = WHITE
|
||||||
|
return """<table class="monthcalendar" border="0" cellpadding="0" cellspacing="0" frame="box">
|
||||||
|
<tr bgcolor="%s"><td class="calcol" colspan="2" align="center">%s</td></tr>\n""" % (
|
||||||
|
color,
|
||||||
|
MONTHNAMES_ABREV[month - 1],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def MonthTableTail():
|
||||||
|
return "</table>\n"
|
||||||
|
|
||||||
|
|
||||||
|
def MonthTableBody(
|
||||||
|
month, year, events=[], halfday=0, trattributes="", work_saturday=False, pad_width=8
|
||||||
|
):
|
||||||
|
firstday, nbdays = calendar.monthrange(year, month)
|
||||||
|
localtime = time.localtime()
|
||||||
|
current_weeknum = time.strftime("%U", localtime)
|
||||||
|
current_year = localtime[0]
|
||||||
|
T = []
|
||||||
|
# cherche date du lundi de la 1ere semaine de ce mois
|
||||||
|
monday = ddmmyyyy("1/%d/%d" % (month, year))
|
||||||
|
while monday.weekday != 0:
|
||||||
|
monday = monday.prev()
|
||||||
|
|
||||||
|
if work_saturday:
|
||||||
|
weekend = ("D",)
|
||||||
|
else:
|
||||||
|
weekend = ("S", "D")
|
||||||
|
|
||||||
|
if not halfday:
|
||||||
|
for d in range(1, nbdays + 1):
|
||||||
|
weeknum = time.strftime(
|
||||||
|
"%U", time.strptime("%d/%d/%d" % (d, month, year), "%d/%m/%Y")
|
||||||
|
)
|
||||||
|
day = DAYNAMES_ABREV[(firstday + d - 1) % 7]
|
||||||
|
if day in weekend:
|
||||||
|
bgcolor = WEEKENDCOLOR
|
||||||
|
weekclass = "wkend"
|
||||||
|
attrs = ""
|
||||||
|
else:
|
||||||
|
bgcolor = WEEKDAYCOLOR
|
||||||
|
weekclass = "wk" + str(monday).replace("/", "_")
|
||||||
|
attrs = trattributes
|
||||||
|
color = None
|
||||||
|
legend = ""
|
||||||
|
href = ""
|
||||||
|
descr = ""
|
||||||
|
# event this day ?
|
||||||
|
# each event is a tuple (date, text, color, href)
|
||||||
|
# where date is a string in ISO format (yyyy-mm-dd)
|
||||||
|
for ev in events:
|
||||||
|
ev_year = int(ev[0][:4])
|
||||||
|
ev_month = int(ev[0][5:7])
|
||||||
|
ev_day = int(ev[0][8:10])
|
||||||
|
if year == ev_year and month == ev_month and ev_day == d:
|
||||||
|
if ev[1]:
|
||||||
|
legend = ev[1]
|
||||||
|
if ev[2]:
|
||||||
|
color = ev[2]
|
||||||
|
if ev[3]:
|
||||||
|
href = ev[3]
|
||||||
|
if len(ev) > 4 and ev[4]:
|
||||||
|
descr = ev[4]
|
||||||
|
#
|
||||||
|
cc = []
|
||||||
|
if color is not None:
|
||||||
|
cc.append('<td bgcolor="%s" class="calcell">' % color)
|
||||||
|
else:
|
||||||
|
cc.append('<td class="calcell">')
|
||||||
|
|
||||||
|
if href:
|
||||||
|
href = 'href="%s"' % href
|
||||||
|
if descr:
|
||||||
|
descr = 'title="%s"' % html.escape(descr, quote=True)
|
||||||
|
if href or descr:
|
||||||
|
cc.append("<a %s %s>" % (href, descr))
|
||||||
|
|
||||||
|
if legend or d == 1:
|
||||||
|
if pad_width is not None:
|
||||||
|
n = pad_width - len(legend) # pad to 8 cars
|
||||||
|
if n > 0:
|
||||||
|
legend = (
|
||||||
|
" " * (n // 2) + legend + " " * ((n + 1) // 2)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
legend = " " # empty cell
|
||||||
|
cc.append(legend)
|
||||||
|
if href or descr:
|
||||||
|
cc.append("</a>")
|
||||||
|
cc.append("</td>")
|
||||||
|
cell = "".join(cc)
|
||||||
|
if day == "D":
|
||||||
|
monday = monday.next_day(7)
|
||||||
|
if (
|
||||||
|
weeknum == current_weeknum
|
||||||
|
and current_year == year
|
||||||
|
and weekclass != "wkend"
|
||||||
|
):
|
||||||
|
weekclass += " currentweek"
|
||||||
|
T.append(
|
||||||
|
'<tr bgcolor="%s" class="%s" %s><td class="calday">%d%s</td>%s</tr>'
|
||||||
|
% (bgcolor, weekclass, attrs, d, day, cell)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Calendar with 2 cells / day
|
||||||
|
for d in range(1, nbdays + 1):
|
||||||
|
weeknum = time.strftime(
|
||||||
|
"%U", time.strptime("%d/%d/%d" % (d, month, year), "%d/%m/%Y")
|
||||||
|
)
|
||||||
|
day = DAYNAMES_ABREV[(firstday + d - 1) % 7]
|
||||||
|
if day in weekend:
|
||||||
|
bgcolor = WEEKENDCOLOR
|
||||||
|
weekclass = "wkend"
|
||||||
|
attrs = ""
|
||||||
|
else:
|
||||||
|
bgcolor = WEEKDAYCOLOR
|
||||||
|
weekclass = "wk" + str(monday).replace("/", "_")
|
||||||
|
attrs = trattributes
|
||||||
|
if (
|
||||||
|
weeknum == current_weeknum
|
||||||
|
and current_year == year
|
||||||
|
and weekclass != "wkend"
|
||||||
|
):
|
||||||
|
weeknum += " currentweek"
|
||||||
|
|
||||||
|
if day == "D":
|
||||||
|
monday = monday.next_day(7)
|
||||||
|
T.append(
|
||||||
|
'<tr bgcolor="%s" class="wk%s" %s><td class="calday">%d%s</td>'
|
||||||
|
% (bgcolor, weekclass, attrs, d, day)
|
||||||
|
)
|
||||||
|
cc = []
|
||||||
|
for morning in (True, False):
|
||||||
|
color = None
|
||||||
|
legend = ""
|
||||||
|
href = ""
|
||||||
|
descr = ""
|
||||||
|
for ev in events:
|
||||||
|
ev_year = int(ev[0][:4])
|
||||||
|
ev_month = int(ev[0][5:7])
|
||||||
|
ev_day = int(ev[0][8:10])
|
||||||
|
if ev[4] is not None:
|
||||||
|
ev_half = int(ev[4])
|
||||||
|
else:
|
||||||
|
ev_half = 0
|
||||||
|
if (
|
||||||
|
year == ev_year
|
||||||
|
and month == ev_month
|
||||||
|
and ev_day == d
|
||||||
|
and morning == ev_half
|
||||||
|
):
|
||||||
|
if ev[1]:
|
||||||
|
legend = ev[1]
|
||||||
|
if ev[2]:
|
||||||
|
color = ev[2]
|
||||||
|
if ev[3]:
|
||||||
|
href = ev[3]
|
||||||
|
if len(ev) > 5 and ev[5]:
|
||||||
|
descr = ev[5]
|
||||||
|
#
|
||||||
|
if color is not None:
|
||||||
|
cc.append('<td bgcolor="%s" class="calcell">' % (color))
|
||||||
|
else:
|
||||||
|
cc.append('<td class="calcell">')
|
||||||
|
if href:
|
||||||
|
href = 'href="%s"' % href
|
||||||
|
if descr:
|
||||||
|
descr = 'title="%s"' % html.escape(descr, quote=True)
|
||||||
|
if href or descr:
|
||||||
|
cc.append("<a %s %s>" % (href, descr))
|
||||||
|
if legend or d == 1:
|
||||||
|
n = 3 - len(legend) # pad to 3 cars
|
||||||
|
if n > 0:
|
||||||
|
legend = (
|
||||||
|
" " * (n // 2) + legend + " " * ((n + 1) // 2)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
legend = " " # empty cell
|
||||||
|
cc.append(legend)
|
||||||
|
if href or descr:
|
||||||
|
cc.append("</a>")
|
||||||
|
cc.append("</td>\n")
|
||||||
|
T.append("".join(cc) + "</tr>")
|
||||||
|
return "\n".join(T)
|
@ -1389,14 +1389,14 @@ def ue_sharing_code(ue_code: str = "", ue_id: int = None, hide_ue_id: int = None
|
|||||||
# UE du même code, code formation et departement:
|
# UE du même code, code formation et departement:
|
||||||
q_ues = (
|
q_ues = (
|
||||||
UniteEns.query.filter_by(ue_code=ue_code)
|
UniteEns.query.filter_by(ue_code=ue_code)
|
||||||
.join(UniteEns.formation, aliased=True)
|
.join(UniteEns.formation)
|
||||||
.filter_by(dept_id=g.scodoc_dept_id, formation_code=formation_code)
|
.filter_by(dept_id=g.scodoc_dept_id, formation_code=formation_code)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Toutes les UE du departement avec ce code:
|
# Toutes les UE du departement avec ce code:
|
||||||
q_ues = (
|
q_ues = (
|
||||||
UniteEns.query.filter_by(ue_code=ue_code)
|
UniteEns.query.filter_by(ue_code=ue_code)
|
||||||
.join(UniteEns.formation, aliased=True)
|
.join(UniteEns.formation)
|
||||||
.filter_by(dept_id=g.scodoc_dept_id)
|
.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -29,36 +29,18 @@
|
|||||||
"""
|
"""
|
||||||
from flask import url_for, g
|
from flask import url_for, g
|
||||||
|
|
||||||
|
from app import db
|
||||||
|
from app.models import Evaluation, FormSemestre, Identite
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
import app.scodoc.notesdb as ndb
|
|
||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
from app.scodoc import sco_abs
|
|
||||||
from app.scodoc import sco_etud
|
|
||||||
from app.scodoc import sco_evaluations
|
from app.scodoc import sco_evaluations
|
||||||
from app.scodoc import sco_evaluation_db
|
from app.scodoc import sco_evaluation_db
|
||||||
from app.scodoc import sco_formsemestre
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
from app.scodoc import sco_groups
|
from app.scodoc import sco_groups
|
||||||
from app.scodoc import sco_moduleimpl
|
|
||||||
|
|
||||||
# matin et/ou après-midi ?
|
|
||||||
def _eval_demijournee(E):
|
|
||||||
"1 si matin, 0 si apres midi, 2 si toute la journee"
|
|
||||||
am, pm = False, False
|
|
||||||
if E["heure_debut"] < "13:00":
|
|
||||||
am = True
|
|
||||||
if E["heure_fin"] > "13:00":
|
|
||||||
pm = True
|
|
||||||
if am and pm:
|
|
||||||
demijournee = 2
|
|
||||||
elif am:
|
|
||||||
demijournee = 1
|
|
||||||
else:
|
|
||||||
demijournee = 0
|
|
||||||
pm = True
|
|
||||||
return am, pm, demijournee
|
|
||||||
|
|
||||||
|
|
||||||
def evaluation_check_absences(evaluation_id):
|
# XXX TODO-ASSIDUITE https://scodoc.org/git/ScoDoc/ScoDoc/issues/685
|
||||||
|
def evaluation_check_absences(evaluation: Evaluation):
|
||||||
"""Vérifie les absences au moment de cette évaluation.
|
"""Vérifie les absences au moment de cette évaluation.
|
||||||
Cas incohérents que l'on peut rencontrer pour chaque étudiant:
|
Cas incohérents que l'on peut rencontrer pour chaque étudiant:
|
||||||
note et absent
|
note et absent
|
||||||
@ -66,51 +48,60 @@ def evaluation_check_absences(evaluation_id):
|
|||||||
ABS et absent justifié
|
ABS et absent justifié
|
||||||
EXC et pas noté absent
|
EXC et pas noté absent
|
||||||
EXC et pas justifie
|
EXC et pas justifie
|
||||||
Ramene 3 listes d'etudid
|
Ramene 5 listes d'etudid
|
||||||
"""
|
"""
|
||||||
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
|
raise ScoValueError("Fonction non disponible, patience !") # XXX TODO-ASSIDUITE
|
||||||
if not E["jour"]:
|
|
||||||
|
if not evaluation.date_debut:
|
||||||
return [], [], [], [], [] # evaluation sans date
|
return [], [], [], [], [] # evaluation sans date
|
||||||
|
|
||||||
am, pm, demijournee = _eval_demijournee(E)
|
am, pm = evaluation.is_matin(), evaluation.is_apresmidi()
|
||||||
|
|
||||||
# Liste les absences à ce moment:
|
# Liste les absences à ce moment:
|
||||||
A = sco_abs.list_abs_jour(ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm)
|
absences = sco_abs.list_abs_jour(evaluation.date_debut, am=am, pm=pm)
|
||||||
As = set([x["etudid"] for x in A]) # ensemble des etudiants absents
|
abs_etudids = set([x["etudid"] for x in absences]) # ensemble des etudiants absents
|
||||||
NJ = sco_abs.list_abs_non_just_jour(ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm)
|
abs_non_just = sco_abs.list_abs_non_just_jour(
|
||||||
NJs = set([x["etudid"] for x in NJ]) # ensemble des etudiants absents non justifies
|
evaluation.date_debut.date(), am=am, pm=pm
|
||||||
Just = sco_abs.list_abs_jour(
|
|
||||||
ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm, is_abs=None, is_just=True
|
|
||||||
)
|
)
|
||||||
Justs = set([x["etudid"] for x in Just]) # ensemble des etudiants avec justif
|
abs_nj_etudids = set(
|
||||||
|
[x["etudid"] for x in abs_non_just]
|
||||||
|
) # ensemble des etudiants absents non justifies
|
||||||
|
justifs = sco_abs.list_abs_jour(
|
||||||
|
evaluation.date_debut.date(), am=am, pm=pm, is_abs=None, is_just=True
|
||||||
|
)
|
||||||
|
just_etudids = set(
|
||||||
|
[x["etudid"] for x in justifs]
|
||||||
|
) # ensemble des etudiants avec justif
|
||||||
|
|
||||||
# Les notes:
|
# Les notes:
|
||||||
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
|
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation.id)
|
||||||
ValButAbs = [] # une note mais noté absent
|
ValButAbs = [] # une note mais noté absent
|
||||||
AbsNonSignalee = [] # note ABS mais pas noté absent
|
AbsNonSignalee = [] # note ABS mais pas noté absent
|
||||||
ExcNonSignalee = [] # note EXC mais pas noté absent
|
ExcNonSignalee = [] # note EXC mais pas noté absent
|
||||||
ExcNonJust = [] # note EXC mais absent non justifie
|
ExcNonJust = [] # note EXC mais absent non justifie
|
||||||
AbsButExc = [] # note ABS mais justifié
|
AbsButExc = [] # note ABS mais justifié
|
||||||
for etudid, _ in sco_groups.do_evaluation_listeetuds_groups(
|
for etudid, _ in sco_groups.do_evaluation_listeetuds_groups(
|
||||||
evaluation_id, getallstudents=True
|
evaluation.id, getallstudents=True
|
||||||
):
|
):
|
||||||
if etudid in notes_db:
|
if etudid in notes_db:
|
||||||
val = notes_db[etudid]["value"]
|
val = notes_db[etudid]["value"]
|
||||||
if (
|
if (
|
||||||
val != None and val != scu.NOTES_NEUTRALISE and val != scu.NOTES_ATTENTE
|
val is not None
|
||||||
) and etudid in As:
|
and val != scu.NOTES_NEUTRALISE
|
||||||
|
and val != scu.NOTES_ATTENTE
|
||||||
|
) and etudid in abs_etudids:
|
||||||
# note valide et absent
|
# note valide et absent
|
||||||
ValButAbs.append(etudid)
|
ValButAbs.append(etudid)
|
||||||
if val is None and not etudid in As:
|
if val is None and not etudid in abs_etudids:
|
||||||
# absent mais pas signale comme tel
|
# absent mais pas signale comme tel
|
||||||
AbsNonSignalee.append(etudid)
|
AbsNonSignalee.append(etudid)
|
||||||
if val == scu.NOTES_NEUTRALISE and not etudid in As:
|
if val == scu.NOTES_NEUTRALISE and not etudid in abs_etudids:
|
||||||
# Neutralisé mais pas signale absent
|
# Neutralisé mais pas signale absent
|
||||||
ExcNonSignalee.append(etudid)
|
ExcNonSignalee.append(etudid)
|
||||||
if val == scu.NOTES_NEUTRALISE and etudid in NJs:
|
if val == scu.NOTES_NEUTRALISE and etudid in abs_nj_etudids:
|
||||||
# EXC mais pas justifié
|
# EXC mais pas justifié
|
||||||
ExcNonJust.append(etudid)
|
ExcNonJust.append(etudid)
|
||||||
if val is None and etudid in Justs:
|
if val is None and etudid in just_etudids:
|
||||||
# ABS mais justificatif
|
# ABS mais justificatif
|
||||||
AbsButExc.append(etudid)
|
AbsButExc.append(etudid)
|
||||||
|
|
||||||
@ -119,9 +110,16 @@ def evaluation_check_absences(evaluation_id):
|
|||||||
|
|
||||||
def evaluation_check_absences_html(evaluation_id, with_header=True, show_ok=True):
|
def evaluation_check_absences_html(evaluation_id, with_header=True, show_ok=True):
|
||||||
"""Affiche état vérification absences d'une évaluation"""
|
"""Affiche état vérification absences d'une évaluation"""
|
||||||
|
evaluation: Evaluation = db.session.get(Evaluation, evaluation_id)
|
||||||
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
|
am, pm = evaluation.is_matin(), evaluation.is_apresmidi()
|
||||||
am, pm, demijournee = _eval_demijournee(E)
|
# 1 si matin, 0 si apres midi, 2 si toute la journee:
|
||||||
|
match am, pm:
|
||||||
|
case False, True:
|
||||||
|
demijournee = 0
|
||||||
|
case True, False:
|
||||||
|
demijournee = 1
|
||||||
|
case _:
|
||||||
|
demijournee = 2
|
||||||
|
|
||||||
(
|
(
|
||||||
ValButAbs,
|
ValButAbs,
|
||||||
@ -129,19 +127,23 @@ def evaluation_check_absences_html(evaluation_id, with_header=True, show_ok=True
|
|||||||
ExcNonSignalee,
|
ExcNonSignalee,
|
||||||
ExcNonJust,
|
ExcNonJust,
|
||||||
AbsButExc,
|
AbsButExc,
|
||||||
) = evaluation_check_absences(evaluation_id)
|
) = evaluation_check_absences(evaluation)
|
||||||
|
|
||||||
if with_header:
|
if with_header:
|
||||||
H = [
|
H = [
|
||||||
html_sco_header.html_sem_header("Vérification absences à l'évaluation"),
|
html_sco_header.html_sem_header("Vérification absences à l'évaluation"),
|
||||||
sco_evaluations.evaluation_describe(evaluation_id=evaluation_id),
|
sco_evaluations.evaluation_describe(evaluation_id=evaluation.id),
|
||||||
"""<p class="help">Vérification de la cohérence entre les notes saisies et les absences signalées.</p>""",
|
"""<p class="help">Vérification de la cohérence entre les notes saisies
|
||||||
|
et les absences signalées.</p>""",
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
# pas de header, mais un titre
|
# pas de header, mais un titre
|
||||||
H = [
|
H = [
|
||||||
"""<h2 class="eval_check_absences">%s du %s """
|
f"""<h2 class="eval_check_absences">{
|
||||||
% (E["description"], E["jour"])
|
evaluation.description or "évaluation"
|
||||||
|
} du {
|
||||||
|
evaluation.date_debut.strftime("%d/%m/%Y") if evaluation.date_debut else ""
|
||||||
|
} """
|
||||||
]
|
]
|
||||||
if (
|
if (
|
||||||
not ValButAbs
|
not ValButAbs
|
||||||
@ -157,26 +159,27 @@ def evaluation_check_absences_html(evaluation_id, with_header=True, show_ok=True
|
|||||||
if not etudids and show_ok:
|
if not etudids and show_ok:
|
||||||
H.append("<li>aucun</li>")
|
H.append("<li>aucun</li>")
|
||||||
for etudid in etudids:
|
for etudid in etudids:
|
||||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
etud: Identite = db.session.get(Identite, etudid)
|
||||||
H.append(
|
H.append(
|
||||||
'<li><a class="discretelink" href="%s">'
|
f"""<li><a class="discretelink" href="{
|
||||||
% url_for(
|
url_for(
|
||||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
|
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
||||||
)
|
)
|
||||||
+ "%(nomprenom)s</a>" % etud
|
}">{etud.nomprenom}</a>"""
|
||||||
)
|
)
|
||||||
if linkabs:
|
if linkabs:
|
||||||
H.append(
|
url = url_for(
|
||||||
f"""<a class="stdlink" href="{url_for(
|
"absences.doSignaleAbsence", # XXX TODO-ASSIDUITE
|
||||||
'absences.doSignaleAbsence',
|
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
etudid=etud["etudid"],
|
etudid=etudid,
|
||||||
datedebut=E["jour"],
|
# par defaut signale le jour du début de l'éval
|
||||||
datefin=E["jour"],
|
datedebut=evaluation.date_debut.strftime("%d/%m/%Y"),
|
||||||
|
datefin=evaluation.date_debut.strftime("%d/%m/%Y"),
|
||||||
demijournee=demijournee,
|
demijournee=demijournee,
|
||||||
moduleimpl_id=E["moduleimpl_id"],
|
moduleimpl_id=evaluation.moduleimpl_id,
|
||||||
)
|
)
|
||||||
}">signaler cette absence</a>"""
|
H.append(
|
||||||
|
f"""<a class="stdlink" href="{url}">signaler cette absence</a>"""
|
||||||
)
|
)
|
||||||
H.append("</li>")
|
H.append("</li>")
|
||||||
H.append("</ul>")
|
H.append("</ul>")
|
||||||
@ -218,7 +221,9 @@ def evaluation_check_absences_html(evaluation_id, with_header=True, show_ok=True
|
|||||||
|
|
||||||
def formsemestre_check_absences_html(formsemestre_id):
|
def formsemestre_check_absences_html(formsemestre_id):
|
||||||
"""Affiche etat verification absences pour toutes les evaluations du semestre !"""
|
"""Affiche etat verification absences pour toutes les evaluations du semestre !"""
|
||||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
formsemestre: FormSemestre = FormSemestre.query.filter_by(
|
||||||
|
dept_id=g.scodoc_dept_id, id=formsemestre_id
|
||||||
|
).first_or_404()
|
||||||
H = [
|
H = [
|
||||||
html_sco_header.html_sem_header(
|
html_sco_header.html_sem_header(
|
||||||
"Vérification absences aux évaluations de ce semestre",
|
"Vérification absences aux évaluations de ce semestre",
|
||||||
@ -229,29 +234,27 @@ def formsemestre_check_absences_html(formsemestre_id):
|
|||||||
</p>""",
|
</p>""",
|
||||||
]
|
]
|
||||||
# Modules, dans l'ordre
|
# Modules, dans l'ordre
|
||||||
Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
|
for modimpl in formsemestre.modimpls_sorted:
|
||||||
for M in Mlist:
|
if modimpl.evaluations.count() > 0:
|
||||||
evals = sco_evaluation_db.do_evaluation_list(
|
|
||||||
{"moduleimpl_id": M["moduleimpl_id"]}
|
|
||||||
)
|
|
||||||
if evals:
|
|
||||||
H.append(
|
H.append(
|
||||||
'<div class="module_check_absences"><h2><a href="moduleimpl_status?moduleimpl_id=%s">%s: %s</a></h2>'
|
f"""<div class="module_check_absences">
|
||||||
% (
|
<h2><a href="{
|
||||||
M["moduleimpl_id"],
|
url_for("notes.moduleimpl_status",
|
||||||
M["module"]["code"] or "",
|
scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
|
||||||
M["module"]["abbrev"] or "",
|
}">{modimpl.module.code or ""}: {modimpl.module.abbrev or ""}</a>
|
||||||
|
</h2>"""
|
||||||
)
|
)
|
||||||
)
|
for evaluation in modimpl.evaluations.order_by(
|
||||||
for E in evals:
|
Evaluation.numero, Evaluation.date_debut
|
||||||
|
):
|
||||||
H.append(
|
H.append(
|
||||||
evaluation_check_absences_html(
|
evaluation_check_absences_html(
|
||||||
E["evaluation_id"],
|
evaluation.id, # XXX TODO-ASSIDUITE remplacer par evaluation ...
|
||||||
with_header=False,
|
with_header=False,
|
||||||
show_ok=False,
|
show_ok=False,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if evals:
|
|
||||||
H.append("</div>")
|
H.append("</div>")
|
||||||
|
|
||||||
H.append(html_sco_header.sco_footer())
|
H.append(html_sco_header.sco_footer())
|
||||||
return "\n".join(H)
|
return "\n".join(H)
|
||||||
|
@ -37,14 +37,13 @@ from flask_login import current_user
|
|||||||
from app import db, log
|
from app import db, log
|
||||||
|
|
||||||
from app.models import Evaluation, ModuleImpl, ScolarNews
|
from app.models import Evaluation, ModuleImpl, ScolarNews
|
||||||
from app.models.evaluations import evaluation_enrich_dict, check_evaluation_args
|
from app.models.evaluations import check_convert_evaluation_args
|
||||||
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_exceptions import AccessDenied, ScoValueError
|
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||||
|
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
from app.scodoc import sco_moduleimpl
|
from app.scodoc import sco_moduleimpl
|
||||||
from app.scodoc import sco_permissions_check
|
|
||||||
|
|
||||||
|
|
||||||
_evaluationEditor = ndb.EditableTable(
|
_evaluationEditor = ndb.EditableTable(
|
||||||
@ -53,9 +52,8 @@ _evaluationEditor = ndb.EditableTable(
|
|||||||
(
|
(
|
||||||
"evaluation_id",
|
"evaluation_id",
|
||||||
"moduleimpl_id",
|
"moduleimpl_id",
|
||||||
"jour",
|
"date_debut",
|
||||||
"heure_debut",
|
"date_fin",
|
||||||
"heure_fin",
|
|
||||||
"description",
|
"description",
|
||||||
"note_max",
|
"note_max",
|
||||||
"coefficient",
|
"coefficient",
|
||||||
@ -64,15 +62,11 @@ _evaluationEditor = ndb.EditableTable(
|
|||||||
"evaluation_type",
|
"evaluation_type",
|
||||||
"numero",
|
"numero",
|
||||||
),
|
),
|
||||||
sortkey="numero desc, jour desc, heure_debut desc", # plus recente d'abord
|
sortkey="numero, date_debut desc", # plus recente d'abord
|
||||||
output_formators={
|
output_formators={
|
||||||
"jour": ndb.DateISOtoDMY,
|
|
||||||
"numero": ndb.int_null_is_zero,
|
"numero": ndb.int_null_is_zero,
|
||||||
},
|
},
|
||||||
input_formators={
|
input_formators={
|
||||||
"jour": ndb.DateDMYtoISO,
|
|
||||||
"heure_debut": ndb.TimetoISO8601, # converti par evaluation_enrich_dict
|
|
||||||
"heure_fin": ndb.TimetoISO8601, # converti par evaluation_enrich_dict
|
|
||||||
"visibulletin": bool,
|
"visibulletin": bool,
|
||||||
"publish_incomplete": bool,
|
"publish_incomplete": bool,
|
||||||
"evaluation_type": int,
|
"evaluation_type": int,
|
||||||
@ -80,8 +74,9 @@ _evaluationEditor = ndb.EditableTable(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def do_evaluation_list(args, sortkey=None):
|
def get_evaluation_dict(args: dict) -> list[dict]:
|
||||||
"""List evaluations, sorted by numero (or most recent date first).
|
"""Liste evaluations, triées numero (or most recent date first).
|
||||||
|
Fonction de transition pour ancien code ScoDoc7.
|
||||||
|
|
||||||
Ajoute les champs:
|
Ajoute les champs:
|
||||||
'duree' : '2h30'
|
'duree' : '2h30'
|
||||||
@ -89,14 +84,8 @@ def do_evaluation_list(args, sortkey=None):
|
|||||||
'apresmidi' : 1 (termine après 12:00) ou 0
|
'apresmidi' : 1 (termine après 12:00) ou 0
|
||||||
'descrheure' : ' de 15h00 à 16h30'
|
'descrheure' : ' de 15h00 à 16h30'
|
||||||
"""
|
"""
|
||||||
# Attention: transformation fonction ScoDoc7 en SQLAlchemy
|
|
||||||
cnx = ndb.GetDBConnexion()
|
|
||||||
evals = _evaluationEditor.list(cnx, args, sortkey=sortkey)
|
|
||||||
# calcule duree (chaine de car.) de chaque evaluation et ajoute jour_iso, matin, apresmidi
|
# calcule duree (chaine de car.) de chaque evaluation et ajoute jour_iso, matin, apresmidi
|
||||||
for e in evals:
|
return [e.to_dict() for e in Evaluation.query.filter_by(**args)]
|
||||||
evaluation_enrich_dict(e)
|
|
||||||
|
|
||||||
return evals
|
|
||||||
|
|
||||||
|
|
||||||
def do_evaluation_list_in_formsemestre(formsemestre_id):
|
def do_evaluation_list_in_formsemestre(formsemestre_id):
|
||||||
@ -104,119 +93,37 @@ def do_evaluation_list_in_formsemestre(formsemestre_id):
|
|||||||
mods = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
|
mods = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
|
||||||
evals = []
|
evals = []
|
||||||
for modimpl in mods:
|
for modimpl in mods:
|
||||||
evals += do_evaluation_list(args={"moduleimpl_id": modimpl["moduleimpl_id"]})
|
evals += get_evaluation_dict(args={"moduleimpl_id": modimpl["moduleimpl_id"]})
|
||||||
return evals
|
return evals
|
||||||
|
|
||||||
|
|
||||||
def do_evaluation_create(
|
|
||||||
moduleimpl_id=None,
|
|
||||||
jour=None,
|
|
||||||
heure_debut=None,
|
|
||||||
heure_fin=None,
|
|
||||||
description=None,
|
|
||||||
note_max=None,
|
|
||||||
coefficient=None,
|
|
||||||
visibulletin=None,
|
|
||||||
publish_incomplete=None,
|
|
||||||
evaluation_type=None,
|
|
||||||
numero=None,
|
|
||||||
**kw, # ceci pour absorber les arguments excedentaires de tf #sco8
|
|
||||||
):
|
|
||||||
"""Create an evaluation"""
|
|
||||||
if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id):
|
|
||||||
raise AccessDenied(
|
|
||||||
f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
|
|
||||||
)
|
|
||||||
args = locals()
|
|
||||||
log("do_evaluation_create: args=" + str(args))
|
|
||||||
modimpl: ModuleImpl = db.session.get(ModuleImpl, moduleimpl_id)
|
|
||||||
if modimpl is None:
|
|
||||||
raise ValueError("module not found")
|
|
||||||
check_evaluation_args(args)
|
|
||||||
# Check numeros
|
|
||||||
moduleimpl_evaluation_renumber(moduleimpl_id, only_if_unumbered=True)
|
|
||||||
if not "numero" in args or args["numero"] is None:
|
|
||||||
n = None
|
|
||||||
# determine le numero avec la date
|
|
||||||
# Liste des eval existantes triees par date, la plus ancienne en tete
|
|
||||||
mod_evals = do_evaluation_list(
|
|
||||||
args={"moduleimpl_id": moduleimpl_id},
|
|
||||||
sortkey="jour asc, heure_debut asc",
|
|
||||||
)
|
|
||||||
if args["jour"]:
|
|
||||||
next_eval = None
|
|
||||||
t = (
|
|
||||||
ndb.DateDMYtoISO(args["jour"], null_is_empty=True),
|
|
||||||
ndb.TimetoISO8601(args["heure_debut"], null_is_empty=True),
|
|
||||||
)
|
|
||||||
for e in mod_evals:
|
|
||||||
if (
|
|
||||||
ndb.DateDMYtoISO(e["jour"], null_is_empty=True),
|
|
||||||
ndb.TimetoISO8601(e["heure_debut"], null_is_empty=True),
|
|
||||||
) > t:
|
|
||||||
next_eval = e
|
|
||||||
break
|
|
||||||
if next_eval:
|
|
||||||
n = moduleimpl_evaluation_insert_before(mod_evals, next_eval)
|
|
||||||
else:
|
|
||||||
n = None # a placer en fin
|
|
||||||
if n is None: # pas de date ou en fin:
|
|
||||||
if mod_evals:
|
|
||||||
log(pprint.pformat(mod_evals[-1]))
|
|
||||||
n = mod_evals[-1]["numero"] + 1
|
|
||||||
else:
|
|
||||||
n = 0 # the only one
|
|
||||||
# log("creating with numero n=%d" % n)
|
|
||||||
args["numero"] = n
|
|
||||||
|
|
||||||
#
|
|
||||||
cnx = ndb.GetDBConnexion()
|
|
||||||
r = _evaluationEditor.create(cnx, args)
|
|
||||||
|
|
||||||
# news
|
|
||||||
sco_cache.invalidate_formsemestre(formsemestre_id=modimpl.formsemestre_id)
|
|
||||||
url = url_for(
|
|
||||||
"notes.moduleimpl_status",
|
|
||||||
scodoc_dept=g.scodoc_dept,
|
|
||||||
moduleimpl_id=moduleimpl_id,
|
|
||||||
)
|
|
||||||
ScolarNews.add(
|
|
||||||
typ=ScolarNews.NEWS_NOTE,
|
|
||||||
obj=moduleimpl_id,
|
|
||||||
text=f"""Création d'une évaluation dans <a href="{url}">{
|
|
||||||
modimpl.module.titre or '(module sans titre)'}</a>""",
|
|
||||||
url=url,
|
|
||||||
)
|
|
||||||
|
|
||||||
return r
|
|
||||||
|
|
||||||
|
|
||||||
def do_evaluation_edit(args):
|
def do_evaluation_edit(args):
|
||||||
"edit an evaluation"
|
"edit an evaluation"
|
||||||
evaluation_id = args["evaluation_id"]
|
evaluation_id = args["evaluation_id"]
|
||||||
the_evals = do_evaluation_list({"evaluation_id": evaluation_id})
|
evaluation: Evaluation = db.session.get(Evaluation, evaluation_id)
|
||||||
if not the_evals:
|
if evaluation is None:
|
||||||
raise ValueError("evaluation inexistante !")
|
raise ValueError("evaluation inexistante !")
|
||||||
moduleimpl_id = the_evals[0]["moduleimpl_id"]
|
|
||||||
if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id):
|
if not evaluation.moduleimpl.can_edit_evaluation(current_user):
|
||||||
raise AccessDenied(
|
raise AccessDenied(
|
||||||
"Modification évaluation impossible pour %s" % current_user.get_nomplogin()
|
f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
|
||||||
)
|
)
|
||||||
args["moduleimpl_id"] = moduleimpl_id
|
args["moduleimpl_id"] = evaluation.moduleimpl.id
|
||||||
check_evaluation_args(args)
|
check_convert_evaluation_args(evaluation.moduleimpl, args)
|
||||||
|
|
||||||
cnx = ndb.GetDBConnexion()
|
cnx = ndb.GetDBConnexion()
|
||||||
_evaluationEditor.edit(cnx, args)
|
_evaluationEditor.edit(cnx, args)
|
||||||
# inval cache pour ce semestre
|
# inval cache pour ce semestre
|
||||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
sco_cache.invalidate_formsemestre(
|
||||||
sco_cache.invalidate_formsemestre(formsemestre_id=M["formsemestre_id"])
|
formsemestre_id=evaluation.moduleimpl.formsemestre_id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def do_evaluation_delete(evaluation_id):
|
def do_evaluation_delete(evaluation_id):
|
||||||
"delete evaluation"
|
"delete evaluation"
|
||||||
evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id)
|
evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id)
|
||||||
modimpl: ModuleImpl = evaluation.moduleimpl
|
modimpl: ModuleImpl = evaluation.moduleimpl
|
||||||
if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=modimpl.id):
|
if not modimpl.can_edit_evaluation(current_user):
|
||||||
raise AccessDenied(
|
raise AccessDenied(
|
||||||
f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
|
f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
|
||||||
)
|
)
|
||||||
@ -226,7 +133,7 @@ def do_evaluation_delete(evaluation_id):
|
|||||||
raise ScoValueError(
|
raise ScoValueError(
|
||||||
"Impossible de supprimer cette évaluation: il reste des notes"
|
"Impossible de supprimer cette évaluation: il reste des notes"
|
||||||
)
|
)
|
||||||
|
log(f"deleting evaluation {evaluation}")
|
||||||
db.session.delete(evaluation)
|
db.session.delete(evaluation)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
@ -287,68 +194,6 @@ def do_evaluation_get_all_notes(
|
|||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
def moduleimpl_evaluation_renumber(moduleimpl_id, only_if_unumbered=False, redirect=0):
|
|
||||||
"""Renumber evaluations in this module, according to their date. (numero=0: oldest one)
|
|
||||||
Needed because previous versions of ScoDoc did not have eval numeros
|
|
||||||
Note: existing numeros are ignored
|
|
||||||
"""
|
|
||||||
redirect = int(redirect)
|
|
||||||
# log('moduleimpl_evaluation_renumber( moduleimpl_id=%s )' % moduleimpl_id )
|
|
||||||
# List sorted according to date/heure, ignoring numeros:
|
|
||||||
# (note that we place evaluations with NULL date at the end)
|
|
||||||
mod_evals = do_evaluation_list(
|
|
||||||
args={"moduleimpl_id": moduleimpl_id},
|
|
||||||
sortkey="jour asc, heure_debut asc",
|
|
||||||
)
|
|
||||||
|
|
||||||
all_numbered = False not in [x["numero"] > 0 for x in mod_evals]
|
|
||||||
if all_numbered and only_if_unumbered:
|
|
||||||
return # all ok
|
|
||||||
|
|
||||||
# Reset all numeros:
|
|
||||||
i = 1
|
|
||||||
for e in mod_evals:
|
|
||||||
e["numero"] = i
|
|
||||||
do_evaluation_edit(e)
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
# If requested, redirect to moduleimpl page:
|
|
||||||
if redirect:
|
|
||||||
return flask.redirect(
|
|
||||||
url_for(
|
|
||||||
"notes.moduleimpl_status",
|
|
||||||
scodoc_dept=g.scodoc_dept,
|
|
||||||
moduleimpl_id=moduleimpl_id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def moduleimpl_evaluation_insert_before(mod_evals, next_eval):
|
|
||||||
"""Renumber evals such that an evaluation with can be inserted before next_eval
|
|
||||||
Returns numero suitable for the inserted evaluation
|
|
||||||
"""
|
|
||||||
if next_eval:
|
|
||||||
n = next_eval["numero"]
|
|
||||||
if not n:
|
|
||||||
log("renumbering old evals")
|
|
||||||
moduleimpl_evaluation_renumber(next_eval["moduleimpl_id"])
|
|
||||||
next_eval = do_evaluation_list(
|
|
||||||
args={"evaluation_id": next_eval["evaluation_id"]}
|
|
||||||
)[0]
|
|
||||||
n = next_eval["numero"]
|
|
||||||
else:
|
|
||||||
n = 1
|
|
||||||
# log('inserting at position numero %s' % n )
|
|
||||||
# all numeros >= n are incremented
|
|
||||||
for e in mod_evals:
|
|
||||||
if e["numero"] >= n:
|
|
||||||
e["numero"] += 1
|
|
||||||
# log('incrementing %s to %s' % (e['evaluation_id'], e['numero']))
|
|
||||||
do_evaluation_edit(e)
|
|
||||||
|
|
||||||
return n
|
|
||||||
|
|
||||||
|
|
||||||
def moduleimpl_evaluation_move(evaluation_id: int, after=0, redirect=1):
|
def moduleimpl_evaluation_move(evaluation_id: int, after=0, redirect=1):
|
||||||
"""Move before/after previous one (decrement/increment numero)
|
"""Move before/after previous one (decrement/increment numero)
|
||||||
(published)
|
(published)
|
||||||
@ -357,18 +202,19 @@ def moduleimpl_evaluation_move(evaluation_id: int, after=0, redirect=1):
|
|||||||
moduleimpl_id = evaluation.moduleimpl_id
|
moduleimpl_id = evaluation.moduleimpl_id
|
||||||
redirect = int(redirect)
|
redirect = int(redirect)
|
||||||
# access: can change eval ?
|
# access: can change eval ?
|
||||||
if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id):
|
if not evaluation.moduleimpl.can_edit_evaluation(current_user):
|
||||||
raise AccessDenied(
|
raise AccessDenied(
|
||||||
f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
|
f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
|
||||||
)
|
)
|
||||||
|
Evaluation.moduleimpl_evaluation_renumber(
|
||||||
moduleimpl_evaluation_renumber(moduleimpl_id, only_if_unumbered=True)
|
evaluation.moduleimpl, only_if_unumbered=True
|
||||||
e = do_evaluation_list(args={"evaluation_id": evaluation_id})[0]
|
)
|
||||||
|
e = get_evaluation_dict(args={"evaluation_id": evaluation_id})[0]
|
||||||
|
|
||||||
after = int(after) # 0: deplace avant, 1 deplace apres
|
after = int(after) # 0: deplace avant, 1 deplace apres
|
||||||
if after not in (0, 1):
|
if after not in (0, 1):
|
||||||
raise ValueError('invalid value for "after"')
|
raise ValueError('invalid value for "after"')
|
||||||
mod_evals = do_evaluation_list({"moduleimpl_id": e["moduleimpl_id"]})
|
mod_evals = get_evaluation_dict({"moduleimpl_id": e["moduleimpl_id"]})
|
||||||
if len(mod_evals) > 1:
|
if len(mod_evals) > 1:
|
||||||
idx = [p["evaluation_id"] for p in mod_evals].index(evaluation_id)
|
idx = [p["evaluation_id"] for p in mod_evals].index(evaluation_id)
|
||||||
neigh = None # object to swap with
|
neigh = None # object to swap with
|
||||||
@ -379,8 +225,8 @@ def moduleimpl_evaluation_move(evaluation_id: int, after=0, redirect=1):
|
|||||||
if neigh: #
|
if neigh: #
|
||||||
if neigh["numero"] == e["numero"]:
|
if neigh["numero"] == e["numero"]:
|
||||||
log("Warning: moduleimpl_evaluation_move: forcing renumber")
|
log("Warning: moduleimpl_evaluation_move: forcing renumber")
|
||||||
moduleimpl_evaluation_renumber(
|
Evaluation.moduleimpl_evaluation_renumber(
|
||||||
e["moduleimpl_id"], only_if_unumbered=False
|
evaluation.moduleimpl, only_if_unumbered=False
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# swap numero with neighbor
|
# swap numero with neighbor
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
|
|
||||||
"""Formulaire ajout/édition d'une évaluation
|
"""Formulaire ajout/édition d'une évaluation
|
||||||
"""
|
"""
|
||||||
|
import datetime
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
@ -38,6 +38,7 @@ from flask import request
|
|||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import Evaluation, FormSemestre, ModuleImpl
|
from app.models import Evaluation, FormSemestre, ModuleImpl
|
||||||
|
from app.models.evaluations import heure_to_time
|
||||||
|
|
||||||
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
|
||||||
@ -47,7 +48,6 @@ from app.scodoc import html_sco_header
|
|||||||
from app.scodoc import sco_evaluations
|
from app.scodoc import sco_evaluations
|
||||||
from app.scodoc import sco_evaluation_db
|
from app.scodoc import sco_evaluation_db
|
||||||
from app.scodoc import sco_moduleimpl
|
from app.scodoc import sco_moduleimpl
|
||||||
from app.scodoc import sco_permissions_check
|
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
|
|
||||||
|
|
||||||
@ -83,7 +83,7 @@ def evaluation_create_form(
|
|||||||
can_edit_poids = not preferences["but_disable_edit_poids_evaluations"]
|
can_edit_poids = not preferences["but_disable_edit_poids_evaluations"]
|
||||||
min_note_max = scu.NOTES_PRECISION # le plus petit bareme possible
|
min_note_max = scu.NOTES_PRECISION # le plus petit bareme possible
|
||||||
#
|
#
|
||||||
if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id):
|
if not modimpl.can_edit_evaluation(current_user):
|
||||||
return f"""
|
return f"""
|
||||||
{html_sco_header.sco_header()}
|
{html_sco_header.sco_header()}
|
||||||
<h2>Opération non autorisée</h2>
|
<h2>Opération non autorisée</h2>
|
||||||
@ -139,15 +139,8 @@ def evaluation_create_form(
|
|||||||
|
|
||||||
heures = [f"{h:02d}h{m:02d}" for h in range(8, 19) for m in (0, 30)]
|
heures = [f"{h:02d}h{m:02d}" for h in range(8, 19) for m in (0, 30)]
|
||||||
#
|
#
|
||||||
initvalues["visibulletin"] = initvalues.get("visibulletin", True)
|
|
||||||
if initvalues["visibulletin"]:
|
|
||||||
initvalues["visibulletinlist"] = ["X"]
|
|
||||||
else:
|
|
||||||
initvalues["visibulletinlist"] = []
|
|
||||||
initvalues["coefficient"] = initvalues.get("coefficient", 1.0)
|
initvalues["coefficient"] = initvalues.get("coefficient", 1.0)
|
||||||
vals = scu.get_request_args()
|
vals = scu.get_request_args()
|
||||||
if vals.get("tf_submitted", False) and "visibulletinlist" not in vals:
|
|
||||||
vals["visibulletinlist"] = []
|
|
||||||
#
|
#
|
||||||
ue_coef_dict = modimpl.module.get_ue_coef_dict()
|
ue_coef_dict = modimpl.module.get_ue_coef_dict()
|
||||||
if is_apc: # BUT: poids vers les UE
|
if is_apc: # BUT: poids vers les UE
|
||||||
@ -236,11 +229,9 @@ def evaluation_create_form(
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"visibulletinlist",
|
"visibulletin",
|
||||||
{
|
{
|
||||||
"input_type": "checkbox",
|
"input_type": "boolcheckbox",
|
||||||
"allowed_values": ["X"],
|
|
||||||
"labels": [""],
|
|
||||||
"title": "Visible sur bulletins",
|
"title": "Visible sur bulletins",
|
||||||
"explanation": "(pour les bulletins en version intermédiaire)",
|
"explanation": "(pour les bulletins en version intermédiaire)",
|
||||||
},
|
},
|
||||||
@ -349,15 +340,41 @@ def evaluation_create_form(
|
|||||||
return flask.redirect(dest_url)
|
return flask.redirect(dest_url)
|
||||||
else:
|
else:
|
||||||
# form submission
|
# form submission
|
||||||
if tf[2]["visibulletinlist"]:
|
args = tf[2]
|
||||||
tf[2]["visibulletin"] = True
|
# modifie le codage des dates
|
||||||
|
# (nb: ce formulaire ne permet de créer que des évaluation sur la même journée)
|
||||||
|
if args.get("jour"):
|
||||||
|
try:
|
||||||
|
date_debut = datetime.datetime.strptime(args["jour"], "%d/%m/%Y")
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ScoValueError("Date (j/m/a) invalide") from exc
|
||||||
else:
|
else:
|
||||||
tf[2]["visibulletin"] = False
|
date_debut = None
|
||||||
|
args.pop("jour", None)
|
||||||
|
if date_debut and args.get("heure_debut"):
|
||||||
|
try:
|
||||||
|
heure_debut = heure_to_time(args["heure_debut"])
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ScoValueError("Heure début invalide") from exc
|
||||||
|
args["date_debut"] = datetime.datetime.combine(date_debut, heure_debut)
|
||||||
|
args.pop("heure_debut", None)
|
||||||
|
# note: ce formulaire ne permet de créer que des évaluation avec debut et fin sur le même jour.
|
||||||
|
if date_debut and args.get("heure_fin"):
|
||||||
|
try:
|
||||||
|
heure_fin = heure_to_time(args["heure_fin"])
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ScoValueError("Heure fin invalide") from exc
|
||||||
|
args["date_fin"] = datetime.datetime.combine(date_debut, heure_fin)
|
||||||
|
args.pop("heure_fin", None)
|
||||||
|
#
|
||||||
if edit:
|
if edit:
|
||||||
sco_evaluation_db.do_evaluation_edit(tf[2])
|
evaluation.from_dict(args)
|
||||||
else:
|
else:
|
||||||
# création d'une evaluation (via fonction ScoDoc7)
|
# création d'une evaluation
|
||||||
evaluation_id = sco_evaluation_db.do_evaluation_create(**tf[2])
|
evaluation = Evaluation.create(moduleimpl=modimpl, **args)
|
||||||
|
db.session.add(evaluation)
|
||||||
|
db.session.commit()
|
||||||
|
evaluation_id = evaluation.id
|
||||||
if is_apc:
|
if is_apc:
|
||||||
# Set poids
|
# Set poids
|
||||||
evaluation = db.session.get(Evaluation, evaluation_id)
|
evaluation = db.session.get(Evaluation, evaluation_id)
|
||||||
|
@ -126,8 +126,8 @@ def evaluations_recap_table(formsemestre: FormSemestre) -> list[dict]:
|
|||||||
evaluation_id=evaluation_id,
|
evaluation_id=evaluation_id,
|
||||||
),
|
),
|
||||||
"_titre_target_attrs": 'class="discretelink"',
|
"_titre_target_attrs": 'class="discretelink"',
|
||||||
"date": e.jour.strftime("%d/%m/%Y") if e.jour else "",
|
"date": e.date_debut.strftime("%d/%m/%Y") if e.date_debut else "",
|
||||||
"_date_order": e.jour.isoformat() if e.jour else "",
|
"_date_order": e.date_debut.isoformat() if e.date_debut else "",
|
||||||
"complete": "oui" if eval_etat.is_complete else "non",
|
"complete": "oui" if eval_etat.is_complete else "non",
|
||||||
"_complete_target": "#",
|
"_complete_target": "#",
|
||||||
"_complete_target_attrs": 'class="bull_link" title="prise en compte dans les moyennes"'
|
"_complete_target_attrs": 'class="bull_link" title="prise en compte dans les moyennes"'
|
||||||
|
@ -40,15 +40,14 @@ from flask import request
|
|||||||
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
|
||||||
from app.models import ScolarNews
|
|
||||||
|
|
||||||
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
|
||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
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_cal
|
||||||
from app.scodoc import sco_evaluation_db
|
from app.scodoc import sco_evaluation_db
|
||||||
from app.scodoc import sco_abs
|
|
||||||
from app.scodoc import sco_edit_module
|
from app.scodoc import sco_edit_module
|
||||||
from app.scodoc import sco_edit_ue
|
from app.scodoc import sco_edit_ue
|
||||||
from app.scodoc import sco_formsemestre_inscriptions
|
from app.scodoc import sco_formsemestre_inscriptions
|
||||||
@ -102,6 +101,14 @@ def do_evaluation_etat(
|
|||||||
) -> dict:
|
) -> dict:
|
||||||
"""Donne infos sur l'état de l'évaluation.
|
"""Donne infos sur l'état de l'évaluation.
|
||||||
Ancienne fonction, lente: préférer ModuleImplResults pour tout calcul.
|
Ancienne fonction, lente: préférer ModuleImplResults pour tout calcul.
|
||||||
|
XXX utilisée par de très nombreuses fonctions, dont
|
||||||
|
- _eval_etat<do_evaluation_etat_in_sem (en cours de remplacement)
|
||||||
|
|
||||||
|
- _eval_etat<do_evaluation_etat_in_mod<formsemestre_tableau_modules
|
||||||
|
qui a seulement besoin de
|
||||||
|
nb_evals_completes, nb_evals_en_cours, nb_evals_vides, attente
|
||||||
|
|
||||||
|
renvoie:
|
||||||
{
|
{
|
||||||
nb_inscrits : inscrits au module
|
nb_inscrits : inscrits au module
|
||||||
nb_notes
|
nb_notes
|
||||||
@ -125,7 +132,7 @@ def do_evaluation_etat(
|
|||||||
) # { etudid : note }
|
) # { etudid : note }
|
||||||
|
|
||||||
# ---- Liste des groupes complets et incomplets
|
# ---- Liste des groupes complets et incomplets
|
||||||
E = sco_evaluation_db.do_evaluation_list(args={"evaluation_id": evaluation_id})[0]
|
E = sco_evaluation_db.get_evaluation_dict(args={"evaluation_id": evaluation_id})[0]
|
||||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
|
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
|
||||||
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
||||||
is_malus = Mod["module_type"] == ModuleType.MALUS # True si module de malus
|
is_malus = Mod["module_type"] == ModuleType.MALUS # True si module de malus
|
||||||
@ -217,19 +224,19 @@ def do_evaluation_etat(
|
|||||||
(TotalNbMissing > 0)
|
(TotalNbMissing > 0)
|
||||||
and (E["evaluation_type"] != scu.EVALUATION_RATTRAPAGE)
|
and (E["evaluation_type"] != scu.EVALUATION_RATTRAPAGE)
|
||||||
and (E["evaluation_type"] != scu.EVALUATION_SESSION2)
|
and (E["evaluation_type"] != scu.EVALUATION_SESSION2)
|
||||||
and not is_malus
|
|
||||||
):
|
):
|
||||||
complete = False
|
complete = False
|
||||||
else:
|
else:
|
||||||
complete = True
|
complete = True
|
||||||
if (
|
|
||||||
TotalNbMissing > 0
|
complete = (
|
||||||
and ((TotalNbMissing == TotalNbAtt) or E["publish_incomplete"])
|
(TotalNbMissing == 0)
|
||||||
and not is_malus
|
or (E["evaluation_type"] == scu.EVALUATION_RATTRAPAGE)
|
||||||
):
|
or (E["evaluation_type"] == scu.EVALUATION_SESSION2)
|
||||||
evalattente = True
|
)
|
||||||
else:
|
evalattente = (TotalNbMissing > 0) and (
|
||||||
evalattente = False
|
(TotalNbMissing == TotalNbAtt) or E["publish_incomplete"]
|
||||||
|
)
|
||||||
# mais ne met pas en attente les evals immediates sans aucune notes:
|
# mais ne met pas en attente les evals immediates sans aucune notes:
|
||||||
if E["publish_incomplete"] and nb_notes == 0:
|
if E["publish_incomplete"] and nb_notes == 0:
|
||||||
evalattente = False
|
evalattente = False
|
||||||
@ -276,7 +283,8 @@ def do_evaluation_etat(
|
|||||||
|
|
||||||
|
|
||||||
def do_evaluation_list_in_sem(formsemestre_id, with_etat=True):
|
def do_evaluation_list_in_sem(formsemestre_id, with_etat=True):
|
||||||
"""Liste les evaluations de tous les modules de ce semestre.
|
"""Liste les évaluations de tous les modules de ce semestre.
|
||||||
|
Triée par module, numero desc, date_debut desc
|
||||||
Donne pour chaque eval son état (voir do_evaluation_etat)
|
Donne pour chaque eval son état (voir do_evaluation_etat)
|
||||||
{ evaluation_id,nb_inscrits, nb_notes, nb_abs, nb_neutre, moy, median, last_modif ... }
|
{ evaluation_id,nb_inscrits, nb_notes, nb_abs, nb_neutre, moy, median, last_modif ... }
|
||||||
|
|
||||||
@ -316,7 +324,7 @@ def do_evaluation_list_in_sem(formsemestre_id, with_etat=True):
|
|||||||
'evaluation_type': 0,
|
'evaluation_type': 0,
|
||||||
'heure_debut': datetime.time(8, 0),
|
'heure_debut': datetime.time(8, 0),
|
||||||
'heure_fin': datetime.time(9, 30),
|
'heure_fin': datetime.time(9, 30),
|
||||||
'jour': datetime.date(2015, 11, 3), // vide => 1/1/1
|
'jour': datetime.date(2015, 11, 3), // vide => 1/1/1900
|
||||||
'moduleimpl_id': 'GEAMIP80490',
|
'moduleimpl_id': 'GEAMIP80490',
|
||||||
'note_max': 20.0,
|
'note_max': 20.0,
|
||||||
'numero': 0,
|
'numero': 0,
|
||||||
@ -328,7 +336,7 @@ def do_evaluation_list_in_sem(formsemestre_id, with_etat=True):
|
|||||||
FROM notes_evaluation E, notes_moduleimpl MI
|
FROM notes_evaluation E, notes_moduleimpl MI
|
||||||
WHERE MI.formsemestre_id = %(formsemestre_id)s
|
WHERE MI.formsemestre_id = %(formsemestre_id)s
|
||||||
and MI.id = E.moduleimpl_id
|
and MI.id = E.moduleimpl_id
|
||||||
ORDER BY MI.id, numero desc, jour desc, heure_debut DESC
|
ORDER BY MI.id, numero desc, date_debut desc
|
||||||
"""
|
"""
|
||||||
cnx = ndb.GetDBConnexion()
|
cnx = ndb.GetDBConnexion()
|
||||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||||
@ -336,9 +344,9 @@ def do_evaluation_list_in_sem(formsemestre_id, with_etat=True):
|
|||||||
res = cursor.dictfetchall()
|
res = cursor.dictfetchall()
|
||||||
# etat de chaque evaluation:
|
# etat de chaque evaluation:
|
||||||
for r in res:
|
for r in res:
|
||||||
r["jour"] = r["jour"] or datetime.date(1900, 1, 1) # pour les comparaisons
|
|
||||||
if with_etat:
|
if with_etat:
|
||||||
r["etat"] = do_evaluation_etat(r["evaluation_id"])
|
r["etat"] = do_evaluation_etat(r["evaluation_id"])
|
||||||
|
r["jour"] = r["date_debut"] or datetime.date(1900, 1, 1)
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@ -380,7 +388,20 @@ def _eval_etat(evals):
|
|||||||
|
|
||||||
def do_evaluation_etat_in_sem(formsemestre_id):
|
def do_evaluation_etat_in_sem(formsemestre_id):
|
||||||
"""-> nb_eval_completes, nb_evals_en_cours, nb_evals_vides,
|
"""-> nb_eval_completes, nb_evals_en_cours, nb_evals_vides,
|
||||||
date derniere modif, attente"""
|
date derniere modif, attente
|
||||||
|
|
||||||
|
XXX utilisé par
|
||||||
|
- formsemestre_status_head
|
||||||
|
- gen_formsemestre_recapcomplet_xml
|
||||||
|
- gen_formsemestre_recapcomplet_json
|
||||||
|
|
||||||
|
"nb_evals_completes"
|
||||||
|
"nb_evals_en_cours"
|
||||||
|
"nb_evals_vides"
|
||||||
|
"date_derniere_note"
|
||||||
|
"last_modif"
|
||||||
|
"attente"
|
||||||
|
"""
|
||||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
evals = nt.get_evaluations_etats()
|
evals = nt.get_evaluations_etats()
|
||||||
@ -404,88 +425,97 @@ def formsemestre_evaluations_cal(formsemestre_id):
|
|||||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
|
|
||||||
evals = nt.get_evaluations_etats()
|
evaluations = formsemestre.get_evaluations() # TODO
|
||||||
nb_evals = len(evals)
|
nb_evals = len(evaluations)
|
||||||
|
|
||||||
color_incomplete = "#FF6060"
|
color_incomplete = "#FF6060"
|
||||||
color_complete = "#A0FFA0"
|
color_complete = "#A0FFA0"
|
||||||
color_futur = "#70E0FF"
|
color_futur = "#70E0FF"
|
||||||
|
|
||||||
today = time.strftime("%Y-%m-%d")
|
year = formsemestre.annee_scolaire()
|
||||||
|
|
||||||
year = formsemestre.date_debut.year
|
|
||||||
if formsemestre.date_debut.month < 8:
|
|
||||||
year -= 1 # calendrier septembre a septembre
|
|
||||||
events = {} # (day, halfday) : event
|
events = {} # (day, halfday) : event
|
||||||
for e in evals:
|
for e in evaluations:
|
||||||
etat = e["etat"]
|
if e.date_debut is None:
|
||||||
if not e["jour"]:
|
continue # éval. sans date
|
||||||
continue
|
txt = e.moduleimpl.module.code or e.moduleimpl.module.abbrev or "éval."
|
||||||
day = e["jour"].strftime("%Y-%m-%d")
|
if e.date_debut == e.date_fin:
|
||||||
mod = sco_moduleimpl.moduleimpl_withmodule_list(
|
heure_debut_txt, heure_fin_txt = "?", "?"
|
||||||
moduleimpl_id=e["moduleimpl_id"]
|
|
||||||
)[0]
|
|
||||||
txt = mod["module"]["code"] or mod["module"]["abbrev"] or "eval"
|
|
||||||
if e["heure_debut"]:
|
|
||||||
debut = e["heure_debut"].strftime("%Hh%M")
|
|
||||||
else:
|
else:
|
||||||
debut = "?"
|
heure_debut_txt = e.date_debut.strftime("%Hh%M") if e.date_debut else "?"
|
||||||
if e["heure_fin"]:
|
heure_fin_txt = e.date_fin.strftime("%Hh%M") if e.date_fin else "?"
|
||||||
fin = e["heure_fin"].strftime("%Hh%M")
|
|
||||||
else:
|
description = f"""{
|
||||||
fin = "?"
|
e.moduleimpl.module.titre
|
||||||
description = "%s, de %s à %s" % (mod["module"]["titre"], debut, fin)
|
}, de {heure_debut_txt} à {heure_fin_txt}"""
|
||||||
if etat["evalcomplete"]:
|
|
||||||
|
# Etat (notes completes) de l'évaluation:
|
||||||
|
modimpl_result = nt.modimpls_results[e.moduleimpl.id]
|
||||||
|
if modimpl_result.evaluations_etat[e.id].is_complete:
|
||||||
color = color_complete
|
color = color_complete
|
||||||
else:
|
else:
|
||||||
color = color_incomplete
|
color = color_incomplete
|
||||||
if day > today:
|
if e.date_debut > datetime.datetime.now(scu.TIME_ZONE):
|
||||||
color = color_futur
|
color = color_futur
|
||||||
href = "moduleimpl_status?moduleimpl_id=%s" % e["moduleimpl_id"]
|
href = url_for(
|
||||||
# if e['heure_debut'].hour < 12:
|
"notes.moduleimpl_status",
|
||||||
# halfday = True
|
scodoc_dept=g.scodoc_dept,
|
||||||
# else:
|
moduleimpl_id=e.moduleimpl_id,
|
||||||
# halfday = False
|
)
|
||||||
if not day in events:
|
day = e.date_debut.date().isoformat() # yyyy-mm-dd
|
||||||
# events[(day,halfday)] = [day, txt, color, href, halfday, description, mod]
|
event = events.get(day)
|
||||||
events[day] = [day, txt, color, href, description, mod]
|
if not event:
|
||||||
|
events[day] = [day, txt, color, href, description, e.moduleimpl]
|
||||||
else:
|
else:
|
||||||
e = events[day]
|
if event[-1].id != e.moduleimpl.id:
|
||||||
if e[-1]["moduleimpl_id"] != mod["moduleimpl_id"]:
|
|
||||||
# plusieurs evals de modules differents a la meme date
|
# plusieurs evals de modules differents a la meme date
|
||||||
e[1] += ", " + txt
|
event[1] += ", " + txt
|
||||||
e[4] += ", " + description
|
event[4] += ", " + description
|
||||||
if not etat["evalcomplete"]:
|
if color == color_incomplete:
|
||||||
e[2] = color_incomplete
|
event[2] = color_incomplete
|
||||||
if day > today:
|
if color == color_futur:
|
||||||
e[2] = color_futur
|
event[2] = color_futur
|
||||||
|
|
||||||
CalHTML = sco_abs.YearTable(
|
cal_html = sco_cal.YearTable(
|
||||||
year, events=list(events.values()), halfday=False, pad_width=None
|
year, events=list(events.values()), halfday=False, pad_width=None
|
||||||
)
|
)
|
||||||
|
|
||||||
H = [
|
return f"""
|
||||||
|
{
|
||||||
html_sco_header.html_sem_header(
|
html_sco_header.html_sem_header(
|
||||||
"Evaluations du semestre",
|
"Evaluations du semestre",
|
||||||
cssstyles=["css/calabs.css"],
|
cssstyles=["css/calabs.css"],
|
||||||
),
|
)
|
||||||
'<div class="cal_evaluations">',
|
}
|
||||||
CalHTML,
|
<div class="cal_evaluations">
|
||||||
"</div>",
|
{ cal_html }
|
||||||
"<p>soit %s évaluations planifiées;" % nb_evals,
|
</div>
|
||||||
"""<ul><li>en <span style="background-color: %s">rouge</span> les évaluations passées auxquelles il manque des notes</li>
|
<p>soit {nb_evals} évaluations planifiées;
|
||||||
<li>en <span style="background-color: %s">vert</span> les évaluations déjà notées</li>
|
</p>
|
||||||
<li>en <span style="background-color: %s">bleu</span> les évaluations futures</li></ul></p>"""
|
<ul>
|
||||||
% (color_incomplete, color_complete, color_futur),
|
<li>en <span style=
|
||||||
"""<p><a href="formsemestre_evaluations_delai_correction?formsemestre_id=%s" class="stdlink">voir les délais de correction</a></p>
|
"background-color: {color_incomplete}">rouge</span>
|
||||||
|
les évaluations passées auxquelles il manque des notes
|
||||||
|
</li>
|
||||||
|
<li>en <span style=
|
||||||
|
"background-color: {color_complete}">vert</span>
|
||||||
|
les évaluations déjà notées
|
||||||
|
</li>
|
||||||
|
<li>en <span style=
|
||||||
|
"background-color: {color_futur}">bleu</span>
|
||||||
|
les évaluations futures
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p><a href="{
|
||||||
|
url_for("notes.formsemestre_evaluations_delai_correction",
|
||||||
|
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id
|
||||||
|
)
|
||||||
|
}" class="stdlink">voir les délais de correction</a>
|
||||||
|
</p>
|
||||||
|
{ html_sco_header.sco_footer() }
|
||||||
"""
|
"""
|
||||||
% (formsemestre_id,),
|
|
||||||
html_sco_header.sco_footer(),
|
|
||||||
]
|
|
||||||
return "\n".join(H)
|
|
||||||
|
|
||||||
|
|
||||||
def evaluation_date_first_completion(evaluation_id):
|
def evaluation_date_first_completion(evaluation_id) -> datetime.datetime:
|
||||||
"""Première date à laquelle l'évaluation a été complète
|
"""Première date à laquelle l'évaluation a été complète
|
||||||
ou None si actuellement incomplète
|
ou None si actuellement incomplète
|
||||||
"""
|
"""
|
||||||
@ -497,7 +527,7 @@ def evaluation_date_first_completion(evaluation_id):
|
|||||||
# Il faut considerer les inscriptions au semestre
|
# Il faut considerer les inscriptions au semestre
|
||||||
# (pour avoir l'etat et le groupe) et aussi les inscriptions
|
# (pour avoir l'etat et le groupe) et aussi les inscriptions
|
||||||
# au module (pour gerer les modules optionnels correctement)
|
# au module (pour gerer les modules optionnels correctement)
|
||||||
# E = do_evaluation_list(args={"evaluation_id": evaluation_id})[0]
|
# E = get_evaluation_dict({"id":evaluation_id})[0]
|
||||||
# M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
|
# M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
|
||||||
# formsemestre_id = M["formsemestre_id"]
|
# formsemestre_id = M["formsemestre_id"]
|
||||||
# insem = sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits( formsemestre_id)
|
# insem = sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits( formsemestre_id)
|
||||||
@ -537,40 +567,44 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, format="html"):
|
|||||||
N'indique pas les évaluations de rattrapage ni celles des modules de bonus/malus.
|
N'indique pas les évaluations de rattrapage ni celles des modules de bonus/malus.
|
||||||
"""
|
"""
|
||||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
evaluations = formsemestre.get_evaluations()
|
||||||
|
rows = []
|
||||||
evals = nt.get_evaluations_etats()
|
for e in evaluations:
|
||||||
T = []
|
if (e.evaluation_type != scu.EVALUATION_NORMALE) or (
|
||||||
for e in evals:
|
e.moduleimpl.module.module_type == ModuleType.MALUS
|
||||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=e["moduleimpl_id"])[0]
|
|
||||||
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
|
||||||
if (e["evaluation_type"] != scu.EVALUATION_NORMALE) or (
|
|
||||||
Mod["module_type"] == ModuleType.MALUS
|
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
e["date_first_complete"] = evaluation_date_first_completion(e["evaluation_id"])
|
date_first_complete = evaluation_date_first_completion(e.id)
|
||||||
if e["date_first_complete"]:
|
if date_first_complete and e.date_fin:
|
||||||
e["delai_correction"] = (e["date_first_complete"].date() - e["jour"]).days
|
delai_correction = (date_first_complete.date() - e.date_fin).days
|
||||||
else:
|
else:
|
||||||
e["delai_correction"] = None
|
delai_correction = None
|
||||||
|
|
||||||
e["module_code"] = Mod["code"]
|
rows.append(
|
||||||
e["_module_code_target"] = url_for(
|
{
|
||||||
"notes.moduleimpl_status",
|
"date_first_complete": date_first_complete,
|
||||||
scodoc_dept=g.scodoc_dept,
|
"delai_correction": delai_correction,
|
||||||
moduleimpl_id=M["moduleimpl_id"],
|
"jour": e.date_debut.strftime("%d/%m/%Y")
|
||||||
)
|
if e.date_debut
|
||||||
e["module_titre"] = Mod["titre"]
|
else "sans date",
|
||||||
e["responsable_id"] = M["responsable_id"]
|
"_jour_target": url_for(
|
||||||
e["responsable_nomplogin"] = sco_users.user_info(M["responsable_id"])[
|
|
||||||
"nomplogin"
|
|
||||||
]
|
|
||||||
e["_jour_target"] = url_for(
|
|
||||||
"notes.evaluation_listenotes",
|
"notes.evaluation_listenotes",
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
evaluation_id=e["evaluation_id"],
|
evaluation_id=e["evaluation_id"],
|
||||||
|
),
|
||||||
|
"module_code": e.moduleimpl.module.code,
|
||||||
|
"_module_code_target": url_for(
|
||||||
|
"notes.moduleimpl_status",
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
moduleimpl_id=e.moduleimpl.id,
|
||||||
|
),
|
||||||
|
"module_titre": e.moduleimpl.module.abbrev or e.moduleimpl.module.titre,
|
||||||
|
"responsable_id": e.moduleimpl.responsable_id,
|
||||||
|
"responsable_nomplogin": sco_users.user_info(
|
||||||
|
e.moduleimpl.responsable_id
|
||||||
|
)["nomplogin"],
|
||||||
|
}
|
||||||
)
|
)
|
||||||
T.append(e)
|
|
||||||
|
|
||||||
columns_ids = (
|
columns_ids = (
|
||||||
"module_code",
|
"module_code",
|
||||||
@ -593,16 +627,14 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, format="html"):
|
|||||||
tab = GenTable(
|
tab = GenTable(
|
||||||
titles=titles,
|
titles=titles,
|
||||||
columns_ids=columns_ids,
|
columns_ids=columns_ids,
|
||||||
rows=T,
|
rows=rows,
|
||||||
html_class="table_leftalign table_coldate",
|
html_class="table_leftalign table_coldate",
|
||||||
html_sortable=True,
|
html_sortable=True,
|
||||||
html_title="<h2>Correction des évaluations du semestre</h2>",
|
html_title="<h2>Correction des évaluations du semestre</h2>",
|
||||||
caption="Correction des évaluations du semestre",
|
caption="Correction des évaluations du semestre",
|
||||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||||
base_url="%s?formsemestre_id=%s" % (request.base_url, formsemestre_id),
|
base_url="%s?formsemestre_id=%s" % (request.base_url, formsemestre_id),
|
||||||
origin="Généré par %s le " % sco_version.SCONAME
|
origin=f"""Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}""",
|
||||||
+ scu.timedate_human_repr()
|
|
||||||
+ "",
|
|
||||||
filename=scu.make_filename("evaluations_delais_" + formsemestre.titre_annee()),
|
filename=scu.make_filename("evaluations_delais_" + formsemestre.titre_annee()),
|
||||||
)
|
)
|
||||||
return tab.make_page(format=format)
|
return tab.make_page(format=format)
|
||||||
@ -613,7 +645,7 @@ def evaluation_describe(evaluation_id="", edit_in_place=True, link_saisie=True):
|
|||||||
"""HTML description of evaluation, for page headers
|
"""HTML description of evaluation, for page headers
|
||||||
edit_in_place: allow in-place editing when permitted (not implemented)
|
edit_in_place: allow in-place editing when permitted (not implemented)
|
||||||
"""
|
"""
|
||||||
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
|
E = sco_evaluation_db.get_evaluation_dict({"evaluation_id": evaluation_id})[0]
|
||||||
moduleimpl_id = E["moduleimpl_id"]
|
moduleimpl_id = E["moduleimpl_id"]
|
||||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
||||||
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
||||||
@ -668,10 +700,13 @@ def evaluation_describe(evaluation_id="", edit_in_place=True, link_saisie=True):
|
|||||||
group_id = sco_groups.get_default_group(formsemestre_id)
|
group_id = sco_groups.get_default_group(formsemestre_id)
|
||||||
H.append(
|
H.append(
|
||||||
f"""<span class="noprint"><a href="{url_for(
|
f"""<span class="noprint"><a href="{url_for(
|
||||||
'absences.EtatAbsencesDate',
|
'assiduites.get_etat_abs_date',
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
group_ids=group_id,
|
group_ids=group_id,
|
||||||
date=E["jour"]
|
desc=E["description"],
|
||||||
|
jour=E["jour"],
|
||||||
|
heure_debut=E["heure_debut"],
|
||||||
|
heure_fin=E["heure_fin"],
|
||||||
)
|
)
|
||||||
}">(absences ce jour)</a></span>"""
|
}">(absences ce jour)</a></span>"""
|
||||||
)
|
)
|
||||||
|
@ -512,8 +512,7 @@ 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)"
|
f"Evaluation {evaluation.descr_date()} (coef. {(evaluation.coefficient or 0.0):g})",
|
||||||
% (evaluation.jour or "sans date", evaluation.coefficient or 0.0),
|
|
||||||
style,
|
style,
|
||||||
)
|
)
|
||||||
# ligne blanche
|
# ligne blanche
|
||||||
|
@ -93,7 +93,7 @@ _formsemestreEditor = ndb.EditableTable(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_formsemestre(formsemestre_id: int):
|
def get_formsemestre(formsemestre_id: int) -> dict:
|
||||||
"list ONE formsemestre"
|
"list ONE formsemestre"
|
||||||
if formsemestre_id is None:
|
if formsemestre_id is None:
|
||||||
raise ValueError("get_formsemestre: id manquant")
|
raise ValueError("get_formsemestre: id manquant")
|
||||||
|
@ -29,7 +29,10 @@
|
|||||||
"""
|
"""
|
||||||
import flask
|
import flask
|
||||||
from flask import g, url_for, request
|
from flask import g, url_for, request
|
||||||
|
from flask_login import current_user
|
||||||
|
|
||||||
|
from app.models.config import ScoDocSiteConfig, PersonalizedLink
|
||||||
|
from app.models import FormSemestre
|
||||||
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.TrivialFormulator import TrivialFormulator
|
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||||
@ -58,6 +61,28 @@ def formsemestre_custommenu_get(formsemestre_id):
|
|||||||
return vals
|
return vals
|
||||||
|
|
||||||
|
|
||||||
|
def build_context_dict(formsemestre_id: int) -> dict:
|
||||||
|
"""returns a dict with "current" ids, to pass to external links"""
|
||||||
|
params = {
|
||||||
|
"dept": g.scodoc_dept,
|
||||||
|
"formsemestre_id": formsemestre_id,
|
||||||
|
"user_name": current_user.user_name,
|
||||||
|
}
|
||||||
|
cas_id = getattr(current_user, "cas_id", None)
|
||||||
|
if cas_id:
|
||||||
|
params["cas_id"] = cas_id
|
||||||
|
etudid = getattr(g, "current_etudid", None)
|
||||||
|
if etudid is not None:
|
||||||
|
params["etudid"] = etudid
|
||||||
|
evaluation_id = getattr(g, "current_evaluation_id", None)
|
||||||
|
if evaluation_id is not None:
|
||||||
|
params["evaluation_id"] = evaluation_id
|
||||||
|
moduleimpl_id = getattr(g, "current_moduleimpl_id", None)
|
||||||
|
if moduleimpl_id is not None:
|
||||||
|
params["moduleimpl_id"] = moduleimpl_id
|
||||||
|
return params
|
||||||
|
|
||||||
|
|
||||||
def formsemestre_custommenu_html(formsemestre_id):
|
def formsemestre_custommenu_html(formsemestre_id):
|
||||||
"HTML code for custom menu"
|
"HTML code for custom menu"
|
||||||
menu = []
|
menu = []
|
||||||
@ -66,6 +91,13 @@ def formsemestre_custommenu_html(formsemestre_id):
|
|||||||
ics_url = sco_edt_cal.formsemestre_get_ics_url(sem)
|
ics_url = sco_edt_cal.formsemestre_get_ics_url(sem)
|
||||||
if ics_url:
|
if ics_url:
|
||||||
menu.append({"title": "Emploi du temps (ics)", "url": ics_url})
|
menu.append({"title": "Emploi du temps (ics)", "url": ics_url})
|
||||||
|
# Liens globaux (config. générale)
|
||||||
|
params = build_context_dict(formsemestre_id)
|
||||||
|
for link in ScoDocSiteConfig.get_perso_links():
|
||||||
|
if link.title:
|
||||||
|
menu.append({"title": link.title, "url": link.get_url(params=params)})
|
||||||
|
|
||||||
|
# Liens propres à ce semestre
|
||||||
menu += formsemestre_custommenu_get(formsemestre_id)
|
menu += formsemestre_custommenu_get(formsemestre_id)
|
||||||
menu.append(
|
menu.append(
|
||||||
{
|
{
|
||||||
@ -79,14 +111,25 @@ def formsemestre_custommenu_html(formsemestre_id):
|
|||||||
|
|
||||||
def formsemestre_custommenu_edit(formsemestre_id):
|
def formsemestre_custommenu_edit(formsemestre_id):
|
||||||
"""Dialog to edit the custom menu"""
|
"""Dialog to edit the custom menu"""
|
||||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||||
dest_url = (
|
dest_url = url_for(
|
||||||
scu.NotesURL() + "/formsemestre_status?formsemestre_id=%s" % formsemestre_id
|
"notes.formsemestre_status",
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
formsemestre_id=formsemestre_id,
|
||||||
)
|
)
|
||||||
H = [
|
H = [
|
||||||
html_sco_header.html_sem_header("Modification du menu du semestre "),
|
html_sco_header.html_sem_header("Modification du menu du semestre "),
|
||||||
"""<p class="help">Ce menu, spécifique à chaque semestre, peut être utilisé pour placer des liens vers vos applications préférées.</p>
|
"""<div class="help">
|
||||||
<p class="help">Procédez en plusieurs fois si vous voulez ajouter plusieurs items.</p>""",
|
<p>Ce menu, spécifique à chaque semestre, peut être utilisé pour
|
||||||
|
placer des liens vers vos applications préférées.
|
||||||
|
</p>
|
||||||
|
<p>Les premiers liens du menus sont définis au niveau global (pour tous les
|
||||||
|
départements) et peuvent être modifiés par l'administrateur via la page
|
||||||
|
de configuration principale.
|
||||||
|
</p>
|
||||||
|
<p>Procédez en plusieurs fois si vous voulez ajouter plusieurs items.
|
||||||
|
</p>
|
||||||
|
""",
|
||||||
]
|
]
|
||||||
descr = [
|
descr = [
|
||||||
("formsemestre_id", {"input_type": "hidden"}),
|
("formsemestre_id", {"input_type": "hidden"}),
|
||||||
|
@ -1245,7 +1245,9 @@ def do_formsemestre_clone(
|
|||||||
moduleimpl_id=mod_orig["moduleimpl_id"]
|
moduleimpl_id=mod_orig["moduleimpl_id"]
|
||||||
):
|
):
|
||||||
# copie en enlevant la date
|
# copie en enlevant la date
|
||||||
new_eval = e.clone(not_copying=("jour", "moduleimpl_id"))
|
new_eval = e.clone(
|
||||||
|
not_copying=("date_debut", "date_fin", "moduleimpl_id")
|
||||||
|
)
|
||||||
new_eval.moduleimpl_id = mid
|
new_eval.moduleimpl_id = mid
|
||||||
# Copie les poids APC de l'évaluation
|
# Copie les poids APC de l'évaluation
|
||||||
new_eval.set_ue_poids_dict(e.get_ue_poids_dict())
|
new_eval.set_ue_poids_dict(e.get_ue_poids_dict())
|
||||||
@ -1443,7 +1445,7 @@ def do_formsemestre_delete(formsemestre_id):
|
|||||||
mods = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
|
mods = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
|
||||||
for mod in mods:
|
for mod in mods:
|
||||||
# evaluations
|
# evaluations
|
||||||
evals = sco_evaluation_db.do_evaluation_list(
|
evals = sco_evaluation_db.get_evaluation_dict(
|
||||||
args={"moduleimpl_id": mod["moduleimpl_id"]}
|
args={"moduleimpl_id": mod["moduleimpl_id"]}
|
||||||
)
|
)
|
||||||
for e in evals:
|
for e in evals:
|
||||||
|
@ -314,7 +314,7 @@ def do_formsemestre_inscription_with_modules(
|
|||||||
formsemestre_id=formsemestre_id,
|
formsemestre_id=formsemestre_id,
|
||||||
)
|
)
|
||||||
# Mise à jour des inscriptions aux parcours:
|
# Mise à jour des inscriptions aux parcours:
|
||||||
formsemestre.update_inscriptions_parcours_from_groups()
|
formsemestre.update_inscriptions_parcours_from_groups(etudid=etudid)
|
||||||
|
|
||||||
|
|
||||||
def formsemestre_inscription_with_modules_etud(
|
def formsemestre_inscription_with_modules_etud(
|
||||||
|
111
app/scodoc/sco_formsemestre_status.py
Normal file → Executable file
111
app/scodoc/sco_formsemestre_status.py
Normal file → Executable file
@ -50,6 +50,7 @@ from app.models import (
|
|||||||
ModuleImpl,
|
ModuleImpl,
|
||||||
NotesNotes,
|
NotesNotes,
|
||||||
)
|
)
|
||||||
|
from app.scodoc.codes_cursus import UE_SPORT
|
||||||
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
|
||||||
@ -60,7 +61,6 @@ from app.scodoc.sco_exceptions import (
|
|||||||
)
|
)
|
||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
from app.scodoc import htmlutils
|
from app.scodoc import htmlutils
|
||||||
from app.scodoc import sco_abs
|
|
||||||
from app.scodoc import sco_archives
|
from app.scodoc import sco_archives
|
||||||
from app.scodoc import sco_bulletins
|
from app.scodoc import sco_bulletins
|
||||||
from app.scodoc import codes_cursus
|
from app.scodoc import codes_cursus
|
||||||
@ -219,13 +219,14 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
|
|||||||
"enabled": True,
|
"enabled": True,
|
||||||
"helpmsg": "",
|
"helpmsg": "",
|
||||||
},
|
},
|
||||||
{
|
# TODO: Mettre à jour avec module Assiduités
|
||||||
"title": "Vérifier absences aux évaluations",
|
# {
|
||||||
"endpoint": "notes.formsemestre_check_absences_html",
|
# "title": "Vérifier absences aux évaluations",
|
||||||
"args": {"formsemestre_id": formsemestre_id},
|
# "endpoint": "notes.formsemestre_check_absences_html",
|
||||||
"enabled": True,
|
# "args": {"formsemestre_id": formsemestre_id},
|
||||||
"helpmsg": "",
|
# "enabled": True,
|
||||||
},
|
# "helpmsg": "",
|
||||||
|
# },
|
||||||
{
|
{
|
||||||
"title": "Lister tous les enseignants",
|
"title": "Lister tous les enseignants",
|
||||||
"endpoint": "notes.formsemestre_enseignants_list",
|
"endpoint": "notes.formsemestre_enseignants_list",
|
||||||
@ -497,7 +498,7 @@ def retreive_formsemestre_from_request() -> int:
|
|||||||
modimpl = modimpl[0]
|
modimpl = modimpl[0]
|
||||||
formsemestre_id = modimpl["formsemestre_id"]
|
formsemestre_id = modimpl["formsemestre_id"]
|
||||||
elif "evaluation_id" in args:
|
elif "evaluation_id" in args:
|
||||||
E = sco_evaluation_db.do_evaluation_list(
|
E = sco_evaluation_db.get_evaluation_dict(
|
||||||
{"evaluation_id": args["evaluation_id"]}
|
{"evaluation_id": args["evaluation_id"]}
|
||||||
)
|
)
|
||||||
if not E:
|
if not E:
|
||||||
@ -619,7 +620,7 @@ def formsemestre_description_table(
|
|||||||
columns_ids += ["Inscrits", "Responsable", "Enseignants"]
|
columns_ids += ["Inscrits", "Responsable", "Enseignants"]
|
||||||
if with_evals:
|
if with_evals:
|
||||||
columns_ids += [
|
columns_ids += [
|
||||||
"jour",
|
"date_evaluation",
|
||||||
"description",
|
"description",
|
||||||
"coefficient",
|
"coefficient",
|
||||||
"evalcomplete_str",
|
"evalcomplete_str",
|
||||||
@ -629,7 +630,7 @@ def formsemestre_description_table(
|
|||||||
titles = {title: title for title in columns_ids}
|
titles = {title: title for title in columns_ids}
|
||||||
titles.update({f"ue_{ue.id}": ue.acronyme for ue in ues})
|
titles.update({f"ue_{ue.id}": ue.acronyme for ue in ues})
|
||||||
titles["ects"] = "ECTS"
|
titles["ects"] = "ECTS"
|
||||||
titles["jour"] = "Évaluation"
|
titles["date_evaluation"] = "Évaluation"
|
||||||
titles["description"] = ""
|
titles["description"] = ""
|
||||||
titles["coefficient"] = "Coef. éval."
|
titles["coefficient"] = "Coef. éval."
|
||||||
titles["evalcomplete_str"] = "Complète"
|
titles["evalcomplete_str"] = "Complète"
|
||||||
@ -659,9 +660,11 @@ def formsemestre_description_table(
|
|||||||
"Module": ue.titre,
|
"Module": ue.titre,
|
||||||
"_css_row_class": "table_row_ue",
|
"_css_row_class": "table_row_ue",
|
||||||
}
|
}
|
||||||
if use_ue_coefs:
|
if use_ue_coefs and ue.type != UE_SPORT:
|
||||||
ue_info["Coef."] = ue.coefficient
|
ue_info["Coef."] = ue.coefficient or "0."
|
||||||
ue_info["Coef._class"] = "ue_coef"
|
ue_info["_Coef._class"] = "ue_coef"
|
||||||
|
if not ue.coefficient:
|
||||||
|
ue_info["_Coef._class"] += " ue_coef_nul"
|
||||||
if ue.color:
|
if ue.color:
|
||||||
for k in list(ue_info.keys()):
|
for k in list(ue_info.keys()):
|
||||||
if not k.startswith("_"):
|
if not k.startswith("_"):
|
||||||
@ -737,8 +740,10 @@ def formsemestre_description_table(
|
|||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
evaluation_id=e["evaluation_id"],
|
evaluation_id=e["evaluation_id"],
|
||||||
)
|
)
|
||||||
e["_jour_order"] = e["jour"].isoformat()
|
e["_date_evaluation_order"] = e["jour"].isoformat()
|
||||||
e["jour"] = e["jour"].strftime("%d/%m/%Y") if e["jour"] else ""
|
e["date_evaluation"] = (
|
||||||
|
e["jour"].strftime("%d/%m/%Y") if e["jour"] else ""
|
||||||
|
)
|
||||||
e["UE"] = row["UE"]
|
e["UE"] = row["UE"]
|
||||||
e["_UE_td_attrs"] = row["_UE_td_attrs"]
|
e["_UE_td_attrs"] = row["_UE_td_attrs"]
|
||||||
e["Code"] = row["Code"]
|
e["Code"] = row["Code"]
|
||||||
@ -837,40 +842,33 @@ def _make_listes_sem(formsemestre: FormSemestre, with_absences=True):
|
|||||||
weekday = datetime.datetime.today().weekday()
|
weekday = datetime.datetime.today().weekday()
|
||||||
try:
|
try:
|
||||||
if with_absences:
|
if with_absences:
|
||||||
first_monday = sco_abs.ddmmyyyy(
|
|
||||||
formsemestre.date_debut.strftime("%d/%m/%Y")
|
|
||||||
).prev_monday()
|
|
||||||
form_abs_tmpl = f"""
|
form_abs_tmpl = f"""
|
||||||
<td>
|
<td>
|
||||||
<a href="%(url_etat)s">absences</a>
|
<a class="btn" href="{
|
||||||
</td>
|
url_for("assiduites.visu_assi_group", scodoc_dept=g.scodoc_dept)
|
||||||
<td>
|
}?group_ids=%(group_id)s&date_debut={formsemestre.date_debut.isoformat()}&date_fin={formsemestre.date_fin.isoformat()}"><button>Visualiser l'assiduité</button></a>
|
||||||
<form action="{url_for(
|
|
||||||
"absences.SignaleAbsenceGrSemestre", scodoc_dept=g.scodoc_dept
|
|
||||||
)}" method="get">
|
|
||||||
<input type="hidden" name="datefin" value="{
|
|
||||||
formsemestre.date_fin.strftime("%d/%m/%Y")}"/>
|
|
||||||
<input type="hidden" name="group_ids" value="%(group_id)s"/>
|
|
||||||
<input type="hidden" name="destination" value="{destination}"/>
|
|
||||||
<input type="submit" value="Saisir abs des" />
|
|
||||||
<select name="datedebut" class="noprint">
|
|
||||||
"""
|
"""
|
||||||
date = first_monday
|
|
||||||
for idx, jour in enumerate(sco_abs.day_names()):
|
|
||||||
form_abs_tmpl += f"""<option value="{date}" {
|
|
||||||
'selected' if idx == weekday else ''
|
|
||||||
}>{jour}s</option>"""
|
|
||||||
date = date.next_day()
|
|
||||||
form_abs_tmpl += f"""
|
form_abs_tmpl += f"""
|
||||||
</select>
|
<a class="btn" href="{
|
||||||
|
url_for("assiduites.signal_assiduites_group", scodoc_dept=g.scodoc_dept)
|
||||||
<a href="{
|
}?group_ids=%(group_id)s&jour={
|
||||||
url_for("absences.choix_semaine", scodoc_dept=g.scodoc_dept)
|
datetime.date.today().isoformat()
|
||||||
}?group_id=%(group_id)s">saisie par semaine</a>
|
}&formsemestre_id={formsemestre.id}"><button>Saisie journalière</button></a>
|
||||||
</form></td>
|
<a class="btn" href="{
|
||||||
|
url_for("assiduites.signal_assiduites_diff", scodoc_dept=g.scodoc_dept)
|
||||||
|
}?group_ids=%(group_id)s&formsemestre_id={
|
||||||
|
formsemestre.formsemestre_id
|
||||||
|
}"><button>Saisie différée</button></a>
|
||||||
|
</td>
|
||||||
"""
|
"""
|
||||||
else:
|
else:
|
||||||
form_abs_tmpl = ""
|
form_abs_tmpl = f"""
|
||||||
|
<td>
|
||||||
|
<a class="btn" href="{
|
||||||
|
url_for("assiduites.visu_assiduites_group", scodoc_dept=g.scodoc_dept)
|
||||||
|
}?group_ids=%(group_id)s&jour={datetime.date.today().isoformat()}&formsemestre_id={formsemestre.id}"><button>Voir l'assiduité</button></a>
|
||||||
|
</td>
|
||||||
|
"""
|
||||||
except ScoInvalidDateError: # dates incorrectes dans semestres ?
|
except ScoInvalidDateError: # dates incorrectes dans semestres ?
|
||||||
form_abs_tmpl = ""
|
form_abs_tmpl = ""
|
||||||
#
|
#
|
||||||
@ -890,13 +888,14 @@ def _make_listes_sem(formsemestre: FormSemestre, with_absences=True):
|
|||||||
if n_members == 0:
|
if n_members == 0:
|
||||||
continue # skip empty groups
|
continue # skip empty groups
|
||||||
partition_is_empty = False
|
partition_is_empty = False
|
||||||
group["url_etat"] = url_for(
|
# XXX TODO-ASSIDUITE
|
||||||
"absences.EtatAbsencesGr",
|
group["url_etat"] = "non disponible" # url_for(
|
||||||
group_ids=group["group_id"],
|
# "absences.EtatAbsencesGr",
|
||||||
debut=formsemestre.date_debut.strftime("%d/%m/%Y"),
|
# group_ids=group["group_id"],
|
||||||
fin=formsemestre.date_fin.strftime("%d/%m/%Y"),
|
# debut=formsemestre.date_debut.strftime("%d/%m/%Y"),
|
||||||
scodoc_dept=g.scodoc_dept,
|
# fin=formsemestre.date_fin.strftime("%d/%m/%Y"),
|
||||||
)
|
# scodoc_dept=g.scodoc_dept,
|
||||||
|
# )
|
||||||
if group["group_name"]:
|
if group["group_name"]:
|
||||||
group["label"] = "groupe %(group_name)s" % group
|
group["label"] = "groupe %(group_name)s" % group
|
||||||
else:
|
else:
|
||||||
@ -917,7 +916,6 @@ def _make_listes_sem(formsemestre: FormSemestre, with_absences=True):
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
if with_absences:
|
|
||||||
H.append(form_abs_tmpl % group)
|
H.append(form_abs_tmpl % group)
|
||||||
|
|
||||||
H.append("</tr>")
|
H.append("</tr>")
|
||||||
@ -935,12 +933,11 @@ def _make_listes_sem(formsemestre: FormSemestre, with_absences=True):
|
|||||||
H.append("</p>")
|
H.append("</p>")
|
||||||
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"""<h4><a
|
f"""<h4><a class="stdlink"
|
||||||
href="{
|
href="{url_for("scolar.partition_editor",
|
||||||
url_for("scolar.edit_partition_form",
|
|
||||||
formsemestre_id=formsemestre.id,
|
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
)
|
formsemestre_id=formsemestre.id,
|
||||||
|
edit_partition=1)
|
||||||
}">Ajouter une partition</a></h4>"""
|
}">Ajouter une partition</a></h4>"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ from app.scodoc.codes_cursus import *
|
|||||||
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
|
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
|
||||||
|
|
||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
from app.scodoc import sco_abs
|
from app.scodoc import sco_assiduites
|
||||||
from app.scodoc import codes_cursus
|
from app.scodoc import codes_cursus
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
from app.scodoc import sco_edit_ue
|
from app.scodoc import sco_edit_ue
|
||||||
@ -704,7 +704,7 @@ def formsemestre_recap_parcours_table(
|
|||||||
f"""<td class="rcp_moy">{scu.fmt_note(nt.get_etud_moy_gen(etudid))}</td>"""
|
f"""<td class="rcp_moy">{scu.fmt_note(nt.get_etud_moy_gen(etudid))}</td>"""
|
||||||
)
|
)
|
||||||
# Absences (nb d'abs non just. dans ce semestre)
|
# Absences (nb d'abs non just. dans ce semestre)
|
||||||
nbabs, nbabsjust = sco_abs.get_abs_count(etudid, sem)
|
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem)
|
||||||
H.append(f"""<td class="rcp_abs">{nbabs - nbabsjust}</td>""")
|
H.append(f"""<td class="rcp_abs">{nbabs - nbabsjust}</td>""")
|
||||||
|
|
||||||
# UEs
|
# UEs
|
||||||
|
@ -684,7 +684,7 @@ def change_etud_group_in_partition(etudid: int, group: GroupDescr) -> bool:
|
|||||||
|
|
||||||
# - Update parcours
|
# - Update parcours
|
||||||
if group.partition.partition_name == scu.PARTITION_PARCOURS:
|
if group.partition.partition_name == scu.PARTITION_PARCOURS:
|
||||||
formsemestre.update_inscriptions_parcours_from_groups()
|
formsemestre.update_inscriptions_parcours_from_groups(etudid=etudid)
|
||||||
|
|
||||||
# - invalidate cache
|
# - invalidate cache
|
||||||
sco_cache.invalidate_formsemestre(
|
sco_cache.invalidate_formsemestre(
|
||||||
|
@ -33,7 +33,6 @@
|
|||||||
|
|
||||||
import collections
|
import collections
|
||||||
import datetime
|
import datetime
|
||||||
import operator
|
|
||||||
import urllib
|
import urllib
|
||||||
from urllib.parse import parse_qs
|
from urllib.parse import parse_qs
|
||||||
import time
|
import time
|
||||||
@ -42,9 +41,11 @@ import time
|
|||||||
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
|
||||||
|
from app.models import FormSemestre
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
from app.scodoc import sco_abs
|
from app.scodoc import sco_cal
|
||||||
from app.scodoc import sco_excel
|
from app.scodoc import sco_excel
|
||||||
from app.scodoc import sco_formsemestre
|
from app.scodoc import sco_formsemestre
|
||||||
from app.scodoc import sco_groups
|
from app.scodoc import sco_groups
|
||||||
@ -65,6 +66,7 @@ JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + [
|
|||||||
|
|
||||||
CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
|
CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
|
||||||
|
|
||||||
|
|
||||||
# view:
|
# view:
|
||||||
def groups_view(
|
def groups_view(
|
||||||
group_ids=(),
|
group_ids=(),
|
||||||
@ -285,7 +287,7 @@ if (group_id) {
|
|||||||
return "\n".join(H)
|
return "\n".join(H)
|
||||||
|
|
||||||
|
|
||||||
class DisplayedGroupsInfos(object):
|
class DisplayedGroupsInfos:
|
||||||
"""Container with attributes describing groups to display in the page
|
"""Container with attributes describing groups to display in the page
|
||||||
.groups_query_args : 'group_ids=xxx&group_ids=yyy'
|
.groups_query_args : 'group_ids=xxx&group_ids=yyy'
|
||||||
.base_url : url de la requete, avec les groupes, sans les autres paramètres
|
.base_url : url de la requete, avec les groupes, sans les autres paramètres
|
||||||
@ -346,7 +348,7 @@ class DisplayedGroupsInfos(object):
|
|||||||
self.tous_les_etuds_du_sem = (
|
self.tous_les_etuds_du_sem = (
|
||||||
False # affiche tous les etuds du semestre ? (si un seul semestre)
|
False # affiche tous les etuds du semestre ? (si un seul semestre)
|
||||||
)
|
)
|
||||||
self.sems = collections.OrderedDict() # formsemestre_id : sem
|
self.sems = {} # formsemestre_id : sem
|
||||||
self.formsemestre = None
|
self.formsemestre = None
|
||||||
self.formsemestre_id = formsemestre_id
|
self.formsemestre_id = formsemestre_id
|
||||||
self.nbdem = 0 # nombre d'étudiants démissionnaires en tout
|
self.nbdem = 0 # nombre d'étudiants démissionnaires en tout
|
||||||
@ -422,6 +424,13 @@ class DisplayedGroupsInfos(object):
|
|||||||
H.append(f'<input type="hidden" name="group_ids" value="{group_id}"/>')
|
H.append(f'<input type="hidden" name="group_ids" value="{group_id}"/>')
|
||||||
return "\n".join(H)
|
return "\n".join(H)
|
||||||
|
|
||||||
|
def get_formsemestre(self) -> FormSemestre:
|
||||||
|
return (
|
||||||
|
db.session.get(FormSemestre, self.formsemestre_id)
|
||||||
|
if self.formsemestre_id
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Ancien ZScolar.group_list renommé ici en group_table
|
# Ancien ZScolar.group_list renommé ici en group_table
|
||||||
def groups_table(
|
def groups_table(
|
||||||
@ -820,7 +829,8 @@ def tab_absences_html(groups_infos, etat=None):
|
|||||||
"<li>",
|
"<li>",
|
||||||
form_choix_jour_saisie_hebdo(groups_infos),
|
form_choix_jour_saisie_hebdo(groups_infos),
|
||||||
"</li>",
|
"</li>",
|
||||||
"""<li><a class="stdlink" href="Absences/EtatAbsencesGr?%s&debut=%s&fin=%s">État des absences du groupe</a></li>"""
|
# XXX TODO-ASSIDUITE
|
||||||
|
"""<li><a class="stdlink" href="Absences/EtatAbsencesGr?%s&debut=%s&fin=%s">XXX État des absences du groupe</a></li>"""
|
||||||
% (
|
% (
|
||||||
groups_infos.groups_query_args,
|
groups_infos.groups_query_args,
|
||||||
groups_infos.formsemestre["date_debut"],
|
groups_infos.formsemestre["date_debut"],
|
||||||
@ -881,12 +891,13 @@ def form_choix_jour_saisie_hebdo(groups_infos, moduleimpl_id=None):
|
|||||||
if not authuser.has_permission(Permission.ScoAbsChange):
|
if not authuser.has_permission(Permission.ScoAbsChange):
|
||||||
return ""
|
return ""
|
||||||
sem = groups_infos.formsemestre
|
sem = groups_infos.formsemestre
|
||||||
first_monday = sco_abs.ddmmyyyy(sem["date_debut"]).prev_monday()
|
first_monday = sco_cal.ddmmyyyy(sem["date_debut"]).prev_monday()
|
||||||
today_idx = datetime.date.today().weekday()
|
today_idx = datetime.date.today().weekday()
|
||||||
|
|
||||||
FA = [] # formulaire avec menu saisi absences
|
FA = [] # formulaire avec menu saisi absences
|
||||||
FA.append(
|
FA.append(
|
||||||
'<form id="form_choix_jour_saisie_hebdo" action="Absences/SignaleAbsenceGrSemestre" method="get">'
|
# TODO-ASSIDUITE et utiliser url_for... (was Absences/SignaleAbsenceGrSemestre)
|
||||||
|
'<form id="form_choix_jour_saisie_hebdo" action="XXX" method="get">'
|
||||||
)
|
)
|
||||||
FA.append('<input type="hidden" name="datefin" value="%(date_fin)s"/>' % sem)
|
FA.append('<input type="hidden" name="datefin" value="%(date_fin)s"/>' % sem)
|
||||||
FA.append(groups_infos.get_form_elem())
|
FA.append(groups_infos.get_form_elem())
|
||||||
@ -897,12 +908,12 @@ def form_choix_jour_saisie_hebdo(groups_infos, moduleimpl_id=None):
|
|||||||
FA.append('<input type="hidden" name="destination" value=""/>')
|
FA.append('<input type="hidden" name="destination" value=""/>')
|
||||||
|
|
||||||
FA.append(
|
FA.append(
|
||||||
"""<input type="button" onclick="$('#form_choix_jour_saisie_hebdo')[0].destination.value=get_current_url(); $('#form_choix_jour_saisie_hebdo').submit();" value="Saisir absences du "/>"""
|
"""<input type="button" onclick="$('#form_choix_jour_saisie_hebdo')[0].destination.value=get_current_url(); $('#form_choix_jour_saisie_hebdo').submit();" value="Saisir absences du (NON DISPONIBLE) "/>"""
|
||||||
)
|
)
|
||||||
FA.append("""<select name="datedebut">""")
|
FA.append("""<select name="datedebut">""")
|
||||||
date = first_monday
|
date = first_monday
|
||||||
i = 0
|
i = 0
|
||||||
for jour in sco_abs.day_names():
|
for jour in sco_cal.day_names():
|
||||||
if i == today_idx:
|
if i == today_idx:
|
||||||
sel = "selected"
|
sel = "selected"
|
||||||
else:
|
else:
|
||||||
@ -936,14 +947,17 @@ def form_choix_saisie_semaine(groups_infos):
|
|||||||
) # car ici utilisee dans un format string !
|
) # car ici utilisee dans un format string !
|
||||||
|
|
||||||
DateJour = time.strftime("%d/%m/%Y")
|
DateJour = time.strftime("%d/%m/%Y")
|
||||||
datelundi = sco_abs.ddmmyyyy(DateJour).prev_monday()
|
datelundi = sco_cal.ddmmyyyy(DateJour).prev_monday()
|
||||||
FA = [] # formulaire avec menu saisie hebdo des absences
|
FA = [] # formulaire avec menu saisie hebdo des absences
|
||||||
|
# XXX TODO-ASSIDUITE et utiliser un POST
|
||||||
FA.append('<form action="Absences/SignaleAbsenceGrHebdo" method="get">')
|
FA.append('<form action="Absences/SignaleAbsenceGrHebdo" method="get">')
|
||||||
FA.append('<input type="hidden" name="datelundi" value="%s"/>' % datelundi)
|
FA.append('<input type="hidden" name="datelundi" value="%s"/>' % datelundi)
|
||||||
FA.append('<input type="hidden" name="moduleimpl_id" value="%s"/>' % moduleimpl_id)
|
FA.append('<input type="hidden" name="moduleimpl_id" value="%s"/>' % moduleimpl_id)
|
||||||
FA.append('<input type="hidden" name="destination" value="%s"/>' % destination)
|
FA.append('<input type="hidden" name="destination" value="%s"/>' % destination)
|
||||||
FA.append(groups_infos.get_form_elem())
|
FA.append(groups_infos.get_form_elem())
|
||||||
FA.append('<input type="submit" class="button" value="Saisie à la semaine" />')
|
FA.append(
|
||||||
|
'<input type="submit" class="button" value="Saisie à la semaine (NON DISPONIBLE)" />'
|
||||||
|
) # XXX
|
||||||
FA.append("</form>")
|
FA.append("</form>")
|
||||||
return "\n".join(FA)
|
return "\n".join(FA)
|
||||||
|
|
||||||
|
@ -69,38 +69,44 @@ def do_evaluation_listenotes(
|
|||||||
mode = None
|
mode = None
|
||||||
if moduleimpl_id:
|
if moduleimpl_id:
|
||||||
mode = "module"
|
mode = "module"
|
||||||
evals = sco_evaluation_db.do_evaluation_list({"moduleimpl_id": moduleimpl_id})
|
evals = sco_evaluation_db.get_evaluation_dict({"moduleimpl_id": moduleimpl_id})
|
||||||
elif evaluation_id:
|
elif evaluation_id:
|
||||||
mode = "eval"
|
mode = "eval"
|
||||||
evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})
|
evals = sco_evaluation_db.get_evaluation_dict({"evaluation_id": evaluation_id})
|
||||||
else:
|
else:
|
||||||
raise ValueError("missing argument: evaluation or module")
|
raise ValueError("missing argument: evaluation or module")
|
||||||
if not evals:
|
if not evals:
|
||||||
return "<p>Aucune évaluation !</p>", "ScoDoc"
|
return "<p>Aucune évaluation !</p>", "ScoDoc"
|
||||||
|
|
||||||
E = evals[0] # il y a au moins une evaluation
|
eval_dict = evals[0] # il y a au moins une evaluation
|
||||||
modimpl = db.session.get(ModuleImpl, E["moduleimpl_id"])
|
modimpl = db.session.get(ModuleImpl, eval_dict["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)]
|
||||||
page_title = f"Notes {E['description'] or modimpl.module.code}"
|
page_title = f"Notes {eval_dict['description'] or modimpl.module.code}"
|
||||||
else:
|
else:
|
||||||
H = []
|
H = []
|
||||||
page_title = f"Notes {modimpl.module.code}"
|
page_title = f"Notes {modimpl.module.code}"
|
||||||
# groupes
|
# groupes
|
||||||
groups = sco_groups.do_evaluation_listegroupes(
|
groups = sco_groups.do_evaluation_listegroupes(
|
||||||
E["evaluation_id"], include_default=True
|
eval_dict["evaluation_id"], include_default=True
|
||||||
)
|
)
|
||||||
grlabs = [g["group_name"] or "tous" for g in groups] # legendes des boutons
|
grlabs = [g["group_name"] or "tous" for g in groups] # legendes des boutons
|
||||||
grnams = [str(g["group_id"]) for g in groups] # noms des checkbox
|
grnams = [str(g["group_id"]) for g in groups] # noms des checkbox
|
||||||
|
|
||||||
if len(evals) > 1:
|
if len(evals) > 1:
|
||||||
descr = [
|
descr = [
|
||||||
("moduleimpl_id", {"default": E["moduleimpl_id"], "input_type": "hidden"})
|
(
|
||||||
|
"moduleimpl_id",
|
||||||
|
{"default": eval_dict["moduleimpl_id"], "input_type": "hidden"},
|
||||||
|
)
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
descr = [
|
descr = [
|
||||||
("evaluation_id", {"default": E["evaluation_id"], "input_type": "hidden"})
|
(
|
||||||
|
"evaluation_id",
|
||||||
|
{"default": eval_dict["evaluation_id"], "input_type": "hidden"},
|
||||||
|
)
|
||||||
]
|
]
|
||||||
if len(grnams) > 1:
|
if len(grnams) > 1:
|
||||||
descr += [
|
descr += [
|
||||||
@ -199,7 +205,7 @@ def do_evaluation_listenotes(
|
|||||||
url_for(
|
url_for(
|
||||||
"notes.moduleimpl_status",
|
"notes.moduleimpl_status",
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
moduleimpl_id=E["moduleimpl_id"],
|
moduleimpl_id=eval_dict["moduleimpl_id"],
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
"",
|
"",
|
||||||
|
@ -230,7 +230,7 @@ def do_moduleimpl_inscription_list(moduleimpl_id=None, etudid=None):
|
|||||||
return _moduleimpl_inscriptionEditor.list(cnx, args)
|
return _moduleimpl_inscriptionEditor.list(cnx, args)
|
||||||
|
|
||||||
|
|
||||||
def moduleimpl_listeetuds(moduleimpl_id):
|
def moduleimpl_listeetuds(moduleimpl_id): # XXX OBSOLETE
|
||||||
"retourne liste des etudids inscrits a ce module"
|
"retourne liste des etudids inscrits a ce module"
|
||||||
req = """SELECT DISTINCT Im.etudid
|
req = """SELECT DISTINCT Im.etudid
|
||||||
FROM notes_moduleimpl_inscription Im,
|
FROM notes_moduleimpl_inscription Im,
|
||||||
|
@ -47,7 +47,7 @@ from app.scodoc.sco_permissions import Permission
|
|||||||
|
|
||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
from app.scodoc import htmlutils
|
from app.scodoc import htmlutils
|
||||||
from app.scodoc import sco_abs
|
from app.scodoc import sco_cal
|
||||||
from app.scodoc import sco_compute_moy
|
from app.scodoc import sco_compute_moy
|
||||||
from app.scodoc import sco_evaluations
|
from app.scodoc import sco_evaluations
|
||||||
from app.scodoc import sco_evaluation_db
|
from app.scodoc import sco_evaluation_db
|
||||||
@ -61,7 +61,7 @@ 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"
|
||||||
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
|
E = sco_evaluation_db.get_evaluation_dict({"evaluation_id": evaluation_id})[0]
|
||||||
modimpl = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
|
modimpl = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
|
||||||
|
|
||||||
group_id = sco_groups.get_default_group(modimpl["formsemestre_id"])
|
group_id = sco_groups.get_default_group(modimpl["formsemestre_id"])
|
||||||
@ -138,10 +138,13 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str:
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Absences ce jour",
|
"title": "Absences ce jour",
|
||||||
"endpoint": "absences.EtatAbsencesDate",
|
"endpoint": "assiduites.get_etat_abs_date",
|
||||||
"args": {
|
"args": {
|
||||||
"date": E["jour"],
|
|
||||||
"group_ids": group_id,
|
"group_ids": group_id,
|
||||||
|
"desc": E["description"],
|
||||||
|
"jour": E["jour"],
|
||||||
|
"heure_debut": E["heure_debut"],
|
||||||
|
"heure_fin": E["heure_fin"],
|
||||||
},
|
},
|
||||||
"enabled": E["jour"],
|
"enabled": E["jour"],
|
||||||
},
|
},
|
||||||
@ -191,6 +194,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||||||
if not isinstance(moduleimpl_id, int):
|
if not isinstance(moduleimpl_id, int):
|
||||||
raise ScoInvalidIdType("moduleimpl_id must be an integer !")
|
raise ScoInvalidIdType("moduleimpl_id must be an integer !")
|
||||||
modimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id)
|
modimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id)
|
||||||
|
g.current_moduleimpl_id = modimpl.id
|
||||||
module: Module = modimpl.module
|
module: Module = modimpl.module
|
||||||
formsemestre_id = modimpl.formsemestre_id
|
formsemestre_id = modimpl.formsemestre_id
|
||||||
formsemestre: FormSemestre = modimpl.formsemestre
|
formsemestre: FormSemestre = modimpl.formsemestre
|
||||||
@ -199,11 +203,10 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
# Evaluations, la plus RECENTE en tête
|
# Evaluations, par numéros ou la plus RECENTE en tête
|
||||||
evaluations = modimpl.evaluations.order_by(
|
evaluations = modimpl.evaluations.order_by(
|
||||||
Evaluation.numero.desc(),
|
Evaluation.numero.desc(),
|
||||||
Evaluation.jour.desc(),
|
Evaluation.date_debut.desc(),
|
||||||
Evaluation.heure_debut.desc(),
|
|
||||||
).all()
|
).all()
|
||||||
nb_evaluations = len(evaluations)
|
nb_evaluations = len(evaluations)
|
||||||
max_poids = max(
|
max_poids = max(
|
||||||
@ -329,10 +332,6 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||||||
'<tr><td colspan="4">'
|
'<tr><td colspan="4">'
|
||||||
# <em title="mode de calcul de la moyenne du module">règle de calcul standard</em>'
|
# <em title="mode de calcul de la moyenne du module">règle de calcul standard</em>'
|
||||||
)
|
)
|
||||||
# if sco_moduleimpl.can_change_ens(moduleimpl_id, raise_exc=False):
|
|
||||||
# H.append(
|
|
||||||
# f' (<a class="stdlink" href="edit_moduleimpl_expr?moduleimpl_id={moduleimpl_id}">changer</a>)'
|
|
||||||
# )
|
|
||||||
H.append("</td></tr>")
|
H.append("</td></tr>")
|
||||||
H.append(
|
H.append(
|
||||||
f"""<tr><td colspan="4"><span class="moduleimpl_abs_link"><a class="stdlink"
|
f"""<tr><td colspan="4"><span class="moduleimpl_abs_link"><a class="stdlink"
|
||||||
@ -346,17 +345,20 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||||||
current_user.has_permission(Permission.ScoAbsChange)
|
current_user.has_permission(Permission.ScoAbsChange)
|
||||||
and formsemestre.est_courant()
|
and formsemestre.est_courant()
|
||||||
):
|
):
|
||||||
datelundi = sco_abs.ddmmyyyy(time.strftime("%d/%m/%Y")).prev_monday()
|
datelundi = sco_cal.ddmmyyyy(time.strftime("%d/%m/%Y")).prev_monday()
|
||||||
group_id = sco_groups.get_default_group(formsemestre_id)
|
group_id = sco_groups.get_default_group(formsemestre_id)
|
||||||
H.append(
|
H.append(
|
||||||
f"""
|
f"""
|
||||||
<span class="moduleimpl_abs_link"><a class="stdlink"
|
<span class="moduleimpl_abs_link"><a class="stdlink" href="XXX"
|
||||||
href="{url_for("absences.SignaleAbsenceGrHebdo",
|
>Saisie Absences hebdo. (INDISPONIBLE)</a></span>
|
||||||
scodoc_dept=g.scodoc_dept,formsemestre_id=formsemestre_id,
|
|
||||||
moduleimpl_id=moduleimpl_id, datelundi=datelundi, group_ids=group_id)}">
|
|
||||||
Saisie Absences hebdo.</a></span>
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
# TODO-ASSIDUITE
|
||||||
|
# href="{
|
||||||
|
# url_for("absences.SignaleAbsenceGrHebdo",
|
||||||
|
# scodoc_dept=g.scodoc_dept,formsemestre_id=formsemestre_id,
|
||||||
|
# moduleimpl_id=moduleimpl_id, datelundi=datelundi, group_ids=group_id)
|
||||||
|
# }"
|
||||||
|
|
||||||
H.append("</td></tr></table>")
|
H.append("</td></tr></table>")
|
||||||
#
|
#
|
||||||
@ -431,8 +433,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||||||
top_table_links += f"""
|
top_table_links += f"""
|
||||||
<a class="stdlink" style="margin-left:2em;" href="{
|
<a class="stdlink" style="margin-left:2em;" href="{
|
||||||
url_for("notes.moduleimpl_evaluation_renumber",
|
url_for("notes.moduleimpl_evaluation_renumber",
|
||||||
scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id,
|
scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
|
||||||
redirect=1)
|
|
||||||
}">Trier par date</a>
|
}">Trier par date</a>
|
||||||
"""
|
"""
|
||||||
if nb_evaluations > 0:
|
if nb_evaluations > 0:
|
||||||
@ -560,15 +561,16 @@ def _ligne_evaluation(
|
|||||||
if modimpl.module.ue.type != UE_SPORT:
|
if modimpl.module.ue.type != UE_SPORT:
|
||||||
# Avertissement si coefs x poids nuls
|
# Avertissement si coefs x poids nuls
|
||||||
if coef < scu.NOTES_PRECISION:
|
if coef < scu.NOTES_PRECISION:
|
||||||
|
if modimpl.module.module_type == scu.ModuleType.MALUS:
|
||||||
|
H.append("""<span class="eval_warning_coef">malus</span>""")
|
||||||
|
else:
|
||||||
H.append("""<span class="eval_warning_coef">coef. nul !</span>""")
|
H.append("""<span class="eval_warning_coef">coef. nul !</span>""")
|
||||||
elif is_apc:
|
elif is_apc:
|
||||||
# visualisation des poids (Hinton map)
|
# visualisation des poids (Hinton map)
|
||||||
H.append(_evaluation_poids_html(evaluation, max_poids))
|
H.append(_evaluation_poids_html(evaluation, max_poids))
|
||||||
H.append("""<div class="evaluation_titre">""")
|
H.append("""<div class="evaluation_titre">""")
|
||||||
if evaluation.jour:
|
if evaluation.date_debut:
|
||||||
H.append(
|
H.append(evaluation.descr_date())
|
||||||
f"""Le {evaluation.jour.strftime("%d/%m/%Y")} {evaluation.descr_heure()}"""
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
H.append(
|
H.append(
|
||||||
f"""<a href="{url_for("notes.evaluation_edit",
|
f"""<a href="{url_for("notes.evaluation_edit",
|
||||||
|
@ -57,6 +57,8 @@ _SCO_PERMISSIONS = (
|
|||||||
(1 << 29, "ScoUsersChangeCASId", "Paramétrer l'id CAS"),
|
(1 << 29, "ScoUsersChangeCASId", "Paramétrer l'id CAS"),
|
||||||
#
|
#
|
||||||
(1 << 40, "ScoEtudChangePhoto", "Modifier la photo d'un étudiant"),
|
(1 << 40, "ScoEtudChangePhoto", "Modifier la photo d'un étudiant"),
|
||||||
|
# Permissions du module Assiduité)
|
||||||
|
(1 << 50, "ScoJustifView", "Visualisation des fichiers justificatifs"),
|
||||||
# Attention: les permissions sont codées sur 64 bits.
|
# Attention: les permissions sont codées sur 64 bits.
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -71,7 +73,7 @@ class Permission:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def init_permissions():
|
def init_permissions():
|
||||||
for (perm, symbol, description) in _SCO_PERMISSIONS:
|
for perm, symbol, description in _SCO_PERMISSIONS:
|
||||||
setattr(Permission, symbol, perm)
|
setattr(Permission, symbol, perm)
|
||||||
Permission.description[symbol] = description
|
Permission.description[symbol] = description
|
||||||
Permission.permission_by_name[symbol] = perm
|
Permission.permission_by_name[symbol] = perm
|
||||||
|
@ -54,34 +54,6 @@ def can_edit_notes(authuser, moduleimpl_id, allow_ens=True):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def can_edit_evaluation(moduleimpl_id=None):
|
|
||||||
"""Vérifie que l'on a le droit de modifier, créer ou détruire une
|
|
||||||
évaluation dans ce module.
|
|
||||||
Sinon, lance une exception.
|
|
||||||
(nb: n'implique pas le droit de saisir ou modifier des notes)
|
|
||||||
"""
|
|
||||||
from app.scodoc import sco_formsemestre
|
|
||||||
|
|
||||||
# acces pour resp. moduleimpl et resp. form semestre (dir etud)
|
|
||||||
if moduleimpl_id is None:
|
|
||||||
raise ValueError("no moduleimpl specified") # bug
|
|
||||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
|
||||||
sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
|
|
||||||
|
|
||||||
if (
|
|
||||||
current_user.has_permission(Permission.ScoEditAllEvals)
|
|
||||||
or current_user.id == M["responsable_id"]
|
|
||||||
or current_user.id in sem["responsables"]
|
|
||||||
):
|
|
||||||
return True
|
|
||||||
elif sem["ens_can_edit_eval"]:
|
|
||||||
for ens in M["ens"]:
|
|
||||||
if ens["ens_id"] == current_user.id:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def can_suppress_annotation(annotation_id):
|
def can_suppress_annotation(annotation_id):
|
||||||
"""True if current user can suppress this annotation
|
"""True if current user can suppress this annotation
|
||||||
Seuls l'auteur de l'annotation et le chef de dept peuvent supprimer
|
Seuls l'auteur de l'annotation et le chef de dept peuvent supprimer
|
||||||
|
69
app/scodoc/sco_photos.py
Normal file → Executable file
69
app/scodoc/sco_photos.py
Normal file → Executable file
@ -59,7 +59,7 @@ from flask.helpers import make_response, url_for
|
|||||||
|
|
||||||
from app import log
|
from app import log
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import Identite
|
from app.models import Identite, Scolog
|
||||||
from app.scodoc import sco_etud
|
from app.scodoc import sco_etud
|
||||||
from app.scodoc import sco_portal_apogee
|
from app.scodoc import sco_portal_apogee
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
@ -86,12 +86,12 @@ def unknown_image_url() -> str:
|
|||||||
return url_for("scolar.get_photo_image", scodoc_dept=g.scodoc_dept, etudid="")
|
return url_for("scolar.get_photo_image", scodoc_dept=g.scodoc_dept, etudid="")
|
||||||
|
|
||||||
|
|
||||||
def photo_portal_url(etud):
|
def photo_portal_url(code_nip: str):
|
||||||
"""Returns external URL to retreive photo on portal,
|
"""Returns external URL to retreive photo on portal,
|
||||||
or None if no portal configured"""
|
or None if no portal configured"""
|
||||||
photo_url = sco_portal_apogee.get_photo_url()
|
photo_url = sco_portal_apogee.get_photo_url()
|
||||||
if photo_url and etud["code_nip"]:
|
if photo_url and code_nip:
|
||||||
return photo_url + "?nip=" + etud["code_nip"]
|
return photo_url + "?nip=" + code_nip
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -120,13 +120,13 @@ def etud_photo_url(etud: dict, size="small", fast=False) -> str:
|
|||||||
path = photo_pathname(etud["photo_filename"], size=size)
|
path = photo_pathname(etud["photo_filename"], size=size)
|
||||||
if not path:
|
if not path:
|
||||||
# Portail ?
|
# Portail ?
|
||||||
ext_url = photo_portal_url(etud)
|
ext_url = photo_portal_url(etud["code_nip"])
|
||||||
if not ext_url:
|
if not ext_url:
|
||||||
# fallback: Photo "unknown"
|
# fallback: Photo "unknown"
|
||||||
photo_url = unknown_image_url()
|
photo_url = unknown_image_url()
|
||||||
else:
|
else:
|
||||||
# essaie de copier la photo du portail
|
# essaie de copier la photo du portail
|
||||||
new_path, _ = copy_portal_photo_to_fs(etud)
|
new_path, _ = copy_portal_photo_to_fs(etud["etudid"])
|
||||||
if not new_path:
|
if not new_path:
|
||||||
# copy failed, can we use external url ?
|
# copy failed, can we use external url ?
|
||||||
# nb: rarement utile, car le portail est rarement accessible sans authentification
|
# nb: rarement utile, car le portail est rarement accessible sans authentification
|
||||||
@ -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
|
||||||
@ -185,8 +185,8 @@ def _http_jpeg_file(filename):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
def etud_photo_is_local(etud: dict, size="small"):
|
def etud_photo_is_local(photo_filename: str, size="small"):
|
||||||
return photo_pathname(etud["photo_filename"], size=size)
|
return photo_pathname(photo_filename, size=size)
|
||||||
|
|
||||||
|
|
||||||
def etud_photo_html(etud: dict = None, etudid=None, title=None, size="small") -> str:
|
def etud_photo_html(etud: dict = None, etudid=None, title=None, size="small") -> str:
|
||||||
@ -205,7 +205,7 @@ def etud_photo_html(etud: dict = None, etudid=None, title=None, size="small") ->
|
|||||||
nom = etud.get("nomprenom", etud["nom_disp"])
|
nom = etud.get("nomprenom", etud["nom_disp"])
|
||||||
if title is None:
|
if title is None:
|
||||||
title = nom
|
title = nom
|
||||||
if not etud_photo_is_local(etud):
|
if not etud_photo_is_local(etud["photo_filename"]):
|
||||||
fallback = (
|
fallback = (
|
||||||
f"""onerror='this.onerror = null; this.src="{unknown_image_url()}"'"""
|
f"""onerror='this.onerror = null; this.src="{unknown_image_url()}"'"""
|
||||||
)
|
)
|
||||||
@ -254,7 +254,7 @@ def photo_pathname(photo_filename: str, size="orig"):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def store_photo(etud: dict, data, filename: str) -> tuple[bool, str]:
|
def store_photo(etud: Identite, data, filename: str) -> tuple[bool, str]:
|
||||||
"""Store image for this etud.
|
"""Store image for this etud.
|
||||||
If there is an existing photo, it is erased and replaced.
|
If there is an existing photo, it is erased and replaced.
|
||||||
data is a bytes string with image raw data.
|
data is a bytes string with image raw data.
|
||||||
@ -268,21 +268,17 @@ def store_photo(etud: dict, data, filename: str) -> tuple[bool, str]:
|
|||||||
if filesize < 10 or filesize > MAX_FILE_SIZE:
|
if filesize < 10 or filesize > MAX_FILE_SIZE:
|
||||||
return False, f"Fichier image '{filename}' de taille invalide ! ({filesize})"
|
return False, f"Fichier image '{filename}' de taille invalide ! ({filesize})"
|
||||||
try:
|
try:
|
||||||
saved_filename = save_image(etud["etudid"], data)
|
saved_filename = save_image(etud, data)
|
||||||
except (OSError, PIL.UnidentifiedImageError) as exc:
|
except (OSError, PIL.UnidentifiedImageError) as exc:
|
||||||
raise ScoValueError(
|
raise ScoValueError(
|
||||||
msg="Fichier d'image '{filename}' invalide ou format non supporté"
|
msg="Fichier d'image '{filename}' invalide ou format non supporté"
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
# update database:
|
# update database:
|
||||||
etud["photo_filename"] = saved_filename
|
etud.photo_filename = saved_filename
|
||||||
etud["foto"] = None
|
db.session.add(etud)
|
||||||
|
Scolog.logdb(method="changePhoto", msg=saved_filename, etudid=etud.id)
|
||||||
cnx = ndb.GetDBConnexion()
|
db.session.commit()
|
||||||
sco_etud.identite_edit_nocheck(cnx, etud)
|
|
||||||
cnx.commit()
|
|
||||||
#
|
|
||||||
logdb(cnx, method="changePhoto", msg=saved_filename, etudid=etud["etudid"])
|
|
||||||
#
|
#
|
||||||
return True, "ok"
|
return True, "ok"
|
||||||
|
|
||||||
@ -313,7 +309,7 @@ def suppress_photo(etud: Identite) -> None:
|
|||||||
# Internal functions
|
# Internal functions
|
||||||
|
|
||||||
|
|
||||||
def save_image(etudid, data):
|
def save_image(etud: Identite, data: bytes):
|
||||||
"""data is a bytes string.
|
"""data is a bytes string.
|
||||||
Save image in JPEG in 2 sizes (original and h90).
|
Save image in JPEG in 2 sizes (original and h90).
|
||||||
Returns filename (relative to PHOTO_DIR), without extension
|
Returns filename (relative to PHOTO_DIR), without extension
|
||||||
@ -322,7 +318,7 @@ def save_image(etudid, data):
|
|||||||
data_file.write(data)
|
data_file.write(data)
|
||||||
data_file.seek(0)
|
data_file.seek(0)
|
||||||
img = PILImage.open(data_file)
|
img = PILImage.open(data_file)
|
||||||
filename = get_new_filename(etudid)
|
filename = get_new_filename(etud)
|
||||||
path = os.path.join(PHOTO_DIR, filename)
|
path = os.path.join(PHOTO_DIR, filename)
|
||||||
log("saving %dx%d jpeg to %s" % (img.size[0], img.size[1], path))
|
log("saving %dx%d jpeg to %s" % (img.size[0], img.size[1], path))
|
||||||
img = img.convert("RGB")
|
img = img.convert("RGB")
|
||||||
@ -338,16 +334,16 @@ def scale_height(img, W=None, H=REDUCED_HEIGHT):
|
|||||||
if W is None:
|
if W is None:
|
||||||
# keep aspect
|
# keep aspect
|
||||||
W = int((img.size[0] * H) / img.size[1])
|
W = int((img.size[0] * H) / img.size[1])
|
||||||
img.thumbnail((W, H), PILImage.ANTIALIAS)
|
img.thumbnail((W, H), PILImage.LANCZOS)
|
||||||
return img
|
return img
|
||||||
|
|
||||||
|
|
||||||
def get_new_filename(etudid):
|
def get_new_filename(etud: Identite):
|
||||||
"""Constructs a random filename to store a new image.
|
"""Constructs a random filename to store a new image.
|
||||||
The path is constructed as: Fxx/etudid
|
The path is constructed as: Fxx/etudid
|
||||||
"""
|
"""
|
||||||
dept = g.scodoc_dept
|
dept = etud.departement.acronym
|
||||||
return find_new_dir() + dept + "_" + str(etudid)
|
return find_new_dir() + dept + "_" + str(etud.id)
|
||||||
|
|
||||||
|
|
||||||
def find_new_dir():
|
def find_new_dir():
|
||||||
@ -367,15 +363,14 @@ def find_new_dir():
|
|||||||
return d + "/"
|
return d + "/"
|
||||||
|
|
||||||
|
|
||||||
def copy_portal_photo_to_fs(etud: dict):
|
def copy_portal_photo_to_fs(etudid: int):
|
||||||
"""Copy the photo from portal (distant website) to local fs.
|
"""Copy the photo from portal (distant website) to local fs.
|
||||||
Returns rel. path or None if copy failed, with a diagnostic message
|
Returns rel. path or None if copy failed, with a diagnostic message
|
||||||
"""
|
"""
|
||||||
if "nomprenom" not in etud:
|
etud: Identite = Identite.query.get_or_404(etudid)
|
||||||
sco_etud.format_etud_ident(etud)
|
url = photo_portal_url(etud.code_nip)
|
||||||
url = photo_portal_url(etud)
|
|
||||||
if not url:
|
if not url:
|
||||||
return None, f"""{etud['nomprenom']}: pas de code NIP"""
|
return None, f"""{etud.nomprenom}: pas de code NIP"""
|
||||||
portal_timeout = sco_preferences.get_preference("portal_timeout")
|
portal_timeout = sco_preferences.get_preference("portal_timeout")
|
||||||
error_message = None
|
error_message = None
|
||||||
try:
|
try:
|
||||||
@ -394,11 +389,11 @@ def copy_portal_photo_to_fs(etud: dict):
|
|||||||
log(f"copy_portal_photo_to_fs: {error_message}")
|
log(f"copy_portal_photo_to_fs: {error_message}")
|
||||||
return (
|
return (
|
||||||
None,
|
None,
|
||||||
f"""{etud["nomprenom"]}: erreur chargement de {url}\n{error_message}""",
|
f"""{etud.nomprenom}: erreur chargement de {url}\n{error_message}""",
|
||||||
)
|
)
|
||||||
if r.status_code != 200:
|
if r.status_code != 200:
|
||||||
log(f"copy_portal_photo_to_fs: download failed {r.status_code }")
|
log(f"copy_portal_photo_to_fs: download failed {r.status_code }")
|
||||||
return None, f"""{etud["nomprenom"]}: erreur chargement de {url}"""
|
return None, f"""{etud.nomprenom}: erreur chargement de {url}"""
|
||||||
|
|
||||||
data = r.content # image bytes
|
data = r.content # image bytes
|
||||||
try:
|
try:
|
||||||
@ -410,8 +405,8 @@ def copy_portal_photo_to_fs(etud: dict):
|
|||||||
if status:
|
if status:
|
||||||
log(f"copy_portal_photo_to_fs: copied {url}")
|
log(f"copy_portal_photo_to_fs: copied {url}")
|
||||||
return (
|
return (
|
||||||
photo_pathname(etud["photo_filename"]),
|
photo_pathname(etud.photo_filename),
|
||||||
f"{etud['nomprenom']}: photo chargée",
|
f"{etud.nomprenom}: photo chargée",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return None, f"{etud['nomprenom']}: <b>{error_message}</b>"
|
return None, f"{etud.nomprenom}: <b>{error_message}</b>"
|
||||||
|
@ -138,7 +138,7 @@ class PlacementForm(FlaskForm):
|
|||||||
|
|
||||||
def set_evaluation_infos(self, evaluation_id):
|
def set_evaluation_infos(self, evaluation_id):
|
||||||
"""Initialise les données du formulaire avec les données de l'évaluation."""
|
"""Initialise les données du formulaire avec les données de l'évaluation."""
|
||||||
eval_data = sco_evaluation_db.do_evaluation_list(
|
eval_data = sco_evaluation_db.get_evaluation_dict(
|
||||||
{"evaluation_id": evaluation_id}
|
{"evaluation_id": evaluation_id}
|
||||||
)
|
)
|
||||||
if not eval_data:
|
if not eval_data:
|
||||||
@ -239,7 +239,7 @@ class PlacementRunner:
|
|||||||
self.groups_ids = [
|
self.groups_ids = [
|
||||||
gid if gid != TOUS else form.tous_id for gid in form["groups"].data
|
gid if gid != TOUS else form.tous_id for gid in form["groups"].data
|
||||||
]
|
]
|
||||||
self.eval_data = sco_evaluation_db.do_evaluation_list(
|
self.eval_data = sco_evaluation_db.get_evaluation_dict(
|
||||||
{"evaluation_id": self.evaluation_id}
|
{"evaluation_id": self.evaluation_id}
|
||||||
)[0]
|
)[0]
|
||||||
self.groups = sco_groups.listgroups(self.groups_ids)
|
self.groups = sco_groups.listgroups(self.groups_ids)
|
||||||
|
@ -37,7 +37,7 @@ 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
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app.scodoc import sco_abs
|
from app.scodoc import sco_assiduites
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
from app.scodoc import sco_formsemestre
|
from app.scodoc import sco_formsemestre
|
||||||
from app.scodoc import sco_groups
|
from app.scodoc import sco_groups
|
||||||
@ -107,7 +107,7 @@ def etud_get_poursuite_info(sem, etud):
|
|||||||
rangs.append(["rang_" + codeModule, rangModule])
|
rangs.append(["rang_" + codeModule, rangModule])
|
||||||
|
|
||||||
# Absences
|
# Absences
|
||||||
nbabs, nbabsjust = sco_abs.get_abs_count(etudid, nt.sem)
|
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, nt.sem)
|
||||||
if (
|
if (
|
||||||
dec
|
dec
|
||||||
and not sem_descr # not sem_descr pour ne prendre que le semestre validé le plus récent
|
and not sem_descr # not sem_descr pour ne prendre que le semestre validé le plus récent
|
||||||
|
@ -162,7 +162,7 @@ def _convert_pref_type(p, pref_spec):
|
|||||||
# special case for float values (where NULL means 0)
|
# special case for float values (where NULL means 0)
|
||||||
p["value"] = float(p["value"] or 0)
|
p["value"] = float(p["value"] or 0)
|
||||||
elif typ == "int":
|
elif typ == "int":
|
||||||
p["value"] = int(p["value"] or 0)
|
p["value"] = int(float(p["value"] or 0))
|
||||||
else:
|
else:
|
||||||
raise ValueError("invalid preference type")
|
raise ValueError("invalid preference type")
|
||||||
|
|
||||||
@ -204,6 +204,7 @@ PREF_CATEGORIES = (
|
|||||||
("misc", {"title": "Divers"}),
|
("misc", {"title": "Divers"}),
|
||||||
("apc", {"title": "BUT et Approches par Compétences"}),
|
("apc", {"title": "BUT et Approches par Compétences"}),
|
||||||
("abs", {"title": "Suivi des absences", "related": ("bul",)}),
|
("abs", {"title": "Suivi des absences", "related": ("bul",)}),
|
||||||
|
("assi", {"title": "Gestion de l'assiduité"}),
|
||||||
("portal", {"title": "Liaison avec portail (Apogée, etc)"}),
|
("portal", {"title": "Liaison avec portail (Apogée, etc)"}),
|
||||||
("apogee", {"title": "Exports Apogée"}),
|
("apogee", {"title": "Exports Apogée"}),
|
||||||
(
|
(
|
||||||
@ -598,6 +599,86 @@ class BasePreferences(object):
|
|||||||
"category": "abs",
|
"category": "abs",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
# Assiduités
|
||||||
|
(
|
||||||
|
"forcer_module",
|
||||||
|
{
|
||||||
|
"initvalue": 0,
|
||||||
|
"title": "Forcer la déclaration du module.",
|
||||||
|
"input_type": "boolcheckbox",
|
||||||
|
"labels": ["non", "oui"],
|
||||||
|
"category": "assi",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"forcer_present",
|
||||||
|
{
|
||||||
|
"initvalue": 0,
|
||||||
|
"title": "Forcer l'appel des présents",
|
||||||
|
"input_type": "boolcheckbox",
|
||||||
|
"labels": ["non", "oui"],
|
||||||
|
"category": "assi",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"periode_defaut",
|
||||||
|
{
|
||||||
|
"initvalue": 2.0,
|
||||||
|
"size": 10,
|
||||||
|
"title": "Durée par défaut d'un créneau",
|
||||||
|
"type": "float",
|
||||||
|
"category": "assi",
|
||||||
|
"only_global": True,
|
||||||
|
"explanation": "Durée d'un créneau en heure. Utilisé dans les pages de saisie",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"assi_etat_defaut",
|
||||||
|
{
|
||||||
|
"initvalue": "aucun",
|
||||||
|
"input_type": "menu",
|
||||||
|
"labels": ["aucun", "present", "retard", "absent"],
|
||||||
|
"allowed_values": ["aucun", "present", "retard", "absent"],
|
||||||
|
"title": "Définir l'état par défaut",
|
||||||
|
"category": "assi",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"non_travail",
|
||||||
|
{
|
||||||
|
"initvalue": "sam, dim",
|
||||||
|
"title": "Jours non travaillés",
|
||||||
|
"size": 40,
|
||||||
|
"category": "assi",
|
||||||
|
"only_global": True,
|
||||||
|
"explanation": "Liste des jours (lun,mar,mer,jeu,ven,sam,dim)",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"assi_metrique",
|
||||||
|
{
|
||||||
|
"initvalue": "1/2 J.",
|
||||||
|
"input_type": "menu",
|
||||||
|
"labels": scu.AssiduitesMetrics.LONG,
|
||||||
|
"allowed_values": scu.AssiduitesMetrics.SHORT,
|
||||||
|
"title": "Métrique de l'assiduité",
|
||||||
|
"explanation": "Unité utilisée dans la fiche étudiante, les bilans et les calculs",
|
||||||
|
"category": "assi",
|
||||||
|
"only_global": True,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"assi_seuil",
|
||||||
|
{
|
||||||
|
"initvalue": 3,
|
||||||
|
"size": 10,
|
||||||
|
"title": "Seuil d'alerte des absences",
|
||||||
|
"type": "int",
|
||||||
|
"explanation": "Nombres d'absences limite avant alerte dans le bilan (utilisation de l'unité métrique ↑ )",
|
||||||
|
"category": "assi",
|
||||||
|
"only_global": True,
|
||||||
|
},
|
||||||
|
),
|
||||||
# portal
|
# portal
|
||||||
(
|
(
|
||||||
"portal_url",
|
"portal_url",
|
||||||
@ -1700,7 +1781,7 @@ class BasePreferences(object):
|
|||||||
(
|
(
|
||||||
"feuille_releve_abs_taille",
|
"feuille_releve_abs_taille",
|
||||||
{
|
{
|
||||||
"initvalue": "A3",
|
"initvalue": "A4",
|
||||||
"input_type": "menu",
|
"input_type": "menu",
|
||||||
"labels": ["A3", "A4"],
|
"labels": ["A3", "A4"],
|
||||||
"allowed_values": ["A3", "A4"],
|
"allowed_values": ["A3", "A4"],
|
||||||
|
@ -39,7 +39,7 @@ from flask_login import current_user
|
|||||||
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, ScolarAutorisationInscription
|
from app.models import FormSemestre, Identite, ScolarAutorisationInscription
|
||||||
from app.scodoc import sco_abs
|
from app.scodoc import sco_assiduites
|
||||||
from app.scodoc import codes_cursus
|
from app.scodoc import codes_cursus
|
||||||
from app.scodoc import sco_groups
|
from app.scodoc import sco_groups
|
||||||
from app.scodoc import sco_etud
|
from app.scodoc import sco_etud
|
||||||
@ -139,7 +139,7 @@ def feuille_preparation_jury(formsemestre_id):
|
|||||||
main_partition_id, ""
|
main_partition_id, ""
|
||||||
)
|
)
|
||||||
# absences:
|
# absences:
|
||||||
e_nbabs, e_nbabsjust = sco_abs.get_abs_count(etud.id, sem)
|
e_nbabs, e_nbabsjust = sco_assiduites.get_assiduites_count(etud.id, sem)
|
||||||
nbabs[etud.id] = e_nbabs
|
nbabs[etud.id] = e_nbabs
|
||||||
nbabsjust[etud.id] = e_nbabs - e_nbabsjust
|
nbabsjust[etud.id] = e_nbabs - e_nbabsjust
|
||||||
|
|
||||||
|
@ -45,7 +45,6 @@ from app.models import (
|
|||||||
FormSemestre,
|
FormSemestre,
|
||||||
Module,
|
Module,
|
||||||
ModuleImpl,
|
ModuleImpl,
|
||||||
NotesNotes,
|
|
||||||
ScolarNews,
|
ScolarNews,
|
||||||
)
|
)
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
@ -54,16 +53,13 @@ from app.scodoc.sco_exceptions import (
|
|||||||
AccessDenied,
|
AccessDenied,
|
||||||
InvalidNoteValue,
|
InvalidNoteValue,
|
||||||
NoteProcessError,
|
NoteProcessError,
|
||||||
ScoBugCatcher,
|
|
||||||
ScoException,
|
ScoException,
|
||||||
ScoInvalidParamError,
|
ScoInvalidParamError,
|
||||||
ScoValueError,
|
ScoValueError,
|
||||||
)
|
)
|
||||||
from app.scodoc import html_sco_header, sco_users
|
from app.scodoc import html_sco_header, sco_users
|
||||||
from app.scodoc import htmlutils
|
from app.scodoc import htmlutils
|
||||||
from app.scodoc import sco_abs
|
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
from app.scodoc import sco_edit_module
|
|
||||||
from app.scodoc import sco_etud
|
from app.scodoc import sco_etud
|
||||||
from app.scodoc import sco_evaluation_db
|
from app.scodoc import sco_evaluation_db
|
||||||
from app.scodoc import sco_evaluations
|
from app.scodoc import sco_evaluations
|
||||||
@ -71,7 +67,6 @@ from app.scodoc import sco_excel
|
|||||||
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
|
||||||
from app.scodoc import sco_groups_view
|
from app.scodoc import sco_groups_view
|
||||||
from app.scodoc import sco_moduleimpl
|
|
||||||
from app.scodoc import sco_permissions_check
|
from app.scodoc import sco_permissions_check
|
||||||
from app.scodoc import sco_undo_notes
|
from app.scodoc import sco_undo_notes
|
||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
@ -502,6 +497,7 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
|
|||||||
"""
|
"""
|
||||||
]
|
]
|
||||||
# news
|
# news
|
||||||
|
if nb_suppress:
|
||||||
ScolarNews.add(
|
ScolarNews.add(
|
||||||
typ=ScolarNews.NEWS_NOTE,
|
typ=ScolarNews.NEWS_NOTE,
|
||||||
obj=evaluation.moduleimpl.id,
|
obj=evaluation.moduleimpl.id,
|
||||||
@ -884,15 +880,15 @@ def feuille_saisie_notes(evaluation_id, group_ids=[]):
|
|||||||
modimpl = evaluation.moduleimpl
|
modimpl = evaluation.moduleimpl
|
||||||
formsemestre = modimpl.formsemestre
|
formsemestre = modimpl.formsemestre
|
||||||
mod_responsable = sco_users.user_info(modimpl.responsable_id)
|
mod_responsable = sco_users.user_info(modimpl.responsable_id)
|
||||||
if evaluation.jour:
|
if evaluation.date_debut:
|
||||||
indication_date = evaluation.jour.isoformat()
|
indication_date = evaluation.date_debut.date().isoformat()
|
||||||
else:
|
else:
|
||||||
indication_date = scu.sanitize_filename(evaluation.description or "")[:12]
|
indication_date = scu.sanitize_filename(evaluation.description or "")[:12]
|
||||||
eval_name = f"{evaluation.moduleimpl.module.code}-{indication_date}"
|
eval_name = f"{evaluation.moduleimpl.module.code}-{indication_date}"
|
||||||
|
|
||||||
date_str = (
|
date_str = (
|
||||||
f"""du {evaluation.jour.strftime("%d/%m/%Y")}"""
|
f"""du {evaluation.date_debut.strftime("%d/%m/%Y")}"""
|
||||||
if evaluation.jour
|
if evaluation.date_debut
|
||||||
else "(sans date)"
|
else "(sans date)"
|
||||||
)
|
)
|
||||||
eval_titre = f"""{evaluation.description if evaluation.description else "évaluation"} {date_str}"""
|
eval_titre = f"""{evaluation.description if evaluation.description else "évaluation"} {date_str}"""
|
||||||
@ -1107,19 +1103,21 @@ def _get_sorted_etuds(evaluation: Evaluation, etudids: list, formsemestre_id: in
|
|||||||
e["groups"] = sco_groups.get_etud_groups(etudid, formsemestre_id)
|
e["groups"] = sco_groups.get_etud_groups(etudid, formsemestre_id)
|
||||||
|
|
||||||
# Information sur absence (tenant compte de la demi-journée)
|
# Information sur absence (tenant compte de la demi-journée)
|
||||||
jour_iso = evaluation.jour.isoformat() if evaluation.jour else ""
|
jour_iso = (
|
||||||
|
evaluation.date_debut.date().isoformat() if evaluation.date_debut else ""
|
||||||
|
)
|
||||||
warn_abs_lst = []
|
warn_abs_lst = []
|
||||||
if evaluation.is_matin():
|
if evaluation.is_matin():
|
||||||
nbabs = sco_abs.count_abs(etudid, jour_iso, jour_iso, matin=True)
|
nbabs = 0 # TODO-ASSIDUITE sco_abs.count_abs(etudid, jour_iso, jour_iso, matin=True)
|
||||||
nbabsjust = sco_abs.count_abs_just(etudid, jour_iso, jour_iso, matin=True)
|
nbabsjust = 0 # TODO-ASSIDUITE sco_abs.count_abs_just(etudid, jour_iso, jour_iso, matin=True)
|
||||||
if nbabs:
|
if nbabs:
|
||||||
if nbabsjust:
|
if nbabsjust:
|
||||||
warn_abs_lst.append("absent justifié le matin !")
|
warn_abs_lst.append("absent justifié le matin !")
|
||||||
else:
|
else:
|
||||||
warn_abs_lst.append("absent le matin !")
|
warn_abs_lst.append("absent le matin !")
|
||||||
if evaluation.is_apresmidi():
|
if evaluation.is_apresmidi():
|
||||||
nbabs = sco_abs.count_abs(etudid, jour_iso, jour_iso, matin=0)
|
nbabs = 0 # TODO-ASSIDUITE sco_abs.count_abs(etudid, jour_iso, jour_iso, matin=0)
|
||||||
nbabsjust = sco_abs.count_abs_just(etudid, jour_iso, jour_iso, matin=0)
|
nbabsjust = 0 # TODO-ASSIDUITE sco_abs.count_abs_just(etudid, jour_iso, jour_iso, matin=0)
|
||||||
if nbabs:
|
if nbabs:
|
||||||
if nbabsjust:
|
if nbabsjust:
|
||||||
warn_abs_lst.append("absent justifié l'après-midi !")
|
warn_abs_lst.append("absent justifié l'après-midi !")
|
||||||
|
@ -43,7 +43,8 @@ from PIL import Image as PILImage
|
|||||||
import flask
|
import flask
|
||||||
from flask import url_for, g, send_file, request
|
from flask import url_for, g, send_file, request
|
||||||
|
|
||||||
from app import log
|
from app import db, log
|
||||||
|
from app.models import Identite
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
@ -146,7 +147,7 @@ def trombino_html(groups_infos):
|
|||||||
'<span class="trombi_box"><span class="trombi-photo" id="trombi-%s">'
|
'<span class="trombi_box"><span class="trombi-photo" id="trombi-%s">'
|
||||||
% t["etudid"]
|
% t["etudid"]
|
||||||
)
|
)
|
||||||
if sco_photos.etud_photo_is_local(t, size="small"):
|
if sco_photos.etud_photo_is_local(t["photo_filename"], size="small"):
|
||||||
foto = sco_photos.etud_photo_html(t, title="")
|
foto = sco_photos.etud_photo_html(t, title="")
|
||||||
else: # la photo n'est pas immédiatement dispo
|
else: # la photo n'est pas immédiatement dispo
|
||||||
foto = f"""<span class="unloaded_img" id="{t["etudid"]
|
foto = f"""<span class="unloaded_img" id="{t["etudid"]
|
||||||
@ -194,7 +195,7 @@ def check_local_photos_availability(groups_infos, fmt=""):
|
|||||||
nb_missing = 0
|
nb_missing = 0
|
||||||
for t in groups_infos.members:
|
for t in groups_infos.members:
|
||||||
_ = sco_photos.etud_photo_url(t) # -> copy distant files if needed
|
_ = sco_photos.etud_photo_url(t) # -> copy distant files if needed
|
||||||
if not sco_photos.etud_photo_is_local(t):
|
if not sco_photos.etud_photo_is_local(t["photo_filename"]):
|
||||||
nb_missing += 1
|
nb_missing += 1
|
||||||
if nb_missing > 0:
|
if nb_missing > 0:
|
||||||
parameters = {"group_ids": groups_infos.group_ids, "format": fmt}
|
parameters = {"group_ids": groups_infos.group_ids, "format": fmt}
|
||||||
@ -278,7 +279,7 @@ def trombino_copy_photos(group_ids=[], dialog_confirmed=False):
|
|||||||
msg = []
|
msg = []
|
||||||
nok = 0
|
nok = 0
|
||||||
for etud in groups_infos.members:
|
for etud in groups_infos.members:
|
||||||
path, diag = sco_photos.copy_portal_photo_to_fs(etud)
|
path, diag = sco_photos.copy_portal_photo_to_fs(etud["etudid"])
|
||||||
msg.append(diag)
|
msg.append(diag)
|
||||||
if path:
|
if path:
|
||||||
nok += 1
|
nok += 1
|
||||||
@ -539,7 +540,7 @@ def photos_import_files_form(group_ids=()):
|
|||||||
return flask.redirect(back_url)
|
return flask.redirect(back_url)
|
||||||
else:
|
else:
|
||||||
|
|
||||||
def callback(etud, data, filename):
|
def callback(etud: Identite, data, filename):
|
||||||
return sco_photos.store_photo(etud, data, filename)
|
return sco_photos.store_photo(etud, data, filename)
|
||||||
|
|
||||||
(
|
(
|
||||||
@ -640,14 +641,12 @@ def zip_excel_import_files(
|
|||||||
if normname in filename_to_etudid:
|
if normname in filename_to_etudid:
|
||||||
etudid = filename_to_etudid[normname]
|
etudid = filename_to_etudid[normname]
|
||||||
# ok, store photo
|
# ok, store photo
|
||||||
try:
|
etud: Identite = db.session.get(Identite, etudid)
|
||||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
if not etud:
|
||||||
del filename_to_etudid[normname]
|
|
||||||
except Exception as exc:
|
|
||||||
raise ScoValueError(
|
raise ScoValueError(
|
||||||
f"ID étudiant invalide: {etudid}", dest_url=back_url
|
f"ID étudiant invalide: {etudid}", dest_url=back_url
|
||||||
) from exc
|
)
|
||||||
|
del filename_to_etudid[normname]
|
||||||
status, err_msg = callback(
|
status, err_msg = callback(
|
||||||
etud,
|
etud,
|
||||||
data,
|
data,
|
||||||
|
@ -41,7 +41,7 @@ from reportlab.lib.units import cm
|
|||||||
from reportlab.platypus import KeepInFrame, Paragraph, Table, TableStyle
|
from reportlab.platypus import KeepInFrame, Paragraph, Table, TableStyle
|
||||||
from reportlab.platypus.doctemplate import BaseDocTemplate
|
from reportlab.platypus.doctemplate import BaseDocTemplate
|
||||||
|
|
||||||
from app.scodoc import sco_abs
|
from app.scodoc import sco_cal
|
||||||
from app.scodoc import sco_etud
|
from app.scodoc import sco_etud
|
||||||
from app.scodoc.sco_exceptions import ScoPDFFormatError
|
from app.scodoc.sco_exceptions import ScoPDFFormatError
|
||||||
from app.scodoc import sco_groups
|
from app.scodoc import sco_groups
|
||||||
@ -299,9 +299,9 @@ def pdf_feuille_releve_absences(
|
|||||||
NB_CELL_PM = sco_preferences.get_preference("feuille_releve_abs_PM")
|
NB_CELL_PM = sco_preferences.get_preference("feuille_releve_abs_PM")
|
||||||
col_width = 0.85 * cm
|
col_width = 0.85 * cm
|
||||||
if sco_preferences.get_preference("feuille_releve_abs_samedi"):
|
if sco_preferences.get_preference("feuille_releve_abs_samedi"):
|
||||||
days = sco_abs.DAYNAMES[:6] # Lundi, ..., Samedi
|
days = sco_cal.DAYNAMES[:6] # Lundi, ..., Samedi
|
||||||
else:
|
else:
|
||||||
days = sco_abs.DAYNAMES[:5] # Lundi, ..., Vendredi
|
days = sco_cal.DAYNAMES[:5] # Lundi, ..., Vendredi
|
||||||
nb_days = len(days)
|
nb_days = len(days)
|
||||||
|
|
||||||
# Informations sur les groupes à afficher:
|
# Informations sur les groupes à afficher:
|
||||||
|
@ -60,7 +60,7 @@ from app.models.formsemestre import FormSemestre
|
|||||||
|
|
||||||
|
|
||||||
from app import db, log
|
from app import db, log
|
||||||
from app.models import UniteEns
|
from app.models import Evaluation, ModuleImpl, UniteEns
|
||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
from app.scodoc import codes_cursus
|
from app.scodoc import codes_cursus
|
||||||
from app.scodoc import sco_edit_matiere
|
from app.scodoc import sco_edit_matiere
|
||||||
@ -154,6 +154,7 @@ def external_ue_inscrit_et_note(
|
|||||||
"""Inscrit les étudiants au moduleimpl, crée au besoin une évaluation
|
"""Inscrit les étudiants au moduleimpl, crée au besoin une évaluation
|
||||||
et enregistre les notes.
|
et enregistre les notes.
|
||||||
"""
|
"""
|
||||||
|
moduleimpl: ModuleImpl = db.session.get(ModuleImpl, moduleimpl_id)
|
||||||
log(
|
log(
|
||||||
f"external_ue_inscrit_et_note(moduleimpl_id={moduleimpl_id}, notes_etuds={notes_etuds})"
|
f"external_ue_inscrit_et_note(moduleimpl_id={moduleimpl_id}, notes_etuds={notes_etuds})"
|
||||||
)
|
)
|
||||||
@ -163,18 +164,14 @@ def external_ue_inscrit_et_note(
|
|||||||
formsemestre_id,
|
formsemestre_id,
|
||||||
list(notes_etuds.keys()),
|
list(notes_etuds.keys()),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Création d'une évaluation si il n'y en a pas déjà:
|
# Création d'une évaluation si il n'y en a pas déjà:
|
||||||
mod_evals = sco_evaluation_db.do_evaluation_list(
|
if moduleimpl.evaluations.count() > 0:
|
||||||
args={"moduleimpl_id": moduleimpl_id}
|
|
||||||
)
|
|
||||||
if len(mod_evals):
|
|
||||||
# met la note dans le première évaluation existante:
|
# met la note dans le première évaluation existante:
|
||||||
evaluation_id = mod_evals[0]["evaluation_id"]
|
evaluation: Evaluation = moduleimpl.evaluations.first()
|
||||||
else:
|
else:
|
||||||
# crée une évaluation:
|
# crée une évaluation:
|
||||||
evaluation_id = sco_evaluation_db.do_evaluation_create(
|
evaluation: Evaluation = Evaluation.create(
|
||||||
moduleimpl_id=moduleimpl_id,
|
moduleimpl=moduleimpl,
|
||||||
note_max=20.0,
|
note_max=20.0,
|
||||||
coefficient=1.0,
|
coefficient=1.0,
|
||||||
publish_incomplete=True,
|
publish_incomplete=True,
|
||||||
@ -185,7 +182,7 @@ def external_ue_inscrit_et_note(
|
|||||||
# Saisie des notes
|
# Saisie des notes
|
||||||
_, _, _ = sco_saisie_notes.notes_add(
|
_, _, _ = sco_saisie_notes.notes_add(
|
||||||
current_user,
|
current_user,
|
||||||
evaluation_id,
|
evaluation.id,
|
||||||
list(notes_etuds.items()),
|
list(notes_etuds.items()),
|
||||||
do_it=True,
|
do_it=True,
|
||||||
)
|
)
|
||||||
|
@ -149,7 +149,7 @@ def list_operations(evaluation_id):
|
|||||||
|
|
||||||
def evaluation_list_operations(evaluation_id):
|
def evaluation_list_operations(evaluation_id):
|
||||||
"""Page listing operations on evaluation"""
|
"""Page listing operations on evaluation"""
|
||||||
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
|
E = sco_evaluation_db.get_evaluation_dict({"evaluation_id": evaluation_id})[0]
|
||||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
|
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
|
||||||
|
|
||||||
Ops = list_operations(evaluation_id)
|
Ops = list_operations(evaluation_id)
|
||||||
@ -179,7 +179,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"):
|
|||||||
"""
|
"""
|
||||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||||
rows = ndb.SimpleDictFetch(
|
rows = ndb.SimpleDictFetch(
|
||||||
"""SELECT i.nom, i.prenom, code_nip, n.*, mod.titre, e.description, e.jour,
|
"""SELECT i.nom, i.prenom, code_nip, n.*, mod.titre, e.description, e.date_debut,
|
||||||
u.user_name, e.id as evaluation_id
|
u.user_name, e.id as evaluation_id
|
||||||
FROM notes_notes n, notes_evaluation e, notes_moduleimpl mi,
|
FROM notes_notes n, notes_evaluation e, notes_moduleimpl mi,
|
||||||
notes_modules mod, identite i, "user" u
|
notes_modules mod, identite i, "user" u
|
||||||
@ -197,6 +197,12 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"):
|
|||||||
keep_numeric = format in scu.FORMATS_NUMERIQUES
|
keep_numeric = format in scu.FORMATS_NUMERIQUES
|
||||||
for row in rows:
|
for row in rows:
|
||||||
row["value"] = scu.fmt_note(row["value"], keep_numeric=keep_numeric)
|
row["value"] = scu.fmt_note(row["value"], keep_numeric=keep_numeric)
|
||||||
|
row["date_evaluation"] = (
|
||||||
|
row["date_debut"].strftime("%d/%m/%Y %H:%M") if row["date_debut"] else ""
|
||||||
|
)
|
||||||
|
row["_date_evaluation_order"] = (
|
||||||
|
row["date_debut"].isoformat() if row["date_debut"] else ""
|
||||||
|
)
|
||||||
columns_ids = (
|
columns_ids = (
|
||||||
"date",
|
"date",
|
||||||
"code_nip",
|
"code_nip",
|
||||||
@ -207,7 +213,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"):
|
|||||||
"titre",
|
"titre",
|
||||||
"evaluation_id",
|
"evaluation_id",
|
||||||
"description",
|
"description",
|
||||||
"jour",
|
"date_evaluation",
|
||||||
"comment",
|
"comment",
|
||||||
)
|
)
|
||||||
titles = {
|
titles = {
|
||||||
@ -221,7 +227,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"):
|
|||||||
"evaluation_id": "evaluation_id",
|
"evaluation_id": "evaluation_id",
|
||||||
"titre": "Module",
|
"titre": "Module",
|
||||||
"description": "Evaluation",
|
"description": "Evaluation",
|
||||||
"jour": "Date éval.",
|
"date_evaluation": "Date éval.",
|
||||||
}
|
}
|
||||||
tab = GenTable(
|
tab = GenTable(
|
||||||
titles=titles,
|
titles=titles,
|
||||||
|
@ -32,13 +32,14 @@ import base64
|
|||||||
import bisect
|
import bisect
|
||||||
import collections
|
import collections
|
||||||
import datetime
|
import datetime
|
||||||
from enum import IntEnum
|
from enum import IntEnum, Enum
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
import numbers
|
import numbers
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
from shutil import get_terminal_size
|
||||||
import _thread
|
import _thread
|
||||||
import time
|
import time
|
||||||
import unicodedata
|
import unicodedata
|
||||||
@ -50,6 +51,10 @@ from PIL import Image as PILImage
|
|||||||
import pydot
|
import pydot
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
from pytz import timezone
|
||||||
|
|
||||||
|
import dateutil.parser as dtparser
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
from flask import g, request, Response
|
from flask import g, request, Response
|
||||||
from flask import flash, url_for, make_response
|
from flask import flash, url_for, make_response
|
||||||
@ -63,10 +68,15 @@ from app.scodoc.codes_cursus import NOTES_TOLERANCE, CODES_EXPL
|
|||||||
from app.scodoc import sco_xml
|
from app.scodoc import sco_xml
|
||||||
import sco_version
|
import sco_version
|
||||||
|
|
||||||
|
# En principe, aucun champ text ne devrait excéder cette taille
|
||||||
|
MAX_TEXT_LEN = 64 * 1024
|
||||||
|
|
||||||
# le répertoire static, lié à chaque release pour éviter les problèmes de caches
|
# le répertoire static, lié à chaque release pour éviter les problèmes de caches
|
||||||
STATIC_DIR = (
|
STATIC_DIR = (
|
||||||
os.environ.get("SCRIPT_NAME", "") + "/ScoDoc/static/links/" + sco_version.SCOVERSION
|
os.environ.get("SCRIPT_NAME", "") + "/ScoDoc/static/links/" + sco_version.SCOVERSION
|
||||||
)
|
)
|
||||||
|
# La time zone du serveur:
|
||||||
|
TIME_ZONE = timezone("/".join(os.path.realpath("/etc/localtime").split("/")[-2:]))
|
||||||
|
|
||||||
# ----- CALCUL ET PRESENTATION DES NOTES
|
# ----- CALCUL ET PRESENTATION DES NOTES
|
||||||
NOTES_PRECISION = 1e-4 # evite eventuelles erreurs d'arrondis
|
NOTES_PRECISION = 1e-4 # evite eventuelles erreurs d'arrondis
|
||||||
@ -91,6 +101,216 @@ ETATS_INSCRIPTION = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def print_progress_bar(
|
||||||
|
iteration,
|
||||||
|
total,
|
||||||
|
prefix="",
|
||||||
|
suffix="",
|
||||||
|
finish_msg="",
|
||||||
|
decimals=1,
|
||||||
|
length=100,
|
||||||
|
fill="█",
|
||||||
|
autosize=False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Affiche une progress bar à un point donné (mettre dans une boucle pour rendre dynamique)
|
||||||
|
@params:
|
||||||
|
iteration - Required : index du point donné (Int)
|
||||||
|
total - Required : nombre total avant complétion (eg: len(List))
|
||||||
|
prefix - Optional : Préfix -> écrit à gauche de la barre (Str)
|
||||||
|
suffix - Optional : Suffix -> écrit à droite de la barre (Str)
|
||||||
|
decimals - Optional : nombres de chiffres après la virgule (Int)
|
||||||
|
length - Optional : taille de la barre en nombre de caractères (Int)
|
||||||
|
fill - Optional : charactère de remplissange de la barre (Str)
|
||||||
|
autosize - Optional : Choisir automatiquement la taille de la barre en fonction du terminal (Bool)
|
||||||
|
"""
|
||||||
|
percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total)))
|
||||||
|
color = TerminalColor.RED
|
||||||
|
if 50 >= float(percent) > 25:
|
||||||
|
color = TerminalColor.MAGENTA
|
||||||
|
if 75 >= float(percent) > 50:
|
||||||
|
color = TerminalColor.BLUE
|
||||||
|
if 90 >= float(percent) > 75:
|
||||||
|
color = TerminalColor.CYAN
|
||||||
|
if 100 >= float(percent) > 90:
|
||||||
|
color = TerminalColor.GREEN
|
||||||
|
styling = f"{prefix} |{fill}| {percent}% {suffix}"
|
||||||
|
if autosize:
|
||||||
|
cols, _ = get_terminal_size(fallback=(length, 1))
|
||||||
|
length = cols - len(styling)
|
||||||
|
filled_length = int(length * iteration // total)
|
||||||
|
pg_bar = fill * filled_length + "-" * (length - filled_length)
|
||||||
|
print(f"\r{color}{styling.replace(fill, pg_bar)}{TerminalColor.RESET}", end="\r")
|
||||||
|
# Affiche une nouvelle ligne vide
|
||||||
|
if iteration == total:
|
||||||
|
print(f"\n{finish_msg}")
|
||||||
|
|
||||||
|
|
||||||
|
class TerminalColor:
|
||||||
|
"""Ensemble de couleur pour terminaux"""
|
||||||
|
|
||||||
|
BLUE = "\033[94m"
|
||||||
|
CYAN = "\033[96m"
|
||||||
|
GREEN = "\033[92m"
|
||||||
|
MAGENTA = "\033[95m"
|
||||||
|
RED = "\033[91m"
|
||||||
|
RESET = "\033[0m"
|
||||||
|
|
||||||
|
|
||||||
|
class BiDirectionalEnum(Enum):
|
||||||
|
"""Permet la recherche inverse d'un enum
|
||||||
|
Condition : les clés et les valeurs doivent être uniques
|
||||||
|
les clés doivent être en MAJUSCULES
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def contains(cls, attr: str):
|
||||||
|
"""Vérifie sur un attribut existe dans l'enum"""
|
||||||
|
return attr.upper() in cls._member_names_
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def all(cls, keys=True):
|
||||||
|
"""Retourne toutes les clés de l'enum"""
|
||||||
|
return cls._member_names_ if keys else list(cls._value2member_map_.keys())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, attr: str, default: any = None):
|
||||||
|
"""Récupère une valeur à partir de son attribut"""
|
||||||
|
val = None
|
||||||
|
try:
|
||||||
|
val = cls[attr.upper()]
|
||||||
|
except (KeyError, AttributeError):
|
||||||
|
val = default
|
||||||
|
return val
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def inverse(cls):
|
||||||
|
"""Retourne un dictionnaire représentant la map inverse de l'Enum"""
|
||||||
|
return cls._value2member_map_
|
||||||
|
|
||||||
|
|
||||||
|
class EtatAssiduite(int, BiDirectionalEnum):
|
||||||
|
"""Code des états d'assiduité"""
|
||||||
|
|
||||||
|
# Stockés en BD ne pas modifier
|
||||||
|
|
||||||
|
PRESENT = 0
|
||||||
|
RETARD = 1
|
||||||
|
ABSENT = 2
|
||||||
|
|
||||||
|
|
||||||
|
class EtatJustificatif(int, BiDirectionalEnum):
|
||||||
|
"""Code des états des justificatifs"""
|
||||||
|
|
||||||
|
# Stockés en BD ne pas modifier
|
||||||
|
|
||||||
|
VALIDE = 0
|
||||||
|
NON_VALIDE = 1
|
||||||
|
ATTENTE = 2
|
||||||
|
MODIFIE = 3
|
||||||
|
|
||||||
|
|
||||||
|
def is_iso_formated(date: str, convert=False) -> bool or datetime.datetime or None:
|
||||||
|
"""
|
||||||
|
Vérifie si une date est au format iso
|
||||||
|
|
||||||
|
Retourne un booléen Vrai (ou un objet Datetime si convert = True)
|
||||||
|
si l'objet est au format iso
|
||||||
|
|
||||||
|
Retourne Faux si l'objet n'est pas au format et convert = False
|
||||||
|
|
||||||
|
Retourne None sinon
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
date: datetime.datetime = dtparser.isoparse(date)
|
||||||
|
return date if convert else True
|
||||||
|
except (dtparser.ParserError, ValueError, TypeError):
|
||||||
|
return None if convert else False
|
||||||
|
|
||||||
|
|
||||||
|
def localize_datetime(date: datetime.datetime or str) -> datetime.datetime:
|
||||||
|
"""Ajoute un timecode UTC à la date donnée."""
|
||||||
|
if isinstance(date, str):
|
||||||
|
date = is_iso_formated(date, convert=True)
|
||||||
|
|
||||||
|
new_date: datetime.datetime = date
|
||||||
|
if new_date.tzinfo is None:
|
||||||
|
try:
|
||||||
|
new_date = timezone("Europe/Paris").localize(date)
|
||||||
|
except OverflowError:
|
||||||
|
new_date = timezone("UTC").localize(date)
|
||||||
|
return new_date
|
||||||
|
|
||||||
|
|
||||||
|
def is_period_overlapping(
|
||||||
|
periode: tuple[datetime.datetime, datetime.datetime],
|
||||||
|
interval: tuple[datetime.datetime, datetime.datetime],
|
||||||
|
bornes: bool = True,
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Vérifie si la période et l'interval s'intersectent
|
||||||
|
si strict == True : les extrémitées ne comptes pas
|
||||||
|
Retourne Vrai si c'est le cas, faux sinon
|
||||||
|
"""
|
||||||
|
p_deb, p_fin = periode
|
||||||
|
i_deb, i_fin = interval
|
||||||
|
|
||||||
|
if bornes:
|
||||||
|
return p_deb <= i_fin and p_fin >= i_deb
|
||||||
|
return p_deb < i_fin and p_fin > i_deb
|
||||||
|
|
||||||
|
|
||||||
|
class AssiduitesMetrics:
|
||||||
|
"""Labels associés au métrique de l'assiduité"""
|
||||||
|
|
||||||
|
SHORT: list[str] = ["1/2 J.", "J.", "H."]
|
||||||
|
LONG: list[str] = ["Demi-journée", "Journée", "Heure"]
|
||||||
|
TAG: list[str] = ["demi", "journee", "heure"]
|
||||||
|
|
||||||
|
|
||||||
|
def translate_assiduites_metric(metric, inverse=True, short=True) -> str:
|
||||||
|
"""
|
||||||
|
translate_assiduites_metric
|
||||||
|
|
||||||
|
SHORT[true] : "J." "H." "N." "1/2 J."
|
||||||
|
SHORT[false] : "Journée" "Heure" "Nombre" "Demi-Journée"
|
||||||
|
|
||||||
|
inverse[false] : "demi" -> "1/2 J."
|
||||||
|
inverse[true] : "1/2 J." -> "demi"
|
||||||
|
|
||||||
|
|
||||||
|
Args:
|
||||||
|
metric (str): la métrique à traduire
|
||||||
|
inverse (bool, optional). Defaults to True.
|
||||||
|
short (bool, optional). Defaults to True.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: la métrique traduite
|
||||||
|
"""
|
||||||
|
index: int = None
|
||||||
|
if not inverse:
|
||||||
|
try:
|
||||||
|
index = AssiduitesMetrics.TAG.index(metric)
|
||||||
|
return (
|
||||||
|
AssiduitesMetrics.SHORT[index]
|
||||||
|
if short
|
||||||
|
else AssiduitesMetrics.LONG[index]
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
index = (
|
||||||
|
AssiduitesMetrics.SHORT.index(metric)
|
||||||
|
if short
|
||||||
|
else AssiduitesMetrics.LONG.index(metric)
|
||||||
|
)
|
||||||
|
return AssiduitesMetrics.TAG[index]
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# Types de modules
|
# Types de modules
|
||||||
class ModuleType(IntEnum):
|
class ModuleType(IntEnum):
|
||||||
"""Code des types de module."""
|
"""Code des types de module."""
|
||||||
@ -448,6 +668,13 @@ def AbsencesURL():
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def AssiduitesURL():
|
||||||
|
"""URL of Assiduités"""
|
||||||
|
return url_for("assiduites.index_html", scodoc_dept=g.scodoc_dept)[
|
||||||
|
: -len("/index_html")
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def UsersURL():
|
def UsersURL():
|
||||||
"""URL of Users
|
"""URL of Users
|
||||||
e.g. https://scodoc.xxx.fr/ScoDoc/DEPT/Scolarite/Users
|
e.g. https://scodoc.xxx.fr/ScoDoc/DEPT/Scolarite/Users
|
||||||
|
583
app/static/css/assiduites.css
Normal file
583
app/static/css/assiduites.css
Normal file
@ -0,0 +1,583 @@
|
|||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectors>* {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectors:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#validate_selectors {
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-display {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Gestion de la timeline === */
|
||||||
|
|
||||||
|
#tl_date {
|
||||||
|
visibility: hidden;
|
||||||
|
width: 0px;
|
||||||
|
height: 0px;
|
||||||
|
position: absolute;
|
||||||
|
left: 15%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.infos {
|
||||||
|
position: relative;
|
||||||
|
width: fit-content;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
align-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#datestr {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px #444 solid;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
min-width: 100px;
|
||||||
|
display: inline-block;
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tl_slider {
|
||||||
|
width: 90%;
|
||||||
|
cursor: grab;
|
||||||
|
|
||||||
|
/* visibility: hidden; */
|
||||||
|
}
|
||||||
|
|
||||||
|
#datestr,
|
||||||
|
#time {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-slider-handle.tl_handle {
|
||||||
|
background: none;
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
visibility: visible;
|
||||||
|
background-position: top;
|
||||||
|
background-size: cover;
|
||||||
|
border: none;
|
||||||
|
top: -180%;
|
||||||
|
cursor: grab;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#l_handle {
|
||||||
|
background-image: url(../icons/l_handle.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#r_handle {
|
||||||
|
background-image: url(../icons/r_handle.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-slider-range.ui-widget-header.ui-corner-all {
|
||||||
|
background-color: #F9C768;
|
||||||
|
background-image: none;
|
||||||
|
opacity: 0.50;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* === Gestion des etuds row === */
|
||||||
|
|
||||||
|
|
||||||
|
.etud_row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2% 20% 55% auto;
|
||||||
|
gap: 16px;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 4px 16px;
|
||||||
|
margin: 0.5% 0;
|
||||||
|
box-shadow: -1px 12px 5px -8px rgba(68, 68, 68, 0.61);
|
||||||
|
-webkit-box-shadow: -1px 12px 5px -8px rgba(68, 68, 68, 0.61);
|
||||||
|
-moz-box-shadow: -1px 12px 5px -8px rgba(68, 68, 68, 0.61);
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row * {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
height: 50px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row.def,
|
||||||
|
.etud_row.dem {
|
||||||
|
background-color: #c8c8c8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Index --- */
|
||||||
|
.etud_row .index_field {
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Nom étud --- */
|
||||||
|
.etud_row .name_field {
|
||||||
|
grid-column: 2;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row .name_field .name_set {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin: 0 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row.def .nom::after,
|
||||||
|
.tr.def .td.sticky span::after {
|
||||||
|
display: block;
|
||||||
|
content: " (Déf.)";
|
||||||
|
color: #d61616;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row.dem .nom::after,
|
||||||
|
.tr.dem .td.sticky span::after {
|
||||||
|
display: block;
|
||||||
|
content: " (Dém.)";
|
||||||
|
color: #d61616;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row .name_field .name_set * {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row .name_field .name_set h4 {
|
||||||
|
font-size: small;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row .name_field .name_set h5 {
|
||||||
|
font-size: x-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row .pdp {
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Barre assiduités --- */
|
||||||
|
.etud_row .assiduites_bar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 7px 1fr;
|
||||||
|
gap: 13px;
|
||||||
|
grid-column: 3;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.etud_row .assiduites_bar .filler {
|
||||||
|
height: 5px;
|
||||||
|
width: 90%;
|
||||||
|
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row .assiduites_bar #prevDateAssi {
|
||||||
|
height: 7px;
|
||||||
|
width: 7px;
|
||||||
|
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #444;
|
||||||
|
margin: 0px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row .assiduites_bar #prevDateAssi.single {
|
||||||
|
height: 9px;
|
||||||
|
width: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row.conflit {
|
||||||
|
background-color: #ff0000c2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row .assiduites_bar .absent,
|
||||||
|
.demo.absent {
|
||||||
|
background-color: #F1A69C !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row .assiduites_bar .present,
|
||||||
|
.demo.present {
|
||||||
|
background-color: #9CF1AF !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row .assiduites_bar .retard,
|
||||||
|
.demo.retard {
|
||||||
|
background-color: #F1D99C !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row .assiduites_bar .justified,
|
||||||
|
.demo.justified {
|
||||||
|
background-image: repeating-linear-gradient(135deg, transparent, transparent 4px, #7059FF 4px, #7059FF 8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row .assiduites_bar .invalid_justified,
|
||||||
|
.demo.invalid_justified {
|
||||||
|
background-image: repeating-linear-gradient(225deg, transparent, transparent 4px, #d61616 4px, #d61616 8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* --- Boutons assiduités --- */
|
||||||
|
.etud_row .btns_field {
|
||||||
|
grid-column: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btns_field:disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row .btns_field * {
|
||||||
|
margin: 0 5%;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbtn {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbtn::before {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbtn.present::before {
|
||||||
|
background-image: url(../icons/present.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbtn.absent::before {
|
||||||
|
background-image: url(../icons/absent.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbtn.aucun::before {
|
||||||
|
background-image: url(../icons/aucun.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbtn.retard::before {
|
||||||
|
background-image: url(../icons/retard.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbtn:checked:before {
|
||||||
|
outline: 3px solid #7059FF;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbtn:focus {
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*<== Modal conflit ==>*/
|
||||||
|
.modal {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 500;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: #fefefe;
|
||||||
|
margin: 5% auto;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #888;
|
||||||
|
width: 80%;
|
||||||
|
height: 320px;
|
||||||
|
position: relative;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.close {
|
||||||
|
color: #111;
|
||||||
|
position: absolute;
|
||||||
|
right: 5px;
|
||||||
|
top: 0px;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ajout de styles pour la frise chronologique */
|
||||||
|
.modal-timeline {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-labels,
|
||||||
|
.assiduites-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-label {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assiduite {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
z-index: 10;
|
||||||
|
height: 100px;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.assiduite-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assiduite-id,
|
||||||
|
.assiduite-period,
|
||||||
|
.assiduite-state,
|
||||||
|
.assiduite-user_id {
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assiduites-container {
|
||||||
|
min-height: 20px;
|
||||||
|
height: calc(50% - 60px);
|
||||||
|
/* Augmentation de la hauteur du conteneur des assiduités */
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
align-items: center;
|
||||||
|
height: 60px;
|
||||||
|
width: 100%;
|
||||||
|
bottom: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Ajout de la classe CSS pour la bordure en pointillés */
|
||||||
|
.assiduite.selected {
|
||||||
|
border: 2px dashed black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assiduite-special {
|
||||||
|
height: 120px;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 5;
|
||||||
|
border: 2px solid #000;
|
||||||
|
background-color: rgba(36, 36, 36, 0.25);
|
||||||
|
background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, rgba(81, 81, 81, 0.61) 5px, rgba(81, 81, 81, 0.61) 10px);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*<== Info sur l'assiduité sélectionnée ==>*/
|
||||||
|
.modal-assiduite-content {
|
||||||
|
background-color: #fefefe;
|
||||||
|
margin: 5% auto;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #888;
|
||||||
|
width: max-content;
|
||||||
|
position: relative;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.modal-assiduite-content.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-assiduite-content .infos {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*<=== Mass Action ==>*/
|
||||||
|
|
||||||
|
.mass-selection {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
margin: 2% 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mass-selection span {
|
||||||
|
margin: 0 1%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mass-selection .rbtn {
|
||||||
|
background-color: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*<== Loader ==> */
|
||||||
|
|
||||||
|
.loader-container {
|
||||||
|
display: none;
|
||||||
|
/* Cacher le loader par défaut */
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
/* Fond semi-transparent pour bloquer les clics */
|
||||||
|
z-index: 9999;
|
||||||
|
/* Placer le loader au-dessus de tout le contenu */
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
border: 6px solid #f3f3f3;
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top: 6px solid #3498db;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
animation: spin 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: translate(-50%, -50%) rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: translate(-50%, -50%) rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fieldsplit {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fieldsplit legend {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#page-assiduite-content {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5%;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#page-assiduite-content>* {
|
||||||
|
margin: 1.5% 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rouge {
|
||||||
|
color: crimson;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legende {
|
||||||
|
border: 1px dashed #333;
|
||||||
|
width: 75%;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order {
|
||||||
|
background-image: url(../icons/sort.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter {
|
||||||
|
background-image: url(../icons/filter.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[name='destroyFile'] {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
background-image: url(../icons/trash.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[name='destroyFile']:checked {
|
||||||
|
background-image: url(../icons/remove_circle.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: block;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
outline: none !important;
|
||||||
|
border: none !important;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0 2px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon:focus {
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#forcemodule {
|
||||||
|
border-radius: 8px;
|
||||||
|
background: crimson;
|
||||||
|
max-width: fit-content;
|
||||||
|
padding: 5px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo {
|
||||||
|
width: 23px;
|
||||||
|
height: 13px;
|
||||||
|
display: inline-block;
|
||||||
|
border: solid 1px #333;
|
||||||
|
}
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
div.jury_decisions_list div {
|
div.jury_decisions_list div {
|
||||||
font-size: 120%;
|
font-size: 120%;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
@ -79,7 +79,7 @@ div.competence {
|
|||||||
padding-left: calc(var(--arrow-width) + 8px);
|
padding-left: calc(var(--arrow-width) + 8px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.niveaux>div:not(:last-child)::after {
|
.niveaux>div:not(:last-child)::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -394,6 +394,7 @@ body.editionActivated .filtres .nonEditable .move {
|
|||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
#zoneChoix .autoAffectation>select {
|
#zoneChoix .autoAffectation>select {
|
||||||
@ -415,6 +416,23 @@ body.editionActivated .filtres .nonEditable .move {
|
|||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
}
|
}
|
||||||
|
#zoneChoix .autoAffectation .progress {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 4px;
|
||||||
|
background: #717171;
|
||||||
|
}
|
||||||
|
|
||||||
|
#zoneChoix .autoAffectation .progress>div {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: calc(100% * var(--nombre) / var(--reference));
|
||||||
|
bottom: 0;
|
||||||
|
background: #0c9;
|
||||||
|
}
|
||||||
|
|
||||||
#zoneChoix .etudiants>div {
|
#zoneChoix .etudiants>div {
|
||||||
background: #FFF;
|
background: #FFF;
|
||||||
|
@ -1328,6 +1328,13 @@ tr.etuddem td {
|
|||||||
color: rgb(100, 100, 100);
|
color: rgb(100, 100, 100);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
table.gt_table tr.etuddem td a {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
table.gt_table tr.etuddem td.etudinfo:first-child::after {
|
||||||
|
color: red;
|
||||||
|
content: " (dém.)";
|
||||||
|
}
|
||||||
|
|
||||||
td.etudabs,
|
td.etudabs,
|
||||||
td.etudabs a.discretelink,
|
td.etudabs a.discretelink,
|
||||||
@ -1785,6 +1792,10 @@ td.formsemestre_status_inscrits {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.formsemestre_status button {
|
||||||
|
margin-left: 12px;;
|
||||||
|
}
|
||||||
|
|
||||||
td.rcp_titre_sem a.jury_link {
|
td.rcp_titre_sem a.jury_link {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
color: red;
|
color: red;
|
||||||
@ -2461,6 +2472,12 @@ span.ue_type {
|
|||||||
margin-right: 1.5em;
|
margin-right: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table.formsemestre_description td.ue_coef_nul {
|
||||||
|
background-color: yellow!important;
|
||||||
|
color: red;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
ul.notes_module_list span.ue_coefs_list {
|
ul.notes_module_list span.ue_coefs_list {
|
||||||
color: blue;
|
color: blue;
|
||||||
font-size: 70%;
|
font-size: 70%;
|
||||||
|
11
app/static/icons/absent.svg
Executable file
11
app/static/icons/absent.svg
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
<svg width="85" height="85" viewBox="0 0 85 85" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="85" height="85" rx="15" fill="#F1A69C"/>
|
||||||
|
<g opacity="0.5" clip-path="url(#clip0_120_4425)">
|
||||||
|
<path d="M67.2116 70L43 45.707L18.7885 70L15.0809 66.3043L39.305 41.9995L15.0809 17.6939L18.7885 14L43 38.2922L67.2116 14L70.9191 17.6939L46.695 41.9995L70.9191 66.3043L67.2116 70Z" fill="black"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_120_4425">
|
||||||
|
<rect width="56" height="56" fill="white" transform="matrix(1 0 0 -1 15 70)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 547 B |
8
app/static/icons/aucun.svg
Executable file
8
app/static/icons/aucun.svg
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
<svg width="85" height="85" viewBox="0 0 85 85" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="85" height="85" rx="15" fill="#BBB"/>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_120_4425">
|
||||||
|
<rect width="56" height="56" fill="white" transform="matrix(1 0 0 -1 15 70)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 291 B |
1
app/static/icons/filter.svg
Normal file
1
app/static/icons/filter.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000"><path d="M4 3h16a1 1 0 011 1v1.586a1 1 0 01-.293.707l-6.415 6.414a1 1 0 00-.292.707v6.305a1 1 0 01-1.243.97l-2-.5a1 1 0 01-.757-.97v-5.805a1 1 0 00-.293-.707L3.292 6.293A1 1 0 013 5.586V4a1 1 0 011-1z" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>
|
After Width: | Height: | Size: 470 B |
13
app/static/icons/present.svg
Executable file
13
app/static/icons/present.svg
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
<svg width="85" height="85" viewBox="0 0 85 85" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="85" height="85" rx="15" fill="#9CF1AF"/>
|
||||||
|
<g clip-path="url(#clip0_120_4405)">
|
||||||
|
<g opacity="0.5">
|
||||||
|
<path d="M70.7713 27.5875L36.0497 62.3091C35.7438 62.6149 35.2487 62.6149 34.9435 62.3091L15.2286 42.5935C14.9235 42.2891 14.9235 41.7939 15.2286 41.488L20.0191 36.6976C20.3249 36.3924 20.8201 36.3924 21.1252 36.6976L35.4973 51.069L64.8754 21.6909C65.1819 21.3858 65.6757 21.3858 65.9815 21.6909L70.7713 26.4814C71.0771 26.7865 71.0771 27.281 70.7713 27.5875Z" fill="black"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_120_4405">
|
||||||
|
<rect width="56" height="56" fill="white" transform="translate(15 14)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 729 B |
1
app/static/icons/remove_circle.svg
Normal file
1
app/static/icons/remove_circle.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000"><path d="M9.172 14.828L12.001 12m2.828-2.828L12.001 12m0 0L9.172 9.172M12.001 12l2.828 2.828M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z" stroke="#fe4217" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>
|
After Width: | Height: | Size: 434 B |
12
app/static/icons/retard.svg
Executable file
12
app/static/icons/retard.svg
Executable file
@ -0,0 +1,12 @@
|
|||||||
|
<svg width="85" height="85" viewBox="0 0 85 85" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="85" height="85" rx="15" fill="#F1D99C"/>
|
||||||
|
<g opacity="0.5" clip-path="url(#clip0_120_4407)">
|
||||||
|
<path d="M55.2901 49.1836L44.1475 41.3918V28C44.1475 27.3688 43.6311 26.8524 43 26.8524C42.3688 26.8524 41.8524 27.3688 41.8524 28V42C41.8524 42.3787 42.036 42.7229 42.3459 42.941L53.9819 51.077C54.177 51.2147 54.4065 51.2836 54.636 51.2836C54.9918 51.2836 55.3475 51.1115 55.577 50.7787C55.9327 50.2623 55.8065 49.5508 55.2901 49.1836Z" fill="black"/>
|
||||||
|
<path d="M62.7836 22.2164C57.482 16.9148 50.459 14 43 14C35.541 14 28.518 16.9148 23.2164 22.2164C17.9148 27.518 15 34.541 15 42C15 49.459 17.9148 56.482 23.2164 61.7836C28.518 67.0852 35.541 70 43 70C50.459 70 57.482 67.0852 62.7836 61.7836C68.0852 56.482 71 49.459 71 42C71 34.541 68.0852 27.518 62.7836 22.2164ZM44.1475 67.682V63C44.1475 62.3689 43.6311 61.8525 43 61.8525C42.3689 61.8525 41.8525 62.3689 41.8525 63V67.682C28.5869 67.0967 17.9033 56.4131 17.318 43.1475H22C22.6311 43.1475 23.1475 42.6311 23.1475 42C23.1475 41.3689 22.6311 40.8525 22 40.8525H17.318C17.9033 27.5869 28.5869 16.9033 41.8525 16.318V21C41.8525 21.6311 42.3689 22.1475 43 22.1475C43.6311 22.1475 44.1475 21.6311 44.1475 21V16.318C57.4131 16.9033 68.0967 27.5869 68.682 40.8525H64C63.3689 40.8525 62.8525 41.3689 62.8525 42C62.8525 42.6311 63.3689 43.1475 64 43.1475H68.682C68.0967 56.4131 57.4131 67.0967 44.1475 67.682Z" fill="black"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_120_4407">
|
||||||
|
<rect width="56" height="56" fill="white" transform="translate(15 14)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user