Compare commits

...

32 Commits

Author SHA1 Message Date
9511e3c2c3 Fix: tests unitaires test_but_jury.py. + check in package building script. 2023-09-01 14:02:24 +02:00
iziram
1cfcb831ca Assiduites : fix Saisie Absences hebdo (moduleimpl) 2023-08-31 17:37:09 +02:00
iziram
738f795f21 Assiduites : fix #688 2023-08-31 17:12:53 +02:00
iziram
48bca602cc Assiduites : fix #694 2023-08-31 16:40:51 +02:00
iziram
a7374d7428 Assiduites : fix #695 2023-08-31 15:32:08 +02:00
60f9c0ff90 Passage étudiants depuis autres semestres: ajout option pour conserver juste le groupe de parcours + Fix désinscription 2023-08-31 15:14:02 +02:00
f5220025af Correction groupes vers non affectés 2023-08-31 15:14:02 +02:00
1aa9003ea2 Refactor evaluation_delete 2023-08-31 15:14:02 +02:00
c07a8a5e64 Liens pages évaluation 2023-08-31 15:14:02 +02:00
2124566487 Modernise code en-tete evaluation et 'absences ce jour' 2023-08-31 15:14:02 +02:00
d1f90a29e9 API doc: ameliore script sample + sample evaluation 2023-08-31 15:14:02 +02:00
8d3deed6ac Fix: création évaluation sans date via ancien formulaire 2023-08-31 15:14:02 +02:00
8dd39c46e7 9.6.14 2023-08-31 15:14:02 +02:00
4a86fa9c57 Fichier oublié => 9.6.13 2023-08-31 15:14:02 +02:00
4db455d1ed Fichier oublié => 9.6.12 2023-08-31 15:14:02 +02:00
1439c0ee75 Fix tests unitaires API (ok) 2023-08-31 15:14:02 +02:00
2bc70503ec Enlève l'ancien module de gestion des absences 2023-08-31 15:14:02 +02:00
0bb6fba46a Fix: calcul moyenne générale classique avec coef. UEs non renseignés 2023-08-31 15:14:02 +02:00
74e37f31b3 Fix: aliased join / SQLAlchemy 2 2023-08-31 15:14:02 +02:00
f9da8c1d8d API evaluation: create avec poids, /delete + tests unitaires + corrections 2023-08-31 15:14:02 +02:00
331d42f5ee Fix affichage validation RCUE (merci @SebL). Corrige certains liens externes. Ajout lien vers visu cursus. 2023-08-31 15:14:02 +02:00
f98a10ea25 API evaluation: ajout /create (manque poids APC) 2023-08-31 15:14:02 +02:00
dbf765328a Adaptation assiduités pour nouveau codage dates évaluations (à compléter) 2023-08-31 15:14:02 +02:00
4de74b160e Modification codage dates évaluations 2023-08-31 15:14:02 +02:00
7648f98848 removed buggy pylint plugin fro SQLAlchemy 2023-08-31 15:14:02 +02:00
b45d7cd4f5 Fix small bug 2023-08-31 15:14:02 +02:00
7292c76cc2 flake8 config. Code cosmetic. 2023-08-31 15:14:02 +02:00
c8d77023a8 build_release: option to skip tests 2023-08-31 15:14:02 +02:00
37539f4bac Version bump 2023-08-31 15:14:02 +02:00
91ec694e6d Fix: calcul moyenne générale BUT si aucune UE 2023-08-31 15:13:52 +02:00
4e9ac3d3e2 WIP: modernisation evaluations 2023-08-31 15:13:52 +02:00
0963462e31 9.5.8 avec qq fix backportés 2023-08-31 15:13:52 +02:00
106 changed files with 3000 additions and 4974 deletions

3
.flake8 Normal file
View File

@ -0,0 +1,3 @@
[flake8]
max-line-length = 88
ignore = E203,W503

View File

@ -1,10 +1,24 @@
[MASTER] [MASTER]
load-plugins=pylint_flask_sqlalchemy,pylint_flask
[MESSAGES CONTROL] # List of plugins (as comma separated values of python module names) to load,
# pylint and black disagree... # usually to register additional checkers.
disable=bad-continuation load-plugins=pylint_flask
[TYPECHECK] [TYPECHECK]
ignored-classes=Permission,SQLObject,Registrant,scoped_session # List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=Permission,
SQLObject,
Registrant,
scoped_session,
func
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis). It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=entreprises
good-names=d,df,e,f,i,j,k,n,nt,t,u,ue,v,x,y,z,H,F

View File

@ -5,7 +5,7 @@ from flask import Blueprint
from flask import request, g from flask import request, g
from app import db from app import db
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import ScoException from app.scodoc.sco_exceptions import AccessDenied, ScoException
api_bp = Blueprint("api", __name__) api_bp = Blueprint("api", __name__)
api_web_bp = Blueprint("apiweb", __name__) api_web_bp = Blueprint("apiweb", __name__)
@ -15,12 +15,24 @@ API_CLIENT_ERROR = 400 # erreur dans les paramètres fournis par le client
@api_bp.errorhandler(ScoException) @api_bp.errorhandler(ScoException)
@api_web_bp.errorhandler(ScoException)
@api_bp.errorhandler(404) @api_bp.errorhandler(404)
def api_error_handler(e): def api_error_handler(e):
"erreurs API => json" "erreurs API => json"
return scu.json_error(404, message=str(e)) return scu.json_error(404, message=str(e))
@api_bp.errorhandler(AccessDenied)
@api_web_bp.errorhandler(AccessDenied)
def permission_denied_error_handler(exc):
"""
Renvoie message d'erreur pour l'erreur 403
"""
return scu.json_error(
403, f"operation non autorisee ({exc.args[0] if exc.args else ''})"
)
def requested_format(default_format="json", allowed_formats=None): def requested_format(default_format="json", allowed_formats=None):
"""Extract required format from query string. """Extract required format from query string.
* default value is json. A list of allowed formats may be provided * default value is json. A list of allowed formats may be provided
@ -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 tokens
from app.api import ( from app.api import (
absences,
assiduites, assiduites,
billets_absences, billets_absences,
departements, departements,
@ -65,6 +76,7 @@ from app.api import (
jury, jury,
justificatifs, justificatifs,
logos, logos,
moduleimpl,
partitions, partitions,
semset, semset,
users, users,

View File

@ -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)

View File

@ -7,17 +7,17 @@
""" """
ScoDoc 9 API : accès aux évaluations ScoDoc 9 API : accès aux évaluations
""" """
from flask import g, request from flask import g, request
from flask_json import as_json from flask_json import as_json
from flask_login import login_required from flask_login import current_user, login_required
import app import app
from app import log, db
from app.api import api_bp as bp, api_web_bp from app.api import api_bp as bp, api_web_bp
from app.decorators import scodoc, permission_required from app.decorators import scodoc, permission_required
from app.models import Evaluation, ModuleImpl, FormSemestre from app.models import Evaluation, ModuleImpl, FormSemestre
from app.scodoc import sco_evaluation_db, sco_saisie_notes from app.scodoc import sco_evaluation_db, sco_saisie_notes
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -28,7 +28,7 @@ import app.scodoc.sco_utils as scu
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def evaluation(evaluation_id: int): def get_evaluation(evaluation_id: int):
"""Description d'une évaluation. """Description d'une évaluation.
{ {
@ -47,7 +47,7 @@ def evaluation(evaluation_id: int):
'UE1.3': 1.0 'UE1.3': 1.0
}, },
'publish_incomplete': False, 'publish_incomplete': False,
'visi_bulletin': True 'visibulletin': True
} }
""" """
query = Evaluation.query.filter_by(id=evaluation_id) query = Evaluation.query.filter_by(id=evaluation_id)
@ -181,3 +181,97 @@ def evaluation_set_notes(evaluation_id: int):
return sco_saisie_notes.save_notes( return sco_saisie_notes.save_notes(
evaluation, notes, comment=data.get("comment", "") evaluation, notes, comment=data.get("comment", "")
) )
@bp.route("/moduleimpl/<int:moduleimpl_id>/evaluation/create", methods=["POST"])
@api_web_bp.route("/moduleimpl/<int:moduleimpl_id>/evaluation/create", methods=["POST"])
@login_required
@scodoc
@permission_required(Permission.ScoEnsView) # permission gérée dans la fonction
@as_json
def evaluation_create(moduleimpl_id: int):
"""Création d'une évaluation.
The request content type should be "application/json",
and contains:
{
"description" : str,
"evaluation_type" : int, // {0,1,2} default 0 (normale)
"date_debut" : date_iso, // optionnel
"date_fin" : date_iso, // optionnel
"note_max" : float, // si non spécifié, 20.0
"numero" : int, // ordre de présentation, default tri sur date
"visibulletin" : boolean , //default true
"publish_incomplete" : boolean , //default false
"coefficient" : float, // si non spécifié, 1.0
"poids" : { ue_id : poids } // optionnel
}
Result: l'évaluation créée.
"""
moduleimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id)
if not moduleimpl.can_edit_evaluation(current_user):
return scu.json_error(403, "opération non autorisée")
data = request.get_json(force=True) # may raise 400 Bad Request
try:
evaluation = Evaluation.create(moduleimpl=moduleimpl, **data)
except ValueError:
return scu.json_error(400, "paramètre incorrect")
except ScoValueError as exc:
return scu.json_error(
400, f"paramètre de type incorrect ({exc.args[0] if exc.args else ''})"
)
db.session.add(evaluation)
db.session.commit()
# Les poids vers les UEs:
poids = data.get("poids")
if poids is not None:
if not isinstance(poids, dict):
log("API error: canceling evaluation creation")
db.session.delete(evaluation)
db.session.commit()
return scu.json_error(
400, "paramètre de type incorrect (poids must be a dict)"
)
try:
evaluation.set_ue_poids_dict(data["poids"])
except ScoValueError as exc:
log("API error: canceling evaluation creation")
db.session.delete(evaluation)
db.session.commit()
return scu.json_error(
400,
f"erreur enregistrement des poids ({exc.args[0] if exc.args else ''})",
)
db.session.commit()
return evaluation.to_dict_api()
@bp.route("/evaluation/<int:evaluation_id>/delete", methods=["POST"])
@api_web_bp.route("/evaluation/<int:evaluation_id>/delete", methods=["POST"])
@login_required
@scodoc
@permission_required(Permission.ScoEnsView) # permission gérée dans la fonction
@as_json
def evaluation_delete(evaluation_id: int):
"""Suppression d'une évaluation.
Efface aussi toutes ses notes
"""
query = Evaluation.query.filter_by(id=evaluation_id)
if g.scodoc_dept:
query = (
query.join(ModuleImpl)
.join(FormSemestre)
.filter_by(dept_id=g.scodoc_dept_id)
)
evaluation = query.first_or_404()
dept = evaluation.moduleimpl.formsemestre.departement
app.set_sco_dept(dept.acronym)
if not evaluation.moduleimpl.can_edit_evaluation(current_user):
raise AccessDenied("evaluation_delete")
sco_saisie_notes.evaluation_suppress_alln(
evaluation_id=evaluation_id, dialog_confirmed=True
)
evaluation.delete()
return "ok"

View File

@ -21,8 +21,6 @@ from app.models import (
ApcNiveau, ApcNiveau,
ApcParcours, ApcParcours,
Formation, Formation,
FormSemestre,
ModuleImpl,
UniteEns, UniteEns,
) )
from app.scodoc import sco_formations from app.scodoc import sco_formations
@ -249,54 +247,6 @@ def referentiel_competences(formation_id: int):
return formation.referentiel_competence.to_dict() return formation.referentiel_competence.to_dict()
@bp.route("/moduleimpl/<int:moduleimpl_id>")
@api_web_bp.route("/moduleimpl/<int:moduleimpl_id>")
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def moduleimpl(moduleimpl_id: int):
"""
Retourne un moduleimpl en fonction de son id
moduleimpl_id : l'id d'un moduleimpl
Exemple de résultat :
{
"id": 1,
"formsemestre_id": 1,
"module_id": 1,
"responsable_id": 2,
"moduleimpl_id": 1,
"ens": [],
"module": {
"heures_tp": 0,
"code_apogee": "",
"titre": "Initiation aux réseaux informatiques",
"coefficient": 1,
"module_type": 2,
"id": 1,
"ects": null,
"abbrev": "Init aux réseaux informatiques",
"ue_id": 1,
"code": "R101",
"formation_id": 1,
"heures_cours": 0,
"matiere_id": 1,
"heures_td": 0,
"semestre_id": 1,
"numero": 10,
"module_id": 1
}
}
"""
query = ModuleImpl.query.filter_by(id=moduleimpl_id)
if g.scodoc_dept:
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
modimpl: ModuleImpl = query.first_or_404()
return modimpl.to_dict(convert_objects=True)
@bp.route("/set_ue_parcours/<int:ue_id>", methods=["POST"]) @bp.route("/set_ue_parcours/<int:ue_id>", methods=["POST"])
@api_web_bp.route("/set_ue_parcours/<int:ue_id>", methods=["POST"]) @api_web_bp.route("/set_ue_parcours/<int:ue_id>", methods=["POST"])
@login_required @login_required

View File

@ -212,7 +212,7 @@ def bulletins(formsemestre_id: int, version: str = "long"):
@as_json @as_json
def formsemestre_programme(formsemestre_id: int): def formsemestre_programme(formsemestre_id: int):
""" """
Retourne la liste des Ues, ressources et SAE d'un semestre Retourne la liste des UEs, ressources et SAEs d'un semestre
formsemestre_id : l'id d'un formsemestre formsemestre_id : l'id d'un formsemestre

View File

@ -10,7 +10,7 @@
import datetime import datetime
from flask import flash, g, request, url_for from flask import g, request, url_for
from flask_json import as_json from flask_json import as_json
from flask_login import current_user, login_required from flask_login import current_user, login_required

69
app/api/moduleimpl.py Normal file
View 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)

View File

@ -220,20 +220,9 @@ def group_remove_etud(group_id: int, etudid: int):
group = query.first_or_404() group = query.first_or_404()
if not group.partition.formsemestre.etat: if not group.partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé") return json_error(403, "formsemestre verrouillé")
if etud in group.etuds:
group.etuds.remove(etud) group.remove_etud(etud)
db.session.commit()
Scolog.logdb(
method="group_remove_etud",
etudid=etud.id,
msg=f"Retrait du groupe {group.group_name} de {group.partition.partition_name}",
commit=True,
)
# Update parcours
group.partition.formsemestre.update_inscriptions_parcours_from_groups(
etudid=etudid
)
sco_cache.invalidate_formsemestre(group.partition.formsemestre_id)
return {"group_id": group_id, "etudid": etudid} return {"group_id": group_id, "etudid": etudid}

View File

@ -276,11 +276,10 @@ class BulletinBUT:
"coef": fmt_note(e.coefficient) "coef": fmt_note(e.coefficient)
if e.evaluation_type == scu.EVALUATION_NORMALE if e.evaluation_type == scu.EVALUATION_NORMALE
else None, else None,
"date": e.jour.isoformat() if e.jour else None, "date_debut": e.date_debut.isoformat() if e.date_debut else None,
"date_fin": e.date_fin.isoformat() if e.date_fin else None,
"description": e.description, "description": e.description,
"evaluation_type": e.evaluation_type, "evaluation_type": e.evaluation_type,
"heure_debut": e.heure_debut.strftime("%H:%M") if e.heure_debut else None,
"heure_fin": e.heure_fin.strftime("%H:%M") if e.heure_debut else None,
"note": { "note": {
"value": fmt_note( "value": fmt_note(
eval_notes[etud.id], eval_notes[etud.id],
@ -298,6 +297,12 @@ class BulletinBUT:
) )
if has_request_context() if has_request_context()
else "na", else "na",
# deprecated (supprimer avant #sco9.7)
"date": e.date_debut.isoformat() if e.date_debut else None,
"heure_debut": e.date_debut.time().isoformat("minutes")
if e.date_debut
else None,
"heure_fin": e.date_fin.time().isoformat("minutes") if e.date_fin else None,
} }
return d return d

View File

@ -202,12 +202,11 @@ def bulletin_but_xml_compat(
if e.visibulletin or version == "long": if e.visibulletin or version == "long":
x_eval = Element( x_eval = Element(
"evaluation", "evaluation",
jour=e.jour.isoformat() if e.jour else "", date_debut=e.date_debut.isoformat()
heure_debut=e.heure_debut.isoformat() if e.date_debut
if e.heure_debut
else "", else "",
heure_fin=e.heure_fin.isoformat() date_fin=e.date_fin.isoformat()
if e.heure_debut if e.date_debut
else "", else "",
coefficient=str(e.coefficient), coefficient=str(e.coefficient),
# pas les poids en XML compat # pas les poids en XML compat
@ -215,6 +214,12 @@ def bulletin_but_xml_compat(
description=quote_xml_attr(e.description), description=quote_xml_attr(e.description),
# notes envoyées sur 20, ceci juste pour garder trace: # notes envoyées sur 20, ceci juste pour garder trace:
note_max_origin=str(e.note_max), note_max_origin=str(e.note_max),
# --- deprecated
jour=e.date_debut.isoformat()
if e.date_debut
else "",
heure_debut=e.heure_debut(),
heure_fin=e.heure_fin(),
) )
x_mod.append(x_eval) x_mod.append(x_eval)
try: try:

View File

@ -76,6 +76,13 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
f""" f"""
<div class="titre_niveaux"> <div class="titre_niveaux">
<b>Niveaux de compétences et unités d'enseignement du BUT{deca.annee_but}</b> <b>Niveaux de compétences et unités d'enseignement du BUT{deca.annee_but}</b>
<a style="margin-left: 32px;" class="stdlink" target="_blank" rel="noopener noreferrer"
href={
url_for("notes.validation_rcues", scodoc_dept=g.scodoc_dept,
etudid=deca.etud.id,
formsemestre_id=formsemestre_2.id if formsemestre_2 else formsemestre_1.id
)
}>visualiser son cursus</a>
</div> </div>
<div class="but_explanation">{deca.explanation}</div> <div class="but_explanation">{deca.explanation}</div>
<div class="but_annee"> <div class="but_annee">

View File

@ -250,7 +250,7 @@ class ModuleImplResults:
).reshape(-1, 1) ).reshape(-1, 1)
# was _list_notes_evals_titles # was _list_notes_evals_titles
def get_evaluations_completes(self, moduleimpl: ModuleImpl) -> list: def get_evaluations_completes(self, moduleimpl: ModuleImpl) -> list[Evaluation]:
"Liste des évaluations complètes" "Liste des évaluations complètes"
return [ return [
e for e in moduleimpl.evaluations if self.evaluations_completes_dict[e.id] e for e in moduleimpl.evaluations if self.evaluations_completes_dict[e.id]

View File

@ -78,7 +78,11 @@ def compute_sem_moys_apc_using_ects(
else: else:
ects = ects_df.to_numpy() ects = ects_df.to_numpy()
# ects est maintenant un array nb_etuds x nb_ues # ects est maintenant un array nb_etuds x nb_ues
moy_gen = (etud_moy_ue_df * ects).sum(axis=1) / ects.sum(axis=1) moy_gen = (etud_moy_ue_df * ects).sum(axis=1) / ects.sum(axis=1)
except ZeroDivisionError:
# peut arriver si aucun module... on ignore
moy_gen = pd.Series(np.NaN, index=etud_moy_ue_df.index)
except TypeError: except TypeError:
if None in ects: if None in ects:
formation = db.session.get(Formation, formation_id) formation = db.session.get(Formation, formation_id)

View File

@ -394,7 +394,10 @@ def compute_ue_moys_classic(
if sco_preferences.get_preference("use_ue_coefs", formsemestre.id): if sco_preferences.get_preference("use_ue_coefs", formsemestre.id):
# Cas avec coefficients d'UE forcés: (on met à zéro l'UE bonus) # Cas avec coefficients d'UE forcés: (on met à zéro l'UE bonus)
etud_coef_ue_df = pd.DataFrame( etud_coef_ue_df = pd.DataFrame(
{ue.id: ue.coefficient if ue.type != UE_SPORT else 0.0 for ue in ues}, {
ue.id: (ue.coefficient or 0.0) if ue.type != UE_SPORT else 0.0
for ue in ues
},
index=modimpl_inscr_df.index, index=modimpl_inscr_df.index,
columns=[ue.id for ue in ues], columns=[ue.id for ue in ues],
) )

View File

@ -53,8 +53,8 @@ class ResultatsSemestreBUT(NotesTableCompat):
self.store() self.store()
t2 = time.time() t2 = time.time()
log( log(
f"""ResultatsSemestreBUT: cached formsemestre_id={formsemestre.id f"""+++ ResultatsSemestreBUT: cached [{formsemestre.id
} ({(t1-t0):g}s +{(t2-t1):g}s)""" }] ({(t1-t0):g}s +{(t2-t1):g}s) +++"""
) )
def compute(self): def compute(self):

View File

@ -50,8 +50,8 @@ class ResultatsSemestreClassic(NotesTableCompat):
self.store() self.store()
t2 = time.time() t2 = time.time()
log( log(
f"""ResultatsSemestreClassic: cached formsemestre_id={ f"""+++ ResultatsSemestreClassic: cached formsemestre_id={
formsemestre.id} ({(t1-t0):g}s +{(t2-t1):g}s)""" formsemestre.id} ({(t1-t0):g}s +{(t2-t1):g}s) +++"""
) )
# recalculé (aussi rapide que de les cacher) # recalculé (aussi rapide que de les cacher)
self.moy_min = self.etud_moy_gen.min() self.moy_min = self.etud_moy_gen.min()

View File

@ -80,8 +80,8 @@ class ResultatsSemestre(ResultatsCache):
self.moy_gen_rangs_by_group = None # virtual self.moy_gen_rangs_by_group = None # virtual
self.modimpl_inscr_df: pd.DataFrame = None self.modimpl_inscr_df: pd.DataFrame = None
"Inscriptions: row etudid, col modimlpl_id" "Inscriptions: row etudid, col modimlpl_id"
self.modimpls_results: ModuleImplResults = None self.modimpls_results: dict[int, ModuleImplResults] = None
"Résultats de chaque modimpl: dict { modimpl.id : ModuleImplResults(Classique ou BUT) }" "Résultats de chaque modimpl (classique ou BUT)"
self.etud_coef_ue_df = None self.etud_coef_ue_df = None
"""coefs d'UE effectifs pour chaque étudiant (pour form. classiques)""" """coefs d'UE effectifs pour chaque étudiant (pour form. classiques)"""
self.modimpl_coefs_df: pd.DataFrame = None self.modimpl_coefs_df: pd.DataFrame = None
@ -192,6 +192,17 @@ class ResultatsSemestre(ResultatsCache):
*[mr.etudids_attente for mr in self.modimpls_results.values()] *[mr.etudids_attente for mr in self.modimpls_results.values()]
) )
# # Etat des évaluations
# # (se substitue à do_evaluation_etat, sans les moyennes par groupes)
# def get_evaluations_etats(evaluation_id: int) -> dict:
# """Renvoie dict avec les clés:
# last_modif
# nb_evals_completes
# nb_evals_en_cours
# nb_evals_vides
# attente
# """
# --- JURY... # --- JURY...
def get_formsemestre_validations(self) -> ValidationsSemestre: def get_formsemestre_validations(self) -> ValidationsSemestre:
"""Load validations if not already stored, set attribute and return value""" """Load validations if not already stored, set attribute and return value"""

View File

@ -16,7 +16,13 @@ from app import db, log
from app.comp import moy_sem from app.comp import moy_sem
from app.comp.aux_stats import StatsMoyenne from app.comp.aux_stats import StatsMoyenne
from app.comp.res_common import ResultatsSemestre from app.comp.res_common import ResultatsSemestre
from app.models import Identite, FormSemestre, ModuleImpl, ScolarAutorisationInscription from app.models import (
Evaluation,
Identite,
FormSemestre,
ModuleImpl,
ScolarAutorisationInscription,
)
from app.scodoc.codes_cursus import UE_SPORT, DEF from app.scodoc.codes_cursus import UE_SPORT, DEF
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
@ -389,7 +395,7 @@ class NotesTableCompat(ResultatsSemestre):
"ects_total": ects_total, "ects_total": ects_total,
} }
def get_evals_in_mod(self, moduleimpl_id: int) -> list[dict]: def get_modimpl_evaluations_completes(self, moduleimpl_id: int) -> list[Evaluation]:
"""Liste d'informations (compat NotesTable) sur évaluations completes """Liste d'informations (compat NotesTable) sur évaluations completes
de ce module. de ce module.
Évaluation "complete" ssi toutes notes saisies ou en attente. Évaluation "complete" ssi toutes notes saisies ou en attente.
@ -398,34 +404,24 @@ class NotesTableCompat(ResultatsSemestre):
modimpl_results = self.modimpls_results.get(moduleimpl_id) modimpl_results = self.modimpls_results.get(moduleimpl_id)
if not modimpl_results: if not modimpl_results:
return [] # safeguard return [] # safeguard
evals_results = [] evaluations = []
for e in modimpl.evaluations: for e in modimpl.evaluations:
if modimpl_results.evaluations_completes_dict.get(e.id, False): if modimpl_results.evaluations_completes_dict.get(e.id, False):
d = e.to_dict() evaluations.append(e)
d["heure_debut"] = e.heure_debut # datetime.time
d["heure_fin"] = e.heure_fin
d["jour"] = e.jour # datetime
d["notes"] = {
etud.id: {
"etudid": etud.id,
"value": modimpl_results.evals_notes[e.id][etud.id],
}
for etud in self.etuds
}
d["etat"] = {
"evalattente": modimpl_results.evaluations_etat[e.id].nb_attente,
}
evals_results.append(d)
elif e.id not in modimpl_results.evaluations_completes_dict: elif e.id not in modimpl_results.evaluations_completes_dict:
# ne devrait pas arriver ? XXX # ne devrait pas arriver ? XXX
log( log(
f"Warning: 220213 get_evals_in_mod {e.id} not in mod {moduleimpl_id} ?" f"Warning: 220213 get_modimpl_evaluations_completes {e.id} not in mod {moduleimpl_id} ?"
) )
return evals_results return evaluations
def get_evaluations_etats(self) -> list[dict]:
"""Liste de toutes les évaluations du semestre
[ {...evaluation et son etat...} ]"""
# TODO: à moderniser (voir dans ResultatsSemestre)
# utilisé par
# do_evaluation_etat_in_sem
def get_evaluations_etats(self):
"""[ {...evaluation et son etat...} ]"""
# TODO: à moderniser
from app.scodoc import sco_evaluations from app.scodoc import sco_evaluations
if not hasattr(self, "_evaluations_etats"): if not hasattr(self, "_evaluations_etats"):

View File

@ -85,7 +85,9 @@ Adresses d'origine:
) )
current_app.logger.info( current_app.logger.info(
f"""email sent to{' (mode test)' if email_test_mode_address else ''}: {msg.recipients} f"""email sent to{
' (mode test)' if email_test_mode_address else ''
}: {msg.recipients}
from sender {msg.sender} from sender {msg.sender}
""" """
) )
@ -98,7 +100,8 @@ def get_from_addr(dept_acronym: str = None):
"""L'adresse "from" à utiliser pour envoyer un mail """L'adresse "from" à utiliser pour envoyer un mail
Si le departement est spécifié, ou si l'attribut `g.scodoc_dept`existe, Si le departement est spécifié, ou si l'attribut `g.scodoc_dept`existe,
prend le `email_from_addr` des préférences de ce département si ce champ est non vide. prend le `email_from_addr` des préférences de ce département si ce champ
est non vide.
Sinon, utilise le paramètre global `email_from_addr`. Sinon, utilise le paramètre global `email_from_addr`.
Sinon, la variable de config `SCODOC_MAIL_FROM`. Sinon, la variable de config `SCODOC_MAIL_FROM`.
""" """

View File

@ -5,17 +5,28 @@
import datetime import datetime
from operator import attrgetter from operator import attrgetter
from app import db from flask import g, url_for
from flask_login import current_user
import sqlalchemy as sa
from app import db, log
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.events import ScolarNews
from app.models.moduleimpls import ModuleImpl from app.models.moduleimpls import ModuleImpl
from app.models.notes import NotesNotes from app.models.notes import NotesNotes
from app.models.ues import UniteEns from app.models.ues import UniteEns
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc import sco_cache
import app.scodoc.notesdb as ndb from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
import app.scodoc.sco_utils as scu
from app.scodoc.sco_xml import quote_xml_attr
MAX_EVALUATION_DURATION = datetime.timedelta(days=365)
NOON = datetime.time(12, 00)
DEFAULT_EVALUATION_TIME = datetime.time(8, 0) DEFAULT_EVALUATION_TIME = datetime.time(8, 0)
VALID_EVALUATION_TYPES = {0, 1, 2}
class Evaluation(db.Model): class Evaluation(db.Model):
"""Evaluation (contrôle, examen, ...)""" """Evaluation (contrôle, examen, ...)"""
@ -27,15 +38,15 @@ class Evaluation(db.Model):
moduleimpl_id = db.Column( moduleimpl_id = db.Column(
db.Integer, db.ForeignKey("notes_moduleimpl.id"), index=True db.Integer, db.ForeignKey("notes_moduleimpl.id"), index=True
) )
jour = db.Column(db.Date) date_debut = db.Column(db.DateTime(timezone=True), nullable=True)
heure_debut = db.Column(db.Time) date_fin = db.Column(db.DateTime(timezone=True), nullable=True)
heure_fin = db.Column(db.Time)
description = db.Column(db.Text) description = db.Column(db.Text)
note_max = db.Column(db.Float) note_max = db.Column(db.Float)
coefficient = db.Column(db.Float) coefficient = db.Column(db.Float)
visibulletin = db.Column( visibulletin = db.Column(
db.Boolean, nullable=False, default=True, server_default="true" db.Boolean, nullable=False, default=True, server_default="true"
) )
"visible sur les bulletins version intermédiaire"
publish_incomplete = db.Column( publish_incomplete = db.Column(
db.Boolean, nullable=False, default=False, server_default="false" db.Boolean, nullable=False, default=False, server_default="false"
) )
@ -50,47 +61,146 @@ class Evaluation(db.Model):
def __repr__(self): def __repr__(self):
return f"""<Evaluation {self.id} { return f"""<Evaluation {self.id} {
self.jour.isoformat() if self.jour else ''} "{ self.date_debut.isoformat() if self.date_debut else ''} "{
self.description[:16] if self.description else ''}">""" self.description[:16] if self.description else ''}">"""
@classmethod
def create(
cls,
moduleimpl: ModuleImpl = None,
date_debut: datetime.datetime = None,
date_fin: datetime.datetime = None,
description=None,
note_max=None,
coefficient=None,
visibulletin=None,
publish_incomplete=None,
evaluation_type=None,
numero=None,
**kw, # ceci pour absorber les éventuel arguments excedentaires
):
"""Create an evaluation. Check permission and all arguments.
Ne crée pas les poids vers les UEs.
"""
if not moduleimpl.can_edit_evaluation(current_user):
raise AccessDenied(
f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
)
args = locals()
del args["cls"]
del args["kw"]
check_convert_evaluation_args(moduleimpl, args)
# Check numeros
Evaluation.moduleimpl_evaluation_renumber(moduleimpl, only_if_unumbered=True)
if not "numero" in args or args["numero"] is None:
args["numero"] = cls.get_new_numero(moduleimpl, args["date_debut"])
#
evaluation = Evaluation(**args)
sco_cache.invalidate_formsemestre(formsemestre_id=moduleimpl.formsemestre_id)
url = url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=moduleimpl.id,
)
log(f"created evaluation in {moduleimpl.module.titre_str()}")
ScolarNews.add(
typ=ScolarNews.NEWS_NOTE,
obj=moduleimpl.id,
text=f"""Création d'une évaluation dans <a href="{url}">{
moduleimpl.module.titre_str()}</a>""",
url=url,
)
return evaluation
@classmethod
def get_new_numero(
cls, moduleimpl: ModuleImpl, date_debut: datetime.datetime
) -> int:
"""Get a new numero for an evaluation in this moduleimpl
If necessary, renumber existing evals to make room for a new one.
"""
n = None
# Détermine le numero grâce à la date
# Liste des eval existantes triées par date, la plus ancienne en tete
evaluations = moduleimpl.evaluations.order_by(Evaluation.date_debut).all()
if date_debut is not None:
next_eval = None
t = date_debut
for e in evaluations:
if e.date_debut and e.date_debut > t:
next_eval = e
break
if next_eval:
n = _moduleimpl_evaluation_insert_before(evaluations, next_eval)
else:
n = None # à placer en fin
if n is None: # pas de date ou en fin:
if evaluations:
n = evaluations[-1].numero + 1
else:
n = 0 # the only one
return n
def delete(self):
"delete evaluation (commit) (check permission)"
from app.scodoc import sco_evaluation_db
modimpl: ModuleImpl = self.moduleimpl
if not modimpl.can_edit_evaluation(current_user):
raise AccessDenied(
f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
)
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(
self.id
) # { etudid : value }
notes = [x["value"] for x in notes_db.values()]
if notes:
raise ScoValueError(
"Impossible de supprimer cette évaluation: il reste des notes"
)
log(f"deleting evaluation {self}")
db.session.delete(self)
db.session.commit()
# inval cache pour ce semestre
sco_cache.invalidate_formsemestre(formsemestre_id=modimpl.formsemestre_id)
# news
url = url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=modimpl.id,
)
ScolarNews.add(
typ=ScolarNews.NEWS_NOTE,
obj=modimpl.id,
text=f"""Suppression d'une évaluation dans <a href="{
url
}">{modimpl.module.titre}</a>""",
url=url,
)
def to_dict(self) -> dict: def to_dict(self) -> dict:
"Représentation dict (riche, compat ScoDoc 7)" "Représentation dict (riche, compat ScoDoc 7)"
e = dict(self.__dict__) e_dict = dict(self.__dict__)
e.pop("_sa_instance_state", None) e_dict.pop("_sa_instance_state", None)
# ScoDoc7 output_formators # ScoDoc7 output_formators
e["evaluation_id"] = self.id e_dict["evaluation_id"] = self.id
e["jour"] = e["jour"].strftime("%d/%m/%Y") if e["jour"] else "" e_dict["date_debut"] = self.date_debut.isoformat() if self.date_debut else None
if self.jour is None: e_dict["date_fin"] = self.date_debut.isoformat() if self.date_fin else None
e["date_debut"] = None e_dict["numero"] = self.numero or 0
e["date_fin"] = None e_dict["poids"] = self.get_ue_poids_dict() # { ue_id : poids }
else:
e["date_debut"] = datetime.datetime.combine( # Deprecated
self.jour, self.heure_debut or datetime.time(0, 0) e_dict["jour"] = self.date_debut.strftime("%d/%m/%Y") if self.date_debut else ""
).isoformat()
e["date_fin"] = datetime.datetime.combine( return evaluation_enrich_dict(self, e_dict)
self.jour, self.heure_fin or datetime.time(0, 0)
).isoformat()
e["numero"] = ndb.int_null_is_zero(e["numero"])
e["poids"] = self.get_ue_poids_dict() # { ue_id : poids }
return evaluation_enrich_dict(e)
def to_dict_api(self) -> dict: def to_dict_api(self) -> dict:
"Représentation dict pour API JSON" "Représentation dict pour API JSON"
if self.jour is None:
date_debut = None
date_fin = None
else:
date_debut = datetime.datetime.combine(
self.jour, self.heure_debut or datetime.time(0, 0)
).isoformat()
date_fin = datetime.datetime.combine(
self.jour, self.heure_fin or datetime.time(0, 0)
).isoformat()
return { return {
"coefficient": self.coefficient, "coefficient": self.coefficient,
"date_debut": date_debut, "date_debut": self.date_debut.isoformat() if self.date_debut else "",
"date_fin": date_fin, "date_fin": self.date_fin.isoformat() if self.date_fin else "",
"description": self.description, "description": self.description,
"evaluation_type": self.evaluation_type, "evaluation_type": self.evaluation_type,
"id": self.id, "id": self.id,
@ -99,39 +209,135 @@ class Evaluation(db.Model):
"numero": self.numero, "numero": self.numero,
"poids": self.get_ue_poids_dict(), "poids": self.get_ue_poids_dict(),
"publish_incomplete": self.publish_incomplete, "publish_incomplete": self.publish_incomplete,
"visi_bulletin": self.visibulletin, "visibulletin": self.visibulletin,
# Deprecated (supprimer avant #sco9.7)
"date": self.date_debut.date().isoformat() if self.date_debut else "",
"heure_debut": self.date_debut.time().isoformat()
if self.date_debut
else "",
"heure_fin": self.date_fin.time().isoformat() if self.date_fin else "",
} }
def to_dict_bul(self) -> dict:
"dict pour les bulletins json"
# c'est la version API avec quelques champs legacy en plus
e_dict = self.to_dict_api()
# Pour les bulletins (json ou xml), quote toujours la description
e_dict["description"] = quote_xml_attr(self.description or "")
# deprecated fields:
e_dict["evaluation_id"] = self.id
e_dict["jour"] = e_dict["date_debut"] # chaine iso
e_dict["heure_debut"] = (
self.date_debut.time().isoformat() if self.date_debut else ""
)
e_dict["heure_fin"] = self.date_fin.time().isoformat() if self.date_fin else ""
return e_dict
def from_dict(self, data): def from_dict(self, data):
"""Set evaluation attributes from given dict values.""" """Set evaluation attributes from given dict values."""
check_evaluation_args(data) check_convert_evaluation_args(self.moduleimpl, data)
if data.get("numero") is None:
data["numero"] = Evaluation.get_max_numero(self.moduleimpl.id) + 1
for k in self.__dict__.keys(): for k in self.__dict__.keys():
if k != "_sa_instance_state" and k != "id" and k in data: if k != "_sa_instance_state" and k != "id" and k in data:
setattr(self, k, data[k]) setattr(self, k, data[k])
@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: def descr_heure(self) -> str:
"Description de la plage horaire pour affichages" "Description de la plage horaire pour affichages ('de 13h00 à 14h00')"
if self.heure_debut and ( if self.date_debut and (not self.date_fin or self.date_fin == self.date_debut):
not self.heure_fin or self.heure_fin == self.heure_debut return f"""à {self.date_debut.strftime("%Hh%M")}"""
): elif self.date_debut and self.date_fin:
return f"""à {self.heure_debut.strftime("%Hh%M")}""" return f"""de {self.date_debut.strftime("%Hh%M")
elif self.heure_debut and self.heure_fin: } à {self.date_fin.strftime("%Hh%M")}"""
return f"""de {self.heure_debut.strftime("%Hh%M")} à {self.heure_fin.strftime("%Hh%M")}"""
else: else:
return "" return ""
def descr_duree(self) -> str: def descr_duree(self) -> str:
"Description de la durée pour affichages" "Description de la durée pour affichages ('3h' ou '2h30')"
if self.heure_debut is None and self.heure_fin is None: if self.date_debut is None or self.date_fin is None:
return "" return ""
debut = self.heure_debut or DEFAULT_EVALUATION_TIME minutes = (self.date_fin - self.date_debut).seconds // 60
fin = self.heure_fin or DEFAULT_EVALUATION_TIME duree = f"{minutes // 60}h"
d = (fin.hour * 60 + fin.minute) - (debut.hour * 60 + debut.minute) minutes = minutes % 60
duree = f"{d//60}h" if minutes != 0:
if d % 60: duree += f"{minutes:02d}"
duree += f"{d%60:02d}"
return duree return duree
def descr_date(self) -> str:
"""Description de la date pour affichages
'sans date'
'le 21/9/2021 à 13h'
'le 21/9/2021 de 13h à 14h30'
'du 21/9/2021 à 13h30 au 23/9/2021 à 15h'
"""
if self.date_debut is None:
return "sans date"
def _h(dt: datetime.datetime) -> str:
if dt.minute:
return dt.strftime("%Hh%M")
return f"{dt.hour}h"
if self.date_fin is None:
return f"le {self.date_debut.strftime('%d/%m/%Y')} à {_h(self.date_debut)}"
if self.date_debut.date() == self.date_fin.date(): # même jour
if self.date_debut.time() == self.date_fin.time():
return (
f"le {self.date_debut.strftime('%d/%m/%Y')} à {_h(self.date_debut)}"
)
return f"""le {self.date_debut.strftime('%d/%m/%Y')} de {
_h(self.date_debut)} à {_h(self.date_fin)}"""
# évaluation sur plus d'une journée
return f"""du {self.date_debut.strftime('%d/%m/%Y')} à {
_h(self.date_debut)} au {self.date_fin.strftime('%d/%m/%Y')} à {_h(self.date_fin)}"""
def heure_debut(self) -> str:
"""L'heure de début (sans la date), en ISO.
Chaine vide si non renseignée."""
return self.date_debut.time().isoformat("minutes") if self.date_debut else ""
def heure_fin(self) -> str:
"""L'heure de fin (sans la date), en ISO.
Chaine vide si non renseignée."""
return self.date_fin.time().isoformat("minutes") if self.date_fin else ""
def clone(self, not_copying=()): def clone(self, not_copying=()):
"""Clone, not copying the given attrs """Clone, not copying the given attrs
Attention: la copie n'a pas d'id avant le prochain commit Attention: la copie n'a pas d'id avant le prochain commit
@ -146,19 +352,19 @@ class Evaluation(db.Model):
return copy return copy
def is_matin(self) -> bool: def is_matin(self) -> bool:
"Evaluation ayant lieu le matin (faux si pas de date)" "Evaluation commençant le matin (faux si pas de date)"
heure_debut_dt = self.heure_debut or datetime.time(8, 00) if not self.date_debut:
# 8:00 au cas ou pas d'heure (note externe?) return False
return bool(self.jour) and heure_debut_dt < datetime.time(12, 00) return self.date_debut.time() < NOON
def is_apresmidi(self) -> bool: def is_apresmidi(self) -> bool:
"Evaluation ayant lieu l'après midi (faux si pas de date)" "Evaluation commençant l'après midi (faux si pas de date)"
heure_debut_dt = self.heure_debut or datetime.time(8, 00) if not self.date_debut:
# 8:00 au cas ou pas d'heure (note externe?) return False
return bool(self.jour) and heure_debut_dt >= datetime.time(12, 00) return self.date_debut.time() >= NOON
def set_default_poids(self) -> bool: def set_default_poids(self) -> bool:
"""Initialize les poids bvers les UE à leurs valeurs par défaut """Initialize les poids vers les UE à leurs valeurs par défaut
C'est à dire à 1 si le coef. module/UE est non nul, 0 sinon. C'est à dire à 1 si le coef. module/UE est non nul, 0 sinon.
Les poids existants ne sont pas modifiés. Les poids existants ne sont pas modifiés.
Return True if (uncommited) modification, False otherwise. Return True if (uncommited) modification, False otherwise.
@ -191,6 +397,8 @@ class Evaluation(db.Model):
L = [] L = []
for ue_id, poids in ue_poids_dict.items(): for ue_id, poids in ue_poids_dict.items():
ue = db.session.get(UniteEns, ue_id) ue = db.session.get(UniteEns, ue_id)
if ue is None:
raise ScoValueError("poids vers une UE inexistante")
ue_poids = EvaluationUEPoids(evaluation=self, ue=ue, poids=poids) ue_poids = EvaluationUEPoids(evaluation=self, ue=ue, poids=poids)
L.append(ue_poids) L.append(ue_poids)
db.session.add(ue_poids) db.session.add(ue_poids)
@ -274,88 +482,158 @@ class EvaluationUEPoids(db.Model):
return f"<EvaluationUEPoids {self.evaluation} {self.ue} poids={self.poids}>" return f"<EvaluationUEPoids {self.evaluation} {self.ue} poids={self.poids}>"
# Fonction héritée de ScoDoc7 à refactorer # Fonction héritée de ScoDoc7
def evaluation_enrich_dict(e: dict): def evaluation_enrich_dict(e: Evaluation, e_dict: dict):
"""add or convert some fields in an evaluation dict""" """add or convert some fields in an evaluation dict"""
# For ScoDoc7 compat # For ScoDoc7 compat
heure_debut_dt = e["heure_debut"] or datetime.time( e_dict["heure_debut"] = e.date_debut.strftime("%Hh%M") if e.date_debut else ""
8, 00 e_dict["heure_fin"] = e.date_fin.strftime("%Hh%M") if e.date_fin else ""
) # au cas ou pas d'heure (note externe?) e_dict["jour_iso"] = e.date_debut.isoformat() if e.date_debut else ""
heure_fin_dt = e["heure_fin"] or datetime.time(8, 00) # Calcule durée en minutes
e["heure_debut"] = ndb.TimefromISO8601(e["heure_debut"]) e_dict["descrheure"] = e.descr_heure()
e["heure_fin"] = ndb.TimefromISO8601(e["heure_fin"]) e_dict["descrduree"] = e.descr_duree()
e["jour_iso"] = ndb.DateDMYtoISO(e["jour"])
heure_debut, heure_fin = e["heure_debut"], e["heure_fin"]
d = ndb.TimeDuration(heure_debut, heure_fin)
if d is not None:
m = d % 60
e["duree"] = "%dh" % (d / 60)
if m != 0:
e["duree"] += "%02d" % m
else:
e["duree"] = ""
if heure_debut and (not heure_fin or heure_fin == heure_debut):
e["descrheure"] = " à " + heure_debut
elif heure_debut and heure_fin:
e["descrheure"] = " de %s à %s" % (heure_debut, heure_fin)
else:
e["descrheure"] = ""
# matin, apresmidi: utile pour se referer aux absences: # matin, apresmidi: utile pour se referer aux absences:
# note août 2023: si l'évaluation s'étend sur plusieurs jours,
if e["jour"] and heure_debut_dt < datetime.time(12, 00): # cet indicateur n'a pas grand sens
e["matin"] = 1 if e.date_debut and e.date_debut.time() < datetime.time(12, 00):
e_dict["matin"] = 1
else: else:
e["matin"] = 0 e_dict["matin"] = 0
if e["jour"] and heure_fin_dt > datetime.time(12, 00): if e.date_fin and e.date_fin.time() > datetime.time(12, 00):
e["apresmidi"] = 1 e_dict["apresmidi"] = 1
else: else:
e["apresmidi"] = 0 e_dict["apresmidi"] = 0
return e return e_dict
def check_evaluation_args(args): def check_convert_evaluation_args(moduleimpl: ModuleImpl, data: dict):
"Check coefficient, dates and duration, raises exception if invalid" """Check coefficient, dates and duration, raises exception if invalid.
moduleimpl_id = args["moduleimpl_id"] Convert date and time strings to date and time objects.
# check bareme
note_max = args.get("note_max", None) Set required default value for unspecified fields.
if note_max is None: May raise ScoValueError.
raise ScoValueError("missing note_max") """
# --- description
data["description"] = data.get("description", "") or ""
if len(data["description"]) > scu.MAX_TEXT_LEN:
raise ScoValueError("description too large")
# --- evaluation_type
try:
data["evaluation_type"] = int(data.get("evaluation_type", 0) or 0)
if not data["evaluation_type"] in VALID_EVALUATION_TYPES:
raise ScoValueError("invalid evaluation_type value")
except ValueError as exc:
raise ScoValueError("invalid evaluation_type value") from exc
# --- note_max (bareme)
note_max = data.get("note_max", 20.0) or 20.0
try: try:
note_max = float(note_max) note_max = float(note_max)
except ValueError: except ValueError as exc:
raise ScoValueError("Invalid note_max value") raise ScoValueError("invalid note_max value") from exc
if note_max < 0: if note_max < 0:
raise ScoValueError("Invalid note_max value (must be positive or null)") raise ScoValueError("invalid note_max value (must be positive or null)")
# check coefficient data["note_max"] = note_max
coef = args.get("coefficient", None) # --- coefficient
if coef is None: coef = data.get("coefficient", 1.0) or 1.0
raise ScoValueError("missing coefficient")
try: try:
coef = float(coef) coef = float(coef)
except ValueError: except ValueError as exc:
raise ScoValueError("Invalid coefficient value") raise ScoValueError("invalid coefficient value") from exc
if coef < 0: if coef < 0:
raise ScoValueError("Invalid coefficient value (must be positive or null)") raise ScoValueError("invalid coefficient value (must be positive or null)")
# check date data["coefficient"] = coef
jour = args.get("jour", None) # --- date de l'évaluation
args["jour"] = jour formsemestre = moduleimpl.formsemestre
if jour: date_debut = data.get("date_debut", None)
modimpl = db.session.get(ModuleImpl, moduleimpl_id) if date_debut:
formsemestre = modimpl.formsemestre if isinstance(date_debut, str):
y, m, d = [int(x) for x in ndb.DateDMYtoISO(jour).split("-")] data["date_debut"] = datetime.datetime.fromisoformat(date_debut)
jour = datetime.date(y, m, d) if data["date_debut"].tzinfo is None:
if (jour > formsemestre.date_fin) or (jour < formsemestre.date_debut): data["date_debut"] = scu.TIME_ZONE.localize(data["date_debut"])
if not (
formsemestre.date_debut
<= data["date_debut"].date()
<= formsemestre.date_fin
):
raise ScoValueError( raise ScoValueError(
"La date de l'évaluation (%s/%s/%s) n'est pas dans le semestre !" f"""La date de début de l'évaluation ({
% (d, m, y), data["date_debut"].strftime("%d/%m/%Y")
}) n'est pas dans le semestre !""",
dest_url="javascript:history.back();", dest_url="javascript:history.back();",
) )
heure_debut = args.get("heure_debut", None) date_fin = data.get("date_fin", None)
args["heure_debut"] = heure_debut if date_fin:
heure_fin = args.get("heure_fin", None) if isinstance(date_fin, str):
args["heure_fin"] = heure_fin data["date_fin"] = datetime.datetime.fromisoformat(date_fin)
if jour and ((not heure_debut) or (not heure_fin)): if data["date_fin"].tzinfo is None:
raise ScoValueError("Les heures doivent être précisées") data["date_fin"] = scu.TIME_ZONE.localize(data["date_fin"])
d = ndb.TimeDuration(heure_debut, heure_fin) if not (
if d and ((d < 0) or (d > 60 * 12)): formsemestre.date_debut <= data["date_fin"].date() <= formsemestre.date_fin
raise ScoValueError("Heures de l'évaluation incohérentes !") ):
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

View File

@ -30,6 +30,7 @@ from app.models.but_refcomp import (
) )
from app.models.config import ScoDocSiteConfig from app.models.config import ScoDocSiteConfig
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.evaluations import Evaluation
from app.models.formations import Formation from app.models.formations import Formation
from app.models.groups import GroupDescr, Partition from app.models.groups import GroupDescr, Partition
from app.models.moduleimpls import ModuleImpl, ModuleImplInscription from app.models.moduleimpls import ModuleImpl, ModuleImplInscription
@ -350,6 +351,21 @@ class FormSemestre(db.Model):
_cache[key] = ues _cache[key] = ues
return ues return ues
def get_evaluations(self) -> list[Evaluation]:
"Liste de toutes les évaluations du semestre, triées par module/numero"
return (
Evaluation.query.join(ModuleImpl)
.filter_by(formsemestre_id=self.id)
.join(Module)
.order_by(
Module.numero,
Module.code,
Evaluation.numero,
Evaluation.date_debut.desc(),
)
.all()
)
@cached_property @cached_property
def modimpls_sorted(self) -> list[ModuleImpl]: def modimpls_sorted(self) -> list[ModuleImpl]:
"""Liste des modimpls du semestre (y compris bonus) """Liste des modimpls du semestre (y compris bonus)
@ -825,7 +841,7 @@ class FormSemestre(db.Model):
Les groupes de parcours sont ceux de la partition scu.PARTITION_PARCOURS Les groupes de parcours sont ceux de la partition scu.PARTITION_PARCOURS
et leur nom est le code du parcours (eg "Cyber"). et leur nom est le code du parcours (eg "Cyber").
Si etudid est sépcifié, n'affecte que cet étudiant, Si etudid est spécifié, n'affecte que cet étudiant,
sinon traite tous les inscrits du semestre. sinon traite tous les inscrits du semestre.
""" """
if self.formation.referentiel_competence_id is None: if self.formation.referentiel_competence_id is None:

View File

@ -11,8 +11,8 @@ from operator import attrgetter
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from app import db, log from app import db, log
from app.models import SHORT_STR_LEN from app.models import Scolog, GROUPNAME_STR_LEN, SHORT_STR_LEN
from app.models import GROUPNAME_STR_LEN from app.scodoc import sco_cache
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
@ -83,6 +83,14 @@ class Partition(db.Model):
return False return False
return True return True
@classmethod
def formsemestre_remove_etud(cls, formsemestre_id: int, etud: "Identite"):
"retire l'étudiant de toutes les partitions de ce semestre"
for group in GroupDescr.query.join(Partition).filter_by(
formsemestre_id=formsemestre_id
):
group.remove_etud(etud)
def is_parcours(self) -> bool: def is_parcours(self) -> bool:
"Vrai s'il s'agit de la partition de parcours" "Vrai s'il s'agit de la partition de parcours"
return self.partition_name == scu.PARTITION_PARCOURS return self.partition_name == scu.PARTITION_PARCOURS
@ -248,6 +256,24 @@ class GroupDescr(db.Model):
return False return False
return True return True
def remove_etud(self, etud: "Identite"):
"Enlève l'étudiant de ce groupe s'il en fait partie (ne fait rien sinon)"
if etud in self.etuds:
self.etuds.remove(etud)
db.session.commit()
Scolog.logdb(
method="group_remove_etud",
etudid=etud.id,
msg=f"Retrait du groupe {self.group_name} de {self.partition.partition_name}",
commit=True,
)
# Update parcours
if self.partition.partition_name == scu.PARTITION_PARCOURS:
self.partition.formsemestre.update_inscriptions_parcours_from_groups(
etudid=etud.id
)
sco_cache.invalidate_formsemestre(self.partition.formsemestre_id)
group_membership = db.Table( group_membership = db.Table(
"group_membership", "group_membership",

View File

@ -101,6 +101,49 @@ class ModuleImpl(db.Model):
d.pop("module", None) d.pop("module", None)
return d return d
def can_edit_evaluation(self, user) -> bool:
"""True if this user can create, delete or edit and evaluation in this modimpl
(nb: n'implique pas le droit de saisir ou modifier des notes)
"""
# acces pour resp. moduleimpl et resp. form semestre (dir etud)
if (
user.has_permission(Permission.ScoEditAllEvals)
or user.id == self.responsable_id
or user.id in (r.id for r in self.formsemestre.responsables)
):
return True
elif self.formsemestre.ens_can_edit_eval:
if user.id in (e.id for e in self.enseignants):
return True
return False
def can_edit_notes(self, user: "User", allow_ens=True) -> bool:
"""True if authuser can enter or edit notes in this module.
If allow_ens, grant access to all ens in this module
Si des décisions de jury ont déjà été saisies dans ce semestre,
seul le directeur des études peut saisir des notes (et il ne devrait pas).
"""
# was sco_permissions_check.can_edit_notes
from app.scodoc import sco_cursus_dut
if not self.formsemestre.etat:
return False # semestre verrouillé
is_dir_etud = user.id in (u.id for u in self.formsemestre.responsables)
can_edit_all_notes = user.has_permission(Permission.ScoEditAllNotes)
if sco_cursus_dut.formsemestre_has_decisions(self.formsemestre_id):
# il y a des décisions de jury dans ce semestre !
return can_edit_all_notes or is_dir_etud
if (
not can_edit_all_notes
and user.id != self.responsable_id
and not is_dir_etud
):
# enseignant (chargé de TD) ?
return allow_ens and user.id in (ens.id for ens in self.enseignants)
return True
def can_change_ens_by(self, user: User, raise_exc=False) -> bool: def can_change_ens_by(self, user: User, raise_exc=False) -> bool:
"""Check if user can modify module resp. """Check if user can modify module resp.
If raise_exc, raises exception (AccessDenied or ScoLockedSemError) if not. If raise_exc, raises exception (AccessDenied or ScoLockedSemError) if not.

View File

@ -153,6 +153,10 @@ class Module(db.Model):
""" """
return scu.ModuleType.get_abbrev(self.module_type) return scu.ModuleType.get_abbrev(self.module_type)
def titre_str(self) -> str:
"Identifiant du module à afficher : abbrev ou titre ou code"
return self.abbrev or self.titre or self.code
def sort_key_apc(self) -> tuple: def sort_key_apc(self) -> tuple:
"""Clé de tri pour avoir """Clé de tri pour avoir
présentation par type (res, sae), parcours, type, numéro présentation par type (res, sae), parcours, type, numéro

View File

@ -57,8 +57,10 @@ def _pe_view_sem_recap_form(formsemestre_id):
poursuites d'études. poursuites d'études.
<br> <br>
De nombreux aspects sont paramétrables: De nombreux aspects sont paramétrables:
<a href="https://scodoc.org/AvisPoursuiteEtudes" target="_blank" rel="noopener"> <a href="https://scodoc.org/AvisPoursuiteEtudes"
voir la documentation</a>. target="_blank" rel="noopener noreferrer">
voir la documentation
</a>.
</p> </p>
<form method="post" action="pe_view_sem_recap" id="pe_view_sem_recap_form" <form method="post" action="pe_view_sem_recap" id="pe_view_sem_recap_form"
enctype="multipart/form-data"> enctype="multipart/form-data">

View File

@ -152,8 +152,10 @@ def sidebar(etudid: int = None):
# Logo # Logo
H.append( H.append(
f"""<div class="logo-insidebar"> f"""<div class="logo-insidebar">
<div class="sidebar-bottom"><a href="{ url_for( 'scodoc.about', scodoc_dept=g.scodoc_dept ) }" class="sidebar">À propos</a><br> <div class="sidebar-bottom"><a href="{
<a href="{ scu.SCO_USER_MANUAL }" target="_blank" class="sidebar">Aide</a> url_for( 'scodoc.about', scodoc_dept=g.scodoc_dept )
}" class="sidebar">À propos</a><br>
<a href="{ scu.SCO_USER_MANUAL }" target="_blank" rel="noopener" class="sidebar">Aide</a>
</div></div> </div></div>
<div class="logo-logo"> <div class="logo-logo">
<a href="{ url_for( 'scodoc.about', scodoc_dept=g.scodoc_dept ) }"> <a href="{ url_for( 'scodoc.about', scodoc_dept=g.scodoc_dept ) }">

View File

@ -1,19 +1,17 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import datetime
import html import html
import traceback import traceback
from flask import g, current_app, abort
import psycopg2 import psycopg2
import psycopg2.pool import psycopg2.pool
import psycopg2.extras import psycopg2.extras
from flask import g, current_app, abort
import app
import app.scodoc.sco_utils as scu
from app import log from app import log
from app.scodoc.sco_exceptions import ScoException, ScoValueError, NoteProcessError from app.scodoc.sco_exceptions import ScoException, ScoValueError, NoteProcessError
import datetime
quote_html = html.escape quote_html = html.escape
@ -459,8 +457,10 @@ def dictfilter(d, fields, filter_nulls=True):
# --- Misc Tools # --- Misc Tools
def DateDMYtoISO(dmy: str, null_is_empty=False) -> str: def DateDMYtoISO(dmy: str, null_is_empty=False) -> str: # XXX deprecated
"convert date string from french format to ISO" """Convert date string from french format to ISO.
If null_is_empty (default false), returns "" if no input.
"""
if not dmy: if not dmy:
if null_is_empty: if null_is_empty:
return "" return ""
@ -506,7 +506,7 @@ def DateISOtoDMY(isodate):
return "%02d/%02d/%04d" % (day, month, year) return "%02d/%02d/%04d" % (day, month, year)
def TimetoISO8601(t, null_is_empty=False): def TimetoISO8601(t, null_is_empty=False) -> str:
"convert time string to ISO 8601 (allow 16:03, 16h03, 16)" "convert time string to ISO 8601 (allow 16:03, 16h03, 16)"
if isinstance(t, datetime.time): if isinstance(t, datetime.time):
return t.isoformat() return t.isoformat()
@ -518,7 +518,7 @@ def TimetoISO8601(t, null_is_empty=False):
return t return t
def TimefromISO8601(t): def TimefromISO8601(t) -> str:
"convert time string from ISO 8601 to our display format" "convert time string from ISO 8601 to our display format"
if not t: if not t:
return t return t
@ -532,19 +532,6 @@ def TimefromISO8601(t):
return fs[0] + "h" + fs[1] # discard seconds return fs[0] + "h" + fs[1] # discard seconds
def TimeDuration(heure_debut, heure_fin):
"""duree (nb entier de minutes) entre deux heures a notre format
ie 12h23
"""
if heure_debut and heure_fin:
h0, m0 = [int(x) for x in heure_debut.split("h")]
h1, m1 = [int(x) for x in heure_fin.split("h")]
d = (h1 - h0) * 60 + (m1 - m0)
return d
else:
return None
def float_null_is_zero(x): def float_null_is_zero(x):
if x is None or x == "": if x is None or x == "":
return 0.0 return 0.0

View File

@ -318,7 +318,7 @@ def list_abs_in_range(
Returns: Returns:
List of absences List of absences
""" """
if matin != None: if matin is not None:
matin = _toboolean(matin) matin = _toboolean(matin)
ismatin = " AND A.MATIN = %(matin)s " ismatin = " AND A.MATIN = %(matin)s "
else: else:
@ -387,7 +387,7 @@ def count_abs_just(etudid, debut, fin, matin=None, moduleimpl_id=None) -> int:
Returns: Returns:
An integer. An integer.
""" """
if matin != None: if matin is not None:
matin = _toboolean(matin) matin = _toboolean(matin)
ismatin = " AND A.MATIN = %(matin)s " ismatin = " AND A.MATIN = %(matin)s "
else: else:
@ -482,7 +482,9 @@ def _get_abs_description(a, cursor=None):
else: else:
a["matin"] = False a["matin"] = False
cursor.execute( cursor.execute(
"""select * from absences where etudid=%(etudid)s and jour=%(jour)s and matin=%(matin)s order by entry_date desc""", """SELECT * FROM absences
WHERE etudid=%(etudid)s AND jour=%(jour)s AND matin=%(matin)s
ORDER BY entry_date desc""",
a, a,
) )
A = cursor.dictfetchall() A = cursor.dictfetchall()
@ -507,7 +509,7 @@ def _get_abs_description(a, cursor=None):
return "" return ""
def list_abs_jour(date, am=True, pm=True, is_abs=True, is_just=None): def list_abs_jour(date, am=True, pm=True, is_abs=True, is_just=None) -> list[dict]:
"""Liste des absences et/ou justificatifs ce jour. """Liste des absences et/ou justificatifs ce jour.
is_abs: None (peu importe), True, False is_abs: None (peu importe), True, False
is_just: idem is_just: idem
@ -517,9 +519,9 @@ def list_abs_jour(date, am=True, pm=True, is_abs=True, is_just=None):
req = """SELECT DISTINCT etudid, jour, matin FROM ABSENCES A req = """SELECT DISTINCT etudid, jour, matin FROM ABSENCES A
WHERE A.jour = %(date)s WHERE A.jour = %(date)s
""" """
if is_abs != None: if is_abs is not None:
req += " AND A.estabs = %(is_abs)s" req += " AND A.estabs = %(is_abs)s"
if is_just != None: if is_just is not None:
req += " AND A.estjust = %(is_just)s" req += " AND A.estjust = %(is_just)s"
if not am: if not am:
req += " AND NOT matin " req += " AND NOT matin "
@ -533,7 +535,7 @@ WHERE A.jour = %(date)s
return A return A
def list_abs_non_just_jour(date, am=True, pm=True): def list_abs_non_just_jour(date, am=True, pm=True) -> list[dict]:
"Liste des absences non justifiees ce jour" "Liste des absences non justifiees ce jour"
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
@ -883,7 +885,7 @@ def MonthTableBody(
descr = ev[4] descr = ev[4]
# #
cc = [] cc = []
if color != None: if color is not None:
cc.append('<td bgcolor="%s" class="calcell">' % color) cc.append('<td bgcolor="%s" class="calcell">' % color)
else: else:
cc.append('<td class="calcell">') cc.append('<td class="calcell">')
@ -896,7 +898,7 @@ def MonthTableBody(
cc.append("<a %s %s>" % (href, descr)) cc.append("<a %s %s>" % (href, descr))
if legend or d == 1: if legend or d == 1:
if pad_width != None: if pad_width is not None:
n = pad_width - len(legend) # pad to 8 cars n = pad_width - len(legend) # pad to 8 cars
if n > 0: if n > 0:
legend = ( legend = (
@ -959,7 +961,7 @@ def MonthTableBody(
ev_year = int(ev[0][:4]) ev_year = int(ev[0][:4])
ev_month = int(ev[0][5:7]) ev_month = int(ev[0][5:7])
ev_day = int(ev[0][8:10]) ev_day = int(ev[0][8:10])
if ev[4] != None: if ev[4] is not None:
ev_half = int(ev[4]) ev_half = int(ev[4])
else: else:
ev_half = 0 ev_half = 0
@ -978,7 +980,7 @@ def MonthTableBody(
if len(ev) > 5 and ev[5]: if len(ev) > 5 and ev[5]:
descr = ev[5] descr = ev[5]
# #
if color != None: if color is not None:
cc.append('<td bgcolor="%s" class="calcell">' % (color)) cc.append('<td bgcolor="%s" class="calcell">' % (color))
else: else:
cc.append('<td class="calcell">') cc.append('<td class="calcell">')
@ -1072,7 +1074,8 @@ def invalidate_abs_count_sem(sem):
def invalidate_abs_etud_date(etudid, date): # was invalidateAbsEtudDate def invalidate_abs_etud_date(etudid, date): # was invalidateAbsEtudDate
"""Doit etre appelé à chaque modification des absences pour cet étudiant et cette date. """Doit etre appelé à chaque modification des absences
pour cet étudiant et cette date.
Invalide cache absence et caches semestre Invalide cache absence et caches semestre
date: date au format ISO date: date au format ISO
""" """

File diff suppressed because it is too large Load Diff

View File

@ -321,9 +321,10 @@ def filter_by_formsemestre(
def justifies(justi: Justificatif, obj: bool = False) -> list[int] or Query: def justifies(justi: Justificatif, obj: bool = False) -> list[int] or Query:
""" """
Retourne la liste des assiduite_id qui sont justifié par la justification 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 Une assiduité est justifiée si elle est COMPLETEMENT ou PARTIELLEMENT
et que l'état du justificatif est "valide" comprise dans la plage du justificatif
renvoie des id si obj == False, sinon les Assiduités et que l'état du justificatif est "valide".
Renvoie des id si obj == False, sinon les Assiduités
""" """
if justi.etat != scu.EtatJustificatif.VALIDE: if justi.etat != scu.EtatJustificatif.VALIDE:
@ -341,7 +342,10 @@ def justifies(justi: Justificatif, obj: bool = False) -> list[int] or Query:
def get_all_justified( def get_all_justified(
etudid: int, date_deb: datetime = None, date_fin: datetime = None etudid: int,
date_deb: datetime = None,
date_fin: datetime = None,
moduleimpl_id: int = False,
) -> Query: ) -> Query:
"""Retourne toutes les assiduités justifiées sur une période""" """Retourne toutes les assiduités justifiées sur une période"""
@ -359,11 +363,14 @@ def get_all_justified(
date_deb, date_deb,
date_fin, date_fin,
) )
if moduleimpl_id is not False:
after = after.filter_by(moduleimpl_id=moduleimpl_id)
return after return after
# Gestion du cache # 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: """Les comptes d'absences de cet étudiant dans ce semestre:
tuple (nb abs non justifiées, nb abs justifiées) tuple (nb abs non justifiées, nb abs justifiées)
Utilise un cache. Utilise un cache.
@ -377,27 +384,58 @@ def get_assiduites_count(etudid, sem):
) )
def formsemestre_get_assiduites_count(
etudid: int,
formsemestre: FormSemestre,
moduleimpl_id: int = False,
cache: bool = True,
) -> tuple[int, int]:
"""Les comptes d'absences de cet étudiant dans ce semestre:
tuple (nb abs non justifiées, nb abs justifiées)
Utilise par défaut un cache.
"""
metrique = sco_preferences.get_preference("assi_metrique", formsemestre.id)
return get_assiduites_count_in_interval(
etudid,
date_debut_iso=formsemestre.date_debut.isoformat(),
date_fin_iso=formsemestre.date_fin.isoformat(),
metrique=scu.translate_assiduites_metric(metrique),
cache=cache,
moduleimpl_id=moduleimpl_id,
)
def get_assiduites_count_in_interval( 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,
cache: bool = True,
moduleimpl_id: int = False,
): ):
"""Les comptes d'absences de cet étudiant entre ces deux dates, incluses: """Les comptes d'absences de cet étudiant entre ces deux dates, incluses:
tuple (nb abs, nb abs justifiées) tuple (nb abs, nb abs justifiées)
Utilise un cache. On peut spécifier les dates comme datetime ou iso.
Utilise par défaut un cache.
""" """
key = ( date_debut_iso = date_debut_iso or date_debut.isoformat()
str(etudid) date_fin_iso = date_fin_iso or date_fin.isoformat()
+ "_" key = f"{etudid}_{date_debut_iso}_{date_fin_iso}{metrique}_assiduites"
+ date_debut_iso if moduleimpl_id is not False:
+ "_" key += f"_module_{moduleimpl_id}"
+ date_fin_iso
+ f"{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)
assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid) r = False if cache is False else sco_cache.AbsSemEtudCache.get(key)
if not r:
date_debut: datetime = date_debut or datetime.fromisoformat(date_debut_iso)
date_fin: datetime = date_fin or datetime.fromisoformat(date_fin_iso)
assiduites: Assiduite = (
Assiduite.query.filter_by(etudid=etudid)
if moduleimpl_id is False
else Assiduite.query.filter_by(etudid=etudid, moduleimpl_id=moduleimpl_id)
)
assiduites = assiduites.filter(Assiduite.etat == scu.EtatAssiduite.ABSENT) assiduites = assiduites.filter(Assiduite.etat == scu.EtatAssiduite.ABSENT)
justificatifs: Justificatif = Justificatif.query.filter_by(etudid=etudid) justificatifs: Justificatif = Justificatif.query.filter_by(etudid=etudid)
@ -410,16 +448,20 @@ def get_assiduites_count_in_interval(
calculator.compute_assiduites(assiduites) calculator.compute_assiduites(assiduites)
nb_abs: dict = calculator.to_dict()[metrique] nb_abs: dict = calculator.to_dict()[metrique]
abs_just: list[Assiduite] = get_all_justified(etudid, date_debut, date_fin) abs_just: list[Assiduite] = get_all_justified(
etudid, date_debut, date_fin, moduleimpl_id=moduleimpl_id
)
calculator.reset() calculator.reset()
calculator.compute_assiduites(abs_just) calculator.compute_assiduites(abs_just)
nb_abs_just: dict = calculator.to_dict()[metrique] nb_abs_just: dict = calculator.to_dict()[metrique]
r = (nb_abs, nb_abs_just) r = (nb_abs, nb_abs_just)
ans = sco_cache.AbsSemEtudCache.set(key, r)
if not ans: if cache:
log("warning: get_assiduites_count failed to cache") ans = sco_cache.AbsSemEtudCache.set(key, r)
if not ans:
log("warning: get_assiduites_count failed to cache")
return r return r
@ -427,7 +469,7 @@ def invalidate_assiduites_count(etudid, sem):
"""Invalidate (clear) cached counts""" """Invalidate (clear) cached counts"""
date_debut = sem["date_debut_iso"] date_debut = sem["date_debut_iso"]
date_fin = sem["date_fin_iso"] date_fin = sem["date_fin_iso"]
for met in [string.lower() for string in scu.AssiduitesMetricShort.all()]: for met in scu.AssiduitesMetrics.TAG:
key = str(etudid) + "_" + date_debut + "_" + date_fin + f"{met}_assiduites" key = str(etudid) + "_" + date_debut + "_" + date_fin + f"{met}_assiduites"
sco_cache.AbsSemEtudCache.delete(key) sco_cache.AbsSemEtudCache.delete(key)
@ -444,9 +486,9 @@ def invalidate_assiduites_count_sem(sem):
def invalidate_assiduites_etud_date(etudid, date: datetime): 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 Invalide cache absence et caches semestre
date: date au format ISO
""" """
from app.scodoc import sco_compute_moy from app.scodoc import sco_compute_moy

View File

@ -47,6 +47,7 @@ from app.comp.res_but import ResultatsSemestreBUT
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models import ( from app.models import (
ApcParcours, ApcParcours,
Evaluation,
Formation, Formation,
FormSemestre, FormSemestre,
Identite, Identite,
@ -57,14 +58,12 @@ from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import htmlutils from app.scodoc import htmlutils
from app.scodoc import sco_assiduites from app.scodoc import sco_assiduites
from app.scodoc import sco_abs_views
from app.scodoc import sco_bulletins_generator from app.scodoc import sco_bulletins_generator
from app.scodoc import sco_bulletins_json from app.scodoc import sco_bulletins_json
from app.scodoc import sco_bulletins_pdf from app.scodoc import sco_bulletins_pdf
from app.scodoc import sco_bulletins_xml from app.scodoc import sco_bulletins_xml
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
@ -482,6 +481,7 @@ def _ue_mod_bulletin(
mods = [] # result mods = [] # result
ue_attente = False # true si une eval en attente dans cette UE ue_attente = False # true si une eval en attente dans cette UE
for modimpl in ue_modimpls: for modimpl in ue_modimpls:
modimpl_results = nt.modimpls_results.get(modimpl["moduleimpl_id"])
mod_attente = False mod_attente = False
mod = modimpl.copy() mod = modimpl.copy()
mod_moy = nt.get_etud_mod_moy( mod_moy = nt.get_etud_mod_moy(
@ -531,10 +531,13 @@ def _ue_mod_bulletin(
scu.fmt_coef(modimpl["module"]["coefficient"]), scu.fmt_coef(modimpl["module"]["coefficient"]),
sco_users.user_info(modimpl["responsable_id"])["nomcomplet"], sco_users.user_info(modimpl["responsable_id"])["nomcomplet"],
) )
link_mod = ( link_mod = f"""<a class="bull_link" href="{
'<a class="bull_link" href="moduleimpl_status?moduleimpl_id=%s" title="%s">' url_for("notes.moduleimpl_status",
% (modimpl["moduleimpl_id"], mod["mod_descr_txt"]) scodoc_dept=g.scodoc_dept,
) moduleimpl_id=modimpl["moduleimpl_id"]
)
}" title="{mod["mod_descr_txt"]}">"""
if sco_preferences.get_preference("bul_show_codemodules", formsemestre_id): if sco_preferences.get_preference("bul_show_codemodules", formsemestre_id):
mod["code"] = modimpl["module"]["code"] mod["code"] = modimpl["module"]["code"]
mod["code_html"] = link_mod + (mod["code"] or "") + "</a>" mod["code_html"] = link_mod + (mod["code"] or "") + "</a>"
@ -561,91 +564,88 @@ def _ue_mod_bulletin(
mod["code_txt"] = "" mod["code_txt"] = ""
mod["code_html"] = "" mod["code_html"] = ""
# Evaluations: notes de chaque eval # Evaluations: notes de chaque eval
evals = nt.get_evals_in_mod(modimpl["moduleimpl_id"]) evaluations_completes = nt.get_modimpl_evaluations_completes(
modimpl["moduleimpl_id"]
)
# On liste séparément les éval. complètes ou non
mod["evaluations"] = [] mod["evaluations"] = []
for e in evals: mod["evaluations_incompletes"] = []
e = e.copy() complete_eval_ids = {e.id for e in evaluations_completes}
if e["visibulletin"] or version == "long": all_evals: list[Evaluation] = Evaluation.query.filter_by(
# affiche "bonus" quand les points de malus sont négatifs moduleimpl_id=modimpl["moduleimpl_id"]
if is_malus: ).order_by(Evaluation.numero, Evaluation.date_debut)
val = e["notes"].get(etudid, {"value": "NP"})[ # (plus ancienne d'abord)
"value" for e in all_evals:
] # NA si etud demissionnaire if not e.visibulletin and version != "long":
if val == "NP" or val > 0: continue
e["name"] = "Points de malus sur cette UE" is_complete = e.id in complete_eval_ids
else: e_dict = e.to_dict_bul()
e["name"] = "Points de bonus sur cette UE" # 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: else:
e["name"] = e["description"] or f"le {e['jour']}" e_dict["name"] = "Points de bonus sur cette UE"
e["target_html"] = url_for( 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", "notes.evaluation_listenotes",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
evaluation_id=e["evaluation_id"], evaluation_id=e.id,
format="html", format="html",
tf_submitted=1, tf_submitted=1,
) )
e[ e_dict[
"name_html" "name_html"
] = f"""<a class="bull_link" href="{ ] = f"""<a class="bull_link" href="{
e['target_html']}">{e['name']}</a>""" e_dict['target_html']}">{e_dict['name']}</a>"""
val = e["notes"].get(etudid, {"value": "NP"})["value"] if is_complete: # évaluation complète
# val est NP si etud demissionnaire # val est NP si etud demissionnaire
if val == "NP": if val == "NP":
e["note_txt"] = "nd" e_dict["note_txt"] = "nd"
e["note_html"] = '<span class="note_nd">nd</span>' e_dict["note_html"] = '<span class="note_nd">nd</span>'
e["coef_txt"] = scu.fmt_coef(e["coefficient"]) e_dict["coef_txt"] = scu.fmt_coef(e["coefficient"])
else:
# (-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"] = ""
else: else:
e["coef_txt"] = scu.fmt_coef(e["coefficient"]) # (-0.15) s'affiche "bonus de 0.15"
if e["evaluation_type"] == scu.EVALUATION_RATTRAPAGE: if is_malus:
e["coef_txt"] = "rat." val = abs(val)
elif e["evaluation_type"] == scu.EVALUATION_SESSION2: e_dict["note_txt"] = e_dict["note_html"] = scu.fmt_note(
e["coef_txt"] = "Ses. 2" val, note_max=e.note_max
if e["etat"]["evalattente"]: )
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 mod_attente = True # une eval en attente dans ce module
if ((not is_malus) or (val != "NP")) and ( 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 # ne liste pas les eval malus sans notes
# ni les rattrapages et sessions 2 si pas de note # ni les rattrapages et sessions 2 si pas de note
mod["evaluations"].append(e) if e.id in complete_eval_ids:
mod["evaluations"].append(e_dict)
else:
mod["evaluations_incompletes"].append(e_dict)
# Evaluations incomplètes ou futures:
mod["evaluations_incompletes"] = []
if sco_preferences.get_preference("bul_show_all_evals", formsemestre_id):
complete_eval_ids = set([e["evaluation_id"] for e in evals])
all_evals = sco_evaluation_db.do_evaluation_list(
args={"moduleimpl_id": modimpl["moduleimpl_id"]}
)
all_evals.reverse() # plus ancienne d'abord
for e in all_evals:
if e["evaluation_id"] not in complete_eval_ids:
e = e.copy()
mod["evaluations_incompletes"].append(e)
e["name"] = (e["description"] or "") + " (%s)" % e["jour"]
e["target_html"] = url_for(
"notes.evaluation_listenotes",
scodoc_dept=g.scodoc_dept,
evaluation_id=e["evaluation_id"],
tf_submitted=1,
format="html",
)
e["name_html"] = '<a class="bull_link" href="%s">%s</a>' % (
e["target_html"],
e["name"],
)
e["note_txt"] = e["note_html"] = ""
e["coef_txt"] = scu.fmt_coef(e["coefficient"])
# Classement # Classement
if ( if (
bul_show_mod_rangs bul_show_mod_rangs
@ -1114,9 +1114,10 @@ def mail_bulletin(formsemestre_id, infos, pdfdata, filename, recipient_addr):
hea = "" hea = ""
if sco_preferences.get_preference("bul_mail_list_abs"): if sco_preferences.get_preference("bul_mail_list_abs"):
hea += "\n\n" + sco_abs_views.ListeAbsEtud( hea += "\n\n" + "(LISTE D'ABSENCES NON DISPONIBLE)" # XXX TODO-ASSIDUITE
etud["etudid"], with_evals=False, format="text" # sco_abs_views.ListeAbsEtud(
) # etud["etudid"], with_evals=False, format="text"
# )
subject = f"""Relevé de notes de {etud["nomprenom"]}""" subject = f"""Relevé de notes de {etud["nomprenom"]}"""
recipients = [recipient_addr] recipients = [recipient_addr]

View File

@ -37,7 +37,7 @@ from app import db, ScoDocJSONEncoder
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models import but_validations from app.models import but_validations
from app.models import Matiere, ModuleImpl, UniteEns from app.models import Evaluation, Matiere, UniteEns
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.formsemestre import FormSemestre from app.models.formsemestre import FormSemestre
@ -46,7 +46,6 @@ import app.scodoc.notesdb as ndb
from app.scodoc import sco_assiduites from app.scodoc import sco_assiduites
from app.scodoc import sco_edit_ue from app.scodoc import sco_edit_ue
from app.scodoc import sco_evaluations from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_photos from app.scodoc import sco_photos
@ -112,7 +111,7 @@ def formsemestre_bulletinetud_published_dict(
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
if not etudid in nt.identdict: if etudid not in nt.identdict:
abort(404, "etudiant non inscrit dans ce semestre") abort(404, "etudiant non inscrit dans ce semestre")
d = {"type": "classic", "version": "0"} d = {"type": "classic", "version": "0"}
if (not sem["bul_hide_xml"]) or force_publishing: if (not sem["bul_hide_xml"]) or force_publishing:
@ -324,7 +323,7 @@ def formsemestre_bulletinetud_published_dict(
def _list_modimpls( def _list_modimpls(
nt: NotesTableCompat, nt: NotesTableCompat,
etudid: int, etudid: int,
modimpls: list[ModuleImpl], modimpls: list[dict],
prefs: SemPreferences, prefs: SemPreferences,
version: str, version: str,
) -> list[dict]: ) -> list[dict]:
@ -333,6 +332,7 @@ def _list_modimpls(
mod_moy = scu.fmt_note(nt.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid)) mod_moy = scu.fmt_note(nt.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid))
if mod_moy == "NI": # ne mentionne pas les modules ou n'est pas inscrit if mod_moy == "NI": # ne mentionne pas les modules ou n'est pas inscrit
continue continue
modimpl_results = nt.modimpls_results.get(modimpl["moduleimpl_id"])
mod = modimpl["module"] mod = modimpl["module"]
# if mod['ects'] is None: # if mod['ects'] is None:
# ects = '' # ects = ''
@ -363,61 +363,42 @@ def _list_modimpls(
mod_dict["effectif"] = dict(value=nt.mod_rangs[modimpl["moduleimpl_id"]][1]) mod_dict["effectif"] = dict(value=nt.mod_rangs[modimpl["moduleimpl_id"]][1])
# --- notes de chaque eval: # --- notes de chaque eval:
evals = nt.get_evals_in_mod(modimpl["moduleimpl_id"]) evaluations_completes = nt.get_modimpl_evaluations_completes(
modimpl["moduleimpl_id"]
)
mod_dict["evaluation"] = [] mod_dict["evaluation"] = []
if version != "short": if version != "short":
for e in evals: for e in evaluations_completes:
if e["visibulletin"] or version == "long": if e.visibulletin or version == "long":
val = e["notes"].get(etudid, {"value": "NP"})["value"] # Note à l'évaluation:
val = modimpl_results.evals_notes[e.id].get(etudid, "NP")
# nb: val est NA si etud démissionnaire # nb: val est NA si etud démissionnaire
val = scu.fmt_note(val, note_max=e["note_max"]) e_dict = e.to_dict_bul()
eval_dict = dict( e_dict["note"] = scu.fmt_note(val, note_max=e.note_max)
jour=ndb.DateDMYtoISO(e["jour"], null_is_empty=True),
heure_debut=ndb.TimetoISO8601(
e["heure_debut"], null_is_empty=True
),
heure_fin=ndb.TimetoISO8601(e["heure_fin"], null_is_empty=True),
coefficient=e["coefficient"],
evaluation_type=e["evaluation_type"],
# CM : ajout pour permettre de faire le lien sur
# les bulletins en ligne avec l'évaluation:
evaluation_id=e["evaluation_id"],
description=quote_xml_attr(e["description"]),
note=val,
)
if prefs["bul_show_minmax_eval"] or prefs["bul_show_moypromo"]:
etat = sco_evaluations.do_evaluation_etat(e["evaluation_id"])
if prefs["bul_show_minmax_eval"]:
eval_dict["min"] = etat["mini"] # chaine, sur 20
eval_dict["max"] = etat["maxi"]
if prefs["bul_show_moypromo"]:
eval_dict["moy"] = etat["moy"]
mod_dict["evaluation"].append(eval_dict) if prefs["bul_show_minmax_eval"] or prefs["bul_show_moypromo"]:
# XXX à revoir pour utiliser modimplresult
etat = sco_evaluations.do_evaluation_etat(e.id)
if prefs["bul_show_minmax_eval"]:
e_dict["min"] = etat["mini"] # chaine, sur 20
e_dict["max"] = etat["maxi"]
if prefs["bul_show_moypromo"]:
e_dict["moy"] = etat["moy"]
mod_dict["evaluation"].append(e_dict)
# Evaluations incomplètes ou futures: # Evaluations incomplètes ou futures:
complete_eval_ids = set([e["evaluation_id"] for e in evals]) complete_eval_ids = {e.id for e in evaluations_completes}
if prefs["bul_show_all_evals"]: if prefs["bul_show_all_evals"]:
all_evals = sco_evaluation_db.do_evaluation_list( evaluations: list[Evaluation] = Evaluation.query.filter_by(
args={"moduleimpl_id": modimpl["moduleimpl_id"]} moduleimpl_id=modimpl["moduleimpl_id"]
) ).order_by(Evaluation.date_debut)
all_evals.reverse() # plus ancienne d'abord # plus ancienne d'abord
for e in all_evals: for e in evaluations:
if e["evaluation_id"] not in complete_eval_ids: if e.id not in complete_eval_ids:
mod_dict["evaluation"].append( e_dict = e.to_dict_bul()
dict( e_dict["incomplete"] = 1
jour=ndb.DateDMYtoISO(e["jour"], null_is_empty=True), mod_dict["evaluation"].append(e_dict)
heure_debut=ndb.TimetoISO8601(
e["heure_debut"], null_is_empty=True
),
heure_fin=ndb.TimetoISO8601(
e["heure_fin"], null_is_empty=True
),
coefficient=e["coefficient"],
description=quote_xml_attr(e["description"]),
incomplete="1",
)
)
modules_dict.append(mod_dict) modules_dict.append(mod_dict)
return modules_dict return modules_dict

View File

@ -46,6 +46,7 @@ from app.scodoc import sco_pdf
from app.scodoc.sco_pdf import SU from app.scodoc.sco_pdf import SU
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from flask import url_for, g
# Important: Le nom de la classe ne doit pas changer (bien le choisir), car il sera stocké en base de données (dans les préférences) # Important: Le nom de la classe ne doit pas changer (bien le choisir), car il sera stocké en base de données (dans les préférences)
@ -132,10 +133,13 @@ class BulletinGeneratorLegacy(sco_bulletins_generator.BulletinGenerator):
if sco_preferences.get_preference( if sco_preferences.get_preference(
"bul_show_minmax_mod", formsemestre_id "bul_show_minmax_mod", formsemestre_id
): ):
rang_minmax = '%s <span class="bul_minmax" title="[min, max] UE">[%s, %s]</span>' % ( rang_minmax = (
mod["mod_rang_txt"], '%s <span class="bul_minmax" title="[min, max] UE">[%s, %s]</span>'
scu.fmt_note(mod["stats"]["min"]), % (
scu.fmt_note(mod["stats"]["max"]), mod["mod_rang_txt"],
scu.fmt_note(mod["stats"]["min"]),
scu.fmt_note(mod["stats"]["max"]),
)
) )
else: else:
rang_minmax = mod["mod_rang_txt"] # vide si pas option rang rang_minmax = mod["mod_rang_txt"] # vide si pas option rang
@ -302,8 +306,8 @@ class BulletinGeneratorLegacy(sco_bulletins_generator.BulletinGenerator):
H = [] H = []
# --- Absences # --- Absences
H.append( H.append(
"""<p> f"""<p>
<a href="../Absences/CalAbs?etudid=%(etudid)s" class="bull_link"> <a href="{ url_for('assiduites.calendrier_etud', scodoc_dept=g.scodoc_dept, etudid=self.infos["etudid"]) }" class="bull_link">
<b>Absences :</b> %(nbabs)s demi-journées, dont %(nbabsjust)s justifiées <b>Absences :</b> %(nbabs)s demi-journées, dont %(nbabsjust)s justifiées
(pendant ce semestre). (pendant ce semestre).
</a></p> </a></p>

View File

@ -125,8 +125,8 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
story.append(Spacer(1, 2 * mm)) story.append(Spacer(1, 2 * mm))
if nbabs: if nbabs:
H.append( H.append(
"""<p class="bul_abs"> f"""<p class="bul_abs">
<a href="../Absences/CalAbs?etudid=%(etudid)s" class="bull_link"> <a href="{ url_for('assiduites.calendrier_etud', scodoc_dept=g.scodoc_dept, etudid=self.infos["etudid"]) }" class="bull_link">
<b>Absences :</b> %(nbabs)s demi-journées, dont %(nbabsjust)s justifiées <b>Absences :</b> %(nbabs)s demi-journées, dont %(nbabsjust)s justifiées
(pendant ce semestre). (pendant ce semestre).
</a></p> </a></p>

View File

@ -50,11 +50,11 @@ import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app import log from app import log
from app.but.bulletin_but_xml_compat import bulletin_but_xml_compat from app.but.bulletin_but_xml_compat import bulletin_but_xml_compat
from app.models.evaluations import Evaluation
from app.models.formsemestre import FormSemestre from app.models.formsemestre import FormSemestre
from app.scodoc import sco_assiduites from app.scodoc import sco_assiduites
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
from app.scodoc import sco_edit_ue from app.scodoc import sco_edit_ue
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_photos from app.scodoc import sco_photos
@ -242,6 +242,7 @@ def make_xml_formsemestre_bulletinetud(
# Liste les modules de l'UE # Liste les modules de l'UE
ue_modimpls = [mod for mod in modimpls if mod["module"]["ue_id"] == ue["ue_id"]] ue_modimpls = [mod for mod in modimpls if mod["module"]["ue_id"] == ue["ue_id"]]
for modimpl in ue_modimpls: for modimpl in ue_modimpls:
modimpl_results = nt.modimpls_results.get(modimpl["moduleimpl_id"])
mod_moy = scu.fmt_note( mod_moy = scu.fmt_note(
nt.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid) nt.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid)
) )
@ -290,57 +291,34 @@ def make_xml_formsemestre_bulletinetud(
) )
) )
# --- notes de chaque eval: # --- notes de chaque eval:
evals = nt.get_evals_in_mod(modimpl["moduleimpl_id"]) evaluations_completes = nt.get_modimpl_evaluations_completes(
modimpl["moduleimpl_id"]
)
if version != "short": if version != "short":
for e in evals: for e in evaluations_completes:
if e["visibulletin"] or version == "long": if e.visibulletin or version == "long":
x_eval = Element( # pour xml, tout convertir en chaines
"evaluation", e_dict = {k: str(v) for k, v in e.to_dict_bul().items()}
jour=ndb.DateDMYtoISO(e["jour"], null_is_empty=True), # notes envoyées sur 20, ceci juste pour garder trace:
heure_debut=ndb.TimetoISO8601( e_dict["note_max_origin"] = str(e.note_max)
e["heure_debut"], null_is_empty=True x_eval = Element("evaluation", **e_dict)
),
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"]),
)
x_mod.append(x_eval) x_mod.append(x_eval)
val = e["notes"].get(etudid, {"value": "NP"})[ # Note à l'évaluation:
"value" val = modimpl_results.evals_notes[e.id].get(etudid, "NP")
] # NA si etud demissionnaire val = scu.fmt_note(val, note_max=e.note_max)
val = scu.fmt_note(val, note_max=e["note_max"])
x_eval.append(Element("note", value=val)) x_eval.append(Element("note", value=val))
# Evaluations incomplètes ou futures: # Evaluations incomplètes ou futures:
complete_eval_ids = set([e["evaluation_id"] for e in evals]) complete_eval_ids = {e.id for e in evaluations_completes}
if sco_preferences.get_preference( if sco_preferences.get_preference(
"bul_show_all_evals", formsemestre_id "bul_show_all_evals", formsemestre_id
): ):
all_evals = sco_evaluation_db.do_evaluation_list( evaluations = Evaluation.query.filter_by(
args={"moduleimpl_id": modimpl["moduleimpl_id"]} moduleimpl_id=modimpl["moduleimpl_id"]
) ).order_by(Evaluation.date_debut)
all_evals.reverse() # plus ancienne d'abord for e in evaluations:
for e in all_evals: if e.id not in complete_eval_ids:
if e["evaluation_id"] not in complete_eval_ids: e_dict = e.to_dict_bul()
x_eval = Element( x_eval = Element("evaluation", **e_dict)
"evaluation",
jour=ndb.DateDMYtoISO(e["jour"], null_is_empty=True),
heure_debut=ndb.TimetoISO8601(
e["heure_debut"], null_is_empty=True
),
heure_fin=ndb.TimetoISO8601(
e["heure_fin"], null_is_empty=True
),
coefficient=str(e["coefficient"]),
description=quote_xml_attr(e["description"]),
incomplete="1",
# notes envoyées sur 20, ceci juste pour garder trace:
note_max_origin=str(e["note_max"] or ""),
)
x_mod.append(x_eval) x_mod.append(x_eval)
# UE capitalisee (listee seulement si meilleure que l'UE courante) # UE capitalisee (listee seulement si meilleure que l'UE courante)
if ue_status["is_capitalized"]: if ue_status["is_capitalized"]:

View File

@ -273,9 +273,7 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
if formsemestre_id is None: if formsemestre_id is None:
# clear all caches # clear all caches
log( log(f"--- invalidate_formsemestre: clearing all caches. pdfonly={pdfonly}---")
f"----- invalidate_formsemestre: clearing all caches. pdfonly={pdfonly}-----"
)
formsemestre_ids = [ formsemestre_ids = [
formsemestre.id formsemestre.id
for formsemestre in FormSemestre.query.filter_by(dept_id=g.scodoc_dept_id) for formsemestre in FormSemestre.query.filter_by(dept_id=g.scodoc_dept_id)
@ -285,7 +283,7 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
formsemestre_id formsemestre_id
] + sco_cursus.list_formsemestre_utilisateurs_uecap(formsemestre_id) ] + sco_cursus.list_formsemestre_utilisateurs_uecap(formsemestre_id)
log( log(
f"----- invalidate_formsemestre: clearing {formsemestre_ids}. pdfonly={pdfonly} -----" f"--- invalidate_formsemestre: clearing {formsemestre_ids}. pdfonly={pdfonly} ---"
) )
if not pdfonly: if not pdfonly:

511
app/scodoc/sco_cal.py Normal file
View 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&nbsp;",
"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 = (
"&nbsp;" * (n // 2) + legend + "&nbsp;" * ((n + 1) // 2)
)
else:
legend = "&nbsp;" # 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 = (
"&nbsp;" * (n // 2) + legend + "&nbsp;" * ((n + 1) // 2)
)
else:
legend = "&nbsp;&nbsp;&nbsp;" # 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)

View File

@ -1389,14 +1389,14 @@ def ue_sharing_code(ue_code: str = "", ue_id: int = None, hide_ue_id: int = None
# UE du même code, code formation et departement: # UE du même code, code formation et departement:
q_ues = ( q_ues = (
UniteEns.query.filter_by(ue_code=ue_code) UniteEns.query.filter_by(ue_code=ue_code)
.join(UniteEns.formation, aliased=True) .join(UniteEns.formation)
.filter_by(dept_id=g.scodoc_dept_id, formation_code=formation_code) .filter_by(dept_id=g.scodoc_dept_id, formation_code=formation_code)
) )
else: else:
# Toutes les UE du departement avec ce code: # Toutes les UE du departement avec ce code:
q_ues = ( q_ues = (
UniteEns.query.filter_by(ue_code=ue_code) UniteEns.query.filter_by(ue_code=ue_code)
.join(UniteEns.formation, aliased=True) .join(UniteEns.formation)
.filter_by(dept_id=g.scodoc_dept_id) .filter_by(dept_id=g.scodoc_dept_id)
) )

View File

@ -29,36 +29,18 @@
""" """
from flask import url_for, g from flask import url_for, g
from app import db
from app.models import Evaluation, FormSemestre, Identite
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_abs
from app.scodoc import sco_etud
from app.scodoc import sco_evaluations from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_db from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formsemestre from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl
# matin et/ou après-midi ?
def _eval_demijournee(E):
"1 si matin, 0 si apres midi, 2 si toute la journee"
am, pm = False, False
if E["heure_debut"] < "13:00":
am = True
if E["heure_fin"] > "13:00":
pm = True
if am and pm:
demijournee = 2
elif am:
demijournee = 1
else:
demijournee = 0
pm = True
return am, pm, demijournee
def evaluation_check_absences(evaluation_id): # XXX TODO-ASSIDUITE https://scodoc.org/git/ScoDoc/ScoDoc/issues/685
def evaluation_check_absences(evaluation: Evaluation):
"""Vérifie les absences au moment de cette évaluation. """Vérifie les absences au moment de cette évaluation.
Cas incohérents que l'on peut rencontrer pour chaque étudiant: Cas incohérents que l'on peut rencontrer pour chaque étudiant:
note et absent note et absent
@ -66,51 +48,60 @@ def evaluation_check_absences(evaluation_id):
ABS et absent justifié ABS et absent justifié
EXC et pas noté absent EXC et pas noté absent
EXC et pas justifie EXC et pas justifie
Ramene 3 listes d'etudid Ramene 5 listes d'etudid
""" """
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0] raise ScoValueError("Fonction non disponible, patience !") # XXX TODO-ASSIDUITE
if not E["jour"]:
if not evaluation.date_debut:
return [], [], [], [], [] # evaluation sans date return [], [], [], [], [] # evaluation sans date
am, pm, demijournee = _eval_demijournee(E) am, pm = evaluation.is_matin(), evaluation.is_apresmidi()
# Liste les absences à ce moment: # Liste les absences à ce moment:
A = sco_abs.list_abs_jour(ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm) absences = sco_abs.list_abs_jour(evaluation.date_debut, am=am, pm=pm)
As = set([x["etudid"] for x in A]) # ensemble des etudiants absents abs_etudids = set([x["etudid"] for x in absences]) # ensemble des etudiants absents
NJ = sco_abs.list_abs_non_just_jour(ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm) abs_non_just = sco_abs.list_abs_non_just_jour(
NJs = set([x["etudid"] for x in NJ]) # ensemble des etudiants absents non justifies evaluation.date_debut.date(), am=am, pm=pm
Just = sco_abs.list_abs_jour(
ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm, is_abs=None, is_just=True
) )
Justs = set([x["etudid"] for x in Just]) # ensemble des etudiants avec justif abs_nj_etudids = set(
[x["etudid"] for x in abs_non_just]
) # ensemble des etudiants absents non justifies
justifs = sco_abs.list_abs_jour(
evaluation.date_debut.date(), am=am, pm=pm, is_abs=None, is_just=True
)
just_etudids = set(
[x["etudid"] for x in justifs]
) # ensemble des etudiants avec justif
# Les notes: # Les notes:
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id) notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation.id)
ValButAbs = [] # une note mais noté absent ValButAbs = [] # une note mais noté absent
AbsNonSignalee = [] # note ABS mais pas noté absent AbsNonSignalee = [] # note ABS mais pas noté absent
ExcNonSignalee = [] # note EXC mais pas noté absent ExcNonSignalee = [] # note EXC mais pas noté absent
ExcNonJust = [] # note EXC mais absent non justifie ExcNonJust = [] # note EXC mais absent non justifie
AbsButExc = [] # note ABS mais justifié AbsButExc = [] # note ABS mais justifié
for etudid, _ in sco_groups.do_evaluation_listeetuds_groups( for etudid, _ in sco_groups.do_evaluation_listeetuds_groups(
evaluation_id, getallstudents=True evaluation.id, getallstudents=True
): ):
if etudid in notes_db: if etudid in notes_db:
val = notes_db[etudid]["value"] val = notes_db[etudid]["value"]
if ( if (
val != None and val != scu.NOTES_NEUTRALISE and val != scu.NOTES_ATTENTE val is not None
) and etudid in As: and val != scu.NOTES_NEUTRALISE
and val != scu.NOTES_ATTENTE
) and etudid in abs_etudids:
# note valide et absent # note valide et absent
ValButAbs.append(etudid) ValButAbs.append(etudid)
if val is None and not etudid in As: if val is None and not etudid in abs_etudids:
# absent mais pas signale comme tel # absent mais pas signale comme tel
AbsNonSignalee.append(etudid) AbsNonSignalee.append(etudid)
if val == scu.NOTES_NEUTRALISE and not etudid in As: if val == scu.NOTES_NEUTRALISE and not etudid in abs_etudids:
# Neutralisé mais pas signale absent # Neutralisé mais pas signale absent
ExcNonSignalee.append(etudid) ExcNonSignalee.append(etudid)
if val == scu.NOTES_NEUTRALISE and etudid in NJs: if val == scu.NOTES_NEUTRALISE and etudid in abs_nj_etudids:
# EXC mais pas justifié # EXC mais pas justifié
ExcNonJust.append(etudid) ExcNonJust.append(etudid)
if val is None and etudid in Justs: if val is None and etudid in just_etudids:
# ABS mais justificatif # ABS mais justificatif
AbsButExc.append(etudid) AbsButExc.append(etudid)
@ -119,9 +110,16 @@ def evaluation_check_absences(evaluation_id):
def evaluation_check_absences_html(evaluation_id, with_header=True, show_ok=True): def evaluation_check_absences_html(evaluation_id, with_header=True, show_ok=True):
"""Affiche état vérification absences d'une évaluation""" """Affiche état vérification absences d'une évaluation"""
evaluation: Evaluation = db.session.get(Evaluation, evaluation_id)
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0] am, pm = evaluation.is_matin(), evaluation.is_apresmidi()
am, pm, demijournee = _eval_demijournee(E) # 1 si matin, 0 si apres midi, 2 si toute la journee:
match am, pm:
case False, True:
demijournee = 0
case True, False:
demijournee = 1
case _:
demijournee = 2
( (
ValButAbs, ValButAbs,
@ -129,19 +127,23 @@ def evaluation_check_absences_html(evaluation_id, with_header=True, show_ok=True
ExcNonSignalee, ExcNonSignalee,
ExcNonJust, ExcNonJust,
AbsButExc, AbsButExc,
) = evaluation_check_absences(evaluation_id) ) = evaluation_check_absences(evaluation)
if with_header: if with_header:
H = [ H = [
html_sco_header.html_sem_header("Vérification absences à l'évaluation"), html_sco_header.html_sem_header("Vérification absences à l'évaluation"),
sco_evaluations.evaluation_describe(evaluation_id=evaluation_id), sco_evaluations.evaluation_describe(evaluation_id=evaluation.id),
"""<p class="help">Vérification de la cohérence entre les notes saisies et les absences signalées.</p>""", """<p class="help">Vérification de la cohérence entre les notes saisies
et les absences signalées.</p>""",
] ]
else: else:
# pas de header, mais un titre # pas de header, mais un titre
H = [ H = [
"""<h2 class="eval_check_absences">%s du %s """ f"""<h2 class="eval_check_absences">{
% (E["description"], E["jour"]) evaluation.description or "évaluation"
} du {
evaluation.date_debut.strftime("%d/%m/%Y") if evaluation.date_debut else ""
} """
] ]
if ( if (
not ValButAbs not ValButAbs
@ -157,26 +159,27 @@ def evaluation_check_absences_html(evaluation_id, with_header=True, show_ok=True
if not etudids and show_ok: if not etudids and show_ok:
H.append("<li>aucun</li>") H.append("<li>aucun</li>")
for etudid in etudids: for etudid in etudids:
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] etud: Identite = db.session.get(Identite, etudid)
H.append( H.append(
'<li><a class="discretelink" href="%s">' f"""<li><a class="discretelink" href="{
% url_for( url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"] "scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid
) )
+ "%(nomprenom)s</a>" % etud }">{etud.nomprenom}</a>"""
) )
if linkabs: if linkabs:
H.append( url = url_for(
f"""<a class="stdlink" href="{url_for( "absences.doSignaleAbsence", # XXX TODO-ASSIDUITE
'absences.doSignaleAbsence',
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
etudid=etud["etudid"], etudid=etudid,
datedebut=E["jour"], # par defaut signale le jour du début de l'éval
datefin=E["jour"], datedebut=evaluation.date_debut.strftime("%d/%m/%Y"),
datefin=evaluation.date_debut.strftime("%d/%m/%Y"),
demijournee=demijournee, demijournee=demijournee,
moduleimpl_id=E["moduleimpl_id"], moduleimpl_id=evaluation.moduleimpl_id,
) )
}">signaler cette absence</a>""" H.append(
f"""<a class="stdlink" href="{url}">signaler cette absence</a>"""
) )
H.append("</li>") H.append("</li>")
H.append("</ul>") H.append("</ul>")
@ -218,7 +221,9 @@ def evaluation_check_absences_html(evaluation_id, with_header=True, show_ok=True
def formsemestre_check_absences_html(formsemestre_id): def formsemestre_check_absences_html(formsemestre_id):
"""Affiche etat verification absences pour toutes les evaluations du semestre !""" """Affiche etat verification absences pour toutes les evaluations du semestre !"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id) formsemestre: FormSemestre = FormSemestre.query.filter_by(
dept_id=g.scodoc_dept_id, id=formsemestre_id
).first_or_404()
H = [ H = [
html_sco_header.html_sem_header( html_sco_header.html_sem_header(
"Vérification absences aux évaluations de ce semestre", "Vérification absences aux évaluations de ce semestre",
@ -229,29 +234,27 @@ def formsemestre_check_absences_html(formsemestre_id):
</p>""", </p>""",
] ]
# Modules, dans l'ordre # Modules, dans l'ordre
Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id) for modimpl in formsemestre.modimpls_sorted:
for M in Mlist: if modimpl.evaluations.count() > 0:
evals = sco_evaluation_db.do_evaluation_list(
{"moduleimpl_id": M["moduleimpl_id"]}
)
if evals:
H.append( H.append(
'<div class="module_check_absences"><h2><a href="moduleimpl_status?moduleimpl_id=%s">%s: %s</a></h2>' f"""<div class="module_check_absences">
% ( <h2><a href="{
M["moduleimpl_id"], url_for("notes.moduleimpl_status",
M["module"]["code"] or "", scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
M["module"]["abbrev"] or "", }">{modimpl.module.code or ""}: {modimpl.module.abbrev or ""}</a>
) </h2>"""
) )
for E in evals: for evaluation in modimpl.evaluations.order_by(
H.append( Evaluation.numero, Evaluation.date_debut
evaluation_check_absences_html( ):
E["evaluation_id"], H.append(
with_header=False, evaluation_check_absences_html(
show_ok=False, evaluation.id, # XXX TODO-ASSIDUITE remplacer par evaluation ...
with_header=False,
show_ok=False,
)
) )
)
if evals:
H.append("</div>") H.append("</div>")
H.append(html_sco_header.sco_footer()) H.append(html_sco_header.sco_footer())
return "\n".join(H) return "\n".join(H)

View File

@ -28,23 +28,20 @@
"""Gestion évaluations (ScoDoc7, code en voie de modernisation) """Gestion évaluations (ScoDoc7, code en voie de modernisation)
""" """
import pprint
import flask import flask
from flask import url_for, g from flask import url_for, g
from flask_login import current_user from flask_login import current_user
from app import db, log from app import db, log
from app.models import Evaluation, ModuleImpl, ScolarNews from app.models import Evaluation
from app.models.evaluations import evaluation_enrich_dict, check_evaluation_args from app.models.evaluations import check_convert_evaluation_args
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError from app.scodoc.sco_exceptions import AccessDenied
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
from app.scodoc import sco_permissions_check
_evaluationEditor = ndb.EditableTable( _evaluationEditor = ndb.EditableTable(
@ -53,9 +50,8 @@ _evaluationEditor = ndb.EditableTable(
( (
"evaluation_id", "evaluation_id",
"moduleimpl_id", "moduleimpl_id",
"jour", "date_debut",
"heure_debut", "date_fin",
"heure_fin",
"description", "description",
"note_max", "note_max",
"coefficient", "coefficient",
@ -64,15 +60,11 @@ _evaluationEditor = ndb.EditableTable(
"evaluation_type", "evaluation_type",
"numero", "numero",
), ),
sortkey="numero desc, jour desc, heure_debut desc", # plus recente d'abord sortkey="numero, date_debut desc", # plus recente d'abord
output_formators={ output_formators={
"jour": ndb.DateISOtoDMY,
"numero": ndb.int_null_is_zero, "numero": ndb.int_null_is_zero,
}, },
input_formators={ input_formators={
"jour": ndb.DateDMYtoISO,
"heure_debut": ndb.TimetoISO8601, # converti par evaluation_enrich_dict
"heure_fin": ndb.TimetoISO8601, # converti par evaluation_enrich_dict
"visibulletin": bool, "visibulletin": bool,
"publish_incomplete": bool, "publish_incomplete": bool,
"evaluation_type": int, "evaluation_type": int,
@ -80,8 +72,9 @@ _evaluationEditor = ndb.EditableTable(
) )
def do_evaluation_list(args, sortkey=None): def get_evaluation_dict(args: dict) -> list[dict]:
"""List evaluations, sorted by numero (or most recent date first). """Liste evaluations, triées numero (or most recent date first).
Fonction de transition pour ancien code ScoDoc7.
Ajoute les champs: Ajoute les champs:
'duree' : '2h30' 'duree' : '2h30'
@ -89,14 +82,8 @@ def do_evaluation_list(args, sortkey=None):
'apresmidi' : 1 (termine après 12:00) ou 0 'apresmidi' : 1 (termine après 12:00) ou 0
'descrheure' : ' de 15h00 à 16h30' 'descrheure' : ' de 15h00 à 16h30'
""" """
# Attention: transformation fonction ScoDoc7 en SQLAlchemy
cnx = ndb.GetDBConnexion()
evals = _evaluationEditor.list(cnx, args, sortkey=sortkey)
# calcule duree (chaine de car.) de chaque evaluation et ajoute jour_iso, matin, apresmidi # calcule duree (chaine de car.) de chaque evaluation et ajoute jour_iso, matin, apresmidi
for e in evals: return [e.to_dict() for e in Evaluation.query.filter_by(**args)]
evaluation_enrich_dict(e)
return evals
def do_evaluation_list_in_formsemestre(formsemestre_id): def do_evaluation_list_in_formsemestre(formsemestre_id):
@ -104,147 +91,29 @@ def do_evaluation_list_in_formsemestre(formsemestre_id):
mods = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id) mods = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
evals = [] evals = []
for modimpl in mods: for modimpl in mods:
evals += do_evaluation_list(args={"moduleimpl_id": modimpl["moduleimpl_id"]}) evals += get_evaluation_dict(args={"moduleimpl_id": modimpl["moduleimpl_id"]})
return evals return evals
def do_evaluation_create(
moduleimpl_id=None,
jour=None,
heure_debut=None,
heure_fin=None,
description=None,
note_max=None,
coefficient=None,
visibulletin=None,
publish_incomplete=None,
evaluation_type=None,
numero=None,
**kw, # ceci pour absorber les arguments excedentaires de tf #sco8
):
"""Create an evaluation"""
if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id):
raise AccessDenied(
f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
)
args = locals()
log("do_evaluation_create: args=" + str(args))
modimpl: ModuleImpl = db.session.get(ModuleImpl, moduleimpl_id)
if modimpl is None:
raise ValueError("module not found")
check_evaluation_args(args)
# Check numeros
moduleimpl_evaluation_renumber(moduleimpl_id, only_if_unumbered=True)
if not "numero" in args or args["numero"] is None:
n = None
# determine le numero avec la date
# Liste des eval existantes triees par date, la plus ancienne en tete
mod_evals = do_evaluation_list(
args={"moduleimpl_id": moduleimpl_id},
sortkey="jour asc, heure_debut asc",
)
if args["jour"]:
next_eval = None
t = (
ndb.DateDMYtoISO(args["jour"], null_is_empty=True),
ndb.TimetoISO8601(args["heure_debut"], null_is_empty=True),
)
for e in mod_evals:
if (
ndb.DateDMYtoISO(e["jour"], null_is_empty=True),
ndb.TimetoISO8601(e["heure_debut"], null_is_empty=True),
) > t:
next_eval = e
break
if next_eval:
n = moduleimpl_evaluation_insert_before(mod_evals, next_eval)
else:
n = None # a placer en fin
if n is None: # pas de date ou en fin:
if mod_evals:
log(pprint.pformat(mod_evals[-1]))
n = mod_evals[-1]["numero"] + 1
else:
n = 0 # the only one
# log("creating with numero n=%d" % n)
args["numero"] = n
#
cnx = ndb.GetDBConnexion()
r = _evaluationEditor.create(cnx, args)
# news
sco_cache.invalidate_formsemestre(formsemestre_id=modimpl.formsemestre_id)
url = url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=moduleimpl_id,
)
ScolarNews.add(
typ=ScolarNews.NEWS_NOTE,
obj=moduleimpl_id,
text=f"""Création d'une évaluation dans <a href="{url}">{
modimpl.module.titre or '(module sans titre)'}</a>""",
url=url,
)
return r
def do_evaluation_edit(args): def do_evaluation_edit(args):
"edit an evaluation" "edit an evaluation"
evaluation_id = args["evaluation_id"] evaluation_id = args["evaluation_id"]
the_evals = do_evaluation_list({"evaluation_id": evaluation_id}) evaluation: Evaluation = db.session.get(Evaluation, evaluation_id)
if not the_evals: if evaluation is None:
raise ValueError("evaluation inexistante !") raise ValueError("evaluation inexistante !")
moduleimpl_id = the_evals[0]["moduleimpl_id"]
if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id): if not evaluation.moduleimpl.can_edit_evaluation(current_user):
raise AccessDenied( raise AccessDenied(
"Modification évaluation impossible pour %s" % current_user.get_nomplogin() f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
) )
args["moduleimpl_id"] = moduleimpl_id args["moduleimpl_id"] = evaluation.moduleimpl.id
check_evaluation_args(args) check_convert_evaluation_args(evaluation.moduleimpl, args)
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
_evaluationEditor.edit(cnx, args) _evaluationEditor.edit(cnx, args)
# inval cache pour ce semestre # inval cache pour ce semestre
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] sco_cache.invalidate_formsemestre(
sco_cache.invalidate_formsemestre(formsemestre_id=M["formsemestre_id"]) formsemestre_id=evaluation.moduleimpl.formsemestre_id
def do_evaluation_delete(evaluation_id):
"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):
raise AccessDenied(
f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
)
notes_db = do_evaluation_get_all_notes(evaluation_id) # { etudid : value }
notes = [x["value"] for x in notes_db.values()]
if notes:
raise ScoValueError(
"Impossible de supprimer cette évaluation: il reste des notes"
)
db.session.delete(evaluation)
db.session.commit()
# inval cache pour ce semestre
sco_cache.invalidate_formsemestre(formsemestre_id=modimpl.formsemestre_id)
# news
url = url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=modimpl.id,
)
ScolarNews.add(
typ=ScolarNews.NEWS_NOTE,
obj=modimpl.id,
text=f"""Suppression d'une évaluation dans <a href="{
url
}">{modimpl.module.titre}</a>""",
url=url,
) )
@ -287,68 +156,6 @@ def do_evaluation_get_all_notes(
return d return d
def moduleimpl_evaluation_renumber(moduleimpl_id, only_if_unumbered=False, redirect=0):
"""Renumber evaluations in this module, according to their date. (numero=0: oldest one)
Needed because previous versions of ScoDoc did not have eval numeros
Note: existing numeros are ignored
"""
redirect = int(redirect)
# log('moduleimpl_evaluation_renumber( moduleimpl_id=%s )' % moduleimpl_id )
# List sorted according to date/heure, ignoring numeros:
# (note that we place evaluations with NULL date at the end)
mod_evals = do_evaluation_list(
args={"moduleimpl_id": moduleimpl_id},
sortkey="jour asc, heure_debut asc",
)
all_numbered = False not in [x["numero"] > 0 for x in mod_evals]
if all_numbered and only_if_unumbered:
return # all ok
# Reset all numeros:
i = 1
for e in mod_evals:
e["numero"] = i
do_evaluation_edit(e)
i += 1
# If requested, redirect to moduleimpl page:
if redirect:
return flask.redirect(
url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=moduleimpl_id,
)
)
def moduleimpl_evaluation_insert_before(mod_evals, next_eval):
"""Renumber evals such that an evaluation with can be inserted before next_eval
Returns numero suitable for the inserted evaluation
"""
if next_eval:
n = next_eval["numero"]
if not n:
log("renumbering old evals")
moduleimpl_evaluation_renumber(next_eval["moduleimpl_id"])
next_eval = do_evaluation_list(
args={"evaluation_id": next_eval["evaluation_id"]}
)[0]
n = next_eval["numero"]
else:
n = 1
# log('inserting at position numero %s' % n )
# all numeros >= n are incremented
for e in mod_evals:
if e["numero"] >= n:
e["numero"] += 1
# log('incrementing %s to %s' % (e['evaluation_id'], e['numero']))
do_evaluation_edit(e)
return n
def moduleimpl_evaluation_move(evaluation_id: int, after=0, redirect=1): def moduleimpl_evaluation_move(evaluation_id: int, after=0, redirect=1):
"""Move before/after previous one (decrement/increment numero) """Move before/after previous one (decrement/increment numero)
(published) (published)
@ -357,18 +164,19 @@ def moduleimpl_evaluation_move(evaluation_id: int, after=0, redirect=1):
moduleimpl_id = evaluation.moduleimpl_id moduleimpl_id = evaluation.moduleimpl_id
redirect = int(redirect) redirect = int(redirect)
# access: can change eval ? # access: can change eval ?
if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id): if not evaluation.moduleimpl.can_edit_evaluation(current_user):
raise AccessDenied( raise AccessDenied(
f"Modification évaluation impossible pour {current_user.get_nomplogin()}" f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
) )
Evaluation.moduleimpl_evaluation_renumber(
moduleimpl_evaluation_renumber(moduleimpl_id, only_if_unumbered=True) evaluation.moduleimpl, only_if_unumbered=True
e = do_evaluation_list(args={"evaluation_id": evaluation_id})[0] )
e = get_evaluation_dict(args={"evaluation_id": evaluation_id})[0]
after = int(after) # 0: deplace avant, 1 deplace apres after = int(after) # 0: deplace avant, 1 deplace apres
if after not in (0, 1): if after not in (0, 1):
raise ValueError('invalid value for "after"') raise ValueError('invalid value for "after"')
mod_evals = do_evaluation_list({"moduleimpl_id": e["moduleimpl_id"]}) mod_evals = get_evaluation_dict({"moduleimpl_id": e["moduleimpl_id"]})
if len(mod_evals) > 1: if len(mod_evals) > 1:
idx = [p["evaluation_id"] for p in mod_evals].index(evaluation_id) idx = [p["evaluation_id"] for p in mod_evals].index(evaluation_id)
neigh = None # object to swap with neigh = None # object to swap with
@ -379,8 +187,8 @@ def moduleimpl_evaluation_move(evaluation_id: int, after=0, redirect=1):
if neigh: # if neigh: #
if neigh["numero"] == e["numero"]: if neigh["numero"] == e["numero"]:
log("Warning: moduleimpl_evaluation_move: forcing renumber") log("Warning: moduleimpl_evaluation_move: forcing renumber")
moduleimpl_evaluation_renumber( Evaluation.moduleimpl_evaluation_renumber(
e["moduleimpl_id"], only_if_unumbered=False evaluation.moduleimpl, only_if_unumbered=False
) )
else: else:
# swap numero with neighbor # swap numero with neighbor

View File

@ -27,7 +27,7 @@
"""Formulaire ajout/édition d'une évaluation """Formulaire ajout/édition d'une évaluation
""" """
import datetime
import time import time
import flask import flask
@ -38,6 +38,7 @@ from flask import request
from app import db from app import db
from app.models import Evaluation, FormSemestre, ModuleImpl from app.models import Evaluation, FormSemestre, ModuleImpl
from app.models.evaluations import heure_to_time
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType from app.scodoc.sco_utils import ModuleType
@ -47,7 +48,6 @@ from app.scodoc import html_sco_header
from app.scodoc import sco_evaluations from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_db from app.scodoc import sco_evaluation_db
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
from app.scodoc import sco_permissions_check
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
@ -83,7 +83,7 @@ def evaluation_create_form(
can_edit_poids = not preferences["but_disable_edit_poids_evaluations"] can_edit_poids = not preferences["but_disable_edit_poids_evaluations"]
min_note_max = scu.NOTES_PRECISION # le plus petit bareme possible min_note_max = scu.NOTES_PRECISION # le plus petit bareme possible
# #
if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id): if not modimpl.can_edit_evaluation(current_user):
return f""" return f"""
{html_sco_header.sco_header()} {html_sco_header.sco_header()}
<h2>Opération non autorisée</h2> <h2>Opération non autorisée</h2>
@ -139,15 +139,8 @@ def evaluation_create_form(
heures = [f"{h:02d}h{m:02d}" for h in range(8, 19) for m in (0, 30)] heures = [f"{h:02d}h{m:02d}" for h in range(8, 19) for m in (0, 30)]
# #
initvalues["visibulletin"] = initvalues.get("visibulletin", True)
if initvalues["visibulletin"]:
initvalues["visibulletinlist"] = ["X"]
else:
initvalues["visibulletinlist"] = []
initvalues["coefficient"] = initvalues.get("coefficient", 1.0) initvalues["coefficient"] = initvalues.get("coefficient", 1.0)
vals = scu.get_request_args() vals = scu.get_request_args()
if vals.get("tf_submitted", False) and "visibulletinlist" not in vals:
vals["visibulletinlist"] = []
# #
ue_coef_dict = modimpl.module.get_ue_coef_dict() ue_coef_dict = modimpl.module.get_ue_coef_dict()
if is_apc: # BUT: poids vers les UE if is_apc: # BUT: poids vers les UE
@ -236,11 +229,9 @@ def evaluation_create_form(
}, },
), ),
( (
"visibulletinlist", "visibulletin",
{ {
"input_type": "checkbox", "input_type": "boolcheckbox",
"allowed_values": ["X"],
"labels": [""],
"title": "Visible sur bulletins", "title": "Visible sur bulletins",
"explanation": "(pour les bulletins en version intermédiaire)", "explanation": "(pour les bulletins en version intermédiaire)",
}, },
@ -349,20 +340,46 @@ def evaluation_create_form(
return flask.redirect(dest_url) return flask.redirect(dest_url)
else: else:
# form submission # form submission
if tf[2]["visibulletinlist"]: args = tf[2]
tf[2]["visibulletin"] = True # modifie le codage des dates
# (nb: ce formulaire ne permet de créer que des évaluation sur la même journée)
if args.get("jour"):
try:
date_debut = datetime.datetime.strptime(args["jour"], "%d/%m/%Y")
except ValueError as exc:
raise ScoValueError("Date (j/m/a) invalide") from exc
else: else:
tf[2]["visibulletin"] = False date_debut = None
args.pop("jour", None)
if date_debut and args.get("heure_debut"):
try:
heure_debut = heure_to_time(args["heure_debut"])
except ValueError as exc:
raise ScoValueError("Heure début invalide") from exc
args["date_debut"] = datetime.datetime.combine(date_debut, heure_debut)
args.pop("heure_debut", None)
# note: ce formulaire ne permet de créer que des évaluation avec debut et fin sur le même jour.
if date_debut and args.get("heure_fin"):
try:
heure_fin = heure_to_time(args["heure_fin"])
except ValueError as exc:
raise ScoValueError("Heure fin invalide") from exc
args["date_fin"] = datetime.datetime.combine(date_debut, heure_fin)
args.pop("heure_fin", None)
#
if edit: if edit:
sco_evaluation_db.do_evaluation_edit(tf[2]) evaluation.from_dict(args)
else: else:
# création d'une evaluation (via fonction ScoDoc7) # création d'une evaluation
evaluation_id = sco_evaluation_db.do_evaluation_create(**tf[2]) evaluation = Evaluation.create(moduleimpl=modimpl, **args)
db.session.add(evaluation)
db.session.commit()
evaluation_id = evaluation.id
if is_apc: if is_apc:
# Set poids # Set poids
evaluation = db.session.get(Evaluation, evaluation_id) evaluation = db.session.get(Evaluation, evaluation_id)
for ue in sem_ues: for ue in sem_ues:
evaluation.set_ue_poids(ue, tf[2][f"poids_{ue.id}"]) evaluation.set_ue_poids(ue, tf[2][f"poids_{ue.id}"])
db.session.add(evaluation) db.session.add(evaluation)
db.session.commit() db.session.commit()
return flask.redirect(dest_url) return flask.redirect(dest_url)

View File

@ -126,8 +126,8 @@ def evaluations_recap_table(formsemestre: FormSemestre) -> list[dict]:
evaluation_id=evaluation_id, evaluation_id=evaluation_id,
), ),
"_titre_target_attrs": 'class="discretelink"', "_titre_target_attrs": 'class="discretelink"',
"date": e.jour.strftime("%d/%m/%Y") if e.jour else "", "date": e.date_debut.strftime("%d/%m/%Y") if e.date_debut else "",
"_date_order": e.jour.isoformat() if e.jour else "", "_date_order": e.date_debut.isoformat() if e.date_debut else "",
"complete": "oui" if eval_etat.is_complete else "non", "complete": "oui" if eval_etat.is_complete else "non",
"_complete_target": "#", "_complete_target": "#",
"_complete_target_attrs": 'class="bull_link" title="prise en compte dans les moyennes"' "_complete_target_attrs": 'class="bull_link" title="prise en compte dans les moyennes"'

View File

@ -30,24 +30,25 @@
import collections import collections
import datetime import datetime
import operator import operator
import time
from flask import url_for from flask import url_for
from flask import g from flask import g
from flask_login import current_user from flask_login import current_user
from flask import request from flask import request
from app import db
from app.auth.models import User
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre from app.models import Evaluation, FormSemestre
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType from app.scodoc.sco_utils import ModuleType
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_cal
from app.scodoc import sco_evaluation_db from app.scodoc import sco_evaluation_db
from app.scodoc import sco_abs
from app.scodoc import sco_edit_module from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue from app.scodoc import sco_edit_ue
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
@ -101,6 +102,14 @@ def do_evaluation_etat(
) -> dict: ) -> dict:
"""Donne infos sur l'état de l'évaluation. """Donne infos sur l'état de l'évaluation.
Ancienne fonction, lente: préférer ModuleImplResults pour tout calcul. Ancienne fonction, lente: préférer ModuleImplResults pour tout calcul.
XXX utilisée par de très nombreuses fonctions, dont
- _eval_etat<do_evaluation_etat_in_sem (en cours de remplacement)
- _eval_etat<do_evaluation_etat_in_mod<formsemestre_tableau_modules
qui a seulement besoin de
nb_evals_completes, nb_evals_en_cours, nb_evals_vides, attente
renvoie:
{ {
nb_inscrits : inscrits au module nb_inscrits : inscrits au module
nb_notes nb_notes
@ -124,7 +133,7 @@ def do_evaluation_etat(
) # { etudid : note } ) # { etudid : note }
# ---- Liste des groupes complets et incomplets # ---- Liste des groupes complets et incomplets
E = sco_evaluation_db.do_evaluation_list(args={"evaluation_id": evaluation_id})[0] E = sco_evaluation_db.get_evaluation_dict(args={"evaluation_id": evaluation_id})[0]
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
is_malus = Mod["module_type"] == ModuleType.MALUS # True si module de malus is_malus = Mod["module_type"] == ModuleType.MALUS # True si module de malus
@ -275,7 +284,8 @@ def do_evaluation_etat(
def do_evaluation_list_in_sem(formsemestre_id, with_etat=True): def do_evaluation_list_in_sem(formsemestre_id, with_etat=True):
"""Liste les evaluations de tous les modules de ce semestre. """Liste les évaluations de tous les modules de ce semestre.
Triée par module, numero desc, date_debut desc
Donne pour chaque eval son état (voir do_evaluation_etat) Donne pour chaque eval son état (voir do_evaluation_etat)
{ evaluation_id,nb_inscrits, nb_notes, nb_abs, nb_neutre, moy, median, last_modif ... } { evaluation_id,nb_inscrits, nb_notes, nb_abs, nb_neutre, moy, median, last_modif ... }
@ -315,7 +325,7 @@ def do_evaluation_list_in_sem(formsemestre_id, with_etat=True):
'evaluation_type': 0, 'evaluation_type': 0,
'heure_debut': datetime.time(8, 0), 'heure_debut': datetime.time(8, 0),
'heure_fin': datetime.time(9, 30), 'heure_fin': datetime.time(9, 30),
'jour': datetime.date(2015, 11, 3), // vide => 1/1/1 'jour': datetime.date(2015, 11, 3), // vide => 1/1/1900
'moduleimpl_id': 'GEAMIP80490', 'moduleimpl_id': 'GEAMIP80490',
'note_max': 20.0, 'note_max': 20.0,
'numero': 0, 'numero': 0,
@ -327,7 +337,7 @@ def do_evaluation_list_in_sem(formsemestre_id, with_etat=True):
FROM notes_evaluation E, notes_moduleimpl MI FROM notes_evaluation E, notes_moduleimpl MI
WHERE MI.formsemestre_id = %(formsemestre_id)s WHERE MI.formsemestre_id = %(formsemestre_id)s
and MI.id = E.moduleimpl_id and MI.id = E.moduleimpl_id
ORDER BY MI.id, numero desc, jour desc, heure_debut DESC ORDER BY MI.id, numero desc, date_debut desc
""" """
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
@ -335,9 +345,9 @@ def do_evaluation_list_in_sem(formsemestre_id, with_etat=True):
res = cursor.dictfetchall() res = cursor.dictfetchall()
# etat de chaque evaluation: # etat de chaque evaluation:
for r in res: for r in res:
r["jour"] = r["jour"] or datetime.date(1900, 1, 1) # pour les comparaisons
if with_etat: if with_etat:
r["etat"] = do_evaluation_etat(r["evaluation_id"]) r["etat"] = do_evaluation_etat(r["evaluation_id"])
r["jour"] = r["date_debut"] or datetime.date(1900, 1, 1)
return res return res
@ -379,7 +389,20 @@ def _eval_etat(evals):
def do_evaluation_etat_in_sem(formsemestre_id): def do_evaluation_etat_in_sem(formsemestre_id):
"""-> nb_eval_completes, nb_evals_en_cours, nb_evals_vides, """-> nb_eval_completes, nb_evals_en_cours, nb_evals_vides,
date derniere modif, attente""" date derniere modif, attente
XXX utilisé par
- formsemestre_status_head
- gen_formsemestre_recapcomplet_xml
- gen_formsemestre_recapcomplet_json
"nb_evals_completes"
"nb_evals_en_cours"
"nb_evals_vides"
"date_derniere_note"
"last_modif"
"attente"
"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
evals = nt.get_evaluations_etats() evals = nt.get_evaluations_etats()
@ -403,88 +426,97 @@ def formsemestre_evaluations_cal(formsemestre_id):
formsemestre = FormSemestre.get_formsemestre(formsemestre_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
evals = nt.get_evaluations_etats() evaluations = formsemestre.get_evaluations() # TODO
nb_evals = len(evals) nb_evals = len(evaluations)
color_incomplete = "#FF6060" color_incomplete = "#FF6060"
color_complete = "#A0FFA0" color_complete = "#A0FFA0"
color_futur = "#70E0FF" color_futur = "#70E0FF"
today = time.strftime("%Y-%m-%d") year = formsemestre.annee_scolaire()
year = formsemestre.date_debut.year
if formsemestre.date_debut.month < 8:
year -= 1 # calendrier septembre a septembre
events = {} # (day, halfday) : event events = {} # (day, halfday) : event
for e in evals: for e in evaluations:
etat = e["etat"] if e.date_debut is None:
if not e["jour"]: continue # éval. sans date
continue txt = e.moduleimpl.module.code or e.moduleimpl.module.abbrev or "éval."
day = e["jour"].strftime("%Y-%m-%d") if e.date_debut == e.date_fin:
mod = sco_moduleimpl.moduleimpl_withmodule_list( heure_debut_txt, heure_fin_txt = "?", "?"
moduleimpl_id=e["moduleimpl_id"]
)[0]
txt = mod["module"]["code"] or mod["module"]["abbrev"] or "eval"
if e["heure_debut"]:
debut = e["heure_debut"].strftime("%Hh%M")
else: else:
debut = "?" heure_debut_txt = e.date_debut.strftime("%Hh%M") if e.date_debut else "?"
if e["heure_fin"]: heure_fin_txt = e.date_fin.strftime("%Hh%M") if e.date_fin else "?"
fin = e["heure_fin"].strftime("%Hh%M")
else: description = f"""{
fin = "?" e.moduleimpl.module.titre
description = "%s, de %s à %s" % (mod["module"]["titre"], debut, fin) }, de {heure_debut_txt} à {heure_fin_txt}"""
if etat["evalcomplete"]:
# Etat (notes completes) de l'évaluation:
modimpl_result = nt.modimpls_results[e.moduleimpl.id]
if modimpl_result.evaluations_etat[e.id].is_complete:
color = color_complete color = color_complete
else: else:
color = color_incomplete color = color_incomplete
if day > today: if e.date_debut > datetime.datetime.now(scu.TIME_ZONE):
color = color_futur color = color_futur
href = "moduleimpl_status?moduleimpl_id=%s" % e["moduleimpl_id"] href = url_for(
# if e['heure_debut'].hour < 12: "notes.moduleimpl_status",
# halfday = True scodoc_dept=g.scodoc_dept,
# else: moduleimpl_id=e.moduleimpl_id,
# halfday = False )
if not day in events: day = e.date_debut.date().isoformat() # yyyy-mm-dd
# events[(day,halfday)] = [day, txt, color, href, halfday, description, mod] event = events.get(day)
events[day] = [day, txt, color, href, description, mod] if not event:
events[day] = [day, txt, color, href, description, e.moduleimpl]
else: else:
e = events[day] if event[-1].id != e.moduleimpl.id:
if e[-1]["moduleimpl_id"] != mod["moduleimpl_id"]:
# plusieurs evals de modules differents a la meme date # plusieurs evals de modules differents a la meme date
e[1] += ", " + txt event[1] += ", " + txt
e[4] += ", " + description event[4] += ", " + description
if not etat["evalcomplete"]: if color == color_incomplete:
e[2] = color_incomplete event[2] = color_incomplete
if day > today: if color == color_futur:
e[2] = color_futur event[2] = color_futur
CalHTML = sco_abs.YearTable( cal_html = sco_cal.YearTable(
year, events=list(events.values()), halfday=False, pad_width=None year, events=list(events.values()), halfday=False, pad_width=None
) )
H = [ return f"""
{
html_sco_header.html_sem_header( html_sco_header.html_sem_header(
"Evaluations du semestre", "Evaluations du semestre",
cssstyles=["css/calabs.css"], cssstyles=["css/calabs.css"],
), )
'<div class="cal_evaluations">', }
CalHTML, <div class="cal_evaluations">
"</div>", { cal_html }
"<p>soit %s évaluations planifiées;" % nb_evals, </div>
"""<ul><li>en <span style="background-color: %s">rouge</span> les évaluations passées auxquelles il manque des notes</li> <p>soit {nb_evals} évaluations planifiées;
<li>en <span style="background-color: %s">vert</span> les évaluations déjà notées</li> </p>
<li>en <span style="background-color: %s">bleu</span> les évaluations futures</li></ul></p>""" <ul>
% (color_incomplete, color_complete, color_futur), <li>en <span style=
"""<p><a href="formsemestre_evaluations_delai_correction?formsemestre_id=%s" class="stdlink">voir les délais de correction</a></p> "background-color: {color_incomplete}">rouge</span>
""" les évaluations passées auxquelles il manque des notes
% (formsemestre_id,), </li>
html_sco_header.sco_footer(), <li>en <span style=
] "background-color: {color_complete}">vert</span>
return "\n".join(H) 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 """Première date à laquelle l'évaluation a été complète
ou None si actuellement incomplète ou None si actuellement incomplète
""" """
@ -496,7 +528,7 @@ def evaluation_date_first_completion(evaluation_id):
# Il faut considerer les inscriptions au semestre # Il faut considerer les inscriptions au semestre
# (pour avoir l'etat et le groupe) et aussi les inscriptions # (pour avoir l'etat et le groupe) et aussi les inscriptions
# au module (pour gerer les modules optionnels correctement) # au module (pour gerer les modules optionnels correctement)
# E = do_evaluation_list(args={"evaluation_id": evaluation_id})[0] # E = get_evaluation_dict({"id":evaluation_id})[0]
# M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] # M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
# formsemestre_id = M["formsemestre_id"] # formsemestre_id = M["formsemestre_id"]
# insem = sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits( formsemestre_id) # insem = sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits( formsemestre_id)
@ -536,40 +568,44 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, format="html"):
N'indique pas les évaluations de rattrapage ni celles des modules de bonus/malus. N'indique pas les évaluations de rattrapage ni celles des modules de bonus/malus.
""" """
formsemestre = FormSemestre.get_formsemestre(formsemestre_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) evaluations = formsemestre.get_evaluations()
rows = []
evals = nt.get_evaluations_etats() for e in evaluations:
T = [] if (e.evaluation_type != scu.EVALUATION_NORMALE) or (
for e in evals: e.moduleimpl.module.module_type == ModuleType.MALUS
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=e["moduleimpl_id"])[0]
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
if (e["evaluation_type"] != scu.EVALUATION_NORMALE) or (
Mod["module_type"] == ModuleType.MALUS
): ):
continue continue
e["date_first_complete"] = evaluation_date_first_completion(e["evaluation_id"]) date_first_complete = evaluation_date_first_completion(e.id)
if e["date_first_complete"]: if date_first_complete and e.date_fin:
e["delai_correction"] = (e["date_first_complete"].date() - e["jour"]).days delai_correction = (date_first_complete.date() - e.date_fin).days
else: else:
e["delai_correction"] = None delai_correction = None
e["module_code"] = Mod["code"] rows.append(
e["_module_code_target"] = url_for( {
"notes.moduleimpl_status", "date_first_complete": date_first_complete,
scodoc_dept=g.scodoc_dept, "delai_correction": delai_correction,
moduleimpl_id=M["moduleimpl_id"], "jour": e.date_debut.strftime("%d/%m/%Y")
if e.date_debut
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 = ( columns_ids = (
"module_code", "module_code",
@ -592,16 +628,14 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, format="html"):
tab = GenTable( tab = GenTable(
titles=titles, titles=titles,
columns_ids=columns_ids, columns_ids=columns_ids,
rows=T, rows=rows,
html_class="table_leftalign table_coldate", html_class="table_leftalign table_coldate",
html_sortable=True, html_sortable=True,
html_title="<h2>Correction des évaluations du semestre</h2>", html_title="<h2>Correction des évaluations du semestre</h2>",
caption="Correction des évaluations du semestre", caption="Correction des évaluations du semestre",
preferences=sco_preferences.SemPreferences(formsemestre_id), preferences=sco_preferences.SemPreferences(formsemestre_id),
base_url="%s?formsemestre_id=%s" % (request.base_url, formsemestre_id), base_url="%s?formsemestre_id=%s" % (request.base_url, formsemestre_id),
origin="Généré par %s le " % sco_version.SCONAME origin=f"""Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}""",
+ scu.timedate_human_repr()
+ "",
filename=scu.make_filename("evaluations_delais_" + formsemestre.titre_annee()), filename=scu.make_filename("evaluations_delais_" + formsemestre.titre_annee()),
) )
return tab.make_page(format=format) return tab.make_page(format=format)
@ -612,78 +646,64 @@ def evaluation_describe(evaluation_id="", edit_in_place=True, link_saisie=True):
"""HTML description of evaluation, for page headers """HTML description of evaluation, for page headers
edit_in_place: allow in-place editing when permitted (not implemented) edit_in_place: allow in-place editing when permitted (not implemented)
""" """
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0] evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id)
moduleimpl_id = E["moduleimpl_id"] modimpl = evaluation.moduleimpl
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] responsable: User = db.session.get(User, modimpl.responsable_id)
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] resp_nomprenom = responsable.get_prenomnom()
formsemestre_id = M["formsemestre_id"] resp_nomcomplet = responsable.get_nomcomplet()
u = sco_users.user_info(M["responsable_id"]) can_edit = modimpl.can_edit_notes(current_user, allow_ens=False)
resp = u["prenomnom"]
nomcomplet = u["nomcomplet"]
can_edit = sco_permissions_check.can_edit_notes(
current_user, moduleimpl_id, allow_ens=False
)
link = ( mod_descr = f"""<a class="stdlink" href="{url_for("notes.moduleimpl_status",
'<span class="evallink"><a class="stdlink" href="evaluation_listenotes?moduleimpl_id=%s">voir toutes les notes du module</a></span>' scodoc_dept=g.scodoc_dept,
% moduleimpl_id moduleimpl_id=modimpl.id,
) )}">{modimpl.module.code or ""} {modimpl.module.abbrev or modimpl.module.titre or "?"}</a>
mod_descr = ( <span class="resp">(resp. <a title="{resp_nomcomplet}">{resp_nomprenom}</a>)</span>
'<a href="moduleimpl_status?moduleimpl_id=%s">%s %s</a> <span class="resp">(resp. <a title="%s">%s</a>)</span> %s' <span class="evallink"><a class="stdlink"
% ( href="{url_for(
moduleimpl_id, "notes.evaluation_listenotes",
Mod["code"] or "", scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
Mod["titre"] or "?", }">voir toutes les notes du module</a></span>
nomcomplet, """
resp,
link,
)
)
etit = E["description"] or "" eval_titre = f' "{evaluation.description}"' if evaluation.description else ""
if etit: if modimpl.module.module_type == ModuleType.MALUS:
etit = ' "' + etit + '"' eval_titre += ' <span class="eval_malus">(points de malus)</span>'
if Mod["module_type"] == ModuleType.MALUS:
etit += ' <span class="eval_malus">(points de malus)</span>'
H = [ H = [
'<span class="eval_title">Évaluation%s</span><p><b>Module : %s</b></p>' f"""<span class="eval_title">Évaluation{eval_titre}</span>
% (etit, mod_descr) <p><b>Module : {mod_descr}</b>
</p>"""
] ]
if Mod["module_type"] == ModuleType.MALUS: if modimpl.module.module_type == ModuleType.MALUS:
# Indique l'UE # Indique l'UE
ue = sco_edit_ue.ue_list(args={"ue_id": Mod["ue_id"]})[0] ue = modimpl.module.ue
H.append("<p><b>UE : %(acronyme)s</b></p>" % ue) H.append(f"<p><b>UE : {ue.acronyme}</b></p>")
# store min/max values used by JS client-side checks: # store min/max values used by JS client-side checks:
H.append( H.append(
'<span id="eval_note_min" class="sco-hidden">-20.</span><span id="eval_note_max" class="sco-hidden">20.</span>' """<span id="eval_note_min" class="sco-hidden">-20.</span>
<span id="eval_note_max" class="sco-hidden">20.</span>"""
) )
else: else:
# date et absences (pas pour evals de malus) # date et absences (pas pour evals de malus)
if E["jour"]: if evaluation.date_debut is not None:
jour = E["jour"] H.append(f"<p>Réalisée le <b>{evaluation.descr_date()}</b> ")
H.append("<p>Réalisée le <b>%s</b> " % (jour)) group_id = sco_groups.get_default_group(modimpl.formsemestre_id)
if E["heure_debut"] != E["heure_fin"]:
H.append("de %s à %s " % (E["heure_debut"], E["heure_fin"]))
group_id = sco_groups.get_default_group(formsemestre_id)
H.append( H.append(
f"""<span class="noprint"><a href="{url_for( f"""<span class="evallink"><a class="stdlink" href="{url_for(
'assiduites.get_etat_abs_date', 'assiduites.etat_abs_date',
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
group_ids=group_id, group_ids=group_id,
desc=E["description"], desc=evaluation.description or "",
jour=E["jour"], date_debut=evaluation.date_debut.isoformat(),
heure_debut=E["heure_debut"], date_fin=evaluation.date_fin.isoformat(),
heure_fin=E["heure_fin"],
) )
}">(absences ce jour)</a></span>""" }">absences ce jour</a></span>"""
) )
else: else:
jour = "<em>pas de date</em>" H.append("<p><em>sans date</em> ")
H.append("<p>Réalisée le <b>%s</b> " % (jour))
H.append( H.append(
'</p><p>Coefficient dans le module: <b>%s</b>, notes sur <span id="eval_note_max">%g</span> ' f"""</p><p>Coefficient dans le module: <b>{evaluation.coefficient or "0"}</b>,
% (E["coefficient"], E["note_max"]) notes sur <span id="eval_note_max">{(evaluation.note_max or 0):g}</span> """
) )
H.append('<span id="eval_note_min" class="sco-hidden">0.</span>') H.append('<span id="eval_note_min" class="sco-hidden">0.</span>')
if can_edit: if can_edit:
@ -697,7 +717,7 @@ def evaluation_describe(evaluation_id="", edit_in_place=True, link_saisie=True):
if link_saisie: if link_saisie:
H.append( H.append(
f""" f"""
<a class="stdlink" href="{url_for( <a style="margin-left: 12px;" class="stdlink" href="{url_for(
"notes.saisie_notes", scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id) "notes.saisie_notes", scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id)
}">saisie des notes</a> }">saisie des notes</a>
""" """

View File

@ -512,8 +512,7 @@ def excel_feuille_saisie(evaluation: "Evaluation", titreannee, description, line
# description evaluation # description evaluation
ws.append_single_cell_row(scu.unescape_html(description), style_titres) ws.append_single_cell_row(scu.unescape_html(description), style_titres)
ws.append_single_cell_row( ws.append_single_cell_row(
"Evaluation du %s (coef. %g)" f"Evaluation {evaluation.descr_date()} (coef. {(evaluation.coefficient or 0.0):g})",
% (evaluation.jour or "sans date", evaluation.coefficient or 0.0),
style, style,
) )
# ligne blanche # ligne blanche

View File

@ -1245,7 +1245,9 @@ def do_formsemestre_clone(
moduleimpl_id=mod_orig["moduleimpl_id"] moduleimpl_id=mod_orig["moduleimpl_id"]
): ):
# copie en enlevant la date # copie en enlevant la date
new_eval = e.clone(not_copying=("jour", "moduleimpl_id")) new_eval = e.clone(
not_copying=("date_debut", "date_fin", "moduleimpl_id")
)
new_eval.moduleimpl_id = mid new_eval.moduleimpl_id = mid
# Copie les poids APC de l'évaluation # Copie les poids APC de l'évaluation
new_eval.set_ue_poids_dict(e.get_ue_poids_dict()) new_eval.set_ue_poids_dict(e.get_ue_poids_dict())
@ -1443,7 +1445,7 @@ def do_formsemestre_delete(formsemestre_id):
mods = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id) mods = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
for mod in mods: for mod in mods:
# evaluations # evaluations
evals = sco_evaluation_db.do_evaluation_list( evals = sco_evaluation_db.get_evaluation_dict(
args={"moduleimpl_id": mod["moduleimpl_id"]} args={"moduleimpl_id": mod["moduleimpl_id"]}
) )
for e in evals: for e in evals:

View File

@ -38,7 +38,7 @@ from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models import Formation, FormSemestre, FormSemestreInscription, Scolog from app.models import Formation, FormSemestre, FormSemestreInscription, Scolog
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.groups import GroupDescr from app.models.groups import Partition, GroupDescr
from app.models.validations import ScolarEvent from app.models.validations import ScolarEvent
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app import log from app import log
@ -236,6 +236,10 @@ def do_formsemestre_desinscription(etudid, formsemestre_id):
sco_moduleimpl.do_moduleimpl_inscription_delete( sco_moduleimpl.do_moduleimpl_inscription_delete(
moduleimpl_inscription_id, formsemestre_id=formsemestre_id moduleimpl_inscription_id, formsemestre_id=formsemestre_id
) )
# -- désincription de tous les groupes des partitions de ce semestre
Partition.formsemestre_remove_etud(formsemestre_id, etud)
# -- désincription du semestre # -- désincription du semestre
do_formsemestre_inscription_delete( do_formsemestre_inscription_delete(
insem["formsemestre_inscription_id"], formsemestre_id=formsemestre_id insem["formsemestre_inscription_id"], formsemestre_id=formsemestre_id
@ -259,7 +263,7 @@ def do_formsemestre_desinscription(etudid, formsemestre_id):
cnx, cnx,
method="formsemestre_desinscription", method="formsemestre_desinscription",
etudid=etudid, etudid=etudid,
msg="desinscription semestre %s" % formsemestre_id, msg=f"desinscription semestre {formsemestre_id}",
commit=False, commit=False,
) )

View File

@ -50,6 +50,7 @@ from app.models import (
ModuleImpl, ModuleImpl,
NotesNotes, NotesNotes,
) )
from app.scodoc.codes_cursus import UE_SPORT
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType from app.scodoc.sco_utils import ModuleType
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
@ -60,7 +61,6 @@ from app.scodoc.sco_exceptions import (
) )
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import htmlutils from app.scodoc import htmlutils
from app.scodoc import sco_abs
from app.scodoc import sco_archives from app.scodoc import sco_archives
from app.scodoc import sco_bulletins from app.scodoc import sco_bulletins
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
@ -498,7 +498,7 @@ def retreive_formsemestre_from_request() -> int:
modimpl = modimpl[0] modimpl = modimpl[0]
formsemestre_id = modimpl["formsemestre_id"] formsemestre_id = modimpl["formsemestre_id"]
elif "evaluation_id" in args: elif "evaluation_id" in args:
E = sco_evaluation_db.do_evaluation_list( E = sco_evaluation_db.get_evaluation_dict(
{"evaluation_id": args["evaluation_id"]} {"evaluation_id": args["evaluation_id"]}
) )
if not E: if not E:
@ -620,7 +620,7 @@ def formsemestre_description_table(
columns_ids += ["Inscrits", "Responsable", "Enseignants"] columns_ids += ["Inscrits", "Responsable", "Enseignants"]
if with_evals: if with_evals:
columns_ids += [ columns_ids += [
"jour", "date_evaluation",
"description", "description",
"coefficient", "coefficient",
"evalcomplete_str", "evalcomplete_str",
@ -630,7 +630,7 @@ def formsemestre_description_table(
titles = {title: title for title in columns_ids} titles = {title: title for title in columns_ids}
titles.update({f"ue_{ue.id}": ue.acronyme for ue in ues}) titles.update({f"ue_{ue.id}": ue.acronyme for ue in ues})
titles["ects"] = "ECTS" titles["ects"] = "ECTS"
titles["jour"] = "Évaluation" titles["date_evaluation"] = "Évaluation"
titles["description"] = "" titles["description"] = ""
titles["coefficient"] = "Coef. éval." titles["coefficient"] = "Coef. éval."
titles["evalcomplete_str"] = "Complète" titles["evalcomplete_str"] = "Complète"
@ -660,9 +660,11 @@ def formsemestre_description_table(
"Module": ue.titre, "Module": ue.titre,
"_css_row_class": "table_row_ue", "_css_row_class": "table_row_ue",
} }
if use_ue_coefs: if use_ue_coefs and ue.type != UE_SPORT:
ue_info["Coef."] = ue.coefficient ue_info["Coef."] = ue.coefficient or "0."
ue_info["Coef._class"] = "ue_coef" ue_info["_Coef._class"] = "ue_coef"
if not ue.coefficient:
ue_info["_Coef._class"] += " ue_coef_nul"
if ue.color: if ue.color:
for k in list(ue_info.keys()): for k in list(ue_info.keys()):
if not k.startswith("_"): if not k.startswith("_"):
@ -738,8 +740,10 @@ def formsemestre_description_table(
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
evaluation_id=e["evaluation_id"], evaluation_id=e["evaluation_id"],
) )
e["_jour_order"] = e["jour"].isoformat() e["_date_evaluation_order"] = e["jour"].isoformat()
e["jour"] = e["jour"].strftime("%d/%m/%Y") if e["jour"] else "" e["date_evaluation"] = (
e["jour"].strftime("%d/%m/%Y") if e["jour"] else ""
)
e["UE"] = row["UE"] e["UE"] = row["UE"]
e["_UE_td_attrs"] = row["_UE_td_attrs"] e["_UE_td_attrs"] = row["_UE_td_attrs"]
e["Code"] = row["Code"] e["Code"] = row["Code"]
@ -884,13 +888,12 @@ def _make_listes_sem(formsemestre: FormSemestre, with_absences=True):
if n_members == 0: if n_members == 0:
continue # skip empty groups continue # skip empty groups
partition_is_empty = False partition_is_empty = False
group["url_etat"] = url_for( group[
"absences.EtatAbsencesGr", "url_etat"
group_ids=group["group_id"], ] = f"""{
debut=formsemestre.date_debut.strftime("%d/%m/%Y"), url_for("assiduites.visu_assi_group", scodoc_dept=g.scodoc_dept)
fin=formsemestre.date_fin.strftime("%d/%m/%Y"), }?group_ids={group["id"]}&date_debut={formsemestre.date_debut.isoformat()}&date_fin={formsemestre.date_fin.isoformat()}"""
scodoc_dept=g.scodoc_dept,
)
if group["group_name"]: if group["group_name"]:
group["label"] = "groupe %(group_name)s" % group group["label"] = "groupe %(group_name)s" % group
else: else:

View File

@ -45,7 +45,7 @@ from app import db
from app.models import FormSemestre from app.models import FormSemestre
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_abs from app.scodoc import sco_cal
from app.scodoc import sco_excel from app.scodoc import sco_excel
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups from app.scodoc import sco_groups
@ -819,9 +819,12 @@ def tab_absences_html(groups_infos, etat=None):
H = ['<div class="tab-content">'] H = ['<div class="tab-content">']
if not groups_infos.members: if not groups_infos.members:
return "".join(H) + "<h3>Aucun étudiant !</h3></div>" return "".join(H) + "<h3>Aucun étudiant !</h3></div>"
group_ids: str = ",".join(map(str, groups_infos.group_ids))
formsemestre: FormSemestre = groups_infos.get_formsemestre()
H.extend( H.extend(
[ [
"<h3>Absences</h3>", "<h3>Assiduités</h3>",
'<ul class="ul_abs">', '<ul class="ul_abs">',
"<li>", "<li>",
form_choix_saisie_semaine(groups_infos), # Ajout Le Havre form_choix_saisie_semaine(groups_infos), # Ajout Le Havre
@ -829,12 +832,9 @@ def tab_absences_html(groups_infos, etat=None):
"<li>", "<li>",
form_choix_jour_saisie_hebdo(groups_infos), form_choix_jour_saisie_hebdo(groups_infos),
"</li>", "</li>",
"""<li><a class="stdlink" href="Absences/EtatAbsencesGr?%s&debut=%s&fin=%s">État des absences du groupe</a></li>""" f"""<li><a href="{
% ( url_for("assiduites.visu_assi_group", scodoc_dept=g.scodoc_dept)
groups_infos.groups_query_args, }?group_ids={group_ids}&date_debut={formsemestre.date_debut.isoformat()}&date_fin={formsemestre.date_fin.isoformat()}">État des assiduités du groupe</li>""",
groups_infos.formsemestre["date_debut"],
groups_infos.formsemestre["date_fin"],
),
"</ul>", "</ul>",
"<h3>Feuilles</h3>", "<h3>Feuilles</h3>",
'<ul class="ul_feuilles">', '<ul class="ul_feuilles">',
@ -890,13 +890,18 @@ def form_choix_jour_saisie_hebdo(groups_infos, moduleimpl_id=None):
if not authuser.has_permission(Permission.ScoAbsChange): if not authuser.has_permission(Permission.ScoAbsChange):
return "" return ""
sem = groups_infos.formsemestre sem = groups_infos.formsemestre
first_monday = sco_abs.ddmmyyyy(sem["date_debut"]).prev_monday() first_monday = sco_cal.ddmmyyyy(sem["date_debut"]).prev_monday()
today_idx = datetime.date.today().weekday() today_idx = datetime.date.today().weekday()
# TODO-ASSIDUITE
# Utilisation d'un formulaire et de la saisie journalière.
# Dans le formulaire : choisir le jour (lun/mar/...)
FA = [] # formulaire avec menu saisi absences FA = [] # formulaire avec menu saisi absences
FA.append( FA.append(
'<form id="form_choix_jour_saisie_hebdo" action="Absences/SignaleAbsenceGrSemestre" method="get">' # TODO-ASSIDUITE et utiliser url_for... (was Absences/SignaleAbsenceGrSemestre)
'<form id="form_choix_jour_saisie_hebdo" action="XXX" method="get">'
) )
FA.append('<input type="hidden" name="datefin" value="%(date_fin)s"/>' % sem) FA.append('<input type="hidden" name="datefin" value="%(date_fin)s"/>' % sem)
FA.append(groups_infos.get_form_elem()) FA.append(groups_infos.get_form_elem())
if moduleimpl_id: if moduleimpl_id:
@ -906,12 +911,12 @@ def form_choix_jour_saisie_hebdo(groups_infos, moduleimpl_id=None):
FA.append('<input type="hidden" name="destination" value=""/>') FA.append('<input type="hidden" name="destination" value=""/>')
FA.append( FA.append(
"""<input type="button" onclick="$('#form_choix_jour_saisie_hebdo')[0].destination.value=get_current_url(); $('#form_choix_jour_saisie_hebdo').submit();" value="Saisir absences du "/>""" """<input type="button" onclick="$('#form_choix_jour_saisie_hebdo')[0].destination.value=get_current_url(); $('#form_choix_jour_saisie_hebdo').submit();" value="Saisir absences du (NON DISPONIBLE) "/>"""
) )
FA.append("""<select name="datedebut">""") FA.append("""<select name="datedebut">""")
date = first_monday date = first_monday
i = 0 i = 0
for jour in sco_abs.day_names(): for jour in sco_cal.day_names():
if i == today_idx: if i == today_idx:
sel = "selected" sel = "selected"
else: else:
@ -945,14 +950,17 @@ def form_choix_saisie_semaine(groups_infos):
) # car ici utilisee dans un format string ! ) # car ici utilisee dans un format string !
DateJour = time.strftime("%d/%m/%Y") DateJour = time.strftime("%d/%m/%Y")
datelundi = sco_abs.ddmmyyyy(DateJour).prev_monday() datelundi = sco_cal.ddmmyyyy(DateJour).prev_monday()
FA = [] # formulaire avec menu saisie hebdo des absences FA = [] # formulaire avec menu saisie hebdo des absences
# XXX TODO-ASSIDUITE et utiliser un POST
FA.append('<form action="Absences/SignaleAbsenceGrHebdo" method="get">') FA.append('<form action="Absences/SignaleAbsenceGrHebdo" method="get">')
FA.append('<input type="hidden" name="datelundi" value="%s"/>' % datelundi) FA.append('<input type="hidden" name="datelundi" value="%s"/>' % datelundi)
FA.append('<input type="hidden" name="moduleimpl_id" value="%s"/>' % moduleimpl_id) FA.append('<input type="hidden" name="moduleimpl_id" value="%s"/>' % moduleimpl_id)
FA.append('<input type="hidden" name="destination" value="%s"/>' % destination) FA.append('<input type="hidden" name="destination" value="%s"/>' % destination)
FA.append(groups_infos.get_form_elem()) FA.append(groups_infos.get_form_elem())
FA.append('<input type="submit" class="button" value="Saisie à la semaine" />') FA.append(
'<input type="submit" class="button" value="Saisie à la semaine (NON DISPONIBLE)" />'
) # XXX
FA.append("</form>") FA.append("</form>")
return "\n".join(FA) return "\n".join(FA)

View File

@ -171,12 +171,16 @@ def list_inscrits_date(sem):
return [x[0] for x in cursor.fetchall()] return [x[0] for x in cursor.fetchall()]
def do_inscrit(sem, etudids, inscrit_groupes=False): def do_inscrit(sem, etudids, inscrit_groupes=False, inscrit_parcours=False):
"""Inscrit ces etudiants dans ce semestre """Inscrit ces etudiants dans ce semestre
(la liste doit avoir été vérifiée au préalable) (la liste doit avoir été vérifiée au préalable)
En option: inscrit aux mêmes groupes que dans le semestre origine En option:
- Si inscrit_groupes, inscrit aux mêmes groupes que dans le semestre origine
(toutes partitions, y compris parcours)
- Si inscrit_parcours, inscrit au même groupe de parcours (mais ignore les autres partitions)
(si les deux sont vrais, inscrit_parcours n'a pas d'effet)
""" """
# TODO à ré-écrire pour utiliser le smodèle, notamment GroupDescr # TODO à ré-écrire pour utiliser les modèles, notamment GroupDescr
formsemestre: FormSemestre = db.session.get(FormSemestre, sem["formsemestre_id"]) formsemestre: FormSemestre = db.session.get(FormSemestre, sem["formsemestre_id"])
formsemestre.setup_parcours_groups() formsemestre.setup_parcours_groups()
log(f"do_inscrit (inscrit_groupes={inscrit_groupes}): {etudids}") log(f"do_inscrit (inscrit_groupes={inscrit_groupes}): {etudids}")
@ -187,7 +191,7 @@ def do_inscrit(sem, etudids, inscrit_groupes=False):
etat=scu.INSCRIT, etat=scu.INSCRIT,
method="formsemestre_inscr_passage", method="formsemestre_inscr_passage",
) )
if inscrit_groupes: if inscrit_groupes or inscrit_parcours:
# Inscription dans les mêmes groupes que ceux du semestre d'origine, # Inscription dans les mêmes groupes que ceux du semestre d'origine,
# s'ils existent. # s'ils existent.
# (mise en correspondance à partir du nom du groupe, sans tenir compte # (mise en correspondance à partir du nom du groupe, sans tenir compte
@ -223,11 +227,16 @@ def do_inscrit(sem, etudids, inscrit_groupes=False):
group: GroupDescr = db.session.get( group: GroupDescr = db.session.get(
GroupDescr, partition_group["group_id"] GroupDescr, partition_group["group_id"]
) )
sco_groups.change_etud_group_in_partition(etudid, group) if inscrit_groupes or (
group.partition.partition_name == scu.PARTITION_PARCOURS
and inscrit_parcours
):
sco_groups.change_etud_group_in_partition(etudid, group)
def do_desinscrit(sem, etudids): def do_desinscrit(sem: dict, etudids: list[int]):
log("do_desinscrit: %s" % etudids) "désinscrit les étudiants indiqués du formsemestre"
log(f"do_desinscrit: {etudids}")
for etudid in etudids: for etudid in etudids:
sco_formsemestre_inscriptions.do_formsemestre_desinscription( sco_formsemestre_inscriptions.do_formsemestre_desinscription(
etudid, sem["formsemestre_id"] etudid, sem["formsemestre_id"]
@ -273,6 +282,7 @@ def formsemestre_inscr_passage(
formsemestre_id, formsemestre_id,
etuds=[], etuds=[],
inscrit_groupes=False, inscrit_groupes=False,
inscrit_parcours=False,
submitted=False, submitted=False,
dialog_confirmed=False, dialog_confirmed=False,
ignore_jury=False, ignore_jury=False,
@ -291,6 +301,7 @@ def formsemestre_inscr_passage(
""" """
inscrit_groupes = int(inscrit_groupes) inscrit_groupes = int(inscrit_groupes)
inscrit_parcours = int(inscrit_parcours)
ignore_jury = int(ignore_jury) ignore_jury = int(ignore_jury)
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
# -- check lock # -- check lock
@ -335,6 +346,7 @@ def formsemestre_inscr_passage(
candidats_non_inscrits, candidats_non_inscrits,
inscrits_ailleurs, inscrits_ailleurs,
inscrit_groupes=inscrit_groupes, inscrit_groupes=inscrit_groupes,
inscrit_parcours=inscrit_parcours,
ignore_jury=ignore_jury, ignore_jury=ignore_jury,
) )
else: else:
@ -376,6 +388,7 @@ def formsemestre_inscr_passage(
"formsemestre_id": formsemestre_id, "formsemestre_id": formsemestre_id,
"etuds": ",".join([str(x) for x in etuds]), "etuds": ",".join([str(x) for x in etuds]),
"inscrit_groupes": inscrit_groupes, "inscrit_groupes": inscrit_groupes,
"inscrit_parcours": inscrit_parcours,
"ignore_jury": ignore_jury, "ignore_jury": ignore_jury,
"submitted": 1, "submitted": 1,
}, },
@ -388,6 +401,7 @@ def formsemestre_inscr_passage(
sem, sem,
a_inscrire, a_inscrire,
inscrit_groupes=inscrit_groupes, inscrit_groupes=inscrit_groupes,
inscrit_parcours=inscrit_parcours,
) )
# Désinscriptions: # Désinscriptions:
do_desinscrit(sem, a_desinscrire) do_desinscrit(sem, a_desinscrire)
@ -433,15 +447,21 @@ def _build_page(
candidats_non_inscrits, candidats_non_inscrits,
inscrits_ailleurs, inscrits_ailleurs,
inscrit_groupes=False, inscrit_groupes=False,
inscrit_parcours=False,
ignore_jury=False, ignore_jury=False,
): ):
formsemestre: FormSemestre = db.session.get(FormSemestre, sem["formsemestre_id"]) formsemestre: FormSemestre = db.session.get(FormSemestre, sem["formsemestre_id"])
inscrit_groupes = int(inscrit_groupes) inscrit_groupes = int(inscrit_groupes)
inscrit_parcours = int(inscrit_parcours)
ignore_jury = int(ignore_jury) ignore_jury = int(ignore_jury)
if inscrit_groupes: if inscrit_groupes:
inscrit_groupes_checked = " checked" inscrit_groupes_checked = " checked"
else: else:
inscrit_groupes_checked = "" inscrit_groupes_checked = ""
if inscrit_parcours:
inscrit_parcours_checked = " checked"
else:
inscrit_parcours_checked = ""
if ignore_jury: if ignore_jury:
ignore_jury_checked = " checked" ignore_jury_checked = " checked"
else: else:
@ -458,17 +478,23 @@ def _build_page(
&nbsp;<a href="#help">aide</a> &nbsp;<a href="#help">aide</a>
<input name="inscrit_groupes" type="checkbox" value="1" <input name="inscrit_groupes" type="checkbox" value="1"
{inscrit_groupes_checked}>inscrire aux mêmes groupes</input> {inscrit_groupes_checked}>inscrire aux mêmes groupes (y compris parcours)</input>
<input name="inscrit_parcours" type="checkbox" value="1"
{inscrit_parcours_checked}>inscrire aux mêmes parcours</input>
<input name="ignore_jury" type="checkbox" value="1" onchange="document.f.submit()" <input name="ignore_jury" type="checkbox" value="1" onchange="document.f.submit()"
{ignore_jury_checked}>inclure tous les étudiants (même sans décision de jury)</input> {ignore_jury_checked}>inclure tous les étudiants (même sans décision de jury)</input>
<div class="pas_recap">Actuellement <span id="nbinscrits">{len(inscrits)}</span> inscrits <div class="pas_recap">Actuellement <span id="nbinscrits">{len(inscrits)}</span>
et {len(candidats_non_inscrits)} candidats supplémentaires inscrits et {len(candidats_non_inscrits)} candidats supplémentaires.
</div> </div>
<div>{scu.EMO_WARNING} <em>Seuls les semestres dont la date de fin est antérieure à la date de début <div>{scu.EMO_WARNING}
de ce semestre ({formsemestre.date_debut.strftime("%d/%m/%Y")}) sont pris en compte.</em></div> <em>Seuls les semestres dont la date de fin est antérieure à la date de début
de ce semestre ({formsemestre.date_debut.strftime("%d/%m/%Y")}) sont pris en
compte.</em>
</div>
{etuds_select_boxes(auth_etuds_by_sem, inscrits_ailleurs)} {etuds_select_boxes(auth_etuds_by_sem, inscrits_ailleurs)}
<input type="submit" name="submitted" value="Appliquer les modifications"/> <input type="submit" name="submitted" value="Appliquer les modifications"/>
@ -498,7 +524,8 @@ def _build_page(
return H return H
def formsemestre_inscr_passage_help(sem): def formsemestre_inscr_passage_help(sem: dict):
"texte d'aide en bas de la page passage des étudiants"
return f"""<div class="pas_help"><h3><a name="help">Explications</a></h3> return f"""<div class="pas_help"><h3><a name="help">Explications</a></h3>
<p>Cette page permet d'inscrire des étudiants dans le semestre destination <p>Cette page permet d'inscrire des étudiants dans le semestre destination
<a class="stdlink" <a class="stdlink"
@ -507,18 +534,34 @@ def formsemestre_inscr_passage_help(sem):
}">{sem['titreannee']}</a>, }">{sem['titreannee']}</a>,
et d'en désincrire si besoin. et d'en désincrire si besoin.
</p> </p>
<p>Les étudiants sont groupés par semestres d'origines. Ceux qui sont en caractères <p>Les étudiants sont groupés par semestre d'origine. Ceux qui sont en caractères
<span class="inscrit">gras</span> sont déjà inscrits dans le semestre destination. <span class="inscrit">gras</span> sont déjà inscrits dans le semestre destination.
Ceux qui sont en <span class"inscrailleurs">gras et en rouge</span> sont inscrits Ceux qui sont en <span class"inscrailleurs">gras et en rouge</span> sont inscrits
dans un <em>autre</em> semestre.</p> dans un <em>autre</em> semestre.
<p>Au départ, les étudiants déjà inscrits sont sélectionnés; vous pouvez ajouter d'autres </p>
étudiants à inscrire dans le semestre destination.</p> <p>Au départ, les étudiants déjà inscrits sont sélectionnés; vous pouvez ajouter
<p>Si vous -selectionnez un étudiant déjà inscrit (en gras), il sera désinscrit.</p> d'autres étudiants à inscrire dans le semestre destination.
<p>Le bouton <em>inscrire aux mêmes groupes</em> ne prend en compte que les groupes qui existent </p>
dans les deux semestres: pensez à créer les partitions et groupes que vous souhaitez conserver
<b>avant</b> d'inscrire les étudiants. <p>Si vous -selectionnez un étudiant déjà inscrit (en gras), il sera désinscrit.
</p>
<p>Le bouton <em>inscrire aux mêmes groupes</em> ne prend en compte que les groupes
qui existent dans les deux semestres: pensez à créer les partitions et groupes que
vous souhaitez conserver <b>avant</b> d'inscrire les étudiants.
</p>
<p>Les parcours de BUT sont gérés comme des groupes de la partition parcours: si on
conserve les groupes, on conserve les parcours ( aussi, pensez à les cocher dans
<a class="stdlink" href="{
url_for("notes.formsemestre_editwithmodules", scodoc_dept=g.scodoc_dept,
formsemestre_id=sem["formsemestre_id"] )
}">modifier le semestre</a> avant de faire passer les étudiants).
</a>
<p class="help">Aucune action ne sera effectuée si vous n'appuyez pas sur le bouton
"Appliquer les modifications" !
</p> </p>
<p class="help">Aucune action ne sera effectuée si vous n'appuyez pas sur le bouton "Appliquer les modifications" !</p>
</div> </div>
""" """

View File

@ -69,38 +69,44 @@ def do_evaluation_listenotes(
mode = None mode = None
if moduleimpl_id: if moduleimpl_id:
mode = "module" mode = "module"
evals = sco_evaluation_db.do_evaluation_list({"moduleimpl_id": moduleimpl_id}) evals = sco_evaluation_db.get_evaluation_dict({"moduleimpl_id": moduleimpl_id})
elif evaluation_id: elif evaluation_id:
mode = "eval" mode = "eval"
evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id}) evals = sco_evaluation_db.get_evaluation_dict({"evaluation_id": evaluation_id})
else: else:
raise ValueError("missing argument: evaluation or module") raise ValueError("missing argument: evaluation or module")
if not evals: if not evals:
return "<p>Aucune évaluation !</p>", "ScoDoc" return "<p>Aucune évaluation !</p>", "ScoDoc"
E = evals[0] # il y a au moins une evaluation eval_dict = evals[0] # il y a au moins une evaluation
modimpl = db.session.get(ModuleImpl, E["moduleimpl_id"]) modimpl = db.session.get(ModuleImpl, eval_dict["moduleimpl_id"])
# description de l'evaluation # description de l'evaluation
if mode == "eval": if mode == "eval":
H = [sco_evaluations.evaluation_describe(evaluation_id=evaluation_id)] H = [sco_evaluations.evaluation_describe(evaluation_id=evaluation_id)]
page_title = f"Notes {E['description'] or modimpl.module.code}" page_title = f"Notes {eval_dict['description'] or modimpl.module.code}"
else: else:
H = [] H = []
page_title = f"Notes {modimpl.module.code}" page_title = f"Notes {modimpl.module.code}"
# groupes # groupes
groups = sco_groups.do_evaluation_listegroupes( groups = sco_groups.do_evaluation_listegroupes(
E["evaluation_id"], include_default=True eval_dict["evaluation_id"], include_default=True
) )
grlabs = [g["group_name"] or "tous" for g in groups] # legendes des boutons grlabs = [g["group_name"] or "tous" for g in groups] # legendes des boutons
grnams = [str(g["group_id"]) for g in groups] # noms des checkbox grnams = [str(g["group_id"]) for g in groups] # noms des checkbox
if len(evals) > 1: if len(evals) > 1:
descr = [ descr = [
("moduleimpl_id", {"default": E["moduleimpl_id"], "input_type": "hidden"}) (
"moduleimpl_id",
{"default": eval_dict["moduleimpl_id"], "input_type": "hidden"},
)
] ]
else: else:
descr = [ descr = [
("evaluation_id", {"default": E["evaluation_id"], "input_type": "hidden"}) (
"evaluation_id",
{"default": eval_dict["evaluation_id"], "input_type": "hidden"},
)
] ]
if len(grnams) > 1: if len(grnams) > 1:
descr += [ descr += [
@ -199,7 +205,7 @@ def do_evaluation_listenotes(
url_for( url_for(
"notes.moduleimpl_status", "notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
moduleimpl_id=E["moduleimpl_id"], moduleimpl_id=eval_dict["moduleimpl_id"],
) )
), ),
"", "",

View File

@ -230,7 +230,7 @@ def do_moduleimpl_inscription_list(moduleimpl_id=None, etudid=None):
return _moduleimpl_inscriptionEditor.list(cnx, args) return _moduleimpl_inscriptionEditor.list(cnx, args)
def moduleimpl_listeetuds(moduleimpl_id): def moduleimpl_listeetuds(moduleimpl_id): # XXX OBSOLETE
"retourne liste des etudids inscrits a ce module" "retourne liste des etudids inscrits a ce module"
req = """SELECT DISTINCT Im.etudid req = """SELECT DISTINCT Im.etudid
FROM notes_moduleimpl_inscription Im, FROM notes_moduleimpl_inscription Im,

View File

@ -29,6 +29,7 @@
""" """
import math import math
import time import time
import datetime
from flask import g, url_for from flask import g, url_for
from flask_login import current_user from flask_login import current_user
@ -47,10 +48,9 @@ from app.scodoc.sco_permissions import Permission
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import htmlutils from app.scodoc import htmlutils
from app.scodoc import sco_abs from app.scodoc import sco_cal
from app.scodoc import sco_compute_moy from app.scodoc import sco_compute_moy
from app.scodoc import sco_evaluations from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formsemestre_status from app.scodoc import sco_formsemestre_status
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
@ -59,19 +59,15 @@ from app.tables import list_etuds
# menu evaluation dans moduleimpl # menu evaluation dans moduleimpl
def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str: def moduleimpl_evaluation_menu(evaluation: Evaluation, nbnotes: int = 0) -> str:
"Menu avec actions sur une evaluation" "Menu avec actions sur une evaluation"
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0] modimpl: ModuleImpl = evaluation.moduleimpl
modimpl = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] group_id = sco_groups.get_default_group(modimpl.formsemestre_id)
evaluation_id = evaluation.id
can_edit_notes = modimpl.can_edit_notes(current_user, allow_ens=False)
can_edit_notes_ens = modimpl.can_edit_notes(current_user)
group_id = sco_groups.get_default_group(modimpl["formsemestre_id"]) if can_edit_notes and nbnotes != 0:
if (
sco_permissions_check.can_edit_notes(
current_user, E["moduleimpl_id"], allow_ens=False
)
and nbnotes != 0
):
sup_label = "Supprimer évaluation impossible (il y a des notes)" sup_label = "Supprimer évaluation impossible (il y a des notes)"
else: else:
sup_label = "Supprimer évaluation" sup_label = "Supprimer évaluation"
@ -83,9 +79,7 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str:
"args": { "args": {
"evaluation_id": evaluation_id, "evaluation_id": evaluation_id,
}, },
"enabled": sco_permissions_check.can_edit_notes( "enabled": can_edit_notes_ens,
current_user, E["moduleimpl_id"]
),
}, },
{ {
"title": "Modifier évaluation", "title": "Modifier évaluation",
@ -93,9 +87,7 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str:
"args": { "args": {
"evaluation_id": evaluation_id, "evaluation_id": evaluation_id,
}, },
"enabled": sco_permissions_check.can_edit_notes( "enabled": can_edit_notes,
current_user, E["moduleimpl_id"], allow_ens=False
),
}, },
{ {
"title": sup_label, "title": sup_label,
@ -103,10 +95,7 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str:
"args": { "args": {
"evaluation_id": evaluation_id, "evaluation_id": evaluation_id,
}, },
"enabled": nbnotes == 0 "enabled": nbnotes == 0 and can_edit_notes,
and sco_permissions_check.can_edit_notes(
current_user, E["moduleimpl_id"], allow_ens=False
),
}, },
{ {
"title": "Supprimer toutes les notes", "title": "Supprimer toutes les notes",
@ -114,9 +103,7 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str:
"args": { "args": {
"evaluation_id": evaluation_id, "evaluation_id": evaluation_id,
}, },
"enabled": sco_permissions_check.can_edit_notes( "enabled": can_edit_notes,
current_user, E["moduleimpl_id"], allow_ens=False
),
}, },
{ {
"title": "Afficher les notes", "title": "Afficher les notes",
@ -132,21 +119,18 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str:
"args": { "args": {
"evaluation_id": evaluation_id, "evaluation_id": evaluation_id,
}, },
"enabled": sco_permissions_check.can_edit_notes( "enabled": can_edit_notes_ens,
current_user, E["moduleimpl_id"]
),
}, },
{ {
"title": "Absences ce jour", "title": "Absences ce jour",
"endpoint": "assiduites.get_etat_abs_date", "endpoint": "assiduites.etat_abs_date",
"args": { "args": {
"group_ids": group_id, "group_ids": group_id,
"desc": E["description"], "desc": evaluation.description or "",
"jour": E["jour"], "date_debut": evaluation.date_debut.isoformat(),
"heure_debut": E["heure_debut"], "date_fin": evaluation.date_fin.isoformat(),
"heure_fin": E["heure_fin"],
}, },
"enabled": E["jour"], "enabled": evaluation.date_debut is not None,
}, },
{ {
"title": "Vérifier notes vs absents", "title": "Vérifier notes vs absents",
@ -154,7 +138,7 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str:
"args": { "args": {
"evaluation_id": evaluation_id, "evaluation_id": evaluation_id,
}, },
"enabled": nbnotes > 0 and E["jour"], "enabled": nbnotes > 0 and evaluation.date_debut is not None,
}, },
] ]
@ -203,11 +187,10 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
) )
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
# Evaluations, la plus RECENTE en tête # Evaluations, par numéros ou la plus RECENTE en tête
evaluations = modimpl.evaluations.order_by( evaluations = modimpl.evaluations.order_by(
Evaluation.numero.desc(), Evaluation.numero.desc(),
Evaluation.jour.desc(), Evaluation.date_debut.desc(),
Evaluation.heure_debut.desc(),
).all() ).all()
nb_evaluations = len(evaluations) nb_evaluations = len(evaluations)
max_poids = max( max_poids = max(
@ -333,10 +316,6 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
'<tr><td colspan="4">' '<tr><td colspan="4">'
# <em title="mode de calcul de la moyenne du module">règle de calcul standard</em>' # <em title="mode de calcul de la moyenne du module">règle de calcul standard</em>'
) )
# if sco_moduleimpl.can_change_ens(moduleimpl_id, raise_exc=False):
# H.append(
# f' (<a class="stdlink" href="edit_moduleimpl_expr?moduleimpl_id={moduleimpl_id}">changer</a>)'
# )
H.append("</td></tr>") H.append("</td></tr>")
H.append( H.append(
f"""<tr><td colspan="4"><span class="moduleimpl_abs_link"><a class="stdlink" f"""<tr><td colspan="4"><span class="moduleimpl_abs_link"><a class="stdlink"
@ -350,15 +329,17 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
current_user.has_permission(Permission.ScoAbsChange) current_user.has_permission(Permission.ScoAbsChange)
and formsemestre.est_courant() and formsemestre.est_courant()
): ):
datelundi = sco_abs.ddmmyyyy(time.strftime("%d/%m/%Y")).prev_monday()
group_id = sco_groups.get_default_group(formsemestre_id) group_id = sco_groups.get_default_group(formsemestre_id)
H.append( H.append(
f""" f"""
<span class="moduleimpl_abs_link"><a class="stdlink" <span class="moduleimpl_abs_link"><a class="stdlink" href="{
href="{url_for("absences.SignaleAbsenceGrHebdo", url_for("assiduites.signal_assiduites_group", scodoc_dept=g.scodoc_dept)
scodoc_dept=g.scodoc_dept,formsemestre_id=formsemestre_id, }?group_ids={group_id}&jour={
moduleimpl_id=moduleimpl_id, datelundi=datelundi, group_ids=group_id)}"> datetime.date.today().isoformat()
Saisie Absences hebdo.</a></span> }&formsemestre_id={formsemestre.id}
&moduleimpl_id={moduleimpl_id}
"
>Saisie Absences hebdo</a></span>
""" """
) )
@ -435,8 +416,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
top_table_links += f""" top_table_links += f"""
<a class="stdlink" style="margin-left:2em;" href="{ <a class="stdlink" style="margin-left:2em;" href="{
url_for("notes.moduleimpl_evaluation_renumber", url_for("notes.moduleimpl_evaluation_renumber",
scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id, scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
redirect=1)
}">Trier par date</a> }">Trier par date</a>
""" """
if nb_evaluations > 0: if nb_evaluations > 0:
@ -572,10 +552,8 @@ def _ligne_evaluation(
# visualisation des poids (Hinton map) # visualisation des poids (Hinton map)
H.append(_evaluation_poids_html(evaluation, max_poids)) H.append(_evaluation_poids_html(evaluation, max_poids))
H.append("""<div class="evaluation_titre">""") H.append("""<div class="evaluation_titre">""")
if evaluation.jour: if evaluation.date_debut:
H.append( H.append(evaluation.descr_date())
f"""Le {evaluation.jour.strftime("%d/%m/%Y")} {evaluation.descr_heure()}"""
)
else: else:
H.append( H.append(
f"""<a href="{url_for("notes.evaluation_edit", f"""<a href="{url_for("notes.evaluation_edit",
@ -719,7 +697,7 @@ def _ligne_evaluation(
if can_edit_notes: if can_edit_notes:
H.append( H.append(
moduleimpl_evaluation_menu( moduleimpl_evaluation_menu(
evaluation.id, evaluation,
nbnotes=etat["nb_notes"], nbnotes=etat["nb_notes"],
) )
) )

View File

@ -54,34 +54,6 @@ def can_edit_notes(authuser, moduleimpl_id, allow_ens=True):
return True return True
def can_edit_evaluation(moduleimpl_id=None):
"""Vérifie que l'on a le droit de modifier, créer ou détruire une
évaluation dans ce module.
Sinon, lance une exception.
(nb: n'implique pas le droit de saisir ou modifier des notes)
"""
from app.scodoc import sco_formsemestre
# acces pour resp. moduleimpl et resp. form semestre (dir etud)
if moduleimpl_id is None:
raise ValueError("no moduleimpl specified") # bug
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
if (
current_user.has_permission(Permission.ScoEditAllEvals)
or current_user.id == M["responsable_id"]
or current_user.id in sem["responsables"]
):
return True
elif sem["ens_can_edit_eval"]:
for ens in M["ens"]:
if ens["ens_id"] == current_user.id:
return True
return False
def can_suppress_annotation(annotation_id): def can_suppress_annotation(annotation_id):
"""True if current user can suppress this annotation """True if current user can suppress this annotation
Seuls l'auteur de l'annotation et le chef de dept peuvent supprimer Seuls l'auteur de l'annotation et le chef de dept peuvent supprimer

View File

@ -138,7 +138,7 @@ class PlacementForm(FlaskForm):
def set_evaluation_infos(self, evaluation_id): def set_evaluation_infos(self, evaluation_id):
"""Initialise les données du formulaire avec les données de l'évaluation.""" """Initialise les données du formulaire avec les données de l'évaluation."""
eval_data = sco_evaluation_db.do_evaluation_list( eval_data = sco_evaluation_db.get_evaluation_dict(
{"evaluation_id": evaluation_id} {"evaluation_id": evaluation_id}
) )
if not eval_data: if not eval_data:
@ -239,7 +239,7 @@ class PlacementRunner:
self.groups_ids = [ self.groups_ids = [
gid if gid != TOUS else form.tous_id for gid in form["groups"].data gid if gid != TOUS else form.tous_id for gid in form["groups"].data
] ]
self.eval_data = sco_evaluation_db.do_evaluation_list( self.eval_data = sco_evaluation_db.get_evaluation_dict(
{"evaluation_id": self.evaluation_id} {"evaluation_id": self.evaluation_id}
)[0] )[0]
self.groups = sco_groups.listgroups(self.groups_ids) self.groups = sco_groups.listgroups(self.groups_ids)

View File

@ -45,7 +45,6 @@ from app.models import (
FormSemestre, FormSemestre,
Module, Module,
ModuleImpl, ModuleImpl,
NotesNotes,
ScolarNews, ScolarNews,
) )
from app.models.etudiants import Identite from app.models.etudiants import Identite
@ -54,16 +53,13 @@ from app.scodoc.sco_exceptions import (
AccessDenied, AccessDenied,
InvalidNoteValue, InvalidNoteValue,
NoteProcessError, NoteProcessError,
ScoBugCatcher,
ScoException, ScoException,
ScoInvalidParamError, ScoInvalidParamError,
ScoValueError, ScoValueError,
) )
from app.scodoc import html_sco_header, sco_users from app.scodoc import html_sco_header, sco_users
from app.scodoc import htmlutils from app.scodoc import htmlutils
from app.scodoc import sco_abs
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc import sco_edit_module
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc import sco_evaluation_db from app.scodoc import sco_evaluation_db
from app.scodoc import sco_evaluations from app.scodoc import sco_evaluations
@ -71,7 +67,6 @@ from app.scodoc import sco_excel
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_groups_view from app.scodoc import sco_groups_view
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_permissions_check from app.scodoc import sco_permissions_check
from app.scodoc import sco_undo_notes from app.scodoc import sco_undo_notes
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
@ -502,15 +497,16 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
""" """
] ]
# news # news
ScolarNews.add( if nb_suppress:
typ=ScolarNews.NEWS_NOTE, ScolarNews.add(
obj=evaluation.moduleimpl.id, typ=ScolarNews.NEWS_NOTE,
text=f"""Suppression des notes d'une évaluation dans obj=evaluation.moduleimpl.id,
<a class="stdlink" href="{status_url}" text=f"""Suppression des notes d'une évaluation dans
>{evaluation.moduleimpl.module.titre or 'module sans titre'}</a> <a class="stdlink" href="{status_url}"
""", >{evaluation.moduleimpl.module.titre or 'module sans titre'}</a>
url=status_url, """,
) url=status_url,
)
return html_sco_header.sco_header() + "\n".join(H) + html_sco_header.sco_footer() 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 modimpl = evaluation.moduleimpl
formsemestre = modimpl.formsemestre formsemestre = modimpl.formsemestre
mod_responsable = sco_users.user_info(modimpl.responsable_id) mod_responsable = sco_users.user_info(modimpl.responsable_id)
if evaluation.jour: if evaluation.date_debut:
indication_date = evaluation.jour.isoformat() indication_date = evaluation.date_debut.date().isoformat()
else: else:
indication_date = scu.sanitize_filename(evaluation.description or "")[:12] indication_date = scu.sanitize_filename(evaluation.description or "")[:12]
eval_name = f"{evaluation.moduleimpl.module.code}-{indication_date}" eval_name = f"{evaluation.moduleimpl.module.code}-{indication_date}"
date_str = ( date_str = (
f"""du {evaluation.jour.strftime("%d/%m/%Y")}""" f"""du {evaluation.date_debut.strftime("%d/%m/%Y")}"""
if evaluation.jour if evaluation.date_debut
else "(sans date)" else "(sans date)"
) )
eval_titre = f"""{evaluation.description if evaluation.description else "évaluation"} {date_str}""" eval_titre = f"""{evaluation.description if evaluation.description else "évaluation"} {date_str}"""
@ -1107,19 +1103,21 @@ def _get_sorted_etuds(evaluation: Evaluation, etudids: list, formsemestre_id: in
e["groups"] = sco_groups.get_etud_groups(etudid, formsemestre_id) e["groups"] = sco_groups.get_etud_groups(etudid, formsemestre_id)
# Information sur absence (tenant compte de la demi-journée) # Information sur absence (tenant compte de la demi-journée)
jour_iso = evaluation.jour.isoformat() if evaluation.jour else "" jour_iso = (
evaluation.date_debut.date().isoformat() if evaluation.date_debut else ""
)
warn_abs_lst = [] warn_abs_lst = []
if evaluation.is_matin(): if evaluation.is_matin():
nbabs = sco_abs.count_abs(etudid, jour_iso, jour_iso, matin=True) nbabs = 0 # TODO-ASSIDUITE sco_abs.count_abs(etudid, jour_iso, jour_iso, matin=True)
nbabsjust = sco_abs.count_abs_just(etudid, jour_iso, jour_iso, matin=True) nbabsjust = 0 # TODO-ASSIDUITE sco_abs.count_abs_just(etudid, jour_iso, jour_iso, matin=True)
if nbabs: if nbabs:
if nbabsjust: if nbabsjust:
warn_abs_lst.append("absent justifié le matin !") warn_abs_lst.append("absent justifié le matin !")
else: else:
warn_abs_lst.append("absent le matin !") warn_abs_lst.append("absent le matin !")
if evaluation.is_apresmidi(): if evaluation.is_apresmidi():
nbabs = sco_abs.count_abs(etudid, jour_iso, jour_iso, matin=0) nbabs = 0 # TODO-ASSIDUITE sco_abs.count_abs(etudid, jour_iso, jour_iso, matin=0)
nbabsjust = sco_abs.count_abs_just(etudid, jour_iso, jour_iso, matin=0) nbabsjust = 0 # TODO-ASSIDUITE sco_abs.count_abs_just(etudid, jour_iso, jour_iso, matin=0)
if nbabs: if nbabs:
if nbabsjust: if nbabsjust:
warn_abs_lst.append("absent justifié l'après-midi !") warn_abs_lst.append("absent justifié l'après-midi !")

View File

@ -41,7 +41,7 @@ from reportlab.lib.units import cm
from reportlab.platypus import KeepInFrame, Paragraph, Table, TableStyle from reportlab.platypus import KeepInFrame, Paragraph, Table, TableStyle
from reportlab.platypus.doctemplate import BaseDocTemplate from reportlab.platypus.doctemplate import BaseDocTemplate
from app.scodoc import sco_abs from app.scodoc import sco_cal
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc.sco_exceptions import ScoPDFFormatError from app.scodoc.sco_exceptions import ScoPDFFormatError
from app.scodoc import sco_groups from app.scodoc import sco_groups
@ -299,9 +299,9 @@ def pdf_feuille_releve_absences(
NB_CELL_PM = sco_preferences.get_preference("feuille_releve_abs_PM") NB_CELL_PM = sco_preferences.get_preference("feuille_releve_abs_PM")
col_width = 0.85 * cm col_width = 0.85 * cm
if sco_preferences.get_preference("feuille_releve_abs_samedi"): if sco_preferences.get_preference("feuille_releve_abs_samedi"):
days = sco_abs.DAYNAMES[:6] # Lundi, ..., Samedi days = sco_cal.DAYNAMES[:6] # Lundi, ..., Samedi
else: else:
days = sco_abs.DAYNAMES[:5] # Lundi, ..., Vendredi days = sco_cal.DAYNAMES[:5] # Lundi, ..., Vendredi
nb_days = len(days) nb_days = len(days)
# Informations sur les groupes à afficher: # Informations sur les groupes à afficher:

View File

@ -60,7 +60,7 @@ from app.models.formsemestre import FormSemestre
from app import db, log from app import db, log
from app.models import UniteEns from app.models import Evaluation, ModuleImpl, UniteEns
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
from app.scodoc import sco_edit_matiere from app.scodoc import sco_edit_matiere
@ -154,6 +154,7 @@ def external_ue_inscrit_et_note(
"""Inscrit les étudiants au moduleimpl, crée au besoin une évaluation """Inscrit les étudiants au moduleimpl, crée au besoin une évaluation
et enregistre les notes. et enregistre les notes.
""" """
moduleimpl: ModuleImpl = db.session.get(ModuleImpl, moduleimpl_id)
log( log(
f"external_ue_inscrit_et_note(moduleimpl_id={moduleimpl_id}, notes_etuds={notes_etuds})" f"external_ue_inscrit_et_note(moduleimpl_id={moduleimpl_id}, notes_etuds={notes_etuds})"
) )
@ -163,18 +164,14 @@ def external_ue_inscrit_et_note(
formsemestre_id, formsemestre_id,
list(notes_etuds.keys()), list(notes_etuds.keys()),
) )
# Création d'une évaluation si il n'y en a pas déjà: # Création d'une évaluation si il n'y en a pas déjà:
mod_evals = sco_evaluation_db.do_evaluation_list( if moduleimpl.evaluations.count() > 0:
args={"moduleimpl_id": moduleimpl_id}
)
if len(mod_evals):
# met la note dans le première évaluation existante: # met la note dans le première évaluation existante:
evaluation_id = mod_evals[0]["evaluation_id"] evaluation: Evaluation = moduleimpl.evaluations.first()
else: else:
# crée une évaluation: # crée une évaluation:
evaluation_id = sco_evaluation_db.do_evaluation_create( evaluation: Evaluation = Evaluation.create(
moduleimpl_id=moduleimpl_id, moduleimpl=moduleimpl,
note_max=20.0, note_max=20.0,
coefficient=1.0, coefficient=1.0,
publish_incomplete=True, publish_incomplete=True,
@ -185,7 +182,7 @@ def external_ue_inscrit_et_note(
# Saisie des notes # Saisie des notes
_, _, _ = sco_saisie_notes.notes_add( _, _, _ = sco_saisie_notes.notes_add(
current_user, current_user,
evaluation_id, evaluation.id,
list(notes_etuds.items()), list(notes_etuds.items()),
do_it=True, do_it=True,
) )

View File

@ -149,7 +149,7 @@ def list_operations(evaluation_id):
def evaluation_list_operations(evaluation_id): def evaluation_list_operations(evaluation_id):
"""Page listing operations on evaluation""" """Page listing operations on evaluation"""
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0] E = sco_evaluation_db.get_evaluation_dict({"evaluation_id": evaluation_id})[0]
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
Ops = list_operations(evaluation_id) Ops = list_operations(evaluation_id)
@ -179,7 +179,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"):
""" """
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
rows = ndb.SimpleDictFetch( rows = ndb.SimpleDictFetch(
"""SELECT i.nom, i.prenom, code_nip, n.*, mod.titre, e.description, e.jour, """SELECT i.nom, i.prenom, code_nip, n.*, mod.titre, e.description, e.date_debut,
u.user_name, e.id as evaluation_id u.user_name, e.id as evaluation_id
FROM notes_notes n, notes_evaluation e, notes_moduleimpl mi, FROM notes_notes n, notes_evaluation e, notes_moduleimpl mi,
notes_modules mod, identite i, "user" u notes_modules mod, identite i, "user" u
@ -197,6 +197,12 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"):
keep_numeric = format in scu.FORMATS_NUMERIQUES keep_numeric = format in scu.FORMATS_NUMERIQUES
for row in rows: for row in rows:
row["value"] = scu.fmt_note(row["value"], keep_numeric=keep_numeric) row["value"] = scu.fmt_note(row["value"], keep_numeric=keep_numeric)
row["date_evaluation"] = (
row["date_debut"].strftime("%d/%m/%Y %H:%M") if row["date_debut"] else ""
)
row["_date_evaluation_order"] = (
row["date_debut"].isoformat() if row["date_debut"] else ""
)
columns_ids = ( columns_ids = (
"date", "date",
"code_nip", "code_nip",
@ -207,7 +213,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"):
"titre", "titre",
"evaluation_id", "evaluation_id",
"description", "description",
"jour", "date_evaluation",
"comment", "comment",
) )
titles = { titles = {
@ -221,7 +227,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"):
"evaluation_id": "evaluation_id", "evaluation_id": "evaluation_id",
"titre": "Module", "titre": "Module",
"description": "Evaluation", "description": "Evaluation",
"jour": "Date éval.", "date_evaluation": "Date éval.",
} }
tab = GenTable( tab = GenTable(
titles=titles, titles=titles,

View File

@ -68,10 +68,15 @@ from app.scodoc.codes_cursus import NOTES_TOLERANCE, CODES_EXPL
from app.scodoc import sco_xml from app.scodoc import sco_xml
import sco_version import sco_version
# En principe, aucun champ text ne devrait excéder cette taille
MAX_TEXT_LEN = 64 * 1024
# le répertoire static, lié à chaque release pour éviter les problèmes de caches # le répertoire static, lié à chaque release pour éviter les problèmes de caches
STATIC_DIR = ( STATIC_DIR = (
os.environ.get("SCRIPT_NAME", "") + "/ScoDoc/static/links/" + sco_version.SCOVERSION os.environ.get("SCRIPT_NAME", "") + "/ScoDoc/static/links/" + sco_version.SCOVERSION
) )
# La time zone du serveur:
TIME_ZONE = timezone("/".join(os.path.realpath("/etc/localtime").split("/")[-2:]))
# ----- CALCUL ET PRESENTATION DES NOTES # ----- CALCUL ET PRESENTATION DES NOTES
NOTES_PRECISION = 1e-4 # evite eventuelles erreurs d'arrondis NOTES_PRECISION = 1e-4 # evite eventuelles erreurs d'arrondis
@ -260,7 +265,7 @@ class AssiduitesMetrics:
"""Labels associés au métrique de l'assiduité""" """Labels associés au métrique de l'assiduité"""
SHORT: list[str] = ["1/2 J.", "J.", "H."] SHORT: list[str] = ["1/2 J.", "J.", "H."]
LONG: list[str] = ["Demi-Journée", "Journée", "Heure"] LONG: list[str] = ["Demi-journée", "Journée", "Heure"]
TAG: list[str] = ["demi", "journee", "heure"] TAG: list[str] = ["demi", "journee", "heure"]

View File

@ -79,7 +79,7 @@ div.competence {
padding-left: calc(var(--arrow-width) + 8px); padding-left: calc(var(--arrow-width) + 8px);
} }
.niveaux>div:not(:last-child)::after { .niveaux>div:not(:last-child)::before {
content: ""; content: "";
position: absolute; position: absolute;

View File

@ -1315,7 +1315,7 @@ a.smallbutton {
} }
span.evallink { span.evallink {
font-size: 80%; margin-left: 16px;
font-weight: normal; font-weight: normal;
} }
@ -2472,6 +2472,12 @@ span.ue_type {
margin-right: 1.5em; margin-right: 1.5em;
} }
table.formsemestre_description td.ue_coef_nul {
background-color: yellow!important;
color: red;
font-weight: bold;
}
ul.notes_module_list span.ue_coefs_list { ul.notes_module_list span.ue_coefs_list {
color: blue; color: blue;
font-size: 70%; font-size: 70%;

View File

@ -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;
}

View File

@ -385,7 +385,9 @@ class TableRecap(tb.Table):
first_eval_of_mod = True first_eval_of_mod = True
for e in evals: for e in evals:
col_id = f"eval_{e.id}" 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 = [] col_classes = []
if first_eval: if first_eval:
col_classes.append("first") col_classes.append("first")

View File

@ -12,7 +12,8 @@
<p>ScoDoc est un logiciel libre écrit en <p>ScoDoc est un logiciel libre écrit en
<a href="http://www.python.org" target="_blank" rel="noopener noreferrer">Python</a>. <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>
<p>Le logiciel est distribué sous <p>Le logiciel est distribué sous

View File

@ -1,5 +1,6 @@
<h2>Présence lors de l'évaluation {{eval.title}} </h2> <h2>Présence du groupe {{group_title}} le {{date_debut.strftime("%d/%m/%Y")}}
<h3>Réalisé le {{eval.jour}} de {{eval.heure_debut}} à {{eval.heure_fin}}</h3> de {{date_debut.strftime("%H:%M")}} à {{date_fin.strftime("%H:%M")}}
</h2>
<table> <table>
<thead> <thead>
<tr> <tr>
@ -7,7 +8,7 @@
Nom Nom
</th> </th>
<th> <th>
Assiduité Présence
</th> </th>
</tr> </tr>
</thead> </thead>

View File

@ -57,7 +57,7 @@
</div> </div>
{% endfor %} {% endfor %}
<div class="link"> <div class="link">
<a class="stdlink" target="_blank" href="{{ <a class="stdlink" target="_blank" rel="noopener noreferrer" href="{{
url_for('notes.refcomp_show', url_for('notes.refcomp_show',
scodoc_dept=g.scodoc_dept, refcomp_id=formation.referentiel_competence.id ) scodoc_dept=g.scodoc_dept, refcomp_id=formation.referentiel_competence.id )
}}">référentiel de compétences</a> }}">référentiel de compétences</a>

View File

@ -195,8 +195,9 @@
Code postal : {{ entreprise.codepostal }}<br> Code postal : {{ entreprise.codepostal }}<br>
Ville : {{ entreprise.ville }}<br> Ville : {{ entreprise.ville }}<br>
Pays : {{ entreprise.pays }}<br> Pays : {{ entreprise.pays }}<br>
<a href="{{ url_for('entreprises.fiche_entreprise', entreprise_id=entreprise.id) }}" target="_blank">Fiche <a href="{{ url_for('entreprises.fiche_entreprise', entreprise_id=entreprise.id) }}"
entreprise</a> rel="noopener noreferrer" target="_blank"
>Fiche entreprise</a>
</div> </div>
{% for site in entreprise.sites %} {% for site in entreprise.sites %}
<div class="site"> <div class="site">
@ -221,8 +222,9 @@
Code postal : {{ site.codepostal }}<br> Code postal : {{ site.codepostal }}<br>
Ville : {{ site.ville }}<br> Ville : {{ site.ville }}<br>
Pays : {{ site.pays }}<br> Pays : {{ site.pays }}<br>
<a href="{{ url_for('entreprises.fiche_entreprise', entreprise_id=site.entreprise_id) }}" target="_blank">Fiche <a href="{{ url_for('entreprises.fiche_entreprise', entreprise_id=site.entreprise_id)
entreprise</a> }}" rel="noopener noreferrer" target="_blank"
>Fiche entreprise</a>
</div> </div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
@ -255,7 +257,7 @@
Notes : {{ correspondant.notes }}<br> Notes : {{ correspondant.notes }}<br>
{% endif %} {% endif %}
<a href="{{ url_for('entreprises.fiche_entreprise', entreprise_id=correspondant.site.entreprise.id) }}" <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>
</div> </div>
{% endfor %} {% endfor %}

View File

@ -22,7 +22,7 @@
type, et de saisir les coefficients pondérant l'influence de la type, et de saisir les coefficients pondérant l'influence de la
ressource ou SAÉ vers les Unités d'Enseignement (UE). ressource ou SAÉ vers les Unités d'Enseignement (UE).
Voir les détails sur 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> </p>
{%endif%} {%endif%}
</div> </div>

View File

@ -468,7 +468,8 @@
if (to[0] != "n") { if (to[0] != "n") {
groupeSelected.closest(".grpPartitions").querySelector(`[value="${to}"]`).click(); groupeSelected.closest(".grpPartitions").querySelector(`[value="${to}"]`).click();
} else { } else {
groupeSelected.closest(".grpPartitions").querySelector(`[value="aucun"]`).click(); let toNumber = to.split("-")[1];
groupeSelected.closest(".grpPartitions").querySelector(`[data-idpartition="${toNumber}"] [value="aucun"]`).click();
} }
}) })

View File

@ -86,7 +86,8 @@
<div class="sidebar-bottom"><a href="{{ url_for( 'scodoc.about', <div class="sidebar-bottom"><a href="{{ url_for( 'scodoc.about',
scodoc_dept=g.scodoc_dept ) }}" class="sidebar">À propos</a> scodoc_dept=g.scodoc_dept ) }}" class="sidebar">À propos</a>
<br /> <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> </div>
<div class="logo-logo"> <div class="logo-logo">

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,8 @@
import datetime import datetime
from flask import g, request, render_template from flask import g, request, render_template
from flask import abort, url_for from flask import abort, url_for
from flask_login import current_user
from app import db from app import db
from app.comp import res_sem from app.comp import res_sem
@ -25,14 +25,16 @@ from app.views import ScoData
# --------------- # ---------------
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import safehtml
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_groups_view from app.scodoc import sco_groups_view
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc import sco_find_etud from app.scodoc import sco_find_etud
from flask_login import current_user
from app.scodoc import sco_utils as scu
from app.scodoc import sco_assiduites as scass from app.scodoc import sco_assiduites as scass
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import ScoValueError
from app.tables.visu_assiduites import TableAssi, etuds_sorted_from_ids from app.tables.visu_assiduites import TableAssi, etuds_sorted_from_ids
@ -547,7 +549,6 @@ def signal_assiduites_group():
+ [ + [
# Voir fonctionnement JS # Voir fonctionnement JS
"js/etud_info.js", "js/etud_info.js",
"js/abs_ajax.js",
"js/groups_view.js", "js/groups_view.js",
"js/assiduites.js", "js/assiduites.js",
"libjs/moment.new.min.js", "libjs/moment.new.min.js",
@ -732,20 +733,23 @@ def visu_assiduites_group():
).build() ).build()
@bp.route("/EtatAbsencesDate") @bp.route("/etat_abs_date")
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def get_etat_abs_date(): def etat_abs_date():
evaluation = { """date_debut, date_fin en ISO"""
"jour": request.args.get("jour"), date_debut_str = request.args.get("date_debut")
"heure_debut": request.args.get("heure_debut"), date_fin_str = request.args.get("date_fin")
"heure_fin": request.args.get("heure_fin"), title = request.args.get("desc")
"title": request.args.get("desc"),
}
date: str = evaluation["jour"]
group_ids: list[int] = request.args.get("group_ids", None) group_ids: list[int] = request.args.get("group_ids", None)
etudiants: list[dict] = [] try:
date_debut = datetime.datetime.fromisoformat(date_debut_str)
except ValueError as exc:
raise ScoValueError("date_debut invalide") from exc
try:
date_fin = datetime.datetime.fromisoformat(date_fin_str)
except ValueError as exc:
raise ScoValueError("date_fin invalide") from exc
if group_ids is None: if group_ids is None:
group_ids = [] group_ids = []
else: else:
@ -758,14 +762,6 @@ def get_etat_abs_date():
sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0] sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
for m in groups_infos.members for m in groups_infos.members
] ]
date_debut = scu.is_iso_formated(
f"{evaluation['jour']}T{evaluation['heure_debut'].replace('h',':')}", True
)
date_fin = scu.is_iso_formated(
f"{evaluation['jour']}T{evaluation['heure_fin'].replace('h',':')}", True
)
assiduites: Assiduite = Assiduite.query.filter( assiduites: Assiduite = Assiduite.query.filter(
Assiduite.etudid.in_([e["etudid"] for e in etuds]) Assiduite.etudid.in_([e["etudid"] for e in etuds])
) )
@ -773,15 +769,20 @@ def get_etat_abs_date():
assiduites, Assiduite, date_debut, date_fin, False assiduites, Assiduite, date_debut, date_fin, False
) )
etudiants: list[dict] = []
for etud in etuds: for etud in etuds:
assi = assiduites.filter_by(etudid=etud["etudid"]).first() assi = assiduites.filter_by(etudid=etud["etudid"]).first()
etat = "" 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 etat = scu.EtatAssiduite.inverse().get(assi.etat).name
etudiant = { 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, "etat": etat,
} }
@ -790,7 +791,7 @@ def get_etat_abs_date():
etudiants = list(sorted(etudiants, key=lambda x: x["nom"])) etudiants = list(sorted(etudiants, key=lambda x: x["nom"]))
header: str = html_sco_header.sco_header( header: str = html_sco_header.sco_header(
page_title=evaluation["title"], page_title=safehtml.html_to_safe_html(title),
init_qtip=True, init_qtip=True,
) )
@ -799,7 +800,9 @@ def get_etat_abs_date():
render_template( render_template(
"assiduites/pages/etat_absence_date.j2", "assiduites/pages/etat_absence_date.j2",
etudiants=etudiants, etudiants=etudiants,
eval=evaluation, group_title=groups_infos.groups_titles,
date_debut=date_debut,
date_fin=date_fin,
), ),
html_sco_header.sco_footer(), html_sco_header.sco_footer(),
).build() ).build()
@ -1019,7 +1022,7 @@ def _module_selector(
for ue in ues: for ue in ues:
modimpls_list += ntc.get_modimpls_dict(ue_id=ue["ue_id"]) modimpls_list += ntc.get_modimpls_dict(ue_id=ue["ue_id"])
selected = moduleimpl_id is not None selected = "" if moduleimpl_id is not None else "selected"
modules = [] modules = []
@ -1032,7 +1035,10 @@ def _module_selector(
modules.append({"moduleimpl_id": modimpl["moduleimpl_id"], "name": modname}) modules.append({"moduleimpl_id": modimpl["moduleimpl_id"], "name": modname})
return render_template( return render_template(
"assiduites/widgets/moduleimpl_selector.j2", selected=selected, modules=modules "assiduites/widgets/moduleimpl_selector.j2",
selected=selected,
modules=modules,
moduleimpl_id=moduleimpl_id,
) )

View File

@ -57,8 +57,8 @@ from app.but.forms import jury_but_forms
from app.comp import jury, res_sem from app.comp import jury, res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models import ( from app.models import (
Evaluation,
Formation, Formation,
ScolarFormSemestreValidation,
ScolarAutorisationInscription, ScolarAutorisationInscription,
ScolarNews, ScolarNews,
Scolog, Scolog,
@ -97,9 +97,9 @@ from app.scodoc.sco_exceptions import (
) )
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.pe import pe_view from app.pe import pe_view
from app.scodoc import sco_abs
from app.scodoc import sco_apogee_compare from app.scodoc import sco_apogee_compare
from app.scodoc import sco_archives from app.scodoc import sco_archives
from app.scodoc import sco_assiduites
from app.scodoc import sco_bulletins from app.scodoc import sco_bulletins
from app.scodoc import sco_bulletins_pdf from app.scodoc import sco_bulletins_pdf
from app.scodoc import sco_cache from app.scodoc import sco_cache
@ -134,6 +134,7 @@ from app.scodoc import sco_lycee
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
from app.scodoc import sco_moduleimpl_inscriptions from app.scodoc import sco_moduleimpl_inscriptions
from app.scodoc import sco_moduleimpl_status 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_placement
from app.scodoc import sco_poursuite_dut from app.scodoc import sco_poursuite_dut
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
@ -378,11 +379,40 @@ sco_publish(
sco_evaluations.formsemestre_evaluations_delai_correction, sco_evaluations.formsemestre_evaluations_delai_correction,
Permission.ScoView, Permission.ScoView,
) )
sco_publish(
"/moduleimpl_evaluation_renumber",
sco_evaluation_db.moduleimpl_evaluation_renumber, @bp.route("/moduleimpl_evaluation_renumber", methods=["GET", "POST"])
Permission.ScoView, @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( sco_publish(
"/moduleimpl_evaluation_move", "/moduleimpl_evaluation_move",
sco_evaluation_db.moduleimpl_evaluation_move, sco_evaluation_db.moduleimpl_evaluation_move,
@ -1122,175 +1152,51 @@ 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") @bp.route("/view_module_abs")
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@scodoc7func @scodoc7func
def view_module_abs(moduleimpl_id, format="html"): def view_module_abs(moduleimpl_id, fmt="html"):
"""Visualisation des absences a un module""" """Visualisation des absences a un module"""
M = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[0] modimpl: ModuleImpl = (
sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"]) ModuleImpl.query.filter_by(id=moduleimpl_id)
debut_sem = ndb.DateDMYtoISO(sem["date_debut"]) .join(FormSemestre)
fin_sem = ndb.DateDMYtoISO(sem["date_fin"]) .filter_by(dept_id=g.scodoc_dept_id)
list_insc = sco_moduleimpl.moduleimpl_listeetuds(moduleimpl_id) ).first_or_404()
T = [] debut_sem = modimpl.formsemestre.date_debut
for etudid in list_insc: fin_sem = modimpl.formsemestre.date_fin
nb_abs = sco_abs.count_abs( inscrits: list[Identite] = sorted(
etudid=etudid, [i.etud for i in modimpl.inscriptions], key=lambda e: e.sort_key
debut=debut_sem, )
fin=fin_sem,
moduleimpl_id=moduleimpl_id, rows = []
for etud in inscrits:
nb_abs, nb_abs_just = sco_assiduites.formsemestre_get_assiduites_count(
etud.id, modimpl.formsemestre, cache=False, moduleimpl_id=modimpl.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 = [ H = [
html_sco_header.html_sem_header( html_sco_header.html_sem_header(
'Absences du <a href="moduleimpl_status?moduleimpl_id=%s">module %s</a>' f"""Absences du <a href="{
% (moduleimpl_id, M["module"]["titre"]), url_for("notes.moduleimpl_status",
page_title="Absences du module %s" % (M["module"]["titre"]), 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 ( return (
"\n".join(H) "\n".join(H)
+ "<p>Aucune absence signalée</p>" + "<p>Aucune absence signalée</p>"
@ -1305,16 +1211,16 @@ def view_module_abs(moduleimpl_id, format="html"):
"total": "Total", "total": "Total",
}, },
columns_ids=("nomprenom", "just", "nojust", "total"), columns_ids=("nomprenom", "just", "nojust", "total"),
rows=T, rows=rows,
html_class="table_leftalign", html_class="table_leftalign",
base_url="%s?moduleimpl_id=%s" % (request.base_url, moduleimpl_id), base_url=f"{request.base_url}?moduleimpl_id={moduleimpl_id}",
filename="absmodule_" + scu.make_filename(M["module"]["titre"]), filename="absmodule_" + scu.make_filename(modimpl.module.titre_str()),
caption="Absences dans le module %s" % M["module"]["titre"], caption=f"Absences dans le module {modimpl.module.titre_str()}",
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
) )
if format != "html": if fmt != "html":
return tab.make_page(format=format) return tab.make_page(format=fmt)
return "\n".join(H) + tab.html() + html_sco_header.sco_footer() return "\n".join(H) + tab.html() + html_sco_header.sco_footer()
@ -1735,30 +1641,37 @@ sco_publish(
@scodoc7func @scodoc7func
def evaluation_delete(evaluation_id): def evaluation_delete(evaluation_id):
"""Form delete evaluation""" """Form delete evaluation"""
El = sco_evaluation_db.do_evaluation_list(args={"evaluation_id": evaluation_id}) evaluation: Evaluation = (
if not El: Evaluation.query.filter_by(id=evaluation_id)
raise ScoValueError("Evaluation inexistante ! (%s)" % evaluation_id) .join(ModuleImpl)
E = El[0] .join(FormSemestre)
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] .filter_by(dept_id=g.scodoc_dept_id)
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] .first_or_404()
tit = "Suppression de l'évaluation %(description)s (%(jour)s)" % E )
etat = sco_evaluations.do_evaluation_etat(evaluation_id)
tit = f"""Suppression de l'évaluation {evaluation.description or ""} ({evaluation.descr_date()})"""
etat = sco_evaluations.do_evaluation_etat(evaluation.id)
H = [ H = [
html_sco_header.html_sem_header(tit, with_h2=False), f"""
"""<h2 class="formsemestre">Module <tt>%(code)s</tt> %(titre)s</h2>""" % Mod, {html_sco_header.html_sem_header(tit, with_h2=False)}
"""<h3>%s</h3>""" % tit, <h2 class="formsemestre">Module <tt>{evaluation.moduleimpl.module.code}</tt>
"""<p class="help">Opération <span class="redboldtext">irréversible</span>. Si vous supprimez l'évaluation, vous ne pourrez pas retrouver les notes associées.</p>""", {evaluation.moduleimpl.module.titre_str()}</h2>
<h3>{tit}</h3>
<p class="help">Opération <span class="redboldtext">irréversible</span>.
Si vous supprimez l'évaluation, vous ne pourrez pas retrouver les notes associées.
</p>
""",
] ]
warning = False warning = False
if etat["nb_notes_total"]: if etat["nb_notes_total"]:
warning = True warning = True
nb_desinscrits = etat["nb_notes_total"] - etat["nb_notes"] nb_desinscrits = etat["nb_notes_total"] - etat["nb_notes"]
H.append( H.append(
"""<div class="ue_warning"><span>Il y a %s notes""" % etat["nb_notes_total"] f"""<div class="ue_warning"><span>Il y a {etat["nb_notes_total"]} notes"""
) )
if nb_desinscrits: if nb_desinscrits:
H.append( H.append(
""" (dont %s d'étudiants qui ne sont plus inscrits)""" % nb_desinscrits """ (dont {nb_desinscrits} d'étudiants qui ne sont plus inscrits)"""
) )
H.append(""" dans l'évaluation</span>""") H.append(""" dans l'évaluation</span>""")
if etat["nb_notes"] == 0: if etat["nb_notes"] == 0:
@ -1768,8 +1681,13 @@ def evaluation_delete(evaluation_id):
if etat["nb_notes"]: if etat["nb_notes"]:
H.append( H.append(
"""<p>Suppression impossible (effacer les notes d'abord)</p><p><a class="stdlink" href="moduleimpl_status?moduleimpl_id=%s">retour au tableau de bord du module</a></p></div>""" f"""<p>Suppression impossible (effacer les notes d'abord)</p>
% E["moduleimpl_id"] <p><a class="stdlink" href="{
url_for("notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept, moduleimpl_id=evaluation.moduleimpl_id)
}">retour au tableau de bord du module</a>
</p>
</div>"""
) )
return "\n".join(H) + html_sco_header.sco_footer() return "\n".join(H) + html_sco_header.sco_footer()
if warning: if warning:
@ -1779,7 +1697,7 @@ def evaluation_delete(evaluation_id):
request.base_url, request.base_url,
scu.get_request_args(), scu.get_request_args(),
(("evaluation_id", {"input_type": "hidden"}),), (("evaluation_id", {"input_type": "hidden"}),),
initvalues=E, initvalues={"evaluation_id": evaluation.id},
submitlabel="Confirmer la suppression", submitlabel="Confirmer la suppression",
cancelbutton="Annuler", cancelbutton="Annuler",
) )
@ -1790,17 +1708,17 @@ def evaluation_delete(evaluation_id):
url_for( url_for(
"notes.moduleimpl_status", "notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
moduleimpl_id=E["moduleimpl_id"], moduleimpl_id=evaluation.moduleimpl_id,
) )
) )
else: else:
sco_evaluation_db.do_evaluation_delete(E["evaluation_id"]) evaluation.delete()
return ( return (
"\n".join(H) "\n".join(H)
+ f"""<p>OK, évaluation supprimée.</p> + f"""<p>OK, évaluation supprimée.</p>
<p><a class="stdlink" href="{ <p><a class="stdlink" href="{
url_for("notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, url_for("notes.moduleimpl_status", scodoc_dept=g.scodoc_dept,
moduleimpl_id=E["moduleimpl_id"]) moduleimpl_id=evaluation.moduleimpl_id)
}">Continuer</a></p>""" }">Continuer</a></p>"""
+ html_sco_header.sco_footer() + html_sco_header.sco_footer()
) )

View 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")

View File

@ -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

View File

@ -1,7 +1,7 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.6.9" SCOVERSION = "9.6.16"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"

View File

@ -5,7 +5,7 @@
""" """
import datetime
from pprint import pprint as pp from pprint import pprint as pp
import re import re
import sys import sys
@ -82,6 +82,7 @@ def make_shell_context():
"ctx": app.test_request_context(), "ctx": app.test_request_context(),
"current_app": flask.current_app, "current_app": flask.current_app,
"current_user": current_user, "current_user": current_user,
"datetime": datetime,
"Departement": Departement, "Departement": Departement,
"db": db, "db": db,
"Evaluation": Evaluation, "Evaluation": Evaluation,

View File

@ -315,13 +315,12 @@ pp(GET(f"/formsemestre/880/resultats", headers=HEADERS)[0])
# jour = sem["date_fin"] # jour = sem["date_fin"]
# evaluation_id = POST( # evaluation_id = POST(
# s, # s,
# "/Notes/do_evaluation_create", # f"/moduleimpl/{mod['moduleimpl_id']}/evaluation/create",
# data={ # data={
# "moduleimpl_id": mod["moduleimpl_id"],
# "coefficient": 1, # "coefficient": 1,
# "jour": jour, # "5/9/2019", # "jour": jour, # "2023-08-23",
# "heure_debut": "9h00", # "heure_debut": "9:00",
# "heure_fin": "10h00", # "heure_fin": "10:00",
# "note_max": 20, # notes sur 20 # "note_max": 20, # notes sur 20
# "description": "essai", # "description": "essai",
# }, # },

View File

@ -165,37 +165,3 @@ assert isinstance(json.loads(r.text)[0]["billet_id"], int)
# print(f"{len(inscrits)} inscrits dans ce module") # print(f"{len(inscrits)} inscrits dans ce module")
# # prend le premier inscrit, au hasard: # # prend le premier inscrit, au hasard:
# etudid = inscrits[0]["etudid"] # 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",
# },
# )

View File

@ -5,40 +5,51 @@
"""Construction des fichiers exemples pour la documentation. """Construction des fichiers exemples pour la documentation.
Usage: Usage:
cd /opt/scodoc/tests/api python tests/api/make_samples.py [entry_names]
python make_samples.py [entry_names] python tests/api/make_samples.py -i <filepath> [entrynames]
python 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` Si entry_names est spécifié, la génération est restreinte aux exemples cités.
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) Exemple:
cd /opt/scodoc/tests/api python make_samples departements departement-formsemestres
tools/create_database.sh --drop SCODOC_TEST_API && flask db upgrade &&flask sco-db-init --erase && flask init-test-database
Créer éventuellement un fichier `.env` dans /opt/scodoc/tests/api Doit être exécutée immédiatement apres une initialisation de la base pour test API!
avec la config du client API: (car dépendant des identifiants générés lors de la création des objets)
```
SCODOC_URL = "http://localhost:5000/" 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) Cet utilitaire prend en argument le fichier de nom `samples.csv` contenant la description
* 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 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 * l'url utilisée
* la permission nécessaire (par défaut ScoView) * la permission nécessaire (par défaut ScoView)
* la méthode GET,POST à utiliser (si commence par #, la ligne est ignorée) * 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 * les arguments éventuel (en cas de POST): une chaîne de caractère selon json
Implémentation: 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) Chacun des éléments du set est un exemple (Sample)
Quand la structure est complète, on génére tous les fichiers textes Quand la structure est complète, on génére tous les fichiers textes
- nom de l exemple - nom de l'exemple
- un ou plusieurs exemples avec pour chaucn - un ou plusieurs exemples avec pour chacun
- l url utilisée - l'url utilisée
- les arguments éventuels - les arguments éventuels
- le résultat - 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 os
import shutil import shutil
@ -50,7 +61,7 @@ from pprint import pprint as pp
import urllib3 import urllib3
import json import json
from pandas import read_csv import pandas as pd
from setup_test_api import ( from setup_test_api import (
API_PASSWORD, API_PASSWORD,
@ -68,6 +79,10 @@ DATA_DIR = "/tmp/samples/"
SAMPLES_FILENAME = "tests/ressources/samples/samples.csv" SAMPLES_FILENAME = "tests/ressources/samples/samples.csv"
class SampleException(Exception):
pass
class Sample: class Sample:
def __init__(self, url, method="GET", permission="ScoView", content=None): def __init__(self, url, method="GET", permission="ScoView", content=None):
self.content = content self.content = content
@ -83,7 +98,7 @@ class Sample:
elif permission == "ScoUsersAdmin": elif permission == "ScoUsersAdmin":
HEADERS = get_auth_headers("admin_api", "admin_api") HEADERS = get_auth_headers("admin_api", "admin_api")
else: else:
raise Exception(f"Bad permission : {permission}") raise SampleException(f"Bad permission : {permission}")
if self.method == "GET": if self.method == "GET":
self.result = GET(self.url, HEADERS) self.result = GET(self.url, HEADERS)
elif self.method == "POST": elif self.method == "POST":
@ -94,20 +109,19 @@ class Sample:
self.result = POST_JSON(self.url, json.loads(self.content), HEADERS) self.result = POST_JSON(self.url, json.loads(self.content), HEADERS)
elif self.method[0] != "#": elif self.method[0] != "#":
error = f'Bad method : "{self.method}"' error = f'Bad method : "{self.method}"'
raise Exception(error) raise SampleException(error)
self.shorten() self.shorten()
file = open(f"sample_TEST.json.md", "tw") with open("sample_TEST.json.md", "tw", encoding="utf-8") as f:
self.dump(file) self.dump(f)
file.close()
def _shorten( def _shorten(self, item):
self, item "Abrège les longues listes: limite à 2 éléments et affiche '...' etc. à la place"
): # abrege les longues listes (limite à 2 éléments et affiche "... etc. à la place"
if isinstance(item, list): 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 return item
def shorten(self): def shorten(self):
"Abrège le résultat"
self.result = self._shorten(self.result) self.result = self._shorten(self.result)
def pp(self): def pp(self):
@ -122,8 +136,8 @@ class Sample:
file.write(f"#### {self.method} {self.url}\n") file.write(f"#### {self.method} {self.url}\n")
if len(self.content) > 0: if len(self.content) > 0:
file.write(f"> `Content-Type: application/json`\n") file.write("> `Content-Type: application/json`\n")
file.write(f"> \n") file.write("> \n")
file.write(f"> `{self.content}`\n\n") file.write(f"> `{self.content}`\n\n")
file.write("```json\n") file.write("```json\n")
@ -143,7 +157,7 @@ class Samples:
"""Entry_names: la liste des entrées à reconstruire. """Entry_names: la liste des entrées à reconstruire.
si None, la totalité des lignes de samples.csv est prise en compte 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 self.entry_names = entry_names
def add_sample(self, line): def add_sample(self, line):
@ -171,35 +185,35 @@ class Samples:
def dump(self): def dump(self):
for entry, samples in self.entries.items(): for entry, samples in self.entries.items():
file = open(f"{DATA_DIR}sample_{entry}.json.md", "tw") with open(f"{DATA_DIR}sample_{entry}.json.md", "tw", encoding="utf-8") as f:
file.write(f"### {entry}\n\n") f.write(f"### {entry}\n\n")
for sample in sorted( # Trié de façon à rendre le fichier indépendant de l'ordre des résultats
samples, key=lambda s: s.url 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(f)
sample.dump(file)
file.close()
def make_samples(samples_filename): def make_samples(samples_filename):
if len(sys.argv) == 1: "Génère les samples"
entry_names = None entry_names = None
elif len(sys.argv) >= 3 and sys.argv[1] == "-i": if len(sys.argv) >= 3 and sys.argv[1] == "-i":
samples_filename = sys.argv[2] samples_filename = sys.argv[2]
entry_names = sys.argv[3:] if len(sys.argv) > 3 else None 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 os.path.exists(DATA_DIR):
if not os.path.isdir(DATA_DIR): if not os.path.isdir(DATA_DIR):
raise f"{DATA_DIR} existe déjà et n'est pas un répertoire" raise SampleException(f"{DATA_DIR} existe déjà et n'est pas un répertoire")
else: # DATA_DIR existe déjà - effacer et recréer
# DATA_DIR existe déjà - effacer et recréer shutil.rmtree(DATA_DIR)
shutil.rmtree(DATA_DIR) os.mkdir(DATA_DIR)
os.mkdir(DATA_DIR)
else: else:
os.mkdir(DATA_DIR) os.mkdir(DATA_DIR)
samples = Samples(entry_names) samples = Samples(entry_names)
df = read_csv( df = pd.read_csv(
samples_filename, samples_filename,
comment="#",
sep=";", sep=";",
quotechar='"', quotechar='"',
dtype={ dtype={
@ -212,11 +226,12 @@ def make_samples(samples_filename):
keep_default_na=False, keep_default_na=False,
) )
df = df.reset_index() df = df.reset_index()
df.apply(lambda line: samples.add_sample(line), axis=1) df.apply(samples.add_sample, axis=1)
samples.dump() samples.dump()
return samples return samples
if not CHECK_CERTIFICATE: if not CHECK_CERTIFICATE:
urllib3.disable_warnings() urllib3.disable_warnings()
make_samples(SAMPLES_FILENAME) make_samples(SAMPLES_FILENAME)

View File

@ -116,3 +116,73 @@ def POST_JSON(path: str, data: dict = {}, headers: dict = None, errmsg=None, dep
if r.status_code != 200: if r.status_code != 200:
raise APIError(errmsg or f"erreur status={r.status_code} !", r.json()) raise APIError(errmsg or f"erreur status={r.status_code} !", r.json())
return r.json() # decode la reponse JSON return r.json() # decode la reponse JSON
def check_fields(data: dict, fields: dict = None):
"""
Vérifie que le dictionnaire data contient les bonnes clés
et les bons types de valeurs.
Args:
data (dict): un dictionnaire (json de retour de l'api)
fields (dict, optional): Un dictionnaire représentant les clés et les types d'une réponse.
"""
assert set(data.keys()) == set(fields.keys())
for key in data:
if key in ("moduleimpl_id", "desc", "user_id", "external_data"):
assert (
isinstance(data[key], fields[key]) or data[key] is None
), f"error [{key}:{type(data[key])}, {data[key]}, {fields[key]}]"
else:
assert isinstance(
data[key], fields[key]
), f"error [{key}:{type(data[key])}, {data[key]}, {fields[key]}]"
def check_failure_get(path: str, headers: dict, err: str = None):
"""
Vérifie que la requête GET renvoie bien un 404
Args:
path (str): la route de l'api
headers (dict): le token d'auth de l'api
err (str, optional): L'erreur qui est sensée être fournie par l'api.
Raises:
APIError: Une erreur car la requête a fonctionné (mauvais comportement)
"""
try:
GET(path=path, headers=headers)
# ^ Renvoi un 404
except APIError as api_err:
if err is not None:
assert api_err.payload["message"] == err
else:
raise APIError("Le GET n'aurait pas du fonctionner")
def check_failure_post(path: str, headers: dict, data: dict, err: str = None):
"""
Vérifie que la requête POST renvoie bien un 404
Args:
path (str): la route de l'api
headers (dict): le token d'auth
data (dict): un dictionnaire (json) à envoyer
err (str, optional): L'erreur qui est sensée être fournie par l'api.
Raises:
APIError: Une erreur car la requête a fonctionné (mauvais comportement)
"""
try:
data = POST_JSON(path=path, headers=headers, data=data)
# ^ Renvoie un 404
except APIError as api_err:
if err is not None:
assert (
api_err.payload["message"] == err
), f"received: {api_err.payload['message']}"
else:
raise APIError("Le GET n'aurait pas du fonctionner")

View File

@ -1,214 +0,0 @@
# -*- coding: utf-8 -*-
"""Test Logos
Utilisation :
créer les variables d'environnement: (indiquer les valeurs
pour le serveur ScoDoc que vous voulez interroger)
export SCODOC_URL="https://scodoc.xxx.net/"
export SCODOC_USER="xxx"
export SCODOC_PASSWD="xxx"
export CHECK_CERTIFICATE=0 # ou 1 si serveur de production avec certif SSL valide
(on peut aussi placer ces valeurs dans un fichier .env du répertoire tests/api).
Lancer :
pytest tests/api/test_api_absences.py
"""
import requests
from app.scodoc import sco_utils as scu
from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers
# Etudiant pour les tests
from tests.api.tools_test_api import (
verify_fields,
ABSENCES_FIELDS,
ABSENCES_GROUP_ETAT_FIELDS,
)
ETUDID = 1
# absences
def test_absences(api_headers):
"""
Test 'absences'
Route :
- /absences/etudid/<int:etudid>
"""
r = requests.get(
f"{API_URL}/absences/etudid/{ETUDID}",
headers=api_headers,
verify=CHECK_CERTIFICATE,
timeout=scu.SCO_TEST_API_TIMEOUT,
)
assert r.status_code == 200
absences = r.json()
assert isinstance(absences, list)
for abs in absences:
assert verify_fields(abs, ABSENCES_FIELDS) is True
assert isinstance(abs["jour"], str)
assert isinstance(abs["matin"], bool)
assert isinstance(abs["estabs"], bool)
assert isinstance(abs["estjust"], bool)
assert isinstance(abs["description"], str)
assert isinstance(abs["begin"], str)
assert isinstance(abs["end"], str)
assert abs["begin"] < abs["end"]
# absences_justify
def test_absences_justify(api_headers):
"""
Test 'absences_just'
Route :
- /absences/etudid/<int:etudid>/just
"""
r = requests.get(
f"{API_URL}/absences/etudid/{ETUDID}/just",
headers=api_headers,
verify=CHECK_CERTIFICATE,
timeout=scu.SCO_TEST_API_TIMEOUT,
)
assert r.status_code == 200
absences = r.json()
assert isinstance(absences, list)
for abs in absences:
assert verify_fields(abs, ABSENCES_FIELDS) is True
assert isinstance(abs["jour"], str)
assert isinstance(abs["matin"], bool)
assert isinstance(abs["estabs"], bool)
assert isinstance(abs["estjust"], bool)
assert isinstance(abs["description"], str)
assert isinstance(abs["begin"], str)
assert isinstance(abs["end"], str)
assert abs["begin"] < abs["end"]
def test_abs_groupe_etat(api_headers):
"""
Test 'abs_groupe_etat'
Routes :
- /absences/abs_group_etat/<int:group_id>
- /absences/abs_group_etat/group_id/<int:group_id>/date_debut/<string:date_debut>/date_fin/<string:date_fin>
"""
group_id = 1
r = requests.get(
f"{API_URL}/absences/abs_group_etat/{group_id}",
headers=api_headers,
verify=CHECK_CERTIFICATE,
timeout=scu.SCO_TEST_API_TIMEOUT,
)
assert r.status_code == 200
list_absences = r.json()
assert isinstance(list_absences, list)
list_id_etu = []
for etu in list_absences:
list_id_etu.append(etu["etudid"])
assert verify_fields(etu, ABSENCES_GROUP_ETAT_FIELDS) is True
assert isinstance(etu["etudid"], int)
assert isinstance(etu["list_abs"], list)
list_abs = etu["list_abs"]
for abs in list_abs:
assert verify_fields(abs, ABSENCES_FIELDS) is True
assert isinstance(abs["jour"], str)
assert isinstance(abs["matin"], bool)
assert isinstance(abs["estabs"], bool)
assert isinstance(abs["estjust"], bool)
assert isinstance(abs["description"], str)
assert isinstance(abs["begin"], str)
assert isinstance(abs["end"], str)
assert abs["begin"] < abs["end"]
# vérifie que chaque étudiant n'apparait qu'une seule fois
assert len(set(list_id_etu)) == len(list_id_etu)
date_debut = "Fri, 15 Apr 2021 00:00:00 GMT"
date_fin = "Fri, 18 Apr 2022 00:00:00 GMT"
r1 = requests.get(
f"{API_URL}/absences/abs_group_etat/group_id/{group_id}/date_debut/{date_debut}/date_fin/{date_fin}",
headers=api_headers,
verify=CHECK_CERTIFICATE,
timeout=scu.SCO_TEST_API_TIMEOUT,
)
assert r1.status_code == 200
list_absences1 = r.json()
assert isinstance(list_absences1, list)
list_id_etu1 = []
for etu in list_absences1:
list_id_etu1.append(etu["etudid"])
assert verify_fields(etu, ABSENCES_GROUP_ETAT_FIELDS) is True
assert isinstance(etu["etudid"], int)
assert isinstance(etu["list_abs"], list)
list_abs1 = etu["list_abs"]
for abs in list_abs1:
assert verify_fields(abs, ABSENCES_FIELDS) is True
assert isinstance(abs["jour"], str)
assert isinstance(abs["matin"], bool)
assert isinstance(abs["estabs"], bool)
assert isinstance(abs["estjust"], bool)
assert isinstance(abs["description"], str)
assert isinstance(abs["begin"], str)
assert isinstance(abs["end"], str)
assert abs["begin"] < abs["end"]
all_unique1 = True
for id in list_id_etu1:
if list_id_etu1.count(id) > 1:
all_unique1 = False
assert all_unique1 is True
# XXX TODO
# def reset_etud_abs(api_headers):
# """
# Test 'reset_etud_abs'
#
# Routes :
# - /absences/etudid/<int:etudid>/list_abs/<string:list_abs>/reset_etud_abs
# - /absences/etudid/<int:etudid>/list_abs/<string:list_abs>/reset_etud_abs/only_not_just
# - /absences/etudid/<int:etudid>/list_abs/<string:list_abs>/reset_etud_abs/only_just
# """
# list_abs = []
# r = requests.get(
# f"{API_URL}/absences/etudid/{ETUDID}/list_abs/{list_abs}/reset_etud_abs",
# headers=api_headers,
# verify=CHECK_CERTIFICATE,
# timeout=scu.SCO_TEST_API_TIMEOUT,
# )
# assert r.status_code == 200
#
# r_only_not_just = requests.get(
# f"{API_URL}/absences/etudid/{ETUDID}/list_abs/{list_abs}/reset_etud_abs/only_not_just",
# headers=api_headers,
# verify=CHECK_CERTIFICATE,
# timeout=scu.SCO_TEST_API_TIMEOUT,
# )
# assert r.status_code == 200
#
#
# r_only_just = requests.get(
# f"{API_URL}/absences/etudid/{ETUDID}/list_abs/{list_abs}/reset_etud_abs/only_just",
# headers=api_headers,
# verify=CHECK_CERTIFICATE,
# timeout=scu.SCO_TEST_API_TIMEOUT,
# )
# assert r.status_code == 200

View File

@ -13,6 +13,9 @@ from tests.api.setup_test_api import (
APIError, APIError,
api_headers, api_headers,
api_admin_headers, api_admin_headers,
check_failure_get,
check_failure_post,
check_fields,
) )
ETUDID = 1 ETUDID = 1
@ -44,76 +47,6 @@ COUNT_FIELDS = {"compte": int, "journee": int, "demi": int, "heure": float}
TO_REMOVE = [] TO_REMOVE = []
def check_fields(data: dict, fields: dict = None):
"""
Cette fonction permet de vérifier que le dictionnaire data
contient les bonnes clés et les bons types de valeurs.
Args:
data (dict): un dictionnaire (json de retour de l'api)
fields (dict, optional): Un dictionnaire représentant les clés et les types d'une réponse.
"""
if fields is None:
fields = ASSIDUITES_FIELDS
assert set(data.keys()) == set(fields.keys())
for key in data:
if key in ("moduleimpl_id", "desc", "user_id", "external_data"):
assert (
isinstance(data[key], fields[key]) or data[key] is None
), f"error [{key}:{type(data[key])}, {data[key]}, {fields[key]}]"
else:
assert isinstance(
data[key], fields[key]
), f"error [{key}:{type(data[key])}, {data[key]}, {fields[key]}]"
def check_failure_get(path: str, headers: dict, err: str = None):
"""
Cette fonction vérifiée que la requête GET renvoie bien un 404
Args:
path (str): la route de l'api
headers (dict): le token d'auth de l'api
err (str, optional): L'erreur qui est sensée être fournie par l'api.
Raises:
APIError: Une erreur car la requête a fonctionné (mauvais comportement)
"""
try:
GET(path=path, headers=headers)
# ^ Renvoi un 404
except APIError as api_err:
if err is not None:
assert api_err.payload["message"] == err
else:
raise APIError("Le GET n'aurait pas du fonctionner")
def check_failure_post(path: str, headers: dict, data: dict, err: str = None):
"""
Cette fonction vérifiée que la requête POST renvoie bien un 404
Args:
path (str): la route de l'api
headers (dict): le token d'auth
data (dict): un dictionnaire (json) à envoyer
err (str, optional): L'erreur qui est sensée être fournie par l'api.
Raises:
APIError: Une erreur car la requête a fonctionné (mauvais comportement)
"""
try:
data = POST_JSON(path=path, headers=headers, data=data)
# ^ Renvoi un 404
except APIError as api_err:
if err is not None:
assert api_err.payload["message"] == err
else:
raise APIError("Le GET n'aurait pas du fonctionner")
def create_data(etat: str, day: str, module: int = None, desc: str = None): def create_data(etat: str, day: str, module: int = None, desc: str = None):
""" """
Permet de créer un dictionnaire assiduité Permet de créer un dictionnaire assiduité
@ -146,7 +79,7 @@ def test_route_assiduite(api_headers):
# Bon fonctionnement == id connu # Bon fonctionnement == id connu
data = GET(path="/assiduite/1", headers=api_headers) data = GET(path="/assiduite/1", headers=api_headers)
check_fields(data) check_fields(data, fields=ASSIDUITES_FIELDS)
# Mauvais Fonctionnement == id inconnu # Mauvais Fonctionnement == id inconnu
@ -273,7 +206,7 @@ def test_route_create(api_admin_headers):
path=f'/assiduite/{res["success"][0]["message"]["assiduite_id"]}', path=f'/assiduite/{res["success"][0]["message"]["assiduite_id"]}',
headers=api_admin_headers, headers=api_admin_headers,
) )
check_fields(data) check_fields(data, fields=ASSIDUITES_FIELDS)
data2 = create_data("absent", "02", MODULE, "desc") data2 = create_data("absent", "02", MODULE, "desc")
res = POST_JSON(f"/assiduite/{ETUDID}/create", [data2], api_admin_headers) res = POST_JSON(f"/assiduite/{ETUDID}/create", [data2], api_admin_headers)

View File

@ -18,6 +18,7 @@ Utilisation :
""" """
import re import re
from types import NoneType
import requests import requests
@ -513,13 +514,16 @@ def test_etudiant_bulletin_semestre(api_headers):
assert evaluation["description"] is None or isinstance( assert evaluation["description"] is None or isinstance(
evaluation["description"], str evaluation["description"], str
) )
assert evaluation["date"] is None or isinstance(evaluation["date"], str)
assert isinstance(evaluation["heure_debut"], str)
assert isinstance(evaluation["heure_fin"], str)
assert isinstance(evaluation["coef"], str) assert isinstance(evaluation["coef"], str)
assert isinstance(evaluation["poids"], dict) assert isinstance(evaluation["poids"], dict)
assert isinstance(evaluation["note"], dict) assert isinstance(evaluation["note"], dict)
assert isinstance(evaluation["url"], str) assert isinstance(evaluation["url"], str)
assert isinstance(evaluation["date_debut"], (str, NoneType))
assert isinstance(evaluation["date_fin"], (str, NoneType))
# Deprecated (supprimer avant #sco9.7):
assert isinstance(evaluation["date"], (str, NoneType))
assert isinstance(evaluation["heure_debut"], (str, NoneType))
assert isinstance(evaluation["heure_fin"], (str, NoneType))
assert ( assert (
verify_fields( verify_fields(
@ -567,13 +571,16 @@ def test_etudiant_bulletin_semestre(api_headers):
assert evaluation["description"] is None or isinstance( assert evaluation["description"] is None or isinstance(
evaluation["description"], str evaluation["description"], str
) )
assert evaluation["date"] is None or isinstance(evaluation["date"], str)
assert isinstance(evaluation["heure_debut"], str)
assert isinstance(evaluation["heure_fin"], str)
assert isinstance(evaluation["coef"], str) assert isinstance(evaluation["coef"], str)
assert isinstance(evaluation["poids"], dict) assert isinstance(evaluation["poids"], dict)
assert isinstance(evaluation["note"], dict) assert isinstance(evaluation["note"], dict)
assert isinstance(evaluation["url"], str) assert isinstance(evaluation["url"], str)
assert isinstance(evaluation["date_fin"], (str, NoneType))
assert isinstance(evaluation["date_debut"], (str, NoneType))
# Deprecated fields (supprimer avant #sco9.7)
assert isinstance(evaluation["date"], (str, NoneType))
assert isinstance(evaluation["heure_debut"], (str, NoneType))
assert isinstance(evaluation["heure_fin"], (str, NoneType))
assert ( assert (
verify_fields( verify_fields(

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Test Logos """Test API evaluations
Utilisation : Utilisation :
créer les variables d'environnement: (indiquer les valeurs créer les variables d'environnement: (indiquer les valeurs
@ -18,9 +18,18 @@ Utilisation :
""" """
import requests import requests
from types import NoneType
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers from tests.api.setup_test_api import (
API_URL,
CHECK_CERTIFICATE,
GET,
POST_JSON,
api_admin_headers,
api_headers,
check_failure_post,
)
from tests.api.tools_test_api import ( from tests.api.tools_test_api import (
verify_fields, verify_fields,
EVALUATIONS_FIELDS, EVALUATIONS_FIELDS,
@ -43,25 +52,25 @@ def test_evaluations(api_headers):
timeout=scu.SCO_TEST_API_TIMEOUT, timeout=scu.SCO_TEST_API_TIMEOUT,
) )
assert r.status_code == 200 assert r.status_code == 200
list_eval = r.json() evaluations = r.json()
assert list_eval assert evaluations
assert isinstance(list_eval, list) assert isinstance(evaluations, list)
for eval in list_eval: for e in evaluations:
assert verify_fields(eval, EVALUATIONS_FIELDS) is True assert verify_fields(e, EVALUATIONS_FIELDS)
assert isinstance(eval["id"], int) assert isinstance(e["date_debut"], (str, NoneType))
assert isinstance(eval["note_max"], float) assert isinstance(e["date_fin"], (str, NoneType))
assert isinstance(eval["visi_bulletin"], bool) assert isinstance(e["id"], int)
assert isinstance(eval["evaluation_type"], int) assert isinstance(e["note_max"], float)
assert isinstance(eval["moduleimpl_id"], int) assert isinstance(e["visibulletin"], bool)
assert eval["description"] is None or isinstance(eval["description"], str) assert isinstance(e["evaluation_type"], int)
assert isinstance(eval["coefficient"], float) assert isinstance(e["moduleimpl_id"], int)
assert isinstance(eval["publish_incomplete"], bool) assert e["description"] is None or isinstance(e["description"], str)
assert isinstance(eval["numero"], int) assert isinstance(e["coefficient"], float)
assert eval["date_debut"] is None or isinstance(eval["date_debut"], str) assert isinstance(e["publish_incomplete"], bool)
assert eval["date_fin"] is None or isinstance(eval["date_fin"], str) assert isinstance(e["numero"], int)
assert isinstance(eval["poids"], dict) assert isinstance(e["poids"], dict)
assert eval["moduleimpl_id"] == moduleimpl_id assert e["moduleimpl_id"] == moduleimpl_id
def test_evaluation_notes(api_headers): def test_evaluation_notes(api_headers):
@ -92,3 +101,133 @@ def test_evaluation_notes(api_headers):
assert isinstance(note["uid"], int) assert isinstance(note["uid"], int)
assert eval_id == note["evaluation_id"] assert eval_id == note["evaluation_id"]
def test_evaluation_create(api_admin_headers):
"""
Test /moduleimpl/<int:moduleimpl_id>/evaluation/create
"""
moduleimpl_id = 20
# Nombre d'évaluations initial
evaluations = GET(
f"/moduleimpl/{moduleimpl_id}/evaluations", headers=api_admin_headers
)
nb_evals = len(evaluations)
#
e = POST_JSON(
f"/moduleimpl/{moduleimpl_id}/evaluation/create",
{"description": "eval test"},
api_admin_headers,
)
assert isinstance(e, dict)
assert verify_fields(e, EVALUATIONS_FIELDS)
# Check default values
assert e["note_max"] == 20.0
assert e["evaluation_type"] == 0
assert not e["date_debut"]
assert not e["date_fin"]
assert e["visibulletin"] is True
assert e["publish_incomplete"] is False
assert e["coefficient"] == 1.0
new_nb_evals = len(
GET(f"/moduleimpl/{moduleimpl_id}/evaluations", headers=api_admin_headers)
)
assert new_nb_evals == nb_evals + 1
nb_evals = new_nb_evals
# Avec une erreur
check_failure_post(
f"/moduleimpl/{moduleimpl_id}/evaluation/create",
api_admin_headers,
{"evaluation_type": 666},
err="paramètre de type incorrect (invalid evaluation_type value)",
)
new_nb_evals = len(
GET(f"/moduleimpl/{moduleimpl_id}/evaluations", headers=api_admin_headers)
)
assert new_nb_evals == nb_evals # inchangé
# Avec plein de valeurs
data = {
"coefficient": 12.0,
"date_debut": "2021-10-15T08:30:00+02:00",
"date_fin": "2021-10-15T10:30:00+02:00",
"description": "eval test2",
"evaluation_type": 1,
"visibulletin": False,
"publish_incomplete": True,
"note_max": 100.0,
}
e = POST_JSON(
f"/moduleimpl/{moduleimpl_id}/evaluation/create",
data,
api_admin_headers,
)
e_ret = GET(f"/evaluation/{e['id']}", headers=api_admin_headers)
for k, v in data.items():
assert e_ret[k] == v, f"received '{e_ret[k]}'"
# Avec des poids APC
nb_evals = len(
GET(f"/moduleimpl/{moduleimpl_id}/evaluations", headers=api_admin_headers)
)
data.update(
{
"description": "eval test apc erreur",
"poids": {"666": 666.0}, # poids erroné: UE inexistante
}
)
check_failure_post(
f"/moduleimpl/{moduleimpl_id}/evaluation/create",
api_admin_headers,
data,
)
new_nb_evals = len(
GET(f"/moduleimpl/{moduleimpl_id}/evaluations", headers=api_admin_headers)
)
assert new_nb_evals == nb_evals # inchangé
# Avec des poids absurdes
data.update({"description": "eval test apc erreur 2", "poids": "nimporte quoi"})
check_failure_post(
f"/moduleimpl/{moduleimpl_id}/evaluation/create",
api_admin_headers,
data,
)
new_nb_evals = len(
GET(f"/moduleimpl/{moduleimpl_id}/evaluations", headers=api_admin_headers)
)
assert new_nb_evals == nb_evals # inchangé
# Avec de bons poids
# pour cela il nous faut les UEs de ce formsemestre
# sachant que l'on a moduleimpl
modimpl = GET(f"/moduleimpl/{moduleimpl_id}", headers=api_admin_headers)
formation = GET(
f"/formsemestre/{modimpl['formsemestre_id']}/programme",
headers=api_admin_headers,
)
ues = formation["ues"]
assert len(ues)
ue_ids = [ue["id"] for ue in ues]
poids = {ue_id: float(i) + 0.5 for i, ue_id in enumerate(ue_ids)}
data.update({"description": "eval avec poids", "poids": poids})
e = POST_JSON(
f"/moduleimpl/{moduleimpl_id}/evaluation/create",
data,
api_admin_headers,
)
assert e["poids"]
e_ret = GET(f"/evaluation/{e['id']}", headers=api_admin_headers)
assert e_ret["poids"] == e["poids"]
new_nb_evals = len(
GET(f"/moduleimpl/{moduleimpl_id}/evaluations", headers=api_admin_headers)
)
assert new_nb_evals == nb_evals + 1
nb_evals = new_nb_evals
# Delete
ans = POST_JSON(
f"/evaluation/{e_ret['id']}/delete",
headers=api_admin_headers,
)
assert ans == "ok"
assert nb_evals - 1 == len(
GET(f"/moduleimpl/{moduleimpl_id}/evaluations", headers=api_admin_headers)
)

View File

@ -18,6 +18,7 @@ Utilisation :
""" """
import json import json
import requests import requests
from types import NoneType
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
@ -301,14 +302,18 @@ def test_bulletins(api_headers):
assert evaluation["description"] is None or isinstance( assert evaluation["description"] is None or isinstance(
evaluation["description"], str evaluation["description"], str
) )
assert evaluation["date"] is None or isinstance(evaluation["date"], str) assert isinstance(evaluation["date_debut"], (str, NoneType))
assert isinstance(evaluation["heure_debut"], str) assert isinstance(evaluation["date_fin"], (str, NoneType))
assert isinstance(evaluation["heure_fin"], str) assert isinstance(evaluation["coef"], (str, NoneType))
assert isinstance(evaluation["coef"], str)
assert isinstance(evaluation["poids"], dict) assert isinstance(evaluation["poids"], dict)
assert isinstance(evaluation["note"], dict) assert isinstance(evaluation["note"], dict)
assert isinstance(evaluation["url"], str) assert isinstance(evaluation["url"], str)
# Deprecated (supprimer avant #sco9.7):
assert isinstance(evaluation["date"], (str, NoneType))
assert isinstance(evaluation["heure_debut"], (str, NoneType))
assert isinstance(evaluation["heure_fin"], (str, NoneType))
assert ( assert (
verify_fields( verify_fields(
evaluation["poids"], evaluation["poids"],
@ -354,14 +359,18 @@ def test_bulletins(api_headers):
assert evaluation["description"] is None or isinstance( assert evaluation["description"] is None or isinstance(
evaluation["description"], str evaluation["description"], str
) )
assert evaluation["date"] is None or isinstance(evaluation["date"], str) assert isinstance(evaluation["date_debut"], (str, NoneType))
assert isinstance(evaluation["heure_debut"], str) assert isinstance(evaluation["date_fin"], (str, NoneType))
assert isinstance(evaluation["heure_fin"], str)
assert isinstance(evaluation["coef"], str) assert isinstance(evaluation["coef"], str)
assert isinstance(evaluation["poids"], dict) assert isinstance(evaluation["poids"], dict)
assert isinstance(evaluation["note"], dict) assert isinstance(evaluation["note"], dict)
assert isinstance(evaluation["url"], str) assert isinstance(evaluation["url"], str)
# Deprecated (supprimer avant #sco9.7):
assert isinstance(evaluation["date"], (str, NoneType))
assert isinstance(evaluation["heure_debut"], (str, NoneType))
assert isinstance(evaluation["heure_fin"], (str, NoneType))
assert ( assert (
verify_fields( verify_fields(
evaluation["poids"], evaluation["poids"],

View File

@ -491,7 +491,7 @@ EVAL_FIELDS = {
"numero", "numero",
"poids", "poids",
"publish_incomplete", "publish_incomplete",
"visi_bulletin", "visibulletin",
"etat", "etat",
"nb_inscrits", "nb_inscrits",
"nb_notes_manquantes", "nb_notes_manquantes",
@ -565,7 +565,7 @@ EVALUATIONS_FIELDS = {
"numero", "numero",
"poids", "poids",
"publish_incomplete", "publish_incomplete",
"visi_bulletin", "visibulletin",
} }
NOTES_FIELDS = { NOTES_FIELDS = {

View File

@ -1,119 +1,120 @@
"entry_name";"url";"permission";"method";"content" "entry_name";"url";"permission";"method";"content"
"assiduite_create";"/assiduite/1/create";"ScoView";"POST";"{""date_debut"": ""2022-10-27T08:00"",""date_fin"": ""2022-10-27T10:00"",""etat"": ""absent""}"
"assiduite_create";"/assiduite/1/create/batch";"ScoView";"POST";"{""batch"":[{""date_debut"": ""2022-10-27T08:00"",""date_fin"": ""2022-10-27T10:00"",""etat"": ""absent""},{""date_debut"": ""2022-10-27T08:00"",""date_fin"": ""2022-10-27T10:00"",""etat"": ""retard""},{""date_debut"": ""2022-10-27T11:00"",""date_fin"": ""2022-10-27T13:00"",""etat"": ""present""}]}"
"assiduite_delete";"/assiduite/delete";"ScoView";"POST";"{""assiduite_id"": 1}"
"assiduite_delete";"/assiduite/delete/batch";"ScoView";"POST";"{""batch"":[2,2,3]}"
"assiduite_edit";"/assiduite/1/edit";"ScoView";"POST";"{""etat"": ""retard"",""moduleimpl_id"":3}"
"assiduite_edit";"/assiduite/1/edit";"ScoView";"POST";"{""etat"":""absent""}"
"assiduite_edit";"/assiduite/1/edit";"ScoView";"POST";"{""moduleimpl_id"":2}"
"assiduite";"/assiduite/1";"ScoView";"GET"; "assiduite";"/assiduite/1";"ScoView";"GET";
"assiduites";"/assiduites/1";"ScoView";"GET";
"assiduites";"/assiduites/1/query?etat=retard";"ScoView";"GET";
"assiduites";"/assiduites/1/query?moduleimpl_id=1";"ScoView";"GET";
"assiduites_count";"/assiduites/1/count";"ScoView";"GET"; "assiduites_count";"/assiduites/1/count";"ScoView";"GET";
"assiduites_count";"/assiduites/1/count/query?etat=retard";"ScoView";"GET";
"assiduites_count";"/assiduites/1/count/query?etat=present,retard&metric=compte,heure";"ScoView";"GET"; "assiduites_count";"/assiduites/1/count/query?etat=present,retard&metric=compte,heure";"ScoView";"GET";
"assiduites_count";"/assiduites/1/count/query?etat=retard";"ScoView";"GET";
"assiduites_formsemestre_count";"/assiduites/formsemestre/1/count";"ScoView";"GET";
"assiduites_formsemestre_count";"/assiduites/formsemestre/1/count/query?etat=present,retard&metric=compte,heure";"ScoView";"GET";
"assiduites_formsemestre_count";"/assiduites/formsemestre/1/count/query?etat=retard";"ScoView";"GET";
"assiduites_formsemestre";"/assiduites/formsemestre/1";"ScoView";"GET"; "assiduites_formsemestre";"/assiduites/formsemestre/1";"ScoView";"GET";
"assiduites_formsemestre";"/assiduites/formsemestre/1/query?etat=retard";"ScoView";"GET"; "assiduites_formsemestre";"/assiduites/formsemestre/1/query?etat=retard";"ScoView";"GET";
"assiduites_formsemestre";"/assiduites/formsemestre/1/query?moduleimpl_id=1";"ScoView";"GET"; "assiduites_formsemestre";"/assiduites/formsemestre/1/query?moduleimpl_id=1";"ScoView";"GET";
"assiduites_formsemestre_count";"/assiduites/formsemestre/1/count";"ScoView";"GET"; "assiduites";"/assiduites/1";"ScoView";"GET";
"assiduites_formsemestre_count";"/assiduites/formsemestre/1/count/query?etat=retard";"ScoView";"GET"; "assiduites";"/assiduites/1/query?etat=retard";"ScoView";"GET";
"assiduites_formsemestre_count";"/assiduites/formsemestre/1/count/query?etat=present,retard&metric=compte,heure";"ScoView";"GET"; "assiduites";"/assiduites/1/query?moduleimpl_id=1";"ScoView";"GET";
"assiduite_create";"/assiduite/1/create";"ScoView";"POST";"{""date_debut"": ""2022-10-27T08:00"",""date_fin"": ""2022-10-27T10:00"",""etat"": ""absent""}"
"assiduite_create";"/assiduite/1/create/batch";"ScoView";"POST";"{""batch"":[{""date_debut"": ""2022-10-27T08:00"",""date_fin"": ""2022-10-27T10:00"",""etat"": ""absent""},{""date_debut"": ""2022-10-27T08:00"",""date_fin"": ""2022-10-27T10:00"",""etat"": ""retard""},{""date_debut"": ""2022-10-27T11:00"",""date_fin"": ""2022-10-27T13:00"",""etat"": ""present""}]}"
"assiduite_edit";"/assiduite/1/edit";"ScoView";"POST";"{""etat"":""absent""}"
"assiduite_edit";"/assiduite/1/edit";"ScoView";"POST";"{""moduleimpl_id"":2}"
"assiduite_edit";"/assiduite/1/edit";"ScoView";"POST";"{""etat"": ""retard"",""moduleimpl_id"":3}"
"assiduite_delete";"/assiduite/delete";"ScoView";"POST";"{""assiduite_id"": 1}"
"assiduite_delete";"/assiduite/delete/batch";"ScoView";"POST";"{""batch"":[2,2,3]}"
"departements";"/departements";"ScoView";"GET";
"departements-ids";"/departements_ids";"ScoView";"GET";
"departement";"/departement/TAPI";"ScoView";"GET";
"departement";"/departement/id/1";"ScoView";"GET";
"departement-etudiants";"/departement/TAPI/etudiants";"ScoView";"GET";
"departement-etudiants";"/departement/id/1/etudiants";"ScoView";"GET";
"departement-formsemestres_ids";"/departement/TAPI/formsemestres_ids";"ScoView";"GET";
"departement-formsemestres_ids";"/departement/id/1/formsemestres_ids";"ScoView";"GET";
"departement-formsemestres-courants";"/departement/TAPI/formsemestres_courants";"ScoView";"GET";
"departement-formsemestres-courants";"/departement/id/1/formsemestres_courants";"ScoView";"GET";
"departement-create";"/departement/create";"ScoSuperAdmin";"POST";"{""acronym"": ""NEWONE"" , ""visible"": true}" "departement-create";"/departement/create";"ScoSuperAdmin";"POST";"{""acronym"": ""NEWONE"" , ""visible"": true}"
"departement-edit";"/departement/NEWONE/edit";"ScoSuperAdmin";"POST";"{""visible"": false}"
"departement-delete";"/departement/NEWONE/delete";"ScoSuperAdmin";"POST"; "departement-delete";"/departement/NEWONE/delete";"ScoSuperAdmin";"POST";
"etudiants-courants";"/etudiants/courants?date_courante=2022-07-20";"ScoView";"GET"; "departement-edit";"/departement/NEWONE/edit";"ScoSuperAdmin";"POST";"{""visible"": false}"
"etudiants-courants";"/etudiants/courants/long?date_courante=2022-07-20";"ScoView";"GET"; "departement-etudiants";"/departement/id/1/etudiants";"ScoView";"GET";
"departement-etudiants";"/departement/TAPI/etudiants";"ScoView";"GET";
"departement-formsemestres_ids";"/departement/id/1/formsemestres_ids";"ScoView";"GET";
"departement-formsemestres_ids";"/departement/TAPI/formsemestres_ids";"ScoView";"GET";
"departement-formsemestres-courants";"/departement/id/1/formsemestres_courants";"ScoView";"GET";
"departement-formsemestres-courants";"/departement/TAPI/formsemestres_courants";"ScoView";"GET";
"departement-logo";"/departement/id/1/logo/D";"ScoSuperAdmin";"GET";
"departement-logo";"/departement/TAPI/logo/D";"ScoSuperAdmin";"GET";
"departement-logos";"/departement/id/1/logos";"ScoSuperAdmin";"GET";
"departement-logos";"/departement/TAPI/logos";"ScoSuperAdmin";"GET";
"departement";"/departement/id/1";"ScoView";"GET";
"departement";"/departement/TAPI";"ScoView";"GET";
"departements-ids";"/departements_ids";"ScoView";"GET";
"departements";"/departements";"ScoView";"GET";
"etudiant_formsemestres";"/etudiant/nip/11/formsemestres";"ScoView";"GET";
"etudiant-formsemestre-bulletin";"/etudiant/etudid/11/formsemestre/1/bulletin";"ScoView";"GET";
#"etudiant-formsemestre-bulletin";"/etudiant/ine/INE11/formsemestre/1/bulletin";"ScoView";"GET";
#"etudiant-formsemestre-bulletin";"/etudiant/nip/11/formsemestre/1/bulletin";"ScoView";"GET";
#"etudiant-formsemestre-bulletin";"/etudiant/nip/11/formsemestre/1/bulletin/short/pdf";"ScoView";"GET";
"etudiant-formsemestre-groups";"/etudiant/etudid/11/formsemestre/1/groups";"ScoView";"GET";
"etudiant-formsemestres";"/etudiant/etudid/11/formsemestres";"ScoView";"GET";
"etudiant-formsemestres";"/etudiant/ine/INE11/formsemestres";"ScoView";"GET";
"etudiant";"/etudiant/etudid/11";"ScoView";"GET"; "etudiant";"/etudiant/etudid/11";"ScoView";"GET";
"etudiant";"/etudiant/nip/11";"ScoView";"GET";
"etudiant";"/etudiant/ine/INE11";"ScoView";"GET"; "etudiant";"/etudiant/ine/INE11";"ScoView";"GET";
"etudiant";"/etudiant/nip/11";"ScoView";"GET";
"etudiants-clef";"/etudiants/etudid/11";"ScoView";"GET"; "etudiants-clef";"/etudiants/etudid/11";"ScoView";"GET";
"etudiants-clef";"/etudiants/ine/INE11";"ScoView";"GET"; "etudiants-clef";"/etudiants/ine/INE11";"ScoView";"GET";
"etudiants-clef";"/etudiants/nip/11";"ScoView";"GET"; "etudiants-clef";"/etudiants/nip/11";"ScoView";"GET";
"etudiant-formsemestres";"/etudiant/etudid/11/formsemestres";"ScoView";"GET"; "etudiants-courants";"/etudiants/courants?date_courante=2022-07-20";"ScoView";"GET";
"etudiant-formsemestres";"/etudiant/ine/INE11/formsemestres";"ScoView";"GET"; "etudiants-courants";"/etudiants/courants/long?date_courante=2022-07-20";"ScoView";"GET";
"etudiant_formsemestres";"/etudiant/nip/11/formsemestres";"ScoView";"GET"; "evaluation";"/evaluation/1";"ScoView";"GET";
"etudiant-formsemestre-bulletin";"/etudiant/etudid/11/formsemestre/1/bulletin";"ScoView";"GET"; "evaluation-notes";"/evaluation/1/notes";"ScoView";"GET";
"etudiant-formsemestre-bulletin";"/etudiant/ine/INE11/formsemestre/1/bulletin";"ScoView";"GET";
"etudiant-formsemestre-bulletin";"/etudiant/nip/11/formsemestre/1/bulletin";"ScoView";"GET";
"etudiant-formsemestre-bulletin";"/etudiant/nip/11/formsemestre/1/bulletin/short/pdf";"ScoView";"GET";
"etudiant-formsemestre-groups";"/etudiant/etudid/11/formsemestre/1/groups";"ScoView";"GET";
"formations";"/formations";"ScoView";"GET";
"formations_ids";"/formations_ids";"ScoView";"GET";
"formation";"/formation/1";"ScoView";"GET";
"formation-export";"/formation/1/export";"ScoView";"GET";
"formation-export";"/formation/1/export_with_ids";"ScoView";"GET"; "formation-export";"/formation/1/export_with_ids";"ScoView";"GET";
"formation-export";"/formation/1/export";"ScoView";"GET";
"formation-referentiel_competences";"/formation/1/referentiel_competences";"ScoView";"GET"; "formation-referentiel_competences";"/formation/1/referentiel_competences";"ScoView";"GET";
"moduleimpl";"/moduleimpl/1";"ScoView";"GET"; "formation";"/formation/1";"ScoView";"GET";
"formations_ids";"/formations_ids";"ScoView";"GET";
"formations";"/formations";"ScoView";"GET";
"formsemestre-bulletins";"/formsemestre/1/bulletins";"ScoView";"GET";
"formsemestre-decisions_jury";"/formsemestre/1/decisions_jury";"ScoView";"GET";
"formsemestre-etat_evals";"/formsemestre/1/etat_evals";"ScoView";"GET";
"formsemestre-etudiants-query";"/formsemestre/1/etudiants/query?etat=D";"ScoView";"GET";
"formsemestre-etudiants";"/formsemestre/1/etudiants";"ScoView";"GET";
"formsemestre-etudiants";"/formsemestre/1/etudiants/long";"ScoView";"GET";
"formsemestre-partition-create";"/formsemestre/1/partition/create";"ScoSuperAdmin";"POST";"{""partition_name"": ""PART""} "
"formsemestre-partitions-order";"/formsemestre/1/partitions/order";"ScoSuperAdmin";"POST";"[ 1 ]"
"formsemestre-partitions";"/formsemestre/1/partitions";"ScoView";"GET";
"formsemestre-programme";"/formsemestre/1/programme";"ScoView";"GET";
"formsemestre-resultats";"/formsemestre/1/resultats";"ScoView";"GET";
"formsemestre";"/formsemestre/1";"ScoView";"GET"; "formsemestre";"/formsemestre/1";"ScoView";"GET";
"formsemestres-query";"/formsemestres/query?annee_scolaire=2022&etape_apo=A2";"ScoView";"GET"; "formsemestres-query";"/formsemestres/query?annee_scolaire=2022&etape_apo=A2";"ScoView";"GET";
"formsemestres-query";"/formsemestres/query?nip=11";"ScoView";"GET"; "formsemestres-query";"/formsemestres/query?nip=11";"ScoView";"GET";
"formsemestre-bulletins";"/formsemestre/1/bulletins";"ScoView";"GET";
"formsemestre-programme";"/formsemestre/1/programme";"ScoView";"GET";
"formsemestre-etudiants";"/formsemestre/1/etudiants";"ScoView";"GET";
"formsemestre-etudiants";"/formsemestre/1/etudiants/long";"ScoView";"GET";
"formsemestre-etudiants-query";"/formsemestre/1/etudiants/query?etat=D";"ScoView";"GET";
"formsemestre-etat_evals";"/formsemestre/1/etat_evals";"ScoView";"GET";
"formsemestre-resultats";"/formsemestre/1/resultats";"ScoView";"GET";
"formsemestre-decisions_jury";"/formsemestre/1/decisions_jury";"ScoView";"GET";
"formsemestre-partitions";"/formsemestre/1/partitions";"ScoView";"GET";
"partition";"/partition/1";"ScoView";"GET";
"group-etudiants";"/group/1/etudiants";"ScoView";"GET";
"group-etudiants-query";"/group/1/etudiants/query?etat=D";"ScoView";"GET";
"moduleimpl-evaluations";"/moduleimpl/1/evaluations";"ScoView";"GET";
"evaluation-notes";"/evaluation/1/notes";"ScoView";"GET";
"user";"/user/1";"ScoView";"GET";
"users-query";"/users/query?starts_with=u_";"ScoView";"GET";
"permissions";"/permissions";"ScoView";"GET";
"roles";"/roles";"ScoView";"GET";
"role";"/role/Observateur";"ScoView";"GET";
"group-set_etudiant";"/group/1/set_etudiant/10";"ScoSuperAdmin";"POST";
"group-remove_etudiant";"/group/1/remove_etudiant/10";"ScoSuperAdmin";"POST";
"partition-group-create";"/partition/1/group/create";"ScoSuperAdmin";"POST";"{""group_name"": ""NEW_GROUP""}"
"group-edit";"/group/2/edit";"ScoSuperAdmin";"POST";"{""group_name"": ""NEW_GROUP2""}"
"group-delete";"/group/2/delete";"ScoSuperAdmin";"POST"; "group-delete";"/group/2/delete";"ScoSuperAdmin";"POST";
"formsemestre-partition-create";"/formsemestre/1/partition/create";"ScoSuperAdmin";"POST";"{""partition_name"": ""PART""} " "group-edit";"/group/2/edit";"ScoSuperAdmin";"POST";"{""group_name"": ""NEW_GROUP2""}"
"formsemestre-partitions-order";"/formsemestre/1/partitions/order";"ScoSuperAdmin";"POST";"[ 1 ]" "group-etudiants-query";"/group/1/etudiants/query?etat=D";"ScoView";"GET";
"partition-edit";"/partition/1/edit";"ScoSuperAdmin";"POST";"{""partition_name"":""P2BIS"", ""numero"":3,""bul_show_rank"":true,""show_in_lists"":false, ""groups_editable"":true}" "group-etudiants";"/group/1/etudiants";"ScoView";"GET";
"partition-remove_etudiant";"/partition/2/remove_etudiant/10";"ScoSuperAdmin";"POST"; "group-remove_etudiant";"/group/1/remove_etudiant/10";"ScoSuperAdmin";"POST";
"partition-groups-order";"/partition/1/groups/order";"ScoSuperAdmin";"POST";"[ 1 ]" "group-set_etudiant";"/group/1/set_etudiant/10";"ScoSuperAdmin";"POST";
"logo";"/logo/B";"ScoSuperAdmin";"GET";
"logos";"/logos";"ScoSuperAdmin";"GET";
"moduleimpl-evaluations";"/moduleimpl/1/evaluations";"ScoView";"GET";
"moduleimpl";"/moduleimpl/1";"ScoView";"GET";
"partition-delete";"/partition/2/delete";"ScoSuperAdmin";"POST"; "partition-delete";"/partition/2/delete";"ScoSuperAdmin";"POST";
"partition-edit";"/partition/1/edit";"ScoSuperAdmin";"POST";"{""partition_name"":""P2BIS"", ""numero"":3,""bul_show_rank"":true,""show_in_lists"":false, ""groups_editable"":true}"
"partition-group-create";"/partition/1/group/create";"ScoSuperAdmin";"POST";"{""group_name"": ""NEW_GROUP""}"
"partition-groups-order";"/partition/1/groups/order";"ScoSuperAdmin";"POST";"[ 1 ]"
"partition-remove_etudiant";"/partition/2/remove_etudiant/10";"ScoSuperAdmin";"POST";
"partition";"/partition/1";"ScoView";"GET";
"permissions";"/permissions";"ScoView";"GET";
"role-add_permission";"/role/customRole/add_permission/ScoUsersView";"ScoSuperAdmin";"POST";
"role-create";"/role/create/customRole";"ScoSuperAdmin";"POST";"{""permissions"": [""ScoView"", ""ScoUsersView""]}"
"role-delete";"/role/customRole/delete";"ScoSuperAdmin";"POST";
"role-edit";"/role/customRole/edit";"ScoSuperAdmin";"POST";"{ ""name"" : ""LaveurDeVitres"", ""permissions"" : [ ""ScoView"" ] }"
"role-remove_permission";"/role/customRole/remove_permission/ScoUsersView";"ScoSuperAdmin";"POST";
"role";"/role/Observateur";"ScoView";"GET";
"roles";"/roles";"ScoView";"GET";
"test-pdf";"/etudiant/etudid/11/formsemestre/1/bulletin";"ScoView";"GET";
"test-pdf";"/etudiant/etudid/11/formsemestre/1/bulletin/pdf";"ScoView";"GET";
"test-pdf";"/etudiant/etudid/11/formsemestre/1/bulletin/short";"ScoView";"GET";
"test-pdf";"/etudiant/etudid/11/formsemestre/1/bulletin/short/pdf";"ScoView";"GET";
"test-pdf";"/etudiant/ine/INE11/formsemestre/1/bulletin";"ScoView";"GET";
"test-pdf";"/etudiant/ine/INE11/formsemestre/1/bulletin/short";"ScoView";"GET";
"test-pdf";"/etudiant/ine/INE11/formsemestre/1/bulletin/short/pdf";"ScoView";"GET";
"test-pdf";"/etudiant/nip/11/formsemestre/1/bulletin";"ScoView";"GET";
"test-pdf";"/etudiant/nip/11/formsemestre/1/bulletin/pdf";"ScoView";"GET";
"test-pdf";"/etudiant/nip/11/formsemestre/1/bulletin/pdf";"ScoView";"GET";
"test-pdf";"/etudiant/nip/11/formsemestre/1/bulletin/short";"ScoView";"GET";
"test-pdf";"/etudiant/nip/11/formsemestre/1/bulletin/short/pdf";"ScoView";"GET";
"user-create";"/user/create";"ScoSuperAdmin";"POST";"{""user_name"": ""alain"", ""dept"": null, ""nom"": ""alain"", ""prenom"": ""bruno"", ""active"": true }" "user-create";"/user/create";"ScoSuperAdmin";"POST";"{""user_name"": ""alain"", ""dept"": null, ""nom"": ""alain"", ""prenom"": ""bruno"", ""active"": true }"
"user-edit";"/user/10/edit";"ScoSuperAdmin";"POST";"{ ""dept"": ""TAPI"", ""nom"": ""alain2"", ""prenom"": ""bruno2"", ""active"": false }" "user-edit";"/user/10/edit";"ScoSuperAdmin";"POST";"{ ""dept"": ""TAPI"", ""nom"": ""alain2"", ""prenom"": ""bruno2"", ""active"": false }"
"user-password";"/user/3/password";"ScoSuperAdmin";"POST";"{ ""password"": ""rePlaCemeNT456averylongandcomplicated"" }" "user-password";"/user/3/password";"ScoSuperAdmin";"POST";"{ ""password"": ""rePlaCemeNT456averylongandcomplicated"" }"
"user-password";"/user/3/password";"ScoSuperAdmin";"POST";"{ ""password"": ""too_simple"" }" "user-password";"/user/3/password";"ScoSuperAdmin";"POST";"{ ""password"": ""too_simple"" }"
"user-role-add";"/user/10/role/Observateur/add";"ScoSuperAdmin";"POST"; "user-role-add";"/user/10/role/Observateur/add";"ScoSuperAdmin";"POST";
"user-role-remove";"/user/10/role/Observateur/remove";"ScoSuperAdmin";"POST"; "user-role-remove";"/user/10/role/Observateur/remove";"ScoSuperAdmin";"POST";
"role-create";"/role/create/customRole";"ScoSuperAdmin";"POST";"{""permissions"": [""ScoView"", ""ScoUsersView""]}" "user";"/user/1";"ScoView";"GET";
"role-remove_permission";"/role/customRole/remove_permission/ScoUsersView";"ScoSuperAdmin";"POST"; "users-query";"/users/query?starts_with=u_";"ScoView";"GET";
"role-add_permission";"/role/customRole/add_permission/ScoUsersView";"ScoSuperAdmin";"POST";
"role-edit";"/role/customRole/edit";"ScoSuperAdmin";"POST";"{ ""name"" : ""LaveurDeVitres"", ""permissions"" : [ ""ScoView"" ] }"
"role-delete";"/role/customRole/delete";"ScoSuperAdmin";"POST";
"logos";"/logos";"ScoSuperAdmin";"GET";
"logo";"/logo/B";"ScoSuperAdmin";"GET";
"departement-logos";"/departement/TAPI/logos";"ScoSuperAdmin";"GET";
"departement-logos";"/departement/id/1/logos";"ScoSuperAdmin";"GET";
"departement-logo";"/departement/TAPI/logo/D";"ScoSuperAdmin";"GET";
"departement-logo";"/departement/id/1/logo/D";"ScoSuperAdmin";"GET";
"test-pdf";"/etudiant/nip/11/formsemestre/1/bulletin/pdf";"ScoView";"GET";
"test-pdf";"/etudiant/nip/11/formsemestre/1/bulletin/pdf";"ScoView";"GET";
"test-pdf";"/etudiant/etudid/11/formsemestre/1/bulletin/short/pdf";"ScoView";"GET";
"test-pdf";"/etudiant/ine/INE11/formsemestre/1/bulletin/short/pdf";"ScoView";"GET";
"test-pdf";"/etudiant/nip/11/formsemestre/1/bulletin/short/pdf";"ScoView";"GET";
"test-pdf";"/etudiant/etudid/11/formsemestre/1/bulletin/pdf";"ScoView";"GET";
"test-pdf";"/etudiant/etudid/11/formsemestre/1/bulletin/short";"ScoView";"GET";
"test-pdf";"/etudiant/ine/INE11/formsemestre/1/bulletin/short";"ScoView";"GET";
"test-pdf";"/etudiant/nip/11/formsemestre/1/bulletin/short";"ScoView";"GET";
"test-pdf";"/etudiant/etudid/11/formsemestre/1/bulletin";"ScoView";"GET";
"test-pdf";"/etudiant/ine/INE11/formsemestre/1/bulletin";"ScoView";"GET";
"test-pdf";"/etudiant/nip/11/formsemestre/1/bulletin";"ScoView";"GET";

Can't render this file because it contains an unexpected character in line 41 and column 2.

View File

@ -7,7 +7,7 @@ A utiliser avec debug.py (côté serveur).
La classe ScoFake offre un ensemble de raccourcis permettant d'écrire La classe ScoFake offre un ensemble de raccourcis permettant d'écrire
facilement des tests ou de reproduire des bugs. facilement des tests ou de reproduire des bugs.
""" """
import datetime
from functools import wraps from functools import wraps
import random import random
import sys import sys
@ -16,7 +16,14 @@ import typing
from app import db, log from app import db, log
from app.auth.models import User from app.auth.models import User
from app.models import Departement, Formation, FormationModalite, Matiere from app.models import (
Departement,
Evaluation,
Formation,
FormationModalite,
Matiere,
ModuleImpl,
)
from app.scodoc import notesdb as ndb from app.scodoc import notesdb as ndb
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
from app.scodoc import sco_edit_matiere from app.scodoc import sco_edit_matiere
@ -297,9 +304,8 @@ class ScoFake(object):
def create_evaluation( def create_evaluation(
self, self,
moduleimpl_id=None, moduleimpl_id=None,
jour=None, date_debut: datetime.datetime = None,
heure_debut="8h00", date_fin: datetime.datetime = None,
heure_fin="9h00",
description=None, description=None,
note_max=20, note_max=20,
coefficient=None, coefficient=None,
@ -307,14 +313,17 @@ class ScoFake(object):
publish_incomplete=None, publish_incomplete=None,
evaluation_type=None, evaluation_type=None,
numero=None, numero=None,
): ) -> dict:
args = locals() args = locals()
del args["self"] del args["self"]
oid = sco_evaluation_db.do_evaluation_create(**args) moduleimpl: ModuleImpl = db.session.get(ModuleImpl, moduleimpl_id)
oids = sco_evaluation_db.do_evaluation_list(args={"evaluation_id": oid}) assert moduleimpl
if not oids: evaluation: Evaluation = Evaluation.create(moduleimpl=moduleimpl, **args)
raise ScoValueError("evaluation not created !") db.session.add(evaluation)
return oids[0] db.session.commit()
eval_dict = evaluation.to_dict()
eval_dict["id"] = evaluation.id
return eval_dict
@logging_meth @logging_meth
def create_note( def create_note(
@ -406,7 +415,7 @@ class ScoFake(object):
for e_idx in range(1, nb_evaluations_per_module + 1): for e_idx in range(1, nb_evaluations_per_module + 1):
e = self.create_evaluation( e = self.create_evaluation(
moduleimpl_id=moduleimpl_id, moduleimpl_id=moduleimpl_id,
jour=date_debut, date_debut=datetime.datetime.strptime(date_debut, "%d/%m/%Y"),
description="evaluation test %s" % e_idx, description="evaluation test %s" % e_idx,
coefficient=1.0, coefficient=1.0,
) )
@ -427,7 +436,7 @@ class ScoFake(object):
for e in eval_list: for e in eval_list:
for idx, etud in enumerate(etuds): for idx, etud in enumerate(etuds):
self.create_note( self.create_note(
evaluation_id=e["id"], evaluation_id=e["evaluation_id"],
etudid=etud["etudid"], etudid=etud["etudid"],
note=notes[idx % len(notes)], note=notes[idx % len(notes)],
) )

View File

@ -1,9 +1,9 @@
""" """
Quelques fonctions d'initialisation pour tests unitaires Quelques fonctions d'initialisation pour tests unitaires
""" """
import datetime
from app import db, models from app import db, models
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
@ -133,7 +133,7 @@ def build_modules_with_evaluations(
for _ in range(nb_evals_per_modimpl): for _ in range(nb_evals_per_modimpl):
e = G.create_evaluation( e = G.create_evaluation(
moduleimpl_id=moduleimpl_id, moduleimpl_id=moduleimpl_id,
jour="01/01/2021", date_debut=datetime.datetime(2021, 1, 1),
description="evaluation 1", description="evaluation 1",
coefficient=0, coefficient=0,
) )

View File

@ -1,117 +0,0 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
"""
Comptage des absences
"""
# test écrit par Fares Amer, mai 2021 et porté sur ScoDoc 8 en juillet 2021
from tests.unit import sco_fake_gen
from app.scodoc import sco_abs, sco_formsemestre
from app.scodoc import sco_abs_views
def test_abs_counts(test_client):
"""Comptage des absences"""
G = sco_fake_gen.ScoFake(verbose=False)
# --- Création d'étudiants
etud = G.create_etud(code_nip=None)
# --- Création d'une formation
formation_id = G.create_formation(acronyme="")
ue_id = G.create_ue(formation_id=formation_id, acronyme="TST1", titre="ue test")
matiere_id = G.create_matiere(ue_id=ue_id, titre="matière test")
module_id = G.create_module(
matiere_id=matiere_id,
code="TSM1",
coefficient=1.0,
titre="module test",
)
# --- Mise place d'un semestre
formsemestre_id = G.create_formsemestre(
formation_id=formation_id,
semestre_id=1,
date_debut="01/01/2021",
date_fin="30/06/2021",
)
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
_ = G.create_moduleimpl(
module_id=module_id,
formsemestre_id=formsemestre_id,
)
# --- Inscription des étudiants
G.inscrit_etudiant(formsemestre_id, etud)
# --- Saisie absences
etudid = etud["etudid"]
for debut, fin, demijournee in [
("01/01/2020", "31/01/2020", 2), # hors semestre
("15/01/2021", "15/01/2021", 1),
("18/01/2021", "18/01/2021", 0),
("19/01/2021", "19/01/2021", 2),
("22/01/2021", "22/01/2021", 1),
("30/06/2021", "30/06/2021", 2), # dernier jour
]:
sco_abs_views.doSignaleAbsence(
datedebut=debut,
datefin=fin,
demijournee=demijournee,
etudid=etudid,
)
# --- Justification de certaines absences
for debut, fin, demijournee in [
("15/01/2021", "15/01/2021", 1),
("18/01/2021", "18/01/2021", 0),
("19/01/2021", "19/01/2021", 2),
]:
sco_abs_views.doJustifAbsence(
datedebut=debut,
datefin=fin,
demijournee=demijournee,
etudid=etudid,
)
# --- Utilisation de get_abs_count() de sco_abs
nbabs, nbabsjust = sco_abs.get_abs_count(etudid, sem)
# --- Utilisation de sco_abs.count_abs()
nb_abs2 = sco_abs.count_abs(etudid=etudid, debut="2021-01-01", fin="2021-06-30")
nb_absj2 = sco_abs.count_abs_just(
etudid=etudid, debut="2021-01-01", fin="2021-06-30"
)
assert nbabs == nb_abs2 == 7
assert nbabsjust == nb_absj2 == 4
# --- Nombre de justificatifs:
justifs = sco_abs.list_abs_justifs(etudid, "2021-01-01", datefin="2021-06-30")
assert len(justifs) == 4
# --- Suppression d'absence
_ = sco_abs_views.doAnnuleAbsence("19/01/2021", "19/01/2021", 2, etudid=etudid)
# --- Vérification
justifs_2 = sco_abs.list_abs_justifs(etudid, "2021-01-01", datefin="2021-06-30")
assert len(justifs_2) == len(justifs)
new_nbabs, _ = sco_abs.get_abs_count(etudid, sem) # version cachée
new_nbabs2 = sco_abs.count_abs(etudid=etudid, debut="2021-01-01", fin="2021-06-30")
assert new_nbabs == new_nbabs2
assert new_nbabs == (nbabs - 2) # on a supprimé deux absences
# --- annulation absence sans supprimer le justificatif
sco_abs_views.AnnuleAbsencesDatesNoJust(etudid, ["2021-01-15"])
nbabs_3, nbjust_3 = sco_abs.get_abs_count(etudid, sem)
assert nbabs_3 == new_nbabs
justifs_3 = sco_abs.list_abs_justifs(etudid, "2021-01-01", datefin="2021-06-30")
assert len(justifs_3) == len(justifs_2)
# XXX à continuer

View File

@ -1,344 +0,0 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
"""
Créer et justifier des absences en utilisant le parametre demijournee
"""
# test écrit par Fares Amer, mai 2021 et porté sur ScoDoc 8 en juillet 2021
import json
from tests.unit import sco_fake_gen
from app import db
from app.models import Module
from app.scodoc import sco_abs
from app.scodoc import sco_abs_views
from app.scodoc import sco_groups
from app.scodoc import sco_formsemestre
from app.scodoc import sco_preferences
from app.views import absences
def test_abs_demijournee(test_client):
"""Opération élémentaires sur les absences, tests demi-journées
Travaille dans base TEST00 (defaut)
"""
G = sco_fake_gen.ScoFake(verbose=False)
# --- Création d'étudiants
etud = G.create_etud(code_nip=None)
# --- Création d'une formation
formation_id = G.create_formation(acronyme="")
ue_id = G.create_ue(formation_id=formation_id, acronyme="TST1", titre="ue test")
matiere_id = G.create_matiere(ue_id=ue_id, titre="matière test")
module_id = G.create_module(
matiere_id=matiere_id,
code="TSM1",
coefficient=1.0,
titre="module test",
)
# --- Mise place d'un semestre
formsemestre_id = G.create_formsemestre(
formation_id=formation_id,
semestre_id=1,
date_debut="01/01/2021",
date_fin="30/06/2021",
)
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
_ = G.create_moduleimpl(
module_id=module_id,
formsemestre_id=formsemestre_id,
)
# --- Inscription des étudiants
G.inscrit_etudiant(formsemestre_id, etud)
# --- Saisie absences
etudid = etud["etudid"]
_ = sco_abs_views.doSignaleAbsence(
"15/01/2021",
"15/01/2021",
demijournee=2,
etudid=etudid,
)
_ = sco_abs_views.doSignaleAbsence(
"18/01/2021",
"18/01/2021",
demijournee=1,
etudid=etudid,
)
_ = sco_abs_views.doSignaleAbsence(
"19/01/2021",
"19/01/2021",
demijournee=0,
etudid=etudid,
)
# --- Justification de certaines absences
_ = sco_abs_views.doJustifAbsence(
"18/01/2021",
"18/01/2021",
demijournee=1,
etudid=etudid,
)
_ = sco_abs_views.doJustifAbsence(
"19/01/2021",
"19/01/2021",
demijournee=2,
etudid=etudid,
)
# NE JUSTIFIE QUE LE MATIN MALGRES LE PARAMETRE demijournee = 2
# --- Test
nbabs, nbabs_just = sco_abs.get_abs_count(etudid, sem)
assert (
nbabs == 4
) # l'étudiant a été absent le 15 journée compléte (2 abs : 1 matin, 1 apres midi) et le 18 (1 matin), et le 19 (1 apres midi).
assert nbabs_just == 2 # Justifie abs du matin + abs après midi
def test_abs_basic(test_client):
"""creation de 10 étudiants, formation, semestre, ue, module, absences le matin, l'apres midi, la journée compléte
et justification d'absences, supression d'absences, création d'une liste etat absences, creation d'un groupe afin
de tester la fonction EtatAbsencesGroupes
Fonctions de l'API utilisé :
- doSignaleAbsence
- doAnnuleAbsence
- doJustifAbsence
- get_partition_groups
- get_partitions_list
- sco_abs.get_abs_count(etudid, sem)
- ListeAbsEtud
- partition_create
- create_group
- set_group
- EtatAbsencesGr
- AddBilletAbsence
- billets_etud
"""
G = sco_fake_gen.ScoFake(verbose=False)
# --- Création d'étudiants
etuds = [G.create_etud(code_nip=None) for _ in range(10)]
# --- Création d'une formation
formation_id = G.create_formation(acronyme="")
ue_id = G.create_ue(formation_id=formation_id, acronyme="TST1", titre="ue test")
matiere_id = G.create_matiere(ue_id=ue_id, titre="matière test")
module_id = G.create_module(
matiere_id=matiere_id,
code="TSM1",
coefficient=1.0,
titre="module test",
)
# --- Mise place d'un semestre
formsemestre_id = G.create_formsemestre(
formation_id=formation_id,
semestre_id=1,
date_debut="01/01/2021",
date_fin="30/06/2021",
)
moduleimpl_id = G.create_moduleimpl(
module_id=module_id,
formsemestre_id=formsemestre_id,
)
# --- Inscription des étudiants
for etud in etuds:
G.inscrit_etudiant(formsemestre_id, etud)
# --- Création d'une évaluation
e = G.create_evaluation(
moduleimpl_id=moduleimpl_id,
jour="22/01/2021",
description="evaluation test",
coefficient=1.0,
)
# --- Saisie absences
etudid = etuds[0]["etudid"]
_ = sco_abs_views.doSignaleAbsence(
"15/01/2021",
"15/01/2021",
demijournee=1,
etudid=etudid,
)
_ = sco_abs_views.doSignaleAbsence(
"18/01/2021",
"18/01/2021",
demijournee=0,
etudid=etudid,
)
_ = sco_abs_views.doSignaleAbsence(
"19/01/2021",
"19/01/2021",
demijournee=2,
etudid=etudid,
)
_ = sco_abs_views.doSignaleAbsence(
"22/01/2021",
"22/01/2021",
demijournee=1,
etudid=etudid,
)
# --- Justification de certaines absences
_ = sco_abs_views.doJustifAbsence(
"15/01/2021",
"15/01/2021",
demijournee=1,
etudid=etudid,
)
_ = sco_abs_views.doJustifAbsence(
"18/01/2021",
"18/01/2021",
demijournee=0,
etudid=etudid,
)
_ = sco_abs_views.doJustifAbsence(
"19/01/2021",
"19/01/2021",
demijournee=2,
etudid=etudid,
)
# --- Test
b = sco_abs.is_work_saturday()
assert b == 0 # samedi ne sont pas compris
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
nbabs, nbabsjust = sco_abs.get_abs_count(etudid, sem)
# l'étudiant a été absent le 15 (apres midi) , (16 et 17 we),
# 18 (matin) et 19 janvier (matin et apres midi), et 22 (matin)
assert nbabs == 5
# l'étudiant justifie ses abs du 15, 18 et 19
assert nbabsjust == 4
# --- Suppression d'une absence et d'une justification
_ = sco_abs_views.doAnnuleAbsence("19/01/2021", "19/01/2021", 2, etudid=etudid)
nbabs, nbabsjust = sco_abs.get_abs_count(etudid, sem)
assert nbabs == 3
assert nbabsjust == 2
# --- suppression d'une justification pas encore disponible à l'aide de python.
# --- Création d'une liste d'abs
liste_abs = sco_abs_views.ListeAbsEtud(
etudid, format="json", absjust_only=1, sco_year="2020"
).get_data(as_text=True)
liste_abs2 = sco_abs_views.ListeAbsEtud(
etudid, format="json", sco_year="2020"
).get_data(as_text=True)
load_liste_abs = json.loads(liste_abs)
load_liste_abs2 = json.loads(liste_abs2)
assert len(load_liste_abs2) == 1
assert len(load_liste_abs) == 2
assert load_liste_abs2[0]["ampm"] == 1
assert load_liste_abs2[0]["datedmy"] == "22/01/2021"
mod = db.session.get(Module, module_id)
assert load_liste_abs2[0]["exams"] == mod.code
# absjust_only -> seulement les abs justifiés
# --- Création d'un groupe
_ = sco_groups.partition_create(
formsemestre_id=sem["formsemestre_id"],
partition_name="Eleve",
)
li1 = sco_groups.get_partitions_list(sem["formsemestre_id"])
_ = sco_groups.create_group(li1[0]["partition_id"], "Groupe 1")
# --- Affectation des élèves dans des groupes
li_grp1 = sco_groups.get_partition_groups(li1[0])
for etud in etuds:
sco_groups.set_group(etud["etudid"], li_grp1[0]["group_id"])
# --- Test de EtatAbsencesGroupes
grp1_abs = absences.EtatAbsencesGr(
group_ids=[li_grp1[0]["group_id"]],
debut="01/01/2021",
fin="30/06/2021",
format="json",
)
# grp1_abs est une Response car on a appelé une vue (1er appel)
load_grp1_abs = json.loads(grp1_abs.get_data(as_text=True))
assert len(load_grp1_abs) == 10
tab_id = [] # tab des id present dans load_grp1_abs
for un_etud in load_grp1_abs:
tab_id.append(un_etud["etudid"])
for (
etud
) in (
etuds
): # verification si tous les etudiants sont present dans la liste du groupe d'absence
assert etud["etudid"] in tab_id
for un_etud in load_grp1_abs:
if un_etud["etudid"] == etudid:
assert un_etud["nbabs"] == 3
assert un_etud["nbjustifs_noabs"] == 2
assert un_etud["nbabsjust"] == 2
assert un_etud["nbabsnonjust"] == 1
assert un_etud["nomprenom"] == etuds[0]["nomprenom"]
# --- Création de billets
# Active la gestion de billets:
sco_preferences.get_base_preferences().set(None, "handle_billets_abs", 1)
b1 = absences.AddBilletAbsence(
begin="2021-01-22 00:00",
end="2021-01-22 23:59",
etudid=etudid,
description="abs du 22",
justified=False,
code_nip=etuds[0]["code_nip"],
code_ine=etuds[0]["code_ine"],
)
b2 = absences.AddBilletAbsence(
begin="2021-01-15 00:00",
end="2021-01-15 23:59",
etudid=etudid,
description="abs du 15",
code_nip=etuds[0]["code_nip"],
code_ine=etuds[0]["code_ine"],
)
li_bi = absences.billets_etud(etudid=etudid, format="json").get_data(as_text=True)
assert isinstance(li_bi, str)
load_li_bi = json.loads(li_bi)
assert len(load_li_bi) == 2
assert (
load_li_bi[1]["description"] == "abs du 15"
or load_li_bi[1]["description"] == "abs du 22"
)

View File

@ -7,12 +7,15 @@ ses fonctions liées
Ecrit par HARTMANN Matthias (en s'inspirant de tests.unit.test_abs_count.py par Fares Amer ) Ecrit par HARTMANN Matthias (en s'inspirant de tests.unit.test_abs_count.py par Fares Amer )
""" """
import pytest
import app.scodoc.sco_assiduites as scass import app.scodoc.sco_assiduites as scass
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app import db from app import db
from app.models import Assiduite, FormSemestre, Identite, Justificatif, ModuleImpl from app.models import Assiduite, FormSemestre, Identite, Justificatif, ModuleImpl
from app.scodoc import sco_abs_views, sco_formsemestre
# from app.scodoc import sco_abs_views, sco_formsemestre TODO-ASSIDUITE
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
from tests.unit import sco_fake_gen from tests.unit import sco_fake_gen
from tools import downgrade_module, migrate_abs_to_assiduites from tools import downgrade_module, migrate_abs_to_assiduites
@ -36,6 +39,7 @@ def test_bi_directional_enum(test_client):
assert BiInt.inverse()[1] == BiInt.A and BiInt.inverse()[2] == BiInt.B assert BiInt.inverse()[1] == BiInt.A and BiInt.inverse()[2] == BiInt.B
@pytest.mark.skip # XXX TODO-ASSIDUITE (issue #690)
def test_general(test_client): def test_general(test_client):
"""tests général du modèle assiduite""" """tests général du modèle assiduite"""
@ -76,7 +80,9 @@ def test_general(test_client):
date_fin="31/07/2024", date_fin="31/07/2024",
) )
formsemestre_1 = sco_formsemestre.get_formsemestre(formsemestre_id_1) formsemestre_1 = sco_formsemestre.get_formsemestre(
formsemestre_id_1
) # Utiliser plutot FormSemestre de nos jours TODO-ASSIDUITE
formsemestre_2 = sco_formsemestre.get_formsemestre(formsemestre_id_2) formsemestre_2 = sco_formsemestre.get_formsemestre(formsemestre_id_2)
formsemestre_3 = sco_formsemestre.get_formsemestre(formsemestre_id_3) formsemestre_3 = sco_formsemestre.get_formsemestre(formsemestre_id_3)
@ -143,6 +149,7 @@ def test_general(test_client):
editer_supprimer_justificatif(etuds[0]) editer_supprimer_justificatif(etuds[0])
@pytest.mark.skip # XXX TODO-ASSIDUITE (issue #696)
def verif_migration_abs_assiduites(): def verif_migration_abs_assiduites():
"""Vérification que le script de migration fonctionne correctement""" """Vérification que le script de migration fonctionne correctement"""
downgrade_module(assiduites=True, justificatifs=True) downgrade_module(assiduites=True, justificatifs=True)
@ -301,7 +308,7 @@ def verif_migration_abs_assiduites():
False, False,
), # 3 assi 22-23-24/02/2023 08h > 13h (3dj) JUSTI(ext) ), # 3 assi 22-23-24/02/2023 08h > 13h (3dj) JUSTI(ext)
]: ]:
sco_abs_views.doSignaleAbsence( sco_abs_views.doSignaleAbsence( # TODO-ASSIDUITE
datedebut=debut, datedebut=debut,
datefin=fin, datefin=fin,
demijournee=demijournee, demijournee=demijournee,

View File

@ -22,7 +22,7 @@ from tests.unit import test_sco_basic
DEPT = TestConfig.DEPT_TEST DEPT = TestConfig.DEPT_TEST
def test_bulletin(test_client): def test_bulletin_data_classic(test_client):
"""Vérifications sur les bulletins de notes""" """Vérifications sur les bulletins de notes"""
G = sco_fake_gen.ScoFake(verbose=False) G = sco_fake_gen.ScoFake(verbose=False)
app.set_sco_dept(DEPT) app.set_sco_dept(DEPT)

View File

@ -2,6 +2,7 @@
Test modèles évaluations avec poids BUT Test modèles évaluations avec poids BUT
et calcul moyennes modules et calcul moyennes modules
""" """
import datetime
import numpy as np import numpy as np
import pandas as pd import pandas as pd
@ -46,7 +47,7 @@ def test_evaluation_poids(test_client):
) )
_e1 = G.create_evaluation( _e1 = G.create_evaluation(
moduleimpl_id=moduleimpl_id, moduleimpl_id=moduleimpl_id,
jour="01/01/2021", date_debut=datetime.datetime(2021, 1, 1),
description="evaluation 1", description="evaluation 1",
coefficient=0, coefficient=0,
) )
@ -218,7 +219,7 @@ def test_module_moy(test_client):
# Crée une deuxième évaluation dans le même moduleimpl: # Crée une deuxième évaluation dans le même moduleimpl:
evaluation2_id = G.create_evaluation( evaluation2_id = G.create_evaluation(
moduleimpl_id=evaluation1.moduleimpl_id, moduleimpl_id=evaluation1.moduleimpl_id,
jour="02/01/2021", date_debut=datetime.datetime(2021, 1, 2),
description="evaluation 2", description="evaluation 2",
coefficient=coef_e2, coefficient=coef_e2,
)["evaluation_id"] )["evaluation_id"]

View File

@ -15,12 +15,11 @@ import app
from app import db from app import db
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre from app.models import Evaluation, FormSemestre, ModuleImpl
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc import sco_evaluations from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_db from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import notesdb as ndb
from config import TestConfig from config import TestConfig
from tests.unit.test_sco_basic import run_sco_basic from tests.unit.test_sco_basic import run_sco_basic
@ -58,23 +57,24 @@ def test_cache_evaluations(test_client):
# prépare le département avec quelques semestres: # prépare le département avec quelques semestres:
run_sco_basic() run_sco_basic()
# #
sems = sco_formsemestre.do_formsemestre_list() formsemestres = FormSemestre.query
assert len(sems) assert formsemestres.count()
sem_evals = [] evaluation = None
for sem in sems: for formsemestre in formsemestres:
sem_evals = sco_evaluations.do_evaluation_list_in_sem( evaluation: Evaluation = (
sem["formsemestre_id"], with_etat=False Evaluation.query.join(ModuleImpl)
.filter_by(formsemestre_id=formsemestre.id)
.first()
) )
if sem_evals: if evaluation is not None:
break break
if not sem_evals: if evaluation is None:
raise Exception("no evaluations") raise Exception("no evaluations")
# #
evaluation_id = sem_evals[0]["evaluation_id"] eval_notes = sco_evaluation_db.do_evaluation_get_all_notes(evaluation.id)
eval_notes = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
# should have been be cached, except if empty # should have been be cached, except if empty
if eval_notes: if eval_notes:
assert sco_cache.EvaluationCache.get(evaluation_id) assert sco_cache.EvaluationCache.get(evaluation.id)
sco_cache.invalidate_formsemestre(sem["formsemestre_id"]) sco_cache.invalidate_formsemestre(evaluation.moduleimpl.formsemestre.id)
# should have been erased from cache: # should have been erased from cache:
assert not sco_cache.EvaluationCache.get(evaluation_id) assert not sco_cache.EvaluationCache.get(evaluation.id)

View File

@ -2,6 +2,7 @@
Vérif moyennes de modules des bulletins Vérif moyennes de modules des bulletins
et aussi moyennes modules et UE internes (via nt) et aussi moyennes modules et UE internes (via nt)
""" """
import datetime
import numpy as np import numpy as np
from flask import g from flask import g
from config import TestConfig from config import TestConfig
@ -93,13 +94,13 @@ def test_notes_modules(test_client):
coef_2 = 2.0 coef_2 = 2.0
e1 = G.create_evaluation( e1 = G.create_evaluation(
moduleimpl_id=moduleimpl_id, moduleimpl_id=moduleimpl_id,
jour="01/01/2020", date_debut=datetime.datetime(2020, 1, 1),
description="evaluation 1", description="evaluation 1",
coefficient=coef_1, coefficient=coef_1,
) )
e2 = G.create_evaluation( e2 = G.create_evaluation(
moduleimpl_id=moduleimpl_id, moduleimpl_id=moduleimpl_id,
jour="01/01/2020", date_debut=datetime.datetime(2020, 1, 1),
description="evaluation 2", description="evaluation 2",
coefficient=coef_2, coefficient=coef_2,
) )
@ -107,16 +108,16 @@ def test_notes_modules(test_client):
note_1 = 12.0 note_1 = 12.0
note_2 = 13.0 note_2 = 13.0
_, _, _ = G.create_note( _, _, _ = G.create_note(
evaluation_id=e1["id"], etudid=etuds[0]["etudid"], note=note_1 evaluation_id=e1["evaluation_id"], etudid=etuds[0]["etudid"], note=note_1
) )
_, _, _ = G.create_note( _, _, _ = G.create_note(
evaluation_id=e2["id"], etudid=etuds[0]["etudid"], note=note_2 evaluation_id=e2["evaluation_id"], etudid=etuds[0]["etudid"], note=note_2
) )
_, _, _ = G.create_note( _, _, _ = G.create_note(
evaluation_id=e1["id"], etudid=etuds[1]["etudid"], note=note_1 / 2 evaluation_id=e1["evaluation_id"], etudid=etuds[1]["etudid"], note=note_1 / 2
) )
_, _, _ = G.create_note( _, _, _ = G.create_note(
evaluation_id=e2["id"], etudid=etuds[1]["etudid"], note=note_2 / 3 evaluation_id=e2["evaluation_id"], etudid=etuds[1]["etudid"], note=note_2 / 3
) )
b = sco_bulletins.formsemestre_bulletinetud_dict( b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"] sem["formsemestre_id"], etud["etudid"]
@ -138,16 +139,24 @@ def test_notes_modules(test_client):
) )
# Absence à une évaluation # Absence à une évaluation
_, _, _ = G.create_note(evaluation_id=e1["id"], etudid=etudid, note=None) # abs _, _, _ = G.create_note(
_, _, _ = G.create_note(evaluation_id=e2["id"], etudid=etudid, note=note_2) evaluation_id=e1["evaluation_id"], etudid=etudid, note=None
) # abs
_, _, _ = G.create_note(
evaluation_id=e2["evaluation_id"], etudid=etudid, note=note_2
)
b = sco_bulletins.formsemestre_bulletinetud_dict( b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"] sem["formsemestre_id"], etud["etudid"]
) )
note_th = (coef_1 * 0.0 + coef_2 * note_2) / (coef_1 + coef_2) note_th = (coef_1 * 0.0 + coef_2 * note_2) / (coef_1 + coef_2)
assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(note_th) assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(note_th)
# Absences aux deux évaluations # Absences aux deux évaluations
_, _, _ = G.create_note(evaluation_id=e1["id"], etudid=etudid, note=None) # abs _, _, _ = G.create_note(
_, _, _ = G.create_note(evaluation_id=e2["id"], etudid=etudid, note=None) # abs evaluation_id=e1["evaluation_id"], etudid=etudid, note=None
) # abs
_, _, _ = G.create_note(
evaluation_id=e2["evaluation_id"], etudid=etudid, note=None
) # abs
b = sco_bulletins.formsemestre_bulletinetud_dict( b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"] sem["formsemestre_id"], etud["etudid"]
) )
@ -162,9 +171,11 @@ def test_notes_modules(test_client):
) )
# Note excusée EXC <-> scu.NOTES_NEUTRALISE # Note excusée EXC <-> scu.NOTES_NEUTRALISE
_, _, _ = G.create_note(evaluation_id=e1["id"], etudid=etudid, note=note_1)
_, _, _ = G.create_note( _, _, _ = G.create_note(
evaluation_id=e2["id"], etudid=etudid, note=scu.NOTES_NEUTRALISE evaluation_id=e1["evaluation_id"], etudid=etudid, note=note_1
)
_, _, _ = G.create_note(
evaluation_id=e2["evaluation_id"], etudid=etudid, note=scu.NOTES_NEUTRALISE
) # EXC ) # EXC
b = sco_bulletins.formsemestre_bulletinetud_dict( b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"] sem["formsemestre_id"], etud["etudid"]
@ -179,9 +190,11 @@ def test_notes_modules(test_client):
expected_moy_ue=note_1, expected_moy_ue=note_1,
) )
# Note en attente ATT <-> scu.NOTES_ATTENTE # Note en attente ATT <-> scu.NOTES_ATTENTE
_, _, _ = G.create_note(evaluation_id=e1["id"], etudid=etudid, note=note_1)
_, _, _ = G.create_note( _, _, _ = G.create_note(
evaluation_id=e2["id"], etudid=etudid, note=scu.NOTES_ATTENTE evaluation_id=e1["evaluation_id"], etudid=etudid, note=note_1
)
_, _, _ = G.create_note(
evaluation_id=e2["evaluation_id"], etudid=etudid, note=scu.NOTES_ATTENTE
) # ATT ) # ATT
b = sco_bulletins.formsemestre_bulletinetud_dict( b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"] sem["formsemestre_id"], etud["etudid"]
@ -197,10 +210,10 @@ def test_notes_modules(test_client):
) )
# Neutralisation (EXC) des 2 évals # Neutralisation (EXC) des 2 évals
_, _, _ = G.create_note( _, _, _ = G.create_note(
evaluation_id=e1["id"], etudid=etudid, note=scu.NOTES_NEUTRALISE evaluation_id=e1["evaluation_id"], etudid=etudid, note=scu.NOTES_NEUTRALISE
) # EXC ) # EXC
_, _, _ = G.create_note( _, _, _ = G.create_note(
evaluation_id=e2["id"], etudid=etudid, note=scu.NOTES_NEUTRALISE evaluation_id=e2["evaluation_id"], etudid=etudid, note=scu.NOTES_NEUTRALISE
) # EXC ) # EXC
b = sco_bulletins.formsemestre_bulletinetud_dict( b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"] sem["formsemestre_id"], etud["etudid"]
@ -216,10 +229,10 @@ def test_notes_modules(test_client):
) )
# Attente (ATT) sur les 2 evals # Attente (ATT) sur les 2 evals
_, _, _ = G.create_note( _, _, _ = G.create_note(
evaluation_id=e1["id"], etudid=etudid, note=scu.NOTES_ATTENTE evaluation_id=e1["evaluation_id"], etudid=etudid, note=scu.NOTES_ATTENTE
) # ATT ) # ATT
_, _, _ = G.create_note( _, _, _ = G.create_note(
evaluation_id=e2["id"], etudid=etudid, note=scu.NOTES_ATTENTE evaluation_id=e2["evaluation_id"], etudid=etudid, note=scu.NOTES_ATTENTE
) # ATT ) # ATT
b = sco_bulletins.formsemestre_bulletinetud_dict( b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"] sem["formsemestre_id"], etud["etudid"]
@ -277,7 +290,7 @@ def test_notes_modules(test_client):
{"etudid": etudid, "moduleimpl_id": moduleimpl_id}, {"etudid": etudid, "moduleimpl_id": moduleimpl_id},
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
) )
_, _, _ = G.create_note(evaluation_id=e1["id"], etudid=etudid, note=12.5) _, _, _ = G.create_note(evaluation_id=e1["evaluation_id"], etudid=etudid, note=12.5)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
mod_stats = nt.get_mod_stats(moduleimpl_id) mod_stats = nt.get_mod_stats(moduleimpl_id)
@ -301,11 +314,13 @@ def test_notes_modules(test_client):
# Note dans module 2: # Note dans module 2:
e_m2 = G.create_evaluation( e_m2 = G.create_evaluation(
moduleimpl_id=moduleimpl_id2, moduleimpl_id=moduleimpl_id2,
jour="01/01/2020", date_debut=datetime.datetime(2020, 1, 1),
description="evaluation mod 2", description="evaluation mod 2",
coefficient=1.0, coefficient=1.0,
) )
_, _, _ = G.create_note(evaluation_id=e_m2["id"], etudid=etudid, note=19.5) _, _, _ = G.create_note(
evaluation_id=e_m2["evaluation_id"], etudid=etudid, note=19.5
)
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
ue_status = nt.get_etud_ue_status(etudid, ue_id) ue_status = nt.get_etud_ue_status(etudid, ue_id)
@ -314,20 +329,22 @@ def test_notes_modules(test_client):
# 2 modules, notes EXC dans le premier, note valide n dans le second # 2 modules, notes EXC dans le premier, note valide n dans le second
# la moyenne de l'UE doit être n # la moyenne de l'UE doit être n
_, _, _ = G.create_note( _, _, _ = G.create_note(
evaluation_id=e1["id"], etudid=etudid, note=scu.NOTES_NEUTRALISE evaluation_id=e1["evaluation_id"], etudid=etudid, note=scu.NOTES_NEUTRALISE
) # EXC ) # EXC
_, _, _ = G.create_note( _, _, _ = G.create_note(
evaluation_id=e2["id"], etudid=etudid, note=scu.NOTES_NEUTRALISE evaluation_id=e2["evaluation_id"], etudid=etudid, note=scu.NOTES_NEUTRALISE
) # EXC ) # EXC
_, _, _ = G.create_note(evaluation_id=e_m2["id"], etudid=etudid, note=12.5)
_, _, _ = G.create_note( _, _, _ = G.create_note(
evaluation_id=e1["id"], etudid=etuds[1]["etudid"], note=11.0 evaluation_id=e_m2["evaluation_id"], etudid=etudid, note=12.5
) )
_, _, _ = G.create_note( _, _, _ = G.create_note(
evaluation_id=e2["id"], etudid=etuds[1]["etudid"], note=11.0 evaluation_id=e1["evaluation_id"], etudid=etuds[1]["etudid"], note=11.0
) )
_, _, _ = G.create_note( _, _, _ = G.create_note(
evaluation_id=e_m2["id"], etudid=etuds[1]["etudid"], note=11.0 evaluation_id=e2["evaluation_id"], etudid=etuds[1]["etudid"], note=11.0
)
_, _, _ = G.create_note(
evaluation_id=e_m2["evaluation_id"], etudid=etuds[1]["etudid"], note=11.0
) )
b = sco_bulletins.formsemestre_bulletinetud_dict( b = sco_bulletins.formsemestre_bulletinetud_dict(
@ -385,16 +402,20 @@ def test_notes_modules_att_dem(test_client):
coef_1 = 1.0 coef_1 = 1.0
e1 = G.create_evaluation( e1 = G.create_evaluation(
moduleimpl_id=moduleimpl_id, moduleimpl_id=moduleimpl_id,
jour="01/01/2020", date_debut=datetime.datetime(2020, 1, 1),
description="evaluation 1", description="evaluation 1",
coefficient=coef_1, coefficient=coef_1,
) )
# Attente (ATT) sur les 2 evals # Attente (ATT) sur les 2 evals
_, _, _ = G.create_note( _, _, _ = G.create_note(
evaluation_id=e1["id"], etudid=etuds[0]["etudid"], note=scu.NOTES_ATTENTE evaluation_id=e1["evaluation_id"],
etudid=etuds[0]["etudid"],
note=scu.NOTES_ATTENTE,
) # ATT ) # ATT
_, _, _ = G.create_note( _, _, _ = G.create_note(
evaluation_id=e1["id"], etudid=etuds[1]["etudid"], note=scu.NOTES_ATTENTE evaluation_id=e1["evaluation_id"],
etudid=etuds[1]["etudid"],
note=scu.NOTES_ATTENTE,
) # ATT ) # ATT
# Démission du premier étudiant # Démission du premier étudiant
sco_formsemestre_inscriptions.do_formsemestre_demission( sco_formsemestre_inscriptions.do_formsemestre_demission(
@ -435,7 +456,7 @@ def test_notes_modules_att_dem(test_client):
# Saisie note ABS pour le deuxième etud # Saisie note ABS pour le deuxième etud
_, _, _ = G.create_note( _, _, _ = G.create_note(
evaluation_id=e1["id"], etudid=etuds[1]["etudid"], note=None evaluation_id=e1["evaluation_id"], etudid=etuds[1]["etudid"], note=None
) )
nt = check_nt( nt = check_nt(
etuds[1]["etudid"], etuds[1]["etudid"],

View File

@ -1,14 +1,14 @@
"""Test calculs rattrapages """Test calculs rattrapages
""" """
import datetime
import app import app
from app import db from app import db
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_but import ResultatsSemestreBUT
from app.models import FormSemestre, ModuleImpl from app.models import Evaluation, FormSemestre, ModuleImpl
from app.scodoc import ( from app.scodoc import (
sco_bulletins, sco_bulletins,
sco_evaluation_db,
sco_formsemestre, sco_formsemestre,
sco_saisie_notes, sco_saisie_notes,
) )
@ -58,14 +58,14 @@ def test_notes_rattrapage(test_client):
# --- Creation évaluation # --- Creation évaluation
e = G.create_evaluation( e = G.create_evaluation(
moduleimpl_id=moduleimpl_id, moduleimpl_id=moduleimpl_id,
jour="01/01/2020", date_debut=datetime.datetime(2020, 1, 1),
description="evaluation test", description="evaluation test",
coefficient=1.0, coefficient=1.0,
) )
# --- Création d'une évaluation "de rattrapage" # --- Création d'une évaluation "de rattrapage"
e_rat = G.create_evaluation( e_rat = G.create_evaluation(
moduleimpl_id=moduleimpl_id, moduleimpl_id=moduleimpl_id,
jour="02/01/2020", date_debut=datetime.datetime(2020, 1, 2),
description="evaluation rattrapage", description="evaluation rattrapage",
coefficient=1.0, coefficient=1.0,
evaluation_type=scu.EVALUATION_RATTRAPAGE, evaluation_type=scu.EVALUATION_RATTRAPAGE,
@ -130,7 +130,9 @@ def test_notes_rattrapage(test_client):
# Note moyenne: reviens à 10/20 # Note moyenne: reviens à 10/20
assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(10.0) assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(10.0)
# Supprime l'évaluation de rattrapage: # Supprime l'évaluation de rattrapage:
sco_evaluation_db.do_evaluation_delete(e_rat["id"]) evaluation = db.session.get(Evaluation, e_rat["id"])
assert evaluation
evaluation.delete()
b = sco_bulletins.formsemestre_bulletinetud_dict( b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"] sem["formsemestre_id"], etud["etudid"]
) )
@ -139,7 +141,7 @@ def test_notes_rattrapage(test_client):
# Création évaluation session 2: # Création évaluation session 2:
e_session2 = G.create_evaluation( e_session2 = G.create_evaluation(
moduleimpl_id=moduleimpl_id, moduleimpl_id=moduleimpl_id,
jour="02/01/2020", date_debut=datetime.datetime(2020, 1, 2),
description="evaluation session 2", description="evaluation session 2",
coefficient=1.0, coefficient=1.0,
evaluation_type=scu.EVALUATION_SESSION2, evaluation_type=scu.EVALUATION_SESSION2,

Some files were not shown because too many files have changed in this diff Show More