forked from ScoDoc/ScoDoc
Compare commits
10 Commits
1e9796528f
...
22c2fe0f3b
Author | SHA1 | Date |
---|---|---|
Jean-Marie Place | 22c2fe0f3b | |
iziram | f3ceaff307 | |
iziram | 3aa5629d1b | |
iziram | e5b1082e1d | |
Emmanuel Viennet | 2873253cb4 | |
Emmanuel Viennet | 7a4cff2623 | |
Emmanuel Viennet | b04930870e | |
Emmanuel Viennet | 740749e37e | |
iziram | 70cda5a553 | |
iziram | 4e5e15092e |
|
@ -6,18 +6,18 @@
|
|||
"""ScoDoc 9 API : Assiduités
|
||||
"""
|
||||
from datetime import datetime
|
||||
from flask_json import as_json
|
||||
from flask import g, request
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
from flask import g, request
|
||||
from flask_json import as_json
|
||||
from flask_login import current_user, login_required
|
||||
|
||||
from app import db, log
|
||||
import app.scodoc.sco_assiduites as scass
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import db
|
||||
from app.api import api_bp as bp
|
||||
from app.api import api_web_bp
|
||||
from app.api import get_model_api_object
|
||||
from app.api import api_web_bp, get_model_api_object, tools
|
||||
from app.decorators import permission_required, scodoc
|
||||
from app.models import Assiduite, FormSemestre, Identite, ModuleImpl
|
||||
from app.models import Assiduite, FormSemestre, Identite, ModuleImpl, Scolog
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_utils import json_error
|
||||
|
@ -47,15 +47,35 @@ def assiduite(assiduite_id: int = None):
|
|||
return get_model_api_object(Assiduite, assiduite_id, Identite)
|
||||
|
||||
|
||||
@bp.route("/assiduites/<int:etudid>/count", defaults={"with_query": False})
|
||||
@bp.route("/assiduites/<int:etudid>/count/query", defaults={"with_query": True})
|
||||
@api_web_bp.route("/assiduites/<int:etudid>/count", defaults={"with_query": False})
|
||||
@api_web_bp.route("/assiduites/<int:etudid>/count/query", defaults={"with_query": True})
|
||||
# etudid
|
||||
@bp.route("/assiduites/<etudid>/count", defaults={"with_query": False})
|
||||
@api_web_bp.route("/assiduites/<etudid>/count", defaults={"with_query": False})
|
||||
@bp.route("/assiduites/<etudid>/count/query", defaults={"with_query": True})
|
||||
@api_web_bp.route("/assiduites/<etudid>/count/query", defaults={"with_query": True})
|
||||
@bp.route("/assiduites/etudid/<etudid>/count", defaults={"with_query": False})
|
||||
@api_web_bp.route("/assiduites/etudid/<etudid>/count", defaults={"with_query": False})
|
||||
@bp.route("/assiduites/etudid/<etudid>/count/query", defaults={"with_query": True})
|
||||
@api_web_bp.route(
|
||||
"/assiduites/etudid/<etudid>/count/query", defaults={"with_query": True}
|
||||
)
|
||||
# nip
|
||||
@bp.route("/assiduites/nip/<nip>/count", defaults={"with_query": False})
|
||||
@api_web_bp.route("/assiduites/nip/<nip>/count", defaults={"with_query": False})
|
||||
@bp.route("/assiduites/nip/<nip>/count/query", defaults={"with_query": True})
|
||||
@api_web_bp.route("/assiduites/nip/<nip>/count/query", defaults={"with_query": True})
|
||||
# ine
|
||||
@bp.route("/assiduites/ine/<ine>/count", defaults={"with_query": False})
|
||||
@api_web_bp.route("/assiduites/ine/<ine>/count", defaults={"with_query": False})
|
||||
@bp.route("/assiduites/ine/<ine>/count/query", defaults={"with_query": True})
|
||||
@api_web_bp.route("/assiduites/ine/<ine>/count/query", defaults={"with_query": True})
|
||||
#
|
||||
@login_required
|
||||
@scodoc
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
def count_assiduites(etudid: int = None, with_query: bool = False):
|
||||
def count_assiduites(
|
||||
etudid: int = None, nip: str = None, ine: str = None, with_query: bool = False
|
||||
):
|
||||
"""
|
||||
Retourne le nombre d'assiduités d'un étudiant
|
||||
chemin : /assiduites/<int:etudid>/count
|
||||
|
@ -100,11 +120,20 @@ def count_assiduites(etudid: int = None, with_query: bool = False):
|
|||
|
||||
|
||||
"""
|
||||
query = Identite.query.filter_by(id=etudid)
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
# query = Identite.query.filter_by(id=etudid)
|
||||
# if g.scodoc_dept:
|
||||
# query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
|
||||
# etud: Identite = query.first_or_404(etudid)
|
||||
|
||||
etud: Identite = tools.get_etud(etudid, nip, ine)
|
||||
|
||||
if etud is None:
|
||||
return json_error(
|
||||
404,
|
||||
message="étudiant inconnu",
|
||||
)
|
||||
|
||||
etud: Identite = query.first_or_404(etudid)
|
||||
filtered: dict[str, object] = {}
|
||||
metric: str = "all"
|
||||
|
||||
|
@ -116,15 +145,31 @@ def count_assiduites(etudid: int = None, with_query: bool = False):
|
|||
)
|
||||
|
||||
|
||||
@bp.route("/assiduites/<int:etudid>", defaults={"with_query": False})
|
||||
@bp.route("/assiduites/<int:etudid>/query", defaults={"with_query": True})
|
||||
@api_web_bp.route("/assiduites/<int:etudid>", defaults={"with_query": False})
|
||||
@api_web_bp.route("/assiduites/<int:etudid>/query", defaults={"with_query": True})
|
||||
# etudid
|
||||
@bp.route("/assiduites/<etudid>", defaults={"with_query": False})
|
||||
@api_web_bp.route("/assiduites/<etudid>", defaults={"with_query": False})
|
||||
@bp.route("/assiduites/<etudid>/query", defaults={"with_query": True})
|
||||
@api_web_bp.route("/assiduites/<etudid>/query", defaults={"with_query": True})
|
||||
@bp.route("/assiduites/etudid/<etudid>", defaults={"with_query": False})
|
||||
@api_web_bp.route("/assiduites/etudid/<etudid>", defaults={"with_query": False})
|
||||
@bp.route("/assiduites/etudid/<etudid>/query", defaults={"with_query": True})
|
||||
@api_web_bp.route("/assiduites/etudid/<etudid>/query", defaults={"with_query": True})
|
||||
# nip
|
||||
@bp.route("/assiduites/nip/<nip>", defaults={"with_query": False})
|
||||
@api_web_bp.route("/assiduites/nip/<nip>", defaults={"with_query": False})
|
||||
@bp.route("/assiduites/nip/<nip>/query", defaults={"with_query": True})
|
||||
@api_web_bp.route("/assiduites/nip/<nip>/query", defaults={"with_query": True})
|
||||
# ine
|
||||
@bp.route("/assiduites/ine/<ine>", defaults={"with_query": False})
|
||||
@api_web_bp.route("/assiduites/ine/<ine>", defaults={"with_query": False})
|
||||
@bp.route("/assiduites/ine/<ine>/query", defaults={"with_query": True})
|
||||
@api_web_bp.route("/assiduites/ine/<ine>/query", defaults={"with_query": True})
|
||||
#
|
||||
@login_required
|
||||
@scodoc
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
def assiduites(etudid: int = None, with_query: bool = False):
|
||||
def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False):
|
||||
"""
|
||||
Retourne toutes les assiduités d'un étudiant
|
||||
chemin : /assiduites/<int:etudid>
|
||||
|
@ -164,11 +209,18 @@ def assiduites(etudid: int = None, with_query: bool = False):
|
|||
|
||||
"""
|
||||
|
||||
query = Identite.query.filter_by(id=etudid)
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
# query = Identite.query.filter_by(id=etudid)
|
||||
# if g.scodoc_dept:
|
||||
# query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
|
||||
etud: Identite = query.first_or_404(etudid)
|
||||
# etud: Identite = query.first_or_404(etudid)
|
||||
etud: Identite = tools.get_etud(etudid, nip, ine)
|
||||
|
||||
if etud is None:
|
||||
return json_error(
|
||||
404,
|
||||
message="étudiant inconnu",
|
||||
)
|
||||
assiduites_query = etud.assiduites
|
||||
|
||||
if with_query:
|
||||
|
@ -340,13 +392,23 @@ def count_assiduites_formsemestre(
|
|||
return scass.get_assiduites_stats(assiduites_query, metric, filtered)
|
||||
|
||||
|
||||
@bp.route("/assiduite/<int:etudid>/create", methods=["POST"])
|
||||
@api_web_bp.route("/assiduite/<int:etudid>/create", methods=["POST"])
|
||||
# etudid
|
||||
@bp.route("/assiduite/<etudid>/create", methods=["POST"])
|
||||
@api_web_bp.route("/assiduite/<etudid>/create", methods=["POST"])
|
||||
@bp.route("/assiduite/etudid/<etudid>/create", methods=["POST"])
|
||||
@api_web_bp.route("/assiduite/etudid/<etudid>/create", methods=["POST"])
|
||||
# nip
|
||||
@bp.route("/assiduite/nip/<nip>/create", methods=["POST"])
|
||||
@api_web_bp.route("/assiduite/nip/<nip>/create", methods=["POST"])
|
||||
# ine
|
||||
@bp.route("/assiduite/ine/<ine>/create", methods=["POST"])
|
||||
@api_web_bp.route("/assiduite/ine/<ine>/create", methods=["POST"])
|
||||
#
|
||||
@scodoc
|
||||
@as_json
|
||||
@login_required
|
||||
@permission_required(Permission.ScoAbsChange)
|
||||
def assiduite_create(etudid: int = None):
|
||||
def assiduite_create(etudid: int = None, nip=None, ine=None):
|
||||
"""
|
||||
Création d'une assiduité pour l'étudiant (etudid)
|
||||
La requête doit avoir un content type "application/json":
|
||||
|
@ -367,21 +429,27 @@ def assiduite_create(etudid: int = None):
|
|||
]
|
||||
|
||||
"""
|
||||
etud: Identite = Identite.query.filter_by(id=etudid).first_or_404()
|
||||
etud: Identite = tools.get_etud(etudid, nip, ine)
|
||||
|
||||
if etud is None:
|
||||
return json_error(
|
||||
404,
|
||||
message="étudiant inconnu",
|
||||
)
|
||||
|
||||
create_list: list[object] = request.get_json(force=True)
|
||||
|
||||
if not isinstance(create_list, list):
|
||||
return json_error(404, "Le contenu envoyé n'est pas une liste")
|
||||
|
||||
errors: dict[int, str] = {}
|
||||
success: dict[int, object] = {}
|
||||
errors: list = []
|
||||
success: list = []
|
||||
for i, data in enumerate(create_list):
|
||||
code, obj = _create_singular(data, etud)
|
||||
if code == 404:
|
||||
errors[i] = obj
|
||||
errors.append({"indice": i, "message": obj})
|
||||
else:
|
||||
success[i] = obj
|
||||
success.append({"indice": i, "message": obj})
|
||||
scass.simple_invalidate_cache(data, etud.id)
|
||||
|
||||
db.session.commit()
|
||||
|
@ -425,19 +493,19 @@ def assiduites_create():
|
|||
if not isinstance(create_list, list):
|
||||
return json_error(404, "Le contenu envoyé n'est pas une liste")
|
||||
|
||||
errors: dict[int, str] = {}
|
||||
success: dict[int, object] = {}
|
||||
errors: list = []
|
||||
success: list = []
|
||||
for i, data in enumerate(create_list):
|
||||
etud: Identite = Identite.query.filter_by(id=data["etudid"]).first()
|
||||
if etud is None:
|
||||
errors[i] = "Cet étudiant n'existe pas."
|
||||
errors.append({"indice": i, "message": "Cet étudiant n'existe pas."})
|
||||
continue
|
||||
|
||||
code, obj = _create_singular(data, etud)
|
||||
if code == 404:
|
||||
errors[i] = obj
|
||||
errors.append({"indice": i, "message": obj})
|
||||
else:
|
||||
success[i] = obj
|
||||
success.append({"indice": i, "message": obj})
|
||||
scass.simple_invalidate_cache(data)
|
||||
|
||||
return {"errors": errors, "success": success}
|
||||
|
@ -539,14 +607,14 @@ def assiduite_delete():
|
|||
if not isinstance(assiduites_list, list):
|
||||
return json_error(404, "Le contenu envoyé n'est pas une liste")
|
||||
|
||||
output = {"errors": {}, "success": {}}
|
||||
output = {"errors": [], "success": []}
|
||||
|
||||
for i, ass in enumerate(assiduites_list):
|
||||
code, msg = _delete_singular(ass, db)
|
||||
if code == 404:
|
||||
output["errors"][f"{i}"] = msg
|
||||
output["errors"].append({"indice": i, "message": msg})
|
||||
else:
|
||||
output["success"][f"{i}"] = {"OK": True}
|
||||
output["success"].append({"indice": i, "message": "OK"})
|
||||
|
||||
db.session.commit()
|
||||
return output
|
||||
|
@ -556,8 +624,15 @@ def _delete_singular(assiduite_id: int, database):
|
|||
assiduite_unique: Assiduite = Assiduite.query.filter_by(id=assiduite_id).first()
|
||||
if assiduite_unique is None:
|
||||
return (404, "Assiduite non existante")
|
||||
scass.simple_invalidate_cache(assiduite_unique.to_dict())
|
||||
ass_dict = assiduite_unique.to_dict()
|
||||
log(f"delete_assiduite: {assiduite_unique.etudiant.id} {assiduite_unique}")
|
||||
Scolog.logdb(
|
||||
method="delete_assiduite",
|
||||
etudid=assiduite_unique.etudiant.id,
|
||||
msg=f"assiduité: {assiduite_unique}",
|
||||
)
|
||||
database.session.delete(assiduite_unique)
|
||||
scass.simple_invalidate_cache(ass_dict)
|
||||
return (200, "OK")
|
||||
|
||||
|
||||
|
@ -630,6 +705,12 @@ def assiduite_edit(assiduite_id: int):
|
|||
err: str = ", ".join(errors)
|
||||
return json_error(404, err)
|
||||
|
||||
log(f"assiduite_edit: {assiduite_unique.etudiant.id} {assiduite_unique}")
|
||||
Scolog.logdb(
|
||||
"assiduite_edit",
|
||||
assiduite_unique.etudiant.id,
|
||||
msg=f"assiduite: modif {assiduite_unique}",
|
||||
)
|
||||
db.session.add(assiduite_unique)
|
||||
db.session.commit()
|
||||
scass.simple_invalidate_cache(assiduite_unique.to_dict())
|
||||
|
@ -645,33 +726,45 @@ def assiduite_edit(assiduite_id: int):
|
|||
@permission_required(Permission.ScoAbsChange)
|
||||
def assiduites_edit():
|
||||
"""
|
||||
Edition d'une assiduité à partir de son id
|
||||
Edition de plusieurs assiduités
|
||||
La requête doit avoir un content type "application/json":
|
||||
{
|
||||
"etat"?: str,
|
||||
"moduleimpl_id"?: int
|
||||
"desc"?: str
|
||||
"est_just"?: bool
|
||||
}
|
||||
[
|
||||
{
|
||||
"assiduite_id" : int,
|
||||
"etat"?: str,
|
||||
"moduleimpl_id"?: int
|
||||
"desc"?: str
|
||||
"est_just"?: bool
|
||||
}
|
||||
]
|
||||
"""
|
||||
edit_list: list[object] = request.get_json(force=True)
|
||||
|
||||
if not isinstance(edit_list, list):
|
||||
return json_error(404, "Le contenu envoyé n'est pas une liste")
|
||||
|
||||
errors: dict[int, str] = {}
|
||||
success: dict[int, object] = {}
|
||||
errors: list[dict] = []
|
||||
success: list[dict] = []
|
||||
for i, data in enumerate(edit_list):
|
||||
assi: Identite = Assiduite.query.filter_by(id=data["assiduite_id"]).first()
|
||||
if assi is None:
|
||||
errors[i] = "Cet assiduité n'existe pas."
|
||||
errors.append(
|
||||
{
|
||||
"indice": i,
|
||||
"message": f"assiduité {data['assiduite_id']} n'existe pas.",
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
code, obj = _edit_singular(assi, data)
|
||||
obj_retour = {
|
||||
"indice": i,
|
||||
"message": obj,
|
||||
}
|
||||
if code == 404:
|
||||
errors[i] = obj
|
||||
errors.append(obj_retour)
|
||||
else:
|
||||
success[i] = obj
|
||||
success.append(obj_retour)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
|
@ -727,6 +820,12 @@ def _edit_singular(assiduite_unique, data):
|
|||
err: str = ", ".join(errors)
|
||||
return (404, err)
|
||||
|
||||
log(f"_edit_singular: {assiduite_unique.etudiant.id} {assiduite_unique}")
|
||||
Scolog.logdb(
|
||||
"assiduite_edit",
|
||||
assiduite_unique.etudiant.id,
|
||||
msg=f"assiduite: modif {assiduite_unique}",
|
||||
)
|
||||
db.session.add(assiduite_unique)
|
||||
scass.simple_invalidate_cache(assiduite_unique.to_dict())
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ import app.scodoc.sco_utils as scu
|
|||
from app import db
|
||||
from app.api import api_bp as bp
|
||||
from app.api import api_web_bp
|
||||
from app.api import get_model_api_object
|
||||
from app.api import get_model_api_object, tools
|
||||
from app.decorators import permission_required, scodoc
|
||||
from app.models import Identite, Justificatif, Departement
|
||||
from app.models.assiduites import compute_assiduites_justified
|
||||
|
@ -52,15 +52,31 @@ def justificatif(justif_id: int = None):
|
|||
return get_model_api_object(Justificatif, justif_id, Identite)
|
||||
|
||||
|
||||
@bp.route("/justificatifs/<int:etudid>", defaults={"with_query": False})
|
||||
@bp.route("/justificatifs/<int:etudid>/query", defaults={"with_query": True})
|
||||
@api_web_bp.route("/justificatifs/<int:etudid>", defaults={"with_query": False})
|
||||
@api_web_bp.route("/justificatifs/<int:etudid>/query", defaults={"with_query": True})
|
||||
# etudid
|
||||
@bp.route("/justificatifs/<etudid>", defaults={"with_query": False})
|
||||
@api_web_bp.route("/justificatifs/<etudid>", defaults={"with_query": False})
|
||||
@bp.route("/justificatifs/<etudid>/query", defaults={"with_query": True})
|
||||
@api_web_bp.route("/justificatifs/<etudid>/query", defaults={"with_query": True})
|
||||
@bp.route("/justificatifs/etudid/<etudid>", defaults={"with_query": False})
|
||||
@api_web_bp.route("/justificatifs/etudid/<etudid>", defaults={"with_query": False})
|
||||
@bp.route("/justificatifs/etudid/<etudid>/query", defaults={"with_query": True})
|
||||
@api_web_bp.route("/justificatifs/etudid/<etudid>/query", defaults={"with_query": True})
|
||||
# nip
|
||||
@bp.route("/justificatifs/nip/<nip>", defaults={"with_query": False})
|
||||
@api_web_bp.route("/justificatifs/nip/<nip>", defaults={"with_query": False})
|
||||
@bp.route("/justificatifs/nip/<nip>/query", defaults={"with_query": True})
|
||||
@api_web_bp.route("/justificatifs/nip/<nip>/query", defaults={"with_query": True})
|
||||
# ine
|
||||
@bp.route("/justificatifs/ine/<ine>", defaults={"with_query": False})
|
||||
@api_web_bp.route("/justificatifs/ine/<ine>", defaults={"with_query": False})
|
||||
@bp.route("/justificatifs/ine/<ine>/query", defaults={"with_query": True})
|
||||
@api_web_bp.route("/justificatifs/ine/<ine>/query", defaults={"with_query": True})
|
||||
#
|
||||
@login_required
|
||||
@scodoc
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
def justificatifs(etudid: int = None, with_query: bool = False):
|
||||
def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = False):
|
||||
"""
|
||||
Retourne toutes les assiduités d'un étudiant
|
||||
chemin : /justificatifs/<int:etudid>
|
||||
|
@ -87,11 +103,13 @@ def justificatifs(etudid: int = None, with_query: bool = False):
|
|||
ex query?user_id=3
|
||||
"""
|
||||
|
||||
query = Identite.query.filter_by(id=etudid)
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
etud: Identite = tools.get_etud(etudid, nip, ine)
|
||||
|
||||
etud: Identite = query.first_or_404(etudid)
|
||||
if etud is None:
|
||||
return json_error(
|
||||
404,
|
||||
message="étudiant inconnu",
|
||||
)
|
||||
justificatifs_query = etud.justificatifs
|
||||
|
||||
if with_query:
|
||||
|
@ -136,7 +154,7 @@ def justificatifs_dept(dept_id: int = None, with_query: bool = False):
|
|||
@login_required
|
||||
@as_json
|
||||
@permission_required(Permission.ScoAbsChange)
|
||||
def justif_create(etudid: int = None):
|
||||
def justif_create(etudid: int = None, nip=None, ine=None):
|
||||
"""
|
||||
Création d'un justificatif pour l'étudiant (etudid)
|
||||
La requête doit avoir un content type "application/json":
|
||||
|
@ -156,21 +174,27 @@ def justif_create(etudid: int = None):
|
|||
]
|
||||
|
||||
"""
|
||||
etud: Identite = Identite.query.filter_by(id=etudid).first_or_404()
|
||||
etud: Identite = tools.get_etud(etudid, nip, ine)
|
||||
|
||||
if etud is None:
|
||||
return json_error(
|
||||
404,
|
||||
message="étudiant inconnu",
|
||||
)
|
||||
|
||||
create_list: list[object] = request.get_json(force=True)
|
||||
|
||||
if not isinstance(create_list, list):
|
||||
return json_error(404, "Le contenu envoyé n'est pas une liste")
|
||||
|
||||
errors: dict[int, str] = {}
|
||||
success: dict[int, object] = {}
|
||||
errors: list = []
|
||||
success: list = []
|
||||
for i, data in enumerate(create_list):
|
||||
code, obj = _create_singular(data, etud)
|
||||
if code == 404:
|
||||
errors[i] = obj
|
||||
errors.append({"indice": i, "message": obj})
|
||||
else:
|
||||
success[i] = obj
|
||||
success.append({"indice": i, "message": obj})
|
||||
scass.simple_invalidate_cache(data, etud.id)
|
||||
compute_assiduites_justified(Justificatif.query.filter_by(etudid=etudid), True)
|
||||
return {"errors": errors, "success": success}
|
||||
|
@ -359,14 +383,14 @@ def justif_delete():
|
|||
if not isinstance(justificatifs_list, list):
|
||||
return json_error(404, "Le contenu envoyé n'est pas une liste")
|
||||
|
||||
output = {"errors": {}, "success": {}}
|
||||
output = {"errors": [], "success": []}
|
||||
|
||||
for i, ass in enumerate(justificatifs_list):
|
||||
code, msg = _delete_singular(ass, db)
|
||||
if code == 404:
|
||||
output["errors"][f"{i}"] = msg
|
||||
output["errors"].append({"indice": i, "message": msg})
|
||||
else:
|
||||
output["success"][f"{i}"] = {"OK": True}
|
||||
output["success"].append({"indice": i, "message": "OK"})
|
||||
|
||||
db.session.commit()
|
||||
|
||||
|
|
|
@ -110,7 +110,7 @@ def formsemestre_partitions(formsemestre_id: int):
|
|||
def etud_in_group(group_id: int):
|
||||
"""
|
||||
Retourne la liste des étudiants dans un groupe
|
||||
|
||||
(inscrits au groupe et inscrits au semestre).
|
||||
group_id : l'id d'un groupe
|
||||
|
||||
Exemple de résultat :
|
||||
|
@ -133,7 +133,15 @@ def etud_in_group(group_id: int):
|
|||
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
group = query.first_or_404()
|
||||
return [etud.to_dict_short() for etud in group.etuds]
|
||||
|
||||
query = (
|
||||
Identite.query.join(group_membership)
|
||||
.filter_by(group_id=group_id)
|
||||
.join(FormSemestreInscription)
|
||||
.filter_by(formsemestre_id=group.partition.formsemestre_id)
|
||||
)
|
||||
|
||||
return [etud.to_dict_short() for etud in query]
|
||||
|
||||
|
||||
@bp.route("/group/<int:group_id>/etudiants/query")
|
||||
|
@ -161,7 +169,6 @@ def etud_in_group_query(group_id: int):
|
|||
query = query.filter_by(etat=etat)
|
||||
|
||||
query = query.join(group_membership).filter_by(group_id=group_id)
|
||||
|
||||
return [etud.to_dict_short() for etud in query]
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,445 @@
|
|||
# -*- 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
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Feuille excel pour préparation des jurys classiques (non BUT)
|
||||
"""
|
||||
import time
|
||||
|
||||
from flask import abort
|
||||
|
||||
from app.but import jury_but
|
||||
from app.but.cursus_but import EtudCursusBUT
|
||||
from app.but.prepajury_desc import ParcoursDesc, FormsemestreDesc
|
||||
from app.models import (
|
||||
FormSemestre,
|
||||
ApcParcours,
|
||||
Formation,
|
||||
)
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.but.prepajury_xl import ScoExcelBook
|
||||
|
||||
|
||||
class Element:
|
||||
def __init__(self, etudiant):
|
||||
self.etudiant = etudiant
|
||||
self.note = None
|
||||
self.resultat = None
|
||||
self.format = 0
|
||||
|
||||
def set_note(self, note):
|
||||
self.note = note
|
||||
|
||||
def set_res(self, res):
|
||||
self.resultat = res
|
||||
|
||||
def get_res(self):
|
||||
return self.resultat
|
||||
|
||||
def get_note(self):
|
||||
return self.note
|
||||
|
||||
|
||||
class ElementUE(Element):
|
||||
def __init__(self, etudiant):
|
||||
super().__init__(etudiant)
|
||||
self.status = None
|
||||
|
||||
def set_status(self, status):
|
||||
self.status = status
|
||||
|
||||
|
||||
class ElementNiveau(Element):
|
||||
def __init__(self, etudiant, competence_id):
|
||||
super().__init__(etudiant)
|
||||
self.competence_id = competence_id
|
||||
self.validation = None
|
||||
self.ues = {}
|
||||
|
||||
def get_elem(self, periode=None):
|
||||
if periode is None:
|
||||
return self
|
||||
return self.ues.get(periode, None)
|
||||
|
||||
def compute(self, rcue):
|
||||
self.set_note(rcue.moy_rcue)
|
||||
|
||||
|
||||
class ElementFormsemestre(Element):
|
||||
def __init__(self, etudiant, formsemestre_desc: FormsemestreDesc = None):
|
||||
super().__init__(etudiant)
|
||||
self.formsemestre_desc = formsemestre_desc
|
||||
self.formsemestre_id = formsemestre_desc.formsemestre_id
|
||||
self.deca = None
|
||||
self.ues = {}
|
||||
|
||||
def get_elem(self, competence_id=None):
|
||||
if competence_id is None:
|
||||
return self
|
||||
return self.ues.get(competence_id, None)
|
||||
|
||||
|
||||
class ElementAnnee(Element):
|
||||
def __init__(self, etudiant):
|
||||
super().__init__(etudiant)
|
||||
self.formsemestres = {}
|
||||
self.niveaux = {}
|
||||
self.last = None
|
||||
self.deca = None
|
||||
|
||||
def get_elem(self, competence_id=None, periode=None):
|
||||
if competence_id is None and periode is None:
|
||||
return self
|
||||
elif competence_id is None:
|
||||
return self.formsemestres.get(periode, None)
|
||||
elif competence_id in self.niveaux:
|
||||
return self.niveaux[competence_id].get_elem(periode)
|
||||
else:
|
||||
return None
|
||||
|
||||
def set_periode(self, periode, formsemestre_desc):
|
||||
self.formsemestres[periode] = formsemestre_desc
|
||||
|
||||
def set_niveau(self, competence_id, elem_niveau):
|
||||
self.niveaux[competence_id] = elem_niveau
|
||||
|
||||
def add_validation(self, validation):
|
||||
competence_id = validation.ue1.niveau_competence.competence_id
|
||||
self.niveaux[competence_id].set_res(validation.code)
|
||||
|
||||
def create_structure(self):
|
||||
self.last = self.formsemestres.get(2, self.formsemestres.get(1, None))
|
||||
if self.last is not None:
|
||||
self.deca = jury_but.DecisionsProposeesAnnee(
|
||||
self.etudiant.ident, self.last.formsemestre_desc.formsemestre
|
||||
)
|
||||
for niveau in self.deca.niveaux_competences:
|
||||
competence_id = niveau.competence_id
|
||||
elem_niveau = ElementNiveau(self.etudiant, competence_id)
|
||||
self.niveaux[competence_id] = elem_niveau
|
||||
for ue in self.deca.ues_impair:
|
||||
competence_id = ue.niveau_competence.competence_id
|
||||
periode = 1
|
||||
elem_ue = ElementUE(self.etudiant)
|
||||
self.niveaux[competence_id].ues[periode] = elem_ue
|
||||
self.formsemestres[periode].ues[competence_id] = elem_ue
|
||||
for ue in self.deca.ues_pair:
|
||||
competence_id = ue.niveau_competence.competence_id
|
||||
periode = 2
|
||||
elem_ue = ElementUE(self.etudiant)
|
||||
self.niveaux[competence_id].ues[periode] = elem_ue
|
||||
self.formsemestres[periode].ues[competence_id] = elem_ue
|
||||
|
||||
def compute(self):
|
||||
if self.last is not None:
|
||||
self.set_res(self.deca.code_valide)
|
||||
self.set_note(
|
||||
self.last.formsemestre_desc.get_resultats().get_etud_moy_gen(
|
||||
self.etudiant.ident.id
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class EtudiantJury:
|
||||
"""
|
||||
Structure:
|
||||
ident
|
||||
formation
|
||||
parcour
|
||||
cursus
|
||||
inscriptions*
|
||||
current_formsemestre
|
||||
absences_tot
|
||||
absences_just
|
||||
Annees*
|
||||
nb_rcues
|
||||
note
|
||||
resultat
|
||||
niveaux*
|
||||
note
|
||||
resultat
|
||||
ues*
|
||||
note
|
||||
resultat
|
||||
DUT
|
||||
resultat
|
||||
BUT
|
||||
resultat
|
||||
"""
|
||||
|
||||
annee_periode = {
|
||||
1: ("BUT1", 1),
|
||||
2: ("BUT1", 2),
|
||||
3: ("BUT2", 1),
|
||||
4: ("BUT2", 2),
|
||||
5: ("BUT3", 1),
|
||||
6: ("BUT3", 2),
|
||||
}
|
||||
|
||||
def __init__(self, ident, contexte: "_Compilation"):
|
||||
self.ident = ident
|
||||
self.contexte = contexte
|
||||
self.formation = contexte.formation
|
||||
self.current_formsemestre = contexte.formsemestre
|
||||
self.current_formsemestre_id = contexte.formsemestre.formsemestre_id
|
||||
self.current_formsemestre_desc = contexte.get_semestredesc(
|
||||
self.current_formsemestre_id
|
||||
)
|
||||
self.nbabs, self.nbabsjust = self.current_formsemestre.get_abs_count(ident.id)
|
||||
self.cursus: EtudCursusBUT = EtudCursusBUT(ident, self.formation)
|
||||
self.parcour = self.cursus.inscriptions[-1].parcour
|
||||
# donnes propres à l étudiant (à remplir par fill_in)
|
||||
self.history = [] # description historique de l etudiant
|
||||
self.formsemestres = [] # liste historique des formsemestres
|
||||
self.formsemestre_by_semestre = (
|
||||
{}
|
||||
) # semetre_id -> dernier FormSemestreDesc pour chaque semestre
|
||||
self.formsemestre_by_annee = (
|
||||
{}
|
||||
) # annee -> dernier FormSemestreDesc pour chaque semestre
|
||||
self.annees = {}
|
||||
self.DUT = None # Résultat au DUT
|
||||
self.BUT = None # Résultat au BUT
|
||||
|
||||
def get_elem(self, annee=None, competence_id=None, periode=None):
|
||||
if annee is None:
|
||||
return self
|
||||
if annee in self.annees:
|
||||
return self.annees[annee].get_elem(competence_id, periode)
|
||||
return None
|
||||
|
||||
def get_desc(self, annee=None, competence_id=None, periode=None):
|
||||
return self.parcour.get_desc(annee, competence_id, periode)
|
||||
|
||||
def compute_history(self):
|
||||
# calcul historique
|
||||
for inscription in sorted(
|
||||
self.cursus.inscriptions, key=lambda x: x.formsemestre.date_debut
|
||||
):
|
||||
formsemestre = inscription.formsemestre
|
||||
semestre_id = None
|
||||
# if formsemestre.formation == self.formation:
|
||||
if formsemestre.formation.is_apc():
|
||||
formsemestre_id = formsemestre.formsemestre_id
|
||||
formsemestre_desc = self.contexte.get_semestredesc(formsemestre_id)
|
||||
semestre_id = formsemestre.semestre_id
|
||||
self.formsemestres.append(formsemestre)
|
||||
annee, periode = EtudiantJury.annee_periode[semestre_id]
|
||||
self.formsemestre_by_semestre[semestre_id] = formsemestre_desc
|
||||
self.formsemestre_by_annee[annee] = formsemestre_desc
|
||||
etat = inscription.etat
|
||||
Sx = f"S{semestre_id}"
|
||||
if etat != "I":
|
||||
Sx += " (Dem)" if etat == "D" else f"({etat})"
|
||||
self.history.append(Sx)
|
||||
|
||||
def create_structure(self):
|
||||
for annee, formsemestre_desc in self.formsemestre_by_annee.items():
|
||||
elem_annee = ElementAnnee(self)
|
||||
self.annees[annee] = elem_annee
|
||||
|
||||
for semestre_id, formsemestre_desc in self.formsemestre_by_semestre.items():
|
||||
annee, periode = EtudiantJury.annee_periode[semestre_id]
|
||||
elem_formsemestre = ElementFormsemestre(self.ident, formsemestre_desc)
|
||||
self.annees[annee].set_periode(periode, elem_formsemestre)
|
||||
|
||||
for annee in self.annees:
|
||||
self.annees[annee].create_structure()
|
||||
|
||||
def compute(self):
|
||||
for annee in self.annees:
|
||||
self.annees[annee].compute()
|
||||
for (
|
||||
competence_id,
|
||||
validations,
|
||||
) in self.cursus.validation_par_competence_et_annee.items():
|
||||
for annee, validation in validations.items():
|
||||
self.annees[annee].add_validation(validation)
|
||||
|
||||
def fill_in(self):
|
||||
"""
|
||||
Creation des donnees propres à l'étudiant
|
||||
"""
|
||||
self.compute_history()
|
||||
# self.create_structure()
|
||||
# self.compute()
|
||||
# for (
|
||||
# competence_id,
|
||||
# validations_par_competence,
|
||||
# ) in self.cursus.validation_par_competence_et_annee.items():
|
||||
# for annee, validation in validations_par_competence.items():
|
||||
# elem_annee = self.annees[annee]
|
||||
# elem_formsemestre = elem_annee.formsemestres.get(
|
||||
# periode, ElementFormsemestre(self, formsemestre_desc)
|
||||
# )
|
||||
# resultats = formsemestre_desc.get_resultats()
|
||||
# ues = resultats.etud_ues(self.ident.etudid)
|
||||
# for ue in ues:
|
||||
# niveau_id = ue.niveau_competence_id
|
||||
# competence_id = ue.niveau_competence.competence_id
|
||||
# status = resultats.get_etud_ue_status(self.ident.etudid, ue.id)
|
||||
# if competence_id not in self.annees[annee].niveaux:
|
||||
# elem_niveau = ElementNiveau(self, validation)
|
||||
# self.annees[annee].niveaux[competence_id] = elem_niveau
|
||||
# else:
|
||||
# elem_niveau = self.annees[annee].niveaux[competence_id]
|
||||
# elem_ue = ElementUE(self, status)
|
||||
# elem_niveau.ues[periode] = elem_ue
|
||||
#
|
||||
#
|
||||
#
|
||||
# for (
|
||||
# competence_id,
|
||||
# validation,
|
||||
# ) in self.cursus.validation_par_competence_et_annee.items():
|
||||
# self.elements_par_competence_annee_et_periode[competence_id] = {}
|
||||
# for annee in validation:
|
||||
# self.elements_par_competence_annee_et_periode[competence_id][
|
||||
# annee
|
||||
# ] = []
|
||||
# self.deca_by_semestre[semestre_idx] = jury_but.DecisionsProposeesAnnee(
|
||||
# self.ident, self.formsemestre_by_semestre[semestre_idx]
|
||||
# )
|
||||
# # niveau.add_ue_status(semestre_idx, status)
|
||||
# if annee in self.deca:
|
||||
# for rcue in self.deca[annee].rcues_annee:
|
||||
# ue_id1 = rcue.ue_1.id
|
||||
# self.notes_ues[ue_id1] = rcue.moy_ue_1
|
||||
# ue_id2 = rcue.ue_2.id
|
||||
# self.notes_ues[ue_id2] = rcue.moy_ue_2
|
||||
# rcue_id = rcue.ue_1.niveau_competence_id
|
||||
# self.notes_rcues[rcue_id] = rcue.moy_rcue
|
||||
|
||||
def get_note(self, annee, competence_id=None, periode=None):
|
||||
elem: Element = self.get_elem(annee, competence_id, periode)
|
||||
if elem:
|
||||
return elem.get_note()
|
||||
return "-"
|
||||
|
||||
def get_res(self, annee, competence_id=None, periode=None):
|
||||
elem: Element = self.get_elem(annee, competence_id, periode)
|
||||
if elem:
|
||||
return elem.get_res()
|
||||
return "-"
|
||||
|
||||
def get_data(self):
|
||||
result = [
|
||||
self.ident.id,
|
||||
self.ident.code_nip,
|
||||
self.ident.civilite,
|
||||
self.ident.nom,
|
||||
self.ident.prenom,
|
||||
self.ident.etat_civil_pv(with_paragraph=False),
|
||||
self.parcour.code,
|
||||
", ".join(self.history),
|
||||
]
|
||||
return result
|
||||
|
||||
|
||||
class _Compilation:
|
||||
"""
|
||||
structure:
|
||||
semestres: formsemestre_id -> FormSemestreDesc
|
||||
parcours: parcour_code -> ParcoursDesc
|
||||
formation
|
||||
"""
|
||||
|
||||
def __init__(self, formsemestre: FormSemestre):
|
||||
self.semestres = {}
|
||||
self.parcours = {}
|
||||
formsemestre_id = formsemestre.formsemestre_id
|
||||
self.formation: Formation = formsemestre.formation
|
||||
self.formsemestre = formsemestre
|
||||
self.add_semestre(formsemestre_id, formsemestre)
|
||||
self.current_semestre = self.semestres[formsemestre_id]
|
||||
# inventaire des semestres et parcours
|
||||
for ident in (
|
||||
self.semestres[formsemestre_id].get_resultats().get_inscrits(order_by="moy")
|
||||
):
|
||||
etudiant: EtudiantJury = EtudiantJury(
|
||||
ident, self
|
||||
) # initialise etudiant.cursus et etudiant.parcour
|
||||
for inscription in etudiant.cursus.inscriptions:
|
||||
formsemestre = inscription.formsemestre
|
||||
if (
|
||||
formsemestre.formation.referentiel_competence
|
||||
== self.formation.referentiel_competence
|
||||
):
|
||||
self.add_semestre(formsemestre.formsemestre_id, formsemestre)
|
||||
scodocParcour = etudiant.parcour
|
||||
if scodocParcour is None:
|
||||
parcourCode = "TC"
|
||||
else:
|
||||
parcourCode = scodocParcour.code
|
||||
if parcourCode in self.parcours:
|
||||
parcoursDesc = self.parcours[parcourCode]
|
||||
else:
|
||||
parcoursDesc = ParcoursDesc(self.formation, scodocParcour)
|
||||
self.parcours[parcourCode] = parcoursDesc
|
||||
parcoursDesc.add_etudiant(etudiant)
|
||||
etudiant.fill_in()
|
||||
|
||||
def get_semestredesc(self, formsemestre_id):
|
||||
return self.semestres.get(formsemestre_id, None)
|
||||
|
||||
def add_semestre(self, formsemestre_id, formsemestre=None):
|
||||
if formsemestre_id not in self.semestres:
|
||||
self.semestres[formsemestre_id] = FormsemestreDesc(
|
||||
formsemestre_id, formsemestre
|
||||
)
|
||||
|
||||
def add_parcours(self, scodoc_parcour: ApcParcours, etudiant: EtudiantJury):
|
||||
parcour_code = scodoc_parcour.get("code", "TC")
|
||||
if parcour_code not in self.parcours:
|
||||
self.parcours[parcour_code] = ParcoursDesc(self.formation, scodoc_parcour)
|
||||
self.parcours[parcour_code].add(etudiant)
|
||||
|
||||
def computes_decision(self):
|
||||
pass
|
||||
|
||||
def make_excel(self, filename: str):
|
||||
workbook = ScoExcelBook()
|
||||
for parcoursCode, parcours in self.parcours.items():
|
||||
parcours.generate(workbook)
|
||||
|
||||
mime, suffix = scu.get_mime_suffix("xlsx")
|
||||
xls = workbook.generate()
|
||||
return scu.send_file(xls, filename=filename, mime=mime, suffix=suffix)
|
||||
|
||||
|
||||
def feuille_preparation_jury_but(formsemestre_id):
|
||||
if not isinstance(formsemestre_id, int):
|
||||
abort(404)
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
# res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
"""Feuille excel pour préparation des jurys adaptée pour le BUT."""
|
||||
# breakpoint()
|
||||
compilation = _Compilation(formsemestre)
|
||||
# compilation.computes_decision()
|
||||
filename = scu.sanitize_filename(
|
||||
f"""{'jury'}-{formsemestre.titre_num()}-{time.strftime("%Y-%m-%d")}"""
|
||||
)
|
||||
return compilation.make_excel(filename)
|
|
@ -0,0 +1,522 @@
|
|||
import openpyxl
|
||||
from openpyxl.worksheet.worksheet import Worksheet
|
||||
|
||||
from app.but.prepajury_xl_format import (
|
||||
SCO_COLORS,
|
||||
FMT,
|
||||
SCO_FONTSIZE,
|
||||
SCO_VALIGN,
|
||||
SCO_HALIGN,
|
||||
SCO_BORDERTHICKNESS,
|
||||
Sco_Style,
|
||||
HAIR_BLACK,
|
||||
SCO_NUMBER_FORMAT,
|
||||
)
|
||||
from app.comp import res_sem
|
||||
from app.models import ApcCompetence, ApcParcours, FormSemestre
|
||||
from app.but.prepajury_xl import (
|
||||
ScoExcelBook,
|
||||
ScoExcelSheet,
|
||||
base_signature,
|
||||
Frame_Engine,
|
||||
Merge_Engine,
|
||||
)
|
||||
|
||||
|
||||
UNUSED = "XXX"
|
||||
liste_annees = ["BUT1", "BUT2", "BUT3"]
|
||||
header_colors = {
|
||||
"BUT1": {
|
||||
"BUT": SCO_COLORS.BUT1,
|
||||
"RCUE": SCO_COLORS.RCUE1,
|
||||
"UE": SCO_COLORS.UE1,
|
||||
},
|
||||
"BUT2": {
|
||||
"BUT": SCO_COLORS.BUT2,
|
||||
"RCUE": SCO_COLORS.RCUE2,
|
||||
"UE": SCO_COLORS.UE2,
|
||||
},
|
||||
"BUT3": {
|
||||
"BUT": SCO_COLORS.BUT3,
|
||||
"RCUE": SCO_COLORS.RCUE3,
|
||||
"UE": SCO_COLORS.UE3,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def periode(semestre_idx):
|
||||
return 1 + (semestre_idx + 1) % 2
|
||||
|
||||
|
||||
class UeDesc:
|
||||
def __init__(self, scodocUe, competence_id, periode):
|
||||
self.fromScodoc = scodocUe
|
||||
self.competence_id = competence_id
|
||||
self.periode = periode
|
||||
|
||||
|
||||
class NiveauDesc:
|
||||
def __init__(self, scodocNiveau):
|
||||
self.fromScodoc = scodocNiveau
|
||||
self.ues = {1: None, 2: None}
|
||||
for scodocUe in scodocNiveau.ues:
|
||||
ue_desc = UeDesc(
|
||||
scodocUe, scodocNiveau.competence_id, periode(scodocUe.semestre_idx)
|
||||
)
|
||||
self.ues[periode(scodocUe.semestre_idx)] = scodocUe
|
||||
if not scodocUe.is_external:
|
||||
self.ues[periode(scodocUe.semestre_idx)] = scodocUe
|
||||
|
||||
def get_desc(self, periode=None):
|
||||
if periode is None:
|
||||
return self
|
||||
return self.ues.get(periode, None)
|
||||
|
||||
def generate_data(
|
||||
self, ws: ScoExcelSheet, row: int, etudiant: "EtudiantJury", column: int
|
||||
) -> int:
|
||||
for periode in [1, 2]:
|
||||
ue = self.ues[periode]
|
||||
if ue is None:
|
||||
ws.set_cell(row, column, UNUSED)
|
||||
ws.set_cell(row, column + 1, UNUSED)
|
||||
else:
|
||||
ws.set_cell(
|
||||
row,
|
||||
column,
|
||||
etudiant.get_note(
|
||||
self.fromScodoc.annee, self.fromScodoc.competence_id, periode
|
||||
),
|
||||
base_signature,
|
||||
)
|
||||
ws.set_cell(
|
||||
row,
|
||||
column + 1,
|
||||
etudiant.get_res(
|
||||
self.fromScodoc.annee, self.fromScodoc.competence_id, periode
|
||||
),
|
||||
)
|
||||
column += 2
|
||||
ws.set_cell(
|
||||
row,
|
||||
column,
|
||||
etudiant.get_note(self.fromScodoc.annee, self.fromScodoc.competence_id),
|
||||
base_signature,
|
||||
)
|
||||
ws.set_cell(
|
||||
row,
|
||||
column + 1,
|
||||
etudiant.get_res(self.fromScodoc.annee, self.fromScodoc.competence_id),
|
||||
)
|
||||
return column + 2
|
||||
|
||||
@staticmethod
|
||||
def generate_blank_data(ws: ScoExcelSheet, row: int, column: int) -> int:
|
||||
ws.set_cell(row, column + 0, "UN1")
|
||||
ws.set_cell(row, column + 1, "UR1")
|
||||
ws.set_cell(row, column + 2, "UN2")
|
||||
ws.set_cell(row, column + 3, "UR2")
|
||||
ws.set_cell(row, column + 4, "R1")
|
||||
ws.set_cell(row, column + 5, "R2")
|
||||
column += 6
|
||||
return column
|
||||
|
||||
@staticmethod
|
||||
def generate_blank_header(ws: ScoExcelSheet, column: int, annee: str):
|
||||
rcue_signature = FMT.FILL_BGCOLOR.write(
|
||||
value=header_colors[annee]["RCUE"].value,
|
||||
signature=base_signature,
|
||||
)
|
||||
ue_signature = FMT.FILL_BGCOLOR.write(
|
||||
value=header_colors[annee]["UE"].value,
|
||||
signature=base_signature,
|
||||
)
|
||||
merge = ws.get_merge_engine(start_row=2, start_column=column)
|
||||
frame = ws.get_frame_engine(
|
||||
start_row=2, start_column=column, thickness=SCO_BORDERTHICKNESS.BORDER_THIN
|
||||
)
|
||||
ws.set_cell(2, column, UNUSED, from_signature=rcue_signature)
|
||||
for ue in ["UE1", "UE2"]:
|
||||
frame_ue = ws.get_frame_engine(
|
||||
start_row=3,
|
||||
start_column=column,
|
||||
thickness=SCO_BORDERTHICKNESS.BORDER_THIN,
|
||||
color=SCO_COLORS.GREY,
|
||||
)
|
||||
merge_ue = ws.get_merge_engine(start_row=3, start_column=column)
|
||||
ws.set_cell(3, column, UNUSED, ue_signature)
|
||||
ws.set_cell(4, column, "Note", ue_signature)
|
||||
ws.set_column_dimension_hidden(ScoExcelSheet.i2col(column - 1), True)
|
||||
column += 1
|
||||
ws.set_cell(4, column, "Rés.", ue_signature)
|
||||
ws.set_column_dimension_hidden(ScoExcelSheet.i2col(column - 1), True)
|
||||
column += 1
|
||||
merge_ue.close(end_row=3, end_column=column - 1)
|
||||
frame_ue.close(end_row=4, end_column=column - 1)
|
||||
merge_rcue = ws.get_merge_engine(start_row=3, start_column=column)
|
||||
ws.set_cell(3, column, UNUSED, rcue_signature)
|
||||
ws.set_cell(4, column, UNUSED, rcue_signature)
|
||||
ws.set_column_dimension_hidden(ScoExcelSheet.i2col(column - 1), True)
|
||||
column += 1
|
||||
ws.set_cell(4, column, UNUSED, rcue_signature)
|
||||
ws.set_column_dimension_hidden(ScoExcelSheet.i2col(column - 1), True)
|
||||
column += 1
|
||||
merge_rcue.close(end_row=3, end_column=column - 1)
|
||||
frame.close(end_row=4, end_column=column - 1)
|
||||
merge.close(end_row=2, end_column=column - 1)
|
||||
return column
|
||||
|
||||
def generate_header(self, ws: ScoExcelSheet, column: int):
|
||||
rcue_signature = FMT.FILL_BGCOLOR.write(
|
||||
value=header_colors[self.fromScodoc.annee]["RCUE"].value,
|
||||
signature=base_signature,
|
||||
)
|
||||
ue_signature = FMT.FILL_BGCOLOR.write(
|
||||
value=header_colors[self.fromScodoc.annee]["UE"].value,
|
||||
signature=base_signature,
|
||||
)
|
||||
merge = ws.get_merge_engine(start_row=2, start_column=column)
|
||||
frame = ws.get_frame_engine(
|
||||
start_row=2, start_column=column, thickness=SCO_BORDERTHICKNESS.BORDER_THIN
|
||||
)
|
||||
ws.set_cell(
|
||||
2, column, self.fromScodoc.competence.titre, from_signature=rcue_signature
|
||||
)
|
||||
for periode in [1, 2]:
|
||||
ue = self.ues[periode]
|
||||
frame_ue = ws.get_frame_engine(
|
||||
start_row=3,
|
||||
start_column=column,
|
||||
thickness=SCO_BORDERTHICKNESS.BORDER_THIN,
|
||||
color=SCO_COLORS.GREY,
|
||||
)
|
||||
merge_ue = ws.get_merge_engine(start_row=3, start_column=column)
|
||||
if ue is None:
|
||||
ws.set_cell(3, column, "XXX", ue_signature)
|
||||
else:
|
||||
ws.set_cell(3, column, ue.acronyme, ue_signature)
|
||||
ws.set_cell(4, column, "Note", ue_signature)
|
||||
column += 1
|
||||
ws.set_cell(4, column, "Rés.", ue_signature)
|
||||
column += 1
|
||||
merge_ue.close(end_row=3, end_column=column - 1)
|
||||
frame_ue.close(end_row=4, end_column=column - 1)
|
||||
merge_rcue = ws.get_merge_engine(start_row=3, start_column=column)
|
||||
ws.set_cell(3, column, "Competence", rcue_signature)
|
||||
ws.set_cell(4, column, "Note", rcue_signature)
|
||||
column += 1
|
||||
ws.set_cell(4, column, "Rés.", rcue_signature)
|
||||
column += 1
|
||||
merge_rcue.close(end_row=3, end_column=column - 1)
|
||||
frame.close(end_row=4, end_column=column - 1)
|
||||
merge.close(end_row=2, end_column=column - 1)
|
||||
return column
|
||||
|
||||
def get_ues(self, etudiant):
|
||||
"""get list of candidates UEs for Niveau"""
|
||||
ues = [None, None]
|
||||
for inscription in etudiant.cursus.inscriptions:
|
||||
formation_id = inscription.formsemestre.formation_id
|
||||
semestre_idx = inscription.formsemestre.semestre_id
|
||||
if semestre_idx in self.ues:
|
||||
# identifier les ues cocernées
|
||||
ues[periode(semestre_idx)] = inscription.formsemestre
|
||||
return ues
|
||||
|
||||
|
||||
class CompetenceDesc:
|
||||
def __init__(self, scodocCompetence):
|
||||
self.fromScodoc: ApcCompetence = scodocCompetence
|
||||
self.niveaux = {}
|
||||
for scodocNiveau in scodocCompetence.niveaux.all():
|
||||
self.niveaux[scodocNiveau.id] = NiveauDesc(scodocNiveau)
|
||||
|
||||
def getNiveauDesc(self, niveau_id):
|
||||
return self.niveaux[niveau_id]
|
||||
|
||||
def getNiveaux(self, codeAnnee):
|
||||
niveaux = []
|
||||
for niveau_id, niveauDesc in self.niveaux.items():
|
||||
if codeAnnee == niveauDesc.fromScodoc.annee:
|
||||
niveaux.append(niveauDesc)
|
||||
return niveaux
|
||||
|
||||
|
||||
class FormsemestreDesc:
|
||||
def __init__(self, formsemestre_id, formsemestre=None):
|
||||
self.formsemestre_id = formsemestre_id
|
||||
self.formsemestre = formsemestre
|
||||
self.resultats = None
|
||||
|
||||
def get_desc(self, competence_id=None):
|
||||
if competence_id is None:
|
||||
return self
|
||||
return self.ues.get(competence_id, None)
|
||||
|
||||
def get_formsemestre(self):
|
||||
if self.formsemestre is None:
|
||||
self.formsemestre = FormSemestre.get(self.formsemestre_id)
|
||||
return self.formsemestre
|
||||
|
||||
def get_resultats(self):
|
||||
if self.resultats is None:
|
||||
self.resultats = res_sem.load_formsemestre_results(self.get_formsemestre())
|
||||
return self.resultats
|
||||
|
||||
|
||||
class AnneeDesc:
|
||||
def __init__(self, codeAnnee):
|
||||
self.codeAnnee = codeAnnee
|
||||
self.niveaux = {}
|
||||
|
||||
def get_desc(self, competence_id=None, periode=None):
|
||||
if competence_id is None and periode is None:
|
||||
return self
|
||||
elif competence_id is None:
|
||||
return self.formsemestres.get(periode, None)
|
||||
elif competence_id in self.niveaux:
|
||||
return self.niveaux[competence_id].get_desc(periode)
|
||||
else:
|
||||
return None
|
||||
|
||||
def addNiveau(self, niveaux):
|
||||
for niveau in niveaux:
|
||||
competence_id = niveau.fromScodoc.competence_id
|
||||
self.niveaux[competence_id] = niveau
|
||||
|
||||
def generate_blank_niveau(self, ws: ScoExcelSheet, column: int):
|
||||
return column
|
||||
|
||||
def generate_data(
|
||||
self, ws: ScoExcelSheet, row: int, etudiant: "EtudiantJury", column: int
|
||||
) -> int:
|
||||
for niveau in self.niveaux.values():
|
||||
column = niveau.generate_data(ws, row, etudiant, column)
|
||||
for i in range(len(self.niveaux), 6):
|
||||
column = NiveauDesc.generate_blank_data(ws, row, column)
|
||||
ws.set_cell(row, column + 0, "A1")
|
||||
ws.set_cell(row, column + 1, etudiant.get_note(self.codeAnnee), base_signature)
|
||||
ws.set_cell(row, column + 2, etudiant.get_res(self.codeAnnee))
|
||||
column += 3
|
||||
if self.codeAnnee == "BUT2":
|
||||
# ws.set_cell(row, column, etudiant.getDipl("BUT2"))
|
||||
column += 1
|
||||
if self.codeAnnee == "BUT3":
|
||||
# ws.set_cell(row, column, etudiant.getDipl("BUT3"))
|
||||
column += 1
|
||||
return column
|
||||
|
||||
def generate_header(self, ws: ScoExcelSheet, column: int):
|
||||
start = column
|
||||
but_signature = FMT.FILL_BGCOLOR.write(
|
||||
signature=base_signature, value=header_colors[self.codeAnnee]["BUT"].value
|
||||
)
|
||||
merge = ws.get_merge_engine(start_row=1, start_column=column)
|
||||
frame = ws.get_frame_engine(
|
||||
start_row=1, start_column=column, thickness=SCO_BORDERTHICKNESS.BORDER_THICK
|
||||
)
|
||||
ws.set_cell(
|
||||
1,
|
||||
column,
|
||||
text=self.codeAnnee,
|
||||
from_signature=but_signature,
|
||||
composition=[
|
||||
(FMT.BORDER_LEFT_COLOR, SCO_COLORS.BLACK.value),
|
||||
(FMT.BORDER_LEFT_STYLE, SCO_BORDERTHICKNESS.BORDER_MEDIUM.value),
|
||||
],
|
||||
)
|
||||
for niveau in self.niveaux.values():
|
||||
column = niveau.generate_header(ws, column)
|
||||
for i in range(len(self.niveaux), 6):
|
||||
column = NiveauDesc.generate_blank_header(ws, column, self.codeAnnee)
|
||||
merge_annee = ws.get_merge_engine(start_row=2, start_column=column)
|
||||
ws.set_cell(2, column, "Année", from_signature=but_signature)
|
||||
# cell_format(ws.cell(2, column), but_signature, [(FMT.FONT_BOLD, True)])
|
||||
ws.set_cell(3, column, "Nb", from_signature=but_signature)
|
||||
ws.set_cell(4, column, "RCUE", from_signature=but_signature)
|
||||
column += 1
|
||||
ws.set_cell(3, column, from_signature=but_signature)
|
||||
ws.set_cell(3, column, "Moy.", from_signature=but_signature)
|
||||
ws.set_cell(4, column, f"Sem", from_signature=but_signature)
|
||||
column += 1
|
||||
ws.set_cell(3, column, from_signature=but_signature)
|
||||
ws.set_cell(4, column, "Rés.", from_signature=but_signature)
|
||||
column += 1
|
||||
merge_annee.close(end_row=2, end_column=column - 1)
|
||||
if self.codeAnnee == "BUT2":
|
||||
ws.set_cell(2, column, "DUT", from_signature=but_signature)
|
||||
ws.set_cell(3, column, from_signature=but_signature)
|
||||
ws.set_cell(4, column, "Rés.", from_signature=but_signature)
|
||||
column += 1
|
||||
if self.codeAnnee == "BUT3":
|
||||
ws.set_cell(2, column, "BUT", from_signature=but_signature)
|
||||
ws.set_cell(3, column, from_signature=but_signature)
|
||||
ws.set_cell(4, column, "Rés.", from_signature=but_signature)
|
||||
column += 1
|
||||
frame.close(end_row=4, end_column=column - 1)
|
||||
merge.close(end_row=1, end_column=column - 1)
|
||||
return column
|
||||
|
||||
|
||||
class ParcoursDesc:
|
||||
signature_header = FMT.compose(
|
||||
[
|
||||
(FMT.FILL_BGCOLOR, SCO_COLORS.LIGHT_YELLOW.value),
|
||||
(FMT.NUMBER_FORMAT, SCO_NUMBER_FORMAT.NUMBER_GENERAL.value),
|
||||
# (FMT.FONT_BOLD, True),
|
||||
# (FMT.FONT_SIZE, SCO_FONTSIZE.FONTSIZE_13.value),
|
||||
# (FMT.ALIGNMENT_VALIGN, SCO_VALIGN.VALIGN_CENTER.value),
|
||||
# (FMT.ALIGNMENT_HALIGN, SCO_HALIGN.HALIGN_CENTER.value),
|
||||
# (FMT.BORDER_RIGHT, HAIR_BLACK),
|
||||
# (FMT.BORDER_LEFT, HAIR_BLACK),
|
||||
# (FMT.BORDER_TOP, HAIR_BLACK),
|
||||
# (FMT.BORDER_BOTTOM, HAIR_BLACK),
|
||||
],
|
||||
base_signature,
|
||||
)
|
||||
|
||||
def __init__(self, formation, scodocParcour: ApcParcours = None):
|
||||
self.fromScodoc: ApcParcours = scodocParcour # None pour le tronc commun 'TC'
|
||||
self.etudiants = []
|
||||
self.competences = {}
|
||||
self.annees = {}
|
||||
if scodocParcour is None:
|
||||
for (
|
||||
scodocCompetence
|
||||
) in formation.referentiel_competence.get_competences_tronc_commun():
|
||||
self.competences[scodocCompetence.id] = CompetenceDesc(scodocCompetence)
|
||||
else:
|
||||
query = formation.query_competences_parcour(scodocParcour)
|
||||
if not query is None:
|
||||
for scodocCompetence in query.all():
|
||||
self.competences[scodocCompetence.id] = CompetenceDesc(
|
||||
scodocCompetence
|
||||
)
|
||||
for codeAnnee in liste_annees:
|
||||
annee_desc = AnneeDesc(codeAnnee)
|
||||
for competence_id, competence in self.competences.items():
|
||||
annee_desc.addNiveau(competence.getNiveaux(codeAnnee))
|
||||
self.annees[codeAnnee] = annee_desc
|
||||
|
||||
def get_desc(self, annee=None, competence_id=None, periode=None):
|
||||
if annee is None:
|
||||
return self
|
||||
if annee in self.annees:
|
||||
return self.annees[annee].get_desc(competence_id, periode)
|
||||
return None
|
||||
|
||||
def add_etudiant(self, etudiant):
|
||||
if not etudiant in self.etudiants:
|
||||
self.etudiants.append(etudiant)
|
||||
|
||||
def getNiveauDesc(self, competence_id, niveau_id):
|
||||
return self.competences[competence_id].getNiveauDesc(niveau_id)
|
||||
|
||||
def getData(self):
|
||||
data = []
|
||||
for etudiant in self.etudiants:
|
||||
data.append(etudiant.get_data())
|
||||
return data
|
||||
|
||||
def append_title_column(self, worksheet, cells, val1, val2, val3, val4):
|
||||
cells1, cells2, cells3, cells4 = cells
|
||||
cells1.append(worksheet.make_cell(val1))
|
||||
cells2.append(worksheet.make_cell(val2))
|
||||
cells3.append(worksheet.make_cell(val3))
|
||||
cells4.append(worksheet.make_cell(val4))
|
||||
|
||||
def handle_description(
|
||||
self, ws: ScoExcelSheet, description: tuple, row: int, column: int
|
||||
) -> int:
|
||||
title, content_list = description
|
||||
frame_thickness, frame_color = [
|
||||
(SCO_BORDERTHICKNESS.BORDER_THICK, SCO_COLORS.BLACK),
|
||||
(SCO_BORDERTHICKNESS.BORDER_HAIR, SCO_COLORS.BLACK),
|
||||
(SCO_BORDERTHICKNESS.BORDER_HAIR, SCO_COLORS.BLACK),
|
||||
(SCO_BORDERTHICKNESS.NONE, SCO_COLORS.NONE),
|
||||
][row - 1]
|
||||
frame = ws.get_frame_engine(
|
||||
start_row=row,
|
||||
start_column=column,
|
||||
thickness=frame_thickness,
|
||||
color=frame_color,
|
||||
)
|
||||
ws.set_cell(
|
||||
row=row,
|
||||
column=column,
|
||||
text=title,
|
||||
from_signature=self.signature_header,
|
||||
)
|
||||
merge_h = ws.get_merge_engine(start_row=row, start_column=column)
|
||||
merge_v = ws.get_merge_engine(start_row=row, start_column=column)
|
||||
if content_list is None:
|
||||
merge_v.close(end_row=4)
|
||||
column += 1
|
||||
else:
|
||||
for content in content_list:
|
||||
column = self.handle_description(ws, content, row + 1, column)
|
||||
merge_h.close(end_column=column - 1)
|
||||
frame.close(4, column - 1)
|
||||
return column
|
||||
|
||||
def generate_etudiant_header(self, ws: ScoExcelSheet) -> int:
|
||||
titles = (
|
||||
"ETUDIANT",
|
||||
[
|
||||
("id", None),
|
||||
("nip", None),
|
||||
("Civ", None),
|
||||
("nom", None),
|
||||
("prenom", None),
|
||||
("parcours", None),
|
||||
("cursus", None),
|
||||
(
|
||||
"absences",
|
||||
[
|
||||
("Tot.", None),
|
||||
("Non", [("Just.", None)]),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
column = self.handle_description(ws, titles, 1, 1)
|
||||
return column
|
||||
|
||||
def generate_header(self, ws: ScoExcelSheet):
|
||||
column: int = self.generate_etudiant_header(ws)
|
||||
for codeAnnee in liste_annees:
|
||||
column = self.annees[codeAnnee].generate_header(ws, column)
|
||||
|
||||
def generate(self, workbook: ScoExcelBook):
|
||||
if self.fromScodoc:
|
||||
sheet_name = self.fromScodoc.code
|
||||
else:
|
||||
sheet_name = "TC"
|
||||
worksheet: ScoExcelSheet = workbook.create_sheet(sheet_name)
|
||||
self.generate_header(worksheet)
|
||||
self.generate_etudiants(worksheet)
|
||||
|
||||
def generate_data(
|
||||
self, ws: ScoExcelSheet, row: int, column: int, etudiant: "EtudiantJury"
|
||||
):
|
||||
for codeAnnee in liste_annees:
|
||||
column = self.annees[codeAnnee].generate_data(ws, row, etudiant, column)
|
||||
|
||||
def generate_etudiants(self, ws: ScoExcelSheet):
|
||||
ligne = 5
|
||||
for etudiant in self.etudiants:
|
||||
ws.set_cell(ligne, 1, etudiant.ident.id)
|
||||
ws.set_cell(ligne, 2, etudiant.ident.code_nip)
|
||||
ws.set_cell(ligne, 3, etudiant.ident.civilite)
|
||||
ws.set_cell(ligne, 4, etudiant.ident.nom)
|
||||
ws.set_cell(ligne, 5, etudiant.ident.prenom)
|
||||
if etudiant.parcour:
|
||||
ws.set_cell(ligne, 6, etudiant.parcour.code)
|
||||
else:
|
||||
ws.set_cell(ligne, 6, "-")
|
||||
cursus = ", ".join(etudiant.history)
|
||||
ws.set_cell(ligne, 7, cursus)
|
||||
ws.set_cell(ligne, 8, etudiant.nbabs)
|
||||
ws.set_cell(ligne, 9, etudiant.nbabs - etudiant.nbabsjust)
|
||||
# self.generate_data(ws, ligne, 10, etudiant)
|
||||
ligne = ligne + 1
|
|
@ -0,0 +1,410 @@
|
|||
# -*- 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
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from openpyxl.cell import WriteOnlyCell
|
||||
from openpyxl.worksheet.worksheet import Worksheet
|
||||
|
||||
from app.but.prepajury_xl_format import (
|
||||
Sco_Style,
|
||||
FMT,
|
||||
SCO_FONTNAME,
|
||||
SCO_FONTSIZE,
|
||||
SCO_HALIGN,
|
||||
SCO_VALIGN,
|
||||
SCO_NUMBER_FORMAT,
|
||||
SCO_BORDERTHICKNESS,
|
||||
SCO_COLORS,
|
||||
fmt_atomics,
|
||||
)
|
||||
|
||||
""" Excel file handling
|
||||
"""
|
||||
import datetime
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
import openpyxl.utils.datetime
|
||||
from openpyxl.styles.numbers import FORMAT_DATE_DDMMYY
|
||||
from openpyxl.comments import Comment
|
||||
from openpyxl import Workbook
|
||||
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
|
||||
# Un style est enregistré comme un dictionnaire qui précise la valeur d'un attribut dans la liste suivante:
|
||||
# font, border, number_format, fill,...
|
||||
# (cf https://openpyxl.readthedocs.io/en/stable/styles.html#working-with-styles)
|
||||
|
||||
base_signature = (
|
||||
FMT.FONT_NAME.set(SCO_FONTNAME.FONTNAME_CALIBRI)
|
||||
+ FMT.FONT_SIZE.set(SCO_FONTSIZE.FONTSIZE_13)
|
||||
+ FMT.ALIGNMENT_HALIGN.set(SCO_HALIGN.HALIGN_CENTER)
|
||||
+ FMT.ALIGNMENT_VALIGN.set(SCO_VALIGN.VALIGN_CENTER)
|
||||
+ FMT.NUMBER_FORMAT.set(SCO_NUMBER_FORMAT.NUMBER_GENERAL)
|
||||
)
|
||||
|
||||
|
||||
class Sco_Cell:
|
||||
def __init__(self, text: str = "", signature: int = 0):
|
||||
self.text = text
|
||||
self.signature = signature
|
||||
|
||||
def alter(self, signature: int):
|
||||
for fmt in fmt_atomics:
|
||||
value: int = fmt.composante.read(signature)
|
||||
if value > 0:
|
||||
self.signature = fmt.write(value, self.signature)
|
||||
|
||||
def build(self, ws: Worksheet, row: int, column: int):
|
||||
cell = ws.cell(row, column)
|
||||
cell.value = self.text
|
||||
FMT.ALL.apply(cell=cell, signature=self.signature)
|
||||
|
||||
|
||||
class Frame_Engine:
|
||||
def __init__(
|
||||
self,
|
||||
ws: ScoExcelSheet,
|
||||
start_row: int = 1,
|
||||
start_column: int = 1,
|
||||
thickness: SCO_BORDERTHICKNESS = SCO_BORDERTHICKNESS.NONE,
|
||||
color: SCO_COLORS = SCO_COLORS.BLACK,
|
||||
):
|
||||
self.start_row: int = start_row
|
||||
self.start_column: int = start_column
|
||||
self.ws: ScoExcelSheet = ws
|
||||
self.border_style = FMT.BORDER_LEFT.make_zero_based_constant([thickness, color])
|
||||
|
||||
def close(self, end_row: int, end_column: int):
|
||||
left_signature: int = FMT.BORDER_LEFT.write(self.border_style)
|
||||
right_signature: int = FMT.BORDER_RIGHT.write(self.border_style)
|
||||
top_signature: int = FMT.BORDER_TOP.write(self.border_style)
|
||||
bottom_signature: int = FMT.BORDER_BOTTOM.write(self.border_style)
|
||||
for row in range(self.start_row, end_row + 1):
|
||||
self.ws.cells[row][self.start_column].alter(left_signature)
|
||||
self.ws.cells[row][end_column].alter(right_signature)
|
||||
for column in range(self.start_column, end_column + 1):
|
||||
self.ws.cells[self.start_row][column].alter(top_signature)
|
||||
self.ws.cells[end_row][column].alter(bottom_signature)
|
||||
|
||||
|
||||
class Merge_Engine:
|
||||
def __init__(self, start_row: int = 1, start_column: int = 1):
|
||||
self.start_row: int = start_row
|
||||
self.start_column: int = start_column
|
||||
self.end_row: int = None
|
||||
self.end_column: int = None
|
||||
self.closed: bool = False
|
||||
|
||||
def close(self, end_row=None, end_column=None):
|
||||
if end_row is None:
|
||||
self.end_row = self.start_row + 1
|
||||
else:
|
||||
self.end_row = end_row + 1
|
||||
if end_column is None:
|
||||
self.end_column = self.start_column + 1
|
||||
else:
|
||||
self.end_column = end_column + 1
|
||||
self.closed = True
|
||||
|
||||
def write(self, ws: Worksheet):
|
||||
if self.closed:
|
||||
if (self.end_row - self.start_row > 0) and (
|
||||
self.end_column - self.start_column > 0
|
||||
):
|
||||
ws.merge_cells(
|
||||
start_row=self.start_row,
|
||||
start_column=self.start_column,
|
||||
end_row=self.end_row - 1,
|
||||
end_column=self.end_column - 1,
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"( {self.start_row}:{self.start_column}-{self.end_row}:{self.end_column})[{self.closed}]"
|
||||
|
||||
|
||||
def xldate_as_datetime(xldate, datemode=0):
|
||||
"""Conversion d'une date Excel en datetime python
|
||||
Deux formats de chaîne acceptés:
|
||||
* JJ/MM/YYYY (chaîne naïve)
|
||||
* Date ISO (valeur de type date lue dans la feuille)
|
||||
Peut lever une ValueError
|
||||
"""
|
||||
try:
|
||||
return datetime.datetime.strptime(xldate, "%d/%m/%Y")
|
||||
except:
|
||||
return openpyxl.utils.datetime.from_ISO8601(xldate)
|
||||
|
||||
|
||||
def adjust_sheetname(sheet_name):
|
||||
"""Renvoie un nom convenable pour une feuille excel: < 31 cars, sans caractères spéciaux
|
||||
Le / n'est pas autorisé par exemple.
|
||||
Voir https://xlsxwriter.readthedocs.io/workbook.html#add_worksheet
|
||||
"""
|
||||
sheet_name = scu.make_filename(sheet_name)
|
||||
# Le nom de la feuille ne peut faire plus de 31 caractères.
|
||||
# si la taille du nom de feuille est > 31 on tronque (on pourrait remplacer par 'feuille' ?)
|
||||
return sheet_name[:31]
|
||||
|
||||
|
||||
class ScoExcelBook:
|
||||
"""Permet la génération d'un classeur xlsx composé de plusieurs feuilles.
|
||||
usage:
|
||||
wb = ScoExcelBook()
|
||||
ws0 = wb.create_sheet('sheet name 0')
|
||||
ws1 = wb.create_sheet('sheet name 1')
|
||||
...
|
||||
steam = wb.generate()
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.sheets = [] # list of sheets
|
||||
self.wb = Workbook()
|
||||
|
||||
def create_sheet(self, sheet_name="feuille", default_signature: int = 0):
|
||||
"""Crée une nouvelle feuille dans ce classeur
|
||||
sheet_name -- le nom de la feuille
|
||||
default_style -- le style par défaut
|
||||
"""
|
||||
sheet_name = adjust_sheetname(sheet_name)
|
||||
ws = self.wb.create_sheet(sheet_name)
|
||||
sheet = ScoExcelSheet(sheet_name, default_signature=default_signature, ws=ws)
|
||||
self.sheets.append(sheet)
|
||||
return sheet
|
||||
|
||||
def generate(self):
|
||||
"""génération d'un stream binaire représentant la totalité du classeur.
|
||||
retourne le flux
|
||||
"""
|
||||
sheet: Worksheet = self.wb.get_sheet_by_name("Sheet")
|
||||
self.wb.remove_sheet(sheet)
|
||||
|
||||
for sheet in self.sheets:
|
||||
sheet.prepare()
|
||||
# construction d'un flux
|
||||
# (https://openpyxl.readthedocs.io/en/stable/tutorial.html#saving-as-a-stream)
|
||||
with NamedTemporaryFile() as tmp:
|
||||
self.wb.save(tmp.name)
|
||||
tmp.seek(0)
|
||||
return tmp.read()
|
||||
|
||||
|
||||
class ScoExcelSheet:
|
||||
"""Représente une feuille qui peut être indépendante ou intégrée dans un ScoExcelBook.
|
||||
En application des directives de la bibliothèque sur l'écriture optimisée, l'ordre des opérations
|
||||
est imposé:
|
||||
* instructions globales (largeur/maquage des colonnes et ligne, ...)
|
||||
* construction et ajout des cellules et ligne selon le sens de lecture (occidental)
|
||||
ligne de haut en bas et cellules de gauche à droite (i.e. A1, A2, .. B1, B2, ..)
|
||||
* pour finir appel de la méthode de génération
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sheet_name: str = "feuille",
|
||||
default_signature: int = 0,
|
||||
ws: Worksheet = None,
|
||||
):
|
||||
"""Création de la feuille. sheet_name
|
||||
-- le nom de la feuille default_style
|
||||
-- le style par défaut des cellules ws
|
||||
-- None si la feuille est autonome (dans ce cas elle crée son propre wb), sinon c'est la worksheet
|
||||
créée par le workbook propriétaire un workbook est créé et associé à cette feuille.
|
||||
"""
|
||||
# Le nom de la feuille ne peut faire plus de 31 caractères.
|
||||
# si la taille du nom de feuille est > 31 on tronque (on pourrait remplacer par 'feuille' ?)
|
||||
self.sheet_name = adjust_sheetname(sheet_name)
|
||||
self.default_signature = default_signature
|
||||
self.merges: list[Merge_Engine] = []
|
||||
if ws is None:
|
||||
self.wb = Workbook()
|
||||
self.ws = self.wb.active
|
||||
self.ws.title = self.sheet_name
|
||||
else:
|
||||
self.wb = None
|
||||
self.ws = ws
|
||||
# internal data
|
||||
self.cells = defaultdict(lambda: defaultdict(Sco_Cell))
|
||||
self.column_dimensions = {}
|
||||
self.row_dimensions = {}
|
||||
|
||||
def get_frame_engine(
|
||||
self,
|
||||
start_row: int,
|
||||
start_column: int,
|
||||
thickness: SCO_BORDERTHICKNESS = SCO_BORDERTHICKNESS.NONE,
|
||||
color: SCO_COLORS = SCO_COLORS.NONE,
|
||||
):
|
||||
return Frame_Engine(
|
||||
ws=self,
|
||||
start_row=start_row,
|
||||
start_column=start_column,
|
||||
thickness=thickness,
|
||||
color=color,
|
||||
)
|
||||
|
||||
def get_merge_engine(self, start_row: int, start_column: int):
|
||||
merge_engine: Merge_Engine = Merge_Engine(
|
||||
start_row=start_row, start_column=start_column
|
||||
)
|
||||
self.merges.append(merge_engine)
|
||||
return merge_engine
|
||||
|
||||
@staticmethod
|
||||
def i2col(idx):
|
||||
if idx < 26: # one letter key
|
||||
return chr(idx + 65)
|
||||
else: # two letters AA..ZZ
|
||||
first = idx // 26
|
||||
second = idx % 26
|
||||
return "" + chr(first + 64) + chr(second + 65)
|
||||
|
||||
def set_cell(
|
||||
self,
|
||||
row: int,
|
||||
column: int,
|
||||
text: str = "",
|
||||
from_signature: int = 0,
|
||||
composition: list = [],
|
||||
):
|
||||
cell: Sco_Cell = self.cells[row][column]
|
||||
cell.text = text
|
||||
cell.alter(FMT.compose(composition, from_signature))
|
||||
|
||||
def set_column_dimension_width(self, cle=None, value=21):
|
||||
"""Détermine la largeur d'une colonne. cle -- identifie la colonne ("A" "B", ... ou 0, 1, 2, ...) si None,
|
||||
value donne la liste des largeurs de colonnes depuis A, B, C, ... value -- la dimension (unité : 7 pixels
|
||||
comme affiché dans Excel)
|
||||
"""
|
||||
if cle is None:
|
||||
for i, val in enumerate(value):
|
||||
self.ws.column_dimensions[self.i2col(i)].width = val
|
||||
# No keys: value is a list of widths
|
||||
elif isinstance(cle, str): # accepts set_column_with("D", ...)
|
||||
self.ws.column_dimensions[cle].width = value
|
||||
else:
|
||||
self.ws.column_dimensions[self.i2col(cle)].width = value
|
||||
|
||||
def set_row_dimension_height(self, cle=None, value=21):
|
||||
"""Détermine la hauteur d'une ligne. cle -- identifie la ligne (1, 2, ...) si None,
|
||||
value donne la liste des hauteurs de colonnes depuis 1, 2, 3, ... value -- la dimension
|
||||
"""
|
||||
if cle is None:
|
||||
for i, val in enumerate(value, start=1):
|
||||
self.ws.row_dimensions[i].height = val
|
||||
# No keys: value is a list of widths
|
||||
else:
|
||||
self.ws.row_dimensions[cle].height = value
|
||||
|
||||
def set_row_dimension_hidden(self, cle, value):
|
||||
"""Masque ou affiche une ligne.
|
||||
cle -- identifie la colonne (1...)
|
||||
value -- boolean (vrai = colonne cachée)
|
||||
"""
|
||||
self.ws.row_dimensions[cle].hidden = value
|
||||
|
||||
def set_column_dimension_hidden(self, cle, value):
|
||||
"""Masque ou affiche une ligne.
|
||||
cle -- identifie la colonne (1...)
|
||||
value -- boolean (vrai = colonne cachée)
|
||||
"""
|
||||
self.ws.column_dimensions[cle].hidden = value
|
||||
|
||||
# def make_cell(self, value: any = None, style: Sco_Style = None, comment=None):
|
||||
# """Construit une cellule.
|
||||
# value -- contenu de la cellule (texte, numérique, booléen ou date)
|
||||
# style -- style par défaut (dictionnaire cf. excel_make_style) de la feuille si non spécifié
|
||||
# """
|
||||
# # adaptation des valeurs si nécessaire
|
||||
# if value is None:
|
||||
# value = ""
|
||||
# elif value is True:
|
||||
# value = 1
|
||||
# elif value is False:
|
||||
# value = 0
|
||||
# elif isinstance(value, datetime.datetime):
|
||||
# value = value.replace(
|
||||
# tzinfo=None
|
||||
# ) # make date naive (cf https://openpyxl.readthedocs.io/en/latest/datetime.html#timezones)
|
||||
#
|
||||
# # création de la cellule
|
||||
# cell = WriteOnlyCell(self.ws, value)
|
||||
#
|
||||
# if style is not None:
|
||||
# style.apply(cell)
|
||||
#
|
||||
# if not comment is None:
|
||||
# cell.comment = Comment(comment, "scodoc")
|
||||
# lines = comment.splitlines()
|
||||
# cell.comment.width = 7 * max([len(line) for line in lines]) if lines else 7
|
||||
# cell.comment.height = 20 * len(lines) if lines else 20
|
||||
#
|
||||
# # test datatype to overwrite datetime format
|
||||
# if isinstance(value, datetime.date):
|
||||
# cell.data_type = "d"
|
||||
# cell.number_format = FORMAT_DATE_DDMMYY
|
||||
# elif isinstance(value, int) or isinstance(value, float):
|
||||
# cell.data_type = "n"
|
||||
# else:
|
||||
# cell.data_type = "s"
|
||||
#
|
||||
# return cell
|
||||
|
||||
def prepare(self):
|
||||
"""génére un flux décrivant la feuille.
|
||||
Ce flux pourra ensuite être repris dans send_excel_file (classeur mono feille)
|
||||
ou pour la génération d'un classeur multi-feuilles
|
||||
"""
|
||||
# for row in self.column_dimensions.keys():
|
||||
# self.ws.column_dimensions[row] = self.column_dimensions[row]
|
||||
# for row in self.row_dimensions.keys():
|
||||
# self.ws.row_dimensions[row] = self.row_dimensions[row]
|
||||
# for row in self.rows:
|
||||
# self.ws.append(row)
|
||||
for row in self.cells:
|
||||
for column in self.cells[row]:
|
||||
self.cells[row][column].build(self.ws, row, column)
|
||||
for merge_engine in self.merges:
|
||||
merge_engine.write(self.ws)
|
||||
|
||||
def generate(self):
|
||||
"""génération d'un classeur mono-feuille"""
|
||||
# this method makes sense only if it is a standalone worksheet (else call workbook.generate()
|
||||
if self.wb is None: # embeded sheet
|
||||
raise ScoValueError("can't generate a single sheet from a ScoWorkbook")
|
||||
|
||||
# construction d'un flux (https://openpyxl.readthedocs.io/en/stable/tutorial.html#saving-as-a-stream)
|
||||
self.prepare()
|
||||
with NamedTemporaryFile() as tmp:
|
||||
self.wb.save(tmp.name)
|
||||
tmp.seek(0)
|
||||
return tmp.read()
|
|
@ -0,0 +1,666 @@
|
|||
import abc
|
||||
from enum import Enum
|
||||
|
||||
import openpyxl.styles
|
||||
from openpyxl.cell import Cell
|
||||
from openpyxl.styles import Side, Border, Font, PatternFill, Alignment
|
||||
from openpyxl.styles.numbers import FORMAT_GENERAL, FORMAT_NUMBER_00
|
||||
|
||||
# Formatting Enums
|
||||
|
||||
|
||||
class SCO_COLORS(Enum):
|
||||
def __new__(cls, value, argb):
|
||||
obj = object.__new__(cls)
|
||||
obj._value_ = value
|
||||
obj.argb = argb
|
||||
return obj
|
||||
|
||||
NONE = (0, None)
|
||||
BLACK = (1, "FF000000")
|
||||
WHITE = (2, "FFFFFFFF")
|
||||
RED = (3, "FFFF0000")
|
||||
BROWN = (4, "FF993300")
|
||||
PURPLE = (5, "FF993366")
|
||||
BLUE = (6, "FF0000FF")
|
||||
ORANGE = (7, "FFFF3300")
|
||||
LIGHT_YELLOW = (8, "FFFFFF99")
|
||||
GREEN = (9, "FF00FF00")
|
||||
GREY = (10, "FF101010")
|
||||
BUT1 = (23, "FF95B3D7")
|
||||
RCUE1 = (24, "FFB8CCE4")
|
||||
UE1 = (25, "FFDCE6F1")
|
||||
BUT2 = (26, "FFC4D79B")
|
||||
RCUE2 = (27, "FFD8E4BC")
|
||||
UE2 = (28, "FFEBF1DE")
|
||||
BUT3 = (29, "FFFABF8F")
|
||||
RCUE3 = (30, "FFFCD5B4")
|
||||
UE3 = (31, "FFFDE9D9")
|
||||
|
||||
|
||||
class SCO_BORDERTHICKNESS(Enum):
|
||||
def __new__(cls, value, width):
|
||||
obj = object.__new__(cls)
|
||||
obj._value_ = value
|
||||
obj.width = width
|
||||
return obj
|
||||
|
||||
NONE = (0, None)
|
||||
BORDER_HAIR = (1, "hair")
|
||||
BORDER_THIN = (2, "thin")
|
||||
BORDER_MEDIUM = (3, "medium")
|
||||
BORDER_THICK = (4, "thick")
|
||||
|
||||
|
||||
class SCO_FONTNAME(Enum):
|
||||
def __new__(cls, value, fontname):
|
||||
obj = object.__new__(cls)
|
||||
obj._value_ = value
|
||||
obj.fontname = fontname
|
||||
return obj
|
||||
|
||||
NONE = (0, None)
|
||||
FONTNAME_CALIBRI = (1, "Calibri")
|
||||
FONTNAME_ARIAL = (2, "Arial")
|
||||
FONTNAME_COURIER = (3, "Courier New")
|
||||
FONTNAME_TIMES = (4, "Times New Roman")
|
||||
|
||||
|
||||
class SCO_FONTSIZE(Enum):
|
||||
def __new__(cls, value, fontsize):
|
||||
obj = object.__new__(cls)
|
||||
obj._value_ = value
|
||||
obj.fontsize = fontsize
|
||||
return obj
|
||||
|
||||
NONE = (0, None)
|
||||
FONTSIZE_9 = (1, 9.0)
|
||||
FONTSIZE_10 = (2, 10.0)
|
||||
FONTSIZE_11 = (2, 11.0)
|
||||
FONTSIZE_13 = (4, 13.0)
|
||||
|
||||
|
||||
class SCO_NUMBER_FORMAT(Enum):
|
||||
def __new__(cls, value, format):
|
||||
obj = object.__new__(cls)
|
||||
obj._value_ = value
|
||||
obj.format = format
|
||||
return obj
|
||||
|
||||
NONE = (0, None)
|
||||
NUMBER_GENERAL = (0, FORMAT_GENERAL)
|
||||
NUMBER_00 = (1, FORMAT_NUMBER_00)
|
||||
NUMBER_0 = (2, "0.0")
|
||||
|
||||
|
||||
class SCO_HALIGN(Enum):
|
||||
def __new__(cls, value, position):
|
||||
obj = object.__new__(cls)
|
||||
obj._value_ = value
|
||||
obj.position = position
|
||||
return obj
|
||||
|
||||
NONE = (0, None)
|
||||
HALIGN_LEFT = (1, "left")
|
||||
HALIGN_CENTER = (2, "center")
|
||||
HALIGN_RIGHT = (3, "right")
|
||||
|
||||
|
||||
class SCO_VALIGN(Enum):
|
||||
def __new__(cls, value, position):
|
||||
obj = object.__new__(cls)
|
||||
obj._value_ = value
|
||||
obj.position = position
|
||||
return obj
|
||||
|
||||
VALIGN_BOTTOM = (0, "bottom")
|
||||
VALIGN_TOP = (1, "top")
|
||||
VALIGN_CENTER = (2, "center")
|
||||
|
||||
|
||||
# Composante (bitfield) atomique. Based on Enums
|
||||
free = 0
|
||||
|
||||
|
||||
class Composante(abc.ABC):
|
||||
def __init__(self, base=None, width: int = 1):
|
||||
global free
|
||||
if base is None:
|
||||
self.base = free
|
||||
free += width
|
||||
else:
|
||||
self.base = base
|
||||
self.width = width
|
||||
self.end = self.base + self.width
|
||||
self.mask = ((1 << width) - 1) << self.base
|
||||
|
||||
def read(self, signature: int) -> int:
|
||||
return (signature & self.mask) >> self.base
|
||||
|
||||
def clear(self, signature: int) -> int:
|
||||
return signature & ~self.mask
|
||||
|
||||
def write(self, index, signature=0) -> int:
|
||||
return self.clear(signature) + (index << self.base)
|
||||
|
||||
def make_zero_based_constant(self, enums: list[Enum] = None):
|
||||
return 0
|
||||
|
||||
@abc.abstractmethod
|
||||
def build(self, value: int):
|
||||
pass
|
||||
|
||||
|
||||
class Composante_boolean(Composante):
|
||||
def __init__(self):
|
||||
super().__init__(width=1)
|
||||
|
||||
def set(self, data: bool, signature=0) -> int:
|
||||
return self.write(1 if data else 0, signature)
|
||||
|
||||
def build(self, signature) -> bool:
|
||||
value = self.read(signature)
|
||||
assert value < (1 << self.width)
|
||||
return value == 1
|
||||
|
||||
|
||||
class Composante_number_format(Composante):
|
||||
WIDTH: int = 2
|
||||
|
||||
def __init__(self):
|
||||
assert (1 << self.WIDTH) > SCO_NUMBER_FORMAT.__len__()
|
||||
super().__init__(width=self.WIDTH)
|
||||
|
||||
def set(self, data: SCO_NUMBER_FORMAT, signature=0) -> int:
|
||||
return self.write(data.value, signature)
|
||||
|
||||
def build(self, signature: int) -> SCO_NUMBER_FORMAT:
|
||||
value = self.read(signature)
|
||||
assert value < (1 << self.width)
|
||||
return SCO_NUMBER_FORMAT(value)
|
||||
|
||||
|
||||
class Composante_Colors(Composante):
|
||||
WIDTH: int = 5
|
||||
|
||||
def __init__(self, default: SCO_COLORS = SCO_COLORS.BLACK):
|
||||
assert (1 << self.WIDTH) > SCO_COLORS.__len__()
|
||||
super().__init__(width=self.WIDTH)
|
||||
self.default: SCO_COLORS = default
|
||||
|
||||
def set(self, data: SCO_COLORS, signature=0) -> int:
|
||||
return self.write(data.value, signature)
|
||||
|
||||
def build(self, signature: int) -> SCO_COLORS:
|
||||
value = self.read(signature)
|
||||
assert value < (1 << self.width)
|
||||
if value == 0:
|
||||
return None
|
||||
try:
|
||||
return SCO_COLORS(value)
|
||||
except:
|
||||
return self.default
|
||||
|
||||
|
||||
class Composante_borderThickness(Composante):
|
||||
WIDTH: int = 3
|
||||
|
||||
def __init__(self):
|
||||
assert (1 << self.WIDTH) > SCO_BORDERTHICKNESS.__len__()
|
||||
super().__init__(width=self.WIDTH)
|
||||
|
||||
def set(self, data: SCO_BORDERTHICKNESS, signature=0) -> int:
|
||||
return self.write(data.value, signature)
|
||||
|
||||
def build(self, signature: int) -> SCO_BORDERTHICKNESS:
|
||||
value = self.read(signature)
|
||||
assert value < (1 << self.width)
|
||||
try:
|
||||
return SCO_BORDERTHICKNESS(value)
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
class Composante_fontname(Composante):
|
||||
WIDTH: int = 3
|
||||
|
||||
def __init__(self):
|
||||
assert (1 << self.WIDTH) > SCO_FONTNAME.__len__()
|
||||
super().__init__(width=self.WIDTH)
|
||||
|
||||
def set(self, data: SCO_FONTNAME, signature=0) -> int:
|
||||
return self.write(data.value, signature)
|
||||
|
||||
def build(self, signature: int) -> SCO_FONTNAME:
|
||||
value = self.read(signature)
|
||||
assert value < (1 << self.width)
|
||||
try:
|
||||
return SCO_FONTNAME(value)
|
||||
except:
|
||||
return SCO_FONTNAME.FONTNAME_CALIBRI
|
||||
|
||||
|
||||
class Composante_fontsize(Composante):
|
||||
WIDTH: int = 3
|
||||
|
||||
def __init__(self):
|
||||
assert (1 << self.WIDTH) > SCO_FONTSIZE.__len__()
|
||||
super().__init__(width=self.WIDTH)
|
||||
|
||||
def set(self, data: SCO_FONTSIZE, signature=0) -> int:
|
||||
return self.write(data.value, signature)
|
||||
|
||||
def build(self, signature: int) -> SCO_FONTSIZE:
|
||||
value = self.read(signature)
|
||||
assert value < (1 << self.width)
|
||||
return SCO_FONTSIZE(value) or None
|
||||
|
||||
|
||||
class Composante_halign(Composante):
|
||||
WIDTH: int = 3
|
||||
|
||||
def __init__(self):
|
||||
assert (1 << self.WIDTH) > SCO_HALIGN.__len__()
|
||||
super().__init__(width=self.WIDTH)
|
||||
|
||||
def set(self, data: SCO_HALIGN, signature=0) -> int:
|
||||
return self.write(data.value, signature)
|
||||
|
||||
def build(self, signature: int) -> SCO_HALIGN:
|
||||
value = self.read(signature)
|
||||
assert value < (1 << self.width)
|
||||
try:
|
||||
return SCO_HALIGN(value)
|
||||
except:
|
||||
return SCO_HALIGN.HALIGN_LEFT
|
||||
|
||||
|
||||
class Composante_valign(Composante):
|
||||
WIDTH: int = 3
|
||||
|
||||
def __init__(self):
|
||||
assert (1 << self.WIDTH) > SCO_VALIGN.__len__()
|
||||
super().__init__(width=self.WIDTH)
|
||||
|
||||
def set(self, data: SCO_VALIGN, signature=0) -> int:
|
||||
return self.write(data.value, signature)
|
||||
|
||||
def build(self, signature: int) -> SCO_VALIGN:
|
||||
value = self.read(signature)
|
||||
assert value < (1 << self.width)
|
||||
try:
|
||||
return SCO_VALIGN(value)
|
||||
except:
|
||||
return SCO_VALIGN.VALIGN_CENTER
|
||||
|
||||
|
||||
# Formatting objects
|
||||
|
||||
|
||||
class Sco_Fill:
|
||||
def __init__(self, color: SCO_COLORS):
|
||||
self.color = color
|
||||
|
||||
def to_openpyxl(self):
|
||||
return PatternFill(
|
||||
fill_type="solid",
|
||||
fgColor=None if self.color is None else self.color.argb,
|
||||
)
|
||||
|
||||
|
||||
class Sco_BorderSide:
|
||||
def __init__(
|
||||
self,
|
||||
thickness: SCO_BORDERTHICKNESS = None,
|
||||
color: SCO_COLORS = SCO_COLORS.WHITE,
|
||||
):
|
||||
self.thickness = thickness
|
||||
self.color: SCO_COLORS = color
|
||||
|
||||
def to_openpyxl(self):
|
||||
return Side(
|
||||
border_style=None if self.thickness is None else self.thickness.width,
|
||||
color=None if self.color is None else self.color.argb,
|
||||
)
|
||||
|
||||
|
||||
class Sco_Borders:
|
||||
def __init__(
|
||||
self,
|
||||
left: Sco_BorderSide = None,
|
||||
right: Sco_BorderSide = None,
|
||||
top: Sco_BorderSide = None,
|
||||
bottom: Sco_BorderSide = None,
|
||||
):
|
||||
self.left = left
|
||||
self.right = right
|
||||
self.top = top
|
||||
self.bottom = bottom
|
||||
|
||||
def to_openpyxl(self):
|
||||
return Border(
|
||||
left=self.left.to_openpyxl(),
|
||||
right=self.right.to_openpyxl(),
|
||||
top=self.top.to_openpyxl(),
|
||||
bottom=self.bottom.to_openpyxl(),
|
||||
)
|
||||
|
||||
|
||||
class Sco_Alignment:
|
||||
def __init__(
|
||||
self,
|
||||
halign: SCO_HALIGN = None,
|
||||
valign: SCO_VALIGN = None,
|
||||
):
|
||||
self.halign = halign
|
||||
self.valign = valign
|
||||
|
||||
def to_openpyxl(self):
|
||||
return Alignment(
|
||||
horizontal=None if self.halign is None else self.halign.position,
|
||||
vertical=None if self.valign is None else self.valign.position,
|
||||
)
|
||||
|
||||
|
||||
class Sco_Font:
|
||||
def __init__(
|
||||
self,
|
||||
name: SCO_FONTNAME = SCO_FONTNAME(0),
|
||||
fontsize: SCO_FONTSIZE = SCO_FONTSIZE(0),
|
||||
bold: bool = None,
|
||||
italic: bool = None,
|
||||
outline: bool = None,
|
||||
color: "SCO_COLORS" = None,
|
||||
):
|
||||
self.name = name
|
||||
self.bold = bold
|
||||
self.italic = italic
|
||||
self.outline = outline
|
||||
self.color = color
|
||||
self.fontsize = fontsize
|
||||
|
||||
def to_openpyxl(self):
|
||||
return Font(
|
||||
name=None if self.name is None else self.name.fontname,
|
||||
size=None if self.fontsize is None else self.fontsize.fontsize,
|
||||
bold=self.bold,
|
||||
italic=self.italic,
|
||||
outline=self.outline,
|
||||
color=None if self.color is None else self.color.argb,
|
||||
)
|
||||
|
||||
|
||||
class Sco_Style:
|
||||
from openpyxl.cell import Cell
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
font: Sco_Font = None,
|
||||
fill: Sco_Fill = None,
|
||||
alignment: Sco_Alignment = None,
|
||||
borders: Sco_Borders = None,
|
||||
number_format: SCO_NUMBER_FORMAT = FORMAT_GENERAL,
|
||||
):
|
||||
self.font = font or None
|
||||
self.fill = fill or None
|
||||
self.alignment = alignment or None
|
||||
self.borders = borders or None
|
||||
self.number_format = number_format or SCO_NUMBER_FORMAT.NUMBER_GENERAL
|
||||
|
||||
def apply(self, cell: Cell):
|
||||
if self.font:
|
||||
cell.font = self.font.to_openpyxl()
|
||||
if self.fill and self.fill.color:
|
||||
cell.fill = self.fill.to_openpyxl()
|
||||
if self.alignment:
|
||||
cell.alignment = self.alignment.to_openpyxl()
|
||||
if self.borders:
|
||||
cell.border = self.borders.to_openpyxl()
|
||||
cell.number_format = (
|
||||
FORMAT_GENERAL
|
||||
if self.number_format is None
|
||||
or self.number_format == SCO_NUMBER_FORMAT.NONE
|
||||
else self.number_format.format
|
||||
)
|
||||
|
||||
|
||||
# Composantes groupant d'autres composantes et dotées d'un mécanisme de cache
|
||||
|
||||
|
||||
class Composante_group(Composante):
|
||||
def __init__(self, composantes: list[Composante]):
|
||||
self.composantes = composantes
|
||||
self.cache = {}
|
||||
mini = min([comp.base for comp in composantes])
|
||||
maxi = max([comp.end for comp in composantes])
|
||||
width = sum([comp.width for comp in composantes])
|
||||
if not width == (maxi - mini):
|
||||
raise Exception("Composante group non complete ou non connexe")
|
||||
super().__init__(base=mini, width=width)
|
||||
|
||||
def lookup_or_cache(self, signature: int):
|
||||
value = self.read(signature)
|
||||
assert value < (1 << self.width)
|
||||
if not value in self.cache:
|
||||
self.cache[value] = self.build(signature)
|
||||
return self.cache[value]
|
||||
|
||||
def make_zero_based_constant(self, enums: list[Enum] = None) -> int:
|
||||
if enums is None:
|
||||
return 0
|
||||
signature = 0
|
||||
for enum, composante in zip(enums, self.composantes):
|
||||
signature += composante.write(enum.value)
|
||||
return signature >> self.base
|
||||
|
||||
|
||||
class Composante_fill(Composante_group):
|
||||
def __init__(self, color: Composante_Colors):
|
||||
super().__init__([color])
|
||||
self.color = color
|
||||
|
||||
def build(self, signature: int) -> Sco_Fill:
|
||||
return Sco_Fill(color=self.color.build(signature))
|
||||
|
||||
|
||||
class Composante_font(Composante_group):
|
||||
def __init__(
|
||||
self,
|
||||
name: Composante_fontname,
|
||||
fontsize: Composante_fontsize,
|
||||
color: Composante_Colors,
|
||||
bold: Composante_boolean,
|
||||
italic: Composante_boolean,
|
||||
outline: Composante_boolean,
|
||||
):
|
||||
super().__init__([name, fontsize, color, bold, italic, outline])
|
||||
self.name = name
|
||||
self.fontsize = fontsize
|
||||
self.color = color
|
||||
self.bold = bold
|
||||
self.italic = italic
|
||||
self.outline = outline
|
||||
|
||||
def build(self, signature: int) -> Sco_Font:
|
||||
return Sco_Font(
|
||||
name=self.name.build(signature),
|
||||
fontsize=self.fontsize.build(signature),
|
||||
color=self.color.build(signature),
|
||||
bold=self.bold.build(signature),
|
||||
italic=self.italic.build(signature),
|
||||
outline=self.outline.build(signature),
|
||||
)
|
||||
|
||||
|
||||
class Composante_border(Composante_group):
|
||||
def __init__(self, thick: Composante_borderThickness, color: Composante_Colors):
|
||||
super().__init__([thick, color])
|
||||
self.thick = thick
|
||||
self.color = color
|
||||
|
||||
def build(self, signature: int) -> Sco_BorderSide:
|
||||
return Sco_BorderSide(
|
||||
thickness=self.thick.build(signature),
|
||||
color=self.color.build(signature),
|
||||
)
|
||||
|
||||
|
||||
class Composante_borders(Composante_group):
|
||||
def __init__(
|
||||
self,
|
||||
left: Composante_border,
|
||||
right: Composante_border,
|
||||
top: Composante_border,
|
||||
bottom: Composante_border,
|
||||
):
|
||||
super().__init__([left, right, top, bottom])
|
||||
self.left = left
|
||||
self.right = right
|
||||
self.top = top
|
||||
self.bottom = bottom
|
||||
|
||||
def build(self, signature: int) -> Sco_Borders:
|
||||
return Sco_Borders(
|
||||
left=self.left.lookup_or_cache(signature),
|
||||
right=self.right.lookup_or_cache(signature),
|
||||
top=self.top.lookup_or_cache(signature),
|
||||
bottom=self.bottom.lookup_or_cache(signature),
|
||||
)
|
||||
|
||||
|
||||
class Composante_alignment(Composante_group):
|
||||
def __init__(self, halign: Composante_halign, valign: Composante_valign):
|
||||
super().__init__([halign, valign])
|
||||
self.halign = halign
|
||||
self.valign = valign
|
||||
|
||||
def build(self, signature: int) -> Sco_Alignment:
|
||||
return Sco_Alignment(
|
||||
halign=self.halign.build(signature),
|
||||
valign=self.valign.build(signature),
|
||||
)
|
||||
|
||||
|
||||
class Composante_all(Composante_group):
|
||||
def __init__(
|
||||
self,
|
||||
font: Composante_font,
|
||||
fill: Composante_fill,
|
||||
borders: Composante_borders,
|
||||
alignment: Composante_alignment,
|
||||
number_format: Composante_number_format,
|
||||
):
|
||||
super().__init__([font, fill, borders, alignment, number_format])
|
||||
assert self.width < 64
|
||||
self.font = font
|
||||
self.fill = fill
|
||||
self.borders = borders
|
||||
self.alignment = alignment
|
||||
self.number_format = number_format
|
||||
|
||||
def build(self, signature: int) -> Sco_Style:
|
||||
return Sco_Style(
|
||||
fill=self.fill.lookup_or_cache(signature),
|
||||
font=self.font.lookup_or_cache(signature),
|
||||
borders=self.borders.lookup_or_cache(signature),
|
||||
alignment=self.alignment.lookup_or_cache(signature),
|
||||
number_format=self.number_format.build(signature),
|
||||
)
|
||||
|
||||
def get_style(self, signature: int):
|
||||
return self.lookup_or_cache(signature)
|
||||
|
||||
|
||||
class FMT(Enum):
|
||||
def __init__(self, composante: Composante):
|
||||
self.composante = composante
|
||||
|
||||
def write(self, value, signature=0) -> int:
|
||||
return self.composante.write(value, signature)
|
||||
|
||||
def set(self, data, signature: int = 0) -> int:
|
||||
return self.composante.set(data, signature)
|
||||
|
||||
def get_style(self, signature: int):
|
||||
return self.composante.lookup_or_cache(signature)
|
||||
|
||||
def make_zero_based_constant(self, enums: list[Enum]) -> int:
|
||||
return self.composante.make_zero_based_constant(enums=enums)
|
||||
|
||||
def apply(self, cell: Cell, signature: int):
|
||||
self.composante.build(signature).apply(cell)
|
||||
|
||||
@classmethod
|
||||
def compose(cls, composition: list[("FMT", int)], signature: int = 0) -> int:
|
||||
for field, value in composition:
|
||||
signature = field.write(value, field.composante.clear(signature))
|
||||
return signature
|
||||
|
||||
@classmethod
|
||||
def style(cls, signature: int = None) -> Sco_Style:
|
||||
return FMT.ALL.get_style(signature)
|
||||
|
||||
FONT_NAME = Composante_fontname()
|
||||
FONT_SIZE = Composante_fontsize()
|
||||
FONT_COLOR = Composante_Colors()
|
||||
FONT_BOLD = Composante_boolean()
|
||||
FONT_ITALIC = Composante_boolean()
|
||||
FONT_OUTLINE = Composante_boolean()
|
||||
BORDER_LEFT_STYLE = Composante_borderThickness()
|
||||
BORDER_LEFT_COLOR = Composante_Colors()
|
||||
BORDER_RIGHT_STYLE = Composante_borderThickness()
|
||||
BORDER_RIGHT_COLOR = Composante_Colors()
|
||||
BORDER_TOP_STYLE = Composante_borderThickness()
|
||||
BORDER_TOP_COLOR = Composante_Colors()
|
||||
BORDER_BOTTOM_STYLE = Composante_borderThickness()
|
||||
BORDER_BOTTOM_COLOR = Composante_Colors()
|
||||
FILL_BGCOLOR = Composante_Colors(None)
|
||||
ALIGNMENT_HALIGN = Composante_halign()
|
||||
ALIGNMENT_VALIGN = Composante_valign()
|
||||
NUMBER_FORMAT = Composante_number_format()
|
||||
FONT = Composante_font(
|
||||
FONT_NAME, FONT_SIZE, FONT_COLOR, FONT_BOLD, FONT_ITALIC, FONT_OUTLINE
|
||||
)
|
||||
FILL = Composante_fill(FILL_BGCOLOR)
|
||||
BORDER_LEFT = Composante_border(BORDER_LEFT_STYLE, BORDER_LEFT_COLOR)
|
||||
BORDER_RIGHT = Composante_border(BORDER_RIGHT_STYLE, BORDER_RIGHT_COLOR)
|
||||
BORDER_TOP = Composante_border(BORDER_TOP_STYLE, BORDER_TOP_COLOR)
|
||||
BORDER_BOTTOM = Composante_border(BORDER_BOTTOM_STYLE, BORDER_BOTTOM_COLOR)
|
||||
BORDERS = Composante_borders(BORDER_LEFT, BORDER_RIGHT, BORDER_TOP, BORDER_BOTTOM)
|
||||
ALIGNMENT = Composante_alignment(ALIGNMENT_HALIGN, ALIGNMENT_VALIGN)
|
||||
ALL = Composante_all(FONT, FILL, BORDERS, ALIGNMENT, NUMBER_FORMAT)
|
||||
|
||||
|
||||
fmt_atomics = {
|
||||
FMT.FONT_NAME,
|
||||
FMT.FONT_SIZE,
|
||||
FMT.FONT_COLOR,
|
||||
FMT.FONT_BOLD,
|
||||
FMT.FONT_ITALIC,
|
||||
FMT.FONT_OUTLINE,
|
||||
FMT.BORDER_LEFT_STYLE,
|
||||
FMT.BORDER_LEFT_COLOR,
|
||||
FMT.BORDER_RIGHT_STYLE,
|
||||
FMT.BORDER_RIGHT_COLOR,
|
||||
FMT.BORDER_TOP_STYLE,
|
||||
FMT.BORDER_TOP_COLOR,
|
||||
FMT.BORDER_BOTTOM_STYLE,
|
||||
FMT.BORDER_BOTTOM_COLOR,
|
||||
FMT.FILL_BGCOLOR,
|
||||
FMT.ALIGNMENT_HALIGN,
|
||||
FMT.ALIGNMENT_VALIGN,
|
||||
FMT.NUMBER_FORMAT,
|
||||
}
|
||||
|
||||
HAIR_BLACK: int = FMT.BORDER_LEFT.make_zero_based_constant(
|
||||
enums=[SCO_BORDERTHICKNESS.BORDER_HAIR, SCO_COLORS.BLACK]
|
||||
)
|
||||
THIN_BLACK: int = FMT.BORDER_LEFT.make_zero_based_constant(
|
||||
enums=[SCO_BORDERTHICKNESS.BORDER_THIN, SCO_COLORS.BLACK]
|
||||
)
|
||||
MEDIUM_BLACK: int = FMT.BORDER_LEFT.make_zero_based_constant(
|
||||
enums=[SCO_BORDERTHICKNESS.BORDER_MEDIUM, SCO_COLORS.BLACK]
|
||||
)
|
||||
THICK_BLACK: int = FMT.BORDER_LEFT.make_zero_based_constant(
|
||||
enums=[SCO_BORDERTHICKNESS.BORDER_THICK, SCO_COLORS.BLACK]
|
||||
)
|
|
@ -3,16 +3,10 @@
|
|||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from app import db
|
||||
from app.models import ModuleImpl
|
||||
from app import db, log
|
||||
from app.models import ModuleImpl, Scolog
|
||||
from app.models.etudiants import Identite
|
||||
from app.auth.models import User
|
||||
from app.scodoc.sco_utils import (
|
||||
EtatAssiduite,
|
||||
EtatJustificatif,
|
||||
localize_datetime,
|
||||
is_period_overlapping,
|
||||
)
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_utils import (
|
||||
EtatAssiduite,
|
||||
|
@ -92,6 +86,20 @@ class Assiduite(db.Model):
|
|||
}
|
||||
return data
|
||||
|
||||
def __str__(self) -> str:
|
||||
"chaine pour journaux et debug (lisible par humain français)"
|
||||
try:
|
||||
etat_str = EtatAssiduite(self.etat).name.lower().capitalize()
|
||||
except ValueError:
|
||||
etat_str = "Invalide"
|
||||
return f"""{etat_str} {
|
||||
"just." if self.est_just else "non just."
|
||||
} de {
|
||||
self.date_debut.strftime("%d/%m/%Y %Hh%M")
|
||||
} à {
|
||||
self.date_fin.strftime("%d/%m/%Y %Hh%M")
|
||||
}"""
|
||||
|
||||
@classmethod
|
||||
def create_assiduite(
|
||||
cls,
|
||||
|
@ -140,33 +148,12 @@ class Assiduite(db.Model):
|
|||
est_just=est_just,
|
||||
)
|
||||
|
||||
return nouv_assiduite
|
||||
|
||||
@classmethod
|
||||
def fast_create_assiduite(
|
||||
cls,
|
||||
etudid: int,
|
||||
date_debut: datetime,
|
||||
date_fin: datetime,
|
||||
etat: EtatAssiduite,
|
||||
moduleimpl_id: int = None,
|
||||
description: str = None,
|
||||
entry_date: datetime = None,
|
||||
est_just: bool = False,
|
||||
) -> object or int:
|
||||
"""Créer une nouvelle assiduité pour l'étudiant"""
|
||||
# Vérification de non duplication des périodes
|
||||
nouv_assiduite = Assiduite(
|
||||
date_debut=date_debut,
|
||||
date_fin=date_fin,
|
||||
etat=etat,
|
||||
etudid=etudid,
|
||||
moduleimpl_id=moduleimpl_id,
|
||||
description=description,
|
||||
entry_date=entry_date,
|
||||
est_just=est_just,
|
||||
log(f"create_assiduite: {etud.id} {nouv_assiduite}")
|
||||
Scolog.logdb(
|
||||
method="create_assiduite",
|
||||
etudid=etud.id,
|
||||
msg=f"assiduité: {nouv_assiduite}",
|
||||
)
|
||||
|
||||
return nouv_assiduite
|
||||
|
||||
|
||||
|
@ -243,6 +230,18 @@ class Justificatif(db.Model):
|
|||
}
|
||||
return data
|
||||
|
||||
def __str__(self) -> str:
|
||||
"chaine pour journaux et debug (lisible par humain français)"
|
||||
try:
|
||||
etat_str = EtatJustificatif(self.etat).name
|
||||
except ValueError:
|
||||
etat_str = "Invalide"
|
||||
return f"""Justificatif {etat_str} de {
|
||||
self.date_debut.strftime("%d/%m/%Y %Hh%M")
|
||||
} à {
|
||||
self.date_fin.strftime("%d/%m/%Y %Hh%M")
|
||||
}"""
|
||||
|
||||
@classmethod
|
||||
def create_justificatif(
|
||||
cls,
|
||||
|
@ -264,29 +263,12 @@ class Justificatif(db.Model):
|
|||
entry_date=entry_date,
|
||||
user_id=user_id,
|
||||
)
|
||||
return nouv_justificatif
|
||||
|
||||
@classmethod
|
||||
def fast_create_justificatif(
|
||||
cls,
|
||||
etudid: int,
|
||||
date_debut: datetime,
|
||||
date_fin: datetime,
|
||||
etat: EtatJustificatif,
|
||||
raison: str = None,
|
||||
entry_date: datetime = None,
|
||||
) -> object or int:
|
||||
"""Créer un nouveau justificatif pour l'étudiant"""
|
||||
|
||||
nouv_justificatif = Justificatif(
|
||||
date_debut=date_debut,
|
||||
date_fin=date_fin,
|
||||
etat=etat,
|
||||
etudid=etudid,
|
||||
raison=raison,
|
||||
entry_date=entry_date,
|
||||
log(f"create_justificatif: {etud.id} {nouv_justificatif}")
|
||||
Scolog.logdb(
|
||||
method="create_justificatif",
|
||||
etudid=etud.id,
|
||||
msg=f"justificatif: {nouv_justificatif}",
|
||||
)
|
||||
|
||||
return nouv_justificatif
|
||||
|
||||
|
||||
|
|
|
@ -93,7 +93,7 @@ _formsemestreEditor = ndb.EditableTable(
|
|||
)
|
||||
|
||||
|
||||
def get_formsemestre(formsemestre_id: int):
|
||||
def get_formsemestre(formsemestre_id: int) -> dict:
|
||||
"list ONE formsemestre"
|
||||
if formsemestre_id is None:
|
||||
raise ValueError("get_formsemestre: id manquant")
|
||||
|
|
|
@ -838,9 +838,6 @@ def _make_listes_sem(formsemestre: FormSemestre, with_absences=True):
|
|||
weekday = datetime.datetime.today().weekday()
|
||||
try:
|
||||
if with_absences:
|
||||
first_monday = sco_abs.ddmmyyyy(
|
||||
formsemestre.date_debut.strftime("%d/%m/%Y")
|
||||
).prev_monday()
|
||||
form_abs_tmpl = f"""
|
||||
<td>
|
||||
<a class="btn" href="{
|
||||
|
@ -857,7 +854,13 @@ def _make_listes_sem(formsemestre: FormSemestre, with_absences=True):
|
|||
</td>
|
||||
"""
|
||||
else:
|
||||
form_abs_tmpl = ""
|
||||
form_abs_tmpl = f"""
|
||||
<td>
|
||||
<a class="btn" href="{
|
||||
url_for("assiduites.visu_assiduites_group", scodoc_dept=g.scodoc_dept)
|
||||
}?group_ids=%(group_id)s&jour={datetime.date.today().isoformat()}&formsemestre_id={formsemestre.id}"><button>Voir l'assiduité</button></a>
|
||||
</td>
|
||||
"""
|
||||
except ScoInvalidDateError: # dates incorrectes dans semestres ?
|
||||
form_abs_tmpl = ""
|
||||
#
|
||||
|
@ -904,8 +907,7 @@ def _make_listes_sem(formsemestre: FormSemestre, with_absences=True):
|
|||
"""
|
||||
)
|
||||
|
||||
if with_absences:
|
||||
H.append(form_abs_tmpl % group)
|
||||
H.append(form_abs_tmpl % group)
|
||||
|
||||
H.append("</tr>")
|
||||
H.append("</table>")
|
||||
|
|
|
@ -287,7 +287,7 @@ if (group_id) {
|
|||
return "\n".join(H)
|
||||
|
||||
|
||||
class DisplayedGroupsInfos(object):
|
||||
class DisplayedGroupsInfos:
|
||||
"""Container with attributes describing groups to display in the page
|
||||
.groups_query_args : 'group_ids=xxx&group_ids=yyy'
|
||||
.base_url : url de la requete, avec les groupes, sans les autres paramètres
|
||||
|
@ -348,7 +348,7 @@ class DisplayedGroupsInfos(object):
|
|||
self.tous_les_etuds_du_sem = (
|
||||
False # affiche tous les etuds du semestre ? (si un seul semestre)
|
||||
)
|
||||
self.sems = collections.OrderedDict() # formsemestre_id : sem
|
||||
self.sems = {} # formsemestre_id : sem
|
||||
self.formsemestre = None
|
||||
self.formsemestre_id = formsemestre_id
|
||||
self.nbdem = 0 # nombre d'étudiants démissionnaires en tout
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
}
|
||||
|
||||
#validate_selectors {
|
||||
margin-top: 5vh;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.no-display {
|
||||
|
@ -91,9 +91,6 @@
|
|||
|
||||
/* === Gestion des etuds row === */
|
||||
|
||||
.etud_holder {
|
||||
margin-top: 5vh;
|
||||
}
|
||||
|
||||
.etud_row {
|
||||
display: grid;
|
||||
|
@ -315,7 +312,7 @@
|
|||
padding: 20px;
|
||||
border: 1px solid #888;
|
||||
width: 80%;
|
||||
height: 40%;
|
||||
height: 320px;
|
||||
position: relative;
|
||||
border-radius: 10px;
|
||||
|
||||
|
|
|
@ -1328,6 +1328,13 @@ tr.etuddem td {
|
|||
color: rgb(100, 100, 100);
|
||||
font-style: italic;
|
||||
}
|
||||
table.gt_table tr.etuddem td a {
|
||||
color: red;
|
||||
}
|
||||
table.gt_table tr.etuddem td.etudinfo:first-child::after {
|
||||
color: red;
|
||||
content: " (dém.)";
|
||||
}
|
||||
|
||||
td.etudabs,
|
||||
td.etudabs a.discretelink,
|
||||
|
|
|
@ -84,19 +84,19 @@ function validateSelectors(btn) {
|
|||
);
|
||||
});
|
||||
|
||||
if (getModuleImplId() == null && window.forceModule) {
|
||||
const HTML = `
|
||||
<p>Attention, le module doit obligatoirement être renseigné.</p>
|
||||
<p>Cela vient de la configuration du semestre ou plus largement du département.</p>
|
||||
<p>Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.</p>
|
||||
`;
|
||||
// if (getModuleImplId() == null && window.forceModule) {
|
||||
// const HTML = `
|
||||
// <p>Attention, le module doit obligatoirement être renseigné.</p>
|
||||
// <p>Cela vient de la configuration du semestre ou plus largement du département.</p>
|
||||
// <p>Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.</p>
|
||||
// `;
|
||||
|
||||
const content = document.createElement("div");
|
||||
content.innerHTML = HTML;
|
||||
// const content = document.createElement("div");
|
||||
// content.innerHTML = HTML;
|
||||
|
||||
openAlertModal("Sélection du module", content);
|
||||
return;
|
||||
}
|
||||
// openAlertModal("Sélection du module", content);
|
||||
// return;
|
||||
// }
|
||||
|
||||
getAssiduitesFromEtuds(true);
|
||||
|
||||
|
@ -269,6 +269,15 @@ function executeMassActionQueue() {
|
|||
};
|
||||
|
||||
assiduite = setModuleImplId(assiduite);
|
||||
if (assiduite.moduleimpl_id == null && window.forceModule) {
|
||||
const html = `
|
||||
<h3>Aucun module n'a été spécifié</h3>
|
||||
`;
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = html;
|
||||
openAlertModal("Erreur Module", div);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const createQueue = []; //liste des assiduités qui seront créées.
|
||||
|
||||
|
@ -311,6 +320,16 @@ function executeMassActionQueue() {
|
|||
return assiduite;
|
||||
});
|
||||
|
||||
if (getModuleImplId() == null && window.forceModule) {
|
||||
const html = `
|
||||
<h3>Aucun module n'a été spécifié</h3>
|
||||
`;
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = html;
|
||||
openAlertModal("Erreur Module", div);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const path = getUrl() + `/api/assiduites/edit`;
|
||||
sync_post(
|
||||
path,
|
||||
|
@ -496,7 +515,7 @@ function generateMassAssiduites() {
|
|||
});
|
||||
});
|
||||
|
||||
if (!verifyDateInSemester()) {
|
||||
if (!verifyDateInSemester() || readOnly) {
|
||||
content.querySelector(".btns_field.mass").setAttribute("disabled", "true");
|
||||
}
|
||||
}
|
||||
|
@ -849,6 +868,16 @@ function createAssiduite(etat, etudid) {
|
|||
|
||||
assiduite = setModuleImplId(assiduite);
|
||||
|
||||
if (assiduite.moduleimpl_id == null && window.forceModule) {
|
||||
const html = `
|
||||
<h3>Aucun module n'a été spécifié</h3>
|
||||
`;
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = html;
|
||||
openAlertModal("Erreur Module", div);
|
||||
return false;
|
||||
}
|
||||
|
||||
const path = getUrl() + `/api/assiduite/${etudid}/create`;
|
||||
sync_post(
|
||||
path,
|
||||
|
@ -865,6 +894,7 @@ function createAssiduite(etat, etudid) {
|
|||
errorAlert();
|
||||
}
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -889,6 +919,7 @@ function deleteAssiduite(assiduite_id) {
|
|||
errorAlert();
|
||||
}
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -905,6 +936,15 @@ function editAssiduite(assiduite_id, etat) {
|
|||
};
|
||||
|
||||
assiduite = setModuleImplId(assiduite);
|
||||
if (assiduite.moduleimpl_id == null && window.forceModule) {
|
||||
const html = `
|
||||
<h3>Aucun module n'a été spécifié</h3>
|
||||
`;
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = html;
|
||||
openAlertModal("Erreur Module", div);
|
||||
return;
|
||||
}
|
||||
const path = getUrl() + `/api/assiduite/${assiduite_id}/edit`;
|
||||
let bool = false;
|
||||
sync_post(
|
||||
|
@ -1072,15 +1112,16 @@ function assiduiteAction(element) {
|
|||
}
|
||||
} else {
|
||||
// Cas normal -> mise à jour en base
|
||||
let done = false;
|
||||
switch (type) {
|
||||
case "création":
|
||||
createAssiduite(etat, etudid);
|
||||
done = createAssiduite(etat, etudid);
|
||||
break;
|
||||
case "édition":
|
||||
if (etat === "remove") {
|
||||
deleteAssiduite(assiduite_id);
|
||||
done = deleteAssiduite(assiduite_id);
|
||||
} else {
|
||||
editAssiduite(assiduite_id, etat);
|
||||
done = editAssiduite(assiduite_id, etat);
|
||||
}
|
||||
break;
|
||||
case "conflit":
|
||||
|
@ -1105,7 +1146,7 @@ function assiduiteAction(element) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (type != "conflit") {
|
||||
if (type != "conflit" && done) {
|
||||
let etatAffiche;
|
||||
|
||||
switch (etat.toUpperCase()) {
|
||||
|
@ -1262,7 +1303,7 @@ function insertEtudRow(etud, index, output = false) {
|
|||
|
||||
bar.appendChild(createMiniTimeline(assiduites[etud.id]));
|
||||
|
||||
if (!verifyDateInSemester()) {
|
||||
if (!verifyDateInSemester() || readOnly) {
|
||||
row.querySelector(".btns_field.single").setAttribute("disabled", "true");
|
||||
}
|
||||
}
|
||||
|
@ -1485,7 +1526,6 @@ function fastJustify(assiduite) {
|
|||
if (justifs.length > 0) {
|
||||
justifyAssiduite(assiduite.assiduite_id, !assiduite.est_just);
|
||||
} else {
|
||||
console.debug("WIP");
|
||||
//créer un nouveau justificatif
|
||||
// Afficher prompt -> demander raison et état
|
||||
|
||||
|
@ -1608,12 +1648,46 @@ function deleteJustificatif(justif_id) {
|
|||
function errorAlert() {
|
||||
const html = `
|
||||
<h3>Avez vous les droits suffisant pour cette action ?</h3>
|
||||
<p>Si c'est bien le cas : veuillez de l'aide sur le canal Assistance de ScoDoc</p>
|
||||
<p>Si c'est bien le cas : demandez de l'aide sur le canal Assistance de ScoDoc</p>
|
||||
<br>
|
||||
<p><i>pour les développeurs : l'erreur est affichée dans la console JS</i></p>
|
||||
|
||||
`;
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = html;
|
||||
openAlertModal("Une erreur s'est déclanchée", div);
|
||||
openAlertModal("Une erreur s'est produite", div);
|
||||
}
|
||||
|
||||
const moduleimpls = {};
|
||||
|
||||
function getModuleImpl(assiduite) {
|
||||
const id = assiduite.moduleimpl_id;
|
||||
|
||||
if (id == null || id == undefined) {
|
||||
if (
|
||||
"desc" in assiduite &&
|
||||
assiduite.desc != null &&
|
||||
assiduite.desc.indexOf("Module:Autre") != -1
|
||||
) {
|
||||
return "Autre";
|
||||
} else {
|
||||
return "Pas de module";
|
||||
}
|
||||
}
|
||||
|
||||
if (id in moduleimpls) {
|
||||
return moduleimpls[id];
|
||||
}
|
||||
const url_api = getUrl() + `/api/moduleimpl/${id}`;
|
||||
sync_get(
|
||||
url_api,
|
||||
(data) => {
|
||||
moduleimpls[id] = `${data.module.code} ${data.module.abbrev}`;
|
||||
},
|
||||
(data) => {
|
||||
moduleimpls[id] = "Pas de module";
|
||||
}
|
||||
);
|
||||
|
||||
return moduleimpls[id];
|
||||
}
|
||||
|
|
|
@ -3,59 +3,64 @@
|
|||
// utilise jQuery / qTip
|
||||
|
||||
function get_etudid_from_elem(e) {
|
||||
// renvoie l'etudid, obtenu a partir de l'id de l'element
|
||||
// qui est soit de la forme xxxx-etudid, soit tout simplement etudid
|
||||
var etudid = e.id.split("-")[1];
|
||||
if (etudid == undefined) {
|
||||
return e.id;
|
||||
} else {
|
||||
return etudid;
|
||||
}
|
||||
// renvoie l'etudid, obtenu a partir de l'id de l'element
|
||||
// qui est soit de la forme xxxx-etudid, soit tout simplement etudid
|
||||
var etudid = e.id.split("-")[1];
|
||||
if (etudid == undefined) {
|
||||
return e.id;
|
||||
} else {
|
||||
return etudid;
|
||||
}
|
||||
}
|
||||
|
||||
$().ready(function () {
|
||||
var elems = $(".etudinfo:not(th)");
|
||||
|
||||
var elems = $(".etudinfo");
|
||||
|
||||
var q_args = get_query_args();
|
||||
var args_to_pass = new Set(
|
||||
["formsemestre_id", "group_ids", "group_id", "partition_id",
|
||||
"moduleimpl_id", "evaluation_id"
|
||||
]);
|
||||
var qs = "";
|
||||
for (var k in q_args) {
|
||||
if (args_to_pass.has(k)) {
|
||||
qs += '&' + k + '=' + q_args[k];
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < elems.length; i++) {
|
||||
$(elems[i]).qtip({
|
||||
content: {
|
||||
ajax: {
|
||||
url: SCO_URL + "/etud_info_html?etudid=" + get_etudid_from_elem(elems[i]) + qs,
|
||||
type: "GET"
|
||||
//success: function(data, status) {
|
||||
// this.set('content.text', data);
|
||||
// xxx called twice on each success ???
|
||||
// console.log(status);
|
||||
}
|
||||
},
|
||||
text: "Loading...",
|
||||
position: {
|
||||
at: "right bottom",
|
||||
my: "left top"
|
||||
},
|
||||
style: {
|
||||
classes: 'qtip-etud'
|
||||
},
|
||||
hide: {
|
||||
fixed: true,
|
||||
delay: 300
|
||||
}
|
||||
// utile pour debugguer le css:
|
||||
// hide: { event: 'unfocus' }
|
||||
});
|
||||
var q_args = get_query_args();
|
||||
var args_to_pass = new Set([
|
||||
"formsemestre_id",
|
||||
"group_ids",
|
||||
"group_id",
|
||||
"partition_id",
|
||||
"moduleimpl_id",
|
||||
"evaluation_id",
|
||||
]);
|
||||
var qs = "";
|
||||
for (var k in q_args) {
|
||||
if (args_to_pass.has(k)) {
|
||||
qs += "&" + k + "=" + q_args[k];
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < elems.length; i++) {
|
||||
$(elems[i]).qtip({
|
||||
content: {
|
||||
ajax: {
|
||||
url:
|
||||
SCO_URL +
|
||||
"/etud_info_html?etudid=" +
|
||||
get_etudid_from_elem(elems[i]) +
|
||||
qs,
|
||||
type: "GET",
|
||||
//success: function(data, status) {
|
||||
// this.set('content.text', data);
|
||||
// xxx called twice on each success ???
|
||||
// console.log(status);
|
||||
},
|
||||
},
|
||||
text: "Loading...",
|
||||
position: {
|
||||
at: "right bottom",
|
||||
my: "left top",
|
||||
},
|
||||
style: {
|
||||
classes: "qtip-etud",
|
||||
},
|
||||
hide: {
|
||||
fixed: true,
|
||||
delay: 300,
|
||||
},
|
||||
// utile pour debugguer le css:
|
||||
// hide: { event: 'unfocus' }
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -8,10 +8,12 @@
|
|||
"""
|
||||
|
||||
from flask import g, url_for
|
||||
from app.models import Identite, Justificatif
|
||||
from app import log
|
||||
from app.models import FormSemestre, Identite, Justificatif
|
||||
from app.tables import table_builder as tb
|
||||
import app.scodoc.sco_assiduites as scass
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
class TableAssi(tb.Table):
|
||||
|
@ -23,12 +25,13 @@ class TableAssi(tb.Table):
|
|||
self,
|
||||
etuds: list[Identite] = None,
|
||||
dates: tuple[str, str] = None,
|
||||
formsemestre: FormSemestre = None,
|
||||
**kwargs,
|
||||
):
|
||||
self.rows: list["RowEtud"] = [] # juste pour que VSCode nous aide sur .rows
|
||||
classes = ["gt_table", "gt_left"]
|
||||
self.dates = [str(dates[0]) + "T00:00", str(dates[1]) + "T23:59"]
|
||||
|
||||
self.formsemestre = formsemestre
|
||||
super().__init__(
|
||||
row_class=RowAssi,
|
||||
classes=classes,
|
||||
|
@ -50,6 +53,17 @@ class RowAssi(tb.Row):
|
|||
|
||||
# pour le moment très simple, extensible (codes, liens bulletins, ...)
|
||||
def __init__(self, table: TableAssi, etud: Identite, *args, **kwargs):
|
||||
# Etat de l'inscription au formsemestre
|
||||
if "classes" not in kwargs:
|
||||
kwargs["classes"] = []
|
||||
try:
|
||||
inscription = table.formsemestre.etuds_inscriptions[etud.id]
|
||||
if inscription.etat == scu.DEMISSION:
|
||||
kwargs["classes"].append("etuddem")
|
||||
except KeyError:
|
||||
log(f"RowAssi: etudid {etud.id} non inscrit à {table.formsemestre.id}")
|
||||
kwargs["classes"].append("non_inscrit") # ne devrait pas arriver !
|
||||
|
||||
super().__init__(table, etud.id, *args, **kwargs)
|
||||
self.etud = etud
|
||||
self.dates = table.dates
|
||||
|
|
|
@ -2,34 +2,50 @@
|
|||
<section id="content">
|
||||
|
||||
<div class="no-display">
|
||||
|
||||
<span class="formsemestre_id">{{formsemestre_id}}</span>
|
||||
<span id="formsemestre_date_debut">{{formsemestre_date_debut}}</span>
|
||||
<span id="formsemestre_date_fin">{{formsemestre_date_fin}}</span>
|
||||
|
||||
</div>
|
||||
|
||||
<h2>
|
||||
Saisie des assiduités {{gr_tit|safe}} {{sem}}
|
||||
</h2>
|
||||
|
||||
{% if readonly == "true" %}
|
||||
<h1 style="font-weight: bolder;color:crimson">La page est en lecture seule.</h1>
|
||||
{% endif %}
|
||||
|
||||
<fieldset class="selectors">
|
||||
<div>Groupes : {{grp|safe}}</div>
|
||||
<div id="forcemodule" style="display: none;">Une préférence du semestre vous impose d'indiquer le module !</div>
|
||||
<div>Module :{{moduleimpl_select|safe}}</div>
|
||||
|
||||
|
||||
<div class="infos">
|
||||
Date: <span id="datestr"></span>
|
||||
<input type="date" name="tl_date" id="tl_date" value="{{ date }}" onchange="updateDate()">
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
{{timeline|safe}}
|
||||
|
||||
{% if readonly == "true" %}
|
||||
<button id="validate_selectors" onclick="validateSelectors(this)">
|
||||
Voir la saisie
|
||||
</button>
|
||||
{% else %}
|
||||
<button id="validate_selectors" onclick="validateSelectors(this)">
|
||||
Faire la saisie
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
{{timeline|safe}}
|
||||
|
||||
|
||||
|
||||
{% if readonly == "false" %}
|
||||
<div style="margin: 1vh 0;">
|
||||
<div id="forcemodule" style="display: none; margin:10px 0px;">Une préférence du semestre vous impose d'indiquer
|
||||
le module !</div>
|
||||
<div>Module :{{moduleimpl_select|safe}}</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{% endif %}
|
||||
<div class="etud_holder">
|
||||
<p class="placeholder">
|
||||
Veillez à choisir le groupe concerné par la saisie ainsi que la date de la saisie.
|
||||
|
@ -75,6 +91,9 @@
|
|||
|
||||
const nonWorkDays = [{{ nonworkdays| safe }}];
|
||||
|
||||
const readOnly = {{ readonly }};
|
||||
|
||||
|
||||
updateDate();
|
||||
setupDate();
|
||||
setupTimeLine();
|
||||
|
@ -89,17 +108,14 @@
|
|||
|
||||
const select = document.getElementById("moduleimpl_select");
|
||||
|
||||
if (select.value == "") {
|
||||
btn.disabled = true;
|
||||
if (!readOnly && select.value == "") {
|
||||
document.getElementById('forcemodule').style.display = "block";
|
||||
}
|
||||
|
||||
select.addEventListener('change', (e) => {
|
||||
select?.addEventListener('change', (e) => {
|
||||
if (e.target.value != "") {
|
||||
btn.disabled = false;
|
||||
document.getElementById('forcemodule').style.display = "none";
|
||||
} else {
|
||||
btn.disabled = true;
|
||||
document.getElementById('forcemodule').style.display = "block";
|
||||
}
|
||||
});
|
||||
|
|
|
@ -129,6 +129,7 @@
|
|||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button id="finish" class="btnPrompt">Terminer la résolution</button>
|
||||
<button id="delete" class="btnPrompt" disabled>Supprimer</button>
|
||||
<button id="split" class="btnPrompt" disabled>Séparer</button>
|
||||
<button id="edit" class="btnPrompt" disabled>Modifier l'état</button>
|
||||
|
@ -157,6 +158,7 @@
|
|||
this.deleteBtn.addEventListener('click', () => { this.deleteAssiduiteModal() });
|
||||
this.editBtn.addEventListener('click', () => { this.editAssiduiteModal() });
|
||||
this.splitBtn.addEventListener('click', () => { this.splitAssiduiteModal() });
|
||||
document.querySelector("#myModal #finish").addEventListener('click', () => { this.close() })
|
||||
|
||||
document.querySelector('#myModal .close').addEventListener('click', () => { this.close() })
|
||||
|
||||
|
@ -311,7 +313,7 @@
|
|||
}
|
||||
};
|
||||
|
||||
openPromptModal("Entrée demandée", fieldSet, success, () => { }, "#37f05f");
|
||||
openPromptModal("Séparation de l'assiduité sélectionnée", fieldSet, success, () => { }, "#23aa40");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -355,7 +357,7 @@
|
|||
};
|
||||
|
||||
//Affichage du prompt
|
||||
openPromptModal("Entrée demandée", fieldSet, success, () => { }, "#37f05f");
|
||||
openPromptModal("Modification de l'état de l'assiduité sélectionnée", fieldSet, success, () => { }, "#23aa40");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -38,15 +38,15 @@
|
|||
}
|
||||
|
||||
array.forEach((assiduité) => {
|
||||
const startDate = moment(assiduité.date_debut);
|
||||
const endDate = moment(assiduité.date_fin);
|
||||
let startDate = moment(assiduité.date_debut);
|
||||
let endDate = moment(assiduité.date_fin);
|
||||
|
||||
if (startDate.isBefore(dayStart)) {
|
||||
startDate.startOf("day").add(mt_start, "hours");
|
||||
startDate = dayEnd.clone().startOf("day").add(mt_start, "hours");
|
||||
}
|
||||
|
||||
if (endDate.isAfter(dayEnd)) {
|
||||
endDate.startOf("day").add(mt_end, "hours");
|
||||
endDate = dayEnd.clone().startOf("day").add(mt_end, "hours");
|
||||
}
|
||||
|
||||
const block = document.createElement("div");
|
||||
|
@ -140,7 +140,7 @@
|
|||
|
||||
const idDiv = document.createElement("div");
|
||||
idDiv.className = "assiduite-id";
|
||||
idDiv.textContent = `ID: ${assiduite.assiduite_id}`;
|
||||
idDiv.textContent = `${getModuleImpl(assiduite)}`;
|
||||
bubble.appendChild(idDiv);
|
||||
|
||||
const periodDivDeb = document.createElement("div");
|
||||
|
|
|
@ -149,9 +149,10 @@
|
|||
}
|
||||
|
||||
.btnPrompt:disabled {
|
||||
opacity: 0.7;
|
||||
color: black;
|
||||
opacity: 0.8;
|
||||
background-color: whitesmoke;
|
||||
background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, rgba(81, 81, 81, 0.61) 5px, rgba(81, 81, 81, 0.61) 10px)
|
||||
background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, rgba(81, 81, 81, 0.337) 5px, rgba(81, 81, 81, 0.337) 10px)
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
|
@ -63,30 +63,7 @@
|
|||
try { stats() } catch (_) { }
|
||||
}
|
||||
|
||||
const moduleimpls = {}
|
||||
|
||||
function getModuleImpl(assiduite) {
|
||||
const id = assiduite.moduleimpl_id;
|
||||
|
||||
if (id == null || id == undefined) {
|
||||
if ("desc" in assiduite && assiduite.desc != null && assiduite.desc.indexOf('Module:Autre') != -1) {
|
||||
return "Autre"
|
||||
} else {
|
||||
return "Pas de module"
|
||||
}
|
||||
}
|
||||
|
||||
if (id in moduleimpls) {
|
||||
return moduleimpls[id];
|
||||
}
|
||||
const url_api = getUrl() + `/api/moduleimpl/${id}`;
|
||||
sync_get(url_api, (data) => {
|
||||
moduleimpls[id] = `${data.module.code} ${data.module.abbrev}`;
|
||||
}, (data) => { moduleimpls[id] = "Pas de module" });
|
||||
|
||||
return moduleimpls[id];
|
||||
|
||||
}
|
||||
|
||||
function renderTableAssiduites(page, assiduités) {
|
||||
|
||||
|
|
|
@ -234,11 +234,12 @@
|
|||
<style>
|
||||
.timeline-container {
|
||||
width: 75%;
|
||||
margin-left: 5%;
|
||||
margin-left: 25px;
|
||||
background-color: white;
|
||||
border-radius: 15px;
|
||||
position: relative;
|
||||
height: 40px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
/* ... */
|
||||
|
|
|
@ -4,6 +4,7 @@ from flask import g, request, render_template
|
|||
|
||||
from flask import abort, url_for
|
||||
|
||||
from app import db
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.decorators import (
|
||||
|
@ -465,7 +466,6 @@ def signal_assiduites_group():
|
|||
groups_infos = sco_groups_view.DisplayedGroupsInfos(
|
||||
group_ids, moduleimpl_id=moduleimpl_id, formsemestre_id=formsemestre_id
|
||||
)
|
||||
|
||||
if not groups_infos.members:
|
||||
return (
|
||||
html_sco_header.sco_header(page_title="Saisie journalière des Assiduités")
|
||||
|
@ -571,6 +571,153 @@ def signal_assiduites_group():
|
|||
dept_id=g.scodoc_dept_id,
|
||||
),
|
||||
defdem=_get_etuds_dem_def(formsemestre),
|
||||
readonly="false",
|
||||
),
|
||||
html_sco_header.sco_footer(),
|
||||
).build()
|
||||
|
||||
|
||||
@bp.route("/VisuAssiduiteGr")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def visu_assiduites_group():
|
||||
"""
|
||||
signal_assiduites_group Saisie des assiduités des groupes pour le jour donnée
|
||||
|
||||
Returns:
|
||||
str: l'html généré
|
||||
"""
|
||||
formsemestre_id: int = request.args.get("formsemestre_id", -1)
|
||||
moduleimpl_id: int = request.args.get("moduleimpl_id")
|
||||
date: str = request.args.get("jour", datetime.date.today().isoformat())
|
||||
group_ids: list[int] = request.args.get("group_ids", None)
|
||||
|
||||
if group_ids is None:
|
||||
group_ids = []
|
||||
else:
|
||||
group_ids = group_ids.split(",")
|
||||
map(str, group_ids)
|
||||
|
||||
# Vérification du moduleimpl_id
|
||||
try:
|
||||
moduleimpl_id = int(moduleimpl_id)
|
||||
except (TypeError, ValueError):
|
||||
moduleimpl_id = None
|
||||
# Vérification du formsemestre_id
|
||||
try:
|
||||
formsemestre_id = int(formsemestre_id)
|
||||
except (TypeError, ValueError):
|
||||
formsemestre_id = None
|
||||
|
||||
groups_infos = sco_groups_view.DisplayedGroupsInfos(
|
||||
group_ids, moduleimpl_id=moduleimpl_id, formsemestre_id=formsemestre_id
|
||||
)
|
||||
if not groups_infos.members:
|
||||
return (
|
||||
html_sco_header.sco_header(page_title="Saisie journalière des Assiduités")
|
||||
+ "<h3>Aucun étudiant ! </h3>"
|
||||
+ html_sco_header.sco_footer()
|
||||
)
|
||||
|
||||
# --- URL DEFAULT ---
|
||||
|
||||
base_url: str = f"SignalAssiduiteGr?date={date}&{groups_infos.groups_query_args}"
|
||||
|
||||
# --- Filtrage par formsemestre ---
|
||||
formsemestre_id = groups_infos.formsemestre_id
|
||||
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
if formsemestre.dept_id != g.scodoc_dept_id:
|
||||
abort(404, "groupes inexistants dans ce département")
|
||||
|
||||
require_module = sco_preferences.get_preference(
|
||||
"abs_require_module", formsemestre_id
|
||||
)
|
||||
|
||||
etuds = [
|
||||
sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
|
||||
for m in groups_infos.members
|
||||
]
|
||||
|
||||
# --- Vérification de la date ---
|
||||
|
||||
real_date = scu.is_iso_formated(date, True).date()
|
||||
|
||||
if real_date < formsemestre.date_debut:
|
||||
date = formsemestre.date_debut.isoformat()
|
||||
elif real_date > formsemestre.date_fin:
|
||||
date = formsemestre.date_fin.isoformat()
|
||||
|
||||
# --- Restriction en fonction du moduleimpl_id ---
|
||||
if moduleimpl_id:
|
||||
mod_inscrits = {
|
||||
x["etudid"]
|
||||
for x in sco_moduleimpl.do_moduleimpl_inscription_list(
|
||||
moduleimpl_id=moduleimpl_id
|
||||
)
|
||||
}
|
||||
etuds_inscrits_module = [e for e in etuds if e["etudid"] in mod_inscrits]
|
||||
if etuds_inscrits_module:
|
||||
etuds = etuds_inscrits_module
|
||||
else:
|
||||
# Si aucun etudiant n'est inscrit au module choisi...
|
||||
moduleimpl_id = None
|
||||
|
||||
# --- Génération de l'HTML ---
|
||||
sem = formsemestre.to_dict()
|
||||
|
||||
if groups_infos.tous_les_etuds_du_sem:
|
||||
gr_tit = "en"
|
||||
else:
|
||||
if len(groups_infos.group_ids) > 1:
|
||||
grp = "des groupes"
|
||||
else:
|
||||
grp = "du groupe"
|
||||
gr_tit = (
|
||||
grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>"
|
||||
)
|
||||
|
||||
header: str = html_sco_header.sco_header(
|
||||
page_title="Saisie journalière des assiduités",
|
||||
init_qtip=True,
|
||||
javascripts=html_sco_header.BOOTSTRAP_MULTISELECT_JS
|
||||
+ [
|
||||
# Voir fonctionnement JS
|
||||
"js/etud_info.js",
|
||||
"js/abs_ajax.js",
|
||||
"js/groups_view.js",
|
||||
"js/assiduites.js",
|
||||
"libjs/moment.new.min.js",
|
||||
"libjs/moment-timezone.js",
|
||||
],
|
||||
cssstyles=CSSSTYLES
|
||||
+ [
|
||||
"css/assiduites.css",
|
||||
],
|
||||
)
|
||||
|
||||
return HTMLBuilder(
|
||||
header,
|
||||
_mini_timeline(),
|
||||
render_template(
|
||||
"assiduites/pages/signal_assiduites_group.j2",
|
||||
gr_tit=gr_tit,
|
||||
sem=sem["titre_num"],
|
||||
date=date,
|
||||
formsemestre_id=formsemestre_id,
|
||||
grp=sco_groups_view.menu_groups_choice(groups_infos),
|
||||
moduleimpl_select=_module_selector(formsemestre, moduleimpl_id),
|
||||
timeline=_timeline(),
|
||||
nonworkdays=_non_work_days(),
|
||||
formsemestre_date_debut=str(formsemestre.date_debut),
|
||||
formsemestre_date_fin=str(formsemestre.date_fin),
|
||||
forcer_module=sco_preferences.get_preference(
|
||||
"forcer_module",
|
||||
formsemestre_id=formsemestre_id,
|
||||
dept_id=g.scodoc_dept_id,
|
||||
),
|
||||
defdem=_get_etuds_dem_def(formsemestre),
|
||||
readonly="true",
|
||||
),
|
||||
html_sco_header.sco_footer(),
|
||||
).build()
|
||||
|
@ -669,9 +816,11 @@ def visu_assi_group():
|
|||
map(str, group_ids)
|
||||
|
||||
groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)
|
||||
|
||||
formsemestre = db.session.get(FormSemestre, groups_infos.formsemestre_id)
|
||||
etuds = etuds_sorted_from_ids([m["etudid"] for m in groups_infos.members])
|
||||
table: TableAssi = TableAssi(etuds=etuds, dates=list(dates.values()))
|
||||
table: TableAssi = TableAssi(
|
||||
etuds=etuds, dates=list(dates.values()), formsemestre=formsemestre
|
||||
)
|
||||
|
||||
if fmt.startswith("xls"):
|
||||
return scu.send_file(
|
||||
|
@ -934,6 +1083,7 @@ def _get_etuds_dem_def(formsemestre):
|
|||
for etud in etuds_dem_def:
|
||||
json_str += template.replace("£", str(etud[0])).replace("$", etud[1])
|
||||
|
||||
json_str = json_str[:-1] + "}"
|
||||
if json_str != "{":
|
||||
json_str = json_str[:-1]
|
||||
|
||||
return json_str
|
||||
return json_str + "}"
|
||||
|
|
|
@ -35,7 +35,7 @@ ASSIDUITES_FIELDS = {
|
|||
}
|
||||
|
||||
CREATE_FIELD = {"assiduite_id": int}
|
||||
BATCH_FIELD = {"errors": dict, "success": dict}
|
||||
BATCH_FIELD = {"errors": list, "success": list}
|
||||
|
||||
COUNT_FIELDS = {"compte": int, "journee": int, "demi": int, "heure": float}
|
||||
|
||||
|
@ -262,14 +262,14 @@ def test_route_create(api_admin_headers):
|
|||
check_fields(res, BATCH_FIELD)
|
||||
assert len(res["success"]) == 1
|
||||
|
||||
TO_REMOVE.append(res["success"]["0"]["assiduite_id"])
|
||||
TO_REMOVE.append(res["success"][0]["message"]["assiduite_id"])
|
||||
|
||||
data2 = create_data("absent", "02", MODULE, "desc")
|
||||
res = POST_JSON(f"/assiduite/{ETUDID}/create", [data2], api_admin_headers)
|
||||
check_fields(res, BATCH_FIELD)
|
||||
assert len(res["success"]) == 1
|
||||
|
||||
TO_REMOVE.append(res["success"]["0"]["assiduite_id"])
|
||||
TO_REMOVE.append(res["success"][0]["message"]["assiduite_id"])
|
||||
|
||||
# Mauvais fonctionnement
|
||||
check_failure_post(f"/assiduite/{FAUX}/create", api_admin_headers, [data])
|
||||
|
@ -278,7 +278,7 @@ def test_route_create(api_admin_headers):
|
|||
check_fields(res, BATCH_FIELD)
|
||||
assert len(res["errors"]) == 1
|
||||
assert (
|
||||
res["errors"]["0"]
|
||||
res["errors"][0]["message"]
|
||||
== "Duplication des assiduités (la période rentrée rentre en conflit avec une assiduité enregistrée)"
|
||||
)
|
||||
|
||||
|
@ -289,7 +289,7 @@ def test_route_create(api_admin_headers):
|
|||
)
|
||||
check_fields(res, BATCH_FIELD)
|
||||
assert len(res["errors"]) == 1
|
||||
assert res["errors"]["0"] == "param 'moduleimpl_id': invalide"
|
||||
assert res["errors"][0]["message"] == "param 'moduleimpl_id': invalide"
|
||||
|
||||
# -== Multiple ==-
|
||||
|
||||
|
@ -304,8 +304,8 @@ def test_route_create(api_admin_headers):
|
|||
res = POST_JSON(f"/assiduite/{ETUDID}/create", data, api_admin_headers)
|
||||
check_fields(res, BATCH_FIELD)
|
||||
for dat in res["success"]:
|
||||
check_fields(res["success"][dat], CREATE_FIELD)
|
||||
TO_REMOVE.append(res["success"][dat]["assiduite_id"])
|
||||
check_fields(dat["message"], CREATE_FIELD)
|
||||
TO_REMOVE.append(dat["message"]["assiduite_id"])
|
||||
|
||||
# Mauvais Fonctionnement
|
||||
|
||||
|
@ -321,13 +321,13 @@ def test_route_create(api_admin_headers):
|
|||
assert len(res["errors"]) == 4
|
||||
|
||||
assert (
|
||||
res["errors"]["0"]
|
||||
res["errors"][0]["message"]
|
||||
== "Duplication des assiduités (la période rentrée rentre en conflit avec une assiduité enregistrée)"
|
||||
)
|
||||
assert res["errors"]["1"] == "param 'moduleimpl_id': invalide"
|
||||
assert res["errors"]["2"] == "param 'etat': invalide"
|
||||
assert res["errors"][1]["message"] == "param 'moduleimpl_id': invalide"
|
||||
assert res["errors"][2]["message"] == "param 'etat': invalide"
|
||||
assert (
|
||||
res["errors"]["3"]
|
||||
res["errors"][3]["message"]
|
||||
== "param 'date_debut': format invalide, param 'date_fin': format invalide"
|
||||
)
|
||||
|
||||
|
@ -367,7 +367,7 @@ def test_route_delete(api_admin_headers):
|
|||
res = POST_JSON("/assiduite/delete", [data], api_admin_headers)
|
||||
check_fields(res, BATCH_FIELD)
|
||||
for dat in res["success"]:
|
||||
assert res["success"][dat] == {"OK": True}
|
||||
assert dat["message"] == "OK"
|
||||
|
||||
# Mauvais fonctionnement
|
||||
res = POST_JSON("/assiduite/delete", [data], api_admin_headers)
|
||||
|
@ -383,7 +383,7 @@ def test_route_delete(api_admin_headers):
|
|||
res = POST_JSON("/assiduite/delete", data, api_admin_headers)
|
||||
check_fields(res, BATCH_FIELD)
|
||||
for dat in res["success"]:
|
||||
assert res["success"][dat] == {"OK": True}
|
||||
assert dat["message"] == "OK"
|
||||
|
||||
# Mauvais Fonctionnement
|
||||
|
||||
|
@ -397,4 +397,4 @@ def test_route_delete(api_admin_headers):
|
|||
check_fields(res, BATCH_FIELD)
|
||||
assert len(res["errors"]) == 3
|
||||
|
||||
assert all([res["errors"][i] == "Assiduite non existante" for i in res["errors"]])
|
||||
assert all(i["message"] == "Assiduite non existante" for i in res["errors"])
|
||||
|
|
|
@ -35,7 +35,7 @@ JUSTIFICATIFS_FIELDS = {
|
|||
}
|
||||
|
||||
CREATE_FIELD = {"justif_id": int, "couverture": list}
|
||||
BATCH_FIELD = {"errors": dict, "success": dict}
|
||||
BATCH_FIELD = {"errors": list, "success": list}
|
||||
|
||||
TO_REMOVE = []
|
||||
|
||||
|
@ -172,14 +172,14 @@ def test_route_create(api_admin_headers):
|
|||
check_fields(res, BATCH_FIELD)
|
||||
assert len(res["success"]) == 1
|
||||
|
||||
TO_REMOVE.append(res["success"]["0"]["justif_id"])
|
||||
TO_REMOVE.append(res["success"][0]["message"]["justif_id"])
|
||||
|
||||
data2 = create_data("modifie", "02", "raison")
|
||||
res = POST_JSON(f"/justificatif/{ETUDID}/create", [data2], api_admin_headers)
|
||||
check_fields(res, BATCH_FIELD)
|
||||
assert len(res["success"]) == 1
|
||||
|
||||
TO_REMOVE.append(res["success"]["0"]["justif_id"])
|
||||
TO_REMOVE.append(res["success"][0]["message"]["justif_id"])
|
||||
|
||||
# Mauvais fonctionnement
|
||||
check_failure_post(f"/justificatif/{FAUX}/create", api_admin_headers, [data])
|
||||
|
@ -191,7 +191,7 @@ def test_route_create(api_admin_headers):
|
|||
)
|
||||
check_fields(res, BATCH_FIELD)
|
||||
assert len(res["errors"]) == 1
|
||||
assert res["errors"]["0"] == "param 'etat': invalide"
|
||||
assert res["errors"][0]["message"] == "param 'etat': invalide"
|
||||
|
||||
# -== Multiple ==-
|
||||
|
||||
|
@ -206,8 +206,8 @@ def test_route_create(api_admin_headers):
|
|||
res = POST_JSON(f"/justificatif/{ETUDID}/create", data, api_admin_headers)
|
||||
check_fields(res, BATCH_FIELD)
|
||||
for dat in res["success"]:
|
||||
check_fields(res["success"][dat], CREATE_FIELD)
|
||||
TO_REMOVE.append(res["success"][dat]["justif_id"])
|
||||
check_fields(dat["message"], CREATE_FIELD)
|
||||
TO_REMOVE.append(dat["message"]["justif_id"])
|
||||
|
||||
# Mauvais Fonctionnement
|
||||
|
||||
|
@ -221,10 +221,10 @@ def test_route_create(api_admin_headers):
|
|||
check_fields(res, BATCH_FIELD)
|
||||
assert len(res["errors"]) == 3
|
||||
|
||||
assert res["errors"]["0"] == "param 'etat': manquant"
|
||||
assert res["errors"]["1"] == "param 'etat': invalide"
|
||||
assert res["errors"][0]["message"] == "param 'etat': manquant"
|
||||
assert res["errors"][1]["message"] == "param 'etat': invalide"
|
||||
assert (
|
||||
res["errors"]["2"]
|
||||
res["errors"][2]["message"]
|
||||
== "param 'date_debut': format invalide, param 'date_fin': format invalide"
|
||||
)
|
||||
|
||||
|
@ -263,7 +263,7 @@ def test_route_delete(api_admin_headers):
|
|||
res = POST_JSON("/justificatif/delete", [data], api_admin_headers)
|
||||
check_fields(res, BATCH_FIELD)
|
||||
for dat in res["success"]:
|
||||
assert res["success"][dat] == {"OK": True}
|
||||
assert dat["message"] == "OK"
|
||||
|
||||
# Mauvais fonctionnement
|
||||
res = POST_JSON("/justificatif/delete", [data], api_admin_headers)
|
||||
|
@ -279,7 +279,7 @@ def test_route_delete(api_admin_headers):
|
|||
res = POST_JSON("/justificatif/delete", data, api_admin_headers)
|
||||
check_fields(res, BATCH_FIELD)
|
||||
for dat in res["success"]:
|
||||
assert res["success"][dat] == {"OK": True}
|
||||
assert dat["message"] == "OK"
|
||||
|
||||
# Mauvais Fonctionnement
|
||||
|
||||
|
@ -293,7 +293,7 @@ def test_route_delete(api_admin_headers):
|
|||
check_fields(res, BATCH_FIELD)
|
||||
assert len(res["errors"]) == 3
|
||||
|
||||
assert all([res["errors"][i] == "Justificatif non existant" for i in res["errors"]])
|
||||
assert all(i["message"] == "Justificatif non existant" for i in res["errors"])
|
||||
|
||||
|
||||
# Gestion de l'archivage
|
||||
|
|
|
@ -121,15 +121,6 @@ class _Merger:
|
|||
"entry_date": self.entry_date,
|
||||
},
|
||||
)
|
||||
# retour = Justificatif.fast_create_justificatif(
|
||||
# etudid=self.etudid,
|
||||
# date_debut=date_deb,
|
||||
# date_fin=date_fin,
|
||||
# etat=EtatJustificatif.VALIDE,
|
||||
# raison=self.raison,
|
||||
# entry_date=self.entry_date,
|
||||
# )
|
||||
# return retour
|
||||
|
||||
def _to_assi(self):
|
||||
date_deb = _Merger._tuple_to_date(self.deb)
|
||||
|
@ -161,17 +152,6 @@ class _Merger:
|
|||
},
|
||||
)
|
||||
|
||||
# retour = Assiduite.fast_create_assiduite(
|
||||
# etudid=self.etudid,
|
||||
# date_debut=date_deb,
|
||||
# date_fin=date_fin,
|
||||
# etat=EtatAssiduite.ABSENT,
|
||||
# moduleimpl_id=self.moduleimpl,
|
||||
# description=self.raison,
|
||||
# entry_date=self.entry_date,
|
||||
# )
|
||||
# return retour
|
||||
|
||||
def export(self):
|
||||
"""Génère un nouvel objet Assiduité ou Justificatif"""
|
||||
obj: Assiduite or Justificatif = None
|
||||
|
|
Loading…
Reference in New Issue