forked from ScoDoc/ScoDoc
Update opolka/ScoDoc from ScoDoc/ScoDoc #2
26
.pylintrc
26
.pylintrc
@ -1,10 +1,24 @@
|
||||
|
||||
[MASTER]
|
||||
load-plugins=pylint_flask_sqlalchemy,pylint_flask
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
# pylint and black disagree...
|
||||
disable=bad-continuation
|
||||
# List of plugins (as comma separated values of python module names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=pylint_flask
|
||||
|
||||
[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
|
||||
|
||||
|
@ -5,7 +5,7 @@ from flask import Blueprint
|
||||
from flask import request, g
|
||||
from app import db
|
||||
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_web_bp = Blueprint("apiweb", __name__)
|
||||
@ -15,12 +15,24 @@ API_CLIENT_ERROR = 400 # erreur dans les paramètres fournis par le client
|
||||
|
||||
|
||||
@api_bp.errorhandler(ScoException)
|
||||
@api_web_bp.errorhandler(ScoException)
|
||||
@api_bp.errorhandler(404)
|
||||
def api_error_handler(e):
|
||||
"erreurs API => json"
|
||||
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):
|
||||
"""Extract required format from query string.
|
||||
* default value is json. A list of allowed formats may be provided
|
||||
@ -54,7 +66,6 @@ def get_model_api_object(model_cls: db.Model, model_id: int, join_cls: db.Model
|
||||
|
||||
from app.api import tokens
|
||||
from app.api import (
|
||||
absences,
|
||||
assiduites,
|
||||
billets_absences,
|
||||
departements,
|
||||
@ -65,6 +76,7 @@ from app.api import (
|
||||
jury,
|
||||
justificatifs,
|
||||
logos,
|
||||
moduleimpl,
|
||||
partitions,
|
||||
semset,
|
||||
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)
|
@ -14,6 +14,7 @@ from flask_login import current_user, login_required
|
||||
from app import db, log
|
||||
import app.scodoc.sco_assiduites as scass
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc import sco_preferences
|
||||
from app.api import api_bp as bp
|
||||
from app.api import api_web_bp, get_model_api_object, tools
|
||||
from app.decorators import permission_required, scodoc
|
||||
@ -25,6 +26,7 @@ from app.models import (
|
||||
Scolog,
|
||||
Justificatif,
|
||||
)
|
||||
from flask_sqlalchemy.query import Query
|
||||
from app.models.assiduites import get_assiduites_justif
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
@ -256,7 +258,7 @@ def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False)
|
||||
404,
|
||||
message="étudiant inconnu",
|
||||
)
|
||||
assiduites_query = etud.assiduites
|
||||
assiduites_query: Query = etud.assiduites
|
||||
|
||||
if with_query:
|
||||
assiduites_query = _filter_manager(request, assiduites_query)
|
||||
@ -372,7 +374,9 @@ def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
|
||||
if formsemestre is None:
|
||||
return json_error(404, "le paramètre 'formsemestre_id' n'existe pas")
|
||||
|
||||
assiduites_query = scass.filter_by_formsemestre(Assiduite.query,Assiduite, formsemestre)
|
||||
assiduites_query = scass.filter_by_formsemestre(
|
||||
Assiduite.query, Assiduite, formsemestre
|
||||
)
|
||||
|
||||
if with_query:
|
||||
assiduites_query = _filter_manager(request, assiduites_query)
|
||||
@ -597,8 +601,8 @@ def _create_singular(
|
||||
|
||||
desc: str = data.get("desc", None)
|
||||
|
||||
external_data = data.get("external_data", False)
|
||||
if external_data is not False:
|
||||
external_data = data.get("external_data", None)
|
||||
if external_data is not None:
|
||||
if not isinstance(external_data, dict):
|
||||
errors.append("param 'external_data' : n'est pas un objet JSON")
|
||||
|
||||
@ -959,7 +963,7 @@ def _count_manager(requested) -> tuple[str, dict]:
|
||||
return (metric, filtered)
|
||||
|
||||
|
||||
def _filter_manager(requested, assiduites_query: Assiduite):
|
||||
def _filter_manager(requested, assiduites_query: Query) -> Query:
|
||||
"""
|
||||
Retourne les assiduites entrées filtrées en fonction de la request
|
||||
"""
|
||||
@ -977,7 +981,7 @@ def _filter_manager(requested, assiduites_query: Assiduite):
|
||||
fin = scu.is_iso_formated(fin, True)
|
||||
|
||||
if (deb, fin) != (None, None):
|
||||
assiduites_query: Assiduite = scass.filter_by_date(
|
||||
assiduites_query: Query = scass.filter_by_date(
|
||||
assiduites_query, Assiduite, deb, fin
|
||||
)
|
||||
|
||||
@ -1015,11 +1019,11 @@ def _filter_manager(requested, assiduites_query: Assiduite):
|
||||
falses: tuple[str] = ("f", "faux", "false")
|
||||
|
||||
if est_just.lower() in trues:
|
||||
assiduites_query: Assiduite = scass.filter_assiduites_by_est_just(
|
||||
assiduites_query: Query = scass.filter_assiduites_by_est_just(
|
||||
assiduites_query, True
|
||||
)
|
||||
elif est_just.lower() in falses:
|
||||
assiduites_query: Assiduite = scass.filter_assiduites_by_est_just(
|
||||
assiduites_query: Query = scass.filter_assiduites_by_est_just(
|
||||
assiduites_query, False
|
||||
)
|
||||
|
||||
@ -1027,7 +1031,7 @@ def _filter_manager(requested, assiduites_query: Assiduite):
|
||||
|
||||
user_id = requested.args.get("user_id", False)
|
||||
if user_id is not False:
|
||||
assiduites_query: Assiduite = scass.filter_by_user_id(assiduites_query, user_id)
|
||||
assiduites_query: Query = scass.filter_by_user_id(assiduites_query, user_id)
|
||||
|
||||
return assiduites_query
|
||||
|
||||
|
@ -281,7 +281,15 @@ def dept_formsemestres_courants(acronym: str):
|
||||
FormSemestre.date_debut <= 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")
|
||||
|
@ -154,8 +154,6 @@ def get_photo_image(etudid: int = None, nip: str = None, ine: str = None):
|
||||
etudid : l'etudid de l'étudiant
|
||||
nip : le code nip de l'étudiant
|
||||
ine : le code ine de l'étudiant
|
||||
|
||||
Attention : Ne peut être qu'utilisée en tant que route de département
|
||||
"""
|
||||
|
||||
etud = tools.get_etud(etudid, nip, ine)
|
||||
@ -176,6 +174,44 @@ def get_photo_image(etudid: int = None, nip: str = None, ine: str = None):
|
||||
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/nip/<string:nip>", methods=["GET"])
|
||||
@bp.route("/etudiants/ine/<string:ine>", methods=["GET"])
|
||||
|
@ -7,17 +7,17 @@
|
||||
"""
|
||||
ScoDoc 9 API : accès aux évaluations
|
||||
"""
|
||||
|
||||
from flask import g, request
|
||||
from flask_json import as_json
|
||||
from flask_login import login_required
|
||||
from flask_login import current_user, login_required
|
||||
|
||||
import app
|
||||
|
||||
from app import log, db
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import Evaluation, ModuleImpl, FormSemestre
|
||||
from app.scodoc import sco_evaluation_db, sco_saisie_notes
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
@ -28,7 +28,7 @@ import app.scodoc.sco_utils as scu
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def evaluation(evaluation_id: int):
|
||||
def get_evaluation(evaluation_id: int):
|
||||
"""Description d'une évaluation.
|
||||
|
||||
{
|
||||
@ -47,7 +47,7 @@ def evaluation(evaluation_id: int):
|
||||
'UE1.3': 1.0
|
||||
},
|
||||
'publish_incomplete': False,
|
||||
'visi_bulletin': True
|
||||
'visibulletin': True
|
||||
}
|
||||
"""
|
||||
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(
|
||||
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,
|
||||
ApcParcours,
|
||||
Formation,
|
||||
FormSemestre,
|
||||
ModuleImpl,
|
||||
UniteEns,
|
||||
)
|
||||
from app.scodoc import sco_formations
|
||||
@ -249,54 +247,6 @@ def referentiel_competences(formation_id: int):
|
||||
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"])
|
||||
@api_web_bp.route("/set_ue_parcours/<int:ue_id>", methods=["POST"])
|
||||
@login_required
|
||||
|
@ -99,18 +99,20 @@ def formsemestre_infos(formsemestre_id: int):
|
||||
def formsemestres_query():
|
||||
"""
|
||||
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
|
||||
annee_scolaire : année de début de l'année scolaire
|
||||
dept_acronym : acronyme du département (eg "RT")
|
||||
dept_id : id du département
|
||||
ine ou nip: code d'un étudiant: ramène alors tous les semestres auxquels il est inscrit.
|
||||
etat: 0 si verrouillé, 1 sinon
|
||||
"""
|
||||
etape_apo = request.args.get("etape_apo")
|
||||
annee_scolaire = request.args.get("annee_scolaire")
|
||||
dept_acronym = request.args.get("dept_acronym")
|
||||
dept_id = request.args.get("dept_id")
|
||||
etat = request.args.get("etat")
|
||||
nip = request.args.get("nip")
|
||||
ine = request.args.get("ine")
|
||||
formsemestres = FormSemestre.query
|
||||
@ -126,6 +128,12 @@ def formsemestres_query():
|
||||
formsemestres = formsemestres.filter(
|
||||
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:
|
||||
formsemestres = formsemestres.join(Departement).filter_by(acronym=dept_acronym)
|
||||
if dept_id is not None:
|
||||
@ -151,7 +159,15 @@ def formsemestres_query():
|
||||
formsemestres = formsemestres.join(FormSemestreInscription).join(Identite)
|
||||
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")
|
||||
@ -196,7 +212,7 @@ def bulletins(formsemestre_id: int, version: str = "long"):
|
||||
@as_json
|
||||
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
|
||||
|
||||
|
@ -10,7 +10,7 @@
|
||||
|
||||
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_login import current_user, login_required
|
||||
|
||||
|
@ -26,6 +26,7 @@ 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
|
||||
@ -261,7 +262,7 @@ def _create_singular(
|
||||
# TOUT EST OK
|
||||
|
||||
try:
|
||||
nouv_justificatif: Justificatif = Justificatif.create_justificatif(
|
||||
nouv_justificatif: Query = Justificatif.create_justificatif(
|
||||
date_debut=deb,
|
||||
date_fin=fin,
|
||||
etat=etat,
|
||||
@ -307,7 +308,7 @@ def justif_edit(justif_id: int):
|
||||
"date_fin"?: str
|
||||
}
|
||||
"""
|
||||
justificatif_unique: Justificatif = Justificatif.query.filter_by(
|
||||
justificatif_unique: Query = Justificatif.query.filter_by(
|
||||
id=justif_id
|
||||
).first_or_404()
|
||||
|
||||
@ -426,9 +427,7 @@ def justif_delete():
|
||||
|
||||
|
||||
def _delete_singular(justif_id: int, database):
|
||||
justificatif_unique: Justificatif = Justificatif.query.filter_by(
|
||||
id=justif_id
|
||||
).first()
|
||||
justificatif_unique: Query = Justificatif.query.filter_by(id=justif_id).first()
|
||||
if justificatif_unique is None:
|
||||
return (404, "Justificatif non existant")
|
||||
|
||||
@ -470,7 +469,7 @@ def justif_import(justif_id: int = None):
|
||||
if file.filename == "":
|
||||
return json_error(404, "Il n'y a pas de fichier joint")
|
||||
|
||||
query = Justificatif.query.filter_by(id=justif_id)
|
||||
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)
|
||||
|
||||
@ -509,11 +508,11 @@ def justif_export(justif_id: int = None, filename: str = None):
|
||||
Retourne un fichier d'une archive d'un justificatif
|
||||
"""
|
||||
|
||||
query = Justificatif.query.filter_by(id=justif_id)
|
||||
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()
|
||||
justificatif_unique: Justificaitf = query.first_or_404()
|
||||
|
||||
archive_name: str = justificatif_unique.fichier
|
||||
if archive_name is None:
|
||||
@ -551,7 +550,7 @@ def justif_remove(justif_id: int = None):
|
||||
|
||||
data: dict = request.get_json(force=True)
|
||||
|
||||
query = Justificatif.query.filter_by(id=justif_id)
|
||||
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)
|
||||
|
||||
@ -604,7 +603,7 @@ def justif_list(justif_id: int = None):
|
||||
Liste les fichiers du justificatif
|
||||
"""
|
||||
|
||||
query = Justificatif.query.filter_by(id=justif_id)
|
||||
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)
|
||||
|
||||
@ -642,7 +641,7 @@ def justif_justifies(justif_id: int = None):
|
||||
Liste assiduite_id justifiées par le justificatif
|
||||
"""
|
||||
|
||||
query = Justificatif.query.filter_by(id=justif_id)
|
||||
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)
|
||||
|
||||
@ -676,13 +675,13 @@ def _filter_manager(requested, justificatifs_query):
|
||||
fin = scu.is_iso_formated(fin, True)
|
||||
|
||||
if (deb, fin) != (None, None):
|
||||
justificatifs_query: Justificatif = scass.filter_by_date(
|
||||
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: Justificatif = scass.filter_by_user_id(
|
||||
justificatifs_query: Query = scass.filter_by_user_id(
|
||||
justificatifs_query, user_id
|
||||
)
|
||||
|
||||
|
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)
|
@ -276,11 +276,10 @@ class BulletinBUT:
|
||||
"coef": fmt_note(e.coefficient)
|
||||
if e.evaluation_type == scu.EVALUATION_NORMALE
|
||||
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,
|
||||
"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": {
|
||||
"value": fmt_note(
|
||||
eval_notes[etud.id],
|
||||
@ -298,6 +297,12 @@ class BulletinBUT:
|
||||
)
|
||||
if has_request_context()
|
||||
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
|
||||
|
||||
|
@ -202,12 +202,11 @@ def bulletin_but_xml_compat(
|
||||
if e.visibulletin or version == "long":
|
||||
x_eval = Element(
|
||||
"evaluation",
|
||||
jour=e.jour.isoformat() if e.jour else "",
|
||||
heure_debut=e.heure_debut.isoformat()
|
||||
if e.heure_debut
|
||||
date_debut=e.date_debut.isoformat()
|
||||
if e.date_debut
|
||||
else "",
|
||||
heure_fin=e.heure_fin.isoformat()
|
||||
if e.heure_debut
|
||||
date_fin=e.date_fin.isoformat()
|
||||
if e.date_debut
|
||||
else "",
|
||||
coefficient=str(e.coefficient),
|
||||
# pas les poids en XML compat
|
||||
@ -215,6 +214,12 @@ def bulletin_but_xml_compat(
|
||||
description=quote_xml_attr(e.description),
|
||||
# notes envoyées sur 20, ceci juste pour garder trace:
|
||||
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)
|
||||
try:
|
||||
|
@ -76,6 +76,13 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
|
||||
f"""
|
||||
<div class="titre_niveaux">
|
||||
<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 class="but_explanation">{deca.explanation}</div>
|
||||
<div class="but_annee">
|
||||
|
@ -250,7 +250,7 @@ class ModuleImplResults:
|
||||
).reshape(-1, 1)
|
||||
|
||||
# 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"
|
||||
return [
|
||||
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:
|
||||
ects = ects_df.to_numpy()
|
||||
# ects est maintenant un array nb_etuds x nb_ues
|
||||
|
||||
moy_gen = (etud_moy_ue_df * ects).sum(axis=1) / ects.sum(axis=1)
|
||||
except ZeroDivisionError:
|
||||
# peut arriver si aucun module... on ignore
|
||||
moy_gen = pd.Series(np.NaN, index=etud_moy_ue_df.index)
|
||||
except TypeError:
|
||||
if None in ects:
|
||||
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):
|
||||
# Cas avec coefficients d'UE forcés: (on met à zéro l'UE bonus)
|
||||
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,
|
||||
columns=[ue.id for ue in ues],
|
||||
)
|
||||
|
@ -53,8 +53,8 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
self.store()
|
||||
t2 = time.time()
|
||||
log(
|
||||
f"""ResultatsSemestreBUT: cached formsemestre_id={formsemestre.id
|
||||
} ({(t1-t0):g}s +{(t2-t1):g}s)"""
|
||||
f"""+++ ResultatsSemestreBUT: cached [{formsemestre.id
|
||||
}] ({(t1-t0):g}s +{(t2-t1):g}s) +++"""
|
||||
)
|
||||
|
||||
def compute(self):
|
||||
|
@ -50,8 +50,8 @@ class ResultatsSemestreClassic(NotesTableCompat):
|
||||
self.store()
|
||||
t2 = time.time()
|
||||
log(
|
||||
f"""ResultatsSemestreClassic: cached formsemestre_id={
|
||||
formsemestre.id} ({(t1-t0):g}s +{(t2-t1):g}s)"""
|
||||
f"""+++ ResultatsSemestreClassic: cached formsemestre_id={
|
||||
formsemestre.id} ({(t1-t0):g}s +{(t2-t1):g}s) +++"""
|
||||
)
|
||||
# recalculé (aussi rapide que de les cacher)
|
||||
self.moy_min = self.etud_moy_gen.min()
|
||||
|
@ -80,8 +80,8 @@ class ResultatsSemestre(ResultatsCache):
|
||||
self.moy_gen_rangs_by_group = None # virtual
|
||||
self.modimpl_inscr_df: pd.DataFrame = None
|
||||
"Inscriptions: row etudid, col modimlpl_id"
|
||||
self.modimpls_results: ModuleImplResults = None
|
||||
"Résultats de chaque modimpl: dict { modimpl.id : ModuleImplResults(Classique ou BUT) }"
|
||||
self.modimpls_results: dict[int, ModuleImplResults] = None
|
||||
"Résultats de chaque modimpl (classique ou BUT)"
|
||||
self.etud_coef_ue_df = None
|
||||
"""coefs d'UE effectifs pour chaque étudiant (pour form. classiques)"""
|
||||
self.modimpl_coefs_df: pd.DataFrame = None
|
||||
@ -192,6 +192,17 @@ class ResultatsSemestre(ResultatsCache):
|
||||
*[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...
|
||||
def get_formsemestre_validations(self) -> ValidationsSemestre:
|
||||
"""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.aux_stats import StatsMoyenne
|
||||
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 import sco_utils as scu
|
||||
|
||||
@ -389,7 +395,7 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
"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
|
||||
de ce module.
|
||||
Évaluation "complete" ssi toutes notes saisies ou en attente.
|
||||
@ -398,34 +404,24 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
modimpl_results = self.modimpls_results.get(moduleimpl_id)
|
||||
if not modimpl_results:
|
||||
return [] # safeguard
|
||||
evals_results = []
|
||||
evaluations = []
|
||||
for e in modimpl.evaluations:
|
||||
if modimpl_results.evaluations_completes_dict.get(e.id, False):
|
||||
d = e.to_dict()
|
||||
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)
|
||||
evaluations.append(e)
|
||||
elif e.id not in modimpl_results.evaluations_completes_dict:
|
||||
# ne devrait pas arriver ? XXX
|
||||
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
|
||||
|
||||
if not hasattr(self, "_evaluations_etats"):
|
||||
|
@ -85,7 +85,9 @@ Adresses d'origine:
|
||||
)
|
||||
|
||||
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}
|
||||
"""
|
||||
)
|
||||
@ -98,7 +100,8 @@ def get_from_addr(dept_acronym: str = None):
|
||||
"""L'adresse "from" à utiliser pour envoyer un mail
|
||||
|
||||
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, la variable de config `SCODOC_MAIL_FROM`.
|
||||
"""
|
||||
|
@ -14,6 +14,8 @@ from app.scodoc.sco_utils import (
|
||||
localize_datetime,
|
||||
)
|
||||
|
||||
from flask_sqlalchemy.query import Query
|
||||
|
||||
|
||||
class Assiduite(db.Model):
|
||||
"""
|
||||
@ -124,7 +126,7 @@ class Assiduite(db.Model):
|
||||
) -> object or int:
|
||||
"""Créer une nouvelle assiduité pour l'étudiant"""
|
||||
# Vérification de non duplication des périodes
|
||||
assiduites: list[Assiduite] = etud.assiduites
|
||||
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)"
|
||||
@ -307,7 +309,7 @@ class Justificatif(db.Model):
|
||||
def is_period_conflicting(
|
||||
date_debut: datetime,
|
||||
date_fin: datetime,
|
||||
collection: list[Assiduite or Justificatif],
|
||||
collection: Query,
|
||||
collection_cls: Assiduite or Justificatif,
|
||||
) -> bool:
|
||||
"""
|
||||
|
@ -5,17 +5,28 @@
|
||||
import datetime
|
||||
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.events import ScolarNews
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
from app.models.notes import NotesNotes
|
||||
from app.models.ues import UniteEns
|
||||
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc import sco_cache
|
||||
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)
|
||||
|
||||
VALID_EVALUATION_TYPES = {0, 1, 2}
|
||||
|
||||
|
||||
class Evaluation(db.Model):
|
||||
"""Evaluation (contrôle, examen, ...)"""
|
||||
@ -27,15 +38,15 @@ class Evaluation(db.Model):
|
||||
moduleimpl_id = db.Column(
|
||||
db.Integer, db.ForeignKey("notes_moduleimpl.id"), index=True
|
||||
)
|
||||
jour = db.Column(db.Date)
|
||||
heure_debut = db.Column(db.Time)
|
||||
heure_fin = db.Column(db.Time)
|
||||
date_debut = db.Column(db.DateTime(timezone=True), nullable=True)
|
||||
date_fin = db.Column(db.DateTime(timezone=True), nullable=True)
|
||||
description = db.Column(db.Text)
|
||||
note_max = db.Column(db.Float)
|
||||
coefficient = db.Column(db.Float)
|
||||
visibulletin = db.Column(
|
||||
db.Boolean, nullable=False, default=True, server_default="true"
|
||||
)
|
||||
"visible sur les bulletins version intermédiaire"
|
||||
publish_incomplete = db.Column(
|
||||
db.Boolean, nullable=False, default=False, server_default="false"
|
||||
)
|
||||
@ -50,47 +61,108 @@ class Evaluation(db.Model):
|
||||
|
||||
def __repr__(self):
|
||||
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 ''}">"""
|
||||
|
||||
@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:
|
||||
"Représentation dict (riche, compat ScoDoc 7)"
|
||||
e = dict(self.__dict__)
|
||||
e.pop("_sa_instance_state", None)
|
||||
e_dict = dict(self.__dict__)
|
||||
e_dict.pop("_sa_instance_state", None)
|
||||
# ScoDoc7 output_formators
|
||||
e["evaluation_id"] = self.id
|
||||
e["jour"] = e["jour"].strftime("%d/%m/%Y") if e["jour"] else ""
|
||||
if self.jour is None:
|
||||
e["date_debut"] = None
|
||||
e["date_fin"] = None
|
||||
else:
|
||||
e["date_debut"] = datetime.datetime.combine(
|
||||
self.jour, self.heure_debut or datetime.time(0, 0)
|
||||
).isoformat()
|
||||
e["date_fin"] = datetime.datetime.combine(
|
||||
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)
|
||||
e_dict["evaluation_id"] = self.id
|
||||
e_dict["date_debut"] = self.date_debut.isoformat() if self.date_debut else None
|
||||
e_dict["date_fin"] = self.date_debut.isoformat() if self.date_fin else None
|
||||
e_dict["numero"] = self.numero or 0
|
||||
e_dict["poids"] = self.get_ue_poids_dict() # { ue_id : poids }
|
||||
|
||||
# Deprecated
|
||||
e_dict["jour"] = self.date_debut.strftime("%d/%m/%Y") if self.date_debut else ""
|
||||
|
||||
return evaluation_enrich_dict(self, e_dict)
|
||||
|
||||
def to_dict_api(self) -> dict:
|
||||
"Représentation dict pour API JSON"
|
||||
if self.jour is None:
|
||||
date_debut = None
|
||||
date_fin = None
|
||||
else:
|
||||
date_debut = datetime.datetime.combine(
|
||||
self.jour, self.heure_debut or datetime.time(0, 0)
|
||||
).isoformat()
|
||||
date_fin = datetime.datetime.combine(
|
||||
self.jour, self.heure_fin or datetime.time(0, 0)
|
||||
).isoformat()
|
||||
|
||||
return {
|
||||
"coefficient": self.coefficient,
|
||||
"date_debut": date_debut,
|
||||
"date_fin": date_fin,
|
||||
"date_debut": self.date_debut.isoformat() if self.date_debut else "",
|
||||
"date_fin": self.date_fin.isoformat() if self.date_fin else "",
|
||||
"description": self.description,
|
||||
"evaluation_type": self.evaluation_type,
|
||||
"id": self.id,
|
||||
@ -99,39 +171,135 @@ class Evaluation(db.Model):
|
||||
"numero": self.numero,
|
||||
"poids": self.get_ue_poids_dict(),
|
||||
"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):
|
||||
"""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():
|
||||
if k != "_sa_instance_state" and k != "id" and k in data:
|
||||
setattr(self, k, data[k])
|
||||
|
||||
@classmethod
|
||||
def get_max_numero(cls, moduleimpl_id: int) -> int:
|
||||
"""Return max numero among evaluations in this
|
||||
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
|
||||
):
|
||||
"""Renumber evaluations in this moduleimpl, according to their date. (numero=0: oldest one)
|
||||
Needed because previous versions of ScoDoc did not have eval numeros
|
||||
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"
|
||||
if self.heure_debut and (
|
||||
not self.heure_fin or self.heure_fin == self.heure_debut
|
||||
):
|
||||
return f"""à {self.heure_debut.strftime("%Hh%M")}"""
|
||||
elif self.heure_debut and self.heure_fin:
|
||||
return f"""de {self.heure_debut.strftime("%Hh%M")} à {self.heure_fin.strftime("%Hh%M")}"""
|
||||
"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:
|
||||
return ""
|
||||
|
||||
def descr_duree(self) -> str:
|
||||
"Description de la durée pour affichages"
|
||||
if self.heure_debut is None and self.heure_fin is None:
|
||||
"Description de la durée pour affichages ('3h' ou '2h30')"
|
||||
if self.date_debut is None or self.date_fin is None:
|
||||
return ""
|
||||
debut = self.heure_debut or DEFAULT_EVALUATION_TIME
|
||||
fin = self.heure_fin or DEFAULT_EVALUATION_TIME
|
||||
d = (fin.hour * 60 + fin.minute) - (debut.hour * 60 + debut.minute)
|
||||
duree = f"{d//60}h"
|
||||
if d % 60:
|
||||
duree += f"{d%60:02d}"
|
||||
minutes = (self.date_fin - self.date_debut).seconds // 60
|
||||
duree = f"{minutes // 60}h"
|
||||
minutes = minutes % 60
|
||||
if minutes != 0:
|
||||
duree += f"{minutes:02d}"
|
||||
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=()):
|
||||
"""Clone, not copying the given attrs
|
||||
Attention: la copie n'a pas d'id avant le prochain commit
|
||||
@ -146,19 +314,19 @@ class Evaluation(db.Model):
|
||||
return copy
|
||||
|
||||
def is_matin(self) -> bool:
|
||||
"Evaluation ayant lieu le matin (faux si pas de date)"
|
||||
heure_debut_dt = self.heure_debut or datetime.time(8, 00)
|
||||
# 8:00 au cas ou pas d'heure (note externe?)
|
||||
return bool(self.jour) and heure_debut_dt < datetime.time(12, 00)
|
||||
"Evaluation commençant le matin (faux si pas de date)"
|
||||
if not self.date_debut:
|
||||
return False
|
||||
return self.date_debut.time() < NOON
|
||||
|
||||
def is_apresmidi(self) -> bool:
|
||||
"Evaluation ayant lieu l'après midi (faux si pas de date)"
|
||||
heure_debut_dt = self.heure_debut or datetime.time(8, 00)
|
||||
# 8:00 au cas ou pas d'heure (note externe?)
|
||||
return bool(self.jour) and heure_debut_dt >= datetime.time(12, 00)
|
||||
"Evaluation commençant l'après midi (faux si pas de date)"
|
||||
if not self.date_debut:
|
||||
return False
|
||||
return self.date_debut.time() >= NOON
|
||||
|
||||
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.
|
||||
Les poids existants ne sont pas modifiés.
|
||||
Return True if (uncommited) modification, False otherwise.
|
||||
@ -191,6 +359,8 @@ class Evaluation(db.Model):
|
||||
L = []
|
||||
for ue_id, poids in ue_poids_dict.items():
|
||||
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)
|
||||
L.append(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}>"
|
||||
|
||||
|
||||
# Fonction héritée de ScoDoc7 à refactorer
|
||||
def evaluation_enrich_dict(e: dict):
|
||||
# Fonction héritée de ScoDoc7
|
||||
def evaluation_enrich_dict(e: Evaluation, e_dict: dict):
|
||||
"""add or convert some fields in an evaluation dict"""
|
||||
# For ScoDoc7 compat
|
||||
heure_debut_dt = e["heure_debut"] or datetime.time(
|
||||
8, 00
|
||||
) # au cas ou pas d'heure (note externe?)
|
||||
heure_fin_dt = e["heure_fin"] or datetime.time(8, 00)
|
||||
e["heure_debut"] = ndb.TimefromISO8601(e["heure_debut"])
|
||||
e["heure_fin"] = ndb.TimefromISO8601(e["heure_fin"])
|
||||
e["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"] = ""
|
||||
e_dict["heure_debut"] = e.date_debut.strftime("%Hh%M") if e.date_debut else ""
|
||||
e_dict["heure_fin"] = e.date_fin.strftime("%Hh%M") if e.date_fin else ""
|
||||
e_dict["jour_iso"] = e.date_debut.isoformat() if e.date_debut else ""
|
||||
# Calcule durée en minutes
|
||||
e_dict["descrheure"] = e.descr_heure()
|
||||
e_dict["descrduree"] = e.descr_duree()
|
||||
# matin, apresmidi: utile pour se referer aux absences:
|
||||
|
||||
if e["jour"] and heure_debut_dt < datetime.time(12, 00):
|
||||
e["matin"] = 1
|
||||
# note août 2023: si l'évaluation s'étend sur plusieurs jours,
|
||||
# cet indicateur n'a pas grand sens
|
||||
if e.date_debut and e.date_debut.time() < datetime.time(12, 00):
|
||||
e_dict["matin"] = 1
|
||||
else:
|
||||
e["matin"] = 0
|
||||
if e["jour"] and heure_fin_dt > datetime.time(12, 00):
|
||||
e["apresmidi"] = 1
|
||||
e_dict["matin"] = 0
|
||||
if e.date_fin and e.date_fin.time() > datetime.time(12, 00):
|
||||
e_dict["apresmidi"] = 1
|
||||
else:
|
||||
e["apresmidi"] = 0
|
||||
return e
|
||||
e_dict["apresmidi"] = 0
|
||||
return e_dict
|
||||
|
||||
|
||||
def check_evaluation_args(args):
|
||||
"Check coefficient, dates and duration, raises exception if invalid"
|
||||
moduleimpl_id = args["moduleimpl_id"]
|
||||
# check bareme
|
||||
note_max = args.get("note_max", None)
|
||||
if note_max is None:
|
||||
raise ScoValueError("missing note_max")
|
||||
def check_convert_evaluation_args(moduleimpl: ModuleImpl, data: dict):
|
||||
"""Check coefficient, dates and duration, raises exception if invalid.
|
||||
Convert date and time strings to date and time objects.
|
||||
|
||||
Set required default value for unspecified fields.
|
||||
May raise ScoValueError.
|
||||
"""
|
||||
# --- 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:
|
||||
note_max = float(note_max)
|
||||
except ValueError:
|
||||
raise ScoValueError("Invalid note_max value")
|
||||
except ValueError as exc:
|
||||
raise ScoValueError("invalid note_max value") from exc
|
||||
if note_max < 0:
|
||||
raise ScoValueError("Invalid note_max value (must be positive or null)")
|
||||
# check coefficient
|
||||
coef = args.get("coefficient", None)
|
||||
if coef is None:
|
||||
raise ScoValueError("missing coefficient")
|
||||
raise ScoValueError("invalid note_max value (must be positive or null)")
|
||||
data["note_max"] = note_max
|
||||
# --- coefficient
|
||||
coef = data.get("coefficient", 1.0) or 1.0
|
||||
try:
|
||||
coef = float(coef)
|
||||
except ValueError:
|
||||
raise ScoValueError("Invalid coefficient value")
|
||||
except ValueError as exc:
|
||||
raise ScoValueError("invalid coefficient value") from exc
|
||||
if coef < 0:
|
||||
raise ScoValueError("Invalid coefficient value (must be positive or null)")
|
||||
# check date
|
||||
jour = args.get("jour", None)
|
||||
args["jour"] = jour
|
||||
if jour:
|
||||
modimpl = db.session.get(ModuleImpl, moduleimpl_id)
|
||||
formsemestre = modimpl.formsemestre
|
||||
y, m, d = [int(x) for x in ndb.DateDMYtoISO(jour).split("-")]
|
||||
jour = datetime.date(y, m, d)
|
||||
if (jour > formsemestre.date_fin) or (jour < formsemestre.date_debut):
|
||||
raise ScoValueError("invalid coefficient value (must be positive or null)")
|
||||
data["coefficient"] = coef
|
||||
# --- date de l'évaluation
|
||||
formsemestre = moduleimpl.formsemestre
|
||||
date_debut = data.get("date_debut", None)
|
||||
if date_debut:
|
||||
if isinstance(date_debut, str):
|
||||
data["date_debut"] = datetime.datetime.fromisoformat(date_debut)
|
||||
if data["date_debut"].tzinfo is None:
|
||||
data["date_debut"] = scu.TIME_ZONE.localize(data["date_debut"])
|
||||
if not (
|
||||
formsemestre.date_debut
|
||||
<= data["date_debut"].date()
|
||||
<= formsemestre.date_fin
|
||||
):
|
||||
raise ScoValueError(
|
||||
"La date de l'évaluation (%s/%s/%s) n'est pas dans le semestre !"
|
||||
% (d, m, y),
|
||||
f"""La date de début de l'évaluation ({
|
||||
data["date_debut"].strftime("%d/%m/%Y")
|
||||
}) n'est pas dans le semestre !""",
|
||||
dest_url="javascript:history.back();",
|
||||
)
|
||||
heure_debut = args.get("heure_debut", None)
|
||||
args["heure_debut"] = heure_debut
|
||||
heure_fin = args.get("heure_fin", None)
|
||||
args["heure_fin"] = heure_fin
|
||||
if jour and ((not heure_debut) or (not heure_fin)):
|
||||
raise ScoValueError("Les heures doivent être précisées")
|
||||
d = ndb.TimeDuration(heure_debut, heure_fin)
|
||||
if d and ((d < 0) or (d > 60 * 12)):
|
||||
raise ScoValueError("Heures de l'évaluation incohérentes !")
|
||||
date_fin = data.get("date_fin", None)
|
||||
if date_fin:
|
||||
if isinstance(date_fin, str):
|
||||
data["date_fin"] = datetime.datetime.fromisoformat(date_fin)
|
||||
if data["date_fin"].tzinfo is None:
|
||||
data["date_fin"] = scu.TIME_ZONE.localize(data["date_fin"])
|
||||
if not (
|
||||
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 !")
|
||||
# # --- 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.etudiants import Identite
|
||||
from app.models.evaluations import Evaluation
|
||||
from app.models.formations import Formation
|
||||
from app.models.groups import GroupDescr, Partition
|
||||
from app.models.moduleimpls import ModuleImpl, ModuleImplInscription
|
||||
@ -350,6 +351,21 @@ class FormSemestre(db.Model):
|
||||
_cache[key] = 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
|
||||
def modimpls_sorted(self) -> list[ModuleImpl]:
|
||||
"""Liste des modimpls du semestre (y compris bonus)
|
||||
|
@ -101,6 +101,23 @@ class ModuleImpl(db.Model):
|
||||
d.pop("module", None)
|
||||
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:
|
||||
"""Check if user can modify module resp.
|
||||
If raise_exc, raises exception (AccessDenied or ScoLockedSemError) if not.
|
||||
|
@ -153,6 +153,10 @@ class Module(db.Model):
|
||||
"""
|
||||
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:
|
||||
"""Clé de tri pour avoir
|
||||
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.
|
||||
<br>
|
||||
De nombreux aspects sont paramétrables:
|
||||
<a href="https://scodoc.org/AvisPoursuiteEtudes" target="_blank" rel="noopener">
|
||||
voir la documentation</a>.
|
||||
<a href="https://scodoc.org/AvisPoursuiteEtudes"
|
||||
target="_blank" rel="noopener noreferrer">
|
||||
voir la documentation
|
||||
</a>.
|
||||
</p>
|
||||
<form method="post" action="pe_view_sem_recap" id="pe_view_sem_recap_form"
|
||||
enctype="multipart/form-data">
|
||||
|
@ -152,8 +152,10 @@ def sidebar(etudid: int = None):
|
||||
# Logo
|
||||
H.append(
|
||||
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>
|
||||
<a href="{ scu.SCO_USER_MANUAL }" target="_blank" class="sidebar">Aide</a>
|
||||
<div class="sidebar-bottom"><a href="{
|
||||
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 class="logo-logo">
|
||||
<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):
|
||||
im = PILImage.open(img_file)
|
||||
im.thumbnail((maxx, maxy), PILImage.ANTIALIAS)
|
||||
im.thumbnail((maxx, maxy), PILImage.LANCZOS)
|
||||
out_file_str = io.BytesIO()
|
||||
im.save(out_file_str, im.format)
|
||||
out_file_str.seek(0)
|
||||
@ -20,7 +20,7 @@ def ImageScaleH(img_file, W=None, H=90):
|
||||
if W is None:
|
||||
# keep aspect
|
||||
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()
|
||||
im.save(out_file_str, im.format)
|
||||
out_file_str.seek(0)
|
||||
|
@ -1,19 +1,17 @@
|
||||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import datetime
|
||||
import html
|
||||
import traceback
|
||||
|
||||
from flask import g, current_app, abort
|
||||
import psycopg2
|
||||
import psycopg2.pool
|
||||
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.scodoc.sco_exceptions import ScoException, ScoValueError, NoteProcessError
|
||||
import datetime
|
||||
|
||||
quote_html = html.escape
|
||||
|
||||
@ -459,8 +457,10 @@ def dictfilter(d, fields, filter_nulls=True):
|
||||
# --- Misc Tools
|
||||
|
||||
|
||||
def DateDMYtoISO(dmy: str, null_is_empty=False) -> str:
|
||||
"convert date string from french format to ISO"
|
||||
def DateDMYtoISO(dmy: str, null_is_empty=False) -> str: # XXX deprecated
|
||||
"""Convert date string from french format to ISO.
|
||||
If null_is_empty (default false), returns "" if no input.
|
||||
"""
|
||||
if not dmy:
|
||||
if null_is_empty:
|
||||
return ""
|
||||
@ -506,7 +506,7 @@ def DateISOtoDMY(isodate):
|
||||
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)"
|
||||
if isinstance(t, datetime.time):
|
||||
return t.isoformat()
|
||||
@ -518,7 +518,7 @@ def TimetoISO8601(t, null_is_empty=False):
|
||||
return t
|
||||
|
||||
|
||||
def TimefromISO8601(t):
|
||||
def TimefromISO8601(t) -> str:
|
||||
"convert time string from ISO 8601 to our display format"
|
||||
if not t:
|
||||
return t
|
||||
@ -532,19 +532,6 @@ def TimefromISO8601(t):
|
||||
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):
|
||||
if x is None or x == "":
|
||||
return 0.0
|
||||
|
@ -318,7 +318,7 @@ def list_abs_in_range(
|
||||
Returns:
|
||||
List of absences
|
||||
"""
|
||||
if matin != None:
|
||||
if matin is not None:
|
||||
matin = _toboolean(matin)
|
||||
ismatin = " AND A.MATIN = %(matin)s "
|
||||
else:
|
||||
@ -387,7 +387,7 @@ def count_abs_just(etudid, debut, fin, matin=None, moduleimpl_id=None) -> int:
|
||||
Returns:
|
||||
An integer.
|
||||
"""
|
||||
if matin != None:
|
||||
if matin is not None:
|
||||
matin = _toboolean(matin)
|
||||
ismatin = " AND A.MATIN = %(matin)s "
|
||||
else:
|
||||
@ -482,7 +482,9 @@ def _get_abs_description(a, cursor=None):
|
||||
else:
|
||||
a["matin"] = False
|
||||
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 = cursor.dictfetchall()
|
||||
@ -507,7 +509,7 @@ def _get_abs_description(a, cursor=None):
|
||||
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.
|
||||
is_abs: None (peu importe), True, False
|
||||
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
|
||||
WHERE A.jour = %(date)s
|
||||
"""
|
||||
if is_abs != None:
|
||||
if is_abs is not None:
|
||||
req += " AND A.estabs = %(is_abs)s"
|
||||
if is_just != None:
|
||||
if is_just is not None:
|
||||
req += " AND A.estjust = %(is_just)s"
|
||||
if not am:
|
||||
req += " AND NOT matin "
|
||||
@ -533,7 +535,7 @@ WHERE A.jour = %(date)s
|
||||
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"
|
||||
cnx = ndb.GetDBConnexion()
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
@ -883,7 +885,7 @@ def MonthTableBody(
|
||||
descr = ev[4]
|
||||
#
|
||||
cc = []
|
||||
if color != None:
|
||||
if color is not None:
|
||||
cc.append('<td bgcolor="%s" class="calcell">' % color)
|
||||
else:
|
||||
cc.append('<td class="calcell">')
|
||||
@ -896,7 +898,7 @@ def MonthTableBody(
|
||||
cc.append("<a %s %s>" % (href, descr))
|
||||
|
||||
if legend or d == 1:
|
||||
if pad_width != None:
|
||||
if pad_width is not None:
|
||||
n = pad_width - len(legend) # pad to 8 cars
|
||||
if n > 0:
|
||||
legend = (
|
||||
@ -959,7 +961,7 @@ def MonthTableBody(
|
||||
ev_year = int(ev[0][:4])
|
||||
ev_month = int(ev[0][5:7])
|
||||
ev_day = int(ev[0][8:10])
|
||||
if ev[4] != None:
|
||||
if ev[4] is not None:
|
||||
ev_half = int(ev[4])
|
||||
else:
|
||||
ev_half = 0
|
||||
@ -978,7 +980,7 @@ def MonthTableBody(
|
||||
if len(ev) > 5 and ev[5]:
|
||||
descr = ev[5]
|
||||
#
|
||||
if color != None:
|
||||
if color is not None:
|
||||
cc.append('<td bgcolor="%s" class="calcell">' % (color))
|
||||
else:
|
||||
cc.append('<td class="calcell">')
|
||||
@ -1072,7 +1074,8 @@ def invalidate_abs_count_sem(sem):
|
||||
|
||||
|
||||
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
|
||||
date: date au format ISO
|
||||
"""
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -34,6 +34,7 @@ from flask import flash, render_template, url_for
|
||||
from flask import g, request
|
||||
from flask_login import current_user
|
||||
|
||||
from app.models import Identite
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc import sco_import_etuds
|
||||
from app.scodoc import sco_groups
|
||||
@ -351,10 +352,8 @@ def etudarchive_import_files(
|
||||
):
|
||||
"Importe des fichiers"
|
||||
|
||||
def callback(etud, data, filename):
|
||||
return _store_etud_file_to_new_archive(
|
||||
etud["etudid"], data, filename, description
|
||||
)
|
||||
def callback(etud: Identite, data, filename):
|
||||
return _store_etud_file_to_new_archive(etud.id, data, filename, description)
|
||||
|
||||
# Utilise la fontion developpée au depart pour les photos
|
||||
(
|
||||
|
@ -13,6 +13,7 @@ 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:
|
||||
@ -167,7 +168,7 @@ class CountCalculator:
|
||||
|
||||
self.hours += delta.total_seconds() / 3600
|
||||
|
||||
def to_dict(self) -> dict[str, object]:
|
||||
def to_dict(self) -> dict[str, int or float]:
|
||||
"""Retourne les métriques sous la forme d'un dictionnaire"""
|
||||
return {
|
||||
"compte": self.count,
|
||||
@ -178,8 +179,8 @@ class CountCalculator:
|
||||
|
||||
|
||||
def get_assiduites_stats(
|
||||
assiduites: Assiduite, metric: str = "all", filtered: dict[str, object] = None
|
||||
) -> Assiduite:
|
||||
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:
|
||||
@ -218,7 +219,7 @@ def get_assiduites_stats(
|
||||
return output if output else count
|
||||
|
||||
|
||||
def filter_assiduites_by_etat(assiduites: Assiduite, etat: str) -> Assiduite:
|
||||
def filter_assiduites_by_etat(assiduites: Assiduite, etat: str) -> Query:
|
||||
"""
|
||||
Filtrage d'une collection d'assiduites en fonction de leur état
|
||||
"""
|
||||
@ -227,9 +228,7 @@ def filter_assiduites_by_etat(assiduites: Assiduite, etat: str) -> Assiduite:
|
||||
return assiduites.filter(Assiduite.etat.in_(etats))
|
||||
|
||||
|
||||
def filter_assiduites_by_est_just(
|
||||
assiduites: Assiduite, est_just: bool
|
||||
) -> Justificatif:
|
||||
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
|
||||
"""
|
||||
@ -239,7 +238,7 @@ def filter_assiduites_by_est_just(
|
||||
def filter_by_user_id(
|
||||
collection: Assiduite or Justificatif,
|
||||
user_id: int,
|
||||
) -> Justificatif:
|
||||
) -> Query:
|
||||
"""
|
||||
Filtrage d'une collection en fonction de l'user_id
|
||||
"""
|
||||
@ -252,7 +251,7 @@ def filter_by_date(
|
||||
date_deb: datetime = None,
|
||||
date_fin: datetime = None,
|
||||
strict: bool = False,
|
||||
):
|
||||
) -> Query:
|
||||
"""
|
||||
Filtrage d'une collection d'assiduites en fonction d'une date
|
||||
"""
|
||||
@ -272,9 +271,7 @@ def filter_by_date(
|
||||
)
|
||||
|
||||
|
||||
def filter_justificatifs_by_etat(
|
||||
justificatifs: Justificatif, etat: str
|
||||
) -> Justificatif:
|
||||
def filter_justificatifs_by_etat(justificatifs: Justificatif, etat: str) -> Query:
|
||||
"""
|
||||
Filtrage d'une collection de justificatifs en fonction de leur état
|
||||
"""
|
||||
@ -283,9 +280,7 @@ def filter_justificatifs_by_etat(
|
||||
return justificatifs.filter(Justificatif.etat.in_(etats))
|
||||
|
||||
|
||||
def filter_by_module_impl(
|
||||
assiduites: Assiduite, module_impl_id: int or None
|
||||
) -> Assiduite:
|
||||
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
|
||||
"""
|
||||
@ -296,7 +291,7 @@ 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
|
||||
"""
|
||||
@ -323,12 +318,13 @@ def filter_by_formsemestre(
|
||||
return collection_result.filter(collection_class.date_fin <= form_date_fin)
|
||||
|
||||
|
||||
def justifies(justi: Justificatif, obj: bool = False) -> list[int]:
|
||||
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
|
||||
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:
|
||||
@ -347,7 +343,7 @@ def justifies(justi: Justificatif, obj: bool = False) -> list[int]:
|
||||
|
||||
def get_all_justified(
|
||||
etudid: int, date_deb: datetime = None, date_fin: datetime = None
|
||||
) -> list[Assiduite]:
|
||||
) -> Query:
|
||||
"""Retourne toutes les assiduités justifiées sur une période"""
|
||||
|
||||
if date_deb is None:
|
||||
@ -368,7 +364,7 @@ def get_all_justified(
|
||||
|
||||
|
||||
# Gestion du cache
|
||||
def get_assiduites_count(etudid, sem):
|
||||
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.
|
||||
@ -382,25 +378,43 @@ def get_assiduites_count(etudid, sem):
|
||||
)
|
||||
|
||||
|
||||
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, date_fin_iso, metrique="demi"
|
||||
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.
|
||||
"""
|
||||
key = (
|
||||
str(etudid)
|
||||
+ "_"
|
||||
+ date_debut_iso
|
||||
+ "_"
|
||||
+ date_fin_iso
|
||||
+ f"{metrique}_assiduites"
|
||||
)
|
||||
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.datetime = scu.is_iso_formated(date_debut_iso, True)
|
||||
date_fin: datetime.datetime = scu.is_iso_formated(date_fin_iso, True)
|
||||
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)
|
||||
@ -432,7 +446,7 @@ 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 ["demi", "journee", "compte", "heure"]:
|
||||
for met in scu.AssiduitesMetrics.TAG:
|
||||
key = str(etudid) + "_" + date_debut + "_" + date_fin + f"{met}_assiduites"
|
||||
sco_cache.AbsSemEtudCache.delete(key)
|
||||
|
||||
@ -449,9 +463,9 @@ def invalidate_assiduites_count_sem(sem):
|
||||
|
||||
|
||||
def invalidate_assiduites_etud_date(etudid, date: datetime):
|
||||
"""Doit etre appelé à chaque modification des assiduites pour cet étudiant et cette date.
|
||||
"""Doit etre appelé à chaque modification des assiduites
|
||||
pour cet étudiant et cette date.
|
||||
Invalide cache absence et caches semestre
|
||||
date: date au format ISO
|
||||
"""
|
||||
from app.scodoc import sco_compute_moy
|
||||
|
||||
|
@ -47,6 +47,7 @@ from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import (
|
||||
ApcParcours,
|
||||
Evaluation,
|
||||
Formation,
|
||||
FormSemestre,
|
||||
Identite,
|
||||
@ -57,14 +58,12 @@ from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import htmlutils
|
||||
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_json
|
||||
from app.scodoc import sco_bulletins_pdf
|
||||
from app.scodoc import sco_bulletins_xml
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_preferences
|
||||
@ -482,6 +481,7 @@ def _ue_mod_bulletin(
|
||||
mods = [] # result
|
||||
ue_attente = False # true si une eval en attente dans cette UE
|
||||
for modimpl in ue_modimpls:
|
||||
modimpl_results = nt.modimpls_results.get(modimpl["moduleimpl_id"])
|
||||
mod_attente = False
|
||||
mod = modimpl.copy()
|
||||
mod_moy = nt.get_etud_mod_moy(
|
||||
@ -531,10 +531,13 @@ def _ue_mod_bulletin(
|
||||
scu.fmt_coef(modimpl["module"]["coefficient"]),
|
||||
sco_users.user_info(modimpl["responsable_id"])["nomcomplet"],
|
||||
)
|
||||
link_mod = (
|
||||
'<a class="bull_link" href="moduleimpl_status?moduleimpl_id=%s" title="%s">'
|
||||
% (modimpl["moduleimpl_id"], mod["mod_descr_txt"])
|
||||
)
|
||||
link_mod = f"""<a class="bull_link" href="{
|
||||
url_for("notes.moduleimpl_status",
|
||||
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):
|
||||
mod["code"] = modimpl["module"]["code"]
|
||||
mod["code_html"] = link_mod + (mod["code"] or "") + "</a>"
|
||||
@ -561,91 +564,88 @@ def _ue_mod_bulletin(
|
||||
mod["code_txt"] = ""
|
||||
mod["code_html"] = ""
|
||||
# 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"] = []
|
||||
for e in evals:
|
||||
e = e.copy()
|
||||
if e["visibulletin"] or version == "long":
|
||||
# affiche "bonus" quand les points de malus sont négatifs
|
||||
if is_malus:
|
||||
val = e["notes"].get(etudid, {"value": "NP"})[
|
||||
"value"
|
||||
] # NA si etud demissionnaire
|
||||
if val == "NP" or val > 0:
|
||||
e["name"] = "Points de malus sur cette UE"
|
||||
else:
|
||||
e["name"] = "Points de bonus sur cette UE"
|
||||
mod["evaluations_incompletes"] = []
|
||||
complete_eval_ids = {e.id for e in evaluations_completes}
|
||||
all_evals: list[Evaluation] = Evaluation.query.filter_by(
|
||||
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 val == "NP":
|
||||
e_dict["name"] = "Points de bonus/malus sur cette UE"
|
||||
elif val > 0:
|
||||
e_dict["name"] = "Points de malus sur cette UE"
|
||||
else:
|
||||
e["name"] = e["description"] or f"le {e['jour']}"
|
||||
e["target_html"] = url_for(
|
||||
e_dict["name"] = "Points de bonus sur cette UE"
|
||||
else:
|
||||
e_dict[
|
||||
"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",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
evaluation_id=e["evaluation_id"],
|
||||
evaluation_id=e.id,
|
||||
format="html",
|
||||
tf_submitted=1,
|
||||
)
|
||||
e[
|
||||
e_dict[
|
||||
"name_html"
|
||||
] = f"""<a class="bull_link" href="{
|
||||
e['target_html']}">{e['name']}</a>"""
|
||||
val = e["notes"].get(etudid, {"value": "NP"})["value"]
|
||||
# val est NP si etud demissionnaire
|
||||
if val == "NP":
|
||||
e["note_txt"] = "nd"
|
||||
e["note_html"] = '<span class="note_nd">nd</span>'
|
||||
e["coef_txt"] = scu.fmt_coef(e["coefficient"])
|
||||
else:
|
||||
# (-0.15) s'affiche "bonus de 0.15"
|
||||
if is_malus:
|
||||
val = abs(val)
|
||||
e["note_txt"] = scu.fmt_note(val, note_max=e["note_max"])
|
||||
e["note_html"] = e["note_txt"]
|
||||
if is_malus:
|
||||
e["coef_txt"] = ""
|
||||
e_dict['target_html']}">{e_dict['name']}</a>"""
|
||||
if is_complete: # évaluation complète
|
||||
# val est NP si etud demissionnaire
|
||||
if val == "NP":
|
||||
e_dict["note_txt"] = "nd"
|
||||
e_dict["note_html"] = '<span class="note_nd">nd</span>'
|
||||
e_dict["coef_txt"] = scu.fmt_coef(e["coefficient"])
|
||||
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"]:
|
||||
# (-0.15) s'affiche "bonus de 0.15"
|
||||
if is_malus:
|
||||
val = abs(val)
|
||||
e_dict["note_txt"] = e_dict["note_html"] = scu.fmt_note(
|
||||
val, note_max=e.note_max
|
||||
)
|
||||
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)
|
||||
)
|
||||
(e.evaluation_type == scu.EVALUATION_NORMALE or not np.isnan(val))
|
||||
):
|
||||
# ne liste pas les eval malus sans notes
|
||||
# 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
|
||||
if (
|
||||
bul_show_mod_rangs
|
||||
@ -1114,9 +1114,10 @@ def mail_bulletin(formsemestre_id, infos, pdfdata, filename, recipient_addr):
|
||||
hea = ""
|
||||
|
||||
if sco_preferences.get_preference("bul_mail_list_abs"):
|
||||
hea += "\n\n" + sco_abs_views.ListeAbsEtud(
|
||||
etud["etudid"], with_evals=False, format="text"
|
||||
)
|
||||
hea += "\n\n" + "(LISTE D'ABSENCES NON DISPONIBLE)" # XXX TODO-ASSIDUITE
|
||||
# sco_abs_views.ListeAbsEtud(
|
||||
# etud["etudid"], with_evals=False, format="text"
|
||||
# )
|
||||
|
||||
subject = f"""Relevé de notes de {etud["nomprenom"]}"""
|
||||
recipients = [recipient_addr]
|
||||
|
@ -37,7 +37,7 @@ from app import db, ScoDocJSONEncoder
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
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.formsemestre import FormSemestre
|
||||
|
||||
@ -46,7 +46,6 @@ import app.scodoc.notesdb as ndb
|
||||
from app.scodoc import sco_assiduites
|
||||
from app.scodoc import sco_edit_ue
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_photos
|
||||
@ -112,7 +111,7 @@ def formsemestre_bulletinetud_published_dict(
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
|
||||
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")
|
||||
d = {"type": "classic", "version": "0"}
|
||||
if (not sem["bul_hide_xml"]) or force_publishing:
|
||||
@ -324,7 +323,7 @@ def formsemestre_bulletinetud_published_dict(
|
||||
def _list_modimpls(
|
||||
nt: NotesTableCompat,
|
||||
etudid: int,
|
||||
modimpls: list[ModuleImpl],
|
||||
modimpls: list[dict],
|
||||
prefs: SemPreferences,
|
||||
version: str,
|
||||
) -> list[dict]:
|
||||
@ -333,6 +332,7 @@ def _list_modimpls(
|
||||
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
|
||||
continue
|
||||
modimpl_results = nt.modimpls_results.get(modimpl["moduleimpl_id"])
|
||||
mod = modimpl["module"]
|
||||
# if mod['ects'] is None:
|
||||
# ects = ''
|
||||
@ -363,61 +363,42 @@ def _list_modimpls(
|
||||
mod_dict["effectif"] = dict(value=nt.mod_rangs[modimpl["moduleimpl_id"]][1])
|
||||
|
||||
# --- 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"] = []
|
||||
if version != "short":
|
||||
for e in evals:
|
||||
if e["visibulletin"] or version == "long":
|
||||
val = e["notes"].get(etudid, {"value": "NP"})["value"]
|
||||
for e in evaluations_completes:
|
||||
if e.visibulletin or version == "long":
|
||||
# Note à l'évaluation:
|
||||
val = modimpl_results.evals_notes[e.id].get(etudid, "NP")
|
||||
# nb: val est NA si etud démissionnaire
|
||||
val = scu.fmt_note(val, note_max=e["note_max"])
|
||||
eval_dict = dict(
|
||||
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"]
|
||||
e_dict = e.to_dict_bul()
|
||||
e_dict["note"] = scu.fmt_note(val, note_max=e.note_max)
|
||||
|
||||
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:
|
||||
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"]:
|
||||
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:
|
||||
mod_dict["evaluation"].append(
|
||||
dict(
|
||||
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"],
|
||||
description=quote_xml_attr(e["description"]),
|
||||
incomplete="1",
|
||||
)
|
||||
)
|
||||
evaluations: list[Evaluation] = Evaluation.query.filter_by(
|
||||
moduleimpl_id=modimpl["moduleimpl_id"]
|
||||
).order_by(Evaluation.date_debut)
|
||||
# plus ancienne d'abord
|
||||
for e in evaluations:
|
||||
if e.id not in complete_eval_ids:
|
||||
e_dict = e.to_dict_bul()
|
||||
e_dict["incomplete"] = 1
|
||||
mod_dict["evaluation"].append(e_dict)
|
||||
modules_dict.append(mod_dict)
|
||||
return modules_dict
|
||||
|
||||
|
@ -132,10 +132,13 @@ class BulletinGeneratorLegacy(sco_bulletins_generator.BulletinGenerator):
|
||||
if sco_preferences.get_preference(
|
||||
"bul_show_minmax_mod", formsemestre_id
|
||||
):
|
||||
rang_minmax = '%s <span class="bul_minmax" title="[min, max] UE">[%s, %s]</span>' % (
|
||||
mod["mod_rang_txt"],
|
||||
scu.fmt_note(mod["stats"]["min"]),
|
||||
scu.fmt_note(mod["stats"]["max"]),
|
||||
rang_minmax = (
|
||||
'%s <span class="bul_minmax" title="[min, max] UE">[%s, %s]</span>'
|
||||
% (
|
||||
mod["mod_rang_txt"],
|
||||
scu.fmt_note(mod["stats"]["min"]),
|
||||
scu.fmt_note(mod["stats"]["max"]),
|
||||
)
|
||||
)
|
||||
else:
|
||||
rang_minmax = mod["mod_rang_txt"] # vide si pas option rang
|
||||
@ -301,9 +304,11 @@ class BulletinGeneratorLegacy(sco_bulletins_generator.BulletinGenerator):
|
||||
authuser = self.authuser
|
||||
H = []
|
||||
# --- Absences
|
||||
# XXX TODO-ASSIDUITE
|
||||
# au passage, utiliser url_for...
|
||||
H.append(
|
||||
"""<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
|
||||
(pendant ce semestre).
|
||||
</a></p>
|
||||
|
@ -124,9 +124,12 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
||||
nbabs = self.infos["nbabs"]
|
||||
story.append(Spacer(1, 2 * mm))
|
||||
if nbabs:
|
||||
# XXX TODO-ASSIDUITE
|
||||
# et utiliser url_for...
|
||||
H.append(
|
||||
"""<p class="bul_abs">
|
||||
<a href="../Absences/CalAbs?etudid=%(etudid)s" class="bull_link">
|
||||
XXX
|
||||
<b>Absences :</b> %(nbabs)s demi-journées, dont %(nbabsjust)s justifiées
|
||||
(pendant ce semestre).
|
||||
</a></p>
|
||||
|
@ -50,11 +50,11 @@ import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app import log
|
||||
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.scodoc import sco_assiduites
|
||||
from app.scodoc import codes_cursus
|
||||
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_groups
|
||||
from app.scodoc import sco_photos
|
||||
@ -242,6 +242,7 @@ def make_xml_formsemestre_bulletinetud(
|
||||
# Liste les modules de l'UE
|
||||
ue_modimpls = [mod for mod in modimpls if mod["module"]["ue_id"] == ue["ue_id"]]
|
||||
for modimpl in ue_modimpls:
|
||||
modimpl_results = nt.modimpls_results.get(modimpl["moduleimpl_id"])
|
||||
mod_moy = scu.fmt_note(
|
||||
nt.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid)
|
||||
)
|
||||
@ -290,57 +291,34 @@ def make_xml_formsemestre_bulletinetud(
|
||||
)
|
||||
)
|
||||
# --- notes de chaque eval:
|
||||
evals = nt.get_evals_in_mod(modimpl["moduleimpl_id"])
|
||||
evaluations_completes = nt.get_modimpl_evaluations_completes(
|
||||
modimpl["moduleimpl_id"]
|
||||
)
|
||||
if version != "short":
|
||||
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"]),
|
||||
)
|
||||
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)
|
||||
val = e["notes"].get(etudid, {"value": "NP"})[
|
||||
"value"
|
||||
] # NA si etud demissionnaire
|
||||
val = scu.fmt_note(val, note_max=e["note_max"])
|
||||
# Note à l'évaluation:
|
||||
val = modimpl_results.evals_notes[e.id].get(etudid, "NP")
|
||||
val = scu.fmt_note(val, note_max=e.note_max)
|
||||
x_eval.append(Element("note", value=val))
|
||||
# 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(
|
||||
"bul_show_all_evals", formsemestre_id
|
||||
):
|
||||
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:
|
||||
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"]),
|
||||
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 ""),
|
||||
)
|
||||
evaluations = Evaluation.query.filter_by(
|
||||
moduleimpl_id=modimpl["moduleimpl_id"]
|
||||
).order_by(Evaluation.date_debut)
|
||||
for e in evaluations:
|
||||
if e.id not in complete_eval_ids:
|
||||
e_dict = e.to_dict_bul()
|
||||
x_eval = Element("evaluation", **e_dict)
|
||||
x_mod.append(x_eval)
|
||||
# UE capitalisee (listee seulement si meilleure que l'UE courante)
|
||||
if ue_status["is_capitalized"]:
|
||||
|
@ -273,9 +273,7 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
|
||||
|
||||
if formsemestre_id is None:
|
||||
# clear all caches
|
||||
log(
|
||||
f"----- invalidate_formsemestre: clearing all caches. pdfonly={pdfonly}-----"
|
||||
)
|
||||
log(f"--- invalidate_formsemestre: clearing all caches. pdfonly={pdfonly}---")
|
||||
formsemestre_ids = [
|
||||
formsemestre.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
|
||||
] + sco_cursus.list_formsemestre_utilisateurs_uecap(formsemestre_id)
|
||||
log(
|
||||
f"----- invalidate_formsemestre: clearing {formsemestre_ids}. pdfonly={pdfonly} -----"
|
||||
f"--- invalidate_formsemestre: clearing {formsemestre_ids}. pdfonly={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:
|
||||
q_ues = (
|
||||
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)
|
||||
)
|
||||
else:
|
||||
# Toutes les UE du departement avec ce code:
|
||||
q_ues = (
|
||||
UniteEns.query.filter_by(ue_code=ue_code)
|
||||
.join(UniteEns.formation, aliased=True)
|
||||
.join(UniteEns.formation)
|
||||
.filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
|
||||
|
@ -29,36 +29,18 @@
|
||||
"""
|
||||
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.notesdb as ndb
|
||||
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_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_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.
|
||||
Cas incohérents que l'on peut rencontrer pour chaque étudiant:
|
||||
note et absent
|
||||
@ -66,51 +48,60 @@ def evaluation_check_absences(evaluation_id):
|
||||
ABS et absent justifié
|
||||
EXC et pas noté absent
|
||||
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]
|
||||
if not E["jour"]:
|
||||
raise ScoValueError("Fonction non disponible, patience !") # XXX TODO-ASSIDUITE
|
||||
|
||||
if not evaluation.date_debut:
|
||||
return [], [], [], [], [] # evaluation sans date
|
||||
|
||||
am, pm, demijournee = _eval_demijournee(E)
|
||||
am, pm = evaluation.is_matin(), evaluation.is_apresmidi()
|
||||
|
||||
# Liste les absences à ce moment:
|
||||
A = sco_abs.list_abs_jour(ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm)
|
||||
As = set([x["etudid"] for x in A]) # ensemble des etudiants absents
|
||||
NJ = sco_abs.list_abs_non_just_jour(ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm)
|
||||
NJs = set([x["etudid"] for x in NJ]) # ensemble des etudiants absents non justifies
|
||||
Just = sco_abs.list_abs_jour(
|
||||
ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm, is_abs=None, is_just=True
|
||||
absences = sco_abs.list_abs_jour(evaluation.date_debut, am=am, pm=pm)
|
||||
abs_etudids = set([x["etudid"] for x in absences]) # ensemble des etudiants absents
|
||||
abs_non_just = sco_abs.list_abs_non_just_jour(
|
||||
evaluation.date_debut.date(), am=am, pm=pm
|
||||
)
|
||||
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:
|
||||
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
|
||||
AbsNonSignalee = [] # note ABS mais pas noté absent
|
||||
ExcNonSignalee = [] # note EXC mais pas noté absent
|
||||
ExcNonJust = [] # note EXC mais absent non justifie
|
||||
AbsButExc = [] # note ABS mais justifié
|
||||
for etudid, _ in sco_groups.do_evaluation_listeetuds_groups(
|
||||
evaluation_id, getallstudents=True
|
||||
evaluation.id, getallstudents=True
|
||||
):
|
||||
if etudid in notes_db:
|
||||
val = notes_db[etudid]["value"]
|
||||
if (
|
||||
val != None and val != scu.NOTES_NEUTRALISE and val != scu.NOTES_ATTENTE
|
||||
) and etudid in As:
|
||||
val is not None
|
||||
and val != scu.NOTES_NEUTRALISE
|
||||
and val != scu.NOTES_ATTENTE
|
||||
) and etudid in abs_etudids:
|
||||
# note valide et absent
|
||||
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
|
||||
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
|
||||
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é
|
||||
ExcNonJust.append(etudid)
|
||||
if val is None and etudid in Justs:
|
||||
if val is None and etudid in just_etudids:
|
||||
# ABS mais justificatif
|
||||
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):
|
||||
"""Affiche état vérification absences d'une évaluation"""
|
||||
|
||||
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
|
||||
am, pm, demijournee = _eval_demijournee(E)
|
||||
evaluation: Evaluation = db.session.get(Evaluation, evaluation_id)
|
||||
am, pm = evaluation.is_matin(), evaluation.is_apresmidi()
|
||||
# 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,
|
||||
@ -129,19 +127,23 @@ def evaluation_check_absences_html(evaluation_id, with_header=True, show_ok=True
|
||||
ExcNonSignalee,
|
||||
ExcNonJust,
|
||||
AbsButExc,
|
||||
) = evaluation_check_absences(evaluation_id)
|
||||
) = evaluation_check_absences(evaluation)
|
||||
|
||||
if with_header:
|
||||
H = [
|
||||
html_sco_header.html_sem_header("Vérification absences à l'évaluation"),
|
||||
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>""",
|
||||
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>""",
|
||||
]
|
||||
else:
|
||||
# pas de header, mais un titre
|
||||
H = [
|
||||
"""<h2 class="eval_check_absences">%s du %s """
|
||||
% (E["description"], E["jour"])
|
||||
f"""<h2 class="eval_check_absences">{
|
||||
evaluation.description or "évaluation"
|
||||
} du {
|
||||
evaluation.date_debut.strftime("%d/%m/%Y") if evaluation.date_debut else ""
|
||||
} """
|
||||
]
|
||||
if (
|
||||
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:
|
||||
H.append("<li>aucun</li>")
|
||||
for etudid in etudids:
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
etud: Identite = db.session.get(Identite, etudid)
|
||||
H.append(
|
||||
'<li><a class="discretelink" href="%s">'
|
||||
% url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
|
||||
)
|
||||
+ "%(nomprenom)s</a>" % etud
|
||||
f"""<li><a class="discretelink" href="{
|
||||
url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
||||
)
|
||||
}">{etud.nomprenom}</a>"""
|
||||
)
|
||||
if linkabs:
|
||||
H.append(
|
||||
f"""<a class="stdlink" href="{url_for(
|
||||
'absences.doSignaleAbsence',
|
||||
url = url_for(
|
||||
"absences.doSignaleAbsence", # XXX TODO-ASSIDUITE
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=etud["etudid"],
|
||||
datedebut=E["jour"],
|
||||
datefin=E["jour"],
|
||||
etudid=etudid,
|
||||
# par defaut signale le jour du début de l'éval
|
||||
datedebut=evaluation.date_debut.strftime("%d/%m/%Y"),
|
||||
datefin=evaluation.date_debut.strftime("%d/%m/%Y"),
|
||||
demijournee=demijournee,
|
||||
moduleimpl_id=E["moduleimpl_id"],
|
||||
)
|
||||
}">signaler cette absence</a>"""
|
||||
moduleimpl_id=evaluation.moduleimpl_id,
|
||||
)
|
||||
H.append(
|
||||
f"""<a class="stdlink" href="{url}">signaler cette absence</a>"""
|
||||
)
|
||||
H.append("</li>")
|
||||
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):
|
||||
"""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 = [
|
||||
html_sco_header.html_sem_header(
|
||||
"Vérification absences aux évaluations de ce semestre",
|
||||
@ -229,29 +234,27 @@ def formsemestre_check_absences_html(formsemestre_id):
|
||||
</p>""",
|
||||
]
|
||||
# Modules, dans l'ordre
|
||||
Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
|
||||
for M in Mlist:
|
||||
evals = sco_evaluation_db.do_evaluation_list(
|
||||
{"moduleimpl_id": M["moduleimpl_id"]}
|
||||
)
|
||||
if evals:
|
||||
for modimpl in formsemestre.modimpls_sorted:
|
||||
if modimpl.evaluations.count() > 0:
|
||||
H.append(
|
||||
'<div class="module_check_absences"><h2><a href="moduleimpl_status?moduleimpl_id=%s">%s: %s</a></h2>'
|
||||
% (
|
||||
M["moduleimpl_id"],
|
||||
M["module"]["code"] or "",
|
||||
M["module"]["abbrev"] or "",
|
||||
)
|
||||
f"""<div class="module_check_absences">
|
||||
<h2><a href="{
|
||||
url_for("notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
|
||||
}">{modimpl.module.code or ""}: {modimpl.module.abbrev or ""}</a>
|
||||
</h2>"""
|
||||
)
|
||||
for E in evals:
|
||||
H.append(
|
||||
evaluation_check_absences_html(
|
||||
E["evaluation_id"],
|
||||
with_header=False,
|
||||
show_ok=False,
|
||||
for evaluation in modimpl.evaluations.order_by(
|
||||
Evaluation.numero, Evaluation.date_debut
|
||||
):
|
||||
H.append(
|
||||
evaluation_check_absences_html(
|
||||
evaluation.id, # XXX TODO-ASSIDUITE remplacer par evaluation ...
|
||||
with_header=False,
|
||||
show_ok=False,
|
||||
)
|
||||
)
|
||||
)
|
||||
if evals:
|
||||
H.append("</div>")
|
||||
|
||||
H.append(html_sco_header.sco_footer())
|
||||
return "\n".join(H)
|
||||
|
@ -37,14 +37,13 @@ from flask_login import current_user
|
||||
from app import db, log
|
||||
|
||||
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.notesdb as ndb
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_permissions_check
|
||||
|
||||
|
||||
_evaluationEditor = ndb.EditableTable(
|
||||
@ -53,9 +52,8 @@ _evaluationEditor = ndb.EditableTable(
|
||||
(
|
||||
"evaluation_id",
|
||||
"moduleimpl_id",
|
||||
"jour",
|
||||
"heure_debut",
|
||||
"heure_fin",
|
||||
"date_debut",
|
||||
"date_fin",
|
||||
"description",
|
||||
"note_max",
|
||||
"coefficient",
|
||||
@ -64,15 +62,11 @@ _evaluationEditor = ndb.EditableTable(
|
||||
"evaluation_type",
|
||||
"numero",
|
||||
),
|
||||
sortkey="numero desc, jour desc, heure_debut desc", # plus recente d'abord
|
||||
sortkey="numero, date_debut desc", # plus recente d'abord
|
||||
output_formators={
|
||||
"jour": ndb.DateISOtoDMY,
|
||||
"numero": ndb.int_null_is_zero,
|
||||
},
|
||||
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,
|
||||
"publish_incomplete": bool,
|
||||
"evaluation_type": int,
|
||||
@ -80,8 +74,9 @@ _evaluationEditor = ndb.EditableTable(
|
||||
)
|
||||
|
||||
|
||||
def do_evaluation_list(args, sortkey=None):
|
||||
"""List evaluations, sorted by numero (or most recent date first).
|
||||
def get_evaluation_dict(args: dict) -> list[dict]:
|
||||
"""Liste evaluations, triées numero (or most recent date first).
|
||||
Fonction de transition pour ancien code ScoDoc7.
|
||||
|
||||
Ajoute les champs:
|
||||
'duree' : '2h30'
|
||||
@ -89,14 +84,8 @@ def do_evaluation_list(args, sortkey=None):
|
||||
'apresmidi' : 1 (termine après 12:00) ou 0
|
||||
'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
|
||||
for e in evals:
|
||||
evaluation_enrich_dict(e)
|
||||
|
||||
return evals
|
||||
return [e.to_dict() for e in Evaluation.query.filter_by(**args)]
|
||||
|
||||
|
||||
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)
|
||||
evals = []
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
"edit an evaluation"
|
||||
evaluation_id = args["evaluation_id"]
|
||||
the_evals = do_evaluation_list({"evaluation_id": evaluation_id})
|
||||
if not the_evals:
|
||||
evaluation: Evaluation = db.session.get(Evaluation, evaluation_id)
|
||||
if evaluation is None:
|
||||
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(
|
||||
"Modification évaluation impossible pour %s" % current_user.get_nomplogin()
|
||||
f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
|
||||
)
|
||||
args["moduleimpl_id"] = moduleimpl_id
|
||||
check_evaluation_args(args)
|
||||
args["moduleimpl_id"] = evaluation.moduleimpl.id
|
||||
check_convert_evaluation_args(evaluation.moduleimpl, args)
|
||||
|
||||
cnx = ndb.GetDBConnexion()
|
||||
_evaluationEditor.edit(cnx, args)
|
||||
# inval cache pour ce semestre
|
||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id=M["formsemestre_id"])
|
||||
sco_cache.invalidate_formsemestre(
|
||||
formsemestre_id=evaluation.moduleimpl.formsemestre_id
|
||||
)
|
||||
|
||||
|
||||
def do_evaluation_delete(evaluation_id):
|
||||
"delete evaluation"
|
||||
evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id)
|
||||
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(
|
||||
f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
|
||||
)
|
||||
@ -226,7 +133,7 @@ def do_evaluation_delete(evaluation_id):
|
||||
raise ScoValueError(
|
||||
"Impossible de supprimer cette évaluation: il reste des notes"
|
||||
)
|
||||
|
||||
log(f"deleting evaluation {evaluation}")
|
||||
db.session.delete(evaluation)
|
||||
db.session.commit()
|
||||
|
||||
@ -287,68 +194,6 @@ def do_evaluation_get_all_notes(
|
||||
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):
|
||||
"""Move before/after previous one (decrement/increment numero)
|
||||
(published)
|
||||
@ -357,18 +202,19 @@ def moduleimpl_evaluation_move(evaluation_id: int, after=0, redirect=1):
|
||||
moduleimpl_id = evaluation.moduleimpl_id
|
||||
redirect = int(redirect)
|
||||
# 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(
|
||||
f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
|
||||
)
|
||||
|
||||
moduleimpl_evaluation_renumber(moduleimpl_id, only_if_unumbered=True)
|
||||
e = do_evaluation_list(args={"evaluation_id": evaluation_id})[0]
|
||||
Evaluation.moduleimpl_evaluation_renumber(
|
||||
evaluation.moduleimpl, only_if_unumbered=True
|
||||
)
|
||||
e = get_evaluation_dict(args={"evaluation_id": evaluation_id})[0]
|
||||
|
||||
after = int(after) # 0: deplace avant, 1 deplace apres
|
||||
if after not in (0, 1):
|
||||
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:
|
||||
idx = [p["evaluation_id"] for p in mod_evals].index(evaluation_id)
|
||||
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["numero"] == e["numero"]:
|
||||
log("Warning: moduleimpl_evaluation_move: forcing renumber")
|
||||
moduleimpl_evaluation_renumber(
|
||||
e["moduleimpl_id"], only_if_unumbered=False
|
||||
Evaluation.moduleimpl_evaluation_renumber(
|
||||
evaluation.moduleimpl, only_if_unumbered=False
|
||||
)
|
||||
else:
|
||||
# swap numero with neighbor
|
||||
|
@ -27,7 +27,7 @@
|
||||
|
||||
"""Formulaire ajout/édition d'une évaluation
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import time
|
||||
|
||||
import flask
|
||||
@ -38,6 +38,7 @@ from flask import request
|
||||
|
||||
from app import db
|
||||
from app.models import Evaluation, FormSemestre, ModuleImpl
|
||||
from app.models.evaluations import heure_to_time
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
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_evaluation_db
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_permissions_check
|
||||
from app.scodoc import sco_preferences
|
||||
|
||||
|
||||
@ -83,7 +83,7 @@ def evaluation_create_form(
|
||||
can_edit_poids = not preferences["but_disable_edit_poids_evaluations"]
|
||||
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"""
|
||||
{html_sco_header.sco_header()}
|
||||
<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)]
|
||||
#
|
||||
initvalues["visibulletin"] = initvalues.get("visibulletin", True)
|
||||
if initvalues["visibulletin"]:
|
||||
initvalues["visibulletinlist"] = ["X"]
|
||||
else:
|
||||
initvalues["visibulletinlist"] = []
|
||||
initvalues["coefficient"] = initvalues.get("coefficient", 1.0)
|
||||
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()
|
||||
if is_apc: # BUT: poids vers les UE
|
||||
@ -236,11 +229,9 @@ def evaluation_create_form(
|
||||
},
|
||||
),
|
||||
(
|
||||
"visibulletinlist",
|
||||
"visibulletin",
|
||||
{
|
||||
"input_type": "checkbox",
|
||||
"allowed_values": ["X"],
|
||||
"labels": [""],
|
||||
"input_type": "boolcheckbox",
|
||||
"title": "Visible sur bulletins",
|
||||
"explanation": "(pour les bulletins en version intermédiaire)",
|
||||
},
|
||||
@ -349,20 +340,46 @@ def evaluation_create_form(
|
||||
return flask.redirect(dest_url)
|
||||
else:
|
||||
# form submission
|
||||
if tf[2]["visibulletinlist"]:
|
||||
tf[2]["visibulletin"] = True
|
||||
args = tf[2]
|
||||
# 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:
|
||||
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:
|
||||
sco_evaluation_db.do_evaluation_edit(tf[2])
|
||||
evaluation.from_dict(args)
|
||||
else:
|
||||
# création d'une evaluation (via fonction ScoDoc7)
|
||||
evaluation_id = sco_evaluation_db.do_evaluation_create(**tf[2])
|
||||
# création d'une evaluation
|
||||
evaluation = Evaluation.create(moduleimpl=modimpl, **args)
|
||||
db.session.add(evaluation)
|
||||
db.session.commit()
|
||||
evaluation_id = evaluation.id
|
||||
if is_apc:
|
||||
# Set poids
|
||||
evaluation = db.session.get(Evaluation, evaluation_id)
|
||||
for ue in sem_ues:
|
||||
evaluation.set_ue_poids(ue, tf[2][f"poids_{ue.id}"])
|
||||
db.session.add(evaluation)
|
||||
db.session.commit()
|
||||
db.session.add(evaluation)
|
||||
db.session.commit()
|
||||
return flask.redirect(dest_url)
|
||||
|
@ -126,8 +126,8 @@ def evaluations_recap_table(formsemestre: FormSemestre) -> list[dict]:
|
||||
evaluation_id=evaluation_id,
|
||||
),
|
||||
"_titre_target_attrs": 'class="discretelink"',
|
||||
"date": e.jour.strftime("%d/%m/%Y") if e.jour else "",
|
||||
"_date_order": e.jour.isoformat() if e.jour else "",
|
||||
"date": e.date_debut.strftime("%d/%m/%Y") if e.date_debut else "",
|
||||
"_date_order": e.date_debut.isoformat() if e.date_debut else "",
|
||||
"complete": "oui" if eval_etat.is_complete else "non",
|
||||
"_complete_target": "#",
|
||||
"_complete_target_attrs": 'class="bull_link" title="prise en compte dans les moyennes"'
|
||||
|
@ -46,8 +46,8 @@ from app.scodoc.sco_utils import ModuleType
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
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_abs
|
||||
from app.scodoc import sco_edit_module
|
||||
from app.scodoc import sco_edit_ue
|
||||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
@ -101,6 +101,14 @@ def do_evaluation_etat(
|
||||
) -> dict:
|
||||
"""Donne infos sur l'état de l'évaluation.
|
||||
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_notes
|
||||
@ -124,7 +132,7 @@ def do_evaluation_etat(
|
||||
) # { etudid : note }
|
||||
|
||||
# ---- 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]
|
||||
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
|
||||
@ -275,7 +283,8 @@ def do_evaluation_etat(
|
||||
|
||||
|
||||
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)
|
||||
{ evaluation_id,nb_inscrits, nb_notes, nb_abs, nb_neutre, moy, median, last_modif ... }
|
||||
|
||||
@ -315,7 +324,7 @@ def do_evaluation_list_in_sem(formsemestre_id, with_etat=True):
|
||||
'evaluation_type': 0,
|
||||
'heure_debut': datetime.time(8, 0),
|
||||
'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',
|
||||
'note_max': 20.0,
|
||||
'numero': 0,
|
||||
@ -327,7 +336,7 @@ def do_evaluation_list_in_sem(formsemestre_id, with_etat=True):
|
||||
FROM notes_evaluation E, notes_moduleimpl MI
|
||||
WHERE MI.formsemestre_id = %(formsemestre_id)s
|
||||
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()
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
@ -335,9 +344,9 @@ def do_evaluation_list_in_sem(formsemestre_id, with_etat=True):
|
||||
res = cursor.dictfetchall()
|
||||
# etat de chaque evaluation:
|
||||
for r in res:
|
||||
r["jour"] = r["jour"] or datetime.date(1900, 1, 1) # pour les comparaisons
|
||||
if with_etat:
|
||||
r["etat"] = do_evaluation_etat(r["evaluation_id"])
|
||||
r["jour"] = r["date_debut"] or datetime.date(1900, 1, 1)
|
||||
|
||||
return res
|
||||
|
||||
@ -379,7 +388,20 @@ def _eval_etat(evals):
|
||||
|
||||
def do_evaluation_etat_in_sem(formsemestre_id):
|
||||
"""-> 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)
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
evals = nt.get_evaluations_etats()
|
||||
@ -403,88 +425,97 @@ def formsemestre_evaluations_cal(formsemestre_id):
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
|
||||
evals = nt.get_evaluations_etats()
|
||||
nb_evals = len(evals)
|
||||
evaluations = formsemestre.get_evaluations() # TODO
|
||||
nb_evals = len(evaluations)
|
||||
|
||||
color_incomplete = "#FF6060"
|
||||
color_complete = "#A0FFA0"
|
||||
color_futur = "#70E0FF"
|
||||
|
||||
today = time.strftime("%Y-%m-%d")
|
||||
|
||||
year = formsemestre.date_debut.year
|
||||
if formsemestre.date_debut.month < 8:
|
||||
year -= 1 # calendrier septembre a septembre
|
||||
year = formsemestre.annee_scolaire()
|
||||
events = {} # (day, halfday) : event
|
||||
for e in evals:
|
||||
etat = e["etat"]
|
||||
if not e["jour"]:
|
||||
continue
|
||||
day = e["jour"].strftime("%Y-%m-%d")
|
||||
mod = sco_moduleimpl.moduleimpl_withmodule_list(
|
||||
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")
|
||||
for e in evaluations:
|
||||
if e.date_debut is None:
|
||||
continue # éval. sans date
|
||||
txt = e.moduleimpl.module.code or e.moduleimpl.module.abbrev or "éval."
|
||||
if e.date_debut == e.date_fin:
|
||||
heure_debut_txt, heure_fin_txt = "?", "?"
|
||||
else:
|
||||
debut = "?"
|
||||
if e["heure_fin"]:
|
||||
fin = e["heure_fin"].strftime("%Hh%M")
|
||||
else:
|
||||
fin = "?"
|
||||
description = "%s, de %s à %s" % (mod["module"]["titre"], debut, fin)
|
||||
if etat["evalcomplete"]:
|
||||
heure_debut_txt = e.date_debut.strftime("%Hh%M") if e.date_debut else "?"
|
||||
heure_fin_txt = e.date_fin.strftime("%Hh%M") if e.date_fin else "?"
|
||||
|
||||
description = f"""{
|
||||
e.moduleimpl.module.titre
|
||||
}, de {heure_debut_txt} à {heure_fin_txt}"""
|
||||
|
||||
# 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
|
||||
else:
|
||||
color = color_incomplete
|
||||
if day > today:
|
||||
if e.date_debut > datetime.datetime.now(scu.TIME_ZONE):
|
||||
color = color_futur
|
||||
href = "moduleimpl_status?moduleimpl_id=%s" % e["moduleimpl_id"]
|
||||
# if e['heure_debut'].hour < 12:
|
||||
# halfday = True
|
||||
# else:
|
||||
# halfday = False
|
||||
if not day in events:
|
||||
# events[(day,halfday)] = [day, txt, color, href, halfday, description, mod]
|
||||
events[day] = [day, txt, color, href, description, mod]
|
||||
href = url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=e.moduleimpl_id,
|
||||
)
|
||||
day = e.date_debut.date().isoformat() # yyyy-mm-dd
|
||||
event = events.get(day)
|
||||
if not event:
|
||||
events[day] = [day, txt, color, href, description, e.moduleimpl]
|
||||
else:
|
||||
e = events[day]
|
||||
if e[-1]["moduleimpl_id"] != mod["moduleimpl_id"]:
|
||||
if event[-1].id != e.moduleimpl.id:
|
||||
# plusieurs evals de modules differents a la meme date
|
||||
e[1] += ", " + txt
|
||||
e[4] += ", " + description
|
||||
if not etat["evalcomplete"]:
|
||||
e[2] = color_incomplete
|
||||
if day > today:
|
||||
e[2] = color_futur
|
||||
event[1] += ", " + txt
|
||||
event[4] += ", " + description
|
||||
if color == color_incomplete:
|
||||
event[2] = color_incomplete
|
||||
if color == 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
|
||||
)
|
||||
|
||||
H = [
|
||||
return f"""
|
||||
{
|
||||
html_sco_header.html_sem_header(
|
||||
"Evaluations du semestre",
|
||||
cssstyles=["css/calabs.css"],
|
||||
),
|
||||
'<div class="cal_evaluations">',
|
||||
CalHTML,
|
||||
"</div>",
|
||||
"<p>soit %s évaluations planifiées;" % nb_evals,
|
||||
"""<ul><li>en <span style="background-color: %s">rouge</span> les évaluations passées auxquelles il manque des notes</li>
|
||||
<li>en <span style="background-color: %s">vert</span> les évaluations déjà notées</li>
|
||||
<li>en <span style="background-color: %s">bleu</span> les évaluations futures</li></ul></p>"""
|
||||
% (color_incomplete, color_complete, color_futur),
|
||||
"""<p><a href="formsemestre_evaluations_delai_correction?formsemestre_id=%s" class="stdlink">voir les délais de correction</a></p>
|
||||
"""
|
||||
% (formsemestre_id,),
|
||||
html_sco_header.sco_footer(),
|
||||
]
|
||||
return "\n".join(H)
|
||||
)
|
||||
}
|
||||
<div class="cal_evaluations">
|
||||
{ cal_html }
|
||||
</div>
|
||||
<p>soit {nb_evals} évaluations planifiées;
|
||||
</p>
|
||||
<ul>
|
||||
<li>en <span style=
|
||||
"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() }
|
||||
"""
|
||||
|
||||
|
||||
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
|
||||
ou None si actuellement incomplète
|
||||
"""
|
||||
@ -496,7 +527,7 @@ def evaluation_date_first_completion(evaluation_id):
|
||||
# Il faut considerer les inscriptions au semestre
|
||||
# (pour avoir l'etat et le groupe) et aussi les inscriptions
|
||||
# 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]
|
||||
# formsemestre_id = M["formsemestre_id"]
|
||||
# insem = sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits( formsemestre_id)
|
||||
@ -536,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.
|
||||
"""
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
|
||||
evals = nt.get_evaluations_etats()
|
||||
T = []
|
||||
for e in evals:
|
||||
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
|
||||
evaluations = formsemestre.get_evaluations()
|
||||
rows = []
|
||||
for e in evaluations:
|
||||
if (e.evaluation_type != scu.EVALUATION_NORMALE) or (
|
||||
e.moduleimpl.module.module_type == ModuleType.MALUS
|
||||
):
|
||||
continue
|
||||
e["date_first_complete"] = evaluation_date_first_completion(e["evaluation_id"])
|
||||
if e["date_first_complete"]:
|
||||
e["delai_correction"] = (e["date_first_complete"].date() - e["jour"]).days
|
||||
date_first_complete = evaluation_date_first_completion(e.id)
|
||||
if date_first_complete and e.date_fin:
|
||||
delai_correction = (date_first_complete.date() - e.date_fin).days
|
||||
else:
|
||||
e["delai_correction"] = None
|
||||
delai_correction = None
|
||||
|
||||
e["module_code"] = Mod["code"]
|
||||
e["_module_code_target"] = url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=M["moduleimpl_id"],
|
||||
rows.append(
|
||||
{
|
||||
"date_first_complete": date_first_complete,
|
||||
"delai_correction": delai_correction,
|
||||
"jour": e.date_debut.strftime("%d/%m/%Y")
|
||||
if e.date_debut
|
||||
else "sans date",
|
||||
"_jour_target": url_for(
|
||||
"notes.evaluation_listenotes",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
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"],
|
||||
}
|
||||
)
|
||||
e["module_titre"] = Mod["titre"]
|
||||
e["responsable_id"] = M["responsable_id"]
|
||||
e["responsable_nomplogin"] = sco_users.user_info(M["responsable_id"])[
|
||||
"nomplogin"
|
||||
]
|
||||
e["_jour_target"] = url_for(
|
||||
"notes.evaluation_listenotes",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
evaluation_id=e["evaluation_id"],
|
||||
)
|
||||
T.append(e)
|
||||
|
||||
columns_ids = (
|
||||
"module_code",
|
||||
@ -592,16 +627,14 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, format="html"):
|
||||
tab = GenTable(
|
||||
titles=titles,
|
||||
columns_ids=columns_ids,
|
||||
rows=T,
|
||||
rows=rows,
|
||||
html_class="table_leftalign table_coldate",
|
||||
html_sortable=True,
|
||||
html_title="<h2>Correction des évaluations du semestre</h2>",
|
||||
caption="Correction des évaluations du semestre",
|
||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
base_url="%s?formsemestre_id=%s" % (request.base_url, formsemestre_id),
|
||||
origin="Généré par %s le " % sco_version.SCONAME
|
||||
+ scu.timedate_human_repr()
|
||||
+ "",
|
||||
origin=f"""Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}""",
|
||||
filename=scu.make_filename("evaluations_delais_" + formsemestre.titre_annee()),
|
||||
)
|
||||
return tab.make_page(format=format)
|
||||
@ -612,7 +645,7 @@ def evaluation_describe(evaluation_id="", edit_in_place=True, link_saisie=True):
|
||||
"""HTML description of evaluation, for page headers
|
||||
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"]
|
||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
||||
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
||||
|
@ -512,8 +512,7 @@ def excel_feuille_saisie(evaluation: "Evaluation", titreannee, description, line
|
||||
# description evaluation
|
||||
ws.append_single_cell_row(scu.unescape_html(description), style_titres)
|
||||
ws.append_single_cell_row(
|
||||
"Evaluation du %s (coef. %g)"
|
||||
% (evaluation.jour or "sans date", evaluation.coefficient or 0.0),
|
||||
f"Evaluation {evaluation.descr_date()} (coef. {(evaluation.coefficient or 0.0):g})",
|
||||
style,
|
||||
)
|
||||
# ligne blanche
|
||||
|
@ -1245,7 +1245,9 @@ def do_formsemestre_clone(
|
||||
moduleimpl_id=mod_orig["moduleimpl_id"]
|
||||
):
|
||||
# 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
|
||||
# Copie les poids APC de l'évaluation
|
||||
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)
|
||||
for mod in mods:
|
||||
# evaluations
|
||||
evals = sco_evaluation_db.do_evaluation_list(
|
||||
evals = sco_evaluation_db.get_evaluation_dict(
|
||||
args={"moduleimpl_id": mod["moduleimpl_id"]}
|
||||
)
|
||||
for e in evals:
|
||||
|
@ -50,6 +50,7 @@ from app.models import (
|
||||
ModuleImpl,
|
||||
NotesNotes,
|
||||
)
|
||||
from app.scodoc.codes_cursus import UE_SPORT
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
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 htmlutils
|
||||
from app.scodoc import sco_abs
|
||||
from app.scodoc import sco_archives
|
||||
from app.scodoc import sco_bulletins
|
||||
from app.scodoc import codes_cursus
|
||||
@ -498,7 +498,7 @@ def retreive_formsemestre_from_request() -> int:
|
||||
modimpl = modimpl[0]
|
||||
formsemestre_id = modimpl["formsemestre_id"]
|
||||
elif "evaluation_id" in args:
|
||||
E = sco_evaluation_db.do_evaluation_list(
|
||||
E = sco_evaluation_db.get_evaluation_dict(
|
||||
{"evaluation_id": args["evaluation_id"]}
|
||||
)
|
||||
if not E:
|
||||
@ -620,7 +620,7 @@ def formsemestre_description_table(
|
||||
columns_ids += ["Inscrits", "Responsable", "Enseignants"]
|
||||
if with_evals:
|
||||
columns_ids += [
|
||||
"jour",
|
||||
"date_evaluation",
|
||||
"description",
|
||||
"coefficient",
|
||||
"evalcomplete_str",
|
||||
@ -630,7 +630,7 @@ def formsemestre_description_table(
|
||||
titles = {title: title for title in columns_ids}
|
||||
titles.update({f"ue_{ue.id}": ue.acronyme for ue in ues})
|
||||
titles["ects"] = "ECTS"
|
||||
titles["jour"] = "Évaluation"
|
||||
titles["date_evaluation"] = "Évaluation"
|
||||
titles["description"] = ""
|
||||
titles["coefficient"] = "Coef. éval."
|
||||
titles["evalcomplete_str"] = "Complète"
|
||||
@ -660,9 +660,11 @@ def formsemestre_description_table(
|
||||
"Module": ue.titre,
|
||||
"_css_row_class": "table_row_ue",
|
||||
}
|
||||
if use_ue_coefs:
|
||||
ue_info["Coef."] = ue.coefficient
|
||||
ue_info["Coef._class"] = "ue_coef"
|
||||
if use_ue_coefs and ue.type != UE_SPORT:
|
||||
ue_info["Coef."] = ue.coefficient or "0."
|
||||
ue_info["_Coef._class"] = "ue_coef"
|
||||
if not ue.coefficient:
|
||||
ue_info["_Coef._class"] += " ue_coef_nul"
|
||||
if ue.color:
|
||||
for k in list(ue_info.keys()):
|
||||
if not k.startswith("_"):
|
||||
@ -738,8 +740,10 @@ def formsemestre_description_table(
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
evaluation_id=e["evaluation_id"],
|
||||
)
|
||||
e["_jour_order"] = e["jour"].isoformat()
|
||||
e["jour"] = e["jour"].strftime("%d/%m/%Y") if e["jour"] else ""
|
||||
e["_date_evaluation_order"] = e["jour"].isoformat()
|
||||
e["date_evaluation"] = (
|
||||
e["jour"].strftime("%d/%m/%Y") if e["jour"] else ""
|
||||
)
|
||||
e["UE"] = row["UE"]
|
||||
e["_UE_td_attrs"] = row["_UE_td_attrs"]
|
||||
e["Code"] = row["Code"]
|
||||
@ -847,10 +851,14 @@ def _make_listes_sem(formsemestre: FormSemestre, with_absences=True):
|
||||
form_abs_tmpl += f"""
|
||||
<a class="btn" href="{
|
||||
url_for("assiduites.signal_assiduites_group", scodoc_dept=g.scodoc_dept)
|
||||
}?group_ids=%(group_id)s&jour={datetime.date.today().isoformat()}&formsemestre_id={formsemestre.id}"><button>Saisie Journalière</button></a>
|
||||
}?group_ids=%(group_id)s&jour={
|
||||
datetime.date.today().isoformat()
|
||||
}&formsemestre_id={formsemestre.id}"><button>Saisie journalière</button></a>
|
||||
<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>
|
||||
}?group_ids=%(group_id)s&formsemestre_id={
|
||||
formsemestre.formsemestre_id
|
||||
}"><button>Saisie différée</button></a>
|
||||
</td>
|
||||
"""
|
||||
else:
|
||||
@ -880,13 +888,14 @@ def _make_listes_sem(formsemestre: FormSemestre, with_absences=True):
|
||||
if n_members == 0:
|
||||
continue # skip empty groups
|
||||
partition_is_empty = False
|
||||
group["url_etat"] = url_for(
|
||||
"absences.EtatAbsencesGr",
|
||||
group_ids=group["group_id"],
|
||||
debut=formsemestre.date_debut.strftime("%d/%m/%Y"),
|
||||
fin=formsemestre.date_fin.strftime("%d/%m/%Y"),
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
)
|
||||
# XXX TODO-ASSIDUITE
|
||||
group["url_etat"] = "non disponible" # url_for(
|
||||
# "absences.EtatAbsencesGr",
|
||||
# group_ids=group["group_id"],
|
||||
# debut=formsemestre.date_debut.strftime("%d/%m/%Y"),
|
||||
# fin=formsemestre.date_fin.strftime("%d/%m/%Y"),
|
||||
# scodoc_dept=g.scodoc_dept,
|
||||
# )
|
||||
if group["group_name"]:
|
||||
group["label"] = "groupe %(group_name)s" % group
|
||||
else:
|
||||
|
@ -45,7 +45,7 @@ from app import db
|
||||
from app.models import FormSemestre
|
||||
import app.scodoc.sco_utils as scu
|
||||
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_formsemestre
|
||||
from app.scodoc import sco_groups
|
||||
@ -829,7 +829,8 @@ def tab_absences_html(groups_infos, etat=None):
|
||||
"<li>",
|
||||
form_choix_jour_saisie_hebdo(groups_infos),
|
||||
"</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.formsemestre["date_debut"],
|
||||
@ -890,12 +891,13 @@ def form_choix_jour_saisie_hebdo(groups_infos, moduleimpl_id=None):
|
||||
if not authuser.has_permission(Permission.ScoAbsChange):
|
||||
return ""
|
||||
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()
|
||||
|
||||
FA = [] # formulaire avec menu saisi absences
|
||||
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(groups_infos.get_form_elem())
|
||||
@ -906,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="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">""")
|
||||
date = first_monday
|
||||
i = 0
|
||||
for jour in sco_abs.day_names():
|
||||
for jour in sco_cal.day_names():
|
||||
if i == today_idx:
|
||||
sel = "selected"
|
||||
else:
|
||||
@ -945,14 +947,17 @@ def form_choix_saisie_semaine(groups_infos):
|
||||
) # car ici utilisee dans un format string !
|
||||
|
||||
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
|
||||
# XXX TODO-ASSIDUITE et utiliser un POST
|
||||
FA.append('<form action="Absences/SignaleAbsenceGrHebdo" method="get">')
|
||||
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="destination" value="%s"/>' % destination)
|
||||
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>")
|
||||
return "\n".join(FA)
|
||||
|
||||
|
@ -69,38 +69,44 @@ def do_evaluation_listenotes(
|
||||
mode = None
|
||||
if moduleimpl_id:
|
||||
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:
|
||||
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:
|
||||
raise ValueError("missing argument: evaluation or module")
|
||||
if not evals:
|
||||
return "<p>Aucune évaluation !</p>", "ScoDoc"
|
||||
|
||||
E = evals[0] # il y a au moins une evaluation
|
||||
modimpl = db.session.get(ModuleImpl, E["moduleimpl_id"])
|
||||
eval_dict = evals[0] # il y a au moins une evaluation
|
||||
modimpl = db.session.get(ModuleImpl, eval_dict["moduleimpl_id"])
|
||||
# description de l'evaluation
|
||||
if mode == "eval":
|
||||
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:
|
||||
H = []
|
||||
page_title = f"Notes {modimpl.module.code}"
|
||||
# groupes
|
||||
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
|
||||
grnams = [str(g["group_id"]) for g in groups] # noms des checkbox
|
||||
|
||||
if len(evals) > 1:
|
||||
descr = [
|
||||
("moduleimpl_id", {"default": E["moduleimpl_id"], "input_type": "hidden"})
|
||||
(
|
||||
"moduleimpl_id",
|
||||
{"default": eval_dict["moduleimpl_id"], "input_type": "hidden"},
|
||||
)
|
||||
]
|
||||
else:
|
||||
descr = [
|
||||
("evaluation_id", {"default": E["evaluation_id"], "input_type": "hidden"})
|
||||
(
|
||||
"evaluation_id",
|
||||
{"default": eval_dict["evaluation_id"], "input_type": "hidden"},
|
||||
)
|
||||
]
|
||||
if len(grnams) > 1:
|
||||
descr += [
|
||||
@ -199,7 +205,7 @@ def do_evaluation_listenotes(
|
||||
url_for(
|
||||
"notes.moduleimpl_status",
|
||||
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)
|
||||
|
||||
|
||||
def moduleimpl_listeetuds(moduleimpl_id):
|
||||
def moduleimpl_listeetuds(moduleimpl_id): # XXX OBSOLETE
|
||||
"retourne liste des etudids inscrits a ce module"
|
||||
req = """SELECT DISTINCT Im.etudid
|
||||
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 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_evaluations
|
||||
from app.scodoc import sco_evaluation_db
|
||||
@ -61,7 +61,7 @@ from app.tables import list_etuds
|
||||
# menu evaluation dans moduleimpl
|
||||
def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str:
|
||||
"Menu avec actions sur une evaluation"
|
||||
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]
|
||||
|
||||
group_id = sco_groups.get_default_group(modimpl["formsemestre_id"])
|
||||
@ -203,11 +203,10 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
||||
)
|
||||
|
||||
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(
|
||||
Evaluation.numero.desc(),
|
||||
Evaluation.jour.desc(),
|
||||
Evaluation.heure_debut.desc(),
|
||||
Evaluation.date_debut.desc(),
|
||||
).all()
|
||||
nb_evaluations = len(evaluations)
|
||||
max_poids = max(
|
||||
@ -333,10 +332,6 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
||||
'<tr><td colspan="4">'
|
||||
# <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(
|
||||
f"""<tr><td colspan="4"><span class="moduleimpl_abs_link"><a class="stdlink"
|
||||
@ -350,17 +345,20 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
||||
current_user.has_permission(Permission.ScoAbsChange)
|
||||
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)
|
||||
H.append(
|
||||
f"""
|
||||
<span class="moduleimpl_abs_link"><a class="stdlink"
|
||||
href="{url_for("absences.SignaleAbsenceGrHebdo",
|
||||
scodoc_dept=g.scodoc_dept,formsemestre_id=formsemestre_id,
|
||||
moduleimpl_id=moduleimpl_id, datelundi=datelundi, group_ids=group_id)}">
|
||||
Saisie Absences hebdo.</a></span>
|
||||
<span class="moduleimpl_abs_link"><a class="stdlink" href="XXX"
|
||||
>Saisie Absences hebdo. (INDISPONIBLE)</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>")
|
||||
#
|
||||
@ -435,8 +433,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
||||
top_table_links += f"""
|
||||
<a class="stdlink" style="margin-left:2em;" href="{
|
||||
url_for("notes.moduleimpl_evaluation_renumber",
|
||||
scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id,
|
||||
redirect=1)
|
||||
scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
|
||||
}">Trier par date</a>
|
||||
"""
|
||||
if nb_evaluations > 0:
|
||||
@ -572,10 +569,8 @@ def _ligne_evaluation(
|
||||
# visualisation des poids (Hinton map)
|
||||
H.append(_evaluation_poids_html(evaluation, max_poids))
|
||||
H.append("""<div class="evaluation_titre">""")
|
||||
if evaluation.jour:
|
||||
H.append(
|
||||
f"""Le {evaluation.jour.strftime("%d/%m/%Y")} {evaluation.descr_heure()}"""
|
||||
)
|
||||
if evaluation.date_debut:
|
||||
H.append(evaluation.descr_date())
|
||||
else:
|
||||
H.append(
|
||||
f"""<a href="{url_for("notes.evaluation_edit",
|
||||
|
@ -54,34 +54,6 @@ def can_edit_notes(authuser, moduleimpl_id, allow_ens=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):
|
||||
"""True if current user can suppress this annotation
|
||||
Seuls l'auteur de l'annotation et le chef de dept peuvent supprimer
|
||||
|
@ -59,7 +59,7 @@ from flask.helpers import make_response, url_for
|
||||
|
||||
from app import log
|
||||
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_portal_apogee
|
||||
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="")
|
||||
|
||||
|
||||
def photo_portal_url(etud):
|
||||
def photo_portal_url(code_nip: str):
|
||||
"""Returns external URL to retreive photo on portal,
|
||||
or None if no portal configured"""
|
||||
photo_url = sco_portal_apogee.get_photo_url()
|
||||
if photo_url and etud["code_nip"]:
|
||||
return photo_url + "?nip=" + etud["code_nip"]
|
||||
if photo_url and code_nip:
|
||||
return photo_url + "?nip=" + code_nip
|
||||
else:
|
||||
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)
|
||||
if not path:
|
||||
# Portail ?
|
||||
ext_url = photo_portal_url(etud)
|
||||
ext_url = photo_portal_url(etud["code_nip"])
|
||||
if not ext_url:
|
||||
# fallback: Photo "unknown"
|
||||
photo_url = unknown_image_url()
|
||||
else:
|
||||
# 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:
|
||||
# copy failed, can we use external url ?
|
||||
# nb: rarement utile, car le portail est rarement accessible sans authentification
|
||||
@ -185,8 +185,8 @@ def build_image_response(filename):
|
||||
return response
|
||||
|
||||
|
||||
def etud_photo_is_local(etud: dict, size="small"):
|
||||
return photo_pathname(etud["photo_filename"], size=size)
|
||||
def etud_photo_is_local(photo_filename: str, size="small"):
|
||||
return photo_pathname(photo_filename, size=size)
|
||||
|
||||
|
||||
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"])
|
||||
if title is None:
|
||||
title = nom
|
||||
if not etud_photo_is_local(etud):
|
||||
if not etud_photo_is_local(etud["photo_filename"]):
|
||||
fallback = (
|
||||
f"""onerror='this.onerror = null; this.src="{unknown_image_url()}"'"""
|
||||
)
|
||||
@ -254,7 +254,7 @@ def photo_pathname(photo_filename: str, size="orig"):
|
||||
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.
|
||||
If there is an existing photo, it is erased and replaced.
|
||||
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:
|
||||
return False, f"Fichier image '{filename}' de taille invalide ! ({filesize})"
|
||||
try:
|
||||
saved_filename = save_image(etud["etudid"], data)
|
||||
saved_filename = save_image(etud, data)
|
||||
except (OSError, PIL.UnidentifiedImageError) as exc:
|
||||
raise ScoValueError(
|
||||
msg="Fichier d'image '{filename}' invalide ou format non supporté"
|
||||
) from exc
|
||||
|
||||
# update database:
|
||||
etud["photo_filename"] = saved_filename
|
||||
etud["foto"] = None
|
||||
|
||||
cnx = ndb.GetDBConnexion()
|
||||
sco_etud.identite_edit_nocheck(cnx, etud)
|
||||
cnx.commit()
|
||||
#
|
||||
logdb(cnx, method="changePhoto", msg=saved_filename, etudid=etud["etudid"])
|
||||
etud.photo_filename = saved_filename
|
||||
db.session.add(etud)
|
||||
Scolog.logdb(method="changePhoto", msg=saved_filename, etudid=etud.id)
|
||||
db.session.commit()
|
||||
#
|
||||
return True, "ok"
|
||||
|
||||
@ -313,7 +309,7 @@ def suppress_photo(etud: Identite) -> None:
|
||||
# Internal functions
|
||||
|
||||
|
||||
def save_image(etudid, data):
|
||||
def save_image(etud: Identite, data: bytes):
|
||||
"""data is a bytes string.
|
||||
Save image in JPEG in 2 sizes (original and h90).
|
||||
Returns filename (relative to PHOTO_DIR), without extension
|
||||
@ -322,7 +318,7 @@ def save_image(etudid, data):
|
||||
data_file.write(data)
|
||||
data_file.seek(0)
|
||||
img = PILImage.open(data_file)
|
||||
filename = get_new_filename(etudid)
|
||||
filename = get_new_filename(etud)
|
||||
path = os.path.join(PHOTO_DIR, filename)
|
||||
log("saving %dx%d jpeg to %s" % (img.size[0], img.size[1], path))
|
||||
img = img.convert("RGB")
|
||||
@ -338,16 +334,16 @@ def scale_height(img, W=None, H=REDUCED_HEIGHT):
|
||||
if W is None:
|
||||
# keep aspect
|
||||
W = int((img.size[0] * H) / img.size[1])
|
||||
img.thumbnail((W, H), PILImage.ANTIALIAS)
|
||||
img.thumbnail((W, H), PILImage.LANCZOS)
|
||||
return img
|
||||
|
||||
|
||||
def get_new_filename(etudid):
|
||||
def get_new_filename(etud: Identite):
|
||||
"""Constructs a random filename to store a new image.
|
||||
The path is constructed as: Fxx/etudid
|
||||
"""
|
||||
dept = g.scodoc_dept
|
||||
return find_new_dir() + dept + "_" + str(etudid)
|
||||
dept = etud.departement.acronym
|
||||
return find_new_dir() + dept + "_" + str(etud.id)
|
||||
|
||||
|
||||
def find_new_dir():
|
||||
@ -367,15 +363,14 @@ def find_new_dir():
|
||||
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.
|
||||
Returns rel. path or None if copy failed, with a diagnostic message
|
||||
"""
|
||||
if "nomprenom" not in etud:
|
||||
sco_etud.format_etud_ident(etud)
|
||||
url = photo_portal_url(etud)
|
||||
etud: Identite = Identite.query.get_or_404(etudid)
|
||||
url = photo_portal_url(etud.code_nip)
|
||||
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")
|
||||
error_message = None
|
||||
try:
|
||||
@ -394,11 +389,11 @@ def copy_portal_photo_to_fs(etud: dict):
|
||||
log(f"copy_portal_photo_to_fs: {error_message}")
|
||||
return (
|
||||
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:
|
||||
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
|
||||
try:
|
||||
@ -410,8 +405,8 @@ def copy_portal_photo_to_fs(etud: dict):
|
||||
if status:
|
||||
log(f"copy_portal_photo_to_fs: copied {url}")
|
||||
return (
|
||||
photo_pathname(etud["photo_filename"]),
|
||||
f"{etud['nomprenom']}: photo chargée",
|
||||
photo_pathname(etud.photo_filename),
|
||||
f"{etud.nomprenom}: photo chargée",
|
||||
)
|
||||
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):
|
||||
"""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}
|
||||
)
|
||||
if not eval_data:
|
||||
@ -239,7 +239,7 @@ class PlacementRunner:
|
||||
self.groups_ids = [
|
||||
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}
|
||||
)[0]
|
||||
self.groups = sco_groups.listgroups(self.groups_ids)
|
||||
|
@ -162,7 +162,7 @@ def _convert_pref_type(p, pref_spec):
|
||||
# special case for float values (where NULL means 0)
|
||||
p["value"] = float(p["value"] or 0)
|
||||
elif typ == "int":
|
||||
p["value"] = int(p["value"] or 0)
|
||||
p["value"] = int(float(p["value"] or 0))
|
||||
else:
|
||||
raise ValueError("invalid preference type")
|
||||
|
||||
@ -629,6 +629,7 @@ class BasePreferences(object):
|
||||
"type": "float",
|
||||
"category": "assi",
|
||||
"only_global": True,
|
||||
"explanation": "Durée d'un créneau en heure. Utilisé dans les pages de saisie",
|
||||
},
|
||||
),
|
||||
(
|
||||
@ -658,10 +659,10 @@ class BasePreferences(object):
|
||||
{
|
||||
"initvalue": "1/2 J.",
|
||||
"input_type": "menu",
|
||||
"labels": ["1/2 J.", "J.", "H."],
|
||||
"allowed_values": ["1/2 J.", "J.", "H."],
|
||||
"labels": scu.AssiduitesMetrics.LONG,
|
||||
"allowed_values": scu.AssiduitesMetrics.SHORT,
|
||||
"title": "Métrique de l'assiduité",
|
||||
"explanation": "Unité utilisée dans la fiche étudiante, le bilan, et dans les calculs (J. = journée, H. = heure)",
|
||||
"explanation": "Unité utilisée dans la fiche étudiante, les bilans et les calculs",
|
||||
"category": "assi",
|
||||
"only_global": True,
|
||||
},
|
||||
@ -669,10 +670,10 @@ class BasePreferences(object):
|
||||
(
|
||||
"assi_seuil",
|
||||
{
|
||||
"initvalue": 3.0,
|
||||
"initvalue": 3,
|
||||
"size": 10,
|
||||
"title": "Seuil d'alerte des absences",
|
||||
"type": "float",
|
||||
"type": "int",
|
||||
"explanation": "Nombres d'absences limite avant alerte dans le bilan (utilisation de l'unité métrique ↑ )",
|
||||
"category": "assi",
|
||||
"only_global": True,
|
||||
|
@ -45,7 +45,6 @@ from app.models import (
|
||||
FormSemestre,
|
||||
Module,
|
||||
ModuleImpl,
|
||||
NotesNotes,
|
||||
ScolarNews,
|
||||
)
|
||||
from app.models.etudiants import Identite
|
||||
@ -54,16 +53,13 @@ from app.scodoc.sco_exceptions import (
|
||||
AccessDenied,
|
||||
InvalidNoteValue,
|
||||
NoteProcessError,
|
||||
ScoBugCatcher,
|
||||
ScoException,
|
||||
ScoInvalidParamError,
|
||||
ScoValueError,
|
||||
)
|
||||
from app.scodoc import html_sco_header, sco_users
|
||||
from app.scodoc import htmlutils
|
||||
from app.scodoc import sco_abs
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_edit_module
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_evaluation_db
|
||||
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_groups
|
||||
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_undo_notes
|
||||
import app.scodoc.notesdb as ndb
|
||||
@ -502,15 +497,16 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
|
||||
"""
|
||||
]
|
||||
# news
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_NOTE,
|
||||
obj=evaluation.moduleimpl.id,
|
||||
text=f"""Suppression des notes d'une évaluation dans
|
||||
<a class="stdlink" href="{status_url}"
|
||||
>{evaluation.moduleimpl.module.titre or 'module sans titre'}</a>
|
||||
""",
|
||||
url=status_url,
|
||||
)
|
||||
if nb_suppress:
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_NOTE,
|
||||
obj=evaluation.moduleimpl.id,
|
||||
text=f"""Suppression des notes d'une évaluation dans
|
||||
<a class="stdlink" href="{status_url}"
|
||||
>{evaluation.moduleimpl.module.titre or 'module sans titre'}</a>
|
||||
""",
|
||||
url=status_url,
|
||||
)
|
||||
|
||||
return html_sco_header.sco_header() + "\n".join(H) + html_sco_header.sco_footer()
|
||||
|
||||
@ -884,15 +880,15 @@ def feuille_saisie_notes(evaluation_id, group_ids=[]):
|
||||
modimpl = evaluation.moduleimpl
|
||||
formsemestre = modimpl.formsemestre
|
||||
mod_responsable = sco_users.user_info(modimpl.responsable_id)
|
||||
if evaluation.jour:
|
||||
indication_date = evaluation.jour.isoformat()
|
||||
if evaluation.date_debut:
|
||||
indication_date = evaluation.date_debut.date().isoformat()
|
||||
else:
|
||||
indication_date = scu.sanitize_filename(evaluation.description or "")[:12]
|
||||
eval_name = f"{evaluation.moduleimpl.module.code}-{indication_date}"
|
||||
|
||||
date_str = (
|
||||
f"""du {evaluation.jour.strftime("%d/%m/%Y")}"""
|
||||
if evaluation.jour
|
||||
f"""du {evaluation.date_debut.strftime("%d/%m/%Y")}"""
|
||||
if evaluation.date_debut
|
||||
else "(sans date)"
|
||||
)
|
||||
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)
|
||||
|
||||
# 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 = []
|
||||
if evaluation.is_matin():
|
||||
nbabs = sco_abs.count_abs(etudid, jour_iso, jour_iso, matin=True)
|
||||
nbabsjust = sco_abs.count_abs_just(etudid, jour_iso, jour_iso, matin=True)
|
||||
nbabs = 0 # TODO-ASSIDUITE sco_abs.count_abs(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 nbabsjust:
|
||||
warn_abs_lst.append("absent justifié le matin !")
|
||||
else:
|
||||
warn_abs_lst.append("absent le matin !")
|
||||
if evaluation.is_apresmidi():
|
||||
nbabs = sco_abs.count_abs(etudid, jour_iso, jour_iso, matin=0)
|
||||
nbabsjust = sco_abs.count_abs_just(etudid, jour_iso, jour_iso, matin=0)
|
||||
nbabs = 0 # TODO-ASSIDUITE sco_abs.count_abs(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 nbabsjust:
|
||||
warn_abs_lst.append("absent justifié l'après-midi !")
|
||||
|
@ -43,7 +43,8 @@ from PIL import Image as PILImage
|
||||
import flask
|
||||
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
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||
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">'
|
||||
% 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="")
|
||||
else: # la photo n'est pas immédiatement dispo
|
||||
foto = f"""<span class="unloaded_img" id="{t["etudid"]
|
||||
@ -194,7 +195,7 @@ def check_local_photos_availability(groups_infos, fmt=""):
|
||||
nb_missing = 0
|
||||
for t in groups_infos.members:
|
||||
_ = 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
|
||||
if nb_missing > 0:
|
||||
parameters = {"group_ids": groups_infos.group_ids, "format": fmt}
|
||||
@ -278,7 +279,7 @@ def trombino_copy_photos(group_ids=[], dialog_confirmed=False):
|
||||
msg = []
|
||||
nok = 0
|
||||
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)
|
||||
if path:
|
||||
nok += 1
|
||||
@ -539,7 +540,7 @@ def photos_import_files_form(group_ids=()):
|
||||
return flask.redirect(back_url)
|
||||
else:
|
||||
|
||||
def callback(etud, data, filename):
|
||||
def callback(etud: Identite, 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:
|
||||
etudid = filename_to_etudid[normname]
|
||||
# ok, store photo
|
||||
try:
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
del filename_to_etudid[normname]
|
||||
except Exception as exc:
|
||||
etud: Identite = db.session.get(Identite, etudid)
|
||||
if not etud:
|
||||
raise ScoValueError(
|
||||
f"ID étudiant invalide: {etudid}", dest_url=back_url
|
||||
) from exc
|
||||
|
||||
)
|
||||
del filename_to_etudid[normname]
|
||||
status, err_msg = callback(
|
||||
etud,
|
||||
data,
|
||||
|
@ -41,7 +41,7 @@ from reportlab.lib.units import cm
|
||||
from reportlab.platypus import KeepInFrame, Paragraph, Table, TableStyle
|
||||
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.sco_exceptions import ScoPDFFormatError
|
||||
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")
|
||||
col_width = 0.85 * cm
|
||||
if sco_preferences.get_preference("feuille_releve_abs_samedi"):
|
||||
days = sco_abs.DAYNAMES[:6] # Lundi, ..., Samedi
|
||||
days = sco_cal.DAYNAMES[:6] # Lundi, ..., Samedi
|
||||
else:
|
||||
days = sco_abs.DAYNAMES[:5] # Lundi, ..., Vendredi
|
||||
days = sco_cal.DAYNAMES[:5] # Lundi, ..., Vendredi
|
||||
nb_days = len(days)
|
||||
|
||||
# Informations sur les groupes à afficher:
|
||||
|
@ -60,7 +60,7 @@ from app.models.formsemestre import FormSemestre
|
||||
|
||||
|
||||
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 codes_cursus
|
||||
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
|
||||
et enregistre les notes.
|
||||
"""
|
||||
moduleimpl: ModuleImpl = db.session.get(ModuleImpl, moduleimpl_id)
|
||||
log(
|
||||
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,
|
||||
list(notes_etuds.keys()),
|
||||
)
|
||||
|
||||
# Création d'une évaluation si il n'y en a pas déjà:
|
||||
mod_evals = sco_evaluation_db.do_evaluation_list(
|
||||
args={"moduleimpl_id": moduleimpl_id}
|
||||
)
|
||||
if len(mod_evals):
|
||||
if moduleimpl.evaluations.count() > 0:
|
||||
# met la note dans le première évaluation existante:
|
||||
evaluation_id = mod_evals[0]["evaluation_id"]
|
||||
evaluation: Evaluation = moduleimpl.evaluations.first()
|
||||
else:
|
||||
# crée une évaluation:
|
||||
evaluation_id = sco_evaluation_db.do_evaluation_create(
|
||||
moduleimpl_id=moduleimpl_id,
|
||||
evaluation: Evaluation = Evaluation.create(
|
||||
moduleimpl=moduleimpl,
|
||||
note_max=20.0,
|
||||
coefficient=1.0,
|
||||
publish_incomplete=True,
|
||||
@ -185,7 +182,7 @@ def external_ue_inscrit_et_note(
|
||||
# Saisie des notes
|
||||
_, _, _ = sco_saisie_notes.notes_add(
|
||||
current_user,
|
||||
evaluation_id,
|
||||
evaluation.id,
|
||||
list(notes_etuds.items()),
|
||||
do_it=True,
|
||||
)
|
||||
|
@ -149,7 +149,7 @@ def list_operations(evaluation_id):
|
||||
|
||||
def evaluation_list_operations(evaluation_id):
|
||||
"""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]
|
||||
|
||||
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)
|
||||
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
|
||||
FROM notes_notes n, notes_evaluation e, notes_moduleimpl mi,
|
||||
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
|
||||
for row in rows:
|
||||
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 = (
|
||||
"date",
|
||||
"code_nip",
|
||||
@ -207,7 +213,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"):
|
||||
"titre",
|
||||
"evaluation_id",
|
||||
"description",
|
||||
"jour",
|
||||
"date_evaluation",
|
||||
"comment",
|
||||
)
|
||||
titles = {
|
||||
@ -221,7 +227,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"):
|
||||
"evaluation_id": "evaluation_id",
|
||||
"titre": "Module",
|
||||
"description": "Evaluation",
|
||||
"jour": "Date éval.",
|
||||
"date_evaluation": "Date éval.",
|
||||
}
|
||||
tab = GenTable(
|
||||
titles=titles,
|
||||
|
@ -68,10 +68,15 @@ from app.scodoc.codes_cursus import NOTES_TOLERANCE, CODES_EXPL
|
||||
from app.scodoc import sco_xml
|
||||
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
|
||||
STATIC_DIR = (
|
||||
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
|
||||
NOTES_PRECISION = 1e-4 # evite eventuelles erreurs d'arrondis
|
||||
@ -163,6 +168,11 @@ class BiDirectionalEnum(Enum):
|
||||
"""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"""
|
||||
@ -251,15 +261,54 @@ def is_period_overlapping(
|
||||
return p_deb < i_fin and p_fin > i_deb
|
||||
|
||||
|
||||
def translate_assiduites_metric(hr_metric) -> str:
|
||||
if hr_metric == "1/2 J.":
|
||||
return "demi"
|
||||
if hr_metric == "J.":
|
||||
return "journee"
|
||||
if hr_metric == "N.":
|
||||
return "compte"
|
||||
if hr_metric == "H.":
|
||||
return "heure"
|
||||
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
|
||||
|
@ -79,7 +79,7 @@ div.competence {
|
||||
padding-left: calc(var(--arrow-width) + 8px);
|
||||
}
|
||||
|
||||
.niveaux>div:not(:last-child)::after {
|
||||
.niveaux>div:not(:last-child)::before {
|
||||
content: "";
|
||||
|
||||
position: absolute;
|
||||
|
@ -1792,6 +1792,10 @@ td.formsemestre_status_inscrits {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div.formsemestre_status button {
|
||||
margin-left: 12px;;
|
||||
}
|
||||
|
||||
td.rcp_titre_sem a.jury_link {
|
||||
margin-left: 8px;
|
||||
color: red;
|
||||
@ -2468,6 +2472,12 @@ span.ue_type {
|
||||
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 {
|
||||
color: blue;
|
||||
font-size: 70%;
|
||||
|
@ -1,47 +0,0 @@
|
||||
|
||||
// JS Ajax code for SignaleAbsenceGrSemestre
|
||||
// Contributed by YLB
|
||||
|
||||
function ajaxFunction(mod, etudid, dat) {
|
||||
var ajaxRequest; // The variable that makes Ajax possible!
|
||||
|
||||
try {
|
||||
// Opera 8.0+, Firefox, Safari
|
||||
ajaxRequest = new XMLHttpRequest();
|
||||
} catch (e) {
|
||||
// Internet Explorer Browsers
|
||||
try {
|
||||
ajaxRequest = new ActiveXObject("Msxml2.XMLHTTP");
|
||||
} catch (e) {
|
||||
try {
|
||||
ajaxRequest = new ActiveXObject("Microsoft.XMLHTTP");
|
||||
} catch (e) {
|
||||
// Something went wrong
|
||||
alert("Your browser broke!");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Create a function that will receive data sent from the server
|
||||
ajaxRequest.onreadystatechange = function () {
|
||||
if (ajaxRequest.readyState == 4 && ajaxRequest.status == 200) {
|
||||
document.getElementById("AjaxDiv").innerHTML = ajaxRequest.responseText;
|
||||
}
|
||||
}
|
||||
ajaxRequest.open("POST", SCO_URL + "/Absences/doSignaleAbsenceGrSemestre", true);
|
||||
ajaxRequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
||||
var oSelectOne = $("#abs_form")[0].elements["moduleimpl_id"];
|
||||
var index = oSelectOne.selectedIndex;
|
||||
var modul_id = oSelectOne.options[index].value;
|
||||
if (mod == 'add') {
|
||||
ajaxRequest.send("reply=0&moduleimpl_id=" + modul_id + "&abslist:list=" + etudid + ":" + dat);
|
||||
}
|
||||
if (mod == 'remove') {
|
||||
ajaxRequest.send("reply=0&moduleimpl_id=" + modul_id + "&etudids=" + etudid + "&dates=" + dat);
|
||||
}
|
||||
}
|
||||
|
||||
// -----
|
||||
function change_moduleimpl(url) {
|
||||
document.location = url + '&moduleimpl_id=' + document.getElementById('moduleimpl_id').value;
|
||||
}
|
@ -885,7 +885,7 @@ function createAssiduite(etat, etudid) {
|
||||
(data, status) => {
|
||||
//success
|
||||
if (data.success.length > 0) {
|
||||
let obj = data.success["0"].assiduite_id;
|
||||
let obj = data.success["0"].message.assiduite_id;
|
||||
}
|
||||
},
|
||||
(data, status) => {
|
||||
@ -910,7 +910,7 @@ function deleteAssiduite(assiduite_id) {
|
||||
(data, status) => {
|
||||
//success
|
||||
if (data.success.length > 0) {
|
||||
let obj = data.success["0"].assiduite_id;
|
||||
let obj = data.success["0"].message.assiduite_id;
|
||||
}
|
||||
},
|
||||
(data, status) => {
|
||||
@ -1411,7 +1411,10 @@ function getModuleImplId() {
|
||||
function setModuleImplId(assiduite, module = null) {
|
||||
const moduleimpl = module == null ? getModuleImplId() : module;
|
||||
if (moduleimpl === "autre") {
|
||||
if ("external_data" in assiduite && assiduite.external_data != undefined) {
|
||||
if (
|
||||
"external_data" in assiduite &&
|
||||
assiduite.external_data instanceof Object
|
||||
) {
|
||||
if ("module" in assiduite.external_data) {
|
||||
assiduite.external_data.module = "Autre";
|
||||
} else {
|
||||
@ -1423,7 +1426,10 @@ function setModuleImplId(assiduite, module = null) {
|
||||
assiduite.moduleimpl_id = null;
|
||||
} else {
|
||||
assiduite["moduleimpl_id"] = moduleimpl;
|
||||
if ("external_data" in assiduite && assiduite.external_data != undefined) {
|
||||
if (
|
||||
"external_data" in assiduite &&
|
||||
assiduite.external_data instanceof Object
|
||||
) {
|
||||
if ("module" in assiduite.external_data) {
|
||||
delete assiduite.external_data.module;
|
||||
}
|
||||
|
@ -385,7 +385,9 @@ class TableRecap(tb.Table):
|
||||
first_eval_of_mod = True
|
||||
for e in evals:
|
||||
col_id = f"eval_{e.id}"
|
||||
title = f'{modimpl.module.code} {eval_index} {e.jour.isoformat() if e.jour else ""}'
|
||||
title = f"""{modimpl.module.code} {eval_index} {
|
||||
e.date_debut.strftime("%d/%m/%Y") if e.date_debut else ""
|
||||
}"""
|
||||
col_classes = []
|
||||
if first_eval:
|
||||
col_classes.append("first")
|
||||
|
@ -125,11 +125,9 @@ class RowAssi(tb.Row):
|
||||
"absent": ["Absences", 0.0, 0.0],
|
||||
}
|
||||
|
||||
assi_metric = {
|
||||
"H.": "heure",
|
||||
"J.": "journee",
|
||||
"1/2 J.": "demi",
|
||||
}.get(sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id))
|
||||
assi_metric = scu.translate_assiduites_metric(
|
||||
sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id),
|
||||
)
|
||||
|
||||
for etat, valeur in retour.items():
|
||||
compte_etat = scass.get_assiduites_stats(
|
||||
|
@ -12,7 +12,8 @@
|
||||
|
||||
<p>ScoDoc est un logiciel libre écrit en
|
||||
<a href="http://www.python.org" target="_blank" rel="noopener noreferrer">Python</a>.
|
||||
Information et documentation sur <a href="https://scodoc.org" target="_blank">scodoc.org</a>.
|
||||
Information et documentation sur
|
||||
<a href="https://scodoc.org" target="_blank" rel="noopener>scodoc.org</a>.
|
||||
</p>
|
||||
|
||||
<p>Le logiciel est distribué sous
|
||||
|
@ -164,7 +164,7 @@
|
||||
dateType: 'json',
|
||||
contentType: false,
|
||||
processData: false,
|
||||
success: () => { },
|
||||
success: () => { console.log("done") },
|
||||
}
|
||||
)
|
||||
)
|
||||
@ -192,8 +192,8 @@
|
||||
errorAlert();
|
||||
}
|
||||
if (Object.keys(data.success).length > 0) {
|
||||
couverture = data.success[0].couverture
|
||||
justif_id = data.success[0].justif_id;
|
||||
couverture = data.success[0].message.couverture
|
||||
justif_id = data.success[0].message.justif_id;
|
||||
importFiles(justif_id);
|
||||
return;
|
||||
}
|
||||
|
@ -336,19 +336,21 @@
|
||||
|
||||
}
|
||||
const defAnnee = {{ annee }}
|
||||
let annees = {{ annees | safe }}
|
||||
annees = annees.filter((x, i) => annees.indexOf(x) === i)
|
||||
const etudid = {{ sco.etud.id }};
|
||||
const nonwork = [{{ nonworkdays | safe }}];
|
||||
window.onload = () => {
|
||||
const select = document.querySelector('#annee');
|
||||
for (let i = defAnnee + 1; i > defAnnee - 6; i--) {
|
||||
annees.forEach((a) => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = i + "",
|
||||
opt.textContent = i + "";
|
||||
if (i === defAnnee) {
|
||||
opt.value = a + "",
|
||||
opt.textContent = `${a} - ${a + 1}`;
|
||||
if (a === defAnnee) {
|
||||
opt.selected = true;
|
||||
}
|
||||
select.appendChild(opt)
|
||||
}
|
||||
})
|
||||
setterAnnee(defAnnee)
|
||||
};
|
||||
|
||||
|
@ -2,7 +2,16 @@
|
||||
{% import 'bootstrap/wtf.html' as wtf %}
|
||||
|
||||
{% block app_content %}
|
||||
<h1>Configuration du Module d'assiduité</h1>
|
||||
<div class="row">
|
||||
|
||||
<h1>Configuration du suivi de l'assiduité</h1>
|
||||
|
||||
<div class="help"> Ces paramètres seront utilisés par tous les départements et
|
||||
affectent notamment les comptages d'absences de tous les bulletins des
|
||||
étudiants : ne changer que lorsque c'est vraiment nécessaire.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
|
@ -1,5 +1,6 @@
|
||||
<h2>Présence lors de l'évaluation {{eval.title}} </h2>
|
||||
<h3>Réalisé le {{eval.jour}} de {{eval.heure_debut}} à {{eval.heure_fin}}</h3>
|
||||
<h2>Présence du groupe {{group_title}} le {{date_debut.strftime("%d/%m/%Y")}}
|
||||
de {{date_debut.strftime("%H:%M")}} à {{date_fin.strftime("%H:%M")}}
|
||||
</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@ -7,7 +8,7 @@
|
||||
Nom
|
||||
</th>
|
||||
<th>
|
||||
Assiduité
|
||||
Présence
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -40,8 +40,9 @@
|
||||
|
||||
{% if readonly == "false" %}
|
||||
<div style="margin: 1vh 0;">
|
||||
<div id="forcemodule" style="display: none; margin:10px 0px;">Une préférence du semestre vous impose d'indiquer
|
||||
le module !</div>
|
||||
<div id="forcemodule" style="display: none; margin:10px 0px;">
|
||||
Vous devez spécifier le module ! (voir réglage préférence du semestre)
|
||||
</div>
|
||||
<div>Module :{{moduleimpl_select|safe}}</div>
|
||||
</div>
|
||||
{% else %}
|
||||
|
@ -21,6 +21,10 @@
|
||||
|
||||
{{tableau | safe}}
|
||||
|
||||
<div class=""help">
|
||||
Les comptes sont exprimés en {{ assi_metric }}.
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const date_debut = "{{date_debut}}";
|
||||
const date_fin = "{{date_fin}}";
|
||||
|
@ -680,7 +680,7 @@
|
||||
rbtn.parentElement.setAttribute('etat', etat);
|
||||
asyncCreateAssiduite(assiduite, (data) => {
|
||||
if (Object.keys(data.success).length > 0) {
|
||||
const assi_id = data.success['0'].assiduite_id;
|
||||
const assi_id = data.success['0'].message.assiduite_id;
|
||||
etudLine.setAttribute('assiduite_id', assi_id);
|
||||
assiduite["assiduite_id"] = assi_id;
|
||||
assiduites[etudid].push(assiduite);
|
||||
@ -917,7 +917,7 @@
|
||||
).done((c, e) => {
|
||||
Object.keys(c[0].success).forEach((k) => {
|
||||
const assiduite = createList[Number.parseInt(k)];
|
||||
assiduite["assiduite_id"] = c[0].success[k].assiduite_id;
|
||||
assiduite["assiduite_id"] = c[0].success[k].message.assiduite_id;
|
||||
assiduites[assiduite.etudid].push(assiduite);
|
||||
})
|
||||
Object.keys(e[0].success).forEach((k) => {
|
||||
|
@ -162,7 +162,11 @@
|
||||
userIdDiv.textContent = `saisi le ${formatDateModal(
|
||||
assiduite.entry_date,
|
||||
"à"
|
||||
)} \npar ${assiduite.user_id}`;
|
||||
)}`;
|
||||
|
||||
if (assiduite.user_id != null) {
|
||||
userIdDiv.textContent += `\npar ${assiduite.user_id}`
|
||||
}
|
||||
bubble.appendChild(userIdDiv);
|
||||
|
||||
bubble.style.left = `${event.clientX - bubble.offsetWidth / 2}px`;
|
||||
|
@ -57,7 +57,7 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="link">
|
||||
<a class="stdlink" target="_blank" href="{{
|
||||
<a class="stdlink" target="_blank" rel="noopener noreferrer" href="{{
|
||||
url_for('notes.refcomp_show',
|
||||
scodoc_dept=g.scodoc_dept, refcomp_id=formation.referentiel_competence.id )
|
||||
}}">référentiel de compétences</a>
|
||||
|
@ -57,18 +57,18 @@
|
||||
<div class="sco_help">Ces images peuvent être intégrées dans les documents
|
||||
générés par ScoDoc: bulletins, PV, etc.
|
||||
</div>
|
||||
<p><a class="stdlink" href="{{url_for('scodoc.configure_logos')}}">configuration des images et logos</a>
|
||||
<p><a class="stdlink" href="{{url_for('scodoc.configure_logos')}}">Configuration des images et logos</a>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Exports Apogée</h2>
|
||||
<p><a class="stdlink" href="{{url_for('scodoc.config_codes_decisions')}}">configuration des codes de décision</a>
|
||||
<p><a class="stdlink" href="{{url_for('scodoc.config_codes_decisions')}}">Configuration des codes de décision</a>
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Assiduités</h2>
|
||||
<p><a class="stdlink" href="{{url_for('scodoc.config_assiduites')}}">configuration du module d'assiduités</a>
|
||||
<p><a class="stdlink" href="{{url_for('scodoc.config_assiduites')}}">Configuration du suivi de l'assiduité</a>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
@ -195,8 +195,9 @@
|
||||
Code postal : {{ entreprise.codepostal }}<br>
|
||||
Ville : {{ entreprise.ville }}<br>
|
||||
Pays : {{ entreprise.pays }}<br>
|
||||
<a href="{{ url_for('entreprises.fiche_entreprise', entreprise_id=entreprise.id) }}" target="_blank">Fiche
|
||||
entreprise</a>
|
||||
<a href="{{ url_for('entreprises.fiche_entreprise', entreprise_id=entreprise.id) }}"
|
||||
rel="noopener noreferrer" target="_blank"
|
||||
>Fiche entreprise</a>
|
||||
</div>
|
||||
{% for site in entreprise.sites %}
|
||||
<div class="site">
|
||||
@ -221,8 +222,9 @@
|
||||
Code postal : {{ site.codepostal }}<br>
|
||||
Ville : {{ site.ville }}<br>
|
||||
Pays : {{ site.pays }}<br>
|
||||
<a href="{{ url_for('entreprises.fiche_entreprise', entreprise_id=site.entreprise_id) }}" target="_blank">Fiche
|
||||
entreprise</a>
|
||||
<a href="{{ url_for('entreprises.fiche_entreprise', entreprise_id=site.entreprise_id)
|
||||
}}" rel="noopener noreferrer" target="_blank"
|
||||
>Fiche entreprise</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
@ -255,7 +257,7 @@
|
||||
Notes : {{ correspondant.notes }}<br>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('entreprises.fiche_entreprise', entreprise_id=correspondant.site.entreprise.id) }}"
|
||||
target="_blank">Fiche entreprise</a>
|
||||
target="_blank" rel="noopener noreferrer">Fiche entreprise</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
@ -22,7 +22,7 @@
|
||||
type, et de saisir les coefficients pondérant l'influence de la
|
||||
ressource ou SAÉ vers les Unités d'Enseignement (UE).
|
||||
Voir les détails sur
|
||||
<a href="https://scodoc.org/BUT" target="_blank">la documentation</a>.
|
||||
<a href="https://scodoc.org/BUT" target="_blank" rel="noopener">la documentation</a>.
|
||||
</p>
|
||||
{%endif%}
|
||||
</div>
|
||||
|
@ -28,7 +28,7 @@
|
||||
<h4>Fichiers chargés:</h4>
|
||||
<ul>
|
||||
{% for (etud, name) in stored_etud_filename %}
|
||||
<li>{{etud["nomprenom"]}}: <tt>{{name}}</tt></li>
|
||||
<li>{{etud.nomprenom}}: <tt>{{name}}</tt></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
@ -18,6 +18,6 @@ Importation des photo effectuée
|
||||
{% if stored_etud_filename %}
|
||||
# Fichiers chargés:
|
||||
{% for (etud, name) in stored_etud_filename %}
|
||||
- {{etud["nomprenom"]}}: <tt>{{name}}</tt></li>
|
||||
- {{etud.nomprenom}}: <tt>{{name}}</tt></li>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
@ -86,7 +86,8 @@
|
||||
<div class="sidebar-bottom"><a href="{{ url_for( 'scodoc.about',
|
||||
scodoc_dept=g.scodoc_dept ) }}" class="sidebar">À propos</a>
|
||||
<br />
|
||||
<a href="{{ scu.SCO_USER_MANUAL }}" target="_blank" class="sidebar">Aide</a>
|
||||
<a href="{{ scu.SCO_USER_MANUAL }}"
|
||||
target="_blank" rel="noopener" class="sidebar">Aide</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="logo-logo">
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -327,11 +327,9 @@ def bilan_etud():
|
||||
date_debut: str = f"{scu.annee_scolaire()}-09-01"
|
||||
date_fin: str = f"{scu.annee_scolaire()+1}-06-30"
|
||||
|
||||
assi_metric = {
|
||||
"H.": "heure",
|
||||
"J.": "journee",
|
||||
"1/2 J.": "demi",
|
||||
}.get(sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id))
|
||||
assi_metric = scu.translate_assiduites_metric(
|
||||
sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id),
|
||||
)
|
||||
|
||||
return HTMLBuilder(
|
||||
header,
|
||||
@ -419,6 +417,16 @@ def calendrier_etud():
|
||||
],
|
||||
)
|
||||
|
||||
annees: list[int] = sorted(
|
||||
[ins.formsemestre.date_debut.year for ins in etud.formsemestre_inscriptions],
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
annees_str: str = "["
|
||||
for ann in annees:
|
||||
annees_str += f"{ann},"
|
||||
annees_str += "]"
|
||||
|
||||
return HTMLBuilder(
|
||||
header,
|
||||
render_template(
|
||||
@ -427,6 +435,7 @@ def calendrier_etud():
|
||||
annee=scu.annee_scolaire(),
|
||||
nonworkdays=_non_work_days(),
|
||||
minitimeline=_mini_timeline(),
|
||||
annees=annees_str,
|
||||
),
|
||||
).build()
|
||||
|
||||
@ -538,7 +547,6 @@ def signal_assiduites_group():
|
||||
+ [
|
||||
# Voir fonctionnement JS
|
||||
"js/etud_info.js",
|
||||
"js/abs_ajax.js",
|
||||
"js/groups_view.js",
|
||||
"js/assiduites.js",
|
||||
"libjs/moment.new.min.js",
|
||||
@ -727,16 +735,13 @@ def visu_assiduites_group():
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def get_etat_abs_date():
|
||||
evaluation = {
|
||||
infos_date = {
|
||||
"jour": request.args.get("jour"),
|
||||
"heure_debut": request.args.get("heure_debut"),
|
||||
"heure_fin": request.args.get("heure_fin"),
|
||||
"title": request.args.get("desc"),
|
||||
}
|
||||
date: str = evaluation["jour"]
|
||||
group_ids: list[int] = request.args.get("group_ids", None)
|
||||
etudiants: list[dict] = []
|
||||
|
||||
if group_ids is None:
|
||||
group_ids = []
|
||||
else:
|
||||
@ -751,10 +756,10 @@ def get_etat_abs_date():
|
||||
]
|
||||
|
||||
date_debut = scu.is_iso_formated(
|
||||
f"{evaluation['jour']}T{evaluation['heure_debut'].replace('h',':')}", True
|
||||
f"{infos_date['jour']}T{infos_date['heure_debut'].replace('h',':')}", True
|
||||
)
|
||||
date_fin = scu.is_iso_formated(
|
||||
f"{evaluation['jour']}T{evaluation['heure_fin'].replace('h',':')}", True
|
||||
f"{infos_date['jour']}T{infos_date['heure_fin'].replace('h',':')}", True
|
||||
)
|
||||
|
||||
assiduites: Assiduite = Assiduite.query.filter(
|
||||
@ -764,15 +769,20 @@ def get_etat_abs_date():
|
||||
assiduites, Assiduite, date_debut, date_fin, False
|
||||
)
|
||||
|
||||
etudiants: list[dict] = []
|
||||
for etud in etuds:
|
||||
assi = assiduites.filter_by(etudid=etud["etudid"]).first()
|
||||
|
||||
etat = ""
|
||||
if assi != None and assi.etat != 0:
|
||||
if assi is not None and assi.etat != 0:
|
||||
etat = scu.EtatAssiduite.inverse().get(assi.etat).name
|
||||
|
||||
etudiant = {
|
||||
"nom": f'<a href="{url_for("assiduites.calendrier_etud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"])}"><font color="#A00000">{etud["nomprenom"]}</font></a>',
|
||||
"nom": f"""<a href="{url_for(
|
||||
"assiduites.calendrier_etud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=etud["etudid"])
|
||||
}"><font color="#A00000">{etud["nomprenom"]}</font></a>""",
|
||||
"etat": etat,
|
||||
}
|
||||
|
||||
@ -781,7 +791,7 @@ def get_etat_abs_date():
|
||||
etudiants = list(sorted(etudiants, key=lambda x: x["nom"]))
|
||||
|
||||
header: str = html_sco_header.sco_header(
|
||||
page_title=evaluation["title"],
|
||||
page_title=infos_date["title"],
|
||||
init_qtip=True,
|
||||
)
|
||||
|
||||
@ -790,7 +800,9 @@ def get_etat_abs_date():
|
||||
render_template(
|
||||
"assiduites/pages/etat_absence_date.j2",
|
||||
etudiants=etudiants,
|
||||
eval=evaluation,
|
||||
group_title=groups_infos.groups_titles,
|
||||
date_debut=date_debut,
|
||||
date_fin=date_fin,
|
||||
),
|
||||
html_sco_header.sco_footer(),
|
||||
).build()
|
||||
@ -807,8 +819,6 @@ def visu_assi_group():
|
||||
fmt = request.args.get("format", "html")
|
||||
|
||||
group_ids: list[int] = request.args.get("group_ids", None)
|
||||
etudiants: list[dict] = []
|
||||
|
||||
if group_ids is None:
|
||||
group_ids = []
|
||||
else:
|
||||
@ -842,16 +852,23 @@ def visu_assi_group():
|
||||
grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>"
|
||||
)
|
||||
|
||||
print()
|
||||
|
||||
return render_template(
|
||||
"assiduites/pages/visu_assi.j2",
|
||||
tableau=table.html(),
|
||||
gr_tit=gr_tit,
|
||||
assi_metric=scu.translate_assiduites_metric(
|
||||
scu.translate_assiduites_metric(
|
||||
sco_preferences.get_preference(
|
||||
"assi_metrique", dept_id=g.scodoc_dept_id
|
||||
),
|
||||
),
|
||||
inverse=False,
|
||||
short=False,
|
||||
),
|
||||
date_debut=dates["debut"],
|
||||
date_fin=dates["fin"],
|
||||
gr_tit=gr_tit,
|
||||
group_ids=request.args.get("group_ids", None),
|
||||
sco=ScoData(formsemestre=groups_infos.get_formsemestre()),
|
||||
tableau=table.html(),
|
||||
title=f"Assiduité {grp} {groups_infos.groups_titles}",
|
||||
)
|
||||
|
||||
|
@ -58,6 +58,7 @@ from app.but.forms import jury_but_forms
|
||||
from app.comp import jury, res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import (
|
||||
Evaluation,
|
||||
Formation,
|
||||
ScolarAutorisationInscription,
|
||||
ScolarNews,
|
||||
@ -97,9 +98,9 @@ from app.scodoc.sco_exceptions import (
|
||||
)
|
||||
from app.scodoc import html_sco_header
|
||||
from app.pe import pe_view
|
||||
from app.scodoc import sco_abs
|
||||
from app.scodoc import sco_apogee_compare
|
||||
from app.scodoc import sco_archives
|
||||
from app.scodoc import sco_assiduites
|
||||
from app.scodoc import sco_bulletins
|
||||
from app.scodoc import sco_bulletins_pdf
|
||||
from app.scodoc import sco_cache
|
||||
@ -134,6 +135,7 @@ from app.scodoc import sco_lycee
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_moduleimpl_inscriptions
|
||||
from app.scodoc import sco_moduleimpl_status
|
||||
from app.scodoc import sco_permissions_check
|
||||
from app.scodoc import sco_placement
|
||||
from app.scodoc import sco_poursuite_dut
|
||||
from app.scodoc import sco_preferences
|
||||
@ -378,11 +380,40 @@ sco_publish(
|
||||
sco_evaluations.formsemestre_evaluations_delai_correction,
|
||||
Permission.ScoView,
|
||||
)
|
||||
sco_publish(
|
||||
"/moduleimpl_evaluation_renumber",
|
||||
sco_evaluation_db.moduleimpl_evaluation_renumber,
|
||||
Permission.ScoView,
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/moduleimpl_evaluation_renumber", methods=["GET", "POST"])
|
||||
@scodoc
|
||||
@permission_required_compat_scodoc7(Permission.ScoView)
|
||||
@scodoc7func
|
||||
def moduleimpl_evaluation_renumber(moduleimpl_id):
|
||||
"Renumérote les évaluations, triant par date"
|
||||
modimpl: ModuleImpl = (
|
||||
ModuleImpl.query.filter_by(id=moduleimpl_id)
|
||||
.join(FormSemestre)
|
||||
.filter_by(dept_id=g.scodoc_dept_id)
|
||||
.first_or_404()
|
||||
)
|
||||
if not modimpl.can_edit_evaluation(current_user):
|
||||
raise ScoPermissionDenied(
|
||||
dest_url=url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=modimpl.id,
|
||||
)
|
||||
)
|
||||
Evaluation.moduleimpl_evaluation_renumber(modimpl)
|
||||
# redirect to moduleimpl page:
|
||||
if redirect:
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=moduleimpl_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
sco_publish(
|
||||
"/moduleimpl_evaluation_move",
|
||||
sco_evaluation_db.moduleimpl_evaluation_move,
|
||||
@ -1122,175 +1153,64 @@ def edit_moduleimpl_resp(moduleimpl_id: int):
|
||||
)
|
||||
|
||||
|
||||
_EXPR_HELP = """<p class="help">Expérimental: formule de calcul de la moyenne %(target)s</p>
|
||||
<p class="help">Attention: l'utilisation de formules ralentit considérablement
|
||||
les traitements. A utiliser uniquement dans les cas ne pouvant pas être traités autrement.</p>
|
||||
<p class="help">Dans la formule, les variables suivantes sont définies:</p>
|
||||
<ul class="help">
|
||||
<li><tt>moy</tt> la moyenne, calculée selon la règle standard (moyenne pondérée)</li>
|
||||
<li><tt>moy_is_valid</tt> vrai si la moyenne est valide (numérique)</li>
|
||||
<li><tt>moy_val</tt> la valeur de la moyenne (nombre, valant 0 si invalide)</li>
|
||||
<li><tt>notes</tt> vecteur des notes (/20) aux %(objs)s</li>
|
||||
<li><tt>coefs</tt> vecteur des coefficients des %(objs)s, les coefs des %(objs)s sans notes (ATT, EXC) étant mis à zéro</li>
|
||||
<li><tt>cmask</tt> vecteur de 0/1, 0 si le coef correspondant a été annulé</li>
|
||||
<li>Nombre d'absences: <tt>nb_abs</tt>, <tt>nb_abs_just</tt>, <tt>nb_abs_nojust</tt> (en demi-journées)</li>
|
||||
</ul>
|
||||
<p class="help">Les éléments des vecteurs sont ordonnés dans l'ordre des %(objs)s%(ordre)s.</p>
|
||||
<p class="help">Les fonctions suivantes sont utilisables: <tt>abs, cmp, dot, len, map, max, min, pow, reduce, round, sum, ifelse</tt>.</p>
|
||||
<p class="help">La notation <tt>V(1,2,3)</tt> représente un vecteur <tt>(1,2,3)</tt>.</p>
|
||||
<p class="help"></p>Pour indiquer que la note calculée n'existe pas, utiliser la chaîne <tt>'NA'</tt>.</p>
|
||||
<p class="help">Vous pouvez désactiver la formule (et revenir au mode de calcul "classique")
|
||||
en supprimant le texte ou en faisant précéder la première ligne par <tt>#</tt></p>
|
||||
"""
|
||||
|
||||
|
||||
@bp.route("/edit_moduleimpl_expr", methods=["GET", "POST"])
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@scodoc7func
|
||||
def edit_moduleimpl_expr(moduleimpl_id):
|
||||
"""Edition formule calcul moyenne module
|
||||
Accessible par Admin, dir des etud et responsable module
|
||||
|
||||
Inutilisé en ScoDoc 9.
|
||||
"""
|
||||
M, sem = sco_moduleimpl.can_change_ens(moduleimpl_id)
|
||||
H = [
|
||||
html_sco_header.html_sem_header(
|
||||
'Modification règle de calcul du <a href="moduleimpl_status?moduleimpl_id=%s">module %s</a>'
|
||||
% (moduleimpl_id, M["module"]["titre"]),
|
||||
),
|
||||
_EXPR_HELP
|
||||
% {
|
||||
"target": "du module",
|
||||
"objs": "évaluations",
|
||||
"ordre": " (le premier élément est la plus ancienne évaluation)",
|
||||
},
|
||||
]
|
||||
initvalues = M
|
||||
form = [
|
||||
("moduleimpl_id", {"input_type": "hidden"}),
|
||||
(
|
||||
"computation_expr",
|
||||
{
|
||||
"title": "Formule de calcul",
|
||||
"input_type": "textarea",
|
||||
"rows": 4,
|
||||
"cols": 60,
|
||||
"explanation": "formule de calcul (expérimental)",
|
||||
},
|
||||
),
|
||||
]
|
||||
tf = TrivialFormulator(
|
||||
request.base_url,
|
||||
scu.get_request_args(),
|
||||
form,
|
||||
submitlabel="Modifier formule de calcul",
|
||||
cancelbutton="Annuler",
|
||||
initvalues=initvalues,
|
||||
)
|
||||
if tf[0] == 0:
|
||||
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
|
||||
elif tf[0] == -1:
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=moduleimpl_id,
|
||||
)
|
||||
)
|
||||
else:
|
||||
sco_moduleimpl.do_moduleimpl_edit(
|
||||
{
|
||||
"moduleimpl_id": moduleimpl_id,
|
||||
"computation_expr": tf[2]["computation_expr"],
|
||||
},
|
||||
formsemestre_id=sem["formsemestre_id"],
|
||||
)
|
||||
sco_cache.invalidate_formsemestre(
|
||||
formsemestre_id=sem["formsemestre_id"]
|
||||
) # > modif regle calcul
|
||||
flash("règle de calcul modifiée")
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=moduleimpl_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/delete_moduleimpl_expr", methods=["GET", "POST"])
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@scodoc7func
|
||||
def delete_moduleimpl_expr(moduleimpl_id):
|
||||
"""Suppression formule calcul moyenne module
|
||||
Accessible par Admin, dir des etud et responsable module
|
||||
"""
|
||||
modimpl = ModuleImpl.query.get_or_404(moduleimpl_id)
|
||||
sco_moduleimpl.can_change_ens(moduleimpl_id)
|
||||
modimpl.computation_expr = None
|
||||
db.session.add(modimpl)
|
||||
db.session.commit()
|
||||
flash("Ancienne formule supprimée")
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=moduleimpl_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/view_module_abs")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@scodoc7func
|
||||
def view_module_abs(moduleimpl_id, format="html"):
|
||||
def view_module_abs(moduleimpl_id, fmt="html"):
|
||||
"""Visualisation des absences a un module"""
|
||||
M = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[0]
|
||||
sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
|
||||
debut_sem = ndb.DateDMYtoISO(sem["date_debut"])
|
||||
fin_sem = ndb.DateDMYtoISO(sem["date_fin"])
|
||||
list_insc = sco_moduleimpl.moduleimpl_listeetuds(moduleimpl_id)
|
||||
modimpl: ModuleImpl = (
|
||||
ModuleImpl.query.filter_by(id=moduleimpl_id)
|
||||
.join(FormSemestre)
|
||||
.filter_by(dept_id=g.scodoc_dept_id)
|
||||
).first_or_404()
|
||||
|
||||
T = []
|
||||
for etudid in list_insc:
|
||||
nb_abs = sco_abs.count_abs(
|
||||
etudid=etudid,
|
||||
debut=debut_sem,
|
||||
fin=fin_sem,
|
||||
moduleimpl_id=moduleimpl_id,
|
||||
debut_sem = modimpl.formsemestre.date_debut
|
||||
fin_sem = modimpl.formsemestre.date_fin
|
||||
inscrits: list[Identite] = sorted(
|
||||
[i.etud for i in modimpl.inscriptions], key=lambda e: e.sort_key
|
||||
)
|
||||
|
||||
rows = []
|
||||
for etud in inscrits:
|
||||
# TODO-ASSIDUITE ne va pas car ne filtre pas sur le moduleimpl
|
||||
# nb_abs, nb_abs_just = sco_assiduites.formsemestre_get_assiduites_count(etud.id, modimpl.formsemestre)
|
||||
nb_abs, nb_abs_just = 0, 0 # XXX TODO-ASSIDUITE
|
||||
# nb_abs = sco_abs.count_abs(
|
||||
# etudid=etud.id,
|
||||
# debut=debut_sem,
|
||||
# fin=fin_sem,
|
||||
# moduleimpl_id=moduleimpl_id,
|
||||
# )
|
||||
# if nb_abs:
|
||||
# nb_abs_just = sco_abs.count_abs_just(
|
||||
# etudid=etud.id,
|
||||
# debut=debut_sem,
|
||||
# fin=fin_sem,
|
||||
# moduleimpl_id=moduleimpl_id,
|
||||
# )
|
||||
rows.append(
|
||||
{
|
||||
"nomprenom": etud.nomprenom,
|
||||
"just": nb_abs_just,
|
||||
"nojust": nb_abs - nb_abs_just,
|
||||
"total": nb_abs,
|
||||
"_nomprenom_target": url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id
|
||||
),
|
||||
}
|
||||
)
|
||||
if nb_abs:
|
||||
nb_abs_just = sco_abs.count_abs_just(
|
||||
etudid=etudid,
|
||||
debut=debut_sem,
|
||||
fin=fin_sem,
|
||||
moduleimpl_id=moduleimpl_id,
|
||||
)
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
T.append(
|
||||
{
|
||||
"nomprenom": etud["nomprenom"],
|
||||
"just": nb_abs_just,
|
||||
"nojust": nb_abs - nb_abs_just,
|
||||
"total": nb_abs,
|
||||
"_nomprenom_target": url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
H = [
|
||||
html_sco_header.html_sem_header(
|
||||
'Absences du <a href="moduleimpl_status?moduleimpl_id=%s">module %s</a>'
|
||||
% (moduleimpl_id, M["module"]["titre"]),
|
||||
page_title="Absences du module %s" % (M["module"]["titre"]),
|
||||
f"""Absences du <a href="{
|
||||
url_for("notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id
|
||||
)}">module {modimpl.module.titre_str()}</a>""",
|
||||
page_title=f"Absences du module {modimpl.module.titre_str()}",
|
||||
)
|
||||
]
|
||||
if not T and format == "html":
|
||||
if not rows and fmt == "html":
|
||||
return (
|
||||
"\n".join(H)
|
||||
+ "<p>Aucune absence signalée</p>"
|
||||
@ -1305,16 +1225,16 @@ def view_module_abs(moduleimpl_id, format="html"):
|
||||
"total": "Total",
|
||||
},
|
||||
columns_ids=("nomprenom", "just", "nojust", "total"),
|
||||
rows=T,
|
||||
rows=rows,
|
||||
html_class="table_leftalign",
|
||||
base_url="%s?moduleimpl_id=%s" % (request.base_url, moduleimpl_id),
|
||||
filename="absmodule_" + scu.make_filename(M["module"]["titre"]),
|
||||
caption="Absences dans le module %s" % M["module"]["titre"],
|
||||
base_url=f"{request.base_url}?moduleimpl_id={moduleimpl_id}",
|
||||
filename="absmodule_" + scu.make_filename(modimpl.module.titre_str()),
|
||||
caption=f"Absences dans le module {modimpl.module.titre_str()}",
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
)
|
||||
|
||||
if format != "html":
|
||||
return tab.make_page(format=format)
|
||||
if fmt != "html":
|
||||
return tab.make_page(format=fmt)
|
||||
|
||||
return "\n".join(H) + tab.html() + html_sco_header.sco_footer()
|
||||
|
||||
@ -1735,7 +1655,7 @@ sco_publish(
|
||||
@scodoc7func
|
||||
def evaluation_delete(evaluation_id):
|
||||
"""Form delete evaluation"""
|
||||
El = sco_evaluation_db.do_evaluation_list(args={"evaluation_id": evaluation_id})
|
||||
El = sco_evaluation_db.get_evaluation_dict(args={"evaluation_id": evaluation_id})
|
||||
if not El:
|
||||
raise ScoValueError("Evaluation inexistante ! (%s)" % evaluation_id)
|
||||
E = El[0]
|
||||
|
@ -1016,27 +1016,28 @@ def etud_photo_orig_page(etudid=None):
|
||||
@scodoc7func
|
||||
def form_change_photo(etudid=None):
|
||||
"""Formulaire changement photo étudiant"""
|
||||
etud = sco_etud.get_etud_info(filled=True)[0]
|
||||
if sco_photos.etud_photo_is_local(etud):
|
||||
etud["photoloc"] = "dans ScoDoc"
|
||||
etud = Identite.get_etud(etudid)
|
||||
if sco_photos.etud_photo_is_local(etud.photo_filename):
|
||||
photo_loc = "dans ScoDoc"
|
||||
else:
|
||||
etud["photoloc"] = "externe"
|
||||
photo_loc = "externe"
|
||||
H = [
|
||||
html_sco_header.sco_header(page_title="Changement de photo"),
|
||||
"""<h2>Changement de la photo de %(nomprenom)s</h2>
|
||||
<p>Photo actuelle (%(photoloc)s):
|
||||
"""
|
||||
% etud,
|
||||
sco_photos.etud_photo_html(etud, title="photo actuelle"),
|
||||
"""</p><p>Le fichier ne doit pas dépasser 500Ko (recadrer l'image, format "portrait" de préférence).</p>
|
||||
<p>L'image sera automagiquement réduite pour obtenir une hauteur de 90 pixels.</p>
|
||||
""",
|
||||
f"""<h2>Changement de la photo de {etud.nomprenom}</h2>
|
||||
<p>Photo actuelle ({photo_loc}):
|
||||
{sco_photos.etud_photo_html(etudid=etud.id, title="photo actuelle")}
|
||||
</p>
|
||||
<p>Le fichier ne doit pas dépasser {sco_photos.MAX_FILE_SIZE//1024}Ko
|
||||
(recadrer l'image, format "portrait" de préférence).
|
||||
</p>
|
||||
<p>L'image sera automagiquement réduite pour obtenir une hauteur de 90 pixels.</p>
|
||||
""",
|
||||
]
|
||||
tf = TrivialFormulator(
|
||||
request.base_url,
|
||||
scu.get_request_args(),
|
||||
(
|
||||
("etudid", {"default": etudid, "input_type": "hidden"}),
|
||||
("etudid", {"default": etud.id, "input_type": "hidden"}),
|
||||
(
|
||||
"photofile",
|
||||
{"input_type": "file", "title": "Fichier image", "size": 20},
|
||||
@ -1045,16 +1046,18 @@ def form_change_photo(etudid=None):
|
||||
submitlabel="Valider",
|
||||
cancelbutton="Annuler",
|
||||
)
|
||||
dest_url = url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
|
||||
)
|
||||
dest_url = url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
|
||||
if tf[0] == 0:
|
||||
return (
|
||||
"\n".join(H)
|
||||
+ tf[1]
|
||||
+ '<p><a class="stdlink" href="form_suppress_photo?etudid=%s">Supprimer cette photo</a></p>'
|
||||
% etudid
|
||||
+ html_sco_header.sco_footer()
|
||||
+ f"""
|
||||
{tf[1]}
|
||||
<p><a class="stdlink" href="{
|
||||
url_for("scolar.form_suppress_photo",
|
||||
scodoc_dept=g.scodoc_dept, etudid=etud.id)
|
||||
}">Supprimer cette photo</a></p>
|
||||
{html_sco_header.sco_footer()}
|
||||
"""
|
||||
)
|
||||
elif tf[0] == -1:
|
||||
return flask.redirect(dest_url)
|
||||
|
58
migrations/versions/5c44d0d215ca_evaluation_date.py
Normal file
58
migrations/versions/5c44d0d215ca_evaluation_date.py
Normal file
@ -0,0 +1,58 @@
|
||||
"""evaluation date: modifie le codage des dates d'évaluations
|
||||
|
||||
Revision ID: 5c44d0d215ca
|
||||
Revises: 45e0a855b8eb
|
||||
Create Date: 2023-08-22 14:39:23.831483
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "5c44d0d215ca"
|
||||
down_revision = "45e0a855b8eb"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
"modifie les colonnes codant les dates d'évaluations"
|
||||
with op.batch_alter_table("notes_evaluation", schema=None) as batch_op:
|
||||
batch_op.add_column(
|
||||
sa.Column("date_debut", sa.DateTime(timezone=True), nullable=True)
|
||||
)
|
||||
batch_op.add_column(
|
||||
sa.Column("date_fin", sa.DateTime(timezone=True), nullable=True)
|
||||
)
|
||||
# recode les dates existantes
|
||||
op.execute("UPDATE notes_evaluation SET date_debut = jour+heure_debut;")
|
||||
op.execute("UPDATE notes_evaluation SET date_fin = jour+heure_fin;")
|
||||
with op.batch_alter_table("notes_evaluation", schema=None) as batch_op:
|
||||
batch_op.drop_column("jour")
|
||||
batch_op.drop_column("heure_fin")
|
||||
batch_op.drop_column("heure_debut")
|
||||
|
||||
|
||||
def downgrade():
|
||||
"modifie les colonnes codant les dates d'évaluations"
|
||||
with op.batch_alter_table("notes_evaluation", schema=None) as batch_op:
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
"heure_debut", postgresql.TIME(), autoincrement=False, nullable=True
|
||||
)
|
||||
)
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
"heure_fin", postgresql.TIME(), autoincrement=False, nullable=True
|
||||
)
|
||||
)
|
||||
batch_op.add_column(
|
||||
sa.Column("jour", sa.DATE(), autoincrement=False, nullable=True)
|
||||
)
|
||||
op.execute("UPDATE notes_evaluation SET jour = DATE(date_debut);")
|
||||
op.execute("UPDATE notes_evaluation SET heure_debut = date_debut::time;")
|
||||
op.execute("UPDATE notes_evaluation SET heure_fin = date_fin::time;")
|
||||
with op.batch_alter_table("notes_evaluation", schema=None) as batch_op:
|
||||
batch_op.drop_column("date_fin")
|
||||
batch_op.drop_column("date_debut")
|
23
pylintrc
23
pylintrc
@ -1,23 +0,0 @@
|
||||
[MASTER]
|
||||
|
||||
# List of plugins (as comma separated values of python module names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=pylint_flask_sqlalchemy, pylint_flask
|
||||
|
||||
[TYPECHECK]
|
||||
# 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,e,f,i,j,k,t,u,v,x,y,z,H,F,ue
|
@ -1,7 +1,7 @@
|
||||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
SCOVERSION = "9.6.7"
|
||||
SCOVERSION = "9.6.15"
|
||||
|
||||
SCONAME = "ScoDoc"
|
||||
|
||||
|
20
scodoc.py
20
scodoc.py
@ -5,7 +5,7 @@
|
||||
|
||||
|
||||
"""
|
||||
|
||||
import datetime
|
||||
from pprint import pprint as pp
|
||||
import re
|
||||
import sys
|
||||
@ -82,6 +82,7 @@ def make_shell_context():
|
||||
"ctx": app.test_request_context(),
|
||||
"current_app": flask.current_app,
|
||||
"current_user": current_user,
|
||||
"datetime": datetime,
|
||||
"Departement": Departement,
|
||||
"db": db,
|
||||
"Evaluation": Evaluation,
|
||||
@ -536,7 +537,7 @@ def photos_import_files(formsemestre_id: int, xlsfile: str, zipfile: str):
|
||||
admin_user = get_super_admin()
|
||||
login_user(admin_user)
|
||||
|
||||
def callback(etud, data, filename):
|
||||
def callback(etud: Identite, data, filename):
|
||||
return sco_photos.store_photo(etud, data, filename)
|
||||
|
||||
(
|
||||
@ -660,7 +661,12 @@ def profile(host, port, length, profile_dir):
|
||||
@click.option(
|
||||
"-n",
|
||||
"--noon",
|
||||
help="Spécifie l'heure de fin du matin (et donc début de l'après-midi) format `hh:mm`",
|
||||
help="Spécifie l'heure de fin du matin format `hh:mm`",
|
||||
)
|
||||
@click.option(
|
||||
"-a",
|
||||
"--afternoon",
|
||||
help="Spécifie l'heure de début de l'après-midi format `hh:mm` valeur identique à --noon si non spécifié",
|
||||
)
|
||||
@click.option(
|
||||
"-e",
|
||||
@ -669,10 +675,14 @@ def profile(host, port, length, profile_dir):
|
||||
)
|
||||
@with_appcontext
|
||||
def migrate_abs_to_assiduites(
|
||||
dept: str = None, morning: str = None, noon: str = None, evening: str = None
|
||||
dept: str = None,
|
||||
morning: str = None,
|
||||
noon: str = None,
|
||||
afternoon: str = None,
|
||||
evening: str = None,
|
||||
): # migrate-abs-to-assiduites
|
||||
"""Permet de migrer les absences vers le nouveau module d'assiduités"""
|
||||
tools.migrate_abs_to_assiduites(dept, morning, noon, evening)
|
||||
tools.migrate_abs_to_assiduites(dept, morning, noon, afternoon, evening)
|
||||
# import cProfile
|
||||
# cProfile.runctx(
|
||||
# f"tools.migrate_abs_to_assiduites({dept})",
|
||||
|
@ -315,13 +315,12 @@ pp(GET(f"/formsemestre/880/resultats", headers=HEADERS)[0])
|
||||
# jour = sem["date_fin"]
|
||||
# evaluation_id = POST(
|
||||
# s,
|
||||
# "/Notes/do_evaluation_create",
|
||||
# f"/moduleimpl/{mod['moduleimpl_id']}/evaluation/create",
|
||||
# data={
|
||||
# "moduleimpl_id": mod["moduleimpl_id"],
|
||||
# "coefficient": 1,
|
||||
# "jour": jour, # "5/9/2019",
|
||||
# "heure_debut": "9h00",
|
||||
# "heure_fin": "10h00",
|
||||
# "jour": jour, # "2023-08-23",
|
||||
# "heure_debut": "9:00",
|
||||
# "heure_fin": "10:00",
|
||||
# "note_max": 20, # notes sur 20
|
||||
# "description": "essai",
|
||||
# },
|
||||
|
@ -165,37 +165,3 @@ assert isinstance(json.loads(r.text)[0]["billet_id"], int)
|
||||
# print(f"{len(inscrits)} inscrits dans ce module")
|
||||
# # prend le premier inscrit, au hasard:
|
||||
# etudid = inscrits[0]["etudid"]
|
||||
|
||||
# # ---- Création d'une evaluation le dernier jour du semestre
|
||||
# jour = sem["date_fin"]
|
||||
# evaluation_id = POST(
|
||||
# "/Notes/do_evaluation_create",
|
||||
# data={
|
||||
# "moduleimpl_id": mod["moduleimpl_id"],
|
||||
# "coefficient": 1,
|
||||
# "jour": jour, # "5/9/2019",
|
||||
# "heure_debut": "9h00",
|
||||
# "heure_fin": "10h00",
|
||||
# "note_max": 20, # notes sur 20
|
||||
# "description": "essai",
|
||||
# },
|
||||
# errmsg="échec création évaluation",
|
||||
# )
|
||||
|
||||
# print(
|
||||
# f"Evaluation créée dans le module {mod['moduleimpl_id']}, evaluation_id={evaluation_id}"
|
||||
# )
|
||||
# print(
|
||||
# f"Pour vérifier, aller sur: {DEPT_URL}/Notes/moduleimpl_status?moduleimpl_id={mod['moduleimpl_id']}",
|
||||
# )
|
||||
|
||||
# # ---- Saisie d'une note
|
||||
# junk = POST(
|
||||
# "/Notes/save_note",
|
||||
# data={
|
||||
# "etudid": etudid,
|
||||
# "evaluation_id": evaluation_id,
|
||||
# "value": 16.66, # la note !
|
||||
# "comment": "test API",
|
||||
# },
|
||||
# )
|
||||
|
@ -5,40 +5,51 @@
|
||||
"""Construction des fichiers exemples pour la documentation.
|
||||
|
||||
Usage:
|
||||
cd /opt/scodoc/tests/api
|
||||
python make_samples.py [entry_names]
|
||||
python make_samples.py -i <filepath> [entrynames]
|
||||
python tests/api/make_samples.py [entry_names]
|
||||
python tests/api/make_samples.py -i <filepath> [entrynames]
|
||||
|
||||
si entry_names est spécifié, la génération est restreints aux exemples cités. expl: `python make_samples departements departement-formsemestres`
|
||||
doit être exécutée immédiatement apres une initialisation de la base pour test API! (car dépendant des identifiants générés lors de la création des objets)
|
||||
cd /opt/scodoc/tests/api
|
||||
tools/create_database.sh --drop SCODOC_TEST_API && flask db upgrade &&flask sco-db-init --erase && flask init-test-database
|
||||
Si entry_names est spécifié, la génération est restreinte aux exemples cités.
|
||||
Exemple:
|
||||
python make_samples departements departement-formsemestres
|
||||
|
||||
Créer éventuellement un fichier `.env` dans /opt/scodoc/tests/api
|
||||
avec la config du client API:
|
||||
```
|
||||
SCODOC_URL = "http://localhost:5000/"
|
||||
Doit être exécutée immédiatement apres une initialisation de la base pour test API!
|
||||
(car dépendant des identifiants générés lors de la création des objets)
|
||||
|
||||
Modifer le /opt/scodoc/.env pour pointer sur la base test
|
||||
SCODOC_DATABASE_URI="postgresql:///SCODOC_TEST_API"
|
||||
|
||||
puis re-créer cette base
|
||||
tools/create_database.sh --drop SCODOC_TEST_API
|
||||
flask db upgrade
|
||||
flask sco-db-init --erase
|
||||
flask init-test-database
|
||||
|
||||
et lancer le serveur test:
|
||||
flask run --debug
|
||||
```
|
||||
|
||||
Cet utilitaire prend en donnée le fichier de nom `samples.csv` contenant la description des exemples (séparés par une tabulation (\t), une ligne par exemple)
|
||||
* Le nom de l'exemple donne le nom du fichier généré (nom_exemple => nom_exemple.json.md). plusieurs lignes peuvent partager le même nom. dans ce cas le fichier contiendra chacun des exemples
|
||||
Cet utilitaire prend en argument le fichier de nom `samples.csv` contenant la description
|
||||
des exemples (séparés par une tabulation (\t), une ligne par exemple)
|
||||
* Le nom de l'exemple donne le nom du fichier généré (nom_exemple => nom_exemple.json.md).
|
||||
Plusieurs lignes peuvent partager le même nom. dans ce cas le fichier contiendra
|
||||
chacun des exemples
|
||||
* l'url utilisée
|
||||
* la permission nécessaire (par défaut ScoView)
|
||||
* la méthode GET,POST à utiliser (si commence par #, la ligne est ignorée)
|
||||
* les arguments éventuel (en cas de POST): une chaîne de caractère selon json
|
||||
|
||||
Implémentation:
|
||||
Le code complète une structure de données (Samples) qui est un dictionnaire de set (indicé par le nom des exemple.
|
||||
Le code complète une structure de données (Samples) qui est un dictionnaire de set
|
||||
(indicé par le nom des exemples).
|
||||
Chacun des éléments du set est un exemple (Sample)
|
||||
Quand la structure est complète, on génére tous les fichiers textes
|
||||
- nom de l exemple
|
||||
- un ou plusieurs exemples avec pour chaucn
|
||||
- l url utilisée
|
||||
- nom de l'exemple
|
||||
- un ou plusieurs exemples avec pour chacun
|
||||
- l'url utilisée
|
||||
- les arguments éventuels
|
||||
- le résultat
|
||||
Le tout mis en forme au format markdown et rangé dans le répertoire DATA_DIR (/tmp/samples) qui est créé ou écrasé si déjà existant
|
||||
|
||||
|
||||
Le tout mis en forme au format markdown et rangé dans le répertoire DATA_DIR (/tmp/samples)
|
||||
qui est créé ou écrasé si déjà existant.
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
@ -50,7 +61,7 @@ from pprint import pprint as pp
|
||||
import urllib3
|
||||
import json
|
||||
|
||||
from pandas import read_csv
|
||||
import pandas as pd
|
||||
|
||||
from setup_test_api import (
|
||||
API_PASSWORD,
|
||||
@ -68,6 +79,10 @@ DATA_DIR = "/tmp/samples/"
|
||||
SAMPLES_FILENAME = "tests/ressources/samples/samples.csv"
|
||||
|
||||
|
||||
class SampleException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Sample:
|
||||
def __init__(self, url, method="GET", permission="ScoView", content=None):
|
||||
self.content = content
|
||||
@ -83,7 +98,7 @@ class Sample:
|
||||
elif permission == "ScoUsersAdmin":
|
||||
HEADERS = get_auth_headers("admin_api", "admin_api")
|
||||
else:
|
||||
raise Exception(f"Bad permission : {permission}")
|
||||
raise SampleException(f"Bad permission : {permission}")
|
||||
if self.method == "GET":
|
||||
self.result = GET(self.url, HEADERS)
|
||||
elif self.method == "POST":
|
||||
@ -94,20 +109,19 @@ class Sample:
|
||||
self.result = POST_JSON(self.url, json.loads(self.content), HEADERS)
|
||||
elif self.method[0] != "#":
|
||||
error = f'Bad method : "{self.method}"'
|
||||
raise Exception(error)
|
||||
raise SampleException(error)
|
||||
self.shorten()
|
||||
file = open(f"sample_TEST.json.md", "tw")
|
||||
self.dump(file)
|
||||
file.close()
|
||||
with open("sample_TEST.json.md", "tw", encoding="utf-8") as f:
|
||||
self.dump(f)
|
||||
|
||||
def _shorten(
|
||||
self, item
|
||||
): # abrege les longues listes (limite à 2 éléments et affiche "... etc. à la place"
|
||||
def _shorten(self, item):
|
||||
"Abrège les longues listes: limite à 2 éléments et affiche '...' etc. à la place"
|
||||
if isinstance(item, list):
|
||||
return [self._shorten(child) for child in item[:2]] + ["... etc."]
|
||||
return [self._shorten(child) for child in item[:2] + ["..."]]
|
||||
return item
|
||||
|
||||
def shorten(self):
|
||||
"Abrège le résultat"
|
||||
self.result = self._shorten(self.result)
|
||||
|
||||
def pp(self):
|
||||
@ -122,8 +136,8 @@ class Sample:
|
||||
|
||||
file.write(f"#### {self.method} {self.url}\n")
|
||||
if len(self.content) > 0:
|
||||
file.write(f"> `Content-Type: application/json`\n")
|
||||
file.write(f"> \n")
|
||||
file.write("> `Content-Type: application/json`\n")
|
||||
file.write("> \n")
|
||||
file.write(f"> `{self.content}`\n\n")
|
||||
|
||||
file.write("```json\n")
|
||||
@ -143,7 +157,7 @@ class Samples:
|
||||
"""Entry_names: la liste des entrées à reconstruire.
|
||||
si None, la totalité des lignes de samples.csv est prise en compte
|
||||
"""
|
||||
self.entries = defaultdict(lambda: set())
|
||||
self.entries = defaultdict(set)
|
||||
self.entry_names = entry_names
|
||||
|
||||
def add_sample(self, line):
|
||||
@ -171,35 +185,35 @@ class Samples:
|
||||
|
||||
def dump(self):
|
||||
for entry, samples in self.entries.items():
|
||||
file = open(f"{DATA_DIR}sample_{entry}.json.md", "tw")
|
||||
file.write(f"### {entry}\n\n")
|
||||
for sample in sorted(
|
||||
samples, key=lambda s: s.url
|
||||
): # sorted de façon à rendre le fichier résultat déterministe (i.e. indépendant de l ordre d arrivée des résultats)
|
||||
sample.dump(file)
|
||||
file.close()
|
||||
with open(f"{DATA_DIR}sample_{entry}.json.md", "tw", encoding="utf-8") as f:
|
||||
f.write(f"### {entry}\n\n")
|
||||
# Trié de façon à rendre le fichier indépendant de l'ordre des résultats
|
||||
for sample in sorted(samples, key=lambda s: s.url):
|
||||
sample.dump(f)
|
||||
|
||||
|
||||
def make_samples(samples_filename):
|
||||
if len(sys.argv) == 1:
|
||||
entry_names = None
|
||||
elif len(sys.argv) >= 3 and sys.argv[1] == "-i":
|
||||
"Génère les samples"
|
||||
entry_names = None
|
||||
if len(sys.argv) >= 3 and sys.argv[1] == "-i":
|
||||
samples_filename = sys.argv[2]
|
||||
entry_names = sys.argv[3:] if len(sys.argv) > 3 else None
|
||||
else:
|
||||
entry_names = sys.argv[1:]
|
||||
|
||||
if os.path.exists(DATA_DIR):
|
||||
if not os.path.isdir(DATA_DIR):
|
||||
raise f"{DATA_DIR} existe déjà et n'est pas un répertoire"
|
||||
else:
|
||||
# DATA_DIR existe déjà - effacer et recréer
|
||||
shutil.rmtree(DATA_DIR)
|
||||
os.mkdir(DATA_DIR)
|
||||
raise SampleException(f"{DATA_DIR} existe déjà et n'est pas un répertoire")
|
||||
# DATA_DIR existe déjà - effacer et recréer
|
||||
shutil.rmtree(DATA_DIR)
|
||||
os.mkdir(DATA_DIR)
|
||||
else:
|
||||
os.mkdir(DATA_DIR)
|
||||
|
||||
samples = Samples(entry_names)
|
||||
df = read_csv(
|
||||
df = pd.read_csv(
|
||||
samples_filename,
|
||||
comment="#",
|
||||
sep=";",
|
||||
quotechar='"',
|
||||
dtype={
|
||||
@ -212,11 +226,12 @@ def make_samples(samples_filename):
|
||||
keep_default_na=False,
|
||||
)
|
||||
df = df.reset_index()
|
||||
df.apply(lambda line: samples.add_sample(line), axis=1)
|
||||
df.apply(samples.add_sample, axis=1)
|
||||
samples.dump()
|
||||
return samples
|
||||
|
||||
|
||||
if not CHECK_CERTIFICATE:
|
||||
urllib3.disable_warnings()
|
||||
|
||||
make_samples(SAMPLES_FILENAME)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user