Module assiduités
4
app/__init__.py
Normal file → Executable file
@ -322,6 +322,7 @@ def create_app(config_class=DevConfig):
|
|||||||
from app.views import notes_bp
|
from app.views import notes_bp
|
||||||
from app.views import users_bp
|
from app.views import users_bp
|
||||||
from app.views import absences_bp
|
from app.views import absences_bp
|
||||||
|
from app.views import assiduites_bp
|
||||||
from app.api import api_bp
|
from app.api import api_bp
|
||||||
from app.api import api_web_bp
|
from app.api import api_web_bp
|
||||||
|
|
||||||
@ -340,6 +341,9 @@ def create_app(config_class=DevConfig):
|
|||||||
app.register_blueprint(
|
app.register_blueprint(
|
||||||
absences_bp, url_prefix="/ScoDoc/<scodoc_dept>/Scolarite/Absences"
|
absences_bp, url_prefix="/ScoDoc/<scodoc_dept>/Scolarite/Absences"
|
||||||
)
|
)
|
||||||
|
app.register_blueprint(
|
||||||
|
assiduites_bp, url_prefix="/ScoDoc/<scodoc_dept>/Scolarite/Assiduites"
|
||||||
|
)
|
||||||
app.register_blueprint(api_bp, url_prefix="/ScoDoc/api")
|
app.register_blueprint(api_bp, url_prefix="/ScoDoc/api")
|
||||||
app.register_blueprint(api_web_bp, url_prefix="/ScoDoc/<scodoc_dept>/api")
|
app.register_blueprint(api_web_bp, url_prefix="/ScoDoc/<scodoc_dept>/api")
|
||||||
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
"""api.__init__
|
"""api.__init__
|
||||||
"""
|
"""
|
||||||
|
from flask_json import as_json
|
||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
from flask import request
|
from flask import request, g
|
||||||
|
from app import db
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
from app.scodoc.sco_exceptions import ScoException
|
from app.scodoc.sco_exceptions import ScoException
|
||||||
|
|
||||||
@ -34,9 +35,27 @@ def requested_format(default_format="json", allowed_formats=None):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@as_json
|
||||||
|
def get_model_api_object(model_cls: db.Model, model_id: int, join_cls: db.Model = None):
|
||||||
|
"""
|
||||||
|
Retourne une réponse contenant la représentation api de l'objet "Model[model_id]"
|
||||||
|
|
||||||
|
Filtrage du département en fonction d'une classe de jointure (eg: Identite, Formsemstre) -> join_cls
|
||||||
|
|
||||||
|
exemple d'utilisation : fonction "justificatif()" -> app/api/justificatifs.py
|
||||||
|
"""
|
||||||
|
query = model_cls.query.filter_by(id=model_id)
|
||||||
|
if g.scodoc_dept and join_cls is not None:
|
||||||
|
query = query.join(join_cls).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
unique: model_cls = query.first_or_404()
|
||||||
|
|
||||||
|
return unique.to_dict(format_api=True)
|
||||||
|
|
||||||
|
|
||||||
from app.api import tokens
|
from app.api import tokens
|
||||||
from app.api import (
|
from app.api import (
|
||||||
absences,
|
absences,
|
||||||
|
assiduites,
|
||||||
billets_absences,
|
billets_absences,
|
||||||
departements,
|
departements,
|
||||||
etudiants,
|
etudiants,
|
||||||
@ -44,6 +63,7 @@ from app.api import (
|
|||||||
formations,
|
formations,
|
||||||
formsemestres,
|
formsemestres,
|
||||||
jury,
|
jury,
|
||||||
|
justificatifs,
|
||||||
logos,
|
logos,
|
||||||
partitions,
|
partitions,
|
||||||
semset,
|
semset,
|
||||||
|
878
app/api/assiduites.py
Normal file
@ -0,0 +1,878 @@
|
|||||||
|
##############################################################################
|
||||||
|
# ScoDoc
|
||||||
|
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||||
|
# See LICENSE
|
||||||
|
##############################################################################
|
||||||
|
"""ScoDoc 9 API : Assiduités
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
from flask_json import as_json
|
||||||
|
from flask import g, request
|
||||||
|
from flask_login import login_required, current_user
|
||||||
|
|
||||||
|
import app.scodoc.sco_assiduites as scass
|
||||||
|
import app.scodoc.sco_utils as scu
|
||||||
|
from app import db
|
||||||
|
from app.api import api_bp as bp
|
||||||
|
from app.api import api_web_bp
|
||||||
|
from app.api import get_model_api_object
|
||||||
|
from app.decorators import permission_required, scodoc
|
||||||
|
from app.models import Assiduite, FormSemestre, Identite, ModuleImpl
|
||||||
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
|
from app.scodoc.sco_permissions import Permission
|
||||||
|
from app.scodoc.sco_utils import json_error
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/assiduite/<int:assiduite_id>")
|
||||||
|
@api_web_bp.route("/assiduite/<int:assiduite_id>")
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def assiduite(assiduite_id: int = None):
|
||||||
|
"""Retourne un objet assiduité à partir de son id
|
||||||
|
|
||||||
|
Exemple de résultat:
|
||||||
|
{
|
||||||
|
"assiduite_id": 1,
|
||||||
|
"etudid": 2,
|
||||||
|
"moduleimpl_id": 3,
|
||||||
|
"date_debut": "2022-10-31T08:00+01:00",
|
||||||
|
"date_fin": "2022-10-31T10:00+01:00",
|
||||||
|
"etat": "retard",
|
||||||
|
"desc": "une description",
|
||||||
|
"user_id: 1 or null,
|
||||||
|
"est_just": False or True,
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
return get_model_api_object(Assiduite, assiduite_id, Identite)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/assiduites/<int:etudid>/count", defaults={"with_query": False})
|
||||||
|
@bp.route("/assiduites/<int:etudid>/count/query", defaults={"with_query": True})
|
||||||
|
@api_web_bp.route("/assiduites/<int:etudid>/count", defaults={"with_query": False})
|
||||||
|
@api_web_bp.route("/assiduites/<int:etudid>/count/query", defaults={"with_query": True})
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@as_json
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def count_assiduites(etudid: int = None, with_query: bool = False):
|
||||||
|
"""
|
||||||
|
Retourne le nombre d'assiduités d'un étudiant
|
||||||
|
chemin : /assiduites/<int:etudid>/count
|
||||||
|
|
||||||
|
Un filtrage peut être donné avec une query
|
||||||
|
chemin : /assiduites/<int:etudid>/count/query?
|
||||||
|
|
||||||
|
Les différents filtres :
|
||||||
|
Type (type de comptage -> journee, demi, heure, nombre d'assiduite):
|
||||||
|
query?type=(journee, demi, heure) -> une seule valeur parmis les trois
|
||||||
|
ex: .../query?type=heure
|
||||||
|
Comportement par défaut : compte le nombre d'assiduité enregistrée
|
||||||
|
|
||||||
|
Etat (etat de l'étudiant -> absent, present ou retard):
|
||||||
|
query?etat=[- liste des états séparé par une virgule -]
|
||||||
|
ex: .../query?etat=present,retard
|
||||||
|
Date debut
|
||||||
|
(date de début de l'assiduité, sont affichés les assiduités
|
||||||
|
dont la date de début est supérieur ou égale à la valeur donnée):
|
||||||
|
query?date_debut=[- date au format iso -]
|
||||||
|
ex: query?date_debut=2022-11-03T08:00+01:00
|
||||||
|
Date fin
|
||||||
|
(date de fin de l'assiduité, sont affichés les assiduités
|
||||||
|
dont la date de fin est inférieure ou égale à la valeur donnée):
|
||||||
|
query?date_fin=[- date au format iso -]
|
||||||
|
ex: query?date_fin=2022-11-03T10:00+01:00
|
||||||
|
Moduleimpl_id (l'id du module concerné par l'assiduité):
|
||||||
|
query?moduleimpl_id=[- int ou vide -]
|
||||||
|
ex: query?moduleimpl_id=1234
|
||||||
|
query?moduleimpl_od=
|
||||||
|
Formsemstre_id (l'id du formsemestre concerné par l'assiduité)
|
||||||
|
query?formsemestre_id=[int]
|
||||||
|
ex query?formsemestre_id=3
|
||||||
|
user_id (l'id de l'auteur de l'assiduité)
|
||||||
|
query?user_id=[int]
|
||||||
|
ex query?user_id=3
|
||||||
|
est_just (si l'assiduité est justifié (fait aussi filtre par abs/retard))
|
||||||
|
query?est_just=[bool]
|
||||||
|
query?est_just=f
|
||||||
|
query?est_just=t
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
query = Identite.query.filter_by(id=etudid)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
|
||||||
|
etud: Identite = query.first_or_404(etudid)
|
||||||
|
filtered: dict[str, object] = {}
|
||||||
|
metric: str = "all"
|
||||||
|
|
||||||
|
if with_query:
|
||||||
|
metric, filtered = _count_manager(request)
|
||||||
|
|
||||||
|
return scass.get_assiduites_stats(
|
||||||
|
assiduites=etud.assiduites, metric=metric, filtered=filtered
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/assiduites/<int:etudid>", defaults={"with_query": False})
|
||||||
|
@bp.route("/assiduites/<int:etudid>/query", defaults={"with_query": True})
|
||||||
|
@api_web_bp.route("/assiduites/<int:etudid>", defaults={"with_query": False})
|
||||||
|
@api_web_bp.route("/assiduites/<int:etudid>/query", defaults={"with_query": True})
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@as_json
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def assiduites(etudid: int = None, with_query: bool = False):
|
||||||
|
"""
|
||||||
|
Retourne toutes les assiduités d'un étudiant
|
||||||
|
chemin : /assiduites/<int:etudid>
|
||||||
|
|
||||||
|
Un filtrage peut être donné avec une query
|
||||||
|
chemin : /assiduites/<int:etudid>/query?
|
||||||
|
|
||||||
|
Les différents filtres :
|
||||||
|
Etat (etat de l'étudiant -> absent, present ou retard):
|
||||||
|
query?etat=[- liste des états séparé par une virgule -]
|
||||||
|
ex: .../query?etat=present,retard
|
||||||
|
Date debut
|
||||||
|
(date de début de l'assiduité, sont affichés les assiduités
|
||||||
|
dont la date de début est supérieur ou égale à la valeur donnée):
|
||||||
|
query?date_debut=[- date au format iso -]
|
||||||
|
ex: query?date_debut=2022-11-03T08:00+01:00
|
||||||
|
Date fin
|
||||||
|
(date de fin de l'assiduité, sont affichés les assiduités
|
||||||
|
dont la date de fin est inférieure ou égale à la valeur donnée):
|
||||||
|
query?date_fin=[- date au format iso -]
|
||||||
|
ex: query?date_fin=2022-11-03T10:00+01:00
|
||||||
|
Moduleimpl_id (l'id du module concerné par l'assiduité):
|
||||||
|
query?moduleimpl_id=[- int ou vide -]
|
||||||
|
ex: query?moduleimpl_id=1234
|
||||||
|
query?moduleimpl_od=
|
||||||
|
Formsemstre_id (l'id du formsemestre concerné par l'assiduité)
|
||||||
|
query?formsemstre_id=[int]
|
||||||
|
ex query?formsemestre_id=3
|
||||||
|
user_id (l'id de l'auteur de l'assiduité)
|
||||||
|
query?user_id=[int]
|
||||||
|
ex query?user_id=3
|
||||||
|
est_just (si l'assiduité est justifié (fait aussi filtre par abs/retard))
|
||||||
|
query?est_just=[bool]
|
||||||
|
query?est_just=f
|
||||||
|
query?est_just=t
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
query = Identite.query.filter_by(id=etudid)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
|
||||||
|
etud: Identite = query.first_or_404(etudid)
|
||||||
|
assiduites_query = etud.assiduites
|
||||||
|
|
||||||
|
if with_query:
|
||||||
|
assiduites_query = _filter_manager(request, assiduites_query)
|
||||||
|
|
||||||
|
data_set: list[dict] = []
|
||||||
|
for ass in assiduites_query.all():
|
||||||
|
data = ass.to_dict(format_api=True)
|
||||||
|
data_set.append(data)
|
||||||
|
|
||||||
|
return data_set
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/assiduites/group/query", defaults={"with_query": True})
|
||||||
|
@api_web_bp.route("/assiduites/group/query", defaults={"with_query": True})
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@as_json
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def assiduites_group(with_query: bool = False):
|
||||||
|
"""
|
||||||
|
Retourne toutes les assiduités d'un groupe d'étudiants
|
||||||
|
chemin : /assiduites/group/query?etudids=1,2,3
|
||||||
|
|
||||||
|
Un filtrage peut être donné avec une query
|
||||||
|
chemin : /assiduites/group/query?etudids=1,2,3
|
||||||
|
|
||||||
|
Les différents filtres :
|
||||||
|
Etat (etat de l'étudiant -> absent, present ou retard):
|
||||||
|
query?etat=[- liste des états séparé par une virgule -]
|
||||||
|
ex: .../query?etat=present,retard
|
||||||
|
Date debut
|
||||||
|
(date de début de l'assiduité, sont affichés les assiduités
|
||||||
|
dont la date de début est supérieur ou égale à la valeur donnée):
|
||||||
|
query?date_debut=[- date au format iso -]
|
||||||
|
ex: query?date_debut=2022-11-03T08:00+01:00
|
||||||
|
Date fin
|
||||||
|
(date de fin de l'assiduité, sont affichés les assiduités
|
||||||
|
dont la date de fin est inférieure ou égale à la valeur donnée):
|
||||||
|
query?date_fin=[- date au format iso -]
|
||||||
|
ex: query?date_fin=2022-11-03T10:00+01:00
|
||||||
|
Moduleimpl_id (l'id du module concerné par l'assiduité):
|
||||||
|
query?moduleimpl_id=[- int ou vide -]
|
||||||
|
ex: query?moduleimpl_id=1234
|
||||||
|
query?moduleimpl_od=
|
||||||
|
Formsemstre_id (l'id du formsemestre concerné par l'assiduité)
|
||||||
|
query?formsemstre_id=[int]
|
||||||
|
ex query?formsemestre_id=3
|
||||||
|
user_id (l'id de l'auteur de l'assiduité)
|
||||||
|
query?user_id=[int]
|
||||||
|
ex query?user_id=3
|
||||||
|
est_just (si l'assiduité est justifié (fait aussi filtre par abs/retard))
|
||||||
|
query?est_just=[bool]
|
||||||
|
query?est_just=f
|
||||||
|
query?est_just=t
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
etuds = request.args.get("etudids", "")
|
||||||
|
etuds = etuds.split(",")
|
||||||
|
try:
|
||||||
|
etuds = [int(etu) for etu in etuds]
|
||||||
|
except ValueError:
|
||||||
|
return json_error(404, "Le champs etudids n'est pas correctement formé")
|
||||||
|
|
||||||
|
query = Identite.query.filter(Identite.id.in_(etuds))
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
|
||||||
|
if len(etuds) != query.count() or len(etuds) == 0:
|
||||||
|
return json_error(
|
||||||
|
404,
|
||||||
|
"Tous les étudiants ne sont pas dans le même département et/ou n'existe pas.",
|
||||||
|
)
|
||||||
|
assiduites_query = Assiduite.query.filter(Assiduite.etudid.in_(etuds))
|
||||||
|
|
||||||
|
if with_query:
|
||||||
|
assiduites_query = _filter_manager(request, assiduites_query)
|
||||||
|
|
||||||
|
data_set: dict[list[dict]] = {str(key): [] for key in etuds}
|
||||||
|
for ass in assiduites_query.all():
|
||||||
|
data = ass.to_dict(format_api=True)
|
||||||
|
data_set.get(str(data["etudid"])).append(data)
|
||||||
|
return data_set
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route(
|
||||||
|
"/assiduites/formsemestre/<int:formsemestre_id>", defaults={"with_query": False}
|
||||||
|
)
|
||||||
|
@api_web_bp.route(
|
||||||
|
"/assiduites/formsemestre/<int:formsemestre_id>", defaults={"with_query": False}
|
||||||
|
)
|
||||||
|
@bp.route(
|
||||||
|
"/assiduites/formsemestre/<int:formsemestre_id>/query",
|
||||||
|
defaults={"with_query": True},
|
||||||
|
)
|
||||||
|
@api_web_bp.route(
|
||||||
|
"/assiduites/formsemestre/<int:formsemestre_id>/query",
|
||||||
|
defaults={"with_query": True},
|
||||||
|
)
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@as_json
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
|
||||||
|
"""Retourne toutes les assiduités du formsemestre"""
|
||||||
|
formsemestre: FormSemestre = None
|
||||||
|
formsemestre_id = int(formsemestre_id)
|
||||||
|
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
|
||||||
|
|
||||||
|
if formsemestre is None:
|
||||||
|
return json_error(404, "le paramètre 'formsemestre_id' n'existe pas")
|
||||||
|
|
||||||
|
assiduites_query = scass.filter_by_formsemestre(Assiduite.query, formsemestre)
|
||||||
|
|
||||||
|
if with_query:
|
||||||
|
assiduites_query = _filter_manager(request, assiduites_query)
|
||||||
|
|
||||||
|
data_set: list[dict] = []
|
||||||
|
for ass in assiduites_query.all():
|
||||||
|
data = ass.to_dict(format_api=True)
|
||||||
|
data_set.append(data)
|
||||||
|
|
||||||
|
return data_set
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route(
|
||||||
|
"/assiduites/formsemestre/<int:formsemestre_id>/count",
|
||||||
|
defaults={"with_query": False},
|
||||||
|
)
|
||||||
|
@api_web_bp.route(
|
||||||
|
"/assiduites/formsemestre/<int:formsemestre_id>/count",
|
||||||
|
defaults={"with_query": False},
|
||||||
|
)
|
||||||
|
@bp.route(
|
||||||
|
"/assiduites/formsemestre/<int:formsemestre_id>/count/query",
|
||||||
|
defaults={"with_query": True},
|
||||||
|
)
|
||||||
|
@api_web_bp.route(
|
||||||
|
"/assiduites/formsemestre/<int:formsemestre_id>/count/query",
|
||||||
|
defaults={"with_query": True},
|
||||||
|
)
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@as_json
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def count_assiduites_formsemestre(
|
||||||
|
formsemestre_id: int = None, with_query: bool = False
|
||||||
|
):
|
||||||
|
"""Comptage des assiduités du formsemestre"""
|
||||||
|
formsemestre: FormSemestre = None
|
||||||
|
formsemestre_id = int(formsemestre_id)
|
||||||
|
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
|
||||||
|
|
||||||
|
if formsemestre is None:
|
||||||
|
return json_error(404, "le paramètre 'formsemestre_id' n'existe pas")
|
||||||
|
|
||||||
|
etuds = formsemestre.etuds.all()
|
||||||
|
etuds_id = [etud.id for etud in etuds]
|
||||||
|
|
||||||
|
assiduites_query = Assiduite.query.filter(Assiduite.etudid.in_(etuds_id))
|
||||||
|
assiduites_query = scass.filter_by_formsemestre(assiduites_query, formsemestre)
|
||||||
|
metric: str = "all"
|
||||||
|
filtered: dict = {}
|
||||||
|
if with_query:
|
||||||
|
metric, filtered = _count_manager(request)
|
||||||
|
|
||||||
|
return scass.get_assiduites_stats(assiduites_query, metric, filtered)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/assiduite/<int:etudid>/create", methods=["POST"])
|
||||||
|
@api_web_bp.route("/assiduite/<int:etudid>/create", methods=["POST"])
|
||||||
|
@scodoc
|
||||||
|
@as_json
|
||||||
|
@login_required
|
||||||
|
@permission_required(Permission.ScoAssiduiteChange)
|
||||||
|
def assiduite_create(etudid: int = None):
|
||||||
|
"""
|
||||||
|
Création d'une assiduité pour l'étudiant (etudid)
|
||||||
|
La requête doit avoir un content type "application/json":
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"date_debut": str,
|
||||||
|
"date_fin": str,
|
||||||
|
"etat": str,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date_debut": str,
|
||||||
|
"date_fin": str,
|
||||||
|
"etat": str,
|
||||||
|
"moduleimpl_id": int,
|
||||||
|
"desc":str,
|
||||||
|
}
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
"""
|
||||||
|
etud: Identite = Identite.query.filter_by(id=etudid).first_or_404()
|
||||||
|
|
||||||
|
create_list: list[object] = request.get_json(force=True)
|
||||||
|
|
||||||
|
if not isinstance(create_list, list):
|
||||||
|
return json_error(404, "Le contenu envoyé n'est pas une liste")
|
||||||
|
|
||||||
|
errors: dict[int, str] = {}
|
||||||
|
success: dict[int, object] = {}
|
||||||
|
for i, data in enumerate(create_list):
|
||||||
|
code, obj = _create_singular(data, etud)
|
||||||
|
if code == 404:
|
||||||
|
errors[i] = obj
|
||||||
|
else:
|
||||||
|
success[i] = obj
|
||||||
|
scass.simple_invalidate_cache(data, etud.id)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return {"errors": errors, "success": success}
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/assiduites/create", methods=["POST"])
|
||||||
|
@api_web_bp.route("/assiduites/create", methods=["POST"])
|
||||||
|
@scodoc
|
||||||
|
@as_json
|
||||||
|
@login_required
|
||||||
|
@permission_required(Permission.ScoAssiduiteChange)
|
||||||
|
def assiduites_create():
|
||||||
|
"""
|
||||||
|
Création d'une assiduité ou plusieurs assiduites
|
||||||
|
La requête doit avoir un content type "application/json":
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"date_debut": str,
|
||||||
|
"date_fin": str,
|
||||||
|
"etat": str,
|
||||||
|
"etudid":int,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date_debut": str,
|
||||||
|
"date_fin": str,
|
||||||
|
"etat": str,
|
||||||
|
"etudid":int,
|
||||||
|
|
||||||
|
"moduleimpl_id": int,
|
||||||
|
"desc":str,
|
||||||
|
}
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
create_list: list[object] = request.get_json(force=True)
|
||||||
|
|
||||||
|
if not isinstance(create_list, list):
|
||||||
|
return json_error(404, "Le contenu envoyé n'est pas une liste")
|
||||||
|
|
||||||
|
errors: dict[int, str] = {}
|
||||||
|
success: dict[int, object] = {}
|
||||||
|
for i, data in enumerate(create_list):
|
||||||
|
etud: Identite = Identite.query.filter_by(id=data["etudid"]).first()
|
||||||
|
if etud is None:
|
||||||
|
errors[i] = "Cet étudiant n'existe pas."
|
||||||
|
continue
|
||||||
|
|
||||||
|
code, obj = _create_singular(data, etud)
|
||||||
|
if code == 404:
|
||||||
|
errors[i] = obj
|
||||||
|
else:
|
||||||
|
success[i] = obj
|
||||||
|
scass.simple_invalidate_cache(data)
|
||||||
|
|
||||||
|
return {"errors": errors, "success": success}
|
||||||
|
|
||||||
|
|
||||||
|
def _create_singular(
|
||||||
|
data: dict,
|
||||||
|
etud: Identite,
|
||||||
|
) -> tuple[int, object]:
|
||||||
|
errors: list[str] = []
|
||||||
|
|
||||||
|
# -- vérifications de l'objet json --
|
||||||
|
# cas 1 : ETAT
|
||||||
|
etat = data.get("etat", None)
|
||||||
|
if etat is None:
|
||||||
|
errors.append("param 'etat': manquant")
|
||||||
|
elif not scu.EtatAssiduite.contains(etat):
|
||||||
|
errors.append("param 'etat': invalide")
|
||||||
|
|
||||||
|
etat = scu.EtatAssiduite.get(etat)
|
||||||
|
|
||||||
|
# cas 2 : date_debut
|
||||||
|
date_debut = data.get("date_debut", None)
|
||||||
|
if date_debut is None:
|
||||||
|
errors.append("param 'date_debut': manquant")
|
||||||
|
deb = scu.is_iso_formated(date_debut, convert=True)
|
||||||
|
if deb is None:
|
||||||
|
errors.append("param 'date_debut': format invalide")
|
||||||
|
|
||||||
|
# cas 3 : date_fin
|
||||||
|
date_fin = data.get("date_fin", None)
|
||||||
|
if date_fin is None:
|
||||||
|
errors.append("param 'date_fin': manquant")
|
||||||
|
fin = scu.is_iso_formated(date_fin, convert=True)
|
||||||
|
if fin is None:
|
||||||
|
errors.append("param 'date_fin': format invalide")
|
||||||
|
|
||||||
|
# cas 4 : moduleimpl_id
|
||||||
|
|
||||||
|
moduleimpl_id = data.get("moduleimpl_id", False)
|
||||||
|
moduleimpl: ModuleImpl = None
|
||||||
|
|
||||||
|
if moduleimpl_id not in [False, None]:
|
||||||
|
moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first()
|
||||||
|
if moduleimpl is None:
|
||||||
|
errors.append("param 'moduleimpl_id': invalide")
|
||||||
|
|
||||||
|
# cas 5 : desc
|
||||||
|
|
||||||
|
desc: str = data.get("desc", None)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
err: str = ", ".join(errors)
|
||||||
|
return (404, err)
|
||||||
|
|
||||||
|
# TOUT EST OK
|
||||||
|
try:
|
||||||
|
nouv_assiduite: Assiduite = Assiduite.create_assiduite(
|
||||||
|
date_debut=deb,
|
||||||
|
date_fin=fin,
|
||||||
|
etat=etat,
|
||||||
|
etud=etud,
|
||||||
|
moduleimpl=moduleimpl,
|
||||||
|
description=desc,
|
||||||
|
user_id=current_user.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(nouv_assiduite)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return (200, {"assiduite_id": nouv_assiduite.id})
|
||||||
|
except ScoValueError as excp:
|
||||||
|
return (
|
||||||
|
404,
|
||||||
|
excp.args[0],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/assiduite/delete", methods=["POST"])
|
||||||
|
@api_web_bp.route("/assiduite/delete", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@as_json
|
||||||
|
@permission_required(Permission.ScoAssiduiteChange)
|
||||||
|
def assiduite_delete():
|
||||||
|
"""
|
||||||
|
Suppression d'une assiduité à partir de son id
|
||||||
|
|
||||||
|
Forme des données envoyées :
|
||||||
|
|
||||||
|
[
|
||||||
|
<assiduite_id:int>,
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
assiduites_list: list[int] = request.get_json(force=True)
|
||||||
|
if not isinstance(assiduites_list, list):
|
||||||
|
return json_error(404, "Le contenu envoyé n'est pas une liste")
|
||||||
|
|
||||||
|
output = {"errors": {}, "success": {}}
|
||||||
|
|
||||||
|
for i, ass in enumerate(assiduites_list):
|
||||||
|
code, msg = _delete_singular(ass, db)
|
||||||
|
if code == 404:
|
||||||
|
output["errors"][f"{i}"] = msg
|
||||||
|
else:
|
||||||
|
output["success"][f"{i}"] = {"OK": True}
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_singular(assiduite_id: int, database):
|
||||||
|
assiduite_unique: Assiduite = Assiduite.query.filter_by(id=assiduite_id).first()
|
||||||
|
if assiduite_unique is None:
|
||||||
|
return (404, "Assiduite non existante")
|
||||||
|
scass.simple_invalidate_cache(assiduite_unique.to_dict())
|
||||||
|
database.session.delete(assiduite_unique)
|
||||||
|
return (200, "OK")
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/assiduite/<int:assiduite_id>/edit", methods=["POST"])
|
||||||
|
@api_web_bp.route("/assiduite/<int:assiduite_id>/edit", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@as_json
|
||||||
|
@permission_required(Permission.ScoAssiduiteChange)
|
||||||
|
def assiduite_edit(assiduite_id: int):
|
||||||
|
"""
|
||||||
|
Edition d'une assiduité à partir de son id
|
||||||
|
La requête doit avoir un content type "application/json":
|
||||||
|
{
|
||||||
|
"etat"?: str,
|
||||||
|
"moduleimpl_id"?: int
|
||||||
|
"desc"?: str
|
||||||
|
"est_just"?: bool
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
assiduite_unique: Assiduite = Assiduite.query.filter_by(
|
||||||
|
id=assiduite_id
|
||||||
|
).first_or_404()
|
||||||
|
errors: list[str] = []
|
||||||
|
data = request.get_json(force=True)
|
||||||
|
|
||||||
|
# Vérifications de data
|
||||||
|
|
||||||
|
# Cas 1 : Etat
|
||||||
|
if data.get("etat") is not None:
|
||||||
|
etat = scu.EtatAssiduite.get(data.get("etat"))
|
||||||
|
if etat is None:
|
||||||
|
errors.append("param 'etat': invalide")
|
||||||
|
else:
|
||||||
|
assiduite_unique.etat = etat
|
||||||
|
|
||||||
|
# Cas 2 : Moduleimpl_id
|
||||||
|
moduleimpl_id = data.get("moduleimpl_id", False)
|
||||||
|
moduleimpl: ModuleImpl = None
|
||||||
|
|
||||||
|
if moduleimpl_id is not False:
|
||||||
|
if moduleimpl_id is not None and moduleimpl_id != "":
|
||||||
|
moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first()
|
||||||
|
if moduleimpl is None:
|
||||||
|
errors.append("param 'moduleimpl_id': invalide")
|
||||||
|
else:
|
||||||
|
if not moduleimpl.est_inscrit(
|
||||||
|
Identite.query.filter_by(id=assiduite_unique.etudid).first()
|
||||||
|
):
|
||||||
|
errors.append("param 'moduleimpl_id': etud non inscrit")
|
||||||
|
else:
|
||||||
|
assiduite_unique.moduleimpl_id = moduleimpl_id
|
||||||
|
else:
|
||||||
|
assiduite_unique.moduleimpl_id = None
|
||||||
|
|
||||||
|
# Cas 3 : desc
|
||||||
|
desc = data.get("desc", False)
|
||||||
|
if desc is not False:
|
||||||
|
assiduite_unique.desc = desc
|
||||||
|
|
||||||
|
# Cas 4 : est_just
|
||||||
|
est_just = data.get("est_just")
|
||||||
|
if est_just is not None:
|
||||||
|
if not isinstance(est_just, bool):
|
||||||
|
errors.append("param 'est_just' : booléen non reconnu")
|
||||||
|
else:
|
||||||
|
assiduite_unique.est_just = est_just
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
err: str = ", ".join(errors)
|
||||||
|
return json_error(404, err)
|
||||||
|
|
||||||
|
db.session.add(assiduite_unique)
|
||||||
|
db.session.commit()
|
||||||
|
scass.simple_invalidate_cache(assiduite_unique.to_dict())
|
||||||
|
|
||||||
|
return {"OK": True}
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/assiduites/edit", methods=["POST"])
|
||||||
|
@api_web_bp.route("/assiduites/edit", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@as_json
|
||||||
|
@permission_required(Permission.ScoAssiduiteChange)
|
||||||
|
def assiduites_edit():
|
||||||
|
"""
|
||||||
|
Edition d'une assiduité à partir de son id
|
||||||
|
La requête doit avoir un content type "application/json":
|
||||||
|
{
|
||||||
|
"etat"?: str,
|
||||||
|
"moduleimpl_id"?: int
|
||||||
|
"desc"?: str
|
||||||
|
"est_just"?: bool
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
edit_list: list[object] = request.get_json(force=True)
|
||||||
|
|
||||||
|
if not isinstance(edit_list, list):
|
||||||
|
return json_error(404, "Le contenu envoyé n'est pas une liste")
|
||||||
|
|
||||||
|
errors: dict[int, str] = {}
|
||||||
|
success: dict[int, object] = {}
|
||||||
|
for i, data in enumerate(edit_list):
|
||||||
|
assi: Identite = Assiduite.query.filter_by(id=data["assiduite_id"]).first()
|
||||||
|
if assi is None:
|
||||||
|
errors[i] = "Cet assiduité n'existe pas."
|
||||||
|
continue
|
||||||
|
|
||||||
|
code, obj = _edit_singular(assi, data)
|
||||||
|
if code == 404:
|
||||||
|
errors[i] = obj
|
||||||
|
else:
|
||||||
|
success[i] = obj
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return {"errors": errors, "success": success}
|
||||||
|
|
||||||
|
|
||||||
|
def _edit_singular(assiduite_unique, data):
|
||||||
|
errors: list[str] = []
|
||||||
|
|
||||||
|
# Vérifications de data
|
||||||
|
|
||||||
|
# Cas 1 : Etat
|
||||||
|
if data.get("etat") is not None:
|
||||||
|
etat = scu.EtatAssiduite.get(data.get("etat"))
|
||||||
|
if etat is None:
|
||||||
|
errors.append("param 'etat': invalide")
|
||||||
|
else:
|
||||||
|
assiduite_unique.etat = etat
|
||||||
|
|
||||||
|
# Cas 2 : Moduleimpl_id
|
||||||
|
moduleimpl_id = data.get("moduleimpl_id", False)
|
||||||
|
moduleimpl: ModuleImpl = None
|
||||||
|
|
||||||
|
if moduleimpl_id is not False:
|
||||||
|
if moduleimpl_id is not None:
|
||||||
|
moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first()
|
||||||
|
if moduleimpl is None:
|
||||||
|
errors.append("param 'moduleimpl_id': invalide")
|
||||||
|
else:
|
||||||
|
if not moduleimpl.est_inscrit(
|
||||||
|
Identite.query.filter_by(id=assiduite_unique.etudid).first()
|
||||||
|
):
|
||||||
|
errors.append("param 'moduleimpl_id': etud non inscrit")
|
||||||
|
else:
|
||||||
|
assiduite_unique.moduleimpl_id = moduleimpl_id
|
||||||
|
else:
|
||||||
|
assiduite_unique.moduleimpl_id = moduleimpl_id
|
||||||
|
|
||||||
|
# Cas 3 : desc
|
||||||
|
desc = data.get("desc", False)
|
||||||
|
if desc is not False:
|
||||||
|
assiduite_unique.desc = desc
|
||||||
|
|
||||||
|
# Cas 4 : est_just
|
||||||
|
est_just = data.get("est_just")
|
||||||
|
if est_just is not None:
|
||||||
|
if not isinstance(est_just, bool):
|
||||||
|
errors.append("param 'est_just' : booléen non reconnu")
|
||||||
|
else:
|
||||||
|
assiduite_unique.est_just = est_just
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
err: str = ", ".join(errors)
|
||||||
|
return (404, err)
|
||||||
|
|
||||||
|
db.session.add(assiduite_unique)
|
||||||
|
scass.simple_invalidate_cache(assiduite_unique.to_dict())
|
||||||
|
|
||||||
|
return (200, "OK")
|
||||||
|
|
||||||
|
|
||||||
|
# -- Utils --
|
||||||
|
|
||||||
|
|
||||||
|
def _count_manager(requested) -> tuple[str, dict]:
|
||||||
|
"""
|
||||||
|
Retourne la/les métriques à utiliser ainsi que le filtre donnés en query de la requête
|
||||||
|
"""
|
||||||
|
filtered: dict = {}
|
||||||
|
# cas 1 : etat assiduite
|
||||||
|
etat = requested.args.get("etat")
|
||||||
|
if etat is not None:
|
||||||
|
filtered["etat"] = etat
|
||||||
|
|
||||||
|
# cas 2 : date de début
|
||||||
|
deb = requested.args.get("date_debut", "").replace(" ", "+")
|
||||||
|
deb: datetime = scu.is_iso_formated(deb, True)
|
||||||
|
if deb is not None:
|
||||||
|
filtered["date_debut"] = deb
|
||||||
|
|
||||||
|
# cas 3 : date de fin
|
||||||
|
fin = requested.args.get("date_fin", "").replace(" ", "+")
|
||||||
|
fin = scu.is_iso_formated(fin, True)
|
||||||
|
|
||||||
|
if fin is not None:
|
||||||
|
filtered["date_fin"] = fin
|
||||||
|
|
||||||
|
# cas 4 : moduleimpl_id
|
||||||
|
module = requested.args.get("moduleimpl_id", False)
|
||||||
|
try:
|
||||||
|
if module is False:
|
||||||
|
raise ValueError
|
||||||
|
if module != "":
|
||||||
|
module = int(module)
|
||||||
|
else:
|
||||||
|
module = None
|
||||||
|
except ValueError:
|
||||||
|
module = False
|
||||||
|
|
||||||
|
if module is not False:
|
||||||
|
filtered["moduleimpl_id"] = module
|
||||||
|
|
||||||
|
# cas 5 : formsemestre_id
|
||||||
|
formsemestre_id = requested.args.get("formsemestre_id")
|
||||||
|
|
||||||
|
if formsemestre_id is not None:
|
||||||
|
formsemestre: FormSemestre = None
|
||||||
|
formsemestre_id = int(formsemestre_id)
|
||||||
|
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
|
||||||
|
filtered["formsemestre"] = formsemestre
|
||||||
|
|
||||||
|
# cas 6 : type
|
||||||
|
metric = requested.args.get("metric", "all")
|
||||||
|
|
||||||
|
# cas 7 : est_just
|
||||||
|
|
||||||
|
est_just: str = requested.args.get("est_just")
|
||||||
|
if est_just is not None:
|
||||||
|
trues: tuple[str] = ("v", "t", "vrai", "true")
|
||||||
|
falses: tuple[str] = ("f", "faux", "false")
|
||||||
|
|
||||||
|
if est_just.lower() in trues:
|
||||||
|
filtered["est_just"] = True
|
||||||
|
elif est_just.lower() in falses:
|
||||||
|
filtered["est_just"] = False
|
||||||
|
|
||||||
|
# cas 8 : user_id
|
||||||
|
|
||||||
|
user_id = requested.args.get("user_id", False)
|
||||||
|
if user_id is not False:
|
||||||
|
filtered["user_id"] = user_id
|
||||||
|
|
||||||
|
return (metric, filtered)
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_manager(requested, assiduites_query: Assiduite):
|
||||||
|
"""
|
||||||
|
Retourne les assiduites entrées filtrées en fonction de la request
|
||||||
|
"""
|
||||||
|
# cas 1 : etat assiduite
|
||||||
|
etat = requested.args.get("etat")
|
||||||
|
if etat is not None:
|
||||||
|
assiduites_query = scass.filter_assiduites_by_etat(assiduites_query, etat)
|
||||||
|
|
||||||
|
# cas 2 : date de début
|
||||||
|
deb = requested.args.get("date_debut", "").replace(" ", "+")
|
||||||
|
deb: datetime = scu.is_iso_formated(deb, True)
|
||||||
|
|
||||||
|
# cas 3 : date de fin
|
||||||
|
fin = requested.args.get("date_fin", "").replace(" ", "+")
|
||||||
|
fin = scu.is_iso_formated(fin, True)
|
||||||
|
|
||||||
|
if (deb, fin) != (None, None):
|
||||||
|
assiduites_query: Assiduite = scass.filter_by_date(
|
||||||
|
assiduites_query, Assiduite, deb, fin
|
||||||
|
)
|
||||||
|
|
||||||
|
# cas 4 : moduleimpl_id
|
||||||
|
module = requested.args.get("moduleimpl_id", False)
|
||||||
|
try:
|
||||||
|
if module is False:
|
||||||
|
raise ValueError
|
||||||
|
if module != "":
|
||||||
|
module = int(module)
|
||||||
|
else:
|
||||||
|
module = None
|
||||||
|
except ValueError:
|
||||||
|
module = False
|
||||||
|
|
||||||
|
if module is not False:
|
||||||
|
assiduites_query = scass.filter_by_module_impl(assiduites_query, module)
|
||||||
|
|
||||||
|
# cas 5 : formsemestre_id
|
||||||
|
formsemestre_id = requested.args.get("formsemestre_id")
|
||||||
|
|
||||||
|
if formsemestre_id is not None:
|
||||||
|
formsemestre: FormSemestre = None
|
||||||
|
formsemestre_id = int(formsemestre_id)
|
||||||
|
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
|
||||||
|
assiduites_query = scass.filter_by_formsemestre(assiduites_query, formsemestre)
|
||||||
|
|
||||||
|
# cas 6 : est_just
|
||||||
|
|
||||||
|
est_just: str = requested.args.get("est_just")
|
||||||
|
if est_just is not None:
|
||||||
|
trues: tuple[str] = ("v", "t", "vrai", "true")
|
||||||
|
falses: tuple[str] = ("f", "faux", "false")
|
||||||
|
|
||||||
|
if est_just.lower() in trues:
|
||||||
|
assiduites_query: Assiduite = scass.filter_assiduites_by_est_just(
|
||||||
|
assiduites_query, True
|
||||||
|
)
|
||||||
|
elif est_just.lower() in falses:
|
||||||
|
assiduites_query: Assiduite = scass.filter_assiduites_by_est_just(
|
||||||
|
assiduites_query, False
|
||||||
|
)
|
||||||
|
|
||||||
|
# cas 8 : user_id
|
||||||
|
|
||||||
|
user_id = requested.args.get("user_id", False)
|
||||||
|
if user_id is not False:
|
||||||
|
assiduites_query: Assiduite = scass.filter_by_user_id(assiduites_query, user_id)
|
||||||
|
|
||||||
|
return assiduites_query
|
37
app/api/etudiants.py
Normal file → Executable file
@ -34,6 +34,7 @@ from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud
|
|||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
from app.scodoc.sco_utils import json_error, suppress_accents
|
from app.scodoc.sco_utils import json_error, suppress_accents
|
||||||
|
|
||||||
|
import app.scodoc.sco_photos as sco_photos
|
||||||
|
|
||||||
# Un exemple:
|
# Un exemple:
|
||||||
# @bp.route("/api_function/<int:arg>")
|
# @bp.route("/api_function/<int:arg>")
|
||||||
@ -136,6 +137,42 @@ def etudiant(etudid: int = None, nip: str = None, ine: str = None):
|
|||||||
return etud.to_dict_api()
|
return etud.to_dict_api()
|
||||||
|
|
||||||
|
|
||||||
|
@api_web_bp.route("/etudiant/etudid/<int:etudid>/photo")
|
||||||
|
@api_web_bp.route("/etudiant/nip/<string:nip>/photo")
|
||||||
|
@api_web_bp.route("/etudiant/ine/<string:ine>/photo")
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def get_photo_image(etudid: int = None, nip: str = None, ine: str = None):
|
||||||
|
"""
|
||||||
|
Retourne la photo de l'étudiant
|
||||||
|
correspondant ou un placeholder si non existant.
|
||||||
|
|
||||||
|
etudid : l'etudid de l'étudiant
|
||||||
|
nip : le code nip de l'étudiant
|
||||||
|
ine : le code ine de l'étudiant
|
||||||
|
|
||||||
|
Attention : Ne peut être qu'utilisée en tant que route de département
|
||||||
|
"""
|
||||||
|
|
||||||
|
etud = tools.get_etud(etudid, nip, ine)
|
||||||
|
|
||||||
|
if etud is None:
|
||||||
|
return json_error(
|
||||||
|
404,
|
||||||
|
message="étudiant inconnu",
|
||||||
|
)
|
||||||
|
if not etudid:
|
||||||
|
filename = sco_photos.UNKNOWN_IMAGE_PATH
|
||||||
|
|
||||||
|
size = request.args.get("size", "orig")
|
||||||
|
filename = sco_photos.photo_pathname(etud.photo_filename, size=size)
|
||||||
|
if not filename:
|
||||||
|
filename = sco_photos.UNKNOWN_IMAGE_PATH
|
||||||
|
res = sco_photos.build_image_response(filename)
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/etudiants/etudid/<int:etudid>", methods=["GET"])
|
@bp.route("/etudiants/etudid/<int:etudid>", methods=["GET"])
|
||||||
@bp.route("/etudiants/nip/<string:nip>", methods=["GET"])
|
@bp.route("/etudiants/nip/<string:nip>", methods=["GET"])
|
||||||
@bp.route("/etudiants/ine/<string:ine>", methods=["GET"])
|
@bp.route("/etudiants/ine/<string:ine>", methods=["GET"])
|
||||||
|
636
app/api/justificatifs.py
Normal file
@ -0,0 +1,636 @@
|
|||||||
|
##############################################################################
|
||||||
|
# ScoDoc
|
||||||
|
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||||
|
# See LICENSE
|
||||||
|
##############################################################################
|
||||||
|
"""ScoDoc 9 API : Assiduités
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from flask_json import as_json
|
||||||
|
from flask import g, jsonify, request
|
||||||
|
from flask_login import login_required, current_user
|
||||||
|
|
||||||
|
import app.scodoc.sco_assiduites as scass
|
||||||
|
import app.scodoc.sco_utils as scu
|
||||||
|
from app import db
|
||||||
|
from app.api import api_bp as bp
|
||||||
|
from app.api import api_web_bp
|
||||||
|
from app.api import get_model_api_object
|
||||||
|
from app.decorators import permission_required, scodoc
|
||||||
|
from app.models import Identite, Justificatif, Departement
|
||||||
|
from app.models.assiduites import compute_assiduites_justified
|
||||||
|
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
|
||||||
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
|
from app.scodoc.sco_permissions import Permission
|
||||||
|
from app.scodoc.sco_utils import json_error
|
||||||
|
|
||||||
|
|
||||||
|
# Partie Modèle
|
||||||
|
@bp.route("/justificatif/<int:justif_id>")
|
||||||
|
@api_web_bp.route("/justificatif/<int:justif_id>")
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def justificatif(justif_id: int = None):
|
||||||
|
"""Retourne un objet justificatif à partir de son id
|
||||||
|
|
||||||
|
Exemple de résultat:
|
||||||
|
{
|
||||||
|
"justif_id": 1,
|
||||||
|
"etudid": 2,
|
||||||
|
"date_debut": "2022-10-31T08:00+01:00",
|
||||||
|
"date_fin": "2022-10-31T10:00+01:00",
|
||||||
|
"etat": "valide",
|
||||||
|
"fichier": "archive_id",
|
||||||
|
"raison": "une raison",
|
||||||
|
"entry_date": "2022-10-31T08:00+01:00",
|
||||||
|
"user_id": 1 or null,
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
return get_model_api_object(Justificatif, justif_id, Identite)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/justificatifs/<int:etudid>", defaults={"with_query": False})
|
||||||
|
@bp.route("/justificatifs/<int:etudid>/query", defaults={"with_query": True})
|
||||||
|
@api_web_bp.route("/justificatifs/<int:etudid>", defaults={"with_query": False})
|
||||||
|
@api_web_bp.route("/justificatifs/<int:etudid>/query", defaults={"with_query": True})
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@as_json
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def justificatifs(etudid: int = None, with_query: bool = False):
|
||||||
|
"""
|
||||||
|
Retourne toutes les assiduités d'un étudiant
|
||||||
|
chemin : /justificatifs/<int:etudid>
|
||||||
|
|
||||||
|
Un filtrage peut être donné avec une query
|
||||||
|
chemin : /justificatifs/<int:etudid>/query?
|
||||||
|
|
||||||
|
Les différents filtres :
|
||||||
|
Etat (etat du justificatif -> validé, non validé, modifé, en attente):
|
||||||
|
query?etat=[- liste des états séparé par une virgule -]
|
||||||
|
ex: .../query?etat=validé,modifié
|
||||||
|
Date debut
|
||||||
|
(date de début du justificatif, sont affichés les justificatifs
|
||||||
|
dont la date de début est supérieur ou égale à la valeur donnée):
|
||||||
|
query?date_debut=[- date au format iso -]
|
||||||
|
ex: query?date_debut=2022-11-03T08:00+01:00
|
||||||
|
Date fin
|
||||||
|
(date de fin du justificatif, sont affichés les justificatifs
|
||||||
|
dont la date de fin est inférieure ou égale à la valeur donnée):
|
||||||
|
query?date_fin=[- date au format iso -]
|
||||||
|
ex: query?date_fin=2022-11-03T10:00+01:00
|
||||||
|
user_id (l'id de l'auteur du justificatif)
|
||||||
|
query?user_id=[int]
|
||||||
|
ex query?user_id=3
|
||||||
|
"""
|
||||||
|
|
||||||
|
query = Identite.query.filter_by(id=etudid)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
|
||||||
|
etud: Identite = query.first_or_404(etudid)
|
||||||
|
justificatifs_query = etud.justificatifs
|
||||||
|
|
||||||
|
if with_query:
|
||||||
|
justificatifs_query = _filter_manager(request, justificatifs_query)
|
||||||
|
|
||||||
|
data_set: list[dict] = []
|
||||||
|
for just in justificatifs_query.all():
|
||||||
|
data = just.to_dict(format_api=True)
|
||||||
|
data_set.append(data)
|
||||||
|
|
||||||
|
return data_set
|
||||||
|
|
||||||
|
|
||||||
|
@api_web_bp.route("/justificatifs/dept/<int:dept_id>", defaults={"with_query": False})
|
||||||
|
@api_web_bp.route(
|
||||||
|
"/justificatifs/dept/<int:dept_id>/query", defaults={"with_query": True}
|
||||||
|
)
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@as_json
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def justificatifs_dept(dept_id: int = None, with_query: bool = False):
|
||||||
|
""" """
|
||||||
|
dept = Departement.query.get_or_404(dept_id)
|
||||||
|
etuds = [etud.id for etud in dept.etudiants]
|
||||||
|
|
||||||
|
justificatifs_query = Justificatif.query.filter(Justificatif.etudid.in_(etuds))
|
||||||
|
|
||||||
|
if with_query:
|
||||||
|
justificatifs_query = _filter_manager(request, justificatifs_query)
|
||||||
|
data_set: list[dict] = []
|
||||||
|
for just in justificatifs_query.all():
|
||||||
|
data = just.to_dict(format_api=True)
|
||||||
|
data_set.append(data)
|
||||||
|
|
||||||
|
return data_set
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/justificatif/<int:etudid>/create", methods=["POST"])
|
||||||
|
@api_web_bp.route("/justificatif/<int:etudid>/create", methods=["POST"])
|
||||||
|
@scodoc
|
||||||
|
@login_required
|
||||||
|
@as_json
|
||||||
|
@permission_required(Permission.ScoJustifChange)
|
||||||
|
def justif_create(etudid: int = None):
|
||||||
|
"""
|
||||||
|
Création d'un justificatif pour l'étudiant (etudid)
|
||||||
|
La requête doit avoir un content type "application/json":
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"date_debut": str,
|
||||||
|
"date_fin": str,
|
||||||
|
"etat": str,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date_debut": str,
|
||||||
|
"date_fin": str,
|
||||||
|
"etat": str,
|
||||||
|
"raison":str,
|
||||||
|
}
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
"""
|
||||||
|
etud: Identite = Identite.query.filter_by(id=etudid).first_or_404()
|
||||||
|
|
||||||
|
create_list: list[object] = request.get_json(force=True)
|
||||||
|
|
||||||
|
if not isinstance(create_list, list):
|
||||||
|
return json_error(404, "Le contenu envoyé n'est pas une liste")
|
||||||
|
|
||||||
|
errors: dict[int, str] = {}
|
||||||
|
success: dict[int, object] = {}
|
||||||
|
for i, data in enumerate(create_list):
|
||||||
|
code, obj = _create_singular(data, etud)
|
||||||
|
if code == 404:
|
||||||
|
errors[i] = obj
|
||||||
|
else:
|
||||||
|
success[i] = obj
|
||||||
|
scass.simple_invalidate_cache(data, etud.id)
|
||||||
|
compute_assiduites_justified(Justificatif.query.filter_by(etudid=etudid), True)
|
||||||
|
return {"errors": errors, "success": success}
|
||||||
|
|
||||||
|
|
||||||
|
def _create_singular(
|
||||||
|
data: dict,
|
||||||
|
etud: Identite,
|
||||||
|
) -> tuple[int, object]:
|
||||||
|
errors: list[str] = []
|
||||||
|
|
||||||
|
# -- vérifications de l'objet json --
|
||||||
|
# cas 1 : ETAT
|
||||||
|
etat = data.get("etat", None)
|
||||||
|
if etat is None:
|
||||||
|
errors.append("param 'etat': manquant")
|
||||||
|
elif not scu.EtatJustificatif.contains(etat):
|
||||||
|
errors.append("param 'etat': invalide")
|
||||||
|
|
||||||
|
etat = scu.EtatJustificatif.get(etat)
|
||||||
|
|
||||||
|
# cas 2 : date_debut
|
||||||
|
date_debut = data.get("date_debut", None)
|
||||||
|
if date_debut is None:
|
||||||
|
errors.append("param 'date_debut': manquant")
|
||||||
|
deb = scu.is_iso_formated(date_debut, convert=True)
|
||||||
|
if deb is None:
|
||||||
|
errors.append("param 'date_debut': format invalide")
|
||||||
|
|
||||||
|
# cas 3 : date_fin
|
||||||
|
date_fin = data.get("date_fin", None)
|
||||||
|
if date_fin is None:
|
||||||
|
errors.append("param 'date_fin': manquant")
|
||||||
|
fin = scu.is_iso_formated(date_fin, convert=True)
|
||||||
|
if fin is None:
|
||||||
|
errors.append("param 'date_fin': format invalide")
|
||||||
|
|
||||||
|
# cas 4 : raison
|
||||||
|
|
||||||
|
raison: str = data.get("raison", None)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
err: str = ", ".join(errors)
|
||||||
|
return (404, err)
|
||||||
|
|
||||||
|
# TOUT EST OK
|
||||||
|
|
||||||
|
try:
|
||||||
|
nouv_justificatif: Justificatif = Justificatif.create_justificatif(
|
||||||
|
date_debut=deb,
|
||||||
|
date_fin=fin,
|
||||||
|
etat=etat,
|
||||||
|
etud=etud,
|
||||||
|
raison=raison,
|
||||||
|
user_id=current_user.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(nouv_justificatif)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return (
|
||||||
|
200,
|
||||||
|
{
|
||||||
|
"justif_id": nouv_justificatif.id,
|
||||||
|
"couverture": scass.justifies(nouv_justificatif),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except ScoValueError as excp:
|
||||||
|
return (
|
||||||
|
404,
|
||||||
|
excp.args[0],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/justificatif/<int:justif_id>/edit", methods=["POST"])
|
||||||
|
@api_web_bp.route("/justificatif/<int:justif_id>/edit", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@as_json
|
||||||
|
@permission_required(Permission.ScoJustifChange)
|
||||||
|
def justif_edit(justif_id: int):
|
||||||
|
"""
|
||||||
|
Edition d'un justificatif à partir de son id
|
||||||
|
La requête doit avoir un content type "application/json":
|
||||||
|
|
||||||
|
{
|
||||||
|
"etat"?: str,
|
||||||
|
"raison"?: str
|
||||||
|
"date_debut"?: str
|
||||||
|
"date_fin"?: str
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
justificatif_unique: Justificatif = Justificatif.query.filter_by(
|
||||||
|
id=justif_id
|
||||||
|
).first_or_404()
|
||||||
|
|
||||||
|
errors: list[str] = []
|
||||||
|
data = request.get_json(force=True)
|
||||||
|
avant_ids: list[int] = scass.justifies(justificatif_unique)
|
||||||
|
# Vérifications de data
|
||||||
|
|
||||||
|
# Cas 1 : Etat
|
||||||
|
if data.get("etat") is not None:
|
||||||
|
etat = scu.EtatJustificatif.get(data.get("etat"))
|
||||||
|
if etat is None:
|
||||||
|
errors.append("param 'etat': invalide")
|
||||||
|
else:
|
||||||
|
justificatif_unique.etat = etat
|
||||||
|
|
||||||
|
# Cas 2 : raison
|
||||||
|
raison = data.get("raison", False)
|
||||||
|
if raison is not False:
|
||||||
|
justificatif_unique.raison = raison
|
||||||
|
|
||||||
|
deb, fin = None, None
|
||||||
|
|
||||||
|
# cas 3 : date_debut
|
||||||
|
date_debut = data.get("date_debut", False)
|
||||||
|
if date_debut is not False:
|
||||||
|
if date_debut is None:
|
||||||
|
errors.append("param 'date_debut': manquant")
|
||||||
|
deb = scu.is_iso_formated(date_debut.replace(" ", "+"), convert=True)
|
||||||
|
if deb is None:
|
||||||
|
errors.append("param 'date_debut': format invalide")
|
||||||
|
|
||||||
|
# cas 4 : date_fin
|
||||||
|
date_fin = data.get("date_fin", False)
|
||||||
|
if date_fin is not False:
|
||||||
|
if date_fin is None:
|
||||||
|
errors.append("param 'date_fin': manquant")
|
||||||
|
fin = scu.is_iso_formated(date_fin.replace(" ", "+"), convert=True)
|
||||||
|
if fin is None:
|
||||||
|
errors.append("param 'date_fin': format invalide")
|
||||||
|
|
||||||
|
# Mise à jour des dates
|
||||||
|
deb = deb if deb is not None else justificatif_unique.date_debut
|
||||||
|
fin = fin if fin is not None else justificatif_unique.date_fin
|
||||||
|
|
||||||
|
if fin <= deb:
|
||||||
|
errors.append("param 'dates' : Date de début après date de fin")
|
||||||
|
|
||||||
|
justificatif_unique.date_debut = deb
|
||||||
|
justificatif_unique.date_fin = fin
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
err: str = ", ".join(errors)
|
||||||
|
return json_error(404, err)
|
||||||
|
|
||||||
|
db.session.add(justificatif_unique)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
retour = {
|
||||||
|
"couverture": {
|
||||||
|
"avant": avant_ids,
|
||||||
|
"après": compute_assiduites_justified(
|
||||||
|
Justificatif.query.filter_by(etudid=justificatif_unique.etudid),
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scass.simple_invalidate_cache(justificatif_unique.to_dict())
|
||||||
|
return retour
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/justificatif/delete", methods=["POST"])
|
||||||
|
@api_web_bp.route("/justificatif/delete", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@as_json
|
||||||
|
@permission_required(Permission.ScoJustifChange)
|
||||||
|
def justif_delete():
|
||||||
|
"""
|
||||||
|
Suppression d'un justificatif à partir de son id
|
||||||
|
|
||||||
|
Forme des données envoyées :
|
||||||
|
|
||||||
|
[
|
||||||
|
<justif_id:int>,
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
justificatifs_list: list[int] = request.get_json(force=True)
|
||||||
|
if not isinstance(justificatifs_list, list):
|
||||||
|
return json_error(404, "Le contenu envoyé n'est pas une liste")
|
||||||
|
|
||||||
|
output = {"errors": {}, "success": {}}
|
||||||
|
|
||||||
|
for i, ass in enumerate(justificatifs_list):
|
||||||
|
code, msg = _delete_singular(ass, db)
|
||||||
|
if code == 404:
|
||||||
|
output["errors"][f"{i}"] = msg
|
||||||
|
else:
|
||||||
|
output["success"][f"{i}"] = {"OK": True}
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_singular(justif_id: int, database):
|
||||||
|
justificatif_unique: Justificatif = Justificatif.query.filter_by(
|
||||||
|
id=justif_id
|
||||||
|
).first()
|
||||||
|
if justificatif_unique is None:
|
||||||
|
return (404, "Justificatif non existant")
|
||||||
|
|
||||||
|
archive_name: str = justificatif_unique.fichier
|
||||||
|
|
||||||
|
if archive_name is not None:
|
||||||
|
archiver: JustificatifArchiver = JustificatifArchiver()
|
||||||
|
try:
|
||||||
|
archiver.delete_justificatif(justificatif_unique.etudid, archive_name)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
database.session.delete(justificatif_unique)
|
||||||
|
|
||||||
|
compute_assiduites_justified(
|
||||||
|
Justificatif.query.filter_by(etudid=justificatif_unique.etudid), True
|
||||||
|
)
|
||||||
|
|
||||||
|
scass.simple_invalidate_cache(justificatif_unique.to_dict())
|
||||||
|
|
||||||
|
return (200, "OK")
|
||||||
|
|
||||||
|
|
||||||
|
# Partie archivage
|
||||||
|
@bp.route("/justificatif/<int:justif_id>/import", methods=["POST"])
|
||||||
|
@api_web_bp.route("/justificatif/<int:justif_id>/import", methods=["POST"])
|
||||||
|
@scodoc
|
||||||
|
@login_required
|
||||||
|
@as_json
|
||||||
|
@permission_required(Permission.ScoJustifChange)
|
||||||
|
def justif_import(justif_id: int = None):
|
||||||
|
"""
|
||||||
|
Importation d'un fichier (création d'archive)
|
||||||
|
"""
|
||||||
|
if len(request.files) == 0:
|
||||||
|
return json_error(404, "Il n'y a pas de fichier joint")
|
||||||
|
|
||||||
|
file = list(request.files.values())[0]
|
||||||
|
if file.filename == "":
|
||||||
|
return json_error(404, "Il n'y a pas de fichier joint")
|
||||||
|
|
||||||
|
query = Justificatif.query.filter_by(id=justif_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
|
||||||
|
justificatif_unique: Justificatif = query.first_or_404()
|
||||||
|
|
||||||
|
archive_name: str = justificatif_unique.fichier
|
||||||
|
|
||||||
|
archiver: JustificatifArchiver = JustificatifArchiver()
|
||||||
|
try:
|
||||||
|
fname: str
|
||||||
|
archive_name, fname = archiver.save_justificatif(
|
||||||
|
etudid=justificatif_unique.etudid,
|
||||||
|
filename=file.filename,
|
||||||
|
data=file.stream.read(),
|
||||||
|
archive_name=archive_name,
|
||||||
|
user_id=current_user.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
justificatif_unique.fichier = archive_name
|
||||||
|
|
||||||
|
db.session.add(justificatif_unique)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return {"filename": fname}
|
||||||
|
except ScoValueError as err:
|
||||||
|
return json_error(404, err.args[0])
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/justificatif/<int:justif_id>/export/<filename>", methods=["POST"])
|
||||||
|
@api_web_bp.route("/justificatif/<int:justif_id>/export/<filename>", methods=["POST"])
|
||||||
|
@scodoc
|
||||||
|
@login_required
|
||||||
|
@permission_required(Permission.ScoJustifChange)
|
||||||
|
def justif_export(justif_id: int = None, filename: str = None):
|
||||||
|
"""
|
||||||
|
Retourne un fichier d'une archive d'un justificatif
|
||||||
|
"""
|
||||||
|
|
||||||
|
query = Justificatif.query.filter_by(id=justif_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
|
||||||
|
justificatif_unique: Justificatif = query.first_or_404()
|
||||||
|
|
||||||
|
archive_name: str = justificatif_unique.fichier
|
||||||
|
if archive_name is None:
|
||||||
|
return json_error(404, "le justificatif ne possède pas de fichier")
|
||||||
|
|
||||||
|
archiver: JustificatifArchiver = JustificatifArchiver()
|
||||||
|
|
||||||
|
try:
|
||||||
|
return archiver.get_justificatif_file(
|
||||||
|
archive_name, justificatif_unique.etudid, filename
|
||||||
|
)
|
||||||
|
except ScoValueError as err:
|
||||||
|
return json_error(404, err.args[0])
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/justificatif/<int:justif_id>/remove", methods=["POST"])
|
||||||
|
@api_web_bp.route("/justificatif/<int:justif_id>/remove", methods=["POST"])
|
||||||
|
@scodoc
|
||||||
|
@login_required
|
||||||
|
@as_json
|
||||||
|
@permission_required(Permission.ScoJustifChange)
|
||||||
|
def justif_remove(justif_id: int = None):
|
||||||
|
"""
|
||||||
|
Supression d'un fichier ou d'une archive
|
||||||
|
# TOTALK: Doc, expliquer les noms coté server
|
||||||
|
{
|
||||||
|
"remove": <"all"/"list">
|
||||||
|
|
||||||
|
"filenames"?: [
|
||||||
|
<filename:str>,
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
data: dict = request.get_json(force=True)
|
||||||
|
|
||||||
|
query = Justificatif.query.filter_by(id=justif_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
|
||||||
|
justificatif_unique: Justificatif = query.first_or_404()
|
||||||
|
|
||||||
|
archive_name: str = justificatif_unique.fichier
|
||||||
|
if archive_name is None:
|
||||||
|
return json_error(404, "le justificatif ne possède pas de fichier")
|
||||||
|
|
||||||
|
remove: str = data.get("remove")
|
||||||
|
if remove is None or remove not in ("all", "list"):
|
||||||
|
return json_error(404, "param 'remove': Valeur invalide")
|
||||||
|
archiver: JustificatifArchiver = JustificatifArchiver()
|
||||||
|
etudid: int = justificatif_unique.etudid
|
||||||
|
try:
|
||||||
|
if remove == "all":
|
||||||
|
archiver.delete_justificatif(etudid=etudid, archive_name=archive_name)
|
||||||
|
justificatif_unique.fichier = None
|
||||||
|
db.session.add(justificatif_unique)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
else:
|
||||||
|
for fname in data.get("filenames", []):
|
||||||
|
archiver.delete_justificatif(
|
||||||
|
etudid=etudid,
|
||||||
|
archive_name=archive_name,
|
||||||
|
filename=fname,
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(archiver.list_justificatifs(archive_name, etudid)) == 0:
|
||||||
|
archiver.delete_justificatif(etudid, archive_name)
|
||||||
|
justificatif_unique.fichier = None
|
||||||
|
db.session.add(justificatif_unique)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
except ScoValueError as err:
|
||||||
|
return json_error(404, err.args[0])
|
||||||
|
|
||||||
|
return {"response": "removed"}
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/justificatif/<int:justif_id>/list", methods=["GET"])
|
||||||
|
@api_web_bp.route("/justificatif/<int:justif_id>/list", methods=["GET"])
|
||||||
|
@scodoc
|
||||||
|
@login_required
|
||||||
|
@as_json
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def justif_list(justif_id: int = None):
|
||||||
|
"""
|
||||||
|
Liste les fichiers du justificatif
|
||||||
|
"""
|
||||||
|
|
||||||
|
query = Justificatif.query.filter_by(id=justif_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
|
||||||
|
justificatif_unique: Justificatif = query.first_or_404()
|
||||||
|
|
||||||
|
archive_name: str = justificatif_unique.fichier
|
||||||
|
|
||||||
|
filenames: list[str] = []
|
||||||
|
|
||||||
|
archiver: JustificatifArchiver = JustificatifArchiver()
|
||||||
|
if archive_name is not None:
|
||||||
|
filenames = archiver.list_justificatifs(
|
||||||
|
archive_name, justificatif_unique.etudid
|
||||||
|
)
|
||||||
|
|
||||||
|
retour = {"total": len(filenames), "filenames": []}
|
||||||
|
|
||||||
|
for fi in filenames:
|
||||||
|
if int(fi[1]) == current_user.id or current_user.has_permission(
|
||||||
|
Permission.ScoJustifView
|
||||||
|
):
|
||||||
|
retour["filenames"].append(fi[0])
|
||||||
|
return retour
|
||||||
|
|
||||||
|
|
||||||
|
# Partie justification
|
||||||
|
@bp.route("/justificatif/<int:justif_id>/justifies", methods=["GET"])
|
||||||
|
@api_web_bp.route("/justificatif/<int:justif_id>/justifies", methods=["GET"])
|
||||||
|
@scodoc
|
||||||
|
@login_required
|
||||||
|
@as_json
|
||||||
|
@permission_required(Permission.ScoJustifChange)
|
||||||
|
def justif_justifies(justif_id: int = None):
|
||||||
|
"""
|
||||||
|
Liste assiduite_id justifiées par le justificatif
|
||||||
|
"""
|
||||||
|
|
||||||
|
query = Justificatif.query.filter_by(id=justif_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
|
||||||
|
justificatif_unique: Justificatif = query.first_or_404()
|
||||||
|
|
||||||
|
assiduites_list: list[int] = scass.justifies(justificatif_unique)
|
||||||
|
|
||||||
|
return assiduites_list
|
||||||
|
|
||||||
|
|
||||||
|
# -- Utils --
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_manager(requested, justificatifs_query):
|
||||||
|
"""
|
||||||
|
Retourne les justificatifs entrés filtrés en fonction de la request
|
||||||
|
"""
|
||||||
|
# cas 1 : etat justificatif
|
||||||
|
etat = requested.args.get("etat")
|
||||||
|
if etat is not None:
|
||||||
|
justificatifs_query = scass.filter_justificatifs_by_etat(
|
||||||
|
justificatifs_query, etat
|
||||||
|
)
|
||||||
|
|
||||||
|
# cas 2 : date de début
|
||||||
|
deb = requested.args.get("date_debut", "").replace(" ", "+")
|
||||||
|
deb: datetime = scu.is_iso_formated(deb, True)
|
||||||
|
|
||||||
|
# cas 3 : date de fin
|
||||||
|
fin = requested.args.get("date_fin", "").replace(" ", "+")
|
||||||
|
fin = scu.is_iso_formated(fin, True)
|
||||||
|
|
||||||
|
if (deb, fin) != (None, None):
|
||||||
|
justificatifs_query: Justificatif = scass.filter_by_date(
|
||||||
|
justificatifs_query, Justificatif, deb, fin
|
||||||
|
)
|
||||||
|
|
||||||
|
user_id = requested.args.get("user_id", False)
|
||||||
|
if user_id is not False:
|
||||||
|
justificatif_query: Justificatif = scass.filter_by_user_id(
|
||||||
|
justificatif_query, user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return justificatifs_query
|
@ -387,6 +387,11 @@ class BulletinBUT:
|
|||||||
semestre_infos["absences"] = {
|
semestre_infos["absences"] = {
|
||||||
"injustifie": nbabs - nbabsjust,
|
"injustifie": nbabs - nbabsjust,
|
||||||
"total": nbabs,
|
"total": nbabs,
|
||||||
|
"metrique": {
|
||||||
|
"H.": "Heure(s)",
|
||||||
|
"J.": "Journée(s)",
|
||||||
|
"1/2 J.": "1/2 Jour.",
|
||||||
|
}.get(sco_preferences.get_preference("assi_metrique")),
|
||||||
}
|
}
|
||||||
decisions_ues = self.res.get_etud_decisions_ue(etud.id) or {}
|
decisions_ues = self.res.get_etud_decisions_ue(etud.id) or {}
|
||||||
if self.prefs["bul_show_ects"]:
|
if self.prefs["bul_show_ects"]:
|
||||||
|
88
app/forms/main/config_assiduites.py
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
# -*- mode: python -*-
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# ScoDoc
|
||||||
|
#
|
||||||
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
Formulaire configuration Module Assiduités
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import SubmitField, DecimalField
|
||||||
|
from wtforms.fields.simple import StringField
|
||||||
|
from wtforms.widgets import TimeInput
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class TimeField(StringField):
|
||||||
|
"""HTML5 time input."""
|
||||||
|
|
||||||
|
widget = TimeInput()
|
||||||
|
|
||||||
|
def __init__(self, label=None, validators=None, fmt="%H:%M:%S", **kwargs):
|
||||||
|
super(TimeField, self).__init__(label, validators, **kwargs)
|
||||||
|
self.fmt = fmt
|
||||||
|
self.data = None
|
||||||
|
|
||||||
|
def _value(self):
|
||||||
|
if self.raw_data:
|
||||||
|
return " ".join(self.raw_data)
|
||||||
|
if self.data and isinstance(self.data, str):
|
||||||
|
self.data = datetime.time(*map(int, self.data.split(":")))
|
||||||
|
return self.data and self.data.strftime(self.fmt) or ""
|
||||||
|
|
||||||
|
def process_formdata(self, valuelist):
|
||||||
|
if valuelist:
|
||||||
|
time_str = " ".join(valuelist)
|
||||||
|
try:
|
||||||
|
components = time_str.split(":")
|
||||||
|
hour = 0
|
||||||
|
minutes = 0
|
||||||
|
seconds = 0
|
||||||
|
if len(components) in range(2, 4):
|
||||||
|
hour = int(components[0])
|
||||||
|
minutes = int(components[1])
|
||||||
|
|
||||||
|
if len(components) == 3:
|
||||||
|
seconds = int(components[2])
|
||||||
|
else:
|
||||||
|
raise ValueError
|
||||||
|
self.data = datetime.time(hour, minutes, seconds)
|
||||||
|
except ValueError:
|
||||||
|
self.data = None
|
||||||
|
raise ValueError(self.gettext("Not a valid time string"))
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigAssiduitesForm(FlaskForm):
|
||||||
|
"Formulaire paramétrage Module Assiduités"
|
||||||
|
|
||||||
|
morning_time = TimeField("Début de la journée")
|
||||||
|
lunch_time = TimeField("Heure de midi (date pivot entre Matin et Après Midi)")
|
||||||
|
afternoon_time = TimeField("Fin de la journée")
|
||||||
|
|
||||||
|
tick_time = DecimalField("Granularité de la Time Line (temps en minutes)", places=0)
|
||||||
|
|
||||||
|
submit = SubmitField("Valider")
|
||||||
|
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
@ -81,3 +81,5 @@ from app.models.but_refcomp import (
|
|||||||
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
||||||
|
|
||||||
from app.models.config import ScoDocSiteConfig
|
from app.models.config import ScoDocSiteConfig
|
||||||
|
|
||||||
|
from app.models.assiduites import Assiduite, Justificatif
|
||||||
|
356
app/models/assiduites.py
Normal file
@ -0,0 +1,356 @@
|
|||||||
|
# -*- coding: UTF-8 -*
|
||||||
|
"""Gestion de l'assiduité (assiduités + justificatifs)
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from app import db
|
||||||
|
from app.models import ModuleImpl
|
||||||
|
from app.models.etudiants import Identite
|
||||||
|
from app.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,
|
||||||
|
EtatJustificatif,
|
||||||
|
localize_datetime,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Assiduite(db.Model):
|
||||||
|
"""
|
||||||
|
Représente une assiduité:
|
||||||
|
- une plage horaire lié à un état et un étudiant
|
||||||
|
- un module si spécifiée
|
||||||
|
- une description si spécifiée
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "assiduites"
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True, nullable=False)
|
||||||
|
assiduite_id = db.synonym("id")
|
||||||
|
|
||||||
|
date_debut = db.Column(
|
||||||
|
db.DateTime(timezone=True), server_default=db.func.now(), nullable=False
|
||||||
|
)
|
||||||
|
date_fin = db.Column(
|
||||||
|
db.DateTime(timezone=True), server_default=db.func.now(), nullable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
moduleimpl_id = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey("notes_moduleimpl.id", ondelete="SET NULL"),
|
||||||
|
)
|
||||||
|
etudid = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||||
|
index=True,
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
etat = db.Column(db.Integer, nullable=False)
|
||||||
|
|
||||||
|
desc = db.Column(db.Text)
|
||||||
|
|
||||||
|
entry_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||||
|
|
||||||
|
user_id = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey("user.id", ondelete="SET NULL"),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
est_just = db.Column(db.Boolean, server_default="false", nullable=False)
|
||||||
|
|
||||||
|
def to_dict(self, format_api=True) -> dict:
|
||||||
|
"""Retourne la représentation json de l'assiduité"""
|
||||||
|
etat = self.etat
|
||||||
|
username = self.user_id
|
||||||
|
if format_api:
|
||||||
|
etat = EtatAssiduite.inverse().get(self.etat).name
|
||||||
|
if self.user_id is not None:
|
||||||
|
user: User = db.session.get(User, self.user_id)
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
username = "Non renseigné"
|
||||||
|
else:
|
||||||
|
username = user.get_prenomnom()
|
||||||
|
data = {
|
||||||
|
"assiduite_id": self.id,
|
||||||
|
"etudid": self.etudid,
|
||||||
|
"moduleimpl_id": self.moduleimpl_id,
|
||||||
|
"date_debut": self.date_debut,
|
||||||
|
"date_fin": self.date_fin,
|
||||||
|
"etat": etat,
|
||||||
|
"desc": self.desc,
|
||||||
|
"entry_date": self.entry_date,
|
||||||
|
"user_id": username,
|
||||||
|
"est_just": self.est_just,
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_assiduite(
|
||||||
|
cls,
|
||||||
|
etud: Identite,
|
||||||
|
date_debut: datetime,
|
||||||
|
date_fin: datetime,
|
||||||
|
etat: EtatAssiduite,
|
||||||
|
moduleimpl: ModuleImpl = None,
|
||||||
|
description: str = None,
|
||||||
|
entry_date: datetime = None,
|
||||||
|
user_id: int = None,
|
||||||
|
est_just: bool = False,
|
||||||
|
) -> object or int:
|
||||||
|
"""Créer une nouvelle assiduité pour l'étudiant"""
|
||||||
|
# Vérification de non duplication des périodes
|
||||||
|
assiduites: list[Assiduite] = etud.assiduites
|
||||||
|
if is_period_conflicting(date_debut, date_fin, assiduites, Assiduite):
|
||||||
|
raise ScoValueError(
|
||||||
|
"Duplication des assiduités (la période rentrée rentre en conflit avec une assiduité enregistrée)"
|
||||||
|
)
|
||||||
|
if moduleimpl is not None:
|
||||||
|
# Vérification de l'existence du module pour l'étudiant
|
||||||
|
if moduleimpl.est_inscrit(etud):
|
||||||
|
nouv_assiduite = Assiduite(
|
||||||
|
date_debut=date_debut,
|
||||||
|
date_fin=date_fin,
|
||||||
|
etat=etat,
|
||||||
|
etudiant=etud,
|
||||||
|
moduleimpl_id=moduleimpl.id,
|
||||||
|
desc=description,
|
||||||
|
entry_date=entry_date,
|
||||||
|
user_id=user_id,
|
||||||
|
est_just=est_just,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ScoValueError("L'étudiant n'est pas inscrit au moduleimpl")
|
||||||
|
else:
|
||||||
|
nouv_assiduite = Assiduite(
|
||||||
|
date_debut=date_debut,
|
||||||
|
date_fin=date_fin,
|
||||||
|
etat=etat,
|
||||||
|
etudiant=etud,
|
||||||
|
desc=description,
|
||||||
|
entry_date=entry_date,
|
||||||
|
user_id=user_id,
|
||||||
|
est_just=est_just,
|
||||||
|
)
|
||||||
|
|
||||||
|
return nouv_assiduite
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fast_create_assiduite(
|
||||||
|
cls,
|
||||||
|
etudid: int,
|
||||||
|
date_debut: datetime,
|
||||||
|
date_fin: datetime,
|
||||||
|
etat: EtatAssiduite,
|
||||||
|
moduleimpl_id: int = None,
|
||||||
|
description: str = None,
|
||||||
|
entry_date: datetime = None,
|
||||||
|
est_just: bool = False,
|
||||||
|
) -> object or int:
|
||||||
|
"""Créer une nouvelle assiduité pour l'étudiant"""
|
||||||
|
# Vérification de non duplication des périodes
|
||||||
|
|
||||||
|
nouv_assiduite = Assiduite(
|
||||||
|
date_debut=date_debut,
|
||||||
|
date_fin=date_fin,
|
||||||
|
etat=etat,
|
||||||
|
etudid=etudid,
|
||||||
|
moduleimpl_id=moduleimpl_id,
|
||||||
|
desc=description,
|
||||||
|
entry_date=entry_date,
|
||||||
|
est_just=est_just,
|
||||||
|
)
|
||||||
|
|
||||||
|
return nouv_assiduite
|
||||||
|
|
||||||
|
|
||||||
|
class Justificatif(db.Model):
|
||||||
|
"""
|
||||||
|
Représente un justificatif:
|
||||||
|
- une plage horaire lié à un état et un étudiant
|
||||||
|
- une raison si spécifiée
|
||||||
|
- un fichier si spécifié
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "justificatifs"
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
justif_id = db.synonym("id")
|
||||||
|
|
||||||
|
date_debut = db.Column(
|
||||||
|
db.DateTime(timezone=True), server_default=db.func.now(), nullable=False
|
||||||
|
)
|
||||||
|
date_fin = db.Column(
|
||||||
|
db.DateTime(timezone=True), server_default=db.func.now(), nullable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
etudid = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||||
|
index=True,
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
etat = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
entry_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||||
|
|
||||||
|
user_id = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey("user.id", ondelete="SET NULL"),
|
||||||
|
nullable=True,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
raison = db.Column(db.Text())
|
||||||
|
|
||||||
|
# Archive_id -> sco_archives_justificatifs.py
|
||||||
|
fichier = db.Column(db.Text())
|
||||||
|
|
||||||
|
def to_dict(self, format_api: bool = False) -> dict:
|
||||||
|
"""transformation de l'objet en dictionnaire sérialisable"""
|
||||||
|
|
||||||
|
etat = self.etat
|
||||||
|
username = self.user_id
|
||||||
|
|
||||||
|
if format_api:
|
||||||
|
etat = EtatJustificatif.inverse().get(self.etat).name
|
||||||
|
if self.user_id is not None:
|
||||||
|
user: User = db.session.get(User, self.user_id)
|
||||||
|
if user is None:
|
||||||
|
username = "Non renseigné"
|
||||||
|
else:
|
||||||
|
username = user.get_prenomnom()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"justif_id": self.justif_id,
|
||||||
|
"etudid": self.etudid,
|
||||||
|
"date_debut": self.date_debut,
|
||||||
|
"date_fin": self.date_fin,
|
||||||
|
"etat": etat,
|
||||||
|
"raison": self.raison,
|
||||||
|
"fichier": self.fichier,
|
||||||
|
"entry_date": self.entry_date,
|
||||||
|
"user_id": username,
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_justificatif(
|
||||||
|
cls,
|
||||||
|
etud: Identite,
|
||||||
|
date_debut: datetime,
|
||||||
|
date_fin: datetime,
|
||||||
|
etat: EtatJustificatif,
|
||||||
|
raison: str = None,
|
||||||
|
entry_date: datetime = None,
|
||||||
|
user_id: int = None,
|
||||||
|
) -> object or int:
|
||||||
|
"""Créer un nouveau justificatif pour l'étudiant"""
|
||||||
|
nouv_justificatif = Justificatif(
|
||||||
|
date_debut=date_debut,
|
||||||
|
date_fin=date_fin,
|
||||||
|
etat=etat,
|
||||||
|
etudiant=etud,
|
||||||
|
raison=raison,
|
||||||
|
entry_date=entry_date,
|
||||||
|
user_id=user_id,
|
||||||
|
)
|
||||||
|
return nouv_justificatif
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fast_create_justificatif(
|
||||||
|
cls,
|
||||||
|
etudid: int,
|
||||||
|
date_debut: datetime,
|
||||||
|
date_fin: datetime,
|
||||||
|
etat: EtatJustificatif,
|
||||||
|
raison: str = None,
|
||||||
|
entry_date: datetime = None,
|
||||||
|
) -> object or int:
|
||||||
|
"""Créer un nouveau justificatif pour l'étudiant"""
|
||||||
|
|
||||||
|
nouv_justificatif = Justificatif(
|
||||||
|
date_debut=date_debut,
|
||||||
|
date_fin=date_fin,
|
||||||
|
etat=etat,
|
||||||
|
etudid=etudid,
|
||||||
|
raison=raison,
|
||||||
|
entry_date=entry_date,
|
||||||
|
)
|
||||||
|
|
||||||
|
return nouv_justificatif
|
||||||
|
|
||||||
|
|
||||||
|
def is_period_conflicting(
|
||||||
|
date_debut: datetime,
|
||||||
|
date_fin: datetime,
|
||||||
|
collection: list[Assiduite or Justificatif],
|
||||||
|
collection_cls: Assiduite or Justificatif,
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Vérifie si une date n'entre pas en collision
|
||||||
|
avec les justificatifs ou assiduites déjà présentes
|
||||||
|
"""
|
||||||
|
|
||||||
|
date_debut = localize_datetime(date_debut)
|
||||||
|
date_fin = localize_datetime(date_fin)
|
||||||
|
|
||||||
|
if (
|
||||||
|
collection.filter_by(date_debut=date_debut, date_fin=date_fin).first()
|
||||||
|
is not None
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
|
||||||
|
count: int = collection.filter(
|
||||||
|
collection_cls.date_debut < date_fin, collection_cls.date_fin > date_debut
|
||||||
|
).count()
|
||||||
|
|
||||||
|
return count > 0
|
||||||
|
|
||||||
|
|
||||||
|
def compute_assiduites_justified(
|
||||||
|
justificatifs: Justificatif = Justificatif, reset: bool = False
|
||||||
|
) -> list[int]:
|
||||||
|
"""Calcule et modifie les champs "est_just" de chaque assiduité lié à l'étud
|
||||||
|
retourne la liste des assiduite_id justifiées
|
||||||
|
|
||||||
|
Si reset alors : met à false toutes les assiduités non justifiées par les justificatifs donnés
|
||||||
|
"""
|
||||||
|
|
||||||
|
list_assiduites_id: set[int] = set()
|
||||||
|
for justi in justificatifs:
|
||||||
|
assiduites: Assiduite = (
|
||||||
|
Assiduite.query.join(Justificatif, Justificatif.etudid == Assiduite.etudid)
|
||||||
|
.filter(justi.etat == EtatJustificatif.VALIDE)
|
||||||
|
.filter(
|
||||||
|
Assiduite.date_debut < justi.date_fin,
|
||||||
|
Assiduite.date_fin > justi.date_debut,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for assi in assiduites:
|
||||||
|
assi.est_just = True
|
||||||
|
list_assiduites_id.add(assi.id)
|
||||||
|
db.session.add(assi)
|
||||||
|
|
||||||
|
if reset:
|
||||||
|
un_justified: Assiduite = Assiduite.query.filter(
|
||||||
|
Assiduite.id.not_in(list_assiduites_id)
|
||||||
|
).join(Justificatif, Justificatif.etudid == Assiduite.etudid)
|
||||||
|
|
||||||
|
for assi in un_justified:
|
||||||
|
assi.est_just = False
|
||||||
|
db.session.add(assi)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return list(list_assiduites_id)
|
@ -8,6 +8,8 @@ from app import current_app, db, log
|
|||||||
from app.comp import bonus_spo
|
from app.comp import bonus_spo
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
from datetime import time
|
||||||
|
|
||||||
from app.scodoc.codes_cursus import (
|
from app.scodoc.codes_cursus import (
|
||||||
ABAN,
|
ABAN,
|
||||||
ABL,
|
ABL,
|
||||||
@ -96,6 +98,10 @@ class ScoDocSiteConfig(db.Model):
|
|||||||
"cas_logout_route": str,
|
"cas_logout_route": str,
|
||||||
"cas_validate_route": str,
|
"cas_validate_route": str,
|
||||||
"cas_attribute_id": str,
|
"cas_attribute_id": str,
|
||||||
|
# Assiduités
|
||||||
|
"morning_time": str,
|
||||||
|
"lunch_time": str,
|
||||||
|
"afternoon_time": str,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, name, value):
|
def __init__(self, name, value):
|
||||||
|
@ -73,6 +73,10 @@ class Identite(db.Model):
|
|||||||
passive_deletes=True,
|
passive_deletes=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Relations avec les assiduites et les justificatifs
|
||||||
|
assiduites = db.relationship("Assiduite", backref="etudiant", lazy="dynamic")
|
||||||
|
justificatifs = db.relationship("Justificatif", backref="etudiant", lazy="dynamic")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return (
|
return (
|
||||||
f"<Etud {self.id}/{self.departement.acronym} {self.nom!r} {self.prenom!r}>"
|
f"<Etud {self.id}/{self.departement.acronym} {self.nom!r} {self.prenom!r}>"
|
||||||
|
@ -39,9 +39,11 @@ from app.models.validations import ScolarFormSemestreValidation
|
|||||||
from app.scodoc import codes_cursus, sco_preferences
|
from app.scodoc import codes_cursus, sco_preferences
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
from app.scodoc.sco_utils import MONTH_NAMES_ABBREV
|
from app.scodoc.sco_utils import MONTH_NAMES_ABBREV, translate_assiduites_metric
|
||||||
from app.scodoc.sco_vdi import ApoEtapeVDI
|
from app.scodoc.sco_vdi import ApoEtapeVDI
|
||||||
|
|
||||||
|
from app.scodoc.sco_utils import translate_assiduites_metric
|
||||||
|
|
||||||
GROUPS_AUTO_ASSIGNMENT_DATA_MAX = 1024 * 1024 # bytes
|
GROUPS_AUTO_ASSIGNMENT_DATA_MAX = 1024 * 1024 # bytes
|
||||||
|
|
||||||
|
|
||||||
@ -700,10 +702,14 @@ class FormSemestre(db.Model):
|
|||||||
tuple (nb abs, nb abs justifiées)
|
tuple (nb abs, nb abs justifiées)
|
||||||
Utilise un cache.
|
Utilise un cache.
|
||||||
"""
|
"""
|
||||||
from app.scodoc import sco_abs
|
from app.scodoc import sco_assiduites
|
||||||
|
|
||||||
return sco_abs.get_abs_count_in_interval(
|
metrique = sco_preferences.get_preference("assi_metrique", self.id)
|
||||||
etudid, self.date_debut.isoformat(), self.date_fin.isoformat()
|
return sco_assiduites.get_assiduites_count_in_interval(
|
||||||
|
etudid,
|
||||||
|
self.date_debut.isoformat(),
|
||||||
|
self.date_fin.isoformat(),
|
||||||
|
translate_assiduites_metric(metrique),
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_codes_apogee(self, category=None) -> set[str]:
|
def get_codes_apogee(self, category=None) -> set[str]:
|
||||||
|
@ -122,6 +122,22 @@ class ModuleImpl(db.Model):
|
|||||||
raise AccessDenied(f"Modification impossible pour {user}")
|
raise AccessDenied(f"Modification impossible pour {user}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def est_inscrit(self, etud: Identite) -> bool:
|
||||||
|
"""
|
||||||
|
Vérifie si l'étudiant est bien inscrit au moduleimpl
|
||||||
|
|
||||||
|
Retourne Vrai si c'est le cas, faux sinon
|
||||||
|
"""
|
||||||
|
|
||||||
|
is_module: int = (
|
||||||
|
ModuleImplInscription.query.filter_by(
|
||||||
|
etudid=etud.id, moduleimpl_id=self.id
|
||||||
|
).count()
|
||||||
|
> 0
|
||||||
|
)
|
||||||
|
|
||||||
|
return is_module
|
||||||
|
|
||||||
|
|
||||||
# Enseignants (chargés de TD ou TP) d'un moduleimpl
|
# Enseignants (chargés de TD ou TP) d'un moduleimpl
|
||||||
notes_modules_enseignants = db.Table(
|
notes_modules_enseignants = db.Table(
|
||||||
|
43
app/profiler.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
from time import time
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class Profiler:
|
||||||
|
OUTPUT: str = "/tmp/scodoc.profiler.csv"
|
||||||
|
|
||||||
|
def __init__(self, tag: str) -> None:
|
||||||
|
self.tag: str = tag
|
||||||
|
self.start_time: time = None
|
||||||
|
self.stop_time: time = None
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self.start_time = time()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.stop_time = time()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def elapsed(self) -> float:
|
||||||
|
return self.stop_time - self.start_time
|
||||||
|
|
||||||
|
def dates(self) -> tuple[datetime, datetime]:
|
||||||
|
return datetime.fromtimestamp(self.start_time), datetime.fromtimestamp(
|
||||||
|
self.stop_time
|
||||||
|
)
|
||||||
|
|
||||||
|
def write(self):
|
||||||
|
with open(Profiler.OUTPUT, "a") as file:
|
||||||
|
dates: tuple = self.dates()
|
||||||
|
date_str = (dates[0].isoformat(), dates[1].isoformat())
|
||||||
|
file.write(f"\n{self.tag},{self.elapsed() : .2}")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def write_in(cls, msg: str):
|
||||||
|
with open(cls.OUTPUT, "a") as file:
|
||||||
|
file.write(f"\n# {msg}")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def clear(cls):
|
||||||
|
with open(cls.OUTPUT, "w") as file:
|
||||||
|
file.write("")
|
18
app/scodoc/html_sidebar.py
Normal file → Executable file
@ -54,7 +54,7 @@ def sidebar_common():
|
|||||||
<h2 class="insidebar">Scolarité</h2>
|
<h2 class="insidebar">Scolarité</h2>
|
||||||
<a href="{scu.ScoURL()}" class="sidebar">Semestres</a> <br>
|
<a href="{scu.ScoURL()}" class="sidebar">Semestres</a> <br>
|
||||||
<a href="{scu.NotesURL()}" class="sidebar">Programmes</a> <br>
|
<a href="{scu.NotesURL()}" class="sidebar">Programmes</a> <br>
|
||||||
<a href="{scu.AbsencesURL()}" class="sidebar">Absences</a> <br>
|
<a href="{scu.AssiduitesURL()}" class="sidebar">Assiduités</a> <br>
|
||||||
"""
|
"""
|
||||||
]
|
]
|
||||||
if current_user.has_permission(
|
if current_user.has_permission(
|
||||||
@ -76,7 +76,7 @@ def sidebar_common():
|
|||||||
def sidebar(etudid: int = None):
|
def sidebar(etudid: int = None):
|
||||||
"Main HTML page sidebar"
|
"Main HTML page sidebar"
|
||||||
# rewritten from legacy DTML code
|
# rewritten from legacy DTML code
|
||||||
from app.scodoc import sco_abs
|
from app.scodoc import sco_assiduites
|
||||||
from app.scodoc import sco_etud
|
from app.scodoc import sco_etud
|
||||||
|
|
||||||
params = {}
|
params = {}
|
||||||
@ -116,19 +116,18 @@ def sidebar(etudid: int = None):
|
|||||||
)
|
)
|
||||||
if etud["cursem"]:
|
if etud["cursem"]:
|
||||||
cur_sem = etud["cursem"]
|
cur_sem = etud["cursem"]
|
||||||
nbabs, nbabsjust = sco_abs.get_abs_count(etudid, cur_sem)
|
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, cur_sem)
|
||||||
nbabsnj = nbabs - nbabsjust
|
nbabsnj = nbabs - nbabsjust
|
||||||
H.append(
|
H.append(
|
||||||
f"""<span title="absences du { cur_sem["date_debut"] } au { cur_sem["date_fin"] }">(1/2 j.)
|
f"""<span title="absences du { cur_sem["date_debut"] } au { cur_sem["date_fin"] }">({sco_preferences.get_preference("assi_metrique", None)})
|
||||||
<br>{ nbabsjust } J., { nbabsnj } N.J.</span>"""
|
<br>{ nbabsjust } J., { nbabsnj } N.J.</span>"""
|
||||||
)
|
)
|
||||||
H.append("<ul>")
|
H.append("<ul>")
|
||||||
if current_user.has_permission(Permission.ScoAbsChange):
|
if current_user.has_permission(Permission.ScoAbsChange):
|
||||||
H.append(
|
H.append(
|
||||||
f"""
|
f"""
|
||||||
<li><a href="{ url_for('absences.SignaleAbsenceEtud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Ajouter</a></li>
|
<li><a href="{ url_for('assiduites.signal_assiduites_etud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Ajouter</a></li>
|
||||||
<li><a href="{ url_for('absences.JustifAbsenceEtud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Justifier</a></li>
|
<li><a href="{ url_for('assiduites.ajout_justificatif_etud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Justifier</a></li>
|
||||||
<li><a href="{ url_for('absences.AnnuleAbsenceEtud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Supprimer</a></li>
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
if sco_preferences.get_preference("handle_billets_abs"):
|
if sco_preferences.get_preference("handle_billets_abs"):
|
||||||
@ -137,8 +136,9 @@ def sidebar(etudid: int = None):
|
|||||||
)
|
)
|
||||||
H.append(
|
H.append(
|
||||||
f"""
|
f"""
|
||||||
<li><a href="{ url_for('absences.CalAbs', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Calendrier</a></li>
|
<li><a href="{ url_for('assiduites.calendrier_etud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Calendrier</a></li>
|
||||||
<li><a href="{ url_for('absences.ListeAbsEtud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Liste</a></li>
|
<li><a href="{ url_for('assiduites.liste_assiduites_etud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Liste</a></li>
|
||||||
|
<li><a href="{ url_for('assiduites.bilan_etud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Bilan</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
0
app/scodoc/sco_abs.py
Normal file → Executable file
@ -47,6 +47,7 @@ import app.scodoc.notesdb as ndb
|
|||||||
from app.scodoc import sco_etud
|
from app.scodoc import sco_etud
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
from app.scodoc import sco_users
|
from app.scodoc import sco_users
|
||||||
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
def abs_notify(etudid, date):
|
def abs_notify(etudid, date):
|
||||||
@ -55,14 +56,21 @@ def abs_notify(etudid, date):
|
|||||||
(s'il n'y a pas de semestre courant, ne fait rien,
|
(s'il n'y a pas de semestre courant, ne fait rien,
|
||||||
car l'etudiant n'est pas inscrit au moment de l'absence!).
|
car l'etudiant n'est pas inscrit au moment de l'absence!).
|
||||||
"""
|
"""
|
||||||
from app.scodoc import sco_abs
|
from app.scodoc import sco_assiduites
|
||||||
|
|
||||||
formsemestre = retreive_current_formsemestre(etudid, date)
|
formsemestre = retreive_current_formsemestre(etudid, date)
|
||||||
if not formsemestre:
|
if not formsemestre:
|
||||||
return # non inscrit a la date, pas de notification
|
return # non inscrit a la date, pas de notification
|
||||||
|
|
||||||
nbabs, nbabsjust = sco_abs.get_abs_count_in_interval(
|
nbabs, nbabsjust = sco_assiduites.get_assiduites_count_in_interval(
|
||||||
etudid, formsemestre.date_debut.isoformat(), formsemestre.date_fin.isoformat()
|
etudid,
|
||||||
|
formsemestre.date_debut.isoformat(),
|
||||||
|
formsemestre.date_fin.isoformat(),
|
||||||
|
scu.translate_assiduites_metric(
|
||||||
|
sco_preferences.get_preference(
|
||||||
|
"assi_metrique", formsemestre.formsemestre_id
|
||||||
|
)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
do_abs_notify(formsemestre, etudid, date, nbabs, nbabsjust)
|
do_abs_notify(formsemestre, etudid, date, nbabs, nbabsjust)
|
||||||
|
|
||||||
@ -85,6 +93,7 @@ def do_abs_notify(formsemestre: FormSemestre, etudid, date, nbabs, nbabsjust):
|
|||||||
return # abort
|
return # abort
|
||||||
|
|
||||||
# Vérification fréquence (pour ne pas envoyer de mails trop souvent)
|
# Vérification fréquence (pour ne pas envoyer de mails trop souvent)
|
||||||
|
# TODO Mettre la fréquence dans les préférences assiduités
|
||||||
abs_notify_max_freq = sco_preferences.get_preference("abs_notify_max_freq")
|
abs_notify_max_freq = sco_preferences.get_preference("abs_notify_max_freq")
|
||||||
destinations_filtered = []
|
destinations_filtered = []
|
||||||
for email_addr in destinations:
|
for email_addr in destinations:
|
||||||
@ -174,6 +183,8 @@ def abs_notify_is_above_threshold(etudid, nbabs, nbabsjust, formsemestre_id):
|
|||||||
|
|
||||||
(nbabs > abs_notify_abs_threshold)
|
(nbabs > abs_notify_abs_threshold)
|
||||||
(nbabs - nbabs_last_notified) > abs_notify_abs_increment
|
(nbabs - nbabs_last_notified) > abs_notify_abs_increment
|
||||||
|
|
||||||
|
TODO Mettre à jour avec le module assiduité + fonctionnement métrique
|
||||||
"""
|
"""
|
||||||
abs_notify_abs_threshold = sco_preferences.get_preference(
|
abs_notify_abs_threshold = sco_preferences.get_preference(
|
||||||
"abs_notify_abs_threshold", formsemestre_id
|
"abs_notify_abs_threshold", formsemestre_id
|
||||||
|
@ -68,7 +68,7 @@ from app import log, ScoDocJSONEncoder
|
|||||||
from app.but import jury_but_pv
|
from app.but import jury_but_pv
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
from app.comp.res_compat import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
from app.models import Departement, FormSemestre
|
from app.models import FormSemestre
|
||||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||||
from app.scodoc.sco_exceptions import ScoException, ScoPermissionDenied
|
from app.scodoc.sco_exceptions import ScoException, ScoPermissionDenied
|
||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
@ -86,6 +86,11 @@ class BaseArchiver(object):
|
|||||||
self.archive_type = archive_type
|
self.archive_type = archive_type
|
||||||
self.initialized = False
|
self.initialized = False
|
||||||
self.root = None
|
self.root = None
|
||||||
|
self.dept_id = None
|
||||||
|
|
||||||
|
def set_dept_id(self, dept_id: int):
|
||||||
|
"set dept"
|
||||||
|
self.dept_id = dept_id
|
||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
if self.initialized:
|
if self.initialized:
|
||||||
@ -107,6 +112,8 @@ class BaseArchiver(object):
|
|||||||
finally:
|
finally:
|
||||||
scu.GSL.release()
|
scu.GSL.release()
|
||||||
self.initialized = True
|
self.initialized = True
|
||||||
|
if self.dept_id is None:
|
||||||
|
self.dept_id = getattr(g, "scodoc_dept_id")
|
||||||
|
|
||||||
def get_obj_dir(self, oid: int):
|
def get_obj_dir(self, oid: int):
|
||||||
"""
|
"""
|
||||||
@ -114,8 +121,7 @@ class BaseArchiver(object):
|
|||||||
If directory does not yet exist, create it.
|
If directory does not yet exist, create it.
|
||||||
"""
|
"""
|
||||||
self.initialize()
|
self.initialize()
|
||||||
dept = Departement.query.filter_by(acronym=g.scodoc_dept).first()
|
dept_dir = os.path.join(self.root, str(self.dept_id))
|
||||||
dept_dir = os.path.join(self.root, str(dept.id))
|
|
||||||
try:
|
try:
|
||||||
scu.GSL.acquire()
|
scu.GSL.acquire()
|
||||||
if not os.path.isdir(dept_dir):
|
if not os.path.isdir(dept_dir):
|
||||||
@ -140,8 +146,7 @@ class BaseArchiver(object):
|
|||||||
:return: list of archive oids
|
:return: list of archive oids
|
||||||
"""
|
"""
|
||||||
self.initialize()
|
self.initialize()
|
||||||
dept = Departement.query.filter_by(acronym=g.scodoc_dept).first()
|
base = os.path.join(self.root, str(self.dept_id)) + os.path.sep
|
||||||
base = os.path.join(self.root, str(dept.id)) + os.path.sep
|
|
||||||
dirs = glob.glob(base + "*")
|
dirs = glob.glob(base + "*")
|
||||||
return [os.path.split(x)[1] for x in dirs]
|
return [os.path.split(x)[1] for x in dirs]
|
||||||
|
|
||||||
|
231
app/scodoc/sco_archives_justificatifs.py
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
"""
|
||||||
|
Gestion de l'archivage des justificatifs
|
||||||
|
|
||||||
|
Ecrit par Matthias HARTMANN
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from shutil import rmtree
|
||||||
|
|
||||||
|
from app.models import Identite
|
||||||
|
from app.scodoc.sco_archives import BaseArchiver
|
||||||
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
|
from app.scodoc.sco_utils import is_iso_formated
|
||||||
|
|
||||||
|
|
||||||
|
class Trace:
|
||||||
|
"""gestionnaire de la trace des fichiers justificatifs"""
|
||||||
|
|
||||||
|
def __init__(self, path: str) -> None:
|
||||||
|
self.path: str = path + "/_trace.csv"
|
||||||
|
self.content: dict[str, list[datetime, datetime, str]] = {}
|
||||||
|
self.import_from_file()
|
||||||
|
|
||||||
|
def import_from_file(self):
|
||||||
|
"""import trace from file"""
|
||||||
|
if os.path.isfile(self.path):
|
||||||
|
with open(self.path, "r", encoding="utf-8") as file:
|
||||||
|
for line in file.readlines():
|
||||||
|
csv = line.split(",")
|
||||||
|
if len(csv) < 4:
|
||||||
|
continue
|
||||||
|
fname: str = csv[0]
|
||||||
|
entry_date: datetime = is_iso_formated(csv[1], True)
|
||||||
|
delete_date: datetime = is_iso_formated(csv[2], True)
|
||||||
|
user_id = csv[3]
|
||||||
|
|
||||||
|
self.content[fname] = [entry_date, delete_date, user_id]
|
||||||
|
|
||||||
|
def set_trace(self, *fnames: str, mode: str = "entry", current_user: str = None):
|
||||||
|
"""Ajoute une trace du fichier donné
|
||||||
|
mode : entry / delete
|
||||||
|
"""
|
||||||
|
modes: list[str] = ["entry", "delete", "user_id"]
|
||||||
|
for fname in fnames:
|
||||||
|
if fname in modes:
|
||||||
|
continue
|
||||||
|
traced: list[datetime, datetime, str] = self.content.get(fname, False)
|
||||||
|
if not traced:
|
||||||
|
self.content[fname] = [None, None, None]
|
||||||
|
traced = self.content[fname]
|
||||||
|
|
||||||
|
traced[modes.index(mode)] = (
|
||||||
|
datetime.now() if mode != "user_id" else current_user
|
||||||
|
)
|
||||||
|
self.save_trace()
|
||||||
|
|
||||||
|
def save_trace(self):
|
||||||
|
"""Enregistre la trace dans le fichier _trace.csv"""
|
||||||
|
lines: list[str] = []
|
||||||
|
for fname, traced in self.content.items():
|
||||||
|
date_fin: datetime or None = traced[1].isoformat() if traced[1] else "None"
|
||||||
|
if traced[0] is not None:
|
||||||
|
lines.append(f"{fname},{traced[0].isoformat()},{date_fin}, {traced[2]}")
|
||||||
|
with open(self.path, "w", encoding="utf-8") as file:
|
||||||
|
file.write("\n".join(lines))
|
||||||
|
|
||||||
|
def get_trace(
|
||||||
|
self, fnames: list[str] = None
|
||||||
|
) -> dict[str, list[datetime, datetime, str]]:
|
||||||
|
"""Récupère la trace pour les noms de fichiers.
|
||||||
|
si aucun nom n'est donné, récupère tous les fichiers"""
|
||||||
|
|
||||||
|
if fnames is None:
|
||||||
|
return self.content
|
||||||
|
|
||||||
|
traced: dict = {}
|
||||||
|
for fname in fnames:
|
||||||
|
traced[fname] = self.content.get(fname, None)
|
||||||
|
|
||||||
|
return traced
|
||||||
|
|
||||||
|
|
||||||
|
class JustificatifArchiver(BaseArchiver):
|
||||||
|
"""
|
||||||
|
|
||||||
|
TOTALK:
|
||||||
|
- oid -> etudid
|
||||||
|
- archive_id -> date de création de l'archive (une archive par dépot de document)
|
||||||
|
|
||||||
|
justificatif
|
||||||
|
└── <dept_id>
|
||||||
|
└── <etudid/oid>
|
||||||
|
├── [_trace.csv]
|
||||||
|
└── <archive_id>
|
||||||
|
├── [_description.txt]
|
||||||
|
└── [<filename.ext>]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
BaseArchiver.__init__(self, archive_type="justificatifs")
|
||||||
|
|
||||||
|
def save_justificatif(
|
||||||
|
self,
|
||||||
|
etudid: int,
|
||||||
|
filename: str,
|
||||||
|
data: bytes or str,
|
||||||
|
archive_name: str = None,
|
||||||
|
description: str = "",
|
||||||
|
user_id: str = None,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Ajoute un fichier dans une archive "justificatif" pour l'etudid donné
|
||||||
|
Retourne l'archive_name utilisé
|
||||||
|
"""
|
||||||
|
self._set_dept(etudid)
|
||||||
|
if archive_name is None:
|
||||||
|
archive_id: str = self.create_obj_archive(
|
||||||
|
oid=etudid, description=description
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
archive_id: str = self.get_id_from_name(etudid, archive_name)
|
||||||
|
|
||||||
|
fname: str = self.store(archive_id, filename, data)
|
||||||
|
|
||||||
|
trace = Trace(self.get_obj_dir(etudid))
|
||||||
|
trace.set_trace(fname, mode="entry")
|
||||||
|
if user_id is not None:
|
||||||
|
trace.set_trace(fname, mode="user_id", current_user=user_id)
|
||||||
|
|
||||||
|
return self.get_archive_name(archive_id), fname
|
||||||
|
|
||||||
|
def delete_justificatif(
|
||||||
|
self,
|
||||||
|
etudid: int,
|
||||||
|
archive_name: str,
|
||||||
|
filename: str = None,
|
||||||
|
has_trace: bool = True,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Supprime une archive ou un fichier particulier de l'archivage de l'étudiant donné
|
||||||
|
|
||||||
|
Si trace == True : sauvegarde le nom du/des fichier(s) supprimé(s) dans la trace de l'étudiant
|
||||||
|
"""
|
||||||
|
self._set_dept(etudid)
|
||||||
|
if str(etudid) not in self.list_oids():
|
||||||
|
raise ValueError(f"Aucune archive pour etudid[{etudid}]")
|
||||||
|
|
||||||
|
archive_id = self.get_id_from_name(etudid, archive_name)
|
||||||
|
|
||||||
|
if filename is not None:
|
||||||
|
if filename not in self.list_archive(archive_id):
|
||||||
|
raise ValueError(
|
||||||
|
f"filename {filename} inconnu dans l'archive archive_id[{archive_id}] -> etudid[{etudid}]"
|
||||||
|
)
|
||||||
|
|
||||||
|
path: str = os.path.join(self.get_obj_dir(etudid), archive_id, filename)
|
||||||
|
|
||||||
|
if os.path.isfile(path):
|
||||||
|
if has_trace:
|
||||||
|
trace = Trace(self.get_obj_dir(etudid))
|
||||||
|
trace.set_trace(filename, mode="delete")
|
||||||
|
os.remove(path)
|
||||||
|
|
||||||
|
else:
|
||||||
|
if has_trace:
|
||||||
|
trace = Trace(self.get_obj_dir(etudid))
|
||||||
|
trace.set_trace(*self.list_archive(archive_id), mode="delete")
|
||||||
|
|
||||||
|
self.delete_archive(
|
||||||
|
os.path.join(
|
||||||
|
self.get_obj_dir(etudid),
|
||||||
|
archive_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def list_justificatifs(
|
||||||
|
self, archive_name: str, etudid: int
|
||||||
|
) -> list[tuple[str, int]]:
|
||||||
|
"""
|
||||||
|
Retourne la liste des noms de fichiers dans l'archive donnée
|
||||||
|
"""
|
||||||
|
self._set_dept(etudid)
|
||||||
|
filenames: list[str] = []
|
||||||
|
archive_id = self.get_id_from_name(etudid, archive_name)
|
||||||
|
|
||||||
|
filenames = self.list_archive(archive_id)
|
||||||
|
trace: Trace = Trace(self.get_obj_dir(etudid))
|
||||||
|
traced = trace.get_trace(filenames)
|
||||||
|
retour = [(key, value[2]) for key, value in traced.items()]
|
||||||
|
|
||||||
|
return retour
|
||||||
|
|
||||||
|
def get_justificatif_file(self, archive_name: str, etudid: int, filename: str):
|
||||||
|
"""
|
||||||
|
Retourne une réponse de téléchargement de fichier si le fichier existe
|
||||||
|
"""
|
||||||
|
self._set_dept(etudid)
|
||||||
|
archive_id: str = self.get_id_from_name(etudid, archive_name)
|
||||||
|
if filename in self.list_archive(archive_id):
|
||||||
|
return self.get_archived_file(etudid, archive_name, filename)
|
||||||
|
raise ScoValueError(
|
||||||
|
f"Fichier {filename} introuvable dans l'archive {archive_name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _set_dept(self, etudid: int):
|
||||||
|
"""
|
||||||
|
Mets à jour le dept_id de l'archiver en fonction du département de l'étudiant
|
||||||
|
"""
|
||||||
|
etud: Identite = Identite.query.filter_by(id=etudid).first()
|
||||||
|
self.set_dept_id(etud.dept_id)
|
||||||
|
|
||||||
|
def remove_dept_archive(self, dept_id: int = None):
|
||||||
|
"""
|
||||||
|
Supprime toutes les archives d'un département (ou de tous les départements)
|
||||||
|
⚠ Supprime aussi les fichiers de trace ⚠
|
||||||
|
"""
|
||||||
|
self.set_dept_id(1)
|
||||||
|
self.initialize()
|
||||||
|
|
||||||
|
if dept_id is None:
|
||||||
|
rmtree(self.root, ignore_errors=True)
|
||||||
|
else:
|
||||||
|
rmtree(os.path.join(self.root, str(dept_id)), ignore_errors=True)
|
||||||
|
|
||||||
|
def get_trace(
|
||||||
|
self, etudid: int, *fnames: str
|
||||||
|
) -> dict[str, list[datetime, datetime]]:
|
||||||
|
"""Récupère la trace des justificatifs de l'étudiant"""
|
||||||
|
trace = Trace(self.get_obj_dir(etudid))
|
||||||
|
return trace.get_trace(fnames)
|
500
app/scodoc/sco_assiduites.py
Normal file
@ -0,0 +1,500 @@
|
|||||||
|
"""
|
||||||
|
Ecrit par Matthias Hartmann.
|
||||||
|
"""
|
||||||
|
from datetime import date, datetime, time, timedelta
|
||||||
|
from pytz import UTC
|
||||||
|
|
||||||
|
import app.scodoc.sco_utils as scu
|
||||||
|
from app.models.assiduites import Assiduite, Justificatif
|
||||||
|
from app.models.etudiants import Identite
|
||||||
|
from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
||||||
|
from app.scodoc import sco_formsemestre_inscriptions
|
||||||
|
from app.scodoc import sco_preferences
|
||||||
|
from app.scodoc import sco_cache
|
||||||
|
from app.scodoc import sco_etud
|
||||||
|
|
||||||
|
|
||||||
|
class CountCalculator:
|
||||||
|
"""Classe qui gére le comptage des assiduités"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
morning: time = time(8, 0),
|
||||||
|
noon: time = time(12, 0),
|
||||||
|
after_noon: time = time(14, 00),
|
||||||
|
evening: time = time(18, 0),
|
||||||
|
skip_saturday: bool = True,
|
||||||
|
) -> None:
|
||||||
|
self.morning: time = morning
|
||||||
|
self.noon: time = noon
|
||||||
|
self.after_noon: time = after_noon
|
||||||
|
self.evening: time = evening
|
||||||
|
self.skip_saturday: bool = skip_saturday
|
||||||
|
|
||||||
|
delta_total: timedelta = datetime.combine(date.min, evening) - datetime.combine(
|
||||||
|
date.min, morning
|
||||||
|
)
|
||||||
|
delta_lunch: timedelta = datetime.combine(
|
||||||
|
date.min, after_noon
|
||||||
|
) - datetime.combine(date.min, noon)
|
||||||
|
|
||||||
|
self.hour_per_day: float = (delta_total - delta_lunch).total_seconds() / 3600
|
||||||
|
|
||||||
|
self.days: list[date] = []
|
||||||
|
self.half_days: list[tuple[date, bool]] = [] # tuple -> (date, morning:bool)
|
||||||
|
self.hours: float = 0.0
|
||||||
|
|
||||||
|
self.count: int = 0
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
"""Remet à zero le compteur"""
|
||||||
|
self.days = []
|
||||||
|
self.half_days = []
|
||||||
|
self.hours = 0.0
|
||||||
|
self.count = 0
|
||||||
|
|
||||||
|
def add_half_day(self, day: date, is_morning: bool = True):
|
||||||
|
"""Ajoute une demi journée dans le comptage"""
|
||||||
|
key: tuple[date, bool] = (day, is_morning)
|
||||||
|
if key not in self.half_days:
|
||||||
|
self.half_days.append(key)
|
||||||
|
|
||||||
|
def add_day(self, day: date):
|
||||||
|
"""Ajoute un jour dans le comptage"""
|
||||||
|
if day not in self.days:
|
||||||
|
self.days.append(day)
|
||||||
|
|
||||||
|
def check_in_morning(self, period: tuple[datetime, datetime]) -> bool:
|
||||||
|
"""Vérifiée si la période donnée fait partie du matin
|
||||||
|
(Test sur la date de début)
|
||||||
|
"""
|
||||||
|
|
||||||
|
interval_morning: tuple[datetime, datetime] = (
|
||||||
|
scu.localize_datetime(datetime.combine(period[0].date(), self.morning)),
|
||||||
|
scu.localize_datetime(datetime.combine(period[0].date(), self.noon)),
|
||||||
|
)
|
||||||
|
|
||||||
|
in_morning: bool = scu.is_period_overlapping(
|
||||||
|
period, interval_morning, bornes=False
|
||||||
|
)
|
||||||
|
return in_morning
|
||||||
|
|
||||||
|
def check_in_evening(self, period: tuple[datetime, datetime]) -> bool:
|
||||||
|
"""Vérifie si la période fait partie de l'aprèm
|
||||||
|
(test sur la date de début)
|
||||||
|
"""
|
||||||
|
|
||||||
|
interval_evening: tuple[datetime, datetime] = (
|
||||||
|
scu.localize_datetime(datetime.combine(period[0].date(), self.after_noon)),
|
||||||
|
scu.localize_datetime(datetime.combine(period[0].date(), self.evening)),
|
||||||
|
)
|
||||||
|
|
||||||
|
in_evening: bool = scu.is_period_overlapping(period, interval_evening)
|
||||||
|
|
||||||
|
return in_evening
|
||||||
|
|
||||||
|
def compute_long_assiduite(self, assi: Assiduite):
|
||||||
|
"""Calcule les métriques sur une assiduité longue (plus d'un jour)"""
|
||||||
|
|
||||||
|
pointer_date: date = assi.date_debut.date() + timedelta(days=1)
|
||||||
|
start_hours: timedelta = assi.date_debut - scu.localize_datetime(
|
||||||
|
datetime.combine(assi.date_debut, self.morning)
|
||||||
|
)
|
||||||
|
finish_hours: timedelta = assi.date_fin - scu.localize_datetime(
|
||||||
|
datetime.combine(assi.date_fin, self.morning)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.add_day(assi.date_debut.date())
|
||||||
|
self.add_day(assi.date_fin.date())
|
||||||
|
|
||||||
|
start_period: tuple[datetime, datetime] = (
|
||||||
|
assi.date_debut,
|
||||||
|
scu.localize_datetime(
|
||||||
|
datetime.combine(assi.date_debut.date(), self.evening)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
finish_period: tuple[datetime, datetime] = (
|
||||||
|
scu.localize_datetime(datetime.combine(assi.date_fin.date(), self.morning)),
|
||||||
|
assi.date_fin,
|
||||||
|
)
|
||||||
|
hours = 0.0
|
||||||
|
for period in (start_period, finish_period):
|
||||||
|
if self.check_in_evening(period):
|
||||||
|
self.add_half_day(period[0].date(), False)
|
||||||
|
if self.check_in_morning(period):
|
||||||
|
self.add_half_day(period[0].date())
|
||||||
|
|
||||||
|
while pointer_date < assi.date_fin.date():
|
||||||
|
# TODO : Utiliser la préférence de département : workdays
|
||||||
|
if pointer_date.weekday() < (6 - self.skip_saturday):
|
||||||
|
self.add_day(pointer_date)
|
||||||
|
self.add_half_day(pointer_date)
|
||||||
|
self.add_half_day(pointer_date, False)
|
||||||
|
self.hours += self.hour_per_day
|
||||||
|
hours += self.hour_per_day
|
||||||
|
|
||||||
|
pointer_date += timedelta(days=1)
|
||||||
|
|
||||||
|
self.hours += finish_hours.total_seconds() / 3600
|
||||||
|
self.hours += self.hour_per_day - (start_hours.total_seconds() / 3600)
|
||||||
|
|
||||||
|
def compute_assiduites(self, assiduites: Assiduite):
|
||||||
|
"""Calcule les métriques pour la collection d'assiduité donnée"""
|
||||||
|
assi: Assiduite
|
||||||
|
assiduites: list[Assiduite] = (
|
||||||
|
assiduites.all() if isinstance(assiduites, Assiduite) else assiduites
|
||||||
|
)
|
||||||
|
for assi in assiduites:
|
||||||
|
self.count += 1
|
||||||
|
delta: timedelta = assi.date_fin - assi.date_debut
|
||||||
|
|
||||||
|
if delta.days > 0:
|
||||||
|
# raise Exception(self.hours)
|
||||||
|
self.compute_long_assiduite(assi)
|
||||||
|
|
||||||
|
continue
|
||||||
|
|
||||||
|
period: tuple[datetime, datetime] = (assi.date_debut, assi.date_fin)
|
||||||
|
deb_date: date = assi.date_debut.date()
|
||||||
|
if self.check_in_morning(period):
|
||||||
|
self.add_half_day(deb_date)
|
||||||
|
if self.check_in_evening(period):
|
||||||
|
self.add_half_day(deb_date, False)
|
||||||
|
|
||||||
|
self.add_day(deb_date)
|
||||||
|
|
||||||
|
self.hours += delta.total_seconds() / 3600
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, object]:
|
||||||
|
"""Retourne les métriques sous la forme d'un dictionnaire"""
|
||||||
|
return {
|
||||||
|
"compte": self.count,
|
||||||
|
"journee": len(self.days),
|
||||||
|
"demi": len(self.half_days),
|
||||||
|
"heure": round(self.hours, 2),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_assiduites_stats(
|
||||||
|
assiduites: Assiduite, metric: str = "all", filtered: dict[str, object] = None
|
||||||
|
) -> Assiduite:
|
||||||
|
"""Compte les assiduités en fonction des filtres"""
|
||||||
|
|
||||||
|
if filtered is not None:
|
||||||
|
deb, fin = None, None
|
||||||
|
for key in filtered:
|
||||||
|
if key == "etat":
|
||||||
|
assiduites = filter_assiduites_by_etat(assiduites, filtered[key])
|
||||||
|
elif key == "date_fin":
|
||||||
|
fin = filtered[key]
|
||||||
|
elif key == "date_debut":
|
||||||
|
deb = filtered[key]
|
||||||
|
elif key == "moduleimpl_id":
|
||||||
|
assiduites = filter_by_module_impl(assiduites, filtered[key])
|
||||||
|
elif key == "formsemestre":
|
||||||
|
assiduites = filter_by_formsemestre(assiduites, filtered[key])
|
||||||
|
elif key == "est_just":
|
||||||
|
assiduites = filter_assiduites_by_est_just(assiduites, filtered[key])
|
||||||
|
elif key == "user_id":
|
||||||
|
assiduites = filter_by_user_id(assiduites, filtered[key])
|
||||||
|
if (deb, fin) != (None, None):
|
||||||
|
assiduites = filter_by_date(assiduites, Assiduite, deb, fin)
|
||||||
|
|
||||||
|
calculator: CountCalculator = CountCalculator()
|
||||||
|
calculator.compute_assiduites(assiduites)
|
||||||
|
count: dict = calculator.to_dict()
|
||||||
|
|
||||||
|
metrics: list[str] = metric.split(",")
|
||||||
|
|
||||||
|
output: dict = {}
|
||||||
|
|
||||||
|
for key, val in count.items():
|
||||||
|
if key in metrics:
|
||||||
|
output[key] = val
|
||||||
|
return output if output else count
|
||||||
|
|
||||||
|
|
||||||
|
def filter_assiduites_by_etat(assiduites: Assiduite, etat: str) -> Assiduite:
|
||||||
|
"""
|
||||||
|
Filtrage d'une collection d'assiduites en fonction de leur état
|
||||||
|
"""
|
||||||
|
etats: list[str] = list(etat.split(","))
|
||||||
|
etats = [scu.EtatAssiduite.get(e, -1) for e in etats]
|
||||||
|
return assiduites.filter(Assiduite.etat.in_(etats))
|
||||||
|
|
||||||
|
|
||||||
|
def filter_assiduites_by_est_just(
|
||||||
|
assiduites: Assiduite, est_just: bool
|
||||||
|
) -> Justificatif:
|
||||||
|
"""
|
||||||
|
Filtrage d'une collection d'assiduites en fonction de s'ils sont justifiés
|
||||||
|
"""
|
||||||
|
return assiduites.filter_by(est_just=est_just)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_by_user_id(
|
||||||
|
collection: Assiduite or Justificatif,
|
||||||
|
user_id: int,
|
||||||
|
) -> Justificatif:
|
||||||
|
"""
|
||||||
|
Filtrage d'une collection en fonction de l'user_id
|
||||||
|
"""
|
||||||
|
return collection.filter_by(user_id=user_id)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_by_date(
|
||||||
|
collection: Assiduite or Justificatif,
|
||||||
|
collection_cls: Assiduite or Justificatif,
|
||||||
|
date_deb: datetime = None,
|
||||||
|
date_fin: datetime = None,
|
||||||
|
strict: bool = False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Filtrage d'une collection d'assiduites en fonction d'une date
|
||||||
|
"""
|
||||||
|
if date_deb is None:
|
||||||
|
date_deb = datetime.min
|
||||||
|
if date_fin is None:
|
||||||
|
date_fin = datetime.max
|
||||||
|
|
||||||
|
date_deb = scu.localize_datetime(date_deb)
|
||||||
|
date_fin = scu.localize_datetime(date_fin)
|
||||||
|
if not strict:
|
||||||
|
return collection.filter(
|
||||||
|
collection_cls.date_debut <= date_fin, collection_cls.date_fin >= date_deb
|
||||||
|
)
|
||||||
|
return collection.filter(
|
||||||
|
collection_cls.date_debut < date_fin, collection_cls.date_fin > date_deb
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_justificatifs_by_etat(
|
||||||
|
justificatifs: Justificatif, etat: str
|
||||||
|
) -> Justificatif:
|
||||||
|
"""
|
||||||
|
Filtrage d'une collection de justificatifs en fonction de leur état
|
||||||
|
"""
|
||||||
|
etats: list[str] = list(etat.split(","))
|
||||||
|
etats = [scu.EtatJustificatif.get(e, -1) for e in etats]
|
||||||
|
return justificatifs.filter(Justificatif.etat.in_(etats))
|
||||||
|
|
||||||
|
|
||||||
|
def filter_by_module_impl(
|
||||||
|
assiduites: Assiduite, module_impl_id: int or None
|
||||||
|
) -> Assiduite:
|
||||||
|
"""
|
||||||
|
Filtrage d'une collection d'assiduites en fonction de l'ID du module_impl
|
||||||
|
"""
|
||||||
|
return assiduites.filter(Assiduite.moduleimpl_id == module_impl_id)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_by_formsemestre(assiduites_query: Assiduite, formsemestre: FormSemestre):
|
||||||
|
"""
|
||||||
|
Filtrage d'une collection d'assiduites en fonction d'un formsemestre
|
||||||
|
"""
|
||||||
|
|
||||||
|
if formsemestre is None:
|
||||||
|
return assiduites_query.filter(False)
|
||||||
|
|
||||||
|
assiduites_query = (
|
||||||
|
assiduites_query.join(Identite, Assiduite.etudid == Identite.id)
|
||||||
|
.join(
|
||||||
|
FormSemestreInscription,
|
||||||
|
Identite.id == FormSemestreInscription.etudid,
|
||||||
|
)
|
||||||
|
.filter(FormSemestreInscription.formsemestre_id == formsemestre.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
form_date_debut = formsemestre.date_debut + timedelta(days=1)
|
||||||
|
form_date_fin = formsemestre.date_fin + timedelta(days=1)
|
||||||
|
|
||||||
|
assiduites_query = assiduites_query.filter(Assiduite.date_debut >= form_date_debut)
|
||||||
|
|
||||||
|
return assiduites_query.filter(Assiduite.date_fin <= form_date_fin)
|
||||||
|
|
||||||
|
|
||||||
|
def justifies(justi: Justificatif, obj: bool = False) -> list[int]:
|
||||||
|
"""
|
||||||
|
Retourne la liste des assiduite_id qui sont justifié par la justification
|
||||||
|
Une assiduité est justifiée si elle est COMPLETEMENT ou PARTIELLEMENT comprise dans la plage du justificatif
|
||||||
|
et que l'état du justificatif est "valide"
|
||||||
|
renvoie des id si obj == False, sinon les Assiduités
|
||||||
|
"""
|
||||||
|
|
||||||
|
if justi.etat != scu.EtatJustificatif.VALIDE:
|
||||||
|
return []
|
||||||
|
|
||||||
|
assiduites_query: Assiduite = Assiduite.query.join(
|
||||||
|
Justificatif, Assiduite.etudid == Justificatif.etudid
|
||||||
|
).filter(
|
||||||
|
Assiduite.date_debut <= justi.date_fin,
|
||||||
|
Assiduite.date_fin >= justi.date_debut,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not obj:
|
||||||
|
return [assi.id for assi in assiduites_query.all()]
|
||||||
|
|
||||||
|
return assiduites_query
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_justified(
|
||||||
|
etudid: int, date_deb: datetime = None, date_fin: datetime = None
|
||||||
|
) -> list[Assiduite]:
|
||||||
|
"""Retourne toutes les assiduités justifiées sur une période"""
|
||||||
|
|
||||||
|
if date_deb is None:
|
||||||
|
date_deb = datetime.min
|
||||||
|
if date_fin is None:
|
||||||
|
date_fin = datetime.max
|
||||||
|
|
||||||
|
date_deb = scu.localize_datetime(date_deb)
|
||||||
|
date_fin = scu.localize_datetime(date_fin)
|
||||||
|
justified = Assiduite.query.filter_by(est_just=True, etudid=etudid)
|
||||||
|
after = filter_by_date(
|
||||||
|
justified,
|
||||||
|
Assiduite,
|
||||||
|
date_deb,
|
||||||
|
date_fin,
|
||||||
|
)
|
||||||
|
return after
|
||||||
|
|
||||||
|
|
||||||
|
# Gestion du cache
|
||||||
|
def get_assiduites_count(etudid, sem):
|
||||||
|
"""Les comptes d'absences de cet étudiant dans ce semestre:
|
||||||
|
tuple (nb abs non justifiées, nb abs justifiées)
|
||||||
|
Utilise un cache.
|
||||||
|
"""
|
||||||
|
metrique = sco_preferences.get_preference("assi_metrique", sem["formsemestre_id"])
|
||||||
|
return get_assiduites_count_in_interval(
|
||||||
|
etudid,
|
||||||
|
sem["date_debut_iso"],
|
||||||
|
sem["date_fin_iso"],
|
||||||
|
scu.translate_assiduites_metric(metrique),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_assiduites_count_in_interval(
|
||||||
|
etudid, date_debut_iso, date_fin_iso, metrique="demi"
|
||||||
|
):
|
||||||
|
"""Les comptes d'absences de cet étudiant entre ces deux dates, incluses:
|
||||||
|
tuple (nb abs, nb abs justifiées)
|
||||||
|
Utilise un cache.
|
||||||
|
"""
|
||||||
|
key = (
|
||||||
|
str(etudid)
|
||||||
|
+ "_"
|
||||||
|
+ date_debut_iso
|
||||||
|
+ "_"
|
||||||
|
+ date_fin_iso
|
||||||
|
+ f"{metrique}_assiduites"
|
||||||
|
)
|
||||||
|
r = sco_cache.AbsSemEtudCache.get(key)
|
||||||
|
if not r:
|
||||||
|
date_debut: datetime.datetime = scu.is_iso_formated(date_debut_iso, True)
|
||||||
|
date_fin: datetime.datetime = scu.is_iso_formated(date_fin_iso, True)
|
||||||
|
|
||||||
|
assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid)
|
||||||
|
assiduites = assiduites.filter(Assiduite.etat == scu.EtatAssiduite.ABSENT)
|
||||||
|
justificatifs: Justificatif = Justificatif.query.filter_by(etudid=etudid)
|
||||||
|
|
||||||
|
assiduites = filter_by_date(assiduites, Assiduite, date_debut, date_fin)
|
||||||
|
justificatifs = filter_by_date(
|
||||||
|
justificatifs, Justificatif, date_debut, date_fin
|
||||||
|
)
|
||||||
|
|
||||||
|
calculator: CountCalculator = CountCalculator()
|
||||||
|
calculator.compute_assiduites(assiduites)
|
||||||
|
nb_abs: dict = calculator.to_dict()[metrique]
|
||||||
|
|
||||||
|
abs_just: list[Assiduite] = get_all_justified(etudid, date_debut, date_fin)
|
||||||
|
|
||||||
|
calculator.reset()
|
||||||
|
calculator.compute_assiduites(abs_just)
|
||||||
|
nb_abs_just: dict = calculator.to_dict()[metrique]
|
||||||
|
|
||||||
|
r = (nb_abs, nb_abs_just)
|
||||||
|
ans = sco_cache.AbsSemEtudCache.set(key, r)
|
||||||
|
if not ans:
|
||||||
|
log("warning: get_assiduites_count failed to cache")
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def invalidate_assiduites_count(etudid, sem):
|
||||||
|
"""Invalidate (clear) cached counts"""
|
||||||
|
date_debut = sem["date_debut_iso"]
|
||||||
|
date_fin = sem["date_fin_iso"]
|
||||||
|
for met in ["demi", "journee", "compte", "heure"]:
|
||||||
|
key = str(etudid) + "_" + date_debut + "_" + date_fin + f"{met}_assiduites"
|
||||||
|
sco_cache.AbsSemEtudCache.delete(key)
|
||||||
|
|
||||||
|
|
||||||
|
def invalidate_assiduites_count_sem(sem):
|
||||||
|
"""Invalidate (clear) cached abs counts for all the students of this semestre"""
|
||||||
|
inscriptions = (
|
||||||
|
sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits(
|
||||||
|
sem["formsemestre_id"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for ins in inscriptions:
|
||||||
|
invalidate_assiduites_count(ins["etudid"], sem)
|
||||||
|
|
||||||
|
|
||||||
|
def invalidate_assiduites_etud_date(etudid, date: datetime):
|
||||||
|
"""Doit etre appelé à chaque modification des assiduites pour cet étudiant et cette date.
|
||||||
|
Invalide cache absence et caches semestre
|
||||||
|
date: date au format ISO
|
||||||
|
"""
|
||||||
|
from app.scodoc import sco_compute_moy
|
||||||
|
|
||||||
|
# Semestres a cette date:
|
||||||
|
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)
|
||||||
|
if len(etud) == 0:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
etud = etud[0]
|
||||||
|
sems = [
|
||||||
|
sem
|
||||||
|
for sem in etud["sems"]
|
||||||
|
if scu.is_iso_formated(sem["date_debut_iso"], True).replace(tzinfo=UTC)
|
||||||
|
<= date.replace(tzinfo=UTC)
|
||||||
|
and scu.is_iso_formated(sem["date_fin_iso"], True).replace(tzinfo=UTC)
|
||||||
|
>= date.replace(tzinfo=UTC)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Invalide les PDF et les absences:
|
||||||
|
for sem in sems:
|
||||||
|
# Inval cache bulletin et/ou note_table
|
||||||
|
if sco_compute_moy.formsemestre_expressions_use_abscounts(
|
||||||
|
sem["formsemestre_id"]
|
||||||
|
):
|
||||||
|
# certaines formules utilisent les absences
|
||||||
|
pdfonly = False
|
||||||
|
else:
|
||||||
|
# efface toujours le PDF car il affiche en général les absences
|
||||||
|
pdfonly = True
|
||||||
|
|
||||||
|
sco_cache.invalidate_formsemestre(
|
||||||
|
formsemestre_id=sem["formsemestre_id"], pdfonly=pdfonly
|
||||||
|
)
|
||||||
|
|
||||||
|
# Inval cache compteurs absences:
|
||||||
|
invalidate_assiduites_count(etudid, sem)
|
||||||
|
|
||||||
|
|
||||||
|
def simple_invalidate_cache(obj: dict, etudid: str or int = None):
|
||||||
|
"""Invalide le cache de l'étudiant et du / des semestres"""
|
||||||
|
date_debut = (
|
||||||
|
obj["date_debut"]
|
||||||
|
if isinstance(obj["date_debut"], datetime)
|
||||||
|
else scu.is_iso_formated(obj["date_debut"], True)
|
||||||
|
)
|
||||||
|
date_fin = (
|
||||||
|
obj["date_fin"]
|
||||||
|
if isinstance(obj["date_fin"], datetime)
|
||||||
|
else scu.is_iso_formated(obj["date_fin"], True)
|
||||||
|
)
|
||||||
|
etudid = etudid if etudid is not None else obj["etudid"]
|
||||||
|
invalidate_assiduites_etud_date(etudid, date_debut)
|
||||||
|
invalidate_assiduites_etud_date(etudid, date_fin)
|
@ -56,7 +56,7 @@ from app.scodoc.sco_permissions import Permission
|
|||||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
from app.scodoc import htmlutils
|
from app.scodoc import htmlutils
|
||||||
from app.scodoc import sco_abs
|
from app.scodoc import sco_assiduites
|
||||||
from app.scodoc import sco_abs_views
|
from app.scodoc import sco_abs_views
|
||||||
from app.scodoc import sco_bulletins_generator
|
from app.scodoc import sco_bulletins_generator
|
||||||
from app.scodoc import sco_bulletins_json
|
from app.scodoc import sco_bulletins_json
|
||||||
@ -142,7 +142,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
|
|||||||
Cette fonction est utilisée pour les bulletins CLASSIQUES (DUT, ...)
|
Cette fonction est utilisée pour les bulletins CLASSIQUES (DUT, ...)
|
||||||
en HTML et PDF, mais pas ceux en XML.
|
en HTML et PDF, mais pas ceux en XML.
|
||||||
"""
|
"""
|
||||||
from app.scodoc import sco_abs
|
from app.scodoc import sco_assiduites
|
||||||
|
|
||||||
if version not in scu.BULLETINS_VERSIONS:
|
if version not in scu.BULLETINS_VERSIONS:
|
||||||
raise ValueError("invalid version code !")
|
raise ValueError("invalid version code !")
|
||||||
@ -197,7 +197,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
|
|||||||
pid = partition["partition_id"]
|
pid = partition["partition_id"]
|
||||||
partitions_etud_groups[pid] = sco_groups.get_etud_groups_in_partition(pid)
|
partitions_etud_groups[pid] = sco_groups.get_etud_groups_in_partition(pid)
|
||||||
# --- Absences
|
# --- Absences
|
||||||
I["nbabs"], I["nbabsjust"] = sco_abs.get_abs_count(etudid, nt.sem)
|
I["nbabs"], I["nbabsjust"] = sco_assiduites.get_assiduites_count(etudid, nt.sem)
|
||||||
|
|
||||||
# --- Decision Jury
|
# --- Decision Jury
|
||||||
infos, dpv = etud_descr_situation_semestre(
|
infos, dpv = etud_descr_situation_semestre(
|
||||||
@ -489,7 +489,7 @@ def _ue_mod_bulletin(
|
|||||||
) # peut etre 'NI'
|
) # peut etre 'NI'
|
||||||
is_malus = mod["module"]["module_type"] == ModuleType.MALUS
|
is_malus = mod["module"]["module_type"] == ModuleType.MALUS
|
||||||
if bul_show_abs_modules:
|
if bul_show_abs_modules:
|
||||||
nbabs, nbabsjust = sco_abs.get_abs_count(etudid, sem)
|
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem)
|
||||||
mod_abs = [nbabs, nbabsjust]
|
mod_abs = [nbabs, nbabsjust]
|
||||||
mod["mod_abs_txt"] = scu.fmt_abs(mod_abs)
|
mod["mod_abs_txt"] = scu.fmt_abs(mod_abs)
|
||||||
else:
|
else:
|
||||||
|
@ -43,7 +43,7 @@ from app.models.formsemestre import FormSemestre
|
|||||||
|
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
from app.scodoc import sco_abs
|
from app.scodoc import sco_assiduites
|
||||||
from app.scodoc import sco_edit_ue
|
from app.scodoc import sco_edit_ue
|
||||||
from app.scodoc import sco_evaluations
|
from app.scodoc import sco_evaluations
|
||||||
from app.scodoc import sco_evaluation_db
|
from app.scodoc import sco_evaluation_db
|
||||||
@ -297,7 +297,7 @@ def formsemestre_bulletinetud_published_dict(
|
|||||||
|
|
||||||
# --- Absences
|
# --- Absences
|
||||||
if prefs["bul_show_abs"]:
|
if prefs["bul_show_abs"]:
|
||||||
nbabs, nbabsjust = sco_abs.get_abs_count(etudid, sem)
|
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem)
|
||||||
d["absences"] = dict(nbabs=nbabs, nbabsjust=nbabsjust)
|
d["absences"] = dict(nbabs=nbabs, nbabsjust=nbabsjust)
|
||||||
|
|
||||||
# --- Décision Jury
|
# --- Décision Jury
|
||||||
|
@ -51,7 +51,7 @@ import app.scodoc.notesdb as ndb
|
|||||||
from app import log
|
from app import log
|
||||||
from app.but.bulletin_but_xml_compat import bulletin_but_xml_compat
|
from app.but.bulletin_but_xml_compat import bulletin_but_xml_compat
|
||||||
from app.models.formsemestre import FormSemestre
|
from app.models.formsemestre import FormSemestre
|
||||||
from app.scodoc import sco_abs
|
from app.scodoc import sco_assiduites
|
||||||
from app.scodoc import codes_cursus
|
from app.scodoc import codes_cursus
|
||||||
from app.scodoc import sco_edit_ue
|
from app.scodoc import sco_edit_ue
|
||||||
from app.scodoc import sco_evaluation_db
|
from app.scodoc import sco_evaluation_db
|
||||||
@ -63,6 +63,7 @@ from app.scodoc import sco_etud
|
|||||||
from app.scodoc import sco_xml
|
from app.scodoc import sco_xml
|
||||||
from app.scodoc.sco_xml import quote_xml_attr
|
from app.scodoc.sco_xml import quote_xml_attr
|
||||||
|
|
||||||
|
|
||||||
# -------- Bulletin en XML
|
# -------- Bulletin en XML
|
||||||
# (fonction séparée: n'utilise pas formsemestre_bulletinetud_dict()
|
# (fonction séparée: n'utilise pas formsemestre_bulletinetud_dict()
|
||||||
# pour simplifier le code, mais attention a la maintenance !)
|
# pour simplifier le code, mais attention a la maintenance !)
|
||||||
@ -369,7 +370,7 @@ def make_xml_formsemestre_bulletinetud(
|
|||||||
|
|
||||||
# --- Absences
|
# --- Absences
|
||||||
if sco_preferences.get_preference("bul_show_abs", formsemestre_id):
|
if sco_preferences.get_preference("bul_show_abs", formsemestre_id):
|
||||||
nbabs, nbabsjust = sco_abs.get_abs_count(etudid, sem)
|
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem)
|
||||||
doc.append(Element("absences", nbabs=str(nbabs), nbabsjust=str(nbabsjust)))
|
doc.append(Element("absences", nbabs=str(nbabs), nbabsjust=str(nbabsjust)))
|
||||||
# --- Decision Jury
|
# --- Decision Jury
|
||||||
if (
|
if (
|
||||||
|
@ -668,10 +668,13 @@ def evaluation_describe(evaluation_id="", edit_in_place=True, link_saisie=True):
|
|||||||
group_id = sco_groups.get_default_group(formsemestre_id)
|
group_id = sco_groups.get_default_group(formsemestre_id)
|
||||||
H.append(
|
H.append(
|
||||||
f"""<span class="noprint"><a href="{url_for(
|
f"""<span class="noprint"><a href="{url_for(
|
||||||
'absences.EtatAbsencesDate',
|
'assiduites.get_etat_abs_date',
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
group_ids=group_id,
|
group_ids=group_id,
|
||||||
date=E["jour"]
|
desc=E["description"],
|
||||||
|
jour=E["jour"],
|
||||||
|
heure_debut=E["heure_debut"],
|
||||||
|
heure_fin=E["heure_fin"],
|
||||||
)
|
)
|
||||||
}">(absences ce jour)</a></span>"""
|
}">(absences ce jour)</a></span>"""
|
||||||
)
|
)
|
||||||
|
49
app/scodoc/sco_formsemestre_status.py
Normal file → Executable file
@ -219,13 +219,14 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
|
|||||||
"enabled": True,
|
"enabled": True,
|
||||||
"helpmsg": "",
|
"helpmsg": "",
|
||||||
},
|
},
|
||||||
{
|
# TODO: Mettre à jour avec module Assiduités
|
||||||
"title": "Vérifier absences aux évaluations",
|
# {
|
||||||
"endpoint": "notes.formsemestre_check_absences_html",
|
# "title": "Vérifier absences aux évaluations",
|
||||||
"args": {"formsemestre_id": formsemestre_id},
|
# "endpoint": "notes.formsemestre_check_absences_html",
|
||||||
"enabled": True,
|
# "args": {"formsemestre_id": formsemestre_id},
|
||||||
"helpmsg": "",
|
# "enabled": True,
|
||||||
},
|
# "helpmsg": "",
|
||||||
|
# },
|
||||||
{
|
{
|
||||||
"title": "Lister tous les enseignants",
|
"title": "Lister tous les enseignants",
|
||||||
"endpoint": "notes.formsemestre_enseignants_list",
|
"endpoint": "notes.formsemestre_enseignants_list",
|
||||||
@ -842,32 +843,18 @@ def _make_listes_sem(formsemestre: FormSemestre, with_absences=True):
|
|||||||
).prev_monday()
|
).prev_monday()
|
||||||
form_abs_tmpl = f"""
|
form_abs_tmpl = f"""
|
||||||
<td>
|
<td>
|
||||||
<a href="%(url_etat)s">absences</a>
|
<a class="btn" href="{
|
||||||
</td>
|
url_for("assiduites.visu_assi_group", scodoc_dept=g.scodoc_dept)
|
||||||
<td>
|
}?group_ids=%(group_id)s&date_debut={formsemestre.date_debut.isoformat()}&date_fin={formsemestre.date_fin.isoformat()}"><button>Visualiser l'assiduité</button></a>
|
||||||
<form action="{url_for(
|
|
||||||
"absences.SignaleAbsenceGrSemestre", scodoc_dept=g.scodoc_dept
|
|
||||||
)}" method="get">
|
|
||||||
<input type="hidden" name="datefin" value="{
|
|
||||||
formsemestre.date_fin.strftime("%d/%m/%Y")}"/>
|
|
||||||
<input type="hidden" name="group_ids" value="%(group_id)s"/>
|
|
||||||
<input type="hidden" name="destination" value="{destination}"/>
|
|
||||||
<input type="submit" value="Saisir abs des" />
|
|
||||||
<select name="datedebut" class="noprint">
|
|
||||||
"""
|
"""
|
||||||
date = first_monday
|
|
||||||
for idx, jour in enumerate(sco_abs.day_names()):
|
|
||||||
form_abs_tmpl += f"""<option value="{date}" {
|
|
||||||
'selected' if idx == weekday else ''
|
|
||||||
}>{jour}s</option>"""
|
|
||||||
date = date.next_day()
|
|
||||||
form_abs_tmpl += f"""
|
form_abs_tmpl += f"""
|
||||||
</select>
|
<a class="btn" href="{
|
||||||
|
url_for("assiduites.signal_assiduites_group", scodoc_dept=g.scodoc_dept)
|
||||||
<a href="{
|
}?group_ids=%(group_id)s&jour={datetime.date.today().isoformat()}&formsemestre_id={formsemestre.id}"><button>Saisie Journalière</button></a>
|
||||||
url_for("absences.choix_semaine", scodoc_dept=g.scodoc_dept)
|
<a class="btn" href="{
|
||||||
}?group_id=%(group_id)s">saisie par semaine</a>
|
url_for("assiduites.signal_assiduites_diff", scodoc_dept=g.scodoc_dept)
|
||||||
</form></td>
|
}?group_ids=%(group_id)s&formsemestre_id={formsemestre.formsemestre_id}"><button>Saisie Différée</button></a>
|
||||||
|
</td>
|
||||||
"""
|
"""
|
||||||
else:
|
else:
|
||||||
form_abs_tmpl = ""
|
form_abs_tmpl = ""
|
||||||
|
@ -54,7 +54,7 @@ from app.scodoc.codes_cursus import *
|
|||||||
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
|
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
|
||||||
|
|
||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
from app.scodoc import sco_abs
|
from app.scodoc import sco_assiduites
|
||||||
from app.scodoc import codes_cursus
|
from app.scodoc import codes_cursus
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
from app.scodoc import sco_edit_ue
|
from app.scodoc import sco_edit_ue
|
||||||
@ -704,7 +704,7 @@ def formsemestre_recap_parcours_table(
|
|||||||
f"""<td class="rcp_moy">{scu.fmt_note(nt.get_etud_moy_gen(etudid))}</td>"""
|
f"""<td class="rcp_moy">{scu.fmt_note(nt.get_etud_moy_gen(etudid))}</td>"""
|
||||||
)
|
)
|
||||||
# Absences (nb d'abs non just. dans ce semestre)
|
# Absences (nb d'abs non just. dans ce semestre)
|
||||||
nbabs, nbabsjust = sco_abs.get_abs_count(etudid, sem)
|
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem)
|
||||||
H.append(f"""<td class="rcp_abs">{nbabs - nbabsjust}</td>""")
|
H.append(f"""<td class="rcp_abs">{nbabs - nbabsjust}</td>""")
|
||||||
|
|
||||||
# UEs
|
# UEs
|
||||||
|
@ -138,10 +138,13 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str:
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Absences ce jour",
|
"title": "Absences ce jour",
|
||||||
"endpoint": "absences.EtatAbsencesDate",
|
"endpoint": "assiduites.get_etat_abs_date",
|
||||||
"args": {
|
"args": {
|
||||||
"date": E["jour"],
|
|
||||||
"group_ids": group_id,
|
"group_ids": group_id,
|
||||||
|
"desc": E["description"],
|
||||||
|
"jour": E["jour"],
|
||||||
|
"heure_debut": E["heure_debut"],
|
||||||
|
"heure_fin": E["heure_fin"],
|
||||||
},
|
},
|
||||||
"enabled": E["jour"],
|
"enabled": E["jour"],
|
||||||
},
|
},
|
||||||
|
@ -57,6 +57,10 @@ _SCO_PERMISSIONS = (
|
|||||||
(1 << 29, "ScoUsersChangeCASId", "Paramétrer l'id CAS"),
|
(1 << 29, "ScoUsersChangeCASId", "Paramétrer l'id CAS"),
|
||||||
#
|
#
|
||||||
(1 << 40, "ScoEtudChangePhoto", "Modifier la photo d'un étudiant"),
|
(1 << 40, "ScoEtudChangePhoto", "Modifier la photo d'un étudiant"),
|
||||||
|
# Permissions du module Assiduité)
|
||||||
|
(1 << 50, "ScoAssiduiteChange", "Modifier des assiduités"),
|
||||||
|
(1 << 51, "ScoJustifChange", "Modifier des justificatifs"),
|
||||||
|
(1 << 52, "ScoJustifView", "Visualisation des fichiers justificatifs"),
|
||||||
# Attention: les permissions sont codées sur 64 bits.
|
# Attention: les permissions sont codées sur 64 bits.
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -71,7 +75,7 @@ class Permission:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def init_permissions():
|
def init_permissions():
|
||||||
for (perm, symbol, description) in _SCO_PERMISSIONS:
|
for perm, symbol, description in _SCO_PERMISSIONS:
|
||||||
setattr(Permission, symbol, perm)
|
setattr(Permission, symbol, perm)
|
||||||
Permission.description[symbol] = description
|
Permission.description[symbol] = description
|
||||||
Permission.permission_by_name[symbol] = perm
|
Permission.permission_by_name[symbol] = perm
|
||||||
|
4
app/scodoc/sco_photos.py
Normal file → Executable file
@ -148,11 +148,11 @@ def get_photo_image(etudid=None, size="small"):
|
|||||||
filename = photo_pathname(etud.photo_filename, size=size)
|
filename = photo_pathname(etud.photo_filename, size=size)
|
||||||
if not filename:
|
if not filename:
|
||||||
filename = UNKNOWN_IMAGE_PATH
|
filename = UNKNOWN_IMAGE_PATH
|
||||||
r = _http_jpeg_file(filename)
|
r = build_image_response(filename)
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
def _http_jpeg_file(filename):
|
def build_image_response(filename):
|
||||||
"""returns an image as a Flask response"""
|
"""returns an image as a Flask response"""
|
||||||
st = os.stat(filename)
|
st = os.stat(filename)
|
||||||
last_modified = st.st_mtime # float timestamp
|
last_modified = st.st_mtime # float timestamp
|
||||||
|
@ -37,7 +37,7 @@ from app.comp import res_sem
|
|||||||
from app.comp.res_compat import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
from app.models import FormSemestre
|
from app.models import FormSemestre
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app.scodoc import sco_abs
|
from app.scodoc import sco_assiduites
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
from app.scodoc import sco_formsemestre
|
from app.scodoc import sco_formsemestre
|
||||||
from app.scodoc import sco_groups
|
from app.scodoc import sco_groups
|
||||||
@ -107,7 +107,7 @@ def etud_get_poursuite_info(sem, etud):
|
|||||||
rangs.append(["rang_" + codeModule, rangModule])
|
rangs.append(["rang_" + codeModule, rangModule])
|
||||||
|
|
||||||
# Absences
|
# Absences
|
||||||
nbabs, nbabsjust = sco_abs.get_abs_count(etudid, nt.sem)
|
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, nt.sem)
|
||||||
if (
|
if (
|
||||||
dec
|
dec
|
||||||
and not sem_descr # not sem_descr pour ne prendre que le semestre validé le plus récent
|
and not sem_descr # not sem_descr pour ne prendre que le semestre validé le plus récent
|
||||||
|
@ -204,6 +204,7 @@ PREF_CATEGORIES = (
|
|||||||
("misc", {"title": "Divers"}),
|
("misc", {"title": "Divers"}),
|
||||||
("apc", {"title": "BUT et Approches par Compétences"}),
|
("apc", {"title": "BUT et Approches par Compétences"}),
|
||||||
("abs", {"title": "Suivi des absences", "related": ("bul",)}),
|
("abs", {"title": "Suivi des absences", "related": ("bul",)}),
|
||||||
|
("assi", {"title": "Gestion de l'assiduité"}),
|
||||||
("portal", {"title": "Liaison avec portail (Apogée, etc)"}),
|
("portal", {"title": "Liaison avec portail (Apogée, etc)"}),
|
||||||
("apogee", {"title": "Exports Apogée"}),
|
("apogee", {"title": "Exports Apogée"}),
|
||||||
(
|
(
|
||||||
@ -598,6 +599,85 @@ class BasePreferences(object):
|
|||||||
"category": "abs",
|
"category": "abs",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
# Assiduités
|
||||||
|
(
|
||||||
|
"forcer_module",
|
||||||
|
{
|
||||||
|
"initvalue": 0,
|
||||||
|
"title": "Forcer la déclaration du module.",
|
||||||
|
"input_type": "boolcheckbox",
|
||||||
|
"labels": ["non", "oui"],
|
||||||
|
"category": "assi",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"forcer_present",
|
||||||
|
{
|
||||||
|
"initvalue": 0,
|
||||||
|
"title": "Forcer l'appel des présents",
|
||||||
|
"input_type": "boolcheckbox",
|
||||||
|
"labels": ["non", "oui"],
|
||||||
|
"category": "assi",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"periode_defaut",
|
||||||
|
{
|
||||||
|
"initvalue": 2.0,
|
||||||
|
"size": 10,
|
||||||
|
"title": "Durée par défaut d'un créneau",
|
||||||
|
"type": "float",
|
||||||
|
"category": "assi",
|
||||||
|
"only_global": True,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"assi_etat_defaut",
|
||||||
|
{
|
||||||
|
"initvalue": "aucun",
|
||||||
|
"input_type": "menu",
|
||||||
|
"labels": ["aucun", "present", "retard", "absent"],
|
||||||
|
"allowed_values": ["aucun", "present", "retard", "absent"],
|
||||||
|
"title": "Définir l'état par défaut",
|
||||||
|
"category": "assi",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"non_travail",
|
||||||
|
{
|
||||||
|
"initvalue": "sam, dim",
|
||||||
|
"title": "Jours non travaillés",
|
||||||
|
"size": 40,
|
||||||
|
"category": "assi",
|
||||||
|
"only_global": True,
|
||||||
|
"explanation": "Liste des jours (lun,mar,mer,jeu,ven,sam,dim)",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"assi_metrique",
|
||||||
|
{
|
||||||
|
"initvalue": "1/2 J.",
|
||||||
|
"input_type": "menu",
|
||||||
|
"labels": ["1/2 J.", "J.", "H."],
|
||||||
|
"allowed_values": ["1/2 J.", "J.", "H."],
|
||||||
|
"title": "Métrique de l'assiduité",
|
||||||
|
"explanation": "Unité utilisée dans la fiche étudiante, le bilan, et dans les calculs (J. = journée, H. = heure)",
|
||||||
|
"category": "assi",
|
||||||
|
"only_global": True,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"assi_seuil",
|
||||||
|
{
|
||||||
|
"initvalue": 3.0,
|
||||||
|
"size": 10,
|
||||||
|
"title": "Seuil d'alerte des absences",
|
||||||
|
"type": "float",
|
||||||
|
"explanation": "Nombres d'absences limite avant alerte dans le bilan (utilisation de l'unité métrique ↑ )",
|
||||||
|
"category": "assi",
|
||||||
|
"only_global": True,
|
||||||
|
},
|
||||||
|
),
|
||||||
# portal
|
# portal
|
||||||
(
|
(
|
||||||
"portal_url",
|
"portal_url",
|
||||||
@ -1700,7 +1780,7 @@ class BasePreferences(object):
|
|||||||
(
|
(
|
||||||
"feuille_releve_abs_taille",
|
"feuille_releve_abs_taille",
|
||||||
{
|
{
|
||||||
"initvalue": "A3",
|
"initvalue": "A4",
|
||||||
"input_type": "menu",
|
"input_type": "menu",
|
||||||
"labels": ["A3", "A4"],
|
"labels": ["A3", "A4"],
|
||||||
"allowed_values": ["A3", "A4"],
|
"allowed_values": ["A3", "A4"],
|
||||||
|
@ -39,7 +39,7 @@ from flask_login import current_user
|
|||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
from app.comp.res_compat import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
from app.models import FormSemestre, Identite, ScolarAutorisationInscription
|
from app.models import FormSemestre, Identite, ScolarAutorisationInscription
|
||||||
from app.scodoc import sco_abs
|
from app.scodoc import sco_assiduites
|
||||||
from app.scodoc import codes_cursus
|
from app.scodoc import codes_cursus
|
||||||
from app.scodoc import sco_groups
|
from app.scodoc import sco_groups
|
||||||
from app.scodoc import sco_etud
|
from app.scodoc import sco_etud
|
||||||
@ -139,7 +139,7 @@ def feuille_preparation_jury(formsemestre_id):
|
|||||||
main_partition_id, ""
|
main_partition_id, ""
|
||||||
)
|
)
|
||||||
# absences:
|
# absences:
|
||||||
e_nbabs, e_nbabsjust = sco_abs.get_abs_count(etud.id, sem)
|
e_nbabs, e_nbabsjust = sco_assiduites.get_assiduites_count(etud.id, sem)
|
||||||
nbabs[etud.id] = e_nbabs
|
nbabs[etud.id] = e_nbabs
|
||||||
nbabsjust[etud.id] = e_nbabs - e_nbabsjust
|
nbabsjust[etud.id] = e_nbabs - e_nbabsjust
|
||||||
|
|
||||||
|
@ -32,13 +32,14 @@ import base64
|
|||||||
import bisect
|
import bisect
|
||||||
import collections
|
import collections
|
||||||
import datetime
|
import datetime
|
||||||
from enum import IntEnum
|
from enum import IntEnum, Enum
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
import numbers
|
import numbers
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
from shutil import get_terminal_size
|
||||||
import _thread
|
import _thread
|
||||||
import time
|
import time
|
||||||
import unicodedata
|
import unicodedata
|
||||||
@ -50,6 +51,10 @@ from PIL import Image as PILImage
|
|||||||
import pydot
|
import pydot
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
from pytz import timezone
|
||||||
|
|
||||||
|
import dateutil.parser as dtparser
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
from flask import g, request, Response
|
from flask import g, request, Response
|
||||||
from flask import flash, url_for, make_response
|
from flask import flash, url_for, make_response
|
||||||
@ -91,6 +96,172 @@ ETATS_INSCRIPTION = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def print_progress_bar(
|
||||||
|
iteration,
|
||||||
|
total,
|
||||||
|
prefix="",
|
||||||
|
suffix="",
|
||||||
|
finish_msg="",
|
||||||
|
decimals=1,
|
||||||
|
length=100,
|
||||||
|
fill="█",
|
||||||
|
autosize=False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Affiche une progress bar à un point donné (mettre dans une boucle pour rendre dynamique)
|
||||||
|
@params:
|
||||||
|
iteration - Required : index du point donné (Int)
|
||||||
|
total - Required : nombre total avant complétion (eg: len(List))
|
||||||
|
prefix - Optional : Préfix -> écrit à gauche de la barre (Str)
|
||||||
|
suffix - Optional : Suffix -> écrit à droite de la barre (Str)
|
||||||
|
decimals - Optional : nombres de chiffres après la virgule (Int)
|
||||||
|
length - Optional : taille de la barre en nombre de caractères (Int)
|
||||||
|
fill - Optional : charactère de remplissange de la barre (Str)
|
||||||
|
autosize - Optional : Choisir automatiquement la taille de la barre en fonction du terminal (Bool)
|
||||||
|
"""
|
||||||
|
percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total)))
|
||||||
|
color = TerminalColor.RED
|
||||||
|
if 50 >= float(percent) > 25:
|
||||||
|
color = TerminalColor.MAGENTA
|
||||||
|
if 75 >= float(percent) > 50:
|
||||||
|
color = TerminalColor.BLUE
|
||||||
|
if 90 >= float(percent) > 75:
|
||||||
|
color = TerminalColor.CYAN
|
||||||
|
if 100 >= float(percent) > 90:
|
||||||
|
color = TerminalColor.GREEN
|
||||||
|
styling = f"{prefix} |{fill}| {percent}% {suffix}"
|
||||||
|
if autosize:
|
||||||
|
cols, _ = get_terminal_size(fallback=(length, 1))
|
||||||
|
length = cols - len(styling)
|
||||||
|
filled_length = int(length * iteration // total)
|
||||||
|
pg_bar = fill * filled_length + "-" * (length - filled_length)
|
||||||
|
print(f"\r{color}{styling.replace(fill, pg_bar)}{TerminalColor.RESET}", end="\r")
|
||||||
|
# Affiche une nouvelle ligne vide
|
||||||
|
if iteration == total:
|
||||||
|
print(f"\n{finish_msg}")
|
||||||
|
|
||||||
|
|
||||||
|
class TerminalColor:
|
||||||
|
"""Ensemble de couleur pour terminaux"""
|
||||||
|
|
||||||
|
BLUE = "\033[94m"
|
||||||
|
CYAN = "\033[96m"
|
||||||
|
GREEN = "\033[92m"
|
||||||
|
MAGENTA = "\033[95m"
|
||||||
|
RED = "\033[91m"
|
||||||
|
RESET = "\033[0m"
|
||||||
|
|
||||||
|
|
||||||
|
class BiDirectionalEnum(Enum):
|
||||||
|
"""Permet la recherche inverse d'un enum
|
||||||
|
Condition : les clés et les valeurs doivent être uniques
|
||||||
|
les clés doivent être en MAJUSCULES
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def contains(cls, attr: str):
|
||||||
|
"""Vérifie sur un attribut existe dans l'enum"""
|
||||||
|
return attr.upper() in cls._member_names_
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, attr: str, default: any = None):
|
||||||
|
"""Récupère une valeur à partir de son attribut"""
|
||||||
|
val = None
|
||||||
|
try:
|
||||||
|
val = cls[attr.upper()]
|
||||||
|
except (KeyError, AttributeError):
|
||||||
|
val = default
|
||||||
|
return val
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def inverse(cls):
|
||||||
|
"""Retourne un dictionnaire représentant la map inverse de l'Enum"""
|
||||||
|
return cls._value2member_map_
|
||||||
|
|
||||||
|
|
||||||
|
class EtatAssiduite(int, BiDirectionalEnum):
|
||||||
|
"""Code des états d'assiduité"""
|
||||||
|
|
||||||
|
# Stockés en BD ne pas modifier
|
||||||
|
|
||||||
|
PRESENT = 0
|
||||||
|
RETARD = 1
|
||||||
|
ABSENT = 2
|
||||||
|
|
||||||
|
|
||||||
|
class EtatJustificatif(int, BiDirectionalEnum):
|
||||||
|
"""Code des états des justificatifs"""
|
||||||
|
|
||||||
|
# Stockés en BD ne pas modifier
|
||||||
|
|
||||||
|
VALIDE = 0
|
||||||
|
NON_VALIDE = 1
|
||||||
|
ATTENTE = 2
|
||||||
|
MODIFIE = 3
|
||||||
|
|
||||||
|
|
||||||
|
def is_iso_formated(date: str, convert=False) -> bool or datetime.datetime or None:
|
||||||
|
"""
|
||||||
|
Vérifie si une date est au format iso
|
||||||
|
|
||||||
|
Retourne un booléen Vrai (ou un objet Datetime si convert = True)
|
||||||
|
si l'objet est au format iso
|
||||||
|
|
||||||
|
Retourne Faux si l'objet n'est pas au format et convert = False
|
||||||
|
|
||||||
|
Retourne None sinon
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
date: datetime.datetime = dtparser.isoparse(date)
|
||||||
|
return date if convert else True
|
||||||
|
except (dtparser.ParserError, ValueError, TypeError):
|
||||||
|
return None if convert else False
|
||||||
|
|
||||||
|
|
||||||
|
def localize_datetime(date: datetime.datetime or str) -> datetime.datetime:
|
||||||
|
"""Ajoute un timecode UTC à la date donnée."""
|
||||||
|
if isinstance(date, str):
|
||||||
|
date = is_iso_formated(date, convert=True)
|
||||||
|
|
||||||
|
new_date: datetime.datetime = date
|
||||||
|
if new_date.tzinfo is None:
|
||||||
|
try:
|
||||||
|
new_date = timezone("Europe/Paris").localize(date)
|
||||||
|
except OverflowError:
|
||||||
|
new_date = timezone("UTC").localize(date)
|
||||||
|
return new_date
|
||||||
|
|
||||||
|
|
||||||
|
def is_period_overlapping(
|
||||||
|
periode: tuple[datetime.datetime, datetime.datetime],
|
||||||
|
interval: tuple[datetime.datetime, datetime.datetime],
|
||||||
|
bornes: bool = True,
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Vérifie si la période et l'interval s'intersectent
|
||||||
|
si strict == True : les extrémitées ne comptes pas
|
||||||
|
Retourne Vrai si c'est le cas, faux sinon
|
||||||
|
"""
|
||||||
|
p_deb, p_fin = periode
|
||||||
|
i_deb, i_fin = interval
|
||||||
|
|
||||||
|
if bornes:
|
||||||
|
return p_deb <= i_fin and p_fin >= i_deb
|
||||||
|
return p_deb < i_fin and p_fin > i_deb
|
||||||
|
|
||||||
|
|
||||||
|
def translate_assiduites_metric(hr_metric) -> str:
|
||||||
|
if hr_metric == "1/2 J.":
|
||||||
|
return "demi"
|
||||||
|
if hr_metric == "J.":
|
||||||
|
return "journee"
|
||||||
|
if hr_metric == "N.":
|
||||||
|
return "compte"
|
||||||
|
if hr_metric == "H.":
|
||||||
|
return "heure"
|
||||||
|
|
||||||
|
|
||||||
# Types de modules
|
# Types de modules
|
||||||
class ModuleType(IntEnum):
|
class ModuleType(IntEnum):
|
||||||
"""Code des types de module."""
|
"""Code des types de module."""
|
||||||
@ -448,6 +619,13 @@ def AbsencesURL():
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def AssiduitesURL():
|
||||||
|
"""URL of Assiduités"""
|
||||||
|
return url_for("assiduites.index_html", scodoc_dept=g.scodoc_dept)[
|
||||||
|
: -len("/index_html")
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def UsersURL():
|
def UsersURL():
|
||||||
"""URL of Users
|
"""URL of Users
|
||||||
e.g. https://scodoc.xxx.fr/ScoDoc/DEPT/Scolarite/Users
|
e.g. https://scodoc.xxx.fr/ScoDoc/DEPT/Scolarite/Users
|
||||||
|
552
app/static/css/assiduites.css
Normal file
@ -0,0 +1,552 @@
|
|||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectors>* {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectors:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#validate_selectors {
|
||||||
|
margin-top: 5vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-display {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Gestion de la timeline === */
|
||||||
|
|
||||||
|
#tl_date {
|
||||||
|
visibility: hidden;
|
||||||
|
width: 0px;
|
||||||
|
height: 0px;
|
||||||
|
position: absolute;
|
||||||
|
left: 15%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.infos {
|
||||||
|
position: relative;
|
||||||
|
width: fit-content;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
align-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#datestr {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px #444 solid;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
min-width: 100px;
|
||||||
|
display: inline-block;
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tl_slider {
|
||||||
|
width: 90%;
|
||||||
|
cursor: grab;
|
||||||
|
|
||||||
|
/* visibility: hidden; */
|
||||||
|
}
|
||||||
|
|
||||||
|
#datestr,
|
||||||
|
#time {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-slider-handle.tl_handle {
|
||||||
|
background: none;
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
visibility: visible;
|
||||||
|
background-position: top;
|
||||||
|
background-size: cover;
|
||||||
|
border: none;
|
||||||
|
top: -180%;
|
||||||
|
cursor: grab;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#l_handle {
|
||||||
|
background-image: url(../icons/l_handle.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#r_handle {
|
||||||
|
background-image: url(../icons/r_handle.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-slider-range.ui-widget-header.ui-corner-all {
|
||||||
|
background-color: #F9C768;
|
||||||
|
background-image: none;
|
||||||
|
opacity: 0.50;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* === Gestion des etuds row === */
|
||||||
|
|
||||||
|
.etud_holder {
|
||||||
|
margin-top: 5vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto auto 1fr auto;
|
||||||
|
gap: 16px;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 4px 16px;
|
||||||
|
margin: 0.5% 0;
|
||||||
|
box-shadow: -1px 12px 5px -8px rgba(68, 68, 68, 0.61);
|
||||||
|
-webkit-box-shadow: -1px 12px 5px -8px rgba(68, 68, 68, 0.61);
|
||||||
|
-moz-box-shadow: -1px 12px 5px -8px rgba(68, 68, 68, 0.61);
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row * {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
height: 50px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Index --- */
|
||||||
|
.etud_row .index_field {
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Nom étud --- */
|
||||||
|
.etud_row .name_field {
|
||||||
|
grid-column: 2;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row .name_field .name_set {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin: 0 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row .name_field .name_set * {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row .name_field .name_set h4 {
|
||||||
|
font-size: small;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row .name_field .name_set h5 {
|
||||||
|
font-size: x-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row .pdp {
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Barre assiduités --- */
|
||||||
|
.etud_row .assiduites_bar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 7px 1fr;
|
||||||
|
gap: 13px;
|
||||||
|
grid-column: 3;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.etud_row .assiduites_bar .filler {
|
||||||
|
height: 5px;
|
||||||
|
width: 90%;
|
||||||
|
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row .assiduites_bar #prevDateAssi {
|
||||||
|
height: 7px;
|
||||||
|
width: 7px;
|
||||||
|
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #444;
|
||||||
|
margin: 0px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row .assiduites_bar #prevDateAssi.single {
|
||||||
|
height: 9px;
|
||||||
|
width: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row.conflit {
|
||||||
|
background-color: #ff0000c2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row .assiduites_bar .absent {
|
||||||
|
background-color: #F1A69C !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row .assiduites_bar .present {
|
||||||
|
background-color: #9CF1AF !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row .assiduites_bar .retard {
|
||||||
|
background-color: #F1D99C !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row .assiduites_bar .justified {
|
||||||
|
background-image: repeating-linear-gradient(135deg, transparent, transparent 4px, #7059FF 4px, #7059FF 8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row .assiduites_bar .invalid_justified {
|
||||||
|
background-image: repeating-linear-gradient(135deg, transparent, transparent 4px, #d61616 4px, #d61616 8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* --- Boutons assiduités --- */
|
||||||
|
.etud_row .btns_field {
|
||||||
|
grid-column: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btns_field:disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.etud_row .btns_field * {
|
||||||
|
margin: 0 5%;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbtn {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbtn::before {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbtn.present::before {
|
||||||
|
background-image: url(../icons/present.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbtn.absent::before {
|
||||||
|
background-image: url(../icons/absent.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbtn.aucun::before {
|
||||||
|
background-image: url(../icons/aucun.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbtn.retard::before {
|
||||||
|
background-image: url(../icons/retard.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbtn:checked:before {
|
||||||
|
outline: 3px solid #7059FF;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbtn:focus {
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*<== Modal conflit ==>*/
|
||||||
|
.modal {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 500;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: #fefefe;
|
||||||
|
margin: 5% auto;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #888;
|
||||||
|
width: 80%;
|
||||||
|
height: 40%;
|
||||||
|
position: relative;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.close {
|
||||||
|
color: #111;
|
||||||
|
position: absolute;
|
||||||
|
right: 5px;
|
||||||
|
top: 0px;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ajout de styles pour la frise chronologique */
|
||||||
|
.modal-timeline {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-labels,
|
||||||
|
.assiduites-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-label {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assiduite {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
z-index: 10;
|
||||||
|
height: 100px;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.assiduite-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assiduite-id,
|
||||||
|
.assiduite-period,
|
||||||
|
.assiduite-state,
|
||||||
|
.assiduite-user_id {
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assiduites-container {
|
||||||
|
min-height: 20px;
|
||||||
|
height: calc(50% - 60px);
|
||||||
|
/* Augmentation de la hauteur du conteneur des assiduités */
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
align-items: center;
|
||||||
|
height: 60px;
|
||||||
|
width: 100%;
|
||||||
|
bottom: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Ajout de la classe CSS pour la bordure en pointillés */
|
||||||
|
.assiduite.selected {
|
||||||
|
border: 2px dashed black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assiduite-special {
|
||||||
|
height: 120px;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 5;
|
||||||
|
border: 2px solid #000;
|
||||||
|
background-color: rgba(36, 36, 36, 0.25);
|
||||||
|
background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, rgba(81, 81, 81, 0.61) 5px, rgba(81, 81, 81, 0.61) 10px);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*<== Info sur l'assiduité sélectionnée ==>*/
|
||||||
|
.modal-assiduite-content {
|
||||||
|
background-color: #fefefe;
|
||||||
|
margin: 5% auto;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #888;
|
||||||
|
width: max-content;
|
||||||
|
position: relative;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.modal-assiduite-content.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-assiduite-content .infos {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*<=== Mass Action ==>*/
|
||||||
|
|
||||||
|
.mass-selection {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
margin: 2% 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mass-selection span {
|
||||||
|
margin: 0 1%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mass-selection .rbtn {
|
||||||
|
background-color: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*<== Loader ==> */
|
||||||
|
|
||||||
|
.loader-container {
|
||||||
|
display: none;
|
||||||
|
/* Cacher le loader par défaut */
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
/* Fond semi-transparent pour bloquer les clics */
|
||||||
|
z-index: 9999;
|
||||||
|
/* Placer le loader au-dessus de tout le contenu */
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
border: 6px solid #f3f3f3;
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top: 6px solid #3498db;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
animation: spin 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: translate(-50%, -50%) rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: translate(-50%, -50%) rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fieldsplit {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fieldsplit legend {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#page-assiduite-content {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5%;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#page-assiduite-content>* {
|
||||||
|
margin: 1.5% 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rouge {
|
||||||
|
color: crimson;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legende {
|
||||||
|
border: 1px dashed #333;
|
||||||
|
width: 75%;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order {
|
||||||
|
background-image: url(../icons/sort.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter {
|
||||||
|
background-image: url(../icons/filter.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[name='destroyFile'] {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
background-image: url(../icons/trash.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[name='destroyFile']:checked {
|
||||||
|
background-image: url(../icons/remove_circle.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: block;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
outline: none !important;
|
||||||
|
border: none !important;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0 2px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon:focus {
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#forcemodule {
|
||||||
|
border-radius: 8px;
|
||||||
|
background: crimson;
|
||||||
|
max-width: fit-content;
|
||||||
|
padding: 5px;
|
||||||
|
color: white;
|
||||||
|
}
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
div.jury_decisions_list div {
|
div.jury_decisions_list div {
|
||||||
font-size: 120%;
|
font-size: 120%;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
11
app/static/icons/absent.svg
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
<svg width="85" height="85" viewBox="0 0 85 85" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="85" height="85" rx="15" fill="#F1A69C"/>
|
||||||
|
<g opacity="0.5" clip-path="url(#clip0_120_4425)">
|
||||||
|
<path d="M67.2116 70L43 45.707L18.7885 70L15.0809 66.3043L39.305 41.9995L15.0809 17.6939L18.7885 14L43 38.2922L67.2116 14L70.9191 17.6939L46.695 41.9995L70.9191 66.3043L67.2116 70Z" fill="black"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_120_4425">
|
||||||
|
<rect width="56" height="56" fill="white" transform="matrix(1 0 0 -1 15 70)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 547 B |
8
app/static/icons/aucun.svg
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
<svg width="85" height="85" viewBox="0 0 85 85" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="85" height="85" rx="15" fill="#BBB"/>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_120_4425">
|
||||||
|
<rect width="56" height="56" fill="white" transform="matrix(1 0 0 -1 15 70)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 291 B |
1
app/static/icons/filter.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000"><path d="M4 3h16a1 1 0 011 1v1.586a1 1 0 01-.293.707l-6.415 6.414a1 1 0 00-.292.707v6.305a1 1 0 01-1.243.97l-2-.5a1 1 0 01-.757-.97v-5.805a1 1 0 00-.293-.707L3.292 6.293A1 1 0 013 5.586V4a1 1 0 011-1z" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>
|
After Width: | Height: | Size: 470 B |
13
app/static/icons/present.svg
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
<svg width="85" height="85" viewBox="0 0 85 85" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="85" height="85" rx="15" fill="#9CF1AF"/>
|
||||||
|
<g clip-path="url(#clip0_120_4405)">
|
||||||
|
<g opacity="0.5">
|
||||||
|
<path d="M70.7713 27.5875L36.0497 62.3091C35.7438 62.6149 35.2487 62.6149 34.9435 62.3091L15.2286 42.5935C14.9235 42.2891 14.9235 41.7939 15.2286 41.488L20.0191 36.6976C20.3249 36.3924 20.8201 36.3924 21.1252 36.6976L35.4973 51.069L64.8754 21.6909C65.1819 21.3858 65.6757 21.3858 65.9815 21.6909L70.7713 26.4814C71.0771 26.7865 71.0771 27.281 70.7713 27.5875Z" fill="black"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_120_4405">
|
||||||
|
<rect width="56" height="56" fill="white" transform="translate(15 14)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 729 B |
1
app/static/icons/remove_circle.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000"><path d="M9.172 14.828L12.001 12m2.828-2.828L12.001 12m0 0L9.172 9.172M12.001 12l2.828 2.828M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z" stroke="#fe4217" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>
|
After Width: | Height: | Size: 434 B |
12
app/static/icons/retard.svg
Executable file
@ -0,0 +1,12 @@
|
|||||||
|
<svg width="85" height="85" viewBox="0 0 85 85" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="85" height="85" rx="15" fill="#F1D99C"/>
|
||||||
|
<g opacity="0.5" clip-path="url(#clip0_120_4407)">
|
||||||
|
<path d="M55.2901 49.1836L44.1475 41.3918V28C44.1475 27.3688 43.6311 26.8524 43 26.8524C42.3688 26.8524 41.8524 27.3688 41.8524 28V42C41.8524 42.3787 42.036 42.7229 42.3459 42.941L53.9819 51.077C54.177 51.2147 54.4065 51.2836 54.636 51.2836C54.9918 51.2836 55.3475 51.1115 55.577 50.7787C55.9327 50.2623 55.8065 49.5508 55.2901 49.1836Z" fill="black"/>
|
||||||
|
<path d="M62.7836 22.2164C57.482 16.9148 50.459 14 43 14C35.541 14 28.518 16.9148 23.2164 22.2164C17.9148 27.518 15 34.541 15 42C15 49.459 17.9148 56.482 23.2164 61.7836C28.518 67.0852 35.541 70 43 70C50.459 70 57.482 67.0852 62.7836 61.7836C68.0852 56.482 71 49.459 71 42C71 34.541 68.0852 27.518 62.7836 22.2164ZM44.1475 67.682V63C44.1475 62.3689 43.6311 61.8525 43 61.8525C42.3689 61.8525 41.8525 62.3689 41.8525 63V67.682C28.5869 67.0967 17.9033 56.4131 17.318 43.1475H22C22.6311 43.1475 23.1475 42.6311 23.1475 42C23.1475 41.3689 22.6311 40.8525 22 40.8525H17.318C17.9033 27.5869 28.5869 16.9033 41.8525 16.318V21C41.8525 21.6311 42.3689 22.1475 43 22.1475C43.6311 22.1475 44.1475 21.6311 44.1475 21V16.318C57.4131 16.9033 68.0967 27.5869 68.682 40.8525H64C63.3689 40.8525 62.8525 41.3689 62.8525 42C62.8525 42.6311 63.3689 43.1475 64 43.1475H68.682C68.0967 56.4131 57.4131 67.0967 44.1475 67.682Z" fill="black"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_120_4407">
|
||||||
|
<rect width="56" height="56" fill="white" transform="translate(15 14)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
1
app/static/icons/sort.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M10 14H2m6-4H2m4-4H2m10 12H2m17 2V4m0 16l3-3m-3 3l-3-3m3-13l3 3m-3-3l-3 3"/></svg>
|
After Width: | Height: | Size: 274 B |
1
app/static/icons/trash.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" viewBox="0 0 24 24" stroke-width="1.5" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000"><path d="M20 9l-1.995 11.346A2 2 0 0116.035 22h-8.07a2 2 0 01-1.97-1.654L4 9M21 6h-5.625M3 6h5.625m0 0V4a2 2 0 012-2h2.75a2 2 0 012 2v2m-6.75 0h6.75" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>
|
After Width: | Height: | Size: 418 B |
1586
app/static/js/assiduites.js
Normal file
@ -2,38 +2,45 @@
|
|||||||
class releveBUT extends HTMLElement {
|
class releveBUT extends HTMLElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.shadow = this.attachShadow({ mode: 'open' });
|
this.shadow = this.attachShadow({ mode: "open" });
|
||||||
|
|
||||||
/* Config par defaut */
|
/* Config par defaut */
|
||||||
this.config = {
|
this.config = {
|
||||||
showURL: true
|
showURL: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Template du module */
|
/* Template du module */
|
||||||
this.shadow.innerHTML = this.template();
|
this.shadow.innerHTML = this.template();
|
||||||
|
|
||||||
/* Style du module */
|
/* Style du module */
|
||||||
const styles = document.createElement('link');
|
const styles = document.createElement("link");
|
||||||
styles.setAttribute('rel', 'stylesheet');
|
styles.setAttribute("rel", "stylesheet");
|
||||||
if (location.href.includes("ScoDoc")) {
|
if (location.href.includes("ScoDoc")) {
|
||||||
styles.setAttribute('href', removeLastTwoComponents(getCurrentScriptPath()) + '/css/releve-but.css'); // Scodoc
|
styles.setAttribute(
|
||||||
|
"href",
|
||||||
|
removeLastTwoComponents(getCurrentScriptPath()) + "/css/releve-but.css"
|
||||||
|
); // Scodoc
|
||||||
} else {
|
} else {
|
||||||
styles.setAttribute('href', '/assets/styles/releve-but.css'); // Passerelle
|
styles.setAttribute("href", "/assets/styles/releve-but.css"); // Passerelle
|
||||||
}
|
}
|
||||||
this.shadow.appendChild(styles);
|
this.shadow.appendChild(styles);
|
||||||
}
|
}
|
||||||
listeOnOff() {
|
listeOnOff() {
|
||||||
this.parentElement.parentElement.classList.toggle("listeOff");
|
this.parentElement.parentElement.classList.toggle("listeOff");
|
||||||
this.parentElement.parentElement.querySelectorAll(".moduleOnOff").forEach(e => {
|
this.parentElement.parentElement
|
||||||
e.classList.remove("moduleOnOff")
|
.querySelectorAll(".moduleOnOff")
|
||||||
})
|
.forEach((e) => {
|
||||||
|
e.classList.remove("moduleOnOff");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
moduleOnOff() {
|
moduleOnOff() {
|
||||||
this.parentElement.classList.toggle("moduleOnOff");
|
this.parentElement.classList.toggle("moduleOnOff");
|
||||||
}
|
}
|
||||||
goTo() {
|
goTo() {
|
||||||
let module = this.dataset.module;
|
let module = this.dataset.module;
|
||||||
this.parentElement.parentElement.parentElement.parentElement.querySelector("#Module_" + module).scrollIntoView();
|
this.parentElement.parentElement.parentElement.parentElement
|
||||||
|
.querySelector("#Module_" + module)
|
||||||
|
.scrollIntoView();
|
||||||
}
|
}
|
||||||
|
|
||||||
set setConfig(config) {
|
set setConfig(config) {
|
||||||
@ -50,15 +57,17 @@ class releveBUT extends HTMLElement {
|
|||||||
|
|
||||||
this.setOptions(data.options);
|
this.setOptions(data.options);
|
||||||
|
|
||||||
this.shadow.querySelectorAll(".CTA_Liste").forEach(e => {
|
this.shadow.querySelectorAll(".CTA_Liste").forEach((e) => {
|
||||||
e.addEventListener("click", this.listeOnOff)
|
e.addEventListener("click", this.listeOnOff);
|
||||||
})
|
});
|
||||||
this.shadow.querySelectorAll(".ue, .module").forEach(e => {
|
this.shadow.querySelectorAll(".ue, .module").forEach((e) => {
|
||||||
e.addEventListener("click", this.moduleOnOff)
|
e.addEventListener("click", this.moduleOnOff);
|
||||||
})
|
});
|
||||||
this.shadow.querySelectorAll(":not(.ueBonus)+.syntheseModule").forEach(e => {
|
this.shadow
|
||||||
e.addEventListener("click", this.goTo)
|
.querySelectorAll(":not(.ueBonus)+.syntheseModule")
|
||||||
})
|
.forEach((e) => {
|
||||||
|
e.addEventListener("click", this.goTo);
|
||||||
|
});
|
||||||
|
|
||||||
this.shadow.children[0].classList.add("ready");
|
this.shadow.children[0].classList.add("ready");
|
||||||
}
|
}
|
||||||
@ -146,9 +155,10 @@ class releveBUT extends HTMLElement {
|
|||||||
/* Informations sur l'étudiant */
|
/* Informations sur l'étudiant */
|
||||||
/********************************/
|
/********************************/
|
||||||
showInformations(data) {
|
showInformations(data) {
|
||||||
this.shadow.querySelector(".studentPic").src = data.etudiant.photo_url || "default_Student.svg";
|
this.shadow.querySelector(".studentPic").src =
|
||||||
|
data.etudiant.photo_url || "default_Student.svg";
|
||||||
|
|
||||||
let output = '';
|
let output = "";
|
||||||
|
|
||||||
if (this.config.showURL) {
|
if (this.config.showURL) {
|
||||||
output += `<a href="${data.etudiant.fiche_url}" class=info_etudiant>`;
|
output += `<a href="${data.etudiant.fiche_url}" class=info_etudiant>`;
|
||||||
@ -163,7 +173,9 @@ class releveBUT extends HTMLElement {
|
|||||||
${data.etudiant.prenom}`;
|
${data.etudiant.prenom}`;
|
||||||
|
|
||||||
if (data.etudiant.date_naissance) {
|
if (data.etudiant.date_naissance) {
|
||||||
output += ` <div class=dateNaissance>né${(data.etudiant.civilite == "F") ? "e" : ""} le ${this.ISOToDate(data.etudiant.date_naissance)}</div>`;
|
output += ` <div class=dateNaissance>né${
|
||||||
|
data.etudiant.civilite == "F" ? "e" : ""
|
||||||
|
} le ${this.ISOToDate(data.etudiant.date_naissance)}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
output += `
|
output += `
|
||||||
@ -195,23 +207,28 @@ class releveBUT extends HTMLElement {
|
|||||||
/*******************************/
|
/*******************************/
|
||||||
showSemestre(data) {
|
showSemestre(data) {
|
||||||
let correspondanceCodes = {
|
let correspondanceCodes = {
|
||||||
"ADM": "Admis",
|
ADM: "Admis",
|
||||||
"AJD": "Admis par décision de jury",
|
AJD: "Admis par décision de jury",
|
||||||
"PASD": "Passage de droit : tout n'est pas validé, mais d'après les règles du BUT, vous passez",
|
PASD: "Passage de droit : tout n'est pas validé, mais d'après les règles du BUT, vous passez",
|
||||||
"PAS1NCI": "Vous passez par décision de jury mais attention, vous n'avez pas partout le niveau suffisant",
|
PAS1NCI:
|
||||||
"RED": "Ajourné mais autorisé à redoubler",
|
"Vous passez par décision de jury mais attention, vous n'avez pas partout le niveau suffisant",
|
||||||
"NAR": "Non admis et non autorisé à redoubler : réorientation",
|
RED: "Ajourné mais autorisé à redoubler",
|
||||||
"DEM": "Démission",
|
NAR: "Non admis et non autorisé à redoubler : réorientation",
|
||||||
"ABAN": "Abandon constaté sans lettre de démission",
|
DEM: "Démission",
|
||||||
"RAT": "En attente d'un rattrapage",
|
ABAN: "Abandon constaté sans lettre de démission",
|
||||||
"EXCLU": "Exclusion dans le cadre d'une décision disciplinaire",
|
RAT: "En attente d'un rattrapage",
|
||||||
"DEF": "Défaillance : non évalué par manque d'assiduité",
|
EXCLU: "Exclusion dans le cadre d'une décision disciplinaire",
|
||||||
"ABL": "Année blanche"
|
DEF: "Défaillance : non évalué par manque d'assiduité",
|
||||||
}
|
ABL: "Année blanche",
|
||||||
|
};
|
||||||
|
|
||||||
this.shadow.querySelector("#identite_etudiant").innerHTML = ` <a href="${data.etudiant.fiche_url}">${data.etudiant.nomprenom}</a> `;
|
this.shadow.querySelector(
|
||||||
this.shadow.querySelector(".dateInscription").innerHTML += this.ISOToDate(data.semestre.inscription);
|
"#identite_etudiant"
|
||||||
let output = '';
|
).innerHTML = ` <a href="${data.etudiant.fiche_url}">${data.etudiant.nomprenom}</a> `;
|
||||||
|
this.shadow.querySelector(".dateInscription").innerHTML += this.ISOToDate(
|
||||||
|
data.semestre.inscription
|
||||||
|
);
|
||||||
|
let output = "";
|
||||||
if (!data.options.block_moyenne_generale) {
|
if (!data.options.block_moyenne_generale) {
|
||||||
output += `
|
output += `
|
||||||
<div>
|
<div>
|
||||||
@ -225,12 +242,16 @@ class releveBUT extends HTMLElement {
|
|||||||
}
|
}
|
||||||
output += `
|
output += `
|
||||||
${(() => {
|
${(() => {
|
||||||
if ((!data.semestre.rang.groupes) ||
|
if (
|
||||||
(Object.keys(data.semestre.rang.groupes).length == 0)) {
|
!data.semestre.rang.groupes ||
|
||||||
|
Object.keys(data.semestre.rang.groupes).length == 0
|
||||||
|
) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
let output = "";
|
let output = "";
|
||||||
let [idGroupe, dataGroupe] = Object.entries(data.semestre.rang.groupes)[0];
|
let [idGroupe, dataGroupe] = Object.entries(
|
||||||
|
data.semestre.rang.groupes
|
||||||
|
)[0];
|
||||||
output += `<div>
|
output += `<div>
|
||||||
<div class=enteteSemestre>${data.semestre.groupes[0]?.group_name}</div><div></div>
|
<div class=enteteSemestre>${data.semestre.groupes[0]?.group_name}</div><div></div>
|
||||||
<div class=rang>Rang :</div><div class=rang>${dataGroupe.value} / ${dataGroupe.total}</div>
|
<div class=rang>Rang :</div><div class=rang>${dataGroupe.value} / ${dataGroupe.total}</div>
|
||||||
@ -241,7 +262,9 @@ class releveBUT extends HTMLElement {
|
|||||||
return output;
|
return output;
|
||||||
})()}
|
})()}
|
||||||
<div class=absencesRecap>
|
<div class=absencesRecap>
|
||||||
<div class=enteteSemestre>Absences</div><div class=enteteSemestre>1/2 jour.</div>
|
<div class=enteteSemestre>Absences</div><div class=enteteSemestre>${
|
||||||
|
data.semestre.absences?.metrique ?? "1/2 jour."
|
||||||
|
}</div>
|
||||||
<div class=abs>Non justifiées</div>
|
<div class=abs>Non justifiées</div>
|
||||||
<div>${data.semestre.absences?.injustifie ?? "-"}</div>
|
<div>${data.semestre.absences?.injustifie ?? "-"}</div>
|
||||||
<div class=abs>Total</div><div>${data.semestre.absences?.total ?? "-"}</div>
|
<div class=abs>Total</div><div>${data.semestre.absences?.total ?? "-"}</div>
|
||||||
@ -252,13 +275,13 @@ class releveBUT extends HTMLElement {
|
|||||||
<div class=enteteSemestre>RCUE</div><div></div>
|
<div class=enteteSemestre>RCUE</div><div></div>
|
||||||
${(() => {
|
${(() => {
|
||||||
let output = "";
|
let output = "";
|
||||||
data.semestre.decision_rcue.forEach(competence => {
|
data.semestre.decision_rcue.forEach((competence) => {
|
||||||
output += `<div class=competence>${competence.niveau.competence.titre}</div><div>${competence.code}</div>`;
|
output += `<div class=competence>${competence.niveau.competence.titre}</div><div>${competence.code}</div>`;
|
||||||
})
|
});
|
||||||
return output;
|
return output;
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>`
|
</div>`;
|
||||||
}
|
}
|
||||||
if (data.semestre.decision_ue?.length) {
|
if (data.semestre.decision_ue?.length) {
|
||||||
output += `
|
output += `
|
||||||
@ -266,18 +289,20 @@ class releveBUT extends HTMLElement {
|
|||||||
<div class=enteteSemestre>UE</div><div></div>
|
<div class=enteteSemestre>UE</div><div></div>
|
||||||
${(() => {
|
${(() => {
|
||||||
let output = "";
|
let output = "";
|
||||||
data.semestre.decision_ue.forEach(ue => {
|
data.semestre.decision_ue.forEach((ue) => {
|
||||||
output += `<div class=competence>${ue.acronyme}</div><div>${ue.code}</div>`;
|
output += `<div class=competence>${ue.acronyme}</div><div>${ue.code}</div>`;
|
||||||
})
|
});
|
||||||
return output;
|
return output;
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>`
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
output += `
|
output += `
|
||||||
<a class=photo href="${data.etudiant.fiche_url}">
|
<a class=photo href="${data.etudiant.fiche_url}">
|
||||||
<img src="${data.etudiant.photo_url || "default_Student.svg"}" alt="photo de l'étudiant" title="fiche de l'étudiant" height="120" border="0">
|
<img src="${
|
||||||
|
data.etudiant.photo_url || "default_Student.svg"
|
||||||
|
}" alt="photo de l'étudiant" title="fiche de l'étudiant" height="120" border="0">
|
||||||
</a>`;
|
</a>`;
|
||||||
/*${data.semestre.groupes.map(groupe => {
|
/*${data.semestre.groupes.map(groupe => {
|
||||||
return `
|
return `
|
||||||
@ -293,16 +318,20 @@ class releveBUT extends HTMLElement {
|
|||||||
}*/
|
}*/
|
||||||
this.shadow.querySelector(".infoSemestre").innerHTML = output;
|
this.shadow.querySelector(".infoSemestre").innerHTML = output;
|
||||||
|
|
||||||
|
|
||||||
/*if(data.semestre.decision_annee?.code){
|
/*if(data.semestre.decision_annee?.code){
|
||||||
this.shadow.querySelector(".decision_annee").innerHTML = "Décision année : " + data.semestre.decision_annee.code + " - " + correspondanceCodes[data.semestre.decision_annee.code];
|
this.shadow.querySelector(".decision_annee").innerHTML = "Décision année : " + data.semestre.decision_annee.code + " - " + correspondanceCodes[data.semestre.decision_annee.code];
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
this.shadow.querySelector(".decision").innerHTML = data.semestre.situation || "";
|
this.shadow.querySelector(".decision").innerHTML =
|
||||||
|
data.semestre.situation || "";
|
||||||
/*if (data.semestre.decision?.code) {
|
/*if (data.semestre.decision?.code) {
|
||||||
this.shadow.querySelector(".decision").innerHTML = "Décision jury: " + (data.semestre.decision?.code || "");
|
this.shadow.querySelector(".decision").innerHTML = "Décision jury: " + (data.semestre.decision?.code || "");
|
||||||
}*/
|
}*/
|
||||||
this.shadow.querySelector("#ects_tot").innerHTML = "ECTS : " + (data.semestre.ECTS?.acquis ?? "-") + " / " + (data.semestre.ECTS?.total ?? "-");
|
this.shadow.querySelector("#ects_tot").innerHTML =
|
||||||
|
"ECTS : " +
|
||||||
|
(data.semestre.ECTS?.acquis ?? "-") +
|
||||||
|
" / " +
|
||||||
|
(data.semestre.ECTS?.total ?? "-");
|
||||||
}
|
}
|
||||||
|
|
||||||
/*******************************/
|
/*******************************/
|
||||||
@ -313,14 +342,15 @@ class releveBUT extends HTMLElement {
|
|||||||
/* Fusion et tri des UE et UE capitalisées */
|
/* Fusion et tri des UE et UE capitalisées */
|
||||||
let fusionUE = [
|
let fusionUE = [
|
||||||
...Object.entries(data.ues),
|
...Object.entries(data.ues),
|
||||||
...Object.entries(data.ues_capitalisees)
|
...Object.entries(data.ues_capitalisees),
|
||||||
].sort((a, b) => {
|
].sort((a, b) => {
|
||||||
return a[1].numero - b[1].numero
|
return a[1].numero - b[1].numero;
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Affichage */
|
/* Affichage */
|
||||||
fusionUE.forEach(([ue, dataUE]) => {
|
fusionUE.forEach(([ue, dataUE]) => {
|
||||||
if (dataUE.type == 1) { // UE Sport / Bonus
|
if (dataUE.type == 1) {
|
||||||
|
// UE Sport / Bonus
|
||||||
output += `
|
output += `
|
||||||
<div>
|
<div>
|
||||||
<div class="ue ueBonus">
|
<div class="ue ueBonus">
|
||||||
@ -335,21 +365,29 @@ class releveBUT extends HTMLElement {
|
|||||||
<div>
|
<div>
|
||||||
<div class="ue ${dataUE.date_capitalisation ? "capitalisee" : ""}">
|
<div class="ue ${dataUE.date_capitalisation ? "capitalisee" : ""}">
|
||||||
<h3>
|
<h3>
|
||||||
${ue}${(dataUE.titre) ? " - " + dataUE.titre : ""}
|
${ue}${dataUE.titre ? " - " + dataUE.titre : ""}
|
||||||
</h3>
|
</h3>
|
||||||
<div>
|
<div>
|
||||||
<div class=moyenne>Moyenne : ${dataUE.moyenne?.value || dataUE.moyenne || "-"}</div>
|
<div class=moyenne>Moyenne : ${
|
||||||
<div class=ue_rang>Rang : ${dataUE.moyenne?.rang} / ${dataUE.moyenne?.total}</div>
|
dataUE.moyenne?.value || dataUE.moyenne || "-"
|
||||||
|
}</div>
|
||||||
|
<div class=ue_rang>Rang : ${dataUE.moyenne?.rang} / ${
|
||||||
|
dataUE.moyenne?.total
|
||||||
|
}</div>
|
||||||
<div class=info>`;
|
<div class=info>`;
|
||||||
if (!dataUE.date_capitalisation) {
|
if (!dataUE.date_capitalisation) {
|
||||||
output += ` Bonus : ${dataUE.bonus || 0} -
|
output += ` Bonus : ${dataUE.bonus || 0} -
|
||||||
Malus : ${dataUE.malus || 0}`;
|
Malus : ${dataUE.malus || 0}`;
|
||||||
} else {
|
} else {
|
||||||
output += ` le ${this.ISOToDate(dataUE.date_capitalisation.split("T")[0])} <a href="${dataUE.bul_orig_url}">dans ce semestre</a>`;
|
output += ` le ${this.ISOToDate(
|
||||||
|
dataUE.date_capitalisation.split("T")[0]
|
||||||
|
)} <a href="${dataUE.bul_orig_url}">dans ce semestre</a>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
output += ` <span class=ects> -
|
output += ` <span class=ects> -
|
||||||
ECTS : ${dataUE.ECTS?.acquis ?? "-"} / ${dataUE.ECTS?.total ?? "-"}
|
ECTS : ${dataUE.ECTS?.acquis ?? "-"} / ${
|
||||||
|
dataUE.ECTS?.total ?? "-"
|
||||||
|
}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
@ -384,7 +422,7 @@ class releveBUT extends HTMLElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
})
|
});
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
ueSport(modules) {
|
ueSport(modules) {
|
||||||
@ -400,8 +438,8 @@ class releveBUT extends HTMLElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -409,7 +447,9 @@ class releveBUT extends HTMLElement {
|
|||||||
/* Evaluations */
|
/* Evaluations */
|
||||||
/*******************************/
|
/*******************************/
|
||||||
showEvaluations(data) {
|
showEvaluations(data) {
|
||||||
this.shadow.querySelector(".evaluations").innerHTML = this.module(data.ressources);
|
this.shadow.querySelector(".evaluations").innerHTML = this.module(
|
||||||
|
data.ressources
|
||||||
|
);
|
||||||
this.shadow.querySelector(".sae").innerHTML += this.module(data.saes);
|
this.shadow.querySelector(".sae").innerHTML += this.module(data.saes);
|
||||||
}
|
}
|
||||||
module(module) {
|
module(module) {
|
||||||
@ -420,7 +460,9 @@ class releveBUT extends HTMLElement {
|
|||||||
<div class=module>
|
<div class=module>
|
||||||
<h3>${this.URL(content.url, `${numero} - ${content.titre}`)}</h3>
|
<h3>${this.URL(content.url, `${numero} - ${content.titre}`)}</h3>
|
||||||
<div>
|
<div>
|
||||||
<div class=moyenne>Moyenne indicative : ${content.moyenne.value}</div>
|
<div class=moyenne>Moyenne indicative : ${
|
||||||
|
content.moyenne.value
|
||||||
|
}</div>
|
||||||
<div class=info>
|
<div class=info>
|
||||||
Classe : ${content.moyenne.moy} -
|
Classe : ${content.moyenne.moy} -
|
||||||
Max : ${content.moyenne.max} -
|
Max : ${content.moyenne.max} -
|
||||||
@ -435,7 +477,7 @@ class releveBUT extends HTMLElement {
|
|||||||
${this.evaluation(content.evaluations)}
|
${this.evaluation(content.evaluations)}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
})
|
});
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -454,16 +496,18 @@ class releveBUT extends HTMLElement {
|
|||||||
<div>Max. promo.</div><div>${evaluation.note.max}</div>
|
<div>Max. promo.</div><div>${evaluation.note.max}</div>
|
||||||
<div>Moy. promo.</div><div>${evaluation.note.moy}</div>
|
<div>Moy. promo.</div><div>${evaluation.note.moy}</div>
|
||||||
<div>Min. promo.</div><div>${evaluation.note.min}</div>
|
<div>Min. promo.</div><div>${evaluation.note.min}</div>
|
||||||
${Object.entries(evaluation.poids).map(([UE, poids]) => {
|
${Object.entries(evaluation.poids)
|
||||||
|
.map(([UE, poids]) => {
|
||||||
return `
|
return `
|
||||||
<div>Poids ${UE}</div>
|
<div>Poids ${UE}</div>
|
||||||
<div>${poids}</div>
|
<div>${poids}</div>
|
||||||
`;
|
`;
|
||||||
}).join("")}
|
})
|
||||||
|
.join("")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
})
|
});
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -478,7 +522,6 @@ class releveBUT extends HTMLElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/********************/
|
/********************/
|
||||||
/* Fonctions d'aide */
|
/* Fonctions d'aide */
|
||||||
/********************/
|
/********************/
|
||||||
@ -491,15 +534,17 @@ class releveBUT extends HTMLElement {
|
|||||||
}
|
}
|
||||||
civilite(txt) {
|
civilite(txt) {
|
||||||
switch (txt) {
|
switch (txt) {
|
||||||
case "M": return "M.";
|
case "M":
|
||||||
case "F": return "Mme";
|
return "M.";
|
||||||
default: return "";
|
case "F":
|
||||||
|
return "Mme";
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ISOToDate(ISO) {
|
ISOToDate(ISO) {
|
||||||
return ISO.split("-").reverse().join("/");
|
return ISO.split("-").reverse().join("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
customElements.define('releve-but', releveBUT);
|
customElements.define("releve-but", releveBUT);
|
||||||
|
1597
app/static/libjs/moment-timezone.js
Normal file
3309
app/static/libjs/moment.new.min.js
vendored
Normal file
146
app/tables/visu_assiduites.py
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
##############################################################################
|
||||||
|
# ScoDoc
|
||||||
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
|
# See LICENSE
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
"""Liste simple d'étudiants
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import g, url_for
|
||||||
|
from app.models import Identite, Assiduite, Justificatif
|
||||||
|
from app.tables import table_builder as tb
|
||||||
|
import app.scodoc.sco_assiduites as scass
|
||||||
|
from app.scodoc import sco_preferences
|
||||||
|
|
||||||
|
|
||||||
|
class TableAssi(tb.Table):
|
||||||
|
"""Table listant l'assiduité des étudiants
|
||||||
|
L'id de la ligne est etuid, et le row stocke etud.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
etuds: list[Identite] = None,
|
||||||
|
dates: tuple[str, str] = 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"]
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
row_class=RowAssi,
|
||||||
|
classes=classes,
|
||||||
|
**kwargs,
|
||||||
|
with_foot_titles=False,
|
||||||
|
)
|
||||||
|
self.add_etuds(etuds)
|
||||||
|
|
||||||
|
def add_etuds(self, etuds: list[Identite]):
|
||||||
|
"Ajoute des étudiants à la table"
|
||||||
|
for etud in etuds:
|
||||||
|
row = self.row_class(self, etud)
|
||||||
|
row.add_etud_cols()
|
||||||
|
self.add_row(row)
|
||||||
|
|
||||||
|
|
||||||
|
class RowAssi(tb.Row):
|
||||||
|
"Ligne de la table assiduité"
|
||||||
|
|
||||||
|
# pour le moment très simple, extensible (codes, liens bulletins, ...)
|
||||||
|
def __init__(self, table: TableAssi, etud: Identite, *args, **kwargs):
|
||||||
|
super().__init__(table, etud.id, *args, **kwargs)
|
||||||
|
self.etud = etud
|
||||||
|
self.dates = table.dates
|
||||||
|
|
||||||
|
def add_etud_cols(self):
|
||||||
|
"""Ajoute les colonnes"""
|
||||||
|
etud = self.etud
|
||||||
|
self.table.group_titles.update(
|
||||||
|
{
|
||||||
|
"etud_codes": "Codes",
|
||||||
|
"identite_detail": "",
|
||||||
|
"identite_court": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
bilan_etud = f"{url_for('assiduites.bilan_etud', scodoc_dept=g.scodoc_dept)}?etudid={etud.id}"
|
||||||
|
self.add_cell(
|
||||||
|
"nom_disp",
|
||||||
|
"Nom",
|
||||||
|
etud.nom_disp(),
|
||||||
|
"identite_detail",
|
||||||
|
data={"order": etud.sort_key},
|
||||||
|
target=bilan_etud,
|
||||||
|
target_attrs={"class": "discretelink", "id": str(etud.id)},
|
||||||
|
)
|
||||||
|
self.add_cell(
|
||||||
|
"prenom",
|
||||||
|
"Prénom",
|
||||||
|
etud.prenom,
|
||||||
|
"identite_detail",
|
||||||
|
data={"order": etud.sort_key},
|
||||||
|
target=bilan_etud,
|
||||||
|
target_attrs={"class": "discretelink", "id": str(etud.id)},
|
||||||
|
)
|
||||||
|
stats = self._get_etud_stats(etud)
|
||||||
|
for key, value in stats.items():
|
||||||
|
self.add_cell(key, value[0], f"{value[1] - value[2]}", "assi_stats")
|
||||||
|
self.add_cell(
|
||||||
|
key + "_justi",
|
||||||
|
value[0] + " Justifiée(s)",
|
||||||
|
f"{value[2]}",
|
||||||
|
"assi_stats",
|
||||||
|
)
|
||||||
|
|
||||||
|
compte_justificatifs = scass.filter_by_date(
|
||||||
|
etud.justificatifs, Justificatif, self.dates[0], self.dates[1]
|
||||||
|
).count()
|
||||||
|
|
||||||
|
self.add_cell("justificatifs", "Justificatifs", f"{compte_justificatifs}")
|
||||||
|
|
||||||
|
def _get_etud_stats(self, etud: Identite) -> dict[str, list[str, float, float]]:
|
||||||
|
retour: dict[str, tuple[str, float, float]] = {
|
||||||
|
"present": ["Présence(s)", 0.0, 0.0],
|
||||||
|
"retard": ["Retard(s)", 0.0, 0.0],
|
||||||
|
"absent": ["Absence(s)", 0.0, 0.0],
|
||||||
|
}
|
||||||
|
|
||||||
|
assi_metric = {
|
||||||
|
"H.": "heure",
|
||||||
|
"J.": "journee",
|
||||||
|
"1/2 J.": "demi",
|
||||||
|
}.get(sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id))
|
||||||
|
|
||||||
|
for etat, valeur in retour.items():
|
||||||
|
compte_etat = scass.get_assiduites_stats(
|
||||||
|
assiduites=etud.assiduites,
|
||||||
|
metric=assi_metric,
|
||||||
|
filtered={
|
||||||
|
"date_debut": self.dates[0],
|
||||||
|
"date_fin": self.dates[1],
|
||||||
|
"etat": etat,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
compte_etat_just = scass.get_assiduites_stats(
|
||||||
|
assiduites=etud.assiduites,
|
||||||
|
metric=assi_metric,
|
||||||
|
filtered={
|
||||||
|
"date_debut": self.dates[0],
|
||||||
|
"date_fin": self.dates[1],
|
||||||
|
"etat": etat,
|
||||||
|
"est_just": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
valeur[1] = compte_etat[assi_metric]
|
||||||
|
valeur[2] = compte_etat_just[assi_metric]
|
||||||
|
return retour
|
||||||
|
|
||||||
|
|
||||||
|
def etuds_sorted_from_ids(etudids) -> list[Identite]:
|
||||||
|
"Liste triée d'etuds à partir d'une collections d'etudids"
|
||||||
|
etuds = [Identite.get_etud(etudid) for etudid in etudids]
|
||||||
|
return sorted(etuds, key=lambda etud: etud.sort_key)
|
220
app/templates/assiduites/pages/ajout_justificatif.j2
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
{% block pageContent %}
|
||||||
|
|
||||||
|
<div class="pageContent">
|
||||||
|
<h3>Justifier des assiduités</h3>
|
||||||
|
{% include "assiduites/widgets/tableau_base.j2" %}
|
||||||
|
<section class="liste">
|
||||||
|
<a class="icon filter" onclick="filter(false)"></a>
|
||||||
|
{% include "assiduites/widgets/tableau_justi.j2" %}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="justi-form">
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<div class="justi-row">
|
||||||
|
<button onclick="validerFormulaire()">Créer le justificatif</button>
|
||||||
|
<button onclick="effacerFormulaire()">Remettre à zero</button>
|
||||||
|
</div>
|
||||||
|
<div class="justi-row">
|
||||||
|
<div class="justi-label">
|
||||||
|
<legend for="justi_date_debut" required>Date de début</legend>
|
||||||
|
<input type="datetime-local" name="justi_date_debut" id="justi_date_debut">
|
||||||
|
</div>
|
||||||
|
<div class="justi-label">
|
||||||
|
<legend for="justi_date_fin" required>Date de fin</legend>
|
||||||
|
<input type="datetime-local" name="justi_date_fin" id="justi_date_fin">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="justi-row">
|
||||||
|
<div class="justi-label">
|
||||||
|
<legend for="justi_etat" required>Etat du justificatif</legend>
|
||||||
|
<select name="justi_etat" id="justi_etat">
|
||||||
|
<option value="attente" selected>En Attente de validation</option>
|
||||||
|
<option value="non_valide">Non Valide</option>
|
||||||
|
<option value="modifie">Modifié</option>
|
||||||
|
<option value="valide">Valide</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="justi-row">
|
||||||
|
<div class="justi-label">
|
||||||
|
<legend for="justi_raison">Raison</legend>
|
||||||
|
<textarea name="justi_raison" id="justi_raison" cols="50" rows="10" maxlength="500"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="justi-row">
|
||||||
|
<div class="justi-sect">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="justi-label">
|
||||||
|
<legend for="justi_fich">Importer un fichier</legend>
|
||||||
|
<input type="file" name="justi_fich" id="justi_fich" multiple>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="legende">
|
||||||
|
|
||||||
|
<h3>Gestion des justificatifs</h3>
|
||||||
|
<p>
|
||||||
|
Faites
|
||||||
|
<span style="font-style: italic;">clic droit</span> sur une ligne du tableau pour afficher le menu
|
||||||
|
contextuel :
|
||||||
|
<ul>
|
||||||
|
<li>Détails : Affiche les détails du justificatif sélectionné</li>
|
||||||
|
<li>Editer : Permet de modifier le justificatif (dates, etat, ajouter/supprimer fichier etc)</li>
|
||||||
|
<li>Supprimer : Permet de supprimer le justificatif (Action Irréversible)</li>
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>Cliquer sur l'icone d'entonoir afin de filtrer le tableau des justificatifs</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.justi-row {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.justi-form fieldset {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageContent {
|
||||||
|
max-width: var(--sco-content-max-width);
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.justi-label {
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[required]::after {
|
||||||
|
content: "*";
|
||||||
|
color: crimson;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
|
||||||
|
function validateFields() {
|
||||||
|
const field = document.querySelector('.justi-form')
|
||||||
|
const in_date_debut = field.querySelector('#justi_date_debut');
|
||||||
|
const in_date_fin = field.querySelector('#justi_date_fin');
|
||||||
|
|
||||||
|
if (in_date_debut.value == "" || in_date_fin.value == "") {
|
||||||
|
openAlertModal("Erreur détéctée", document.createTextNode("Il faut indiquer une date de début et une date de fin."), "", color = "crimson");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const date_debut = moment.tz(in_date_debut.value, TIMEZONE);
|
||||||
|
const date_fin = moment.tz(in_date_fin.value, TIMEZONE);
|
||||||
|
|
||||||
|
if (date_fin.isBefore(date_debut)) {
|
||||||
|
openAlertModal("Erreur détéctée", document.createTextNode("La date de fin doit se trouver après la date de début."), "", color = "crimson");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function fieldsToJustificatif() {
|
||||||
|
const field = document.querySelector('.justi-form')
|
||||||
|
|
||||||
|
const date_debut = field.querySelector('#justi_date_debut').value;
|
||||||
|
const date_fin = field.querySelector('#justi_date_fin').value;
|
||||||
|
const etat = field.querySelector('#justi_etat').value;
|
||||||
|
const raison = field.querySelector('#justi_raison').value;
|
||||||
|
|
||||||
|
return {
|
||||||
|
date_debut: date_debut,
|
||||||
|
date_fin: date_fin,
|
||||||
|
etat: etat,
|
||||||
|
raison: raison,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function importFiles(justif_id) {
|
||||||
|
const field = document.querySelector('.justi-form')
|
||||||
|
|
||||||
|
const in_files = field.querySelector('#justi_fich');
|
||||||
|
const path = getUrl() + `/api/justificatif/${justif_id}/import`;
|
||||||
|
|
||||||
|
const requests = []
|
||||||
|
Array.from(in_files.files).forEach((f) => {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('file', f);
|
||||||
|
requests.push(
|
||||||
|
$.ajax(
|
||||||
|
{
|
||||||
|
url: path,
|
||||||
|
type: 'POST',
|
||||||
|
data: fd,
|
||||||
|
dateType: 'json',
|
||||||
|
contentType: false,
|
||||||
|
processData: false,
|
||||||
|
success: () => { },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
$.when(
|
||||||
|
requests
|
||||||
|
).done(() => {
|
||||||
|
loadAll();
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function validerFormulaire() {
|
||||||
|
if (!validateFields()) return
|
||||||
|
|
||||||
|
const justificatif = fieldsToJustificatif();
|
||||||
|
let justif_id = null;
|
||||||
|
let couverture = null;
|
||||||
|
|
||||||
|
createJustificatif(justificatif, (data) => {
|
||||||
|
if (Object.keys(data.errors).length > 0) {
|
||||||
|
console.error(data.errors);
|
||||||
|
}
|
||||||
|
if (Object.keys(data.success).length > 0) {
|
||||||
|
couverture = data.success[0].couverture
|
||||||
|
justif_id = data.success[0].justif_id;
|
||||||
|
importFiles(justif_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function effacerFormulaire() {
|
||||||
|
const field = document.querySelector('.justi-form')
|
||||||
|
|
||||||
|
field.querySelector('#justi_date_debut').value = "";
|
||||||
|
field.querySelector('#justi_date_fin').value = "";
|
||||||
|
field.querySelector('#justi_etat').value = "attente";
|
||||||
|
field.querySelector('#justi_raison').value = "";
|
||||||
|
field.querySelector('#justi_fich').value = "";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const etudid = {{ sco.etud.id }};
|
||||||
|
window.onload = () => {
|
||||||
|
loadAll();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock pageContent %}
|
169
app/templates/assiduites/pages/bilan_dept.j2
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
{% include "assiduites/widgets/tableau_base.j2" %}
|
||||||
|
<section class="alerte invisible">
|
||||||
|
<p>Attention, cet étudiant a trop d'absences</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="nonvalide">
|
||||||
|
<!-- Tableaux des justificatifs à valider (attente / modifié ) -->
|
||||||
|
<h4>Justificatifs en attente (ou modifiés)</h4>
|
||||||
|
{% include "assiduites/widgets/tableau_justi.j2" %}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="annee">
|
||||||
|
<span>Année scolaire 2022-2023 Changer année: </span>
|
||||||
|
<select name="" id="annee" onchange="setterAnnee(this.value)">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="legende">
|
||||||
|
<h3>Gestion des justificatifs</h3>
|
||||||
|
<p>
|
||||||
|
Faites
|
||||||
|
<span style="font-style: italic;">clic droit</span> sur une ligne du tableau pour afficher le menu contextuel :
|
||||||
|
<ul>
|
||||||
|
<li>Détails : Affiche les détails du justificatif sélectionné</li>
|
||||||
|
<li>Editer : Permet de modifier le justificatif (dates, etat, ajouter/supprimer fichier etc)</li>
|
||||||
|
<li>Supprimer : Permet de supprimer le justificatif (Action Irréversible)</li>
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
function loadAll() {
|
||||||
|
generate(defAnnee)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDeptJustificatifsFromPeriod(action) {
|
||||||
|
const path = getUrl() + `/api/justificatifs/dept/${dept_id}/query?date_debut=${bornes.deb}&date_fin=${bornes.fin}&etat=attente,modifie`
|
||||||
|
async_get(
|
||||||
|
path,
|
||||||
|
(data, status) => {
|
||||||
|
console.log(data);
|
||||||
|
justificatifCallBack(data);
|
||||||
|
|
||||||
|
},
|
||||||
|
(data, status) => {
|
||||||
|
console.error(data, status)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function generate(annee) {
|
||||||
|
|
||||||
|
if (annee < 1999 || annee > 2999) {
|
||||||
|
openAlertModal("Année impossible", document.createTextNode("L'année demandé n'existe pas."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bornes = {
|
||||||
|
deb: `${annee}-09-01T00:00`,
|
||||||
|
fin: `${annee + 1}-06-30T23:59`
|
||||||
|
}
|
||||||
|
|
||||||
|
defAnnee = annee;
|
||||||
|
|
||||||
|
getDeptJustificatifsFromPeriod()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function setterAnnee(annee) {
|
||||||
|
annee = parseInt(annee);
|
||||||
|
document.querySelector('.annee span').textContent = `Année scolaire ${annee}-${annee + 1} Changer année: `
|
||||||
|
generate(annee)
|
||||||
|
|
||||||
|
}
|
||||||
|
let defAnnee = {{ annee }};
|
||||||
|
let bornes = {
|
||||||
|
deb: `${defAnnee}-09-01T00:00`,
|
||||||
|
fin: `${defAnnee + 1}-06-30T23:59`
|
||||||
|
}
|
||||||
|
const dept_id = {{ dept_id }};
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
|
||||||
|
filterJustificatifs = {
|
||||||
|
"columns": [
|
||||||
|
"etudid",
|
||||||
|
"entry_date",
|
||||||
|
"date_debut",
|
||||||
|
"date_fin",
|
||||||
|
"etat",
|
||||||
|
"raison",
|
||||||
|
"fichier"
|
||||||
|
],
|
||||||
|
"filters": {
|
||||||
|
"etat": [
|
||||||
|
"attente",
|
||||||
|
"modifie"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const select = document.querySelector('#annee');
|
||||||
|
for (let i = defAnnee + 1; i > defAnnee - 6; i--) {
|
||||||
|
const opt = document.createElement("option");
|
||||||
|
opt.value = i + "",
|
||||||
|
opt.textContent = i + "";
|
||||||
|
if (i === defAnnee) {
|
||||||
|
opt.selected = true;
|
||||||
|
}
|
||||||
|
select.appendChild(opt)
|
||||||
|
}
|
||||||
|
setterAnnee(defAnnee)
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.stats-values-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
border: 1px solid #333;
|
||||||
|
padding: 5px 2px;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-values {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-values-item h5 {
|
||||||
|
font-weight: bold;
|
||||||
|
text-decoration-line: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-values-part {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerte {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 5px 0;
|
||||||
|
border-radius: 7px;
|
||||||
|
|
||||||
|
background-color: crimson;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerte.invisible {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerte p {
|
||||||
|
font-size: larger;
|
||||||
|
color: whitesmoke;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.suppr {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
</style>
|
369
app/templates/assiduites/pages/bilan_etud.j2
Normal file
@ -0,0 +1,369 @@
|
|||||||
|
{% block app_content %}
|
||||||
|
{% include "assiduites/widgets/tableau_base.j2" %}
|
||||||
|
<div class="pageContent">
|
||||||
|
|
||||||
|
<h2>Bilan de l'assiduité de <span class="rouge">{{sco.etud.nomprenom}}</span></h2>
|
||||||
|
|
||||||
|
<section class="alerte invisible">
|
||||||
|
<p>Attention, cet étudiant a trop d'absences</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="stats">
|
||||||
|
<!-- Statistiques d'assiduité (nb pres, nb retard, nb absence) + nb justifié -->
|
||||||
|
<h4>Statistiques d'assiduité</h4>
|
||||||
|
<div class="stats-inputs">
|
||||||
|
<label class="stats-label"> Date de début<input type="date" name="stats_date_debut" id="stats_date_debut"
|
||||||
|
value="{{date_debut}}"></label>
|
||||||
|
<label class="stats-label"> Date de fin<input type="date" name="stats_date_fin" id="stats_date_fin"
|
||||||
|
value="{{date_fin}}"></label>
|
||||||
|
<button onclick="stats()">Actualiser</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-values">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="nonvalide">
|
||||||
|
<!-- Tableaux des assiduités (retard/abs) non justifiées -->
|
||||||
|
<h4>Assiduités non justifiées (Uniquement les retards et les absences)</h4>
|
||||||
|
{% include "assiduites/widgets/tableau_assi.j2" %}
|
||||||
|
<!-- Tableaux des justificatifs à valider (attente / modifié ) -->
|
||||||
|
<h4>Justificatifs en attente (ou modifiés)</h4>
|
||||||
|
{% include "assiduites/widgets/tableau_justi.j2" %}
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="suppr">
|
||||||
|
<h4>Boutons de suppresions (toute suppression est définitive) </h4>
|
||||||
|
<button type="button" onclick="removeAllAssiduites()">Suppression des assiduités</button>
|
||||||
|
<button type="button" onclick="removeAllJustificatifs()">Suppression des justificatifs</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="legende">
|
||||||
|
<h3>Statistiques</h3>
|
||||||
|
<p>Un message d'alerte apparait si le nombre d'absence dépasse le seuil (indiqué dans les préférences du
|
||||||
|
département)</p>
|
||||||
|
<p>Les statistiques sont effectuées entre les deux dates séléctionnées. Si vous modifier les dates il faudra
|
||||||
|
appuyer sur le bouton "Actualiser"</p>
|
||||||
|
<h3>Gestion des justificatifs</h3>
|
||||||
|
<p>
|
||||||
|
Faites
|
||||||
|
<span style="font-style: italic;">clic droit</span> sur une ligne du tableau pour afficher le menu
|
||||||
|
contextuel :
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Détails : Affiche les détails du justificatif sélectionné</li>
|
||||||
|
<li>Editer : Permet de modifier le justificatif (dates, etat, ajouter/supprimer fichier etc)</li>
|
||||||
|
<li>Supprimer : Permet de supprimer le justificatif (Action Irréversible)</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Gestion des Assiduités</h3>
|
||||||
|
<p>
|
||||||
|
Faites
|
||||||
|
<span style="font-style: italic;">clic droit</span> sur une ligne du tableau pour afficher le menu
|
||||||
|
contextuel :
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Détails : Affiche les détails de l'assiduité sélectionnée</li>
|
||||||
|
<li>Editer : Permet de modifier l'assiduité (moduleimpl, etat)</li>
|
||||||
|
<li>Supprimer : Permet de supprimer l'assiduité (Action Irréversible)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock app_content %}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
function stats() {
|
||||||
|
const dd_val = document.getElementById('stats_date_debut').value;
|
||||||
|
const df_val = document.getElementById('stats_date_fin').value;
|
||||||
|
|
||||||
|
if (dd_val == "" || df_val == "") {
|
||||||
|
openAlertModal("Dates invalides", document.createTextNode('Les dates sélectionnées sont invalides'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const date_debut = new moment.tz(dd_val + "T00:00", TIMEZONE);
|
||||||
|
const date_fin = new moment.tz(df_val + "T23:59", TIMEZONE);
|
||||||
|
|
||||||
|
if (date_debut.valueOf() > date_fin.valueOf()) {
|
||||||
|
openAlertModal("Dates invalides", document.createTextNode('La date de début se situe après la date de fin.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
countAssiduites(date_debut.format(), date_fin.format())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAssiduitesCount(dateDeb, dateFin, query) {
|
||||||
|
const url_api = getUrl() + `/api/assiduites/${etudid}/count/query?date_debut=${dateDeb}&date_fin=${dateFin}&${query}`;
|
||||||
|
return $.ajax({
|
||||||
|
async: true,
|
||||||
|
type: "GET",
|
||||||
|
url: url_api,
|
||||||
|
success: (data, status) => {
|
||||||
|
if (status === "success") {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: () => { },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function countAssiduites(dateDeb, dateFin) {
|
||||||
|
$.when(
|
||||||
|
getAssiduitesCount(dateDeb, dateFin, `etat=present`),
|
||||||
|
getAssiduitesCount(dateDeb, dateFin, `etat=present&est_just=v`),
|
||||||
|
getAssiduitesCount(dateDeb, dateFin, `etat=retard`),
|
||||||
|
getAssiduitesCount(dateDeb, dateFin, `etat=retard&est_just=v`),
|
||||||
|
getAssiduitesCount(dateDeb, dateFin, `etat=absent`),
|
||||||
|
getAssiduitesCount(dateDeb, dateFin, `etat=absent&est_just=v`),
|
||||||
|
).then(
|
||||||
|
(pt, pj, rt, rj, at, aj) => {
|
||||||
|
const counter = {
|
||||||
|
"present": {
|
||||||
|
"total": pt[0],
|
||||||
|
"justi": pj[0],
|
||||||
|
},
|
||||||
|
"retard": {
|
||||||
|
"total": rt[0],
|
||||||
|
"justi": rj[0],
|
||||||
|
},
|
||||||
|
"absent": {
|
||||||
|
"total": at[0],
|
||||||
|
"justi": aj[0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const values = document.querySelector('.stats-values');
|
||||||
|
values.innerHTML = "";
|
||||||
|
|
||||||
|
Object.keys(counter).forEach((key) => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.classList.add('stats-values-item');
|
||||||
|
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.classList.add('stats-values-part');
|
||||||
|
|
||||||
|
const heure = document.createElement('span');
|
||||||
|
heure.textContent = `${counter[key].total.heure} heure(s) dont ${counter[key].justi.heure} justifiées`;
|
||||||
|
|
||||||
|
const demi = document.createElement('span');
|
||||||
|
demi.textContent = `${counter[key].total.demi} demi-journée(s) dont ${counter[key].justi.demi} justifiées`;
|
||||||
|
|
||||||
|
const jour = document.createElement('span');
|
||||||
|
jour.textContent = `${counter[key].total.journee} journée(s) dont ${counter[key].justi.journee} justifiées`;
|
||||||
|
|
||||||
|
div.append(jour, demi, heure);
|
||||||
|
|
||||||
|
const title = document.createElement('h5');
|
||||||
|
title.textContent = key.capitalize();
|
||||||
|
|
||||||
|
item.append(title, div)
|
||||||
|
|
||||||
|
values.appendChild(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
const nbAbs = counter.absent.total[assi_metric] - counter.absent.justi[assi_metric];
|
||||||
|
if (nbAbs > assi_seuil) {
|
||||||
|
document.querySelector('.alerte').classList.remove('invisible');
|
||||||
|
document.querySelector('.alerte p').textContent = `Attention, cet étudiant a trop d'absences ${nbAbs} / ${assi_seuil} (${metriques[assi_metric]})`
|
||||||
|
} else {
|
||||||
|
document.querySelector('.alerte').classList.add('invisible');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeAllAssiduites() {
|
||||||
|
|
||||||
|
openPromptModal(
|
||||||
|
"Suppression des assiduités",
|
||||||
|
document.createTextNode(
|
||||||
|
'Souhaitez vous réelement supprimer toutes les assiduités de cet étudiant ? Cette supression est irréversible.')
|
||||||
|
,
|
||||||
|
() => {
|
||||||
|
getAllAssiduitesFromEtud(etudid, (data) => {
|
||||||
|
const toRemove = data.map((a) => a.assiduite_id);
|
||||||
|
console.log(toRemove)
|
||||||
|
deleteAssiduites(toRemove);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
function removeAllJustificatifs() {
|
||||||
|
openPromptModal(
|
||||||
|
"Suppression des justificatifs",
|
||||||
|
document.createTextNode(
|
||||||
|
'Souhaitez vous réelement supprimer tous les justificatifs de cet étudiant ? Cette supression est irréversible.')
|
||||||
|
,
|
||||||
|
() => {
|
||||||
|
getAllJustificatifsFromEtud(etudid, (data) => {
|
||||||
|
const toRemove = data.map((a) => a.justif_id);
|
||||||
|
|
||||||
|
deleteJustificatifs(toRemove);
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Suppression des assiduties
|
||||||
|
*/
|
||||||
|
function deleteAssiduites(assi) {
|
||||||
|
const path = getUrl() + `/api/assiduite/delete`;
|
||||||
|
async_post(
|
||||||
|
path,
|
||||||
|
assi,
|
||||||
|
(data, status) => {
|
||||||
|
//success
|
||||||
|
if (data.success.length > 0) {
|
||||||
|
}
|
||||||
|
location.reload();
|
||||||
|
},
|
||||||
|
(data, status) => {
|
||||||
|
//error
|
||||||
|
console.error(data, status);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Suppression des justificatifs
|
||||||
|
*/
|
||||||
|
function deleteJustificatifs(justis) {
|
||||||
|
const path = getUrl() + `/api/justificatif/delete`;
|
||||||
|
async_post(
|
||||||
|
path,
|
||||||
|
justis,
|
||||||
|
(data, status) => {
|
||||||
|
//success
|
||||||
|
location.reload();
|
||||||
|
},
|
||||||
|
(data, status) => {
|
||||||
|
//error
|
||||||
|
console.error(data, status);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const metriques = {
|
||||||
|
"heure": "H.",
|
||||||
|
"demi": "1/2 J.",
|
||||||
|
"journee": "J."
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const etudid = {{ sco.etud.id }};
|
||||||
|
const assi_metric = "{{ assi_metric | safe }}";
|
||||||
|
const assi_seuil = {{ assi_seuil }};
|
||||||
|
|
||||||
|
const assi_date_debut = "{{date_debut}}";
|
||||||
|
const assi_date_fin = "{{date_fin}}";
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
filterAssiduites = {
|
||||||
|
"columns": [
|
||||||
|
"entry_date",
|
||||||
|
"date_debut",
|
||||||
|
"date_fin",
|
||||||
|
"etat",
|
||||||
|
"moduleimpl_id",
|
||||||
|
"est_just"
|
||||||
|
],
|
||||||
|
"filters": {
|
||||||
|
"etat": [
|
||||||
|
"retard",
|
||||||
|
"absent"
|
||||||
|
],
|
||||||
|
"moduleimpl_id": "",
|
||||||
|
"est_just": "false"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
filterJustificatifs = {
|
||||||
|
"columns": [
|
||||||
|
"entry_date",
|
||||||
|
"date_debut",
|
||||||
|
"date_fin",
|
||||||
|
"etat",
|
||||||
|
"raison",
|
||||||
|
"fichier"
|
||||||
|
],
|
||||||
|
"filters": {
|
||||||
|
"etat": [
|
||||||
|
"attente",
|
||||||
|
"modifie"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('stats_date_fin').value = assi_date_fin;
|
||||||
|
document.getElementById('stats_date_debut').value = assi_date_debut;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
loadAll();
|
||||||
|
stats();
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.stats-values-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
border: 1px solid #333;
|
||||||
|
padding: 5px 2px;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-values {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-values-item h5 {
|
||||||
|
font-weight: bold;
|
||||||
|
text-decoration-line: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-values-part {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerte {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 5px 0;
|
||||||
|
border-radius: 7px;
|
||||||
|
|
||||||
|
background-color: crimson;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerte.invisible {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerte p {
|
||||||
|
font-size: larger;
|
||||||
|
color: whitesmoke;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.suppr {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
</style>
|
356
app/templates/assiduites/pages/calendrier.j2
Normal file
@ -0,0 +1,356 @@
|
|||||||
|
{% block pageContent %}
|
||||||
|
{% include "assiduites/widgets/alert.j2" %}
|
||||||
|
|
||||||
|
<div class="pageContent">
|
||||||
|
{{minitimeline | safe }}
|
||||||
|
<h2>Assiduités de {{sco.etud.nomprenom}}</h2>
|
||||||
|
<div class="calendrier">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="annee">
|
||||||
|
<span>Année scolaire 2022-2023 Changer année: </span>
|
||||||
|
<select name="" id="annee" onchange="setterAnnee(this.value)">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="legende">
|
||||||
|
<h3>Calendrier</h3>
|
||||||
|
<p>Les jours non travaillés sont affiché en violet</p>
|
||||||
|
<p>Les jours possèdant une bordure "bleu" sont des jours où des assiduités ont été justifiées par un
|
||||||
|
justificatif valide</p>
|
||||||
|
<p>Les jours possèdant une bordure "rouge" sont des jours où des assiduités ont été justifiées par un
|
||||||
|
justificatif non valide</p>
|
||||||
|
<p>Le jour sera affiché en : </p>
|
||||||
|
<ul>
|
||||||
|
<li>Rouge : S'il y a une assiduité "Absent"</li>
|
||||||
|
<li>Orange : S'il y a une assiduité "Retard" et pas d'assiduité "Absent"</li>
|
||||||
|
<li>Vert : S'il y a une assiduité "Present" et pas d'assiduité "Absent" ni "Retard"</li>
|
||||||
|
<li>Blanc : S'il n'y a pas d'assiduité</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>Vous pouvez passer votre curseur sur les jours colorés afin de voir les assiduités de cette journée.</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.pageContent {
|
||||||
|
margin-top: 1vh;
|
||||||
|
max-width: var(--sco-content-max-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendrier {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
border: 1px solid #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month h2 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day {
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0 5px;
|
||||||
|
text-align: center;
|
||||||
|
margin: 2px;
|
||||||
|
cursor: default;
|
||||||
|
font-size: 12px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day.est_just {
|
||||||
|
border-left: 10px solid #7059FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day.est_just.invalide {
|
||||||
|
border-left: 10px solid #f64e4e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day .dayline {
|
||||||
|
position: absolute;
|
||||||
|
display: none;
|
||||||
|
left: -237%;
|
||||||
|
bottom: -420%;
|
||||||
|
z-index: 50;
|
||||||
|
width: 250px;
|
||||||
|
height: 75px;
|
||||||
|
background-color: #dedede;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day:hover .dayline {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dayline .mini-timeline {
|
||||||
|
margin-top: 14%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dayline .mini_tick {
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
top: 0;
|
||||||
|
transform: translateY(-110%);
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dayline .mini_tick::after {
|
||||||
|
display: block;
|
||||||
|
content: "|";
|
||||||
|
position: absolute;
|
||||||
|
bottom: -69%;
|
||||||
|
z-index: 2;
|
||||||
|
transform: translateX(200%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function getDaysBetweenDates(start, end) {
|
||||||
|
let now = moment(start);
|
||||||
|
let dates = [];
|
||||||
|
|
||||||
|
while (now.isSameOrBefore(end)) {
|
||||||
|
dates.push(now.clone());
|
||||||
|
now.add(1, "days");
|
||||||
|
}
|
||||||
|
|
||||||
|
return dates;
|
||||||
|
}
|
||||||
|
|
||||||
|
function organizeByMonth(dates) {
|
||||||
|
let datesByMonth = {};
|
||||||
|
|
||||||
|
dates.forEach((date) => {
|
||||||
|
let month = date.format("MMMM"); // Obtenir le mois
|
||||||
|
|
||||||
|
if (!datesByMonth[month]) {
|
||||||
|
datesByMonth[month] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
datesByMonth[month].push(date);
|
||||||
|
});
|
||||||
|
|
||||||
|
return datesByMonth;
|
||||||
|
}
|
||||||
|
|
||||||
|
function organizeAssiduitiesByDay(datesByMonth, assiduities, justificatifs) {
|
||||||
|
let assiduitiesByDay = {};
|
||||||
|
|
||||||
|
Object.keys(datesByMonth).forEach((month) => {
|
||||||
|
assiduitiesByDay[month] = {};
|
||||||
|
|
||||||
|
datesByMonth[month].forEach((date) => {
|
||||||
|
let dayAssiduities = assiduities.filter((assiduity) => {
|
||||||
|
return moment.tz(date, TIMEZONE).isBetween(
|
||||||
|
moment.tz(assiduity.date_debut, TIMEZONE),
|
||||||
|
moment.tz(assiduity.date_fin, TIMEZONE),
|
||||||
|
"day",
|
||||||
|
"[]"
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let dayJustificatifs = justificatifs.filter((justif) => {
|
||||||
|
return moment.tz(date, TIMEZONE).isBetween(
|
||||||
|
moment.tz(justif.date_debut, TIMEZONE),
|
||||||
|
moment.tz(justif.date_fin, TIMEZONE),
|
||||||
|
"day",
|
||||||
|
"[]"
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
assiduitiesByDay[month][date.format("YYYY-MM-DD")] = {
|
||||||
|
assiduites: dayAssiduities,
|
||||||
|
justificatifs: dayJustificatifs
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return assiduitiesByDay;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDayColor(etat) {
|
||||||
|
let color;
|
||||||
|
switch (etat.toUpperCase()) {
|
||||||
|
case "PRESENT":
|
||||||
|
color = "#6bdb83";
|
||||||
|
break;
|
||||||
|
case "ABSENT":
|
||||||
|
color = "#F1A69C";
|
||||||
|
break;
|
||||||
|
case "RETARD":
|
||||||
|
color = "#f0c865";
|
||||||
|
break;
|
||||||
|
case "NONWORK":
|
||||||
|
color = "#bd81ca"
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
color = "#FFF";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateCalendar(assiduitiesByDay, nonWorkdays = []) {
|
||||||
|
const calendar = document.querySelector('.calendrier')
|
||||||
|
calendar.innerHTML = ""
|
||||||
|
|
||||||
|
const days = {
|
||||||
|
Mon: "Lun",
|
||||||
|
Tue: "Mar",
|
||||||
|
Wed: "Mer",
|
||||||
|
Thu: "Jeu",
|
||||||
|
Fri: "Ven",
|
||||||
|
Sat: "Sam",
|
||||||
|
Sun: "Dim",
|
||||||
|
};
|
||||||
|
|
||||||
|
const months = {
|
||||||
|
January: "Jan.",
|
||||||
|
February: "Fev.",
|
||||||
|
March: "Mar.",
|
||||||
|
April: "Avr.",
|
||||||
|
May: "Mai",
|
||||||
|
June: "Juin",
|
||||||
|
July: "Juil.",
|
||||||
|
August: "Août",
|
||||||
|
September: "Sep.",
|
||||||
|
October: "Oct.",
|
||||||
|
November: "Nov.",
|
||||||
|
December: "Déc.",
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.keys(assiduitiesByDay).forEach((month) => {
|
||||||
|
const monthEl = document.createElement('div')
|
||||||
|
monthEl.classList.add("month")
|
||||||
|
const title = document.createElement('h2');
|
||||||
|
title.textContent = `${months[month]}`;
|
||||||
|
monthEl.appendChild(title)
|
||||||
|
|
||||||
|
const daysEl = document.createElement('div')
|
||||||
|
daysEl.classList.add('days');
|
||||||
|
|
||||||
|
Object.keys(assiduitiesByDay[month]).forEach((date) => {
|
||||||
|
let dayAssiduities = assiduitiesByDay[month][date].assiduites;
|
||||||
|
let dayJustificatifs = assiduitiesByDay[month][date].justificatifs;
|
||||||
|
let color = "white";
|
||||||
|
|
||||||
|
if (dayAssiduities.some((a) => a.etat.toLowerCase() === "absent")) color = "absent";
|
||||||
|
else if (dayAssiduities.some((a) => a.etat.toLowerCase() === "retard"))
|
||||||
|
color = "retard";
|
||||||
|
else if (dayAssiduities.some((a) => a.etat.toLowerCase() === "present"))
|
||||||
|
color = "present";
|
||||||
|
let est_just = ""
|
||||||
|
if (dayJustificatifs.some((j) => j.etat.toLowerCase() === "valide")) {
|
||||||
|
est_just = "est_just";
|
||||||
|
} else if (dayJustificatifs.some((j) => j.etat.toLowerCase() !== "valide")) {
|
||||||
|
est_just = "est_just invalide";
|
||||||
|
}
|
||||||
|
const momentDate = moment.tz(date, TIMEZONE);
|
||||||
|
let dayOfMonth = momentDate.format("D");
|
||||||
|
let dayOfWeek = momentDate.format("ddd");
|
||||||
|
|
||||||
|
dayOfWeek = days[dayOfWeek];
|
||||||
|
|
||||||
|
if (nonWorkdays.includes(dayOfWeek.toLowerCase())) color = "nonwork";
|
||||||
|
|
||||||
|
const day = document.createElement('div');
|
||||||
|
day.className = `day ${est_just}`
|
||||||
|
|
||||||
|
day.style.backgroundColor = getDayColor(color);
|
||||||
|
|
||||||
|
day.textContent = `${dayOfWeek} ${dayOfMonth}`;
|
||||||
|
|
||||||
|
if (!nonWorkdays.includes(dayOfWeek.toLowerCase()) && dayAssiduities.length > 0) {
|
||||||
|
const cache = document.createElement('div')
|
||||||
|
cache.classList.add('dayline');
|
||||||
|
cache.appendChild(
|
||||||
|
createMiniTimeline(dayAssiduities, date)
|
||||||
|
)
|
||||||
|
|
||||||
|
day.appendChild(cache)
|
||||||
|
}
|
||||||
|
|
||||||
|
daysEl.appendChild(day);
|
||||||
|
});
|
||||||
|
monthEl.appendChild(daysEl)
|
||||||
|
calendar.appendChild(monthEl)
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEtudAssiduites(deb, fin, callback = () => { }) {
|
||||||
|
const url_api =
|
||||||
|
getUrl() +
|
||||||
|
`/api/assiduites/${etudid}/query?date_debut=${deb}&date_fin=${fin}`;
|
||||||
|
async_get(url_api, (data, status) => {
|
||||||
|
if (status === "success") {
|
||||||
|
callback(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEtudJustificatifs(deb, fin) {
|
||||||
|
let list = [];
|
||||||
|
const url_api =
|
||||||
|
getUrl() +
|
||||||
|
`/api/justificatifs/${etudid}/query?date_debut=${deb}&date_fin=${fin}`;
|
||||||
|
sync_get(url_api, (data, status) => {
|
||||||
|
if (status === "success") {
|
||||||
|
list = data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
function generate(annee) {
|
||||||
|
|
||||||
|
if (annee < 1999 || annee > 2999) {
|
||||||
|
openAlertModal("Année impossible", document.createTextNode("L'année demandé n'existe pas."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const bornes = {
|
||||||
|
deb: `${annee}-09-01T00:00`,
|
||||||
|
fin: `${annee + 1}-06-30T23:59`
|
||||||
|
}
|
||||||
|
|
||||||
|
let assiduities = getEtudAssiduites(bornes.deb, bornes.fin, (data) => {
|
||||||
|
let dates = getDaysBetweenDates(bornes.deb, bornes.fin);
|
||||||
|
let datesByMonth = organizeByMonth(dates);
|
||||||
|
const justifs = getEtudJustificatifs(bornes.deb, bornes.fin);
|
||||||
|
let assiduitiesByDay = organizeAssiduitiesByDay(datesByMonth, data, justifs);
|
||||||
|
generateCalendar(assiduitiesByDay, nonwork);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function setterAnnee(annee) {
|
||||||
|
annee = parseInt(annee);
|
||||||
|
document.querySelector('.annee span').textContent = `Année scolaire ${annee}-${annee + 1} Changer année: `
|
||||||
|
generate(annee)
|
||||||
|
|
||||||
|
}
|
||||||
|
const defAnnee = {{ annee }}
|
||||||
|
const etudid = {{ sco.etud.id }};
|
||||||
|
const nonwork = [{{ nonworkdays | safe }}];
|
||||||
|
window.onload = () => {
|
||||||
|
const select = document.querySelector('#annee');
|
||||||
|
for (let i = defAnnee + 1; i > defAnnee - 6; i--) {
|
||||||
|
const opt = document.createElement("option");
|
||||||
|
opt.value = i + "",
|
||||||
|
opt.textContent = i + "";
|
||||||
|
if (i === defAnnee) {
|
||||||
|
opt.selected = true;
|
||||||
|
}
|
||||||
|
select.appendChild(opt)
|
||||||
|
}
|
||||||
|
setterAnnee(defAnnee)
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
{% endblock pageContent %}
|
29
app/templates/assiduites/pages/config_assiduites.j2
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{% extends "base.j2" %}
|
||||||
|
{% import 'bootstrap/wtf.html' as wtf %}
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
<h1>Configuration du Module d'assiduité</h1>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
|
||||||
|
<form class="form form-horizontal" method="post" enctype="multipart/form-data" role="form">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
{{ wtf.form_errors(form, hiddens="only") }}
|
||||||
|
|
||||||
|
{{ wtf.form_field(form.morning_time) }}
|
||||||
|
{{ wtf.form_field(form.lunch_time) }}
|
||||||
|
{{ wtf.form_field(form.afternoon_time) }}
|
||||||
|
{{ wtf.form_field(form.tick_time) }}
|
||||||
|
<div class="form-group">
|
||||||
|
{{ wtf.form_field(form.submit) }}
|
||||||
|
{{ wtf.form_field(form.cancel) }}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
36
app/templates/assiduites/pages/etat_absence_date.j2
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<h2>Présence lors de l'évaluation {{eval.title}} </h2>
|
||||||
|
<h3>Réalisé le {{eval.jour}} de {{eval.heure_debut}} à {{eval.heure_fin}}</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
Nom
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Assiduité
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{% for etud in etudiants %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{{etud.nom | safe}}
|
||||||
|
</td>
|
||||||
|
<td style="text-align: center;">
|
||||||
|
{{etud.etat}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
tr,
|
||||||
|
td {
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
55
app/templates/assiduites/pages/liste_assiduites.j2
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
{% block app_content %}
|
||||||
|
<div class="pageContent">
|
||||||
|
|
||||||
|
<h2>Liste de l'assiduité et des justificatifs de <span class="rouge">{{sco.etud.nomprenom}}</span></h2>
|
||||||
|
{% include "assiduites/widgets/tableau_base.j2" %}
|
||||||
|
<h3>Assiduités :</h3>
|
||||||
|
<a class="icon filter" onclick="filter()"></a>
|
||||||
|
{% include "assiduites/widgets/tableau_assi.j2" %}
|
||||||
|
<h3>Justificatifs :</h3>
|
||||||
|
<a class="icon filter" onclick="filter(false)"></a>
|
||||||
|
{% include "assiduites/widgets/tableau_justi.j2" %}
|
||||||
|
<ul id="contextMenu" class="context-menu">
|
||||||
|
<li id="detailOption">Detail</li>
|
||||||
|
<li id="editOption">Editer</li>
|
||||||
|
<li id="deleteOption">Supprimer</li>
|
||||||
|
</ul>
|
||||||
|
<div class="legende">
|
||||||
|
<h3>Gestion des justificatifs</h3>
|
||||||
|
<p>
|
||||||
|
Faites
|
||||||
|
<span style="font-style: italic;">clic droit</span> sur une ligne du tableau pour afficher le menu
|
||||||
|
contextuel :
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Détails : Affiche les détails du justificatif sélectionné</li>
|
||||||
|
<li>Editer : Permet de modifier le justificatif (dates, etat, ajouter/supprimer fichier etc)</li>
|
||||||
|
<li>Supprimer : Permet de supprimer le justificatif (Action Irréversible)</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>Vous pouvez filtrer le tableau en cliquant sur l'icone d'entonoir sous le titre du tableau.</p>
|
||||||
|
|
||||||
|
<h3>Gestion des Assiduités</h3>
|
||||||
|
<p>
|
||||||
|
Faites
|
||||||
|
<span style="font-style: italic;">clic droit</span> sur une ligne du tableau pour afficher le menu
|
||||||
|
contextuel :
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Détails : Affiche les détails de l'assiduité sélectionnée</li>
|
||||||
|
<li>Editer : Permet de modifier l'assiduité (moduleimpl, etat)</li>
|
||||||
|
<li>Supprimer : Permet de supprimer l'assiduité (Action Irréversible)</li>
|
||||||
|
</ul>
|
||||||
|
<p>Vous pouvez filtrer le tableau en cliquant sur l'icone d'entonoir sous le titre du tableau.</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock app_content %}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const etudid = {{ sco.etud.id }}
|
||||||
|
window.onload = () => {
|
||||||
|
loadAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
25
app/templates/assiduites/pages/signal_assiduites_diff.j2
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<h2>Signalement différé des assiduités {{gr |safe}}</h2>
|
||||||
|
<div class="legende">
|
||||||
|
<h3>Explication de la saisie différée</h3>
|
||||||
|
<p>Si la colonne n'est pas valide elle sera affichée en rouge, passez votre curseur sur la colonne pour afficher
|
||||||
|
le message d'erreur</p>
|
||||||
|
<p>Sélectionner la date de début de la colonne mettra automatiquement la date de fin à la durée d'une séance
|
||||||
|
(préférence de département)</p>
|
||||||
|
<p>Modifier le moduleimpl alors que des assiduités sont déjà enregistrées pour la période changera leur
|
||||||
|
moduleimpl.</p>
|
||||||
|
<p>Il y a 4 boutons d'assiduités sur la colonne permettant de mettre l'assiduités à tous les étudiants</p>
|
||||||
|
<p>Le dernier des boutons retire l'assiduité.</p>
|
||||||
|
<p>Vous pouvez ajouter des colonnes en appuyant sur le bouton + </p>
|
||||||
|
<p>Vous pouvez supprimer une colonne en appuyant sur la croix qui se situe dans le coin haut droit de la colonne
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<h3>{{sem | safe }}</h3>
|
||||||
|
|
||||||
|
{{diff | safe}}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{% include "assiduites/widgets/alert.j2" %}
|
||||||
|
{% include "assiduites/widgets/prompt.j2" %}
|
||||||
|
{% include "assiduites/widgets/conflict.j2" %}
|
||||||
|
{% include "assiduites/widgets/toast.j2" %}
|
133
app/templates/assiduites/pages/signal_assiduites_etud.j2
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
{# -*- mode: jinja-html -*- #}
|
||||||
|
{% include "assiduites/widgets/toast.j2" %}
|
||||||
|
{% include "assiduites/widgets/alert.j2" %}
|
||||||
|
{% include "assiduites/widgets/prompt.j2" %}
|
||||||
|
{% include "assiduites/widgets/conflict.j2" %}
|
||||||
|
<div id="page-assiduite-content">
|
||||||
|
{% block content %}
|
||||||
|
<h2>Signalement de l'assiduité de <span class="rouge">{{sco.etud.nomprenom}}</span></h2>
|
||||||
|
|
||||||
|
<div class="infos">
|
||||||
|
Date: <span id="datestr"></span>
|
||||||
|
<input type="date" name="tl_date" id="tl_date" value="{{ date }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{timeline|safe}}
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{{moduleimpl_select | safe }}
|
||||||
|
<button class="btn" onclick="fastJustify(getCurrentAssiduite(etudid))" id="justif-rapide">Justifier</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btn_group">
|
||||||
|
<button class="btn" onclick="setTimeLineTimes({{morning}},{{afternoon}})">Journée</button>
|
||||||
|
<button class="btn" onclick="setTimeLineTimes({{morning}},{{lunch}})">Matin</button>
|
||||||
|
<button class="btn" onclick="setTimeLineTimes({{lunch}},{{afternoon}})">Après-midi</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="etud_holder">
|
||||||
|
<div id="etud_row_{{sco.etud.id}}">
|
||||||
|
<div class="index"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
{{diff | safe}}
|
||||||
|
|
||||||
|
<div class="legende">
|
||||||
|
<h3>Explication de la timeline</h3>
|
||||||
|
<p>
|
||||||
|
Si la période indiquée par la timeline provoque un conflit d'assiduité pour un étudiant sa ligne deviendra
|
||||||
|
rouge.
|
||||||
|
<br>
|
||||||
|
Dans ce cas il faut résoudre manuellement le conflit : cliquez sur un des boutons d'assiduités pour ouvrir
|
||||||
|
le
|
||||||
|
résolveur de conflit.
|
||||||
|
<br>
|
||||||
|
Correspondance des couleurs :
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Vert -> présence de l'étudiant lors de la période</li>
|
||||||
|
<li>Orange -> retard de l'étudiant lors de la période</li>
|
||||||
|
<li>Rouge -> absence de l'étudiant lors de la période</li>
|
||||||
|
<li>Hachure Bleu -> l'assiduité est justifiée par un justificatif valide</li>
|
||||||
|
<li>Hachure Rouge -> l'assiduité est justifiée par un justificatif non valide / en attente de validation
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>Vous pouvez justifier rapidement une assiduité en saisisant l'assiduité puis en appuyant sur "Justifier"</p>
|
||||||
|
|
||||||
|
<h3>Explication de la saisie différée</h3>
|
||||||
|
<p>Si la colonne n'est pas valide elle sera affichée en rouge, passez votre curseur sur la colonne pour afficher
|
||||||
|
le message d'erreur</p>
|
||||||
|
<p>Sélectionner la date de début de la colonne mettra automatiquement la date de fin à la durée d'une séance
|
||||||
|
(préférence de département)</p>
|
||||||
|
<p>Modifier le moduleimpl alors que des assiduités sont déjà enregistrées pour la période changera leur
|
||||||
|
moduleimpl.</p>
|
||||||
|
<p>Il y a 4 boutons d'assiduités sur la colonne permettant de mettre l'assiduités à tous les étudiants</p>
|
||||||
|
<p>Le dernier des boutons retire l'assiduité.</p>
|
||||||
|
<p>Vous pouvez ajouter des colonnes en appuyant sur le bouton + </p>
|
||||||
|
<p>Vous pouvez supprimer une colonne en appuyant sur la croix qui se situe dans le coin haut droit de la colonne
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Ajout d'un conteneur pour le loader -->
|
||||||
|
<div class="loader-container" id="loaderContainer">
|
||||||
|
<div class="loader"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const etudid = {{ sco.etud.id }};
|
||||||
|
const nonWorkDays = [{{ nonworkdays| safe }}];
|
||||||
|
|
||||||
|
setupDate(() => {
|
||||||
|
if (updateDate()) {
|
||||||
|
actualizeEtud(etudid);
|
||||||
|
updateSelect();
|
||||||
|
onlyAbs();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
setupTimeLine(() => {
|
||||||
|
updateJustifyBtn();
|
||||||
|
});
|
||||||
|
|
||||||
|
updateDate();
|
||||||
|
|
||||||
|
getSingleEtud({{ sco.etud.id }});
|
||||||
|
actualizeEtud({{ sco.etud.id }});
|
||||||
|
updateSelect()
|
||||||
|
|
||||||
|
updateJustifyBtn();
|
||||||
|
|
||||||
|
|
||||||
|
function setTimeLineTimes(a, b) {
|
||||||
|
setPeriodValues(a, b);
|
||||||
|
updateJustifyBtn();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
window.forceModule = "{{ forcer_module }}"
|
||||||
|
window.forceModule = window.forceModule == "True" ? true : false
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.justifie {
|
||||||
|
background-color: rgb(104, 104, 252);
|
||||||
|
color: whitesmoke;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
</div>
|
102
app/templates/assiduites/pages/signal_assiduites_group.j2
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
{% include "assiduites/widgets/toast.j2" %}
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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}}
|
||||||
|
|
||||||
|
<button id="validate_selectors" onclick="validateSelectors(this)">
|
||||||
|
Faire la saisie
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="etud_holder">
|
||||||
|
<p class="placeholder">
|
||||||
|
Veillez à choisir le groupe concerné par la saisie ainsi que la date de la saisie.
|
||||||
|
Après validation, il faudra recharger la page pour changer les informations de la saisie.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="legende">
|
||||||
|
<h3>Explication diverses</h3>
|
||||||
|
<p>
|
||||||
|
Si la période indiquée par la timeline provoque un conflit d'assiduité pour un étudiant sa ligne deviendra
|
||||||
|
rouge.
|
||||||
|
<br>
|
||||||
|
Dans ce cas il faut résoudre manuellement le conflit : cliquez sur un des boutons d'assiduités pour ouvrir
|
||||||
|
le
|
||||||
|
résolveur de conflit.
|
||||||
|
<br>
|
||||||
|
Correspondance des couleurs :
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Vert -> présence de l'étudiant lors de la période</li>
|
||||||
|
<li>Orange -> retard de l'étudiant lors de la période</li>
|
||||||
|
<li>Rouge -> absence de l'étudiant lors de la période</li>
|
||||||
|
<li>Hachure Bleu -> l'assiduité est justifiée par un justificatif valide</li>
|
||||||
|
<li>Hachure Rouge -> l'assiduité est justifiée par un justificatif non valide / en attente de validation
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<!-- Ajout d'un conteneur pour le loader -->
|
||||||
|
<div class="loader-container" id="loaderContainer">
|
||||||
|
<div class="loader"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% include "assiduites/widgets/alert.j2" %}
|
||||||
|
{% include "assiduites/widgets/prompt.j2" %}
|
||||||
|
{% include "assiduites/widgets/conflict.j2" %}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
const nonWorkDays = [{{ nonworkdays| safe }}];
|
||||||
|
|
||||||
|
updateDate();
|
||||||
|
setupDate();
|
||||||
|
setupTimeLine();
|
||||||
|
|
||||||
|
window.forceModule = "{{ forcer_module }}"
|
||||||
|
window.forceModule = window.forceModule == "True" ? true : false
|
||||||
|
|
||||||
|
if (window.forceModule) {
|
||||||
|
const btn = document.getElementById("validate_selectors");
|
||||||
|
|
||||||
|
const select = document.getElementById("moduleimpl_select");
|
||||||
|
|
||||||
|
if (select.value == "") {
|
||||||
|
btn.disabled = true;
|
||||||
|
document.getElementById('forcemodule').style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</section>
|
29
app/templates/assiduites/pages/visu_assi.j2
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<h2>Visualisation de l'assiduité {{gr_tit|safe}}</h2>
|
||||||
|
|
||||||
|
<div class="stats-inputs">
|
||||||
|
<label class="stats-label"> Date de début<input type="date" name="stats_date_debut" id="stats_date_debut"
|
||||||
|
value="{{date_debut}}"></label>
|
||||||
|
<label class="stats-label"> Date de fin<input type="date" name="stats_date_fin" id="stats_date_fin"
|
||||||
|
value="{{date_fin}}"></label>
|
||||||
|
<button onclick="stats()">Changer la période</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{tableau | safe}}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const date_debut = "{{date_debut}}"
|
||||||
|
const date_fin = "{{date_fin}}"
|
||||||
|
const group_ids = "{{group_ids}}"
|
||||||
|
|
||||||
|
function stats() {
|
||||||
|
const deb = document.querySelector('#stats_date_debut').value
|
||||||
|
const fin = document.querySelector('#stats_date_fin').value
|
||||||
|
location.href = `VisualisationAssiduitesGroupe?group_ids=${group_ids}&date_debut=${deb}&date_fin=${fin}`
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
document.querySelector('#stats_date_debut').value = date_debut;
|
||||||
|
document.querySelector('#stats_date_fin').value = date_fin;
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
160
app/templates/assiduites/widgets/alert.j2
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
{% block alertmodal %}
|
||||||
|
<div id="alertModal" class="alertmodal">
|
||||||
|
|
||||||
|
<!-- alertModal content -->
|
||||||
|
<div class="alertmodal-content">
|
||||||
|
<div class="alertmodal-header">
|
||||||
|
<span class="alertmodal-close">×</span>
|
||||||
|
<h2 class="alertmodal-title">alertModal Header</h2>
|
||||||
|
</div>
|
||||||
|
<div class="alertmodal-body">
|
||||||
|
<p>Some text in the alertModal Body</p>
|
||||||
|
<p>Some other text...</p>
|
||||||
|
</div>
|
||||||
|
<div class="alertmodal-footer">
|
||||||
|
<h3>alertModal Footer</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<style>
|
||||||
|
/* The alertModal (background) */
|
||||||
|
.alertmodal {
|
||||||
|
display: none;
|
||||||
|
/* Hidden by default */
|
||||||
|
position: fixed;
|
||||||
|
/* Stay in place */
|
||||||
|
z-index: 850;
|
||||||
|
/* Sit on top */
|
||||||
|
padding-top: 100px;
|
||||||
|
/* Location of the box */
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
/* Full width */
|
||||||
|
height: 100%;
|
||||||
|
/* Full height */
|
||||||
|
overflow: auto;
|
||||||
|
/* Enable scroll if needed */
|
||||||
|
background-color: rgb(0, 0, 0);
|
||||||
|
/* Fallback color */
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
/* Black w/ opacity */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* alertModal Content */
|
||||||
|
.alertmodal-content {
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
background-color: #fefefe;
|
||||||
|
margin: auto;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid #888;
|
||||||
|
width: 45%;
|
||||||
|
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
|
||||||
|
-webkit-animation-name: animatetop;
|
||||||
|
-webkit-animation-duration: 0.4s;
|
||||||
|
animation-name: animatetop;
|
||||||
|
animation-duration: 0.4s
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add Animation */
|
||||||
|
@-webkit-keyframes animatetop {
|
||||||
|
from {
|
||||||
|
top: -300px;
|
||||||
|
opacity: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
top: 0;
|
||||||
|
opacity: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes animatetop {
|
||||||
|
from {
|
||||||
|
top: -300px;
|
||||||
|
opacity: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
top: 0;
|
||||||
|
opacity: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The Close Button */
|
||||||
|
.alertmodal-close {
|
||||||
|
color: white;
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alertmodal-close:hover,
|
||||||
|
.alertmodal-close:focus {
|
||||||
|
color: #000;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alertmodal-header {
|
||||||
|
padding: 2px 16px;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alertmodal-body {
|
||||||
|
padding: 2px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alertmodal-footer {
|
||||||
|
padding: 2px 16px;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alertmodal.is-active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const alertmodal = document.getElementById("alertModal");
|
||||||
|
function openAlertModal(titre, contenu, footer, color = "crimson") {
|
||||||
|
alertmodal.classList.add('is-active');
|
||||||
|
|
||||||
|
alertmodal.querySelector('.alertmodal-title').textContent = titre;
|
||||||
|
alertmodal.querySelector('.alertmodal-body').innerHTML = ""
|
||||||
|
alertmodal.querySelector('.alertmodal-body').appendChild(contenu);
|
||||||
|
alertmodal.querySelector('.alertmodal-footer').textContent = footer;
|
||||||
|
|
||||||
|
const banners = Array.from(alertmodal.querySelectorAll('.alertmodal-footer,.alertmodal-header'))
|
||||||
|
banners.forEach((ban) => {
|
||||||
|
ban.style.backgroundColor = color;
|
||||||
|
})
|
||||||
|
|
||||||
|
alertmodal.addEventListener('click', (e) => {
|
||||||
|
if (e.target.id == alertmodal.id) {
|
||||||
|
alertmodal.classList.remove('is-active');
|
||||||
|
alertmodal.removeEventListener('click', this)
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function closeAlertModal() {
|
||||||
|
alertmodal.classList.remove("is-active")
|
||||||
|
}
|
||||||
|
const alertClose = document.querySelector(".alertmodal-close");
|
||||||
|
alertClose.onclick = function () {
|
||||||
|
closeAlertModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
{% endblock alertmodal %}
|
459
app/templates/assiduites/widgets/conflict.j2
Normal file
@ -0,0 +1,459 @@
|
|||||||
|
<script>
|
||||||
|
/**
|
||||||
|
* Transformation d'une date de début en position sur la timeline
|
||||||
|
* @param {String} start
|
||||||
|
* @returns {String} un déplacement par rapport à la gauche en %
|
||||||
|
*/
|
||||||
|
function getLeftPosition(start) {
|
||||||
|
const startTime = new moment.tz(start, TIMEZONE);
|
||||||
|
const startMins = (startTime.hours() - 8) * 60 + startTime.minutes();
|
||||||
|
return (startMins / (18 * 60 - 8 * 60)) * 100 + "%";
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Ajustement de l'espacement vertical entre les assiduités superposées
|
||||||
|
* @param {HTMLElement} container le conteneur des assiduités
|
||||||
|
* @param {String} start la date début de l'assiduité à placer
|
||||||
|
* @param {String} end la date de fin de l'assiduité à placer
|
||||||
|
* @returns {String} La position en px
|
||||||
|
*/
|
||||||
|
function getTopPosition(container, start, end) {
|
||||||
|
const overlaps = (a, b) => {
|
||||||
|
return a.start < b.end && a.end > b.start;
|
||||||
|
};
|
||||||
|
|
||||||
|
const startTime = new moment.tz(start, TIMEZONE);
|
||||||
|
const endTime = new moment.tz(end, TIMEZONE);
|
||||||
|
const assiduiteDuration = { start: startTime, end: endTime };
|
||||||
|
|
||||||
|
let position = 0;
|
||||||
|
let hasOverlap = true;
|
||||||
|
|
||||||
|
while (hasOverlap) {
|
||||||
|
hasOverlap = false;
|
||||||
|
Array.from(container.children).some((el) => {
|
||||||
|
const elStart = new moment.tz(el.getAttribute("data-start"));
|
||||||
|
const elEnd = new moment.tz(el.getAttribute("data-end"));
|
||||||
|
const elDuration = { start: elStart, end: elEnd };
|
||||||
|
|
||||||
|
if (overlaps(assiduiteDuration, elDuration)) {
|
||||||
|
position += 25; // Pour ajuster l'espacement vertical entre les assiduités superposées
|
||||||
|
hasOverlap = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return position + "px";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transformation d'un état en couleur
|
||||||
|
* @param {String} state l'état
|
||||||
|
* @returns {String} la couleur correspondant à l'état
|
||||||
|
*/
|
||||||
|
function getColor(state) {
|
||||||
|
switch (state) {
|
||||||
|
case "PRESENT":
|
||||||
|
return "#9CF1AF";
|
||||||
|
case "ABSENT":
|
||||||
|
return "#F1A69C";
|
||||||
|
case "RETARD":
|
||||||
|
return "#F1D99C";
|
||||||
|
default:
|
||||||
|
return "gray";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcule de la largeur de l'assiduité sur la timeline
|
||||||
|
* @param {String} start date iso de début
|
||||||
|
* @param {String} end date iso de fin
|
||||||
|
* @returns {String} la taille en %
|
||||||
|
*/
|
||||||
|
function getWidth(start, end) {
|
||||||
|
const startTime = new moment.tz(start, TIMEZONE);
|
||||||
|
const endTime = new moment.tz(end, TIMEZONE);
|
||||||
|
|
||||||
|
const duration = (endTime - startTime) / 1000 / 60;
|
||||||
|
|
||||||
|
const percent = (duration / (18 * 60 - 8 * 60)) * 100
|
||||||
|
|
||||||
|
if (percent > 100) {
|
||||||
|
console.log(start, end);
|
||||||
|
console.log(startTime, endTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
return percent + "%";
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConflitResolver {
|
||||||
|
constructor(assiduitesList, conflictPeriod, interval) {
|
||||||
|
this.list = assiduitesList;
|
||||||
|
this.conflictPeriod = conflictPeriod;
|
||||||
|
this.interval = interval;
|
||||||
|
this.selectedAssiduite = null;
|
||||||
|
|
||||||
|
this.element = undefined;
|
||||||
|
|
||||||
|
this.callbacks = {
|
||||||
|
delete: () => { },
|
||||||
|
split: () => { },
|
||||||
|
edit: () => { },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh(assiduitesList, periode) {
|
||||||
|
this.list = assiduitesList;
|
||||||
|
if (periode) {
|
||||||
|
this.conflictPeriod = periode;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.render()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
selectAssiduite() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
open() {
|
||||||
|
const html = `
|
||||||
|
<div id="myModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="close">×</span>
|
||||||
|
<h2>Veuillez régler le conflit pour poursuivre</h2>
|
||||||
|
<!-- Ajout de la frise chronologique -->
|
||||||
|
<div class="modal-timeline">
|
||||||
|
<div class="time-labels"></div>
|
||||||
|
<div class="assiduites-container"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-buttons">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-assiduite-content">
|
||||||
|
<h2>Information de l'assiduité sélectionnée</h2>
|
||||||
|
<div class="infos">
|
||||||
|
<p>Assiduite id : <span id="modal-assiduite-id">A</span></p>
|
||||||
|
<p>Etat : <span id="modal-assiduite-etat">B</span></p>
|
||||||
|
<p>Date de début : <span id="modal-assiduite-deb">C</span></p>
|
||||||
|
<p>Date de fin: <span id="modal-assiduite-fin">D</span></p>
|
||||||
|
<p>Module : <span id="modal-assiduite-module">E</span></p>
|
||||||
|
<p><span id="modal-assiduite-user">F</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.insertAdjacentHTML("afterbegin", html);
|
||||||
|
this.element = document.getElementById('myModal');
|
||||||
|
this.deleteBtn = document.querySelector('#myModal #delete');
|
||||||
|
this.editBtn = document.querySelector('#myModal #edit');
|
||||||
|
this.splitBtn = document.querySelector('#myModal #split');
|
||||||
|
this.deleteBtn.addEventListener('click', () => { this.deleteAssiduiteModal() });
|
||||||
|
this.editBtn.addEventListener('click', () => { this.editAssiduiteModal() });
|
||||||
|
this.splitBtn.addEventListener('click', () => { this.splitAssiduiteModal() });
|
||||||
|
|
||||||
|
document.querySelector('#myModal .close').addEventListener('click', () => { this.close() })
|
||||||
|
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (this.element) {
|
||||||
|
this.element.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sélection d'une assiduité sur la timeline
|
||||||
|
* @param {Assiduité} assiduite l'assiduité sélectionnée
|
||||||
|
*/
|
||||||
|
selectAssiduite(assiduite) {
|
||||||
|
// Désélectionner l'assiduité précédemment sélectionnée
|
||||||
|
if (this.selectedAssiduite) {
|
||||||
|
const prevSelectedEl = document.querySelector(
|
||||||
|
`.assiduite[data-id="${this.selectedAssiduite.assiduite_id}"]`
|
||||||
|
);
|
||||||
|
if (prevSelectedEl) {
|
||||||
|
prevSelectedEl.classList.remove("selected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sélectionner la nouvelle assiduité
|
||||||
|
this.selectedAssiduite = assiduite;
|
||||||
|
const selectedEl = document.querySelector(
|
||||||
|
`.assiduite[data-id="${assiduite.assiduite_id}"]`
|
||||||
|
);
|
||||||
|
if (selectedEl) {
|
||||||
|
selectedEl.classList.add("selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
//Mise à jour de la partie information du modal
|
||||||
|
const selectedModal = document.querySelector(".modal-assiduite-content");
|
||||||
|
|
||||||
|
selectedModal.classList.add("show");
|
||||||
|
|
||||||
|
document.getElementById("modal-assiduite-id").textContent =
|
||||||
|
assiduite.assiduite_id;
|
||||||
|
document.getElementById(
|
||||||
|
"modal-assiduite-user"
|
||||||
|
).textContent = `saisi le ${formatDateModal(
|
||||||
|
assiduite.entry_date,
|
||||||
|
"à"
|
||||||
|
)} \npar ${assiduite.user_id}`;
|
||||||
|
document.getElementById("modal-assiduite-module").textContent =
|
||||||
|
assiduite.moduleimpl_id;
|
||||||
|
document.getElementById("modal-assiduite-deb").textContent = formatDateModal(
|
||||||
|
assiduite.date_debut
|
||||||
|
);
|
||||||
|
document.getElementById("modal-assiduite-fin").textContent = formatDateModal(
|
||||||
|
assiduite.date_fin
|
||||||
|
);
|
||||||
|
document.getElementById("modal-assiduite-etat").textContent =
|
||||||
|
assiduite.etat.capitalize();
|
||||||
|
|
||||||
|
//Activation des boutons d'actions de conflit
|
||||||
|
this.deleteBtn.disabled = false;
|
||||||
|
this.splitBtn.disabled = false;
|
||||||
|
this.editBtn.disabled = false;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Suppression de l'assiduité sélectionnée
|
||||||
|
*/
|
||||||
|
deleteAssiduiteModal() {
|
||||||
|
if (!this.selectedAssiduite) return;
|
||||||
|
deleteAssiduite(this.selectedAssiduite.assiduite_id);
|
||||||
|
|
||||||
|
this.callbacks.delete(this.selectedAssiduite)
|
||||||
|
|
||||||
|
this.refresh(assiduites[this.selectedAssiduite.etudid]);
|
||||||
|
|
||||||
|
// Désélection de l'assiduité
|
||||||
|
this.resetSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Division d'une assiduité
|
||||||
|
*/
|
||||||
|
splitAssiduiteModal() {
|
||||||
|
//Préparation du prompt
|
||||||
|
const htmlPrompt = `<legend>Entrez l'heure de séparation (HH:mm) :</legend>
|
||||||
|
<input type="time" id="promptTime" name="appt"
|
||||||
|
min="08:00" max="18:00" required>`;
|
||||||
|
|
||||||
|
const fieldSet = document.createElement("fieldset");
|
||||||
|
fieldSet.classList.add("fieldsplit");
|
||||||
|
fieldSet.innerHTML = htmlPrompt;
|
||||||
|
|
||||||
|
//Callback de division
|
||||||
|
const success = () => {
|
||||||
|
const separatorTime = document.getElementById("promptTime").value;
|
||||||
|
const dateString =
|
||||||
|
document.querySelector("#tl_date").value + `T${separatorTime}`;
|
||||||
|
const separtorDate = new moment.tz(dateString, TIMEZONE);
|
||||||
|
|
||||||
|
const assiduite_debut = new moment.tz(this.selectedAssiduite.date_debut, TIMEZONE);
|
||||||
|
const assiduite_fin = new moment.tz(this.selectedAssiduite.date_fin, TIMEZONE);
|
||||||
|
|
||||||
|
if (
|
||||||
|
separtorDate.isAfter(assiduite_debut) &&
|
||||||
|
separtorDate.isBefore(assiduite_fin)
|
||||||
|
) {
|
||||||
|
const assiduite_avant = {
|
||||||
|
etat: this.selectedAssiduite.etat,
|
||||||
|
date_debut: assiduite_debut.format(),
|
||||||
|
date_fin: separtorDate.format(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const assiduite_apres = {
|
||||||
|
etat: this.selectedAssiduite.etat,
|
||||||
|
date_debut: separtorDate.format(),
|
||||||
|
date_fin: assiduite_fin.format(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.selectedAssiduite.moduleimpl_id) {
|
||||||
|
assiduite_apres["moduleimpl_id"] = this.selectedAssiduite.moduleimpl_id;
|
||||||
|
assiduite_avant["moduleimpl_id"] = this.selectedAssiduite.moduleimpl_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteAssiduite(this.selectedAssiduite.assiduite_id);
|
||||||
|
|
||||||
|
const path = getUrl() + `/api/assiduite/${this.selectedAssiduite.etudid}/create`;
|
||||||
|
sync_post(
|
||||||
|
path,
|
||||||
|
[assiduite_avant, assiduite_apres],
|
||||||
|
(data, status) => {
|
||||||
|
//success
|
||||||
|
},
|
||||||
|
(data, status) => {
|
||||||
|
//error
|
||||||
|
console.error(data, status);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.callbacks.split(this.selectedAssiduite)
|
||||||
|
this.refresh(assiduites[this.selectedAssiduite.etudid]);
|
||||||
|
this.resetSelection();
|
||||||
|
} else {
|
||||||
|
const att = document.createTextNode(
|
||||||
|
"L'heure de séparation doit être compris dans la période de l'assiduité sélectionnée."
|
||||||
|
);
|
||||||
|
|
||||||
|
openAlertModal("Attention", att, "", "#ecb52a");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
openPromptModal("Entrée demandée", fieldSet, success, () => { }, "#37f05f");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modification d'une assiduité conflictuelle
|
||||||
|
*/
|
||||||
|
editAssiduiteModal() {
|
||||||
|
if (!this.selectedAssiduite) return;
|
||||||
|
|
||||||
|
//Préparation du modal d'édition
|
||||||
|
const htmlPrompt = `<legend>Entrez l'état de l'assiduité :</legend>
|
||||||
|
<select name="promptSelect" id="promptSelect" required>
|
||||||
|
<option value="">Choissez l'état</option>
|
||||||
|
<option value="present">Présent</option>
|
||||||
|
<option value="retard">En Retard</option>
|
||||||
|
<option value="absent">Absent</option>
|
||||||
|
</select>`;
|
||||||
|
|
||||||
|
const fieldSet = document.createElement("fieldset");
|
||||||
|
fieldSet.classList.add("fieldsplit");
|
||||||
|
fieldSet.innerHTML = htmlPrompt;
|
||||||
|
|
||||||
|
//Callback d'action d'édition
|
||||||
|
const success = () => {
|
||||||
|
const newState = document.getElementById("promptSelect").value;
|
||||||
|
if (!["present", "absent", "retard"].includes(newState.toLowerCase())) {
|
||||||
|
const att = document.createTextNode(
|
||||||
|
"L'état doit être 'present', 'absent' ou 'retard'."
|
||||||
|
);
|
||||||
|
openAlertModal("Attention", att, "", "#ecb52a");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualiser l'affichage
|
||||||
|
|
||||||
|
editAssiduite(this.selectedAssiduite.assiduite_id, newState);
|
||||||
|
this.callbacks.edit(this.selectedAssiduite)
|
||||||
|
this.refresh(assiduites[this.selectedAssiduite.etudid]);
|
||||||
|
|
||||||
|
// Désélection de l'assiduité
|
||||||
|
this.resetSelection();
|
||||||
|
};
|
||||||
|
|
||||||
|
//Affichage du prompt
|
||||||
|
openPromptModal("Entrée demandée", fieldSet, success, () => { }, "#37f05f");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génération du modal
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
const timeLabels = document.querySelector(".time-labels");
|
||||||
|
const assiduitesContainer = document.querySelector(".assiduites-container");
|
||||||
|
|
||||||
|
timeLabels.innerHTML = "";
|
||||||
|
assiduitesContainer.innerHTML = '<div class="assiduite-special"></div>';
|
||||||
|
|
||||||
|
// Ajout des labels d'heure sur la frise chronologique
|
||||||
|
// TODO permettre la modification des bornes (8 et 18)
|
||||||
|
for (let i = 8; i <= 18; i++) {
|
||||||
|
const timeLabel = document.createElement("div");
|
||||||
|
timeLabel.className = "time-label";
|
||||||
|
timeLabel.textContent = i < 10 ? `0${i}:00` : `${i}:00`;
|
||||||
|
timeLabels.appendChild(timeLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Placement de la période conflictuelle sur la timeline
|
||||||
|
const specialAssiduiteEl = document.querySelector(".assiduite-special");
|
||||||
|
console.error("special")
|
||||||
|
specialAssiduiteEl.style.width = getWidth(
|
||||||
|
this.conflictPeriod.deb,
|
||||||
|
this.conflictPeriod.fin
|
||||||
|
);
|
||||||
|
specialAssiduiteEl.style.left = getLeftPosition(this.conflictPeriod.deb);
|
||||||
|
specialAssiduiteEl.style.top = "0";
|
||||||
|
specialAssiduiteEl.style.zIndex = "0"; // Place l'assiduité spéciale en arrière-plan
|
||||||
|
assiduitesContainer.appendChild(specialAssiduiteEl);
|
||||||
|
console.error(this.conflictPeriod)
|
||||||
|
console.error(specialAssiduiteEl)
|
||||||
|
|
||||||
|
//Placement des assiduités sur la timeline
|
||||||
|
this.list.forEach((assiduite) => {
|
||||||
|
const period = {
|
||||||
|
deb: new moment.tz(assiduite.date_debut, TIMEZONE),
|
||||||
|
fin: new moment.tz(assiduite.date_fin, TIMEZONE),
|
||||||
|
};
|
||||||
|
if (!hasTimeConflict(period, this.interval)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const el = document.createElement("div");
|
||||||
|
el.className = "assiduite";
|
||||||
|
el.style.backgroundColor = getColor(assiduite.etat);
|
||||||
|
console.error("normal")
|
||||||
|
|
||||||
|
el.style.width = getWidth(assiduite.date_debut, assiduite.date_fin);
|
||||||
|
el.style.left = getLeftPosition(assiduite.date_debut);
|
||||||
|
el.style.top = "10px";
|
||||||
|
el.setAttribute("data-id", assiduite.assiduite_id);
|
||||||
|
el.addEventListener("click", () => this.selectAssiduite(assiduite));
|
||||||
|
|
||||||
|
// Ajout des informations dans la visualisation d'une assiduité
|
||||||
|
const infoContainer = document.createElement("div");
|
||||||
|
infoContainer.className = "assiduite-info";
|
||||||
|
|
||||||
|
const idDiv = document.createElement("div");
|
||||||
|
idDiv.className = "assiduite-id";
|
||||||
|
idDiv.textContent = `ID: ${assiduite.assiduite_id}`;
|
||||||
|
infoContainer.appendChild(idDiv);
|
||||||
|
|
||||||
|
const periodDivDeb = document.createElement("div");
|
||||||
|
periodDivDeb.className = "assiduite-period";
|
||||||
|
periodDivDeb.textContent = `${formatDateModal(assiduite.date_debut)}`;
|
||||||
|
infoContainer.appendChild(periodDivDeb);
|
||||||
|
const periodDivFin = document.createElement("div");
|
||||||
|
periodDivFin.className = "assiduite-period";
|
||||||
|
periodDivFin.textContent = `${formatDateModal(assiduite.date_fin)}`;
|
||||||
|
infoContainer.appendChild(periodDivFin);
|
||||||
|
|
||||||
|
const stateDiv = document.createElement("div");
|
||||||
|
stateDiv.className = "assiduite-state";
|
||||||
|
stateDiv.textContent = `État: ${assiduite.etat.capitalize()}`;
|
||||||
|
infoContainer.appendChild(stateDiv);
|
||||||
|
|
||||||
|
const userIdDiv = document.createElement("div");
|
||||||
|
userIdDiv.className = "assiduite-user_id";
|
||||||
|
userIdDiv.textContent = `saisi le ${formatDateModal(
|
||||||
|
assiduite.entry_date,
|
||||||
|
"à"
|
||||||
|
)} \npar ${assiduite.user_id}`;
|
||||||
|
infoContainer.appendChild(userIdDiv);
|
||||||
|
|
||||||
|
el.appendChild(infoContainer);
|
||||||
|
assiduitesContainer.appendChild(el);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Remise à zéro de la sélection
|
||||||
|
* Désactivation des boutons d'actions de conflit
|
||||||
|
*/
|
||||||
|
resetSelection() {
|
||||||
|
this.selectedAssiduite = null;
|
||||||
|
this.deleteBtn.disabled = true;
|
||||||
|
this.splitBtn.disabled = true;
|
||||||
|
this.editBtn.disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
1014
app/templates/assiduites/widgets/differee.j2
Normal file
312
app/templates/assiduites/widgets/minitimeline.j2
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
<div class="assiduite-bubble">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
const mt_start = {{ t_start }};
|
||||||
|
const mt_end = {{ t_end }};
|
||||||
|
/**
|
||||||
|
* Création de la minitiline d'un étudiant
|
||||||
|
* @param {Array[Assiduité]} assiduitesArray
|
||||||
|
* @returns {HTMLElement} l'élément correspondant à la mini timeline
|
||||||
|
*/
|
||||||
|
function createMiniTimeline(assiduitesArray, day = null) {
|
||||||
|
const array = [...assiduitesArray];
|
||||||
|
const dateiso = day == null ? document.getElementById("tl_date").value : day;
|
||||||
|
const timeline = document.createElement("div");
|
||||||
|
timeline.className = "mini-timeline";
|
||||||
|
if (isSingleEtud()) {
|
||||||
|
timeline.classList.add("single");
|
||||||
|
}
|
||||||
|
const timelineDate = moment(dateiso).startOf("day");
|
||||||
|
const dayStart = timelineDate.clone().add(mt_start, "hours");
|
||||||
|
const dayEnd = timelineDate.clone().add(mt_end, "hours");
|
||||||
|
const dayDuration = moment.duration(dayEnd.diff(dayStart)).asMinutes();
|
||||||
|
|
||||||
|
timeline.appendChild(setMiniTick(timelineDate, dayStart, dayDuration));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (day == null) {
|
||||||
|
const tlTimes = getTimeLineTimes();
|
||||||
|
array.push({
|
||||||
|
date_debut: tlTimes.deb.format(),
|
||||||
|
date_fin: tlTimes.fin.format(),
|
||||||
|
etat: "CRENEAU",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
array.forEach((assiduité) => {
|
||||||
|
const startDate = moment(assiduité.date_debut);
|
||||||
|
const endDate = moment(assiduité.date_fin);
|
||||||
|
|
||||||
|
if (startDate.isBefore(dayStart)) {
|
||||||
|
startDate.startOf("day").add(mt_start, "hours");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endDate.isAfter(dayEnd)) {
|
||||||
|
endDate.startOf("day").add(mt_end, "hours");
|
||||||
|
}
|
||||||
|
|
||||||
|
const block = document.createElement("div");
|
||||||
|
block.className = "mini-timeline-block";
|
||||||
|
const duration = moment.duration(endDate.diff(startDate)).asMinutes();
|
||||||
|
const startOffset = moment.duration(startDate.diff(dayStart)).asMinutes();
|
||||||
|
const leftPercentage = (startOffset / dayDuration) * 100;
|
||||||
|
const widthPercentage = (duration / dayDuration) * 100;
|
||||||
|
|
||||||
|
block.style.left = `${leftPercentage}%`;
|
||||||
|
block.style.width = `${widthPercentage}%`;
|
||||||
|
|
||||||
|
if (assiduité.etat != "CRENEAU") {
|
||||||
|
block.addEventListener("click", () => {
|
||||||
|
let deb = startDate.hours() + startDate.minutes() / 60;
|
||||||
|
let fin = endDate.hours() + endDate.minutes() / 60;
|
||||||
|
deb = Math.max(mt_start, deb);
|
||||||
|
fin = Math.min(mt_end, fin);
|
||||||
|
|
||||||
|
if (day == null) setPeriodValues(deb, fin);
|
||||||
|
if (isSingleEtud()) {
|
||||||
|
updateSelectedSelect(getCurrentAssiduiteModuleImplId());
|
||||||
|
updateJustifyBtn();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
//ajouter affichage assiduites on over
|
||||||
|
setupAssiduiteBuble(block, assiduité);
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = (justificatifs) => {
|
||||||
|
if (justificatifs.length > 0) {
|
||||||
|
let j = "invalid_justified";
|
||||||
|
|
||||||
|
justificatifs.forEach((ju) => {
|
||||||
|
if (ju.etat == "VALIDE") {
|
||||||
|
j = "justified";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
block.classList.add(j);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (assiduité.etudid) {
|
||||||
|
getJustificatifFromPeriod(
|
||||||
|
{
|
||||||
|
deb: new moment.tz(assiduité.date_debut, TIMEZONE),
|
||||||
|
fin: new moment.tz(assiduité.date_fin, TIMEZONE),
|
||||||
|
},
|
||||||
|
assiduité.etudid,
|
||||||
|
action
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (assiduité.etat) {
|
||||||
|
case "PRESENT":
|
||||||
|
block.classList.add("present");
|
||||||
|
break;
|
||||||
|
case "RETARD":
|
||||||
|
block.classList.add("retard");
|
||||||
|
break;
|
||||||
|
case "ABSENT":
|
||||||
|
block.classList.add("absent");
|
||||||
|
break;
|
||||||
|
case "CRENEAU":
|
||||||
|
block.classList.add("creneau");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
block.style.backgroundColor = "white";
|
||||||
|
}
|
||||||
|
|
||||||
|
timeline.appendChild(block);
|
||||||
|
});
|
||||||
|
|
||||||
|
return timeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajout de la visualisation des assiduités de la mini timeline
|
||||||
|
* @param {HTMLElement} el l'élément survollé
|
||||||
|
* @param {Assiduité} assiduite l'assiduité représentée par l'élément
|
||||||
|
*/
|
||||||
|
function setupAssiduiteBuble(el, assiduite) {
|
||||||
|
if (!assiduite) return;
|
||||||
|
el.addEventListener("mouseenter", (event) => {
|
||||||
|
const bubble = document.querySelector(".assiduite-bubble");
|
||||||
|
bubble.className = "assiduite-bubble";
|
||||||
|
bubble.classList.add("is-active", assiduite.etat.toLowerCase());
|
||||||
|
|
||||||
|
bubble.innerHTML = "";
|
||||||
|
|
||||||
|
const idDiv = document.createElement("div");
|
||||||
|
idDiv.className = "assiduite-id";
|
||||||
|
idDiv.textContent = `ID: ${assiduite.assiduite_id}`;
|
||||||
|
bubble.appendChild(idDiv);
|
||||||
|
|
||||||
|
const periodDivDeb = document.createElement("div");
|
||||||
|
periodDivDeb.className = "assiduite-period";
|
||||||
|
periodDivDeb.textContent = `${formatDateModal(assiduite.date_debut)}`;
|
||||||
|
bubble.appendChild(periodDivDeb);
|
||||||
|
const periodDivFin = document.createElement("div");
|
||||||
|
periodDivFin.className = "assiduite-period";
|
||||||
|
periodDivFin.textContent = `${formatDateModal(assiduite.date_fin)}`;
|
||||||
|
bubble.appendChild(periodDivFin);
|
||||||
|
|
||||||
|
const stateDiv = document.createElement("div");
|
||||||
|
stateDiv.className = "assiduite-state";
|
||||||
|
stateDiv.textContent = `État: ${assiduite.etat.capitalize()}`;
|
||||||
|
bubble.appendChild(stateDiv);
|
||||||
|
|
||||||
|
const userIdDiv = document.createElement("div");
|
||||||
|
userIdDiv.className = "assiduite-user_id";
|
||||||
|
userIdDiv.textContent = `saisi le ${formatDateModal(
|
||||||
|
assiduite.entry_date,
|
||||||
|
"à"
|
||||||
|
)} \npar ${assiduite.user_id}`;
|
||||||
|
bubble.appendChild(userIdDiv);
|
||||||
|
|
||||||
|
bubble.style.left = `${event.clientX - bubble.offsetWidth / 2}px`;
|
||||||
|
bubble.style.top = `${event.clientY + 20}px`;
|
||||||
|
});
|
||||||
|
el.addEventListener("mouseout", () => {
|
||||||
|
const bubble = document.querySelector(".assiduite-bubble");
|
||||||
|
bubble.classList.remove("is-active");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMiniTick(timelineDate, dayStart, dayDuration) {
|
||||||
|
const endDate = timelineDate.clone().set({ 'hour': 13, 'minute': 0 });
|
||||||
|
const duration = moment.duration(endDate.diff(dayStart)).asMinutes();
|
||||||
|
const widthPercentage = (duration / dayDuration) * 100;
|
||||||
|
const tick = document.createElement('span');
|
||||||
|
tick.className = "mini_tick"
|
||||||
|
tick.textContent = "13h"
|
||||||
|
tick.style.left = `${widthPercentage}%`
|
||||||
|
|
||||||
|
return tick
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.assiduite-bubble {
|
||||||
|
position: fixed;
|
||||||
|
display: none;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 8px;
|
||||||
|
border: 3px solid #ccc;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
z-index: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assiduite-bubble.is-active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assiduite-bubble::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
border-width: 6px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: transparent transparent #f9f9f9 transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assiduite-bubble::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
border-width: 5px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: transparent transparent #ccc transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assiduite-id,
|
||||||
|
.assiduite-period,
|
||||||
|
.assiduite-state,
|
||||||
|
.assiduite-user_id {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assiduite-bubble.absent {
|
||||||
|
border-color: #F1A69C !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assiduite-bubble.present {
|
||||||
|
border-color: #9CF1AF !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assiduite-bubble.retard {
|
||||||
|
border-color: #F1D99C !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-timeline {
|
||||||
|
height: 7px;
|
||||||
|
border: 1px solid black;
|
||||||
|
position: relative;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-timeline.single {
|
||||||
|
height: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-timeline-block {
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#page-assiduite-content .mini-timeline-block {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini_tick {
|
||||||
|
position: absolute;
|
||||||
|
text-align: start;
|
||||||
|
top: -40px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 50;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini_tick::after {
|
||||||
|
display: block;
|
||||||
|
content: "|";
|
||||||
|
position: absolute;
|
||||||
|
bottom: -2px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-timeline-block.creneau {
|
||||||
|
outline: 3px solid #7059FF;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-timeline-block.absent {
|
||||||
|
background-color: #F1A69C !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-timeline-block.present {
|
||||||
|
background-color: #9CF1AF !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-timeline-block.retard {
|
||||||
|
background-color: #F1D99C !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-timeline-block.justified {
|
||||||
|
background-image: repeating-linear-gradient(135deg, transparent, transparent 4px, #7059FF 4px, #7059FF 8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-timeline-block.invalid_justified {
|
||||||
|
background-image: repeating-linear-gradient(135deg, transparent, transparent 4px, #d61616 4px, #d61616 8px);
|
||||||
|
}
|
||||||
|
</style>
|
135
app/templates/assiduites/widgets/moduleimpl_dynamic_selector.j2
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
<label for="moduleimpl_select">
|
||||||
|
Module
|
||||||
|
<select id="moduleimpl_select" class="dynaSelect">
|
||||||
|
<option value="" selected> Non spécifié </option>
|
||||||
|
<option value="autre"> Autre </option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
function getEtudFormSemestres() {
|
||||||
|
let semestre = {};
|
||||||
|
sync_get(getUrl() + `/api/etudiant/etudid/${etudid}/formsemestres`, (data) => {
|
||||||
|
semestre = data;
|
||||||
|
});
|
||||||
|
return semestre;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterFormSemestres(semestres, dateIso) {
|
||||||
|
const date = new moment.tz(
|
||||||
|
dateIso,
|
||||||
|
TIMEZONE
|
||||||
|
);
|
||||||
|
|
||||||
|
semestres = semestres.filter((fm) => {
|
||||||
|
return date.isBetween(fm.date_debut_iso, fm.date_fin_iso, null, '[]')
|
||||||
|
})
|
||||||
|
|
||||||
|
return semestres;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFormSemestreProgramme(fm_id) {
|
||||||
|
let semestre = {};
|
||||||
|
sync_get(getUrl() + `/api/formsemestre/${fm_id}/programme`, (data) => {
|
||||||
|
semestre = data;
|
||||||
|
});
|
||||||
|
return semestre;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getModulesImplByFormsemestre(semestres) {
|
||||||
|
const map = new Map();
|
||||||
|
|
||||||
|
semestres.forEach((fm) => {
|
||||||
|
const array = [];
|
||||||
|
|
||||||
|
const fm_p = getFormSemestreProgramme(fm.formsemestre_id);
|
||||||
|
["ressources", "saes", "modules"].forEach((r) => {
|
||||||
|
if (r in fm_p) {
|
||||||
|
fm_p[r].forEach((o) => {
|
||||||
|
array.push(getModuleInfos(o))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
map.set(fm.titre_num, array)
|
||||||
|
})
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getModuleInfos(obj) {
|
||||||
|
return {
|
||||||
|
moduleimpl_id: obj.moduleimpl_id,
|
||||||
|
titre: obj.module.titre,
|
||||||
|
code: obj.module.code,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function populateSelect(sems, selected, query) {
|
||||||
|
const select = document.querySelector(query);
|
||||||
|
select.innerHTML = `<option value=""> Non spécifié </option><option value="autre"> Autre </option>`
|
||||||
|
sems.forEach((mods, label) => {
|
||||||
|
const optGrp = document.createElement('optgroup');
|
||||||
|
optGrp.label = label
|
||||||
|
|
||||||
|
mods.forEach((obj) => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = obj.moduleimpl_id;
|
||||||
|
opt.textContent = `${obj.code} ${obj.titre}`
|
||||||
|
if (obj.moduleimpl_id == selected) {
|
||||||
|
opt.setAttribute('selected', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
optGrp.appendChild(opt);
|
||||||
|
})
|
||||||
|
select.appendChild(optGrp);
|
||||||
|
})
|
||||||
|
if (selected === "autre") {
|
||||||
|
select.querySelector('option[value="autre"]').setAttribute('selected', 'true');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSelect(moduleimpl_id, query = "#moduleimpl_select", dateIso = null) {
|
||||||
|
let sem = getEtudFormSemestres()
|
||||||
|
if (dateIso == null) {
|
||||||
|
dateIso = document.querySelector("#tl_date").value
|
||||||
|
}
|
||||||
|
sem = filterFormSemestres(sem, dateIso)
|
||||||
|
const mod = getModulesImplByFormsemestre(sem)
|
||||||
|
populateSelect(mod, moduleimpl_id, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSelectedSelect(moduleimpl_id, query = "#moduleimpl_select") {
|
||||||
|
const mod_id = moduleimpl_id != null ? moduleimpl_id : ""
|
||||||
|
document.querySelector(query).value = mod_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
window.addEventListener("load", () => {
|
||||||
|
document.getElementById('moduleimpl_select').addEventListener('change', (el) => {
|
||||||
|
const assi = getCurrentAssiduite(etudid);
|
||||||
|
if (assi) {
|
||||||
|
editAssiduite(assi.assiduite_id, assi.etat);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const conflicts = getAssiduitesConflict(etudid);
|
||||||
|
if (conflicts.length > 0) {
|
||||||
|
updateSelectedSelect(conflicts[0].moduleimpl_id);
|
||||||
|
}
|
||||||
|
}, { once: true });
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#moduleimpl_select {
|
||||||
|
width: 125px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
</style>
|
15
app/templates/assiduites/widgets/moduleimpl_selector.j2
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<select name="moduleimpl_select" id="moduleimpl_select">
|
||||||
|
|
||||||
|
<option value="" {{selected}}> Non spécifié </option>
|
||||||
|
<option value="autre"> Autre </option>
|
||||||
|
|
||||||
|
{% for mod in modules %}
|
||||||
|
{% if mod.moduleimpl_id == moduleimpl_id %}
|
||||||
|
<option value="{{mod.moduleimpl_id}}" selected> {{mod.name}} </option>
|
||||||
|
{% else %}
|
||||||
|
<option value="{{mod.moduleimpl_id}}"> {{mod.name}} </option>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
|
||||||
|
</select>
|
216
app/templates/assiduites/widgets/prompt.j2
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
{% block promptModal %}
|
||||||
|
<div id="promptModal" class="promptModal">
|
||||||
|
|
||||||
|
<!-- promptModal content -->
|
||||||
|
<div class="promptModal-content">
|
||||||
|
<div class="promptModal-header">
|
||||||
|
<span class="promptModal-close">×</span>
|
||||||
|
<h2 class="promptModal-title">promptModal Header</h2>
|
||||||
|
</div>
|
||||||
|
<div class="promptModal-body">
|
||||||
|
<p>Some text in the promptModal Body</p>
|
||||||
|
<p>Some other text...</p>
|
||||||
|
</div>
|
||||||
|
<div class="promptModal-footer">
|
||||||
|
<h3>promptModal Footer</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<style>
|
||||||
|
/* The promptModal (background) */
|
||||||
|
.promptModal {
|
||||||
|
display: none;
|
||||||
|
/* Hidden by default */
|
||||||
|
position: fixed;
|
||||||
|
/* Stay in place */
|
||||||
|
z-index: 750;
|
||||||
|
/* Sit on top */
|
||||||
|
padding-top: 3vh;
|
||||||
|
/* Location of the box */
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
/* Full width */
|
||||||
|
height: 100%;
|
||||||
|
/* Full height */
|
||||||
|
overflow: auto;
|
||||||
|
/* Enable scroll if needed */
|
||||||
|
background-color: rgb(0, 0, 0);
|
||||||
|
/* Fallback color */
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
/* Black w/ opacity */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* promptModal Content */
|
||||||
|
.promptModal-content {
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
background-color: #fefefe;
|
||||||
|
margin: auto;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid #888;
|
||||||
|
width: 45%;
|
||||||
|
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
|
||||||
|
-webkit-animation-name: animatetop;
|
||||||
|
-webkit-animation-duration: 0.4s;
|
||||||
|
animation-name: animatetop;
|
||||||
|
animation-duration: 0.4s
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add Animation */
|
||||||
|
@-webkit-keyframes animatetop {
|
||||||
|
from {
|
||||||
|
top: -300px;
|
||||||
|
opacity: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
top: 0;
|
||||||
|
opacity: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes animatetop {
|
||||||
|
from {
|
||||||
|
top: -300px;
|
||||||
|
opacity: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
top: 0;
|
||||||
|
opacity: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The Close Button */
|
||||||
|
.promptModal-close {
|
||||||
|
color: white;
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.promptModal-close:hover,
|
||||||
|
.promptModal-close:focus {
|
||||||
|
color: #000;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.promptModal-header {
|
||||||
|
padding: 2px 16px;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.promptModal-body {
|
||||||
|
padding: 2px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.promptModal-footer {
|
||||||
|
padding: 2px 16px;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.promptModal.is-active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnPrompt {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: #6c757d;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease ease;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnPrompt:hover {
|
||||||
|
background-color: #5a6268;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnPrompt:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnPrompt:disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const promptModal = document.getElementById("promptModal");
|
||||||
|
function openPromptModal(titre, contenu, success, cancel = () => { }, color = "crimson") {
|
||||||
|
promptModal.classList.add('is-active');
|
||||||
|
|
||||||
|
promptModal.querySelector('.promptModal-title').textContent = titre;
|
||||||
|
promptModal.querySelector('.promptModal-body').innerHTML = ""
|
||||||
|
promptModal.querySelector('.promptModal-body').appendChild(contenu);
|
||||||
|
|
||||||
|
promptModal.querySelector('.promptModal-footer').innerHTML = ""
|
||||||
|
promptModalButtonAction(success, cancel).forEach((btnPrompt) => {
|
||||||
|
promptModal.querySelector('.promptModal-footer').appendChild(btnPrompt)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const banners = Array.from(promptModal.querySelectorAll('.promptModal-footer,.promptModal-header'))
|
||||||
|
banners.forEach((ban) => {
|
||||||
|
ban.style.backgroundColor = color;
|
||||||
|
})
|
||||||
|
|
||||||
|
promptModal.addEventListener('click', (e) => {
|
||||||
|
if (e.target.id == promptModal.id) {
|
||||||
|
promptModal.classList.remove('is-active');
|
||||||
|
promptModal.removeEventListener('click', this)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function promptModalButtonAction(success, cancel) {
|
||||||
|
const succBtn = document.createElement('button')
|
||||||
|
succBtn.classList.add("btnPrompt")
|
||||||
|
succBtn.textContent = "Valider"
|
||||||
|
succBtn.addEventListener('click', () => {
|
||||||
|
const retour = success();
|
||||||
|
if (retour == null || retour == false || retour == undefined) {
|
||||||
|
closePromptModal();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const cancelBtn = document.createElement('button')
|
||||||
|
cancelBtn.classList.add("btnPrompt")
|
||||||
|
cancelBtn.textContent = "Annuler"
|
||||||
|
cancelBtn.addEventListener('click', () => {
|
||||||
|
cancel();
|
||||||
|
closePromptModal();
|
||||||
|
})
|
||||||
|
|
||||||
|
return [succBtn, cancelBtn]
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePromptModal() {
|
||||||
|
promptModal.classList.remove("is-active")
|
||||||
|
}
|
||||||
|
const promptClose = document.querySelector(".promptModal-close");
|
||||||
|
promptClose.onclick = function () {
|
||||||
|
closePromptModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
{% endblock promptModal %}
|
282
app/templates/assiduites/widgets/tableau_assi.j2
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
<table id="assiduiteTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<div>
|
||||||
|
<span>Début</span>
|
||||||
|
<a class="icon order" onclick="order('date_debut', assiduiteCallBack, this)"></a>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<div>
|
||||||
|
<span>Fin</span>
|
||||||
|
<a class="icon order" onclick="order('date_fin', assiduiteCallBack, this)"></a>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<div>
|
||||||
|
<span>État</span>
|
||||||
|
<a class="icon order" onclick="order('etat', assiduiteCallBack, this)"></a>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<div>
|
||||||
|
<span>Module</span>
|
||||||
|
<a class="icon order" onclick="order('moduleimpl_id', assiduiteCallBack, this)"></a>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<div>
|
||||||
|
<span>Justifiée</span>
|
||||||
|
<a class="icon order" onclick="order('est_just', assiduiteCallBack, this)"></a>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="tableBodyAssiduites">
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div id="paginationContainerAssiduites" class="pagination-container">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: none;" id="cache-module">
|
||||||
|
{% include "assiduites/widgets/moduleimpl_dynamic_selector.j2" %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const paginationContainerAssiduites = document.getElementById("paginationContainerAssiduites");
|
||||||
|
let currentPageAssiduites = 1;
|
||||||
|
let orderAssiduites = true;
|
||||||
|
let filterAssiduites = {
|
||||||
|
columns: [
|
||||||
|
"entry_date", "date_debut", "date_fin", "etat", "moduleimpl_id", "est_just"
|
||||||
|
],
|
||||||
|
filters: {}
|
||||||
|
}
|
||||||
|
const tableBodyAssiduites = document.getElementById("tableBodyAssiduites");
|
||||||
|
|
||||||
|
function assiduiteCallBack(assi) {
|
||||||
|
assi = filterArray(assi, filterAssiduites.filters)
|
||||||
|
renderTableAssiduites(currentPageAssiduites, assi);
|
||||||
|
renderPaginationButtons(assi);
|
||||||
|
|
||||||
|
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) {
|
||||||
|
|
||||||
|
generateTableHead(filterAssiduites.columns, true)
|
||||||
|
|
||||||
|
tableBodyAssiduites.innerHTML = "";
|
||||||
|
const start = (page - 1) * itemsPerPage;
|
||||||
|
const end = start + itemsPerPage;
|
||||||
|
|
||||||
|
assiduités.slice(start, end).forEach((assiduite) => {
|
||||||
|
const row = document.createElement("tr");
|
||||||
|
row.setAttribute('type', "assiduite");
|
||||||
|
row.setAttribute('obj_id', assiduite.assiduite_id);
|
||||||
|
|
||||||
|
const etat = assiduite.etat.toLowerCase();
|
||||||
|
row.classList.add(`l-${etat}`);
|
||||||
|
filterAssiduites.columns.forEach((k) => {
|
||||||
|
const td = document.createElement('td');
|
||||||
|
if (k.indexOf('date') != -1) {
|
||||||
|
td.textContent = moment.tz(assiduite[k], TIMEZONE).format(`DD/MM/Y HH:mm`)
|
||||||
|
} else if (k.indexOf("module") != -1) {
|
||||||
|
td.textContent = getModuleImpl(assiduite);
|
||||||
|
} else if (k.indexOf('est_just') != -1) {
|
||||||
|
td.textContent = assiduite[k] ? "Oui" : "Non"
|
||||||
|
} else {
|
||||||
|
td.textContent = assiduite[k].capitalize()
|
||||||
|
}
|
||||||
|
|
||||||
|
row.appendChild(td)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
row.addEventListener("contextmenu", openContext);
|
||||||
|
|
||||||
|
tableBodyAssiduites.appendChild(row);
|
||||||
|
});
|
||||||
|
updateActivePaginationButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
function detailAssiduites(assiduite_id) {
|
||||||
|
const path = getUrl() + `/api/assiduite/${assiduite_id}`;
|
||||||
|
async_get(
|
||||||
|
path,
|
||||||
|
(data) => {
|
||||||
|
const user = data.user_id;
|
||||||
|
const module = getModuleImpl(data);
|
||||||
|
|
||||||
|
const date_debut = moment.tz(data.date_debut, TIMEZONE).format("DD/MM/YYYY HH:mm");
|
||||||
|
const date_fin = moment.tz(data.date_fin, TIMEZONE).format("DD/MM/YYYY HH:mm");
|
||||||
|
const entry_date = moment.tz(data.entry_date, TIMEZONE).format("DD/MM/YYYY HH:mm");
|
||||||
|
|
||||||
|
const etat = data.etat.capitalize();
|
||||||
|
const desc = data.desc == null ? "" : data.desc.replace("Module:Autre\n", "");
|
||||||
|
const id = data.assiduite_id;
|
||||||
|
const est_just = data.est_just ? "Oui" : "Non";
|
||||||
|
|
||||||
|
const html = `
|
||||||
|
<div class="obj-detail">
|
||||||
|
<div class="obj-dates">
|
||||||
|
<div id="date_debut" class="obj-part">
|
||||||
|
<span class="obj-title">Date de début</span>
|
||||||
|
<span class="obj-content">${date_debut}</span>
|
||||||
|
</div>
|
||||||
|
<div id="date_fin" class="obj-part">
|
||||||
|
<span class="obj-title">Date de fin</span>
|
||||||
|
<span class="obj-content">${date_fin}</span>
|
||||||
|
</div>
|
||||||
|
<div id="entry_date" class="obj-part">
|
||||||
|
<span class="obj-title">Date de saisie</span>
|
||||||
|
<span class="obj-content">${entry_date}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="obj-mod">
|
||||||
|
<div id="module" class="obj-part">
|
||||||
|
<span class="obj-title">Module</span>
|
||||||
|
<span class="obj-content">${module}</span>
|
||||||
|
</div>
|
||||||
|
<div id="etat" class="obj-part">
|
||||||
|
<span class="obj-title">Etat</span>
|
||||||
|
<span class="obj-content">${etat}</span>
|
||||||
|
</div>
|
||||||
|
<div id="user" class="obj-part">
|
||||||
|
<span class="obj-title">Créer par</span>
|
||||||
|
<span class="obj-content">${user}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="obj-rest">
|
||||||
|
<div id="est_just" class="obj-part">
|
||||||
|
<span class="obj-title">Justifié</span>
|
||||||
|
<span class="obj-content">${est_just}</span>
|
||||||
|
</div>
|
||||||
|
<div id="desc" class="obj-part">
|
||||||
|
<span class="obj-title">Description</span>
|
||||||
|
<p class="obj-content">${desc}</p>
|
||||||
|
</div>
|
||||||
|
<div id="id" class="obj-part">
|
||||||
|
<span class="obj-title">Identifiant de l'assiduité</span>
|
||||||
|
<span class="obj-content">${id}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.innerHTML = html;
|
||||||
|
|
||||||
|
openAlertModal("Détails", el.firstElementChild, null, "green")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function editionAssiduites(assiduite_id) {
|
||||||
|
const path = getUrl() + `/api/assiduite/${assiduite_id}`;
|
||||||
|
async_get(
|
||||||
|
path,
|
||||||
|
(data) => {
|
||||||
|
let module = data.moduleimpl_id;
|
||||||
|
|
||||||
|
const etat = data.etat;
|
||||||
|
let desc = data.desc == null ? "" : data.desc;
|
||||||
|
if (desc.indexOf("Module:Autre\n") != -1) {
|
||||||
|
desc = data.desc.replace("Module:Autre\n", "");
|
||||||
|
module = "autre";
|
||||||
|
}
|
||||||
|
const html = `
|
||||||
|
<div class="assi-edit">
|
||||||
|
<div class="assi-edit-part">
|
||||||
|
<legend>État de l'assiduité</legend>
|
||||||
|
<select name="etat" id="etat">
|
||||||
|
<option value="present">Présent</option>
|
||||||
|
<option value="retard">En Retard</option>
|
||||||
|
<option value="absent">Absent</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="assi-edit-part">
|
||||||
|
<legend>Module</legend>
|
||||||
|
<select name="module" id="module">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="assi-edit-part">
|
||||||
|
<legend>Description</legend>
|
||||||
|
<textarea name="desc" id="desc" cols="50" rows="10" maxlength="500"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
const el = document.createElement('div')
|
||||||
|
el.innerHTML = html;
|
||||||
|
const assiEdit = el.firstElementChild;
|
||||||
|
|
||||||
|
assiEdit.querySelector('#etat').value = etat.toLowerCase();
|
||||||
|
assiEdit.querySelector('#desc').value = desc != null ? desc : "";
|
||||||
|
updateSelect(module, '#moduleimpl_select', "2022-09-04")
|
||||||
|
assiEdit.querySelector('#module').replaceWith(document.querySelector('#moduleimpl_select').cloneNode(true));
|
||||||
|
openPromptModal("Modification de l'assiduité", assiEdit, () => {
|
||||||
|
const prompt = document.querySelector('.assi-edit');
|
||||||
|
const etat = prompt.querySelector('#etat').value;
|
||||||
|
const desc = prompt.querySelector('#desc').value;
|
||||||
|
let module = prompt.querySelector('#moduleimpl_select').value;
|
||||||
|
let edit = {
|
||||||
|
"etat": etat,
|
||||||
|
"desc": desc,
|
||||||
|
}
|
||||||
|
|
||||||
|
edit = setModuleImplId(edit, module);
|
||||||
|
|
||||||
|
fullEditAssiduites(data.assiduite_id, edit, () => {
|
||||||
|
try { getAllAssiduitesFromEtud(etudid, assiduiteCallBack) } catch (_) { }
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
}, () => { }, "green");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fullEditAssiduites(assiduite_id, obj, call = () => { }) {
|
||||||
|
const path = getUrl() + `/api/assiduite/${assiduite_id}/edit`;
|
||||||
|
async_post(
|
||||||
|
path,
|
||||||
|
obj,
|
||||||
|
call,
|
||||||
|
(data, status) => {
|
||||||
|
//error
|
||||||
|
console.error(data, status);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</script>
|
824
app/templates/assiduites/widgets/tableau_base.j2
Normal file
@ -0,0 +1,824 @@
|
|||||||
|
<ul id="contextMenu" class="context-menu">
|
||||||
|
<li id="detailOption">Détails</li>
|
||||||
|
<li id="editOption">Éditer</li>
|
||||||
|
<li id="deleteOption">Supprimer</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{% include "assiduites/widgets/alert.j2" %}
|
||||||
|
{% include "assiduites/widgets/prompt.j2" %}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const itemsPerPage = 10;
|
||||||
|
const contextMenu = document.getElementById("contextMenu");
|
||||||
|
const editOption = document.getElementById("editOption");
|
||||||
|
const detailOption = document.getElementById("detailOption");
|
||||||
|
const deleteOption = document.getElementById("deleteOption");
|
||||||
|
|
||||||
|
let selectedRow;
|
||||||
|
|
||||||
|
document.addEventListener("click", () => {
|
||||||
|
contextMenu.style.display = "none";
|
||||||
|
});
|
||||||
|
|
||||||
|
editOption.addEventListener("click", () => {
|
||||||
|
if (selectedRow) {
|
||||||
|
const type = selectedRow.getAttribute('type');
|
||||||
|
const obj_id = selectedRow.getAttribute('obj_id');
|
||||||
|
|
||||||
|
if (type == "assiduite") {
|
||||||
|
editionAssiduites(obj_id);
|
||||||
|
} else {
|
||||||
|
editionJustificatifs(obj_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
detailOption.addEventListener("click", () => {
|
||||||
|
if (selectedRow) {
|
||||||
|
const type = selectedRow.getAttribute('type');
|
||||||
|
const obj_id = selectedRow.getAttribute('obj_id');
|
||||||
|
|
||||||
|
if (type == "assiduite") {
|
||||||
|
detailAssiduites(obj_id);
|
||||||
|
} else {
|
||||||
|
detailJustificatifs(obj_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
deleteOption.addEventListener("click", () => {
|
||||||
|
if (selectedRow) {
|
||||||
|
const type = selectedRow.getAttribute('type');
|
||||||
|
const obj_id = selectedRow.getAttribute('obj_id');
|
||||||
|
if (type == "assiduite") {
|
||||||
|
deleteAssiduite(obj_id);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
deleteJustificatif(obj_id);
|
||||||
|
}
|
||||||
|
loadAll();
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function filterArray(array, f) {
|
||||||
|
return array.filter((el) => {
|
||||||
|
let t = Object.keys(f).every((k) => {
|
||||||
|
if (k == "etat") {
|
||||||
|
return f.etat.includes(el.etat.toLowerCase())
|
||||||
|
};
|
||||||
|
if (k == "est_just") {
|
||||||
|
if (f.est_just != "") {
|
||||||
|
return `${el.est_just}` == f.est_just;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (k.indexOf('date') != -1) {
|
||||||
|
const assi_time = moment.tz(el[k], TIMEZONE);
|
||||||
|
const filter_time = f[k].time;
|
||||||
|
switch (f[k].pref) {
|
||||||
|
|
||||||
|
case "0":
|
||||||
|
return assi_time.isSame(filter_time, 'minute');
|
||||||
|
case "-1":
|
||||||
|
return assi_time.isBefore(filter_time, 'minutes');
|
||||||
|
case "1":
|
||||||
|
return assi_time.isAfter(filter_time, 'minutes');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (k == "moduleimpl_id") {
|
||||||
|
const m = el[k] == undefined || el[k] == null ? "null" : el[k];
|
||||||
|
if (f.moduleimpl_id != '') {
|
||||||
|
return m == f.moduleimpl_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
|
||||||
|
return t;
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateTableHead(columns, assi = true) {
|
||||||
|
const table = assi ? "#assiduiteTable" : "#justificatifTable"
|
||||||
|
const call = assi ? [assiduiteCallBack, true] : [justificatifCallBack, false]
|
||||||
|
const tr = document.querySelector(`${table} thead tr`);
|
||||||
|
|
||||||
|
tr.innerHTML = ""
|
||||||
|
|
||||||
|
columns.forEach((c) => {
|
||||||
|
const th = document.createElement('th');
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.textContent = columnTranslator(c);
|
||||||
|
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.classList.add('icon', "order");
|
||||||
|
a.onclick = () => { order(c, call[0], a, call[1]) }
|
||||||
|
|
||||||
|
div.appendChild(span)
|
||||||
|
div.appendChild(a)
|
||||||
|
|
||||||
|
th.appendChild(div);
|
||||||
|
|
||||||
|
tr.appendChild(th);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPaginationButtons(array, assi = true) {
|
||||||
|
const totalPages = Math.ceil(array.length / itemsPerPage);
|
||||||
|
if (totalPages <= 1) {
|
||||||
|
if (assi) {
|
||||||
|
paginationContainerAssiduites.innerHTML = ""
|
||||||
|
} else {
|
||||||
|
paginationContainerJustificatifs.innerHTML = ""
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assi) {
|
||||||
|
paginationContainerAssiduites.innerHTML = "<span class='liste_pagination'><button class='pagination_moins'><</button><select id='paginationAssi'></select><button class='pagination_plus'>></button></span>"
|
||||||
|
paginationContainerAssiduites.querySelector('#paginationAssi')?.addEventListener('change', (e) => {
|
||||||
|
currentPageAssiduites = e.target.value;
|
||||||
|
assiduiteCallBack(array);
|
||||||
|
})
|
||||||
|
|
||||||
|
paginationContainerAssiduites.querySelector('.pagination_moins').addEventListener('click', () => {
|
||||||
|
if (currentPageAssiduites > 1) {
|
||||||
|
currentPageAssiduites--;
|
||||||
|
paginationContainerAssiduites.querySelector('#paginationAssi').value = currentPageAssiduites
|
||||||
|
assiduiteCallBack(array);
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
paginationContainerAssiduites.querySelector('.pagination_plus').addEventListener('click', () => {
|
||||||
|
if (currentPageAssiduites < totalPages) {
|
||||||
|
currentPageAssiduites++;
|
||||||
|
paginationContainerAssiduites.querySelector('#paginationAssi').value = currentPageAssiduites
|
||||||
|
assiduiteCallBack(array);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
paginationContainerJustificatifs.innerHTML = "<span class='liste_pagination'><button class='pagination_moins'><</button><select id='paginationJusti'></select><button class='pagination_plus'>></button></span>"
|
||||||
|
paginationContainerJustificatifs.querySelector('#paginationJusti')?.addEventListener('change', (e) => {
|
||||||
|
currentPageJustificatifs = e.target.value;
|
||||||
|
justificatifCallBack(array);
|
||||||
|
})
|
||||||
|
|
||||||
|
paginationContainerJustificatifs.querySelector('.pagination_moins').addEventListener('click', () => {
|
||||||
|
if (currentPageJustificatifs > 1) {
|
||||||
|
currentPageJustificatifs--;
|
||||||
|
paginationContainerJustificatifs.querySelector('#paginationJusti').value = currentPageAssiduites
|
||||||
|
justificatifCallBack(array);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
paginationContainerJustificatifs.querySelector('.pagination_plus').addEventListener('click', () => {
|
||||||
|
if (currentPageJustificatifs < totalPages) {
|
||||||
|
currentPageJustificatifs++;
|
||||||
|
paginationContainerJustificatifs.querySelector('#paginationJusti').value = currentPageAssiduites
|
||||||
|
justificatifCallBack(array);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
|
const paginationButton = document.createElement("option");
|
||||||
|
paginationButton.textContent = i;
|
||||||
|
paginationButton.value = i;
|
||||||
|
|
||||||
|
if (assi) {
|
||||||
|
paginationContainerAssiduites.querySelector('#paginationAssi').appendChild(paginationButton)
|
||||||
|
} else {
|
||||||
|
paginationContainerJustificatifs.querySelector('#paginationJusti').appendChild(paginationButton)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateActivePaginationButton(assi);
|
||||||
|
}
|
||||||
|
function updateActivePaginationButton(assi = true) {
|
||||||
|
if (assi) {
|
||||||
|
const paginationButtons =
|
||||||
|
paginationContainerAssiduites.querySelectorAll("#paginationContainerAssiduites .pagination-button");
|
||||||
|
paginationButtons.forEach((button) => {
|
||||||
|
if (parseInt(button.textContent) === currentPageAssiduites) {
|
||||||
|
button.classList.add("active");
|
||||||
|
} else {
|
||||||
|
button.classList.remove("active");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const paginationButtons =
|
||||||
|
paginationContainerJustificatifs.querySelectorAll("#paginationContainerJustificatifs .pagination-button");
|
||||||
|
paginationButtons.forEach((button) => {
|
||||||
|
if (parseInt(button.textContent) === currentPageJustificatifs) {
|
||||||
|
button.classList.add("active");
|
||||||
|
} else {
|
||||||
|
button.classList.remove("active");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadAll() {
|
||||||
|
try { getAllAssiduitesFromEtud(etudid, assiduiteCallBack) } catch (_) { }
|
||||||
|
try { getAllJustificatifsFromEtud(etudid, justificatifCallBack) } catch (_) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
function order(keyword, callback = () => { }, el, assi = true) {
|
||||||
|
const call = (array, ordered) => {
|
||||||
|
const sorted = array.sort((a, b) => {
|
||||||
|
let keyValueA = a[keyword];
|
||||||
|
let keyValueB = b[keyword];
|
||||||
|
|
||||||
|
if (keyword.indexOf("date") != -1) {
|
||||||
|
keyValueA = moment.tz(keyValueA, TIMEZONE)
|
||||||
|
keyValueB = moment.tz(keyValueB, TIMEZONE)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyword.indexOf("module") != -1) {
|
||||||
|
keyValueA = getModuleImpl(keyValueA);
|
||||||
|
keyValueB = getModuleImpl(keyValueB);
|
||||||
|
}
|
||||||
|
|
||||||
|
let orderDertermined = keyValueA > keyValueB;
|
||||||
|
|
||||||
|
if (!ordered) {
|
||||||
|
orderDertermined = keyValueA < keyValueB;
|
||||||
|
}
|
||||||
|
return orderDertermined
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
callback(sorted);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
if (assi) {
|
||||||
|
orderAssiduites = !orderAssiduites;
|
||||||
|
getAllAssiduitesFromEtud(etudid, (a) => { call(a, orderAssiduites) })
|
||||||
|
} else {
|
||||||
|
orderJustificatifs = !orderJustificatifs;
|
||||||
|
getAllJustificatifsFromEtud(etudid, (a) => { call(a, orderJustificatifs) })
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function filter(assi = true) {
|
||||||
|
if (assi) {
|
||||||
|
let html = `
|
||||||
|
<div class="filter-body">
|
||||||
|
<h3>Affichage des colonnes:</h3>
|
||||||
|
<div class="filter-head">
|
||||||
|
<label>
|
||||||
|
Date de saisie
|
||||||
|
<input class="chk" type="checkbox" name="entry_date" id="entry_date">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Date de Début
|
||||||
|
<input class="chk" type="checkbox" name="date_debut" id="date_debut" checked>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Date de Fin
|
||||||
|
<input class="chk" type="checkbox" name="date_fin" id="date_fin" checked>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Etat
|
||||||
|
<input class="chk" type="checkbox" name="etat" id="etat" checked>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Module
|
||||||
|
<input class="chk" type="checkbox" name="moduleimpl_id" id="moduleimpl_id" checked>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Justifiée
|
||||||
|
<input class="chk" type="checkbox" name="est_just" id="est_just" checked>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<h3>Filtrage des colonnes:</h3>
|
||||||
|
<span class="filter-line">
|
||||||
|
<span class="filter-title" for="entry_date">Date de saisie</span>
|
||||||
|
<select name="entry_date_pref" id="entry_date_pref">
|
||||||
|
<option value="-1">Avant</option>
|
||||||
|
<option value="0">Égal</option>
|
||||||
|
<option value="1">Après</option>
|
||||||
|
</select>
|
||||||
|
<input type="datetime-local" name="entry_date_time" id="entry_date_time">
|
||||||
|
</span>
|
||||||
|
<span class="filter-line">
|
||||||
|
<span class="filter-title" for="date_debut">Date de début</span>
|
||||||
|
<select name="date_debut_pref" id="date_debut_pref">
|
||||||
|
<option value="-1">Avant</option>
|
||||||
|
<option value="0">Égal</option>
|
||||||
|
<option value="1">Après</option>
|
||||||
|
</select>
|
||||||
|
<input type="datetime-local" name="date_debut_time" id="date_debut_time">
|
||||||
|
</span>
|
||||||
|
<span class="filter-line">
|
||||||
|
<span class="filter-title" for="date_fin">Date de fin</span>
|
||||||
|
<select name="date_fin_pref" id="date_fin_pref">
|
||||||
|
<option value="-1">Avant</option>
|
||||||
|
<option value="0">Égal</option>
|
||||||
|
<option value="1">Après</option>
|
||||||
|
</select>
|
||||||
|
<input type="datetime-local" name="date_fin_time" id="date_fin_time">
|
||||||
|
</span>
|
||||||
|
<span class="filter-line">
|
||||||
|
<span class="filter-title" for="etat">Etat</span>
|
||||||
|
<input checked type="checkbox" name="etat_present" id="etat_present" class="rbtn present" value="present">
|
||||||
|
<input checked type="checkbox" name="etat_retard" id="etat_retard" class="rbtn retard" value="retard">
|
||||||
|
<input checked type="checkbox" name="etat_absent" id="etat_absent" class="rbtn absent" value="absent">
|
||||||
|
</span>
|
||||||
|
<span class="filter-line">
|
||||||
|
<span class="filter-title" for="moduleimpl_id">Module</span>
|
||||||
|
<select id="moduleimpl_id">
|
||||||
|
<option value="">Pas de filtre</option>
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
<span class="filter-line">
|
||||||
|
<span class="filter-title" for="est_just">Est Justifiée</span>
|
||||||
|
<select id="est_just">
|
||||||
|
<option value="">Pas de filtre</option>
|
||||||
|
<option value="true">Oui</option>
|
||||||
|
<option value="false">Non</option>
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.innerHTML = html
|
||||||
|
html = span.firstElementChild
|
||||||
|
|
||||||
|
const filterHead = html.querySelector('.filter-head');
|
||||||
|
filterHead.innerHTML = ""
|
||||||
|
let cols = ["entry_date", "date_debut", "date_fin", "etat", "moduleimpl_id", "est_just"];
|
||||||
|
|
||||||
|
cols.forEach((k) => {
|
||||||
|
const label = document.createElement('label')
|
||||||
|
label.classList.add('f-label')
|
||||||
|
const s = document.createElement('span');
|
||||||
|
s.textContent = columnTranslator(k);
|
||||||
|
|
||||||
|
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.classList.add('chk')
|
||||||
|
input.type = "checkbox"
|
||||||
|
input.name = k
|
||||||
|
input.id = k;
|
||||||
|
input.checked = filterAssiduites.columns.includes(k)
|
||||||
|
|
||||||
|
label.appendChild(s)
|
||||||
|
label.appendChild(input)
|
||||||
|
filterHead.appendChild(label)
|
||||||
|
})
|
||||||
|
|
||||||
|
const sl = html.querySelector('.filter-line #moduleimpl_id');
|
||||||
|
let opts = []
|
||||||
|
Object.keys(moduleimpls).forEach((k) => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = k == null ? "null" : k;
|
||||||
|
opt.textContent = moduleimpls[k];
|
||||||
|
opts.push(opt);
|
||||||
|
})
|
||||||
|
|
||||||
|
opts = opts.sort((a, b) => {
|
||||||
|
return a.value < b.value
|
||||||
|
})
|
||||||
|
|
||||||
|
sl.append(...opts);
|
||||||
|
|
||||||
|
// Mise à jour des filtres
|
||||||
|
|
||||||
|
Object.keys(filterAssiduites.filters).forEach((key) => {
|
||||||
|
const l = html.querySelector(`.filter-title[for="${key}"]`).parentElement;
|
||||||
|
if (key.indexOf('date') != -1) {
|
||||||
|
l.querySelector(`#${key}_pref`).value = filterAssiduites.filters[key].pref;
|
||||||
|
l.querySelector(`#${key}_time`).value = filterAssiduites.filters[key].time.format("YYYY-MM-DDTHH:mm");
|
||||||
|
|
||||||
|
} else if (key.indexOf('etat') != -1) {
|
||||||
|
l.querySelectorAll('input').forEach((e) => {
|
||||||
|
e.checked = filterAssiduites.filters[key].includes(e.value)
|
||||||
|
})
|
||||||
|
} else if (key.indexOf("module") != -1) {
|
||||||
|
l.querySelector('#moduleimpl_id').value = filterAssiduites.filters[key];
|
||||||
|
} else if (key.indexOf("est_just") != -1) {
|
||||||
|
l.querySelector('#est_just').value = filterAssiduites.filters[key];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
openPromptModal("Filtrage des assiduités", html, () => {
|
||||||
|
|
||||||
|
const columns = [...document.querySelectorAll('.chk')]
|
||||||
|
.map((el) => { if (el.checked) return el.id })
|
||||||
|
.filter((el) => el)
|
||||||
|
|
||||||
|
filterAssiduites.columns = columns
|
||||||
|
filterAssiduites.filters = {}
|
||||||
|
//reste des filtres
|
||||||
|
|
||||||
|
const lines = [...document.querySelectorAll('.filter-line')];
|
||||||
|
|
||||||
|
lines.forEach((l) => {
|
||||||
|
const key = l.querySelector('.filter-title').getAttribute('for');
|
||||||
|
|
||||||
|
if (key.indexOf('date') != -1) {
|
||||||
|
const pref = l.querySelector(`#${key}_pref`).value;
|
||||||
|
const time = l.querySelector(`#${key}_time`).value;
|
||||||
|
if (l.querySelector(`#${key}_time`).value != "") {
|
||||||
|
filterAssiduites.filters[key] = {
|
||||||
|
pref: pref,
|
||||||
|
time: new moment.tz(time, TIMEZONE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (key.indexOf('etat') != -1) {
|
||||||
|
filterAssiduites.filters[key] = [...l.querySelectorAll("input:checked")].map((e) => e.value);
|
||||||
|
} else if (key.indexOf("module") != -1) {
|
||||||
|
filterAssiduites.filters[key] = l.querySelector('#moduleimpl_id').value;
|
||||||
|
} else if (key.indexOf("est_just") != -1) {
|
||||||
|
filterAssiduites.filters[key] = l.querySelector('#est_just').value;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
getAllAssiduitesFromEtud(etudid, assiduiteCallBack)
|
||||||
|
|
||||||
|
}, () => { }, "#7059FF");
|
||||||
|
} else {
|
||||||
|
let html = `
|
||||||
|
<div class="filter-body">
|
||||||
|
<h3>Affichage des colonnes:</h3>
|
||||||
|
<div class="filter-head">
|
||||||
|
<label>
|
||||||
|
Date de saisie
|
||||||
|
<input class="chk" type="checkbox" name="entry_date" id="entry_date">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Date de Début
|
||||||
|
<input class="chk" type="checkbox" name="date_debut" id="date_debut" checked>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Date de Fin
|
||||||
|
<input class="chk" type="checkbox" name="date_fin" id="date_fin" checked>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Etat
|
||||||
|
<input class="chk" type="checkbox" name="etat" id="etat" checked>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Raison
|
||||||
|
<input class="chk" type="checkbox" name="raison" id="raison" checked>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Fichier
|
||||||
|
<input class="chk" type="checkbox" name="fichier" id="fichier" checked>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<h3>Filtrage des colonnes:</h3>
|
||||||
|
<span class="filter-line">
|
||||||
|
<span class="filter-title" for="entry_date">Date de saisie</span>
|
||||||
|
<select name="entry_date_pref" id="entry_date_pref">
|
||||||
|
<option value="-1">Avant</option>
|
||||||
|
<option value="0">Égal</option>
|
||||||
|
<option value="1">Après</option>
|
||||||
|
</select>
|
||||||
|
<input type="datetime-local" name="entry_date_time" id="entry_date_time">
|
||||||
|
</span>
|
||||||
|
<span class="filter-line">
|
||||||
|
<span class="filter-title" for="date_debut">Date de début</span>
|
||||||
|
<select name="date_debut_pref" id="date_debut_pref">
|
||||||
|
<option value="-1">Avant</option>
|
||||||
|
<option value="0">Égal</option>
|
||||||
|
<option value="1">Après</option>
|
||||||
|
</select>
|
||||||
|
<input type="datetime-local" name="date_debut_time" id="date_debut_time">
|
||||||
|
</span>
|
||||||
|
<span class="filter-line">
|
||||||
|
<span class="filter-title" for="date_fin">Date de fin</span>
|
||||||
|
<select name="date_fin_pref" id="date_fin_pref">
|
||||||
|
<option value="-1">Avant</option>
|
||||||
|
<option value="0">Égal</option>
|
||||||
|
<option value="1">Après</option>
|
||||||
|
</select>
|
||||||
|
<input type="datetime-local" name="date_fin_time" id="date_fin_time">
|
||||||
|
</span>
|
||||||
|
<span class="filter-line">
|
||||||
|
<span class="filter-title" for="etat">Etat</span>
|
||||||
|
<label>
|
||||||
|
Valide
|
||||||
|
<input checked type="checkbox" name="etat_valide" id="etat_valide" class="" value="valide">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Non Valide
|
||||||
|
<input checked type="checkbox" name="etat_valide" id="etat_valide" class="" value="non_valide">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
En Attente
|
||||||
|
<input checked type="checkbox" name="etat_valide" id="etat_valide" class="" value="attente">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Modifié
|
||||||
|
<input checked type="checkbox" name="etat_valide" id="etat_valide" class="" value="modifie">
|
||||||
|
</label>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.innerHTML = html
|
||||||
|
html = span.firstElementChild
|
||||||
|
|
||||||
|
const filterHead = html.querySelector('.filter-head');
|
||||||
|
filterHead.innerHTML = ""
|
||||||
|
let cols = ["entry_date", "date_debut", "date_fin", "etat", "raison", "fichier"];
|
||||||
|
|
||||||
|
cols.forEach((k) => {
|
||||||
|
const label = document.createElement('label')
|
||||||
|
label.classList.add('f-label')
|
||||||
|
const s = document.createElement('span');
|
||||||
|
s.textContent = columnTranslator(k);
|
||||||
|
|
||||||
|
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.classList.add('chk')
|
||||||
|
input.type = "checkbox"
|
||||||
|
input.name = k
|
||||||
|
input.id = k;
|
||||||
|
input.checked = filterJustificatifs.columns.includes(k)
|
||||||
|
|
||||||
|
label.appendChild(s)
|
||||||
|
label.appendChild(input)
|
||||||
|
filterHead.appendChild(label)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mise à jour des filtres
|
||||||
|
|
||||||
|
Object.keys(filterJustificatifs.filters).forEach((key) => {
|
||||||
|
const l = html.querySelector(`.filter-title[for="${key}"]`).parentElement;
|
||||||
|
if (key.indexOf('date') != -1) {
|
||||||
|
l.querySelector(`#${key}_pref`).value = filterJustificatifs.filters[key].pref;
|
||||||
|
l.querySelector(`#${key}_time`).value = filterJustificatifs.filters[key].time.format("YYYY-MM-DDTHH:mm");
|
||||||
|
|
||||||
|
} else if (key.indexOf('etat') != -1) {
|
||||||
|
l.querySelectorAll('input').forEach((e) => {
|
||||||
|
e.checked = filterJustificatifs.filters[key].includes(e.value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
openPromptModal("Filtrage des Justificatifs", html, () => {
|
||||||
|
|
||||||
|
const columns = [...document.querySelectorAll('.chk')]
|
||||||
|
.map((el) => { if (el.checked) return el.id })
|
||||||
|
.filter((el) => el)
|
||||||
|
|
||||||
|
filterJustificatifs.columns = columns
|
||||||
|
filterJustificatifs.filters = {}
|
||||||
|
//reste des filtres
|
||||||
|
|
||||||
|
const lines = [...document.querySelectorAll('.filter-line')];
|
||||||
|
|
||||||
|
lines.forEach((l) => {
|
||||||
|
const key = l.querySelector('.filter-title').getAttribute('for');
|
||||||
|
|
||||||
|
if (key.indexOf('date') != -1) {
|
||||||
|
const pref = l.querySelector(`#${key}_pref`).value;
|
||||||
|
const time = l.querySelector(`#${key}_time`).value;
|
||||||
|
if (l.querySelector(`#${key}_time`).value != "") {
|
||||||
|
filterJustificatifs.filters[key] = {
|
||||||
|
pref: pref,
|
||||||
|
time: new moment.tz(time, TIMEZONE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (key.indexOf('etat') != -1) {
|
||||||
|
filterJustificatifs.filters[key] = [...l.querySelectorAll("input:checked")].map((e) => e.value);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
getAllJustificatifsFromEtud(etudid, justificatifCallBack)
|
||||||
|
|
||||||
|
}, () => { }, "#7059FF");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function columnTranslator(colName) {
|
||||||
|
switch (colName) {
|
||||||
|
case "date_debut":
|
||||||
|
return "Début";
|
||||||
|
case "entry_date":
|
||||||
|
return "Saisie le";
|
||||||
|
case "date_fin":
|
||||||
|
return "Fin";
|
||||||
|
case "etat":
|
||||||
|
return "État";
|
||||||
|
case "moduleimpl_id":
|
||||||
|
return "Module";
|
||||||
|
case "est_just":
|
||||||
|
return "Justifiée";
|
||||||
|
case "raison":
|
||||||
|
return "Raison";
|
||||||
|
case "fichier":
|
||||||
|
return "Fichier";
|
||||||
|
case "etudid":
|
||||||
|
return "Etudiant";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openContext(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
selectedRow = e.target.parentElement;
|
||||||
|
contextMenu.style.top = `${e.clientY - contextMenu.offsetHeight}px`;
|
||||||
|
contextMenu.style.left = `${e.clientX}px`;
|
||||||
|
contextMenu.style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.pageContent {
|
||||||
|
width: 100%;
|
||||||
|
max-width: var(--sco-content-max-width);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
|
||||||
|
text-align: left;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
border: 1px solid #dddddd;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover {
|
||||||
|
filter: brightness(1.2)
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 10px 0;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu li {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu li:hover {
|
||||||
|
filter: brightness(0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
#deleteOption {
|
||||||
|
background-color: #F1A69C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.l-present {
|
||||||
|
background-color: #9CF1AF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.l-absent,
|
||||||
|
.l-invalid {
|
||||||
|
background-color: #F1A69C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.l-valid {
|
||||||
|
background-color: #8f7eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.l-retard {
|
||||||
|
background-color: #F1D99C;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ajoutez des styles pour le conteneur de pagination et les boutons */
|
||||||
|
.pagination-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-button {
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
margin: 0 5px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-button:hover {
|
||||||
|
background-color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-button.active {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
th>div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-head {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-line {
|
||||||
|
display: flex;
|
||||||
|
justify-content: start;
|
||||||
|
align-items: center;
|
||||||
|
margin: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-line>* {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.rbtn {
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
margin: 0 5px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.f-label {
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chk {
|
||||||
|
margin-left: 2px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-body label {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obj-title {
|
||||||
|
text-decoration: underline #bbb;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obj-part {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 33%;
|
||||||
|
padding: 5px;
|
||||||
|
border: 1px solid #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obj-dates,
|
||||||
|
.obj-mod,
|
||||||
|
.obj-rest {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.liste_pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
478
app/templates/assiduites/widgets/tableau_justi.j2
Normal file
@ -0,0 +1,478 @@
|
|||||||
|
<table id="justificatifTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<div>
|
||||||
|
<span>Début</span>
|
||||||
|
<a class="icon order" onclick="order('date_debut', justificatifCallBack, this, false)"></a>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<div>
|
||||||
|
<span>Fin</span>
|
||||||
|
<a class="icon order" onclick="order('date_fin', justificatifCallBack, this, false)"></a>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<div>
|
||||||
|
<span>État</span>
|
||||||
|
<a class="icon order" onclick="order('etat', justificatifCallBack, this, false)"></a>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<div>
|
||||||
|
<span>Raison</span>
|
||||||
|
<a class="icon order" onclick="order('raison', justificatifCallBack, this, false)"></a>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<div>
|
||||||
|
<span>Fichier</span>
|
||||||
|
<a class="icon order" onclick="order('fichier', justificatifCallBack, this, false)"></a>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="tableBodyJustificatifs">
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div id="paginationContainerJustificatifs" class="pagination-container">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const paginationContainerJustificatifs = document.getElementById("paginationContainerJustificatifs");
|
||||||
|
let currentPageJustificatifs = 1;
|
||||||
|
let orderJustificatifs = true;
|
||||||
|
let filterJustificatifs = {
|
||||||
|
columns: [
|
||||||
|
"entry_date", "date_debut", "date_fin", "etat", "raison", "fichier"
|
||||||
|
],
|
||||||
|
filters: {}
|
||||||
|
}
|
||||||
|
const tableBodyJustificatifs = document.getElementById("tableBodyJustificatifs");
|
||||||
|
|
||||||
|
function justificatifCallBack(justi) {
|
||||||
|
justi = filterArray(justi, filterJustificatifs.filters)
|
||||||
|
renderTableJustificatifs(currentPageJustificatifs, justi);
|
||||||
|
renderPaginationButtons(justi, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getEtudiant(id) {
|
||||||
|
if (id in etuds) {
|
||||||
|
return etuds[id];
|
||||||
|
}
|
||||||
|
getSingleEtud(id);
|
||||||
|
|
||||||
|
return etuds[id];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTableJustificatifs(page, justificatifs) {
|
||||||
|
generateTableHead(filterJustificatifs.columns, false)
|
||||||
|
|
||||||
|
tableBodyJustificatifs.innerHTML = "";
|
||||||
|
const start = (page - 1) * itemsPerPage;
|
||||||
|
const end = start + itemsPerPage;
|
||||||
|
|
||||||
|
justificatifs.slice(start, end).forEach((justificatif) => {
|
||||||
|
const row = document.createElement("tr");
|
||||||
|
row.setAttribute('type', "justificatif");
|
||||||
|
row.setAttribute('obj_id', justificatif.justif_id);
|
||||||
|
|
||||||
|
const etat = justificatif.etat.toLowerCase();
|
||||||
|
|
||||||
|
if (etat == "valide") {
|
||||||
|
row.classList.add(`l-valid`);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
row.classList.add(`l-invalid`);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
filterJustificatifs.columns.forEach((k) => {
|
||||||
|
const td = document.createElement('td');
|
||||||
|
if (k.indexOf('date') != -1) {
|
||||||
|
td.textContent = moment.tz(justificatif[k], TIMEZONE).format(`DD/MM/Y HH:mm`)
|
||||||
|
} else if (k.indexOf('fichier') != -1) {
|
||||||
|
td.textContent = justificatif.fichier ? "Oui" : "Non";
|
||||||
|
} else if (k.indexOf('etudid') != -1) {
|
||||||
|
const e = getEtudiant(justificatif.etudid);
|
||||||
|
|
||||||
|
td.textContent = `${e.prenom.capitalize()} ${e.nom.toUpperCase()}`;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
td.textContent = `${justificatif[k]}`.capitalize()
|
||||||
|
}
|
||||||
|
|
||||||
|
row.appendChild(td)
|
||||||
|
})
|
||||||
|
|
||||||
|
row.addEventListener("contextmenu", openContext);
|
||||||
|
tableBodyJustificatifs.appendChild(row);
|
||||||
|
});
|
||||||
|
updateActivePaginationButton(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function detailJustificatifs(justi_id) {
|
||||||
|
const path = getUrl() + `/api/justificatif/${justi_id}`;
|
||||||
|
async_get(
|
||||||
|
path,
|
||||||
|
(data) => {
|
||||||
|
const user = data.user_id;
|
||||||
|
const date_debut = moment.tz(data.date_debut, TIMEZONE).format("DD/MM/YYYY HH:mm");
|
||||||
|
const date_fin = moment.tz(data.date_fin, TIMEZONE).format("DD/MM/YYYY HH:mm");
|
||||||
|
const entry_date = moment.tz(data.entry_date, TIMEZONE).format("DD/MM/YYYY HH:mm");
|
||||||
|
|
||||||
|
const etat = data.etat.capitalize();
|
||||||
|
const desc = data.raison == null ? "" : data.raison;
|
||||||
|
const id = data.justif_id;
|
||||||
|
const fichier = data.fichier != null ? "Oui" : "Non";
|
||||||
|
let filenames = []
|
||||||
|
let totalFiles = 0;
|
||||||
|
if (fichier) {
|
||||||
|
sync_get(path + "/list", (data2) => {
|
||||||
|
filenames = data2.filenames;
|
||||||
|
totalFiles = data2.total;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = `
|
||||||
|
<div class="obj-detail">
|
||||||
|
<div class="obj-dates">
|
||||||
|
<div id="date_debut" class="obj-part">
|
||||||
|
<span class="obj-title">Date de début</span>
|
||||||
|
<span class="obj-content">${date_debut}</span>
|
||||||
|
</div>
|
||||||
|
<div id="date_fin" class="obj-part">
|
||||||
|
<span class="obj-title">Date de fin</span>
|
||||||
|
<span class="obj-content">${date_fin}</span>
|
||||||
|
</div>
|
||||||
|
<div id="entry_date" class="obj-part">
|
||||||
|
<span class="obj-title">Date de saisie</span>
|
||||||
|
<span class="obj-content">${entry_date}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="obj-mod">
|
||||||
|
<div id="module" class="obj-part">
|
||||||
|
<span class="obj-title">Raison</span>
|
||||||
|
<span class="obj-content">${desc}</span>
|
||||||
|
</div>
|
||||||
|
<div id="etat" class="obj-part">
|
||||||
|
<span class="obj-title">Etat</span>
|
||||||
|
<span class="obj-content">${etat}</span>
|
||||||
|
</div>
|
||||||
|
<div id="user" class="obj-part">
|
||||||
|
<span class="obj-title">Créer par</span>
|
||||||
|
<span class="obj-content">${user}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="obj-rest">
|
||||||
|
<div id="est_just" class="obj-part obj-66">
|
||||||
|
<span class="obj-title">Fichier(s)</span>
|
||||||
|
<div class="obj-content" id="fich-content"></div>
|
||||||
|
</div>
|
||||||
|
<div id="id" class="obj-part">
|
||||||
|
<span class="obj-title">Identifiant du justificatif</span>
|
||||||
|
<span class="obj-content">${id}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.innerHTML = html;
|
||||||
|
|
||||||
|
const fichContent = el.querySelector('#fich-content');
|
||||||
|
const s = document.createElement('span')
|
||||||
|
s.textContent = `${totalFiles} fichier(s) dont ${filenames.length} visible(s)`
|
||||||
|
|
||||||
|
fichContent.appendChild(s)
|
||||||
|
|
||||||
|
filenames.forEach((name) => {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.textContent = name
|
||||||
|
a.classList.add("fich-file")
|
||||||
|
|
||||||
|
a.onclick = () => { downloadFile(id, name) };
|
||||||
|
|
||||||
|
fichContent.appendChild(a);
|
||||||
|
})
|
||||||
|
|
||||||
|
openAlertModal("Détails", el.firstElementChild, null, "green")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadFile(id, name) {
|
||||||
|
const path = getUrl() + `/api/justificatif/${id}/export/${name}`;
|
||||||
|
|
||||||
|
fetch(path, {
|
||||||
|
method: "POST"
|
||||||
|
|
||||||
|
})
|
||||||
|
// This returns a promise inside of which we are checking for errors from the server.
|
||||||
|
// The catch promise at the end of the call does not getting called when the server returns an error.
|
||||||
|
// More information about the error catching can be found here: https://www.tjvantoll.com/2015/09/13/fetch-and-errors/.
|
||||||
|
.then((result) => {
|
||||||
|
if (!result.ok) {
|
||||||
|
throw Error(result.statusText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We are reading the *Content-Disposition* header for getting the original filename given from the server
|
||||||
|
const header = result.headers.get('Content-Disposition');
|
||||||
|
const parts = header.split(';');
|
||||||
|
filename = parts[1].split('=')[1].replaceAll("\"", "");
|
||||||
|
|
||||||
|
return result.blob();
|
||||||
|
})
|
||||||
|
// We use the download property for triggering the download of the file from our browser.
|
||||||
|
// More information about the following code can be found here: https://stackoverflow.com/questions/32545632/how-can-i-download-a-file-using-window-fetch.
|
||||||
|
// The filename from the first promise is used as name of the file.
|
||||||
|
.then((blob) => {
|
||||||
|
if (blob != null) {
|
||||||
|
var url = window.URL.createObjectURL(blob);
|
||||||
|
var a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// The catch is getting called only for client-side errors.
|
||||||
|
// For example the throw in the first then-promise, which is the error that came from the server.
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function editionJustificatifs(justif_id) {
|
||||||
|
const path = getUrl() + `/api/justificatif/${justif_id}`;
|
||||||
|
async_get(
|
||||||
|
path,
|
||||||
|
(data) => {
|
||||||
|
const html = `
|
||||||
|
<div class="assi-edit">
|
||||||
|
<div class="justi-row">
|
||||||
|
<div class="justi-label">
|
||||||
|
<legend for="justi_date_debut">Date de début</legend>
|
||||||
|
<input type="datetime-local" name="justi_date_debut" id="justi_date_debut">
|
||||||
|
</div>
|
||||||
|
<div class="justi-label">
|
||||||
|
<legend for="justi_date_fin">Date de fin</legend>
|
||||||
|
<input type="datetime-local" name="justi_date_fin" id="justi_date_fin">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="justi-row">
|
||||||
|
<div class="justi-label">
|
||||||
|
<legend for="justi_etat">Etat du justificatif</legend>
|
||||||
|
<select name="justi_etat" id="justi_etat">
|
||||||
|
<option value="attente" selected>En Attente de validation</option>
|
||||||
|
<option value="non_valide">Non Valide</option>
|
||||||
|
<option value="modifie">Modifié</option>
|
||||||
|
<option value="valide">Valide</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="justi-row">
|
||||||
|
<div class="justi-label">
|
||||||
|
<legend for="justi_raison">Raison</legend>
|
||||||
|
<textarea name="justi_raison" id="justi_raison" cols="50" rows="10" maxlength="500"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="justi-row">
|
||||||
|
<div class="justi-sect">
|
||||||
|
</div>
|
||||||
|
<div class="justi-label">
|
||||||
|
<legend for="justi_fich">Importer un fichier</legend>
|
||||||
|
<input type="file" name="justi_fich" id="justi_fich" multiple>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
const desc = data.raison
|
||||||
|
const fichier = data.fichier != null ? "Oui" : "Non";
|
||||||
|
|
||||||
|
|
||||||
|
const el = document.createElement('div')
|
||||||
|
el.innerHTML = html;
|
||||||
|
const assiEdit = el.firstElementChild;
|
||||||
|
|
||||||
|
assiEdit.querySelector('#justi_etat').value = data.etat.toLowerCase();
|
||||||
|
assiEdit.querySelector('#justi_raison').value = desc != null ? desc : "";
|
||||||
|
|
||||||
|
assiEdit.querySelector('#justi_date_debut').value = moment.tz(data.date_debut, TIMEZONE).format("YYYY-MM-DDTHH:MM")
|
||||||
|
assiEdit.querySelector('#justi_date_fin').value = moment.tz(data.date_fin, TIMEZONE).format("YYYY-MM-DDTHH:MM")
|
||||||
|
|
||||||
|
const fichContent = assiEdit.querySelector('.justi-sect');
|
||||||
|
|
||||||
|
let filenames = []
|
||||||
|
let totalFiles = 0;
|
||||||
|
if (data.fichier) {
|
||||||
|
sync_get(path + "/list", (data2) => {
|
||||||
|
filenames = data2.filenames;
|
||||||
|
totalFiles = data2.total;
|
||||||
|
})
|
||||||
|
let html = "<legend>Fichier(s)</legend>"
|
||||||
|
html += `<span>${totalFiles} fichier(s) dont ${filenames.length} visible(s)</span>`
|
||||||
|
fichContent.insertAdjacentHTML('beforeend', html)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
filenames.forEach((name) => {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.textContent = name
|
||||||
|
a.classList.add("fich-file")
|
||||||
|
|
||||||
|
a.onclick = () => { downloadFile(id, name) };
|
||||||
|
|
||||||
|
const input = document.createElement('input')
|
||||||
|
input.type = "checkbox"
|
||||||
|
input.name = "destroyFile";
|
||||||
|
input.classList.add('icon')
|
||||||
|
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.classList.add('file-line')
|
||||||
|
span.appendChild(input)
|
||||||
|
span.appendChild(a)
|
||||||
|
|
||||||
|
|
||||||
|
fichContent.appendChild(span);
|
||||||
|
})
|
||||||
|
|
||||||
|
openPromptModal("Modification du justificatif", assiEdit, () => {
|
||||||
|
const prompt = document.querySelector('.assi-edit');
|
||||||
|
|
||||||
|
let date_debut = prompt.querySelector('#justi_date_debut').value;
|
||||||
|
let date_fin = prompt.querySelector('#justi_date_fin').value;
|
||||||
|
|
||||||
|
if (date_debut == "" || date_fin == "") {
|
||||||
|
openAlertModal("Dates erronées", document.createTextNode('Les dates sont invalides'));
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
date_debut = moment.tz(date_debut, TIMEZONE)
|
||||||
|
date_fin = moment.tz(date_fin, TIMEZONE)
|
||||||
|
|
||||||
|
if (date_debut >= date_fin) {
|
||||||
|
openAlertModal("Dates erronées", document.createTextNode('La date de fin doit être après la date de début'));
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const edit = {
|
||||||
|
date_debut: date_debut.format(),
|
||||||
|
date_fin: date_fin.format(),
|
||||||
|
raison: prompt.querySelector('#justi_raison').value,
|
||||||
|
etat: prompt.querySelector('#justi_etat').value,
|
||||||
|
}
|
||||||
|
|
||||||
|
const toRemoveFiles = [...prompt.querySelectorAll('[name="destroyFile"]:checked')]
|
||||||
|
|
||||||
|
if (toRemoveFiles.length > 0) {
|
||||||
|
removeFiles(justif_id, toRemoveFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
const in_files = prompt.querySelector('#justi_fich');
|
||||||
|
|
||||||
|
if (in_files.files.length > 0) {
|
||||||
|
importNewFiles(justif_id, in_files);
|
||||||
|
}
|
||||||
|
|
||||||
|
fullEditJustificatifs(data.justif_id, edit, () => {
|
||||||
|
loadAll();
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
}, () => { }, "green");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fullEditJustificatifs(justif_id, obj, call = () => { }) {
|
||||||
|
const path = getUrl() + `/api/justificatif/${justif_id}/edit`;
|
||||||
|
async_post(
|
||||||
|
path,
|
||||||
|
obj,
|
||||||
|
call,
|
||||||
|
(data, status) => {
|
||||||
|
//error
|
||||||
|
console.error(data, status);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeFiles(justif_id, files = []) {
|
||||||
|
const path = getUrl() + `/api/justificatif/${justif_id}/remove`;
|
||||||
|
files = files.map((el) => {
|
||||||
|
return el.parentElement.querySelector('a').textContent;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(justif_id, files);
|
||||||
|
sync_post(
|
||||||
|
path,
|
||||||
|
{
|
||||||
|
"remove": "list",
|
||||||
|
"filenames": files,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function importNewFiles(justif_id, in_files) {
|
||||||
|
const path = getUrl() + `/api/justificatif/${justif_id}/import`;
|
||||||
|
|
||||||
|
const requests = []
|
||||||
|
Array.from(in_files.files).forEach((f) => {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('file', f);
|
||||||
|
requests.push(
|
||||||
|
$.ajax(
|
||||||
|
{
|
||||||
|
url: path,
|
||||||
|
type: 'POST',
|
||||||
|
data: fd,
|
||||||
|
dateType: 'json',
|
||||||
|
contentType: false,
|
||||||
|
processData: false,
|
||||||
|
success: () => { },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
$.when(
|
||||||
|
requests
|
||||||
|
).done(() => {
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.fich-file {
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fich-content {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obj-66 {
|
||||||
|
width: 66%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-line {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
312
app/templates/assiduites/widgets/timeline.j2
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
<div class="timeline-container">
|
||||||
|
<div class="period" style="left: 0%; width: 20%">
|
||||||
|
<div class="period-handle left"></div>
|
||||||
|
<div class="period-handle right"></div>
|
||||||
|
<div class="period-time">Time</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
|
||||||
|
const timelineContainer = document.querySelector(".timeline-container");
|
||||||
|
const periodTimeLine = document.querySelector(".period");
|
||||||
|
const t_start = {{ t_start }};
|
||||||
|
const t_end = {{ t_end }};
|
||||||
|
|
||||||
|
const tick_time = 60 / {{ tick_time }};
|
||||||
|
const tick_delay = 1 / tick_time;
|
||||||
|
|
||||||
|
const period_default = {{ periode_defaut }};
|
||||||
|
|
||||||
|
function createTicks() {
|
||||||
|
let i = t_start;
|
||||||
|
|
||||||
|
while (i <= t_end) {
|
||||||
|
const hourTick = document.createElement("div");
|
||||||
|
hourTick.classList.add("tick", "hour");
|
||||||
|
hourTick.style.left = `${((i - t_start) / (t_end - t_start)) * 100}%`;
|
||||||
|
timelineContainer.appendChild(hourTick);
|
||||||
|
|
||||||
|
const tickLabel = document.createElement("div");
|
||||||
|
tickLabel.classList.add("tick-label");
|
||||||
|
tickLabel.style.left = `${((i - t_start) / (t_end - t_start)) * 100}%`;
|
||||||
|
tickLabel.textContent = numberToTime(i);
|
||||||
|
timelineContainer.appendChild(tickLabel);
|
||||||
|
|
||||||
|
if (i < t_end) {
|
||||||
|
let j = Math.floor(i + 1);
|
||||||
|
|
||||||
|
while (i < j) {
|
||||||
|
i += tick_delay;
|
||||||
|
|
||||||
|
if (i <= t_end) {
|
||||||
|
const quarterTick = document.createElement("div");
|
||||||
|
quarterTick.classList.add("tick", "quarter");
|
||||||
|
quarterTick.style.left = `${computePercentage(i, t_start)}%`;
|
||||||
|
timelineContainer.appendChild(quarterTick);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
i = j;
|
||||||
|
} else {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function numberToTime(num) {
|
||||||
|
const integer = Math.floor(num);
|
||||||
|
const decimal = Math.round((num % 1) * 60);
|
||||||
|
|
||||||
|
let dec = `:${decimal}`;
|
||||||
|
if (decimal < 10) {
|
||||||
|
dec = `:0${decimal}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let int = `${integer}`;
|
||||||
|
if (integer < 10) {
|
||||||
|
int = `0${integer}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return int + dec;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function snapToQuarter(value) {
|
||||||
|
|
||||||
|
|
||||||
|
return Math.round(value * tick_time) / tick_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePeriodTimeLabel() {
|
||||||
|
const values = getPeriodValues();
|
||||||
|
const deb = numberToTime(values[0])
|
||||||
|
const fin = numberToTime(values[1])
|
||||||
|
const text = `${deb} - ${fin}`
|
||||||
|
periodTimeLine.querySelector('.period-time').textContent = text;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupTimeLine(callback) {
|
||||||
|
const func_call = callback ? callback : () => { };
|
||||||
|
timelineContainer.addEventListener("mousedown", (event) => {
|
||||||
|
const startX = event.clientX;
|
||||||
|
|
||||||
|
if (event.target === periodTimeLine) {
|
||||||
|
const startLeft = parseFloat(periodTimeLine.style.left);
|
||||||
|
|
||||||
|
const onMouseMove = (moveEvent) => {
|
||||||
|
const deltaX = moveEvent.clientX - startX;
|
||||||
|
const containerWidth = timelineContainer.clientWidth;
|
||||||
|
const newLeft = startLeft + (deltaX / containerWidth) * 100;
|
||||||
|
|
||||||
|
adjustPeriodPosition(newLeft, parseFloat(periodTimeLine.style.width));
|
||||||
|
|
||||||
|
updatePeriodTimeLabel();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("mousemove", onMouseMove);
|
||||||
|
document.addEventListener(
|
||||||
|
"mouseup",
|
||||||
|
() => {
|
||||||
|
generateAllEtudRow();
|
||||||
|
snapHandlesToQuarters();
|
||||||
|
document.removeEventListener("mousemove", onMouseMove);
|
||||||
|
func_call();
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
} else if (event.target.classList.contains("period-handle")) {
|
||||||
|
const startWidth = parseFloat(periodTimeLine.style.width);
|
||||||
|
const startLeft = parseFloat(periodTimeLine.style.left);
|
||||||
|
const isLeftHandle = event.target.classList.contains("left");
|
||||||
|
|
||||||
|
const onMouseMove = (moveEvent) => {
|
||||||
|
const deltaX = moveEvent.clientX - startX;
|
||||||
|
const containerWidth = timelineContainer.clientWidth;
|
||||||
|
const newWidth =
|
||||||
|
startWidth + ((isLeftHandle ? -deltaX : deltaX) / containerWidth) * 100;
|
||||||
|
|
||||||
|
if (isLeftHandle) {
|
||||||
|
const newLeft = startLeft + (deltaX / containerWidth) * 100;
|
||||||
|
adjustPeriodPosition(newLeft, newWidth);
|
||||||
|
} else {
|
||||||
|
adjustPeriodPosition(parseFloat(periodTimeLine.style.left), newWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePeriodTimeLabel();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("mousemove", onMouseMove);
|
||||||
|
document.addEventListener(
|
||||||
|
"mouseup",
|
||||||
|
() => {
|
||||||
|
snapHandlesToQuarters();
|
||||||
|
generateAllEtudRow();
|
||||||
|
|
||||||
|
document.removeEventListener("mousemove", onMouseMove);
|
||||||
|
|
||||||
|
func_call();
|
||||||
|
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function adjustPeriodPosition(newLeft, newWidth) {
|
||||||
|
|
||||||
|
const snappedLeft = snapToQuarter(newLeft);
|
||||||
|
const snappedWidth = snapToQuarter(newWidth);
|
||||||
|
const minLeft = 0;
|
||||||
|
const maxLeft = 100 - snappedWidth;
|
||||||
|
|
||||||
|
const clampedLeft = Math.min(Math.max(snappedLeft, minLeft), maxLeft);
|
||||||
|
|
||||||
|
periodTimeLine.style.left = `${clampedLeft}%`;
|
||||||
|
periodTimeLine.style.width = `${snappedWidth}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPeriodValues() {
|
||||||
|
const leftPercentage = parseFloat(periodTimeLine.style.left);
|
||||||
|
const widthPercentage = parseFloat(periodTimeLine.style.width);
|
||||||
|
|
||||||
|
const startHour = (leftPercentage / 100) * (t_end - t_start) + t_start;
|
||||||
|
const endHour = ((leftPercentage + widthPercentage) / 100) * (t_end - t_start) + t_start;
|
||||||
|
|
||||||
|
const startValue = snapToQuarter(startHour);
|
||||||
|
const endValue = snapToQuarter(endHour);
|
||||||
|
|
||||||
|
const computedValues = [Math.max(startValue, t_start), Math.min(t_end, endValue)];
|
||||||
|
|
||||||
|
if (computedValues[0] > t_end || computedValues[1] < t_start) {
|
||||||
|
return [t_start, min(t_end, t_start + period_default)];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (computedValues[1] - computedValues[0] <= tick_delay && computedValues[1] < t_end - tick_delay) {
|
||||||
|
computedValues[1] += tick_delay;
|
||||||
|
}
|
||||||
|
|
||||||
|
return computedValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPeriodValues(deb, fin) {
|
||||||
|
deb = snapToQuarter(deb);
|
||||||
|
fin = snapToQuarter(fin);
|
||||||
|
let leftPercentage = (deb - t_start) / (t_end - t_start) * 100;
|
||||||
|
let widthPercentage = (fin - deb) / (t_end - t_start) * 100;
|
||||||
|
periodTimeLine.style.left = `${leftPercentage}%`;
|
||||||
|
periodTimeLine.style.width = `${widthPercentage}%`;
|
||||||
|
|
||||||
|
snapHandlesToQuarters();
|
||||||
|
generateAllEtudRow();
|
||||||
|
updatePeriodTimeLabel()
|
||||||
|
}
|
||||||
|
|
||||||
|
function snapHandlesToQuarters() {
|
||||||
|
const periodValues = getPeriodValues();
|
||||||
|
let lef = Math.min(computePercentage(periodValues[0], t_start), computePercentage(t_end, tick_delay));
|
||||||
|
if (lef < 0) {
|
||||||
|
lef = 0;
|
||||||
|
}
|
||||||
|
const left = `${lef}%`;
|
||||||
|
|
||||||
|
let wid = Math.max(computePercentage(periodValues[1], periodValues[0]), computePercentage(tick_delay, 0));
|
||||||
|
if (wid > 100) {
|
||||||
|
wid = 100;
|
||||||
|
}
|
||||||
|
const width = `${wid}%`
|
||||||
|
periodTimeLine.style.left = left;
|
||||||
|
periodTimeLine.style.width = width;
|
||||||
|
|
||||||
|
updatePeriodTimeLabel()
|
||||||
|
}
|
||||||
|
|
||||||
|
function computePercentage(a, b) {
|
||||||
|
return ((a - b) / (t_end - t_start)) * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
createTicks();
|
||||||
|
setPeriodValues(t_start, t_start + period_default);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.timeline-container {
|
||||||
|
width: 75%;
|
||||||
|
margin-left: 5%;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
position: relative;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ... */
|
||||||
|
.tick {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
width: 1px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tick.hour {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tick.quarter {
|
||||||
|
height: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tick-label {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
transform: translateY(100%) translateX(-50%);
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.period {
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 183, 255, 0.5);
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.period-handle {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 8px;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
cursor: col-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.period-handle.right {
|
||||||
|
right: 0;
|
||||||
|
border-radius: 4px 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.period .period-time {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
left: calc(50% - var(--w)/2 - 5px);
|
||||||
|
justify-content: center;
|
||||||
|
align-content: center;
|
||||||
|
top: calc(-60% - 10px);
|
||||||
|
--w: 10em;
|
||||||
|
width: var(--w);
|
||||||
|
}
|
||||||
|
|
||||||
|
.period:hover .period-time {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
background-color: rgba(0, 183, 255, 1);
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
116
app/templates/assiduites/widgets/toast.j2
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
<div class="toast-holder">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.toast-holder {
|
||||||
|
position: fixed;
|
||||||
|
right: 1vw;
|
||||||
|
top: 5vh;
|
||||||
|
height: 80vh;
|
||||||
|
width: 20vw;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
margin: 0.5vh 0;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: fit-content;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 7px;
|
||||||
|
z-index: 250;
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fadeIn {
|
||||||
|
animation: fadeIn 0.5s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fadeOut {
|
||||||
|
animation: fadeOut 0.5s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-content {
|
||||||
|
color: whitesmoke;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeOut {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
function generateToast(content, color = "#12d3a5", ttl = 5) {
|
||||||
|
const toast = document.createElement('div')
|
||||||
|
toast.classList.add('toast', 'fadeIn')
|
||||||
|
|
||||||
|
const toastContent = document.createElement('div')
|
||||||
|
toastContent.classList.add('toast-content')
|
||||||
|
toastContent.appendChild(content)
|
||||||
|
|
||||||
|
toast.style.backgroundColor = color;
|
||||||
|
|
||||||
|
setTimeout(() => { toast.classList.replace('fadeIn', 'fadeOut') }, Math.max(0, ttl * 1000 - 500))
|
||||||
|
setTimeout(() => { toast.remove() }, Math.max(0, ttl * 1000))
|
||||||
|
toast.appendChild(toastContent)
|
||||||
|
return toast
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushToast(toast) {
|
||||||
|
document
|
||||||
|
.querySelector(".toast-holder")
|
||||||
|
.appendChild(
|
||||||
|
toast
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getToastColorFromEtat(etat) {
|
||||||
|
let color;
|
||||||
|
switch (etat.toUpperCase()) {
|
||||||
|
case "PRESENT":
|
||||||
|
color = "#6bdb83";
|
||||||
|
break;
|
||||||
|
case "ABSENT":
|
||||||
|
color = "#F1A69C";
|
||||||
|
break;
|
||||||
|
case "RETARD":
|
||||||
|
color = "#f0c865";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
color = "#AAA";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
@ -52,6 +52,11 @@
|
|||||||
<p><a class="stdlink" href="{{url_for('scodoc.config_codes_decisions')}}">configuration des codes de décision</a>
|
<p><a class="stdlink" href="{{url_for('scodoc.config_codes_decisions')}}">configuration des codes de décision</a>
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2>Assiduités</h2>
|
||||||
|
<p><a class="stdlink" href="{{url_for('scodoc.config_assiduites')}}">configuration du module d'assiduités</a>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
<h2>Utilisateurs et CAS</h2>
|
<h2>Utilisateurs et CAS</h2>
|
||||||
<section>
|
<section>
|
||||||
|
16
app/templates/sidebar.j2
Normal file → Executable file
@ -24,7 +24,7 @@
|
|||||||
<h2 class="insidebar">Scolarité</h2>
|
<h2 class="insidebar">Scolarité</h2>
|
||||||
<a href="{{url_for('scolar.index_html', scodoc_dept=g.scodoc_dept)}}" class="sidebar">Semestres</a> <br>
|
<a href="{{url_for('scolar.index_html', scodoc_dept=g.scodoc_dept)}}" class="sidebar">Semestres</a> <br>
|
||||||
<a href="{{url_for('notes.index_html', scodoc_dept=g.scodoc_dept)}}" class="sidebar">Programmes</a> <br>
|
<a href="{{url_for('notes.index_html', scodoc_dept=g.scodoc_dept)}}" class="sidebar">Programmes</a> <br>
|
||||||
<a href="{{url_for('absences.index_html', scodoc_dept=g.scodoc_dept)}}" class="sidebar">Absences</a> <br>
|
<a href="{{url_for('assiduites.index_html', scodoc_dept=g.scodoc_dept)}}" class="sidebar">Assiduités</a> <br>
|
||||||
|
|
||||||
{% if current_user.has_permission(sco.Permission.ScoUsersAdmin)
|
{% if current_user.has_permission(sco.Permission.ScoUsersAdmin)
|
||||||
or current_user.has_permission(sco.Permission.ScoUsersView)
|
or current_user.has_permission(sco.Permission.ScoUsersView)
|
||||||
@ -55,26 +55,26 @@
|
|||||||
<b>Absences</b>
|
<b>Absences</b>
|
||||||
{% if sco.etud_cur_sem %}
|
{% if sco.etud_cur_sem %}
|
||||||
<span title="absences du {{ sco.etud_cur_sem['date_debut'] }}
|
<span title="absences du {{ sco.etud_cur_sem['date_debut'] }}
|
||||||
au {{ sco.etud_cur_sem['date_fin'] }}">(1/2 j.)
|
au {{ sco.etud_cur_sem['date_fin'] }}">({{sco.prefs["assi_metrique"]}})
|
||||||
<br />{{sco.nbabsjust}} J., {{sco.nbabsnj}} N.J.</span>
|
<br />{{sco.nbabsjust}} J., {{sco.nbabsnj}} N.J.</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<ul>
|
<ul>
|
||||||
{% if current_user.has_permission(sco.Permission.ScoAbsChange) %}
|
{% if current_user.has_permission(sco.Permission.ScoAbsChange) %}
|
||||||
<li><a href="{{ url_for('absences.SignaleAbsenceEtud', scodoc_dept=g.scodoc_dept,
|
<li><a href="{{ url_for('assiduites.signal_assiduites_etud', scodoc_dept=g.scodoc_dept,
|
||||||
etudid=sco.etud.id) }}">Ajouter</a></li>
|
etudid=sco.etud.id) }}">Ajouter</a></li>
|
||||||
<li><a href="{{ url_for('absences.JustifAbsenceEtud', scodoc_dept=g.scodoc_dept,
|
<li><a href="{{ url_for('assiduites.ajout_justificatif_etud', scodoc_dept=g.scodoc_dept,
|
||||||
etudid=sco.etud.id) }}">Justifier</a></li>
|
etudid=sco.etud.id) }}">Justifier</a></li>
|
||||||
<li><a href="{{ url_for('absences.AnnuleAbsenceEtud', scodoc_dept=g.scodoc_dept,
|
|
||||||
etudid=sco.etud.id) }}">Supprimer</a></li>
|
|
||||||
{% if sco.prefs["handle_billets_abs"] %}
|
{% if sco.prefs["handle_billets_abs"] %}
|
||||||
<li><a href="{{ url_for('absences.billets_etud', scodoc_dept=g.scodoc_dept,
|
<li><a href="{{ url_for('absences.billets_etud', scodoc_dept=g.scodoc_dept,
|
||||||
etudid=sco.etud.id) }}">Billets</a></li>
|
etudid=sco.etud.id) }}">Billets</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a href="{{ url_for('absences.CalAbs', scodoc_dept=g.scodoc_dept,
|
<li><a href="{{ url_for('assiduites.calendrier_etud', scodoc_dept=g.scodoc_dept,
|
||||||
etudid=sco.etud.id) }}">Calendrier</a></li>
|
etudid=sco.etud.id) }}">Calendrier</a></li>
|
||||||
<li><a href="{{ url_for('absences.ListeAbsEtud', scodoc_dept=g.scodoc_dept,
|
<li><a href="{{ url_for('assiduites.liste_assiduites_etud', scodoc_dept=g.scodoc_dept,
|
||||||
etudid=sco.etud.id) }}">Liste</a></li>
|
etudid=sco.etud.id) }}">Liste</a></li>
|
||||||
|
<li><a href="{{ url_for('assiduites.bilan_etud', scodoc_dept=g.scodoc_dept,
|
||||||
|
etudid=sco.etud.id) }}">Bilan</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div> {# /etud-insidebar #}
|
</div> {# /etud-insidebar #}
|
||||||
|
@ -11,7 +11,7 @@ from app import db
|
|||||||
from app.models import Identite
|
from app.models import Identite
|
||||||
from app.models.formsemestre import FormSemestre
|
from app.models.formsemestre import FormSemestre
|
||||||
from app.scodoc import notesdb as ndb
|
from app.scodoc import notesdb as ndb
|
||||||
from app.scodoc import sco_abs
|
from app.scodoc import sco_assiduites
|
||||||
from app.scodoc import sco_formsemestre_status
|
from app.scodoc import sco_formsemestre_status
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
@ -23,6 +23,7 @@ scolar_bp = Blueprint("scolar", __name__)
|
|||||||
notes_bp = Blueprint("notes", __name__)
|
notes_bp = Blueprint("notes", __name__)
|
||||||
users_bp = Blueprint("users", __name__)
|
users_bp = Blueprint("users", __name__)
|
||||||
absences_bp = Blueprint("absences", __name__)
|
absences_bp = Blueprint("absences", __name__)
|
||||||
|
assiduites_bp = Blueprint("assiduites", __name__)
|
||||||
|
|
||||||
|
|
||||||
# Cette fonction est bien appelée avant toutes les requêtes
|
# Cette fonction est bien appelée avant toutes les requêtes
|
||||||
@ -71,10 +72,16 @@ class ScoData:
|
|||||||
ins = self.etud.inscription_courante()
|
ins = self.etud.inscription_courante()
|
||||||
if ins:
|
if ins:
|
||||||
self.etud_cur_sem = ins.formsemestre
|
self.etud_cur_sem = ins.formsemestre
|
||||||
self.nbabs, self.nbabsjust = sco_abs.get_abs_count_in_interval(
|
(
|
||||||
|
self.nbabs,
|
||||||
|
self.nbabsjust,
|
||||||
|
) = sco_assiduites.get_assiduites_count_in_interval(
|
||||||
etud.id,
|
etud.id,
|
||||||
self.etud_cur_sem.date_debut.isoformat(),
|
self.etud_cur_sem.date_debut.isoformat(),
|
||||||
self.etud_cur_sem.date_fin.isoformat(),
|
self.etud_cur_sem.date_fin.isoformat(),
|
||||||
|
scu.translate_assiduites_metric(
|
||||||
|
sco_preferences.get_preference("assi_metrique")
|
||||||
|
),
|
||||||
)
|
)
|
||||||
self.nbabsnj = self.nbabs - self.nbabsjust
|
self.nbabsnj = self.nbabs - self.nbabsjust
|
||||||
else:
|
else:
|
||||||
@ -107,6 +114,7 @@ class ScoData:
|
|||||||
|
|
||||||
from app.views import (
|
from app.views import (
|
||||||
absences,
|
absences,
|
||||||
|
assiduites,
|
||||||
but_formation,
|
but_formation,
|
||||||
notes_formsemestre,
|
notes_formsemestre,
|
||||||
notes,
|
notes,
|
||||||
|
905
app/views/assiduites.py
Normal file
@ -0,0 +1,905 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
|
from flask import g, request, render_template
|
||||||
|
|
||||||
|
from flask import abort, url_for
|
||||||
|
|
||||||
|
from app.comp import res_sem
|
||||||
|
from app.comp.res_compat import NotesTableCompat
|
||||||
|
from app.decorators import (
|
||||||
|
scodoc,
|
||||||
|
permission_required,
|
||||||
|
)
|
||||||
|
from app.models import FormSemestre, Identite, ScoDocSiteConfig, Assiduite, Departement
|
||||||
|
from app.views import assiduites_bp as bp
|
||||||
|
from app.views import ScoData
|
||||||
|
|
||||||
|
# ---------------
|
||||||
|
from app.scodoc.sco_permissions import Permission
|
||||||
|
from app.scodoc import html_sco_header
|
||||||
|
from app.scodoc import sco_moduleimpl
|
||||||
|
from app.scodoc import sco_preferences
|
||||||
|
from app.scodoc import sco_groups_view
|
||||||
|
from app.scodoc import sco_etud
|
||||||
|
from app.scodoc import sco_find_etud
|
||||||
|
from flask_login import current_user
|
||||||
|
from app.scodoc import sco_utils as scu
|
||||||
|
from app.scodoc import sco_assiduites as scass
|
||||||
|
|
||||||
|
from app.tables.visu_assiduites import TableAssi, etuds_sorted_from_ids
|
||||||
|
|
||||||
|
|
||||||
|
CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
|
||||||
|
|
||||||
|
# --- UTILS ---
|
||||||
|
|
||||||
|
|
||||||
|
class HTMLElement:
|
||||||
|
""""""
|
||||||
|
|
||||||
|
|
||||||
|
class HTMLElement:
|
||||||
|
"""Représentation d'un HTMLElement version Python"""
|
||||||
|
|
||||||
|
def __init__(self, tag: str, *attr, **kattr) -> None:
|
||||||
|
self.tag: str = tag
|
||||||
|
self.children: list[HTMLElement] = []
|
||||||
|
self.self_close: bool = kattr.get("self_close", False)
|
||||||
|
self.text_content: str = kattr.get("text_content", "")
|
||||||
|
self.key_attributes: dict[str, any] = kattr
|
||||||
|
self.attributes: list[str] = list(attr)
|
||||||
|
|
||||||
|
def add(self, *child: HTMLElement) -> None:
|
||||||
|
"""add child element to self"""
|
||||||
|
for kid in child:
|
||||||
|
self.children.append(kid)
|
||||||
|
|
||||||
|
def remove(self, child: HTMLElement) -> None:
|
||||||
|
"""Remove child element from self"""
|
||||||
|
if child in self.children:
|
||||||
|
self.children.remove(child)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
attr: list[str] = self.attributes
|
||||||
|
|
||||||
|
for att, val in self.key_attributes.items():
|
||||||
|
if att in ("self_close", "text_content"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if att != "cls":
|
||||||
|
attr.append(f'{att}="{val}"')
|
||||||
|
else:
|
||||||
|
attr.append(f'class="{val}"')
|
||||||
|
|
||||||
|
if not self.self_close:
|
||||||
|
head: str = f"<{self.tag} {' '.join(attr)}>{self.text_content}"
|
||||||
|
body: str = "\n".join(map(str, self.children))
|
||||||
|
foot: str = f"</{self.tag}>"
|
||||||
|
return head + body + foot
|
||||||
|
return f"<{self.tag} {' '.join(attr)}/>"
|
||||||
|
|
||||||
|
def __add__(self, other: str):
|
||||||
|
return str(self) + other
|
||||||
|
|
||||||
|
def __radd__(self, other: str):
|
||||||
|
return other + str(self)
|
||||||
|
|
||||||
|
|
||||||
|
class HTMLStringElement(HTMLElement):
|
||||||
|
"""Utilisation d'une chaine de caracètres pour représenter un element"""
|
||||||
|
|
||||||
|
def __init__(self, text: str) -> None:
|
||||||
|
self.text: str = text
|
||||||
|
HTMLElement.__init__(self, "textnode")
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.text
|
||||||
|
|
||||||
|
|
||||||
|
class HTMLBuilder:
|
||||||
|
def __init__(self, *content: HTMLElement or str) -> None:
|
||||||
|
self.content: list[HTMLElement or str] = list(content)
|
||||||
|
|
||||||
|
def add(self, *element: HTMLElement or str):
|
||||||
|
self.content.extend(element)
|
||||||
|
|
||||||
|
def remove(self, element: HTMLElement or str):
|
||||||
|
if element in self.content:
|
||||||
|
self.content.remove(element)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return "\n".join(map(str, self.content))
|
||||||
|
|
||||||
|
def build(self) -> str:
|
||||||
|
return self.__str__()
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# Assiduités (/ScoDoc/<dept>/Scolarite/Assiduites/...)
|
||||||
|
#
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/")
|
||||||
|
@bp.route("/index_html")
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoJustifChange)
|
||||||
|
def index_html():
|
||||||
|
"""Gestionnaire assiduités, page principale"""
|
||||||
|
H = [
|
||||||
|
html_sco_header.sco_header(
|
||||||
|
page_title="Saisie des assiduités",
|
||||||
|
javascripts=[
|
||||||
|
"js/assiduites.js",
|
||||||
|
"libjs/moment.new.min.js",
|
||||||
|
"libjs/moment-timezone.js",
|
||||||
|
],
|
||||||
|
cssstyles=[
|
||||||
|
"css/assiduites.css",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
"""<h2>Traitement des assiduités</h2>
|
||||||
|
<p class="help">
|
||||||
|
Pour saisir des assiduités ou consulter les états, il est recommandé par passer par
|
||||||
|
le semestre concerné (saisie par jour ou saisie différée).
|
||||||
|
</p>
|
||||||
|
""",
|
||||||
|
]
|
||||||
|
H.append(
|
||||||
|
"""<p class="help">Pour signaler, annuler ou justifier une assiduité pour un seul étudiant,
|
||||||
|
choisissez d'abord le concerné:</p>"""
|
||||||
|
)
|
||||||
|
H.append(sco_find_etud.form_search_etud())
|
||||||
|
# if current_user.has_permission(
|
||||||
|
# Permission.ScoAbsChange
|
||||||
|
# ) and sco_preferences.get_preference("handle_billets_abs"):
|
||||||
|
# H.append(
|
||||||
|
# f"""
|
||||||
|
# <h2 style="margin-top: 30px;">Billets d'absence</h2>
|
||||||
|
# <ul><li><a href="{url_for("absences.list_billets", scodoc_dept=g.scodoc_dept)
|
||||||
|
# }">Traitement des billets d'absence en attente</a>
|
||||||
|
# </li></ul>
|
||||||
|
# """
|
||||||
|
# )
|
||||||
|
|
||||||
|
H.append(
|
||||||
|
render_template(
|
||||||
|
"assiduites/pages/bilan_dept.j2",
|
||||||
|
dept_id=g.scodoc_dept_id,
|
||||||
|
annee=scu.annee_scolaire(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
H.append(html_sco_header.sco_footer())
|
||||||
|
return "\n".join(H)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/SignaleAssiduiteEtud")
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoAssiduiteChange)
|
||||||
|
def signal_assiduites_etud():
|
||||||
|
"""
|
||||||
|
signal_assiduites_etud Saisie de l'assiduité d'un étudiant
|
||||||
|
|
||||||
|
Args:
|
||||||
|
etudid (int): l'identifiant de l'étudiant
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: l'html généré
|
||||||
|
"""
|
||||||
|
|
||||||
|
etudid = request.args.get("etudid", -1)
|
||||||
|
etud: Identite = Identite.query.get_or_404(etudid)
|
||||||
|
if etud.dept_id != g.scodoc_dept_id:
|
||||||
|
abort(404, "étudiant inexistant dans ce département")
|
||||||
|
|
||||||
|
header: str = html_sco_header.sco_header(
|
||||||
|
page_title="Saisie Assiduités",
|
||||||
|
init_qtip=True,
|
||||||
|
javascripts=[
|
||||||
|
"js/assiduites.js",
|
||||||
|
"libjs/moment.new.min.js",
|
||||||
|
"libjs/moment-timezone.js",
|
||||||
|
],
|
||||||
|
cssstyles=[
|
||||||
|
"css/assiduites.css",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Gestion des horaires (journée, matin, soir)
|
||||||
|
|
||||||
|
morning = get_time("assi_morning_time", "08:00:00")
|
||||||
|
lunch = get_time("assi_lunch_time", "13:00:00")
|
||||||
|
afternoon = get_time("assi_afternoon_time", "18:00:00")
|
||||||
|
|
||||||
|
select = """
|
||||||
|
<select class="dynaSelect">
|
||||||
|
<option value="" selected> Non spécifié </option>
|
||||||
|
</select>
|
||||||
|
"""
|
||||||
|
|
||||||
|
return HTMLBuilder(
|
||||||
|
header,
|
||||||
|
_mini_timeline(),
|
||||||
|
render_template(
|
||||||
|
"assiduites/pages/signal_assiduites_etud.j2",
|
||||||
|
sco=ScoData(etud),
|
||||||
|
date=datetime.date.today().isoformat(),
|
||||||
|
morning=morning,
|
||||||
|
lunch=lunch,
|
||||||
|
timeline=_timeline(),
|
||||||
|
afternoon=afternoon,
|
||||||
|
nonworkdays=_non_work_days(),
|
||||||
|
forcer_module=sco_preferences.get_preference(
|
||||||
|
"forcer_module", dept_id=g.scodoc_dept_id
|
||||||
|
),
|
||||||
|
moduleimpl_select=_dynamic_module_selector(),
|
||||||
|
diff=_differee(
|
||||||
|
etudiants=[sco_etud.get_etud_info(etudid=etud.etudid, filled=True)[0]],
|
||||||
|
moduleimpl_select=select,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).build()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/ListeAssiduitesEtud")
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def liste_assiduites_etud():
|
||||||
|
"""
|
||||||
|
liste_assiduites_etud Affichage de toutes les assiduites et justificatifs d'un etudiant
|
||||||
|
Args:
|
||||||
|
etudid (int): l'identifiant de l'étudiant
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: l'html généré
|
||||||
|
"""
|
||||||
|
|
||||||
|
etudid = request.args.get("etudid", -1)
|
||||||
|
etud: Identite = Identite.query.get_or_404(etudid)
|
||||||
|
if etud.dept_id != g.scodoc_dept_id:
|
||||||
|
abort(404, "étudiant inexistant dans ce département")
|
||||||
|
|
||||||
|
header: str = html_sco_header.sco_header(
|
||||||
|
page_title="Liste des assiduités",
|
||||||
|
init_qtip=True,
|
||||||
|
javascripts=[
|
||||||
|
"js/assiduites.js",
|
||||||
|
"libjs/moment.new.min.js",
|
||||||
|
"libjs/moment-timezone.js",
|
||||||
|
],
|
||||||
|
cssstyles=CSSSTYLES
|
||||||
|
+ [
|
||||||
|
"css/assiduites.css",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
return HTMLBuilder(
|
||||||
|
header,
|
||||||
|
render_template(
|
||||||
|
"assiduites/pages/liste_assiduites.j2",
|
||||||
|
sco=ScoData(etud),
|
||||||
|
date=datetime.date.today().isoformat(),
|
||||||
|
),
|
||||||
|
).build()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/BilanEtud")
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def bilan_etud():
|
||||||
|
"""
|
||||||
|
bilan_etud Affichage de toutes les assiduites et justificatifs d'un etudiant
|
||||||
|
Args:
|
||||||
|
etudid (int): l'identifiant de l'étudiant
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: l'html généré
|
||||||
|
"""
|
||||||
|
|
||||||
|
etudid = request.args.get("etudid", -1)
|
||||||
|
etud: Identite = Identite.query.get_or_404(etudid)
|
||||||
|
if etud.dept_id != g.scodoc_dept_id:
|
||||||
|
abort(404, "étudiant inexistant dans ce département")
|
||||||
|
|
||||||
|
header: str = html_sco_header.sco_header(
|
||||||
|
page_title="Bilan de l'assiduité étudiante",
|
||||||
|
init_qtip=True,
|
||||||
|
javascripts=[
|
||||||
|
"js/assiduites.js",
|
||||||
|
"libjs/moment.new.min.js",
|
||||||
|
"libjs/moment-timezone.js",
|
||||||
|
],
|
||||||
|
cssstyles=CSSSTYLES
|
||||||
|
+ [
|
||||||
|
"css/assiduites.css",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
date_debut: str = f"{scu.annee_scolaire()}-09-01"
|
||||||
|
date_fin: str = f"{scu.annee_scolaire()+1}-06-30"
|
||||||
|
|
||||||
|
assi_metric = {
|
||||||
|
"H.": "heure",
|
||||||
|
"J.": "journee",
|
||||||
|
"1/2 J.": "demi",
|
||||||
|
}.get(sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id))
|
||||||
|
|
||||||
|
return HTMLBuilder(
|
||||||
|
header,
|
||||||
|
render_template(
|
||||||
|
"assiduites/pages/bilan_etud.j2",
|
||||||
|
sco=ScoData(etud),
|
||||||
|
date_debut=date_debut,
|
||||||
|
date_fin=date_fin,
|
||||||
|
assi_metric=assi_metric,
|
||||||
|
assi_seuil=_get_seuil(),
|
||||||
|
),
|
||||||
|
).build()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/AjoutJustificatifEtud")
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoJustifChange)
|
||||||
|
def ajout_justificatif_etud():
|
||||||
|
"""
|
||||||
|
ajout_justificatif_etud : Affichage et création/modification des justificatifs de l'étudiant
|
||||||
|
Args:
|
||||||
|
etudid (int): l'identifiant de l'étudiant
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: l'html généré
|
||||||
|
"""
|
||||||
|
|
||||||
|
etudid = request.args.get("etudid", -1)
|
||||||
|
etud: Identite = Identite.query.get_or_404(etudid)
|
||||||
|
if etud.dept_id != g.scodoc_dept_id:
|
||||||
|
abort(404, "étudiant inexistant dans ce département")
|
||||||
|
|
||||||
|
header: str = html_sco_header.sco_header(
|
||||||
|
page_title="Justificatifs",
|
||||||
|
init_qtip=True,
|
||||||
|
javascripts=[
|
||||||
|
"js/assiduites.js",
|
||||||
|
"libjs/moment.new.min.js",
|
||||||
|
"libjs/moment-timezone.js",
|
||||||
|
],
|
||||||
|
cssstyles=CSSSTYLES
|
||||||
|
+ [
|
||||||
|
"css/assiduites.css",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
return HTMLBuilder(
|
||||||
|
header,
|
||||||
|
render_template(
|
||||||
|
"assiduites/pages/ajout_justificatif.j2",
|
||||||
|
sco=ScoData(etud),
|
||||||
|
),
|
||||||
|
).build()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/CalendrierAssiduitesEtud")
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def calendrier_etud():
|
||||||
|
"""
|
||||||
|
calendrier_etud : Affichage d'un calendrier des assiduités de l'étudiant
|
||||||
|
Args:
|
||||||
|
etudid (int): l'identifiant de l'étudiant
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: l'html généré
|
||||||
|
"""
|
||||||
|
|
||||||
|
etudid = request.args.get("etudid", -1)
|
||||||
|
etud: Identite = Identite.query.get_or_404(etudid)
|
||||||
|
if etud.dept_id != g.scodoc_dept_id:
|
||||||
|
abort(404, "étudiant inexistant dans ce département")
|
||||||
|
|
||||||
|
header: str = html_sco_header.sco_header(
|
||||||
|
page_title="Calendrier des Assiduités",
|
||||||
|
init_qtip=True,
|
||||||
|
javascripts=[
|
||||||
|
"js/assiduites.js",
|
||||||
|
"libjs/moment.new.min.js",
|
||||||
|
"libjs/moment-timezone.js",
|
||||||
|
],
|
||||||
|
cssstyles=CSSSTYLES
|
||||||
|
+ [
|
||||||
|
"css/assiduites.css",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
return HTMLBuilder(
|
||||||
|
header,
|
||||||
|
render_template(
|
||||||
|
"assiduites/pages/calendrier.j2",
|
||||||
|
sco=ScoData(etud),
|
||||||
|
annee=scu.annee_scolaire(),
|
||||||
|
nonworkdays=_non_work_days(),
|
||||||
|
minitimeline=_mini_timeline(),
|
||||||
|
),
|
||||||
|
).build()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/SignalAssiduiteGr")
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoAssiduiteChange)
|
||||||
|
def signal_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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
html_sco_header.sco_footer(),
|
||||||
|
).build()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/EtatAbsencesDate")
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def get_etat_abs_date():
|
||||||
|
evaluation = {
|
||||||
|
"jour": request.args.get("jour"),
|
||||||
|
"heure_debut": request.args.get("heure_debut"),
|
||||||
|
"heure_fin": request.args.get("heure_fin"),
|
||||||
|
"title": request.args.get("desc"),
|
||||||
|
}
|
||||||
|
date: str = evaluation["jour"]
|
||||||
|
group_ids: list[int] = request.args.get("group_ids", None)
|
||||||
|
etudiants: list[dict] = []
|
||||||
|
|
||||||
|
if group_ids is None:
|
||||||
|
group_ids = []
|
||||||
|
else:
|
||||||
|
group_ids = group_ids.split(",")
|
||||||
|
map(str, group_ids)
|
||||||
|
|
||||||
|
groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)
|
||||||
|
|
||||||
|
etuds = [
|
||||||
|
sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
|
||||||
|
for m in groups_infos.members
|
||||||
|
]
|
||||||
|
|
||||||
|
date_debut = scu.is_iso_formated(
|
||||||
|
f"{evaluation['jour']}T{evaluation['heure_debut'].replace('h',':')}", True
|
||||||
|
)
|
||||||
|
date_fin = scu.is_iso_formated(
|
||||||
|
f"{evaluation['jour']}T{evaluation['heure_fin'].replace('h',':')}", True
|
||||||
|
)
|
||||||
|
|
||||||
|
assiduites: Assiduite = Assiduite.query.filter(
|
||||||
|
Assiduite.etudid.in_([e["etudid"] for e in etuds])
|
||||||
|
)
|
||||||
|
assiduites = scass.filter_by_date(
|
||||||
|
assiduites, Assiduite, date_debut, date_fin, False
|
||||||
|
)
|
||||||
|
|
||||||
|
for etud in etuds:
|
||||||
|
assi = assiduites.filter_by(etudid=etud["etudid"]).first()
|
||||||
|
|
||||||
|
etat = ""
|
||||||
|
if assi != None and assi.etat != 0:
|
||||||
|
etat = scu.EtatAssiduite.inverse().get(assi.etat).name
|
||||||
|
|
||||||
|
etudiant = {
|
||||||
|
"nom": f'<a href="{url_for("assiduites.calendrier_etud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"])}"><font color="#A00000">{etud["nomprenom"]}</font></a>',
|
||||||
|
"etat": etat,
|
||||||
|
}
|
||||||
|
|
||||||
|
etudiants.append(etudiant)
|
||||||
|
|
||||||
|
etudiants = list(sorted(etudiants, key=lambda x: x["nom"]))
|
||||||
|
|
||||||
|
header: str = html_sco_header.sco_header(
|
||||||
|
page_title=evaluation["title"],
|
||||||
|
init_qtip=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
return HTMLBuilder(
|
||||||
|
header,
|
||||||
|
render_template(
|
||||||
|
"assiduites/pages/etat_absence_date.j2",
|
||||||
|
etudiants=etudiants,
|
||||||
|
eval=evaluation,
|
||||||
|
),
|
||||||
|
html_sco_header.sco_footer(),
|
||||||
|
).build()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/VisualisationAssiduitesGroupe")
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def visu_assi_group():
|
||||||
|
dates = {
|
||||||
|
"debut": request.args.get("date_debut"),
|
||||||
|
"fin": request.args.get("date_fin"),
|
||||||
|
}
|
||||||
|
|
||||||
|
group_ids: list[int] = request.args.get("group_ids", None)
|
||||||
|
etudiants: list[dict] = []
|
||||||
|
|
||||||
|
if group_ids is None:
|
||||||
|
group_ids = []
|
||||||
|
else:
|
||||||
|
group_ids = group_ids.split(",")
|
||||||
|
map(str, group_ids)
|
||||||
|
|
||||||
|
groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)
|
||||||
|
|
||||||
|
etuds = etuds_sorted_from_ids([m["etudid"] for m in groups_infos.members])
|
||||||
|
|
||||||
|
header: str = html_sco_header.sco_header(
|
||||||
|
page_title="Visualisation des assiduités",
|
||||||
|
init_qtip=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
table: TableAssi = TableAssi(etuds=etuds, dates=list(dates.values()))
|
||||||
|
|
||||||
|
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>"
|
||||||
|
)
|
||||||
|
|
||||||
|
return HTMLBuilder(
|
||||||
|
header,
|
||||||
|
render_template(
|
||||||
|
"assiduites/pages/visu_assi.j2",
|
||||||
|
tableau=table.html(),
|
||||||
|
gr_tit=gr_tit,
|
||||||
|
date_debut=dates["debut"],
|
||||||
|
date_fin=dates["fin"],
|
||||||
|
group_ids=request.args.get("group_ids", None),
|
||||||
|
),
|
||||||
|
html_sco_header.sco_footer(),
|
||||||
|
).build()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/SignalAssiduiteDifferee")
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoAssiduiteChange)
|
||||||
|
def signal_assiduites_diff():
|
||||||
|
group_ids: list[int] = request.args.get("group_ids", None)
|
||||||
|
formsemestre_id: int = request.args.get("formsemestre_id", -1)
|
||||||
|
date: str = request.args.get("jour", datetime.date.today().isoformat())
|
||||||
|
etudiants: list[dict] = []
|
||||||
|
|
||||||
|
titre = None
|
||||||
|
|
||||||
|
# Vérification du formsemestre_id
|
||||||
|
try:
|
||||||
|
formsemestre_id = int(formsemestre_id)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
formsemestre_id = None
|
||||||
|
|
||||||
|
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||||
|
|
||||||
|
# --- 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()
|
||||||
|
|
||||||
|
if group_ids is None:
|
||||||
|
group_ids = []
|
||||||
|
else:
|
||||||
|
group_ids = group_ids.split(",")
|
||||||
|
map(str, group_ids)
|
||||||
|
|
||||||
|
groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)
|
||||||
|
|
||||||
|
if not groups_infos.members:
|
||||||
|
return (
|
||||||
|
html_sco_header.sco_header(page_title="Assiduités Différées")
|
||||||
|
+ "<h3>Aucun étudiant ! </h3>"
|
||||||
|
+ html_sco_header.sco_footer()
|
||||||
|
)
|
||||||
|
|
||||||
|
etudiants.extend(
|
||||||
|
[
|
||||||
|
sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
|
||||||
|
for m in groups_infos.members
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
etudiants = list(sorted(etudiants, key=lambda x: x["nom"]))
|
||||||
|
|
||||||
|
header: str = html_sco_header.sco_header(
|
||||||
|
page_title="Assiduités Différées",
|
||||||
|
init_qtip=True,
|
||||||
|
cssstyles=[
|
||||||
|
"css/assiduites.css",
|
||||||
|
],
|
||||||
|
javascripts=html_sco_header.BOOTSTRAP_MULTISELECT_JS
|
||||||
|
+ [
|
||||||
|
"js/assiduites.js",
|
||||||
|
"libjs/moment.new.min.js",
|
||||||
|
"libjs/moment-timezone.js",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
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>"
|
||||||
|
)
|
||||||
|
|
||||||
|
return HTMLBuilder(
|
||||||
|
header,
|
||||||
|
render_template(
|
||||||
|
"assiduites/pages/signal_assiduites_diff.j2",
|
||||||
|
diff=_differee(
|
||||||
|
etudiants=etudiants,
|
||||||
|
moduleimpl_select=_module_selector(formsemestre),
|
||||||
|
date=date,
|
||||||
|
periode={
|
||||||
|
"deb": formsemestre.date_debut.isoformat(),
|
||||||
|
"fin": formsemestre.date_fin.isoformat(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
gr=gr_tit,
|
||||||
|
sem=sem["titre_num"],
|
||||||
|
),
|
||||||
|
html_sco_header.sco_footer(),
|
||||||
|
).build()
|
||||||
|
|
||||||
|
|
||||||
|
def _differee(
|
||||||
|
etudiants, moduleimpl_select, date=None, periode=None, formsemestre_id=None
|
||||||
|
):
|
||||||
|
if date is None:
|
||||||
|
date = datetime.date.today().isoformat()
|
||||||
|
|
||||||
|
forcer_module = sco_preferences.get_preference(
|
||||||
|
"forcer_module",
|
||||||
|
formsemestre_id=formsemestre_id,
|
||||||
|
dept_id=g.scodoc_dept_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
etat_def = sco_preferences.get_preference(
|
||||||
|
"assi_etat_defaut",
|
||||||
|
formsemestre_id=formsemestre_id,
|
||||||
|
dept_id=g.scodoc_dept_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"assiduites/widgets/differee.j2",
|
||||||
|
etudiants=etudiants,
|
||||||
|
etat_def=etat_def,
|
||||||
|
forcer_module=forcer_module,
|
||||||
|
moduleimpl_select=moduleimpl_select,
|
||||||
|
date=date,
|
||||||
|
periode=periode,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _module_selector(
|
||||||
|
formsemestre: FormSemestre, moduleimpl_id: int = None
|
||||||
|
) -> HTMLElement:
|
||||||
|
"""
|
||||||
|
_module_selector Génère un HTMLSelectElement à partir des moduleimpl du formsemestre
|
||||||
|
|
||||||
|
Args:
|
||||||
|
formsemestre (FormSemestre): Le formsemestre d'où les moduleimpls seront pris.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: La représentation str d'un HTMLSelectElement
|
||||||
|
"""
|
||||||
|
|
||||||
|
ntc: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
|
|
||||||
|
modimpls_list: list[dict] = []
|
||||||
|
ues = ntc.get_ues_stat_dict()
|
||||||
|
for ue in ues:
|
||||||
|
modimpls_list += ntc.get_modimpls_dict(ue_id=ue["ue_id"])
|
||||||
|
|
||||||
|
selected = moduleimpl_id is not None
|
||||||
|
|
||||||
|
modules = []
|
||||||
|
|
||||||
|
for modimpl in modimpls_list:
|
||||||
|
modname: str = (
|
||||||
|
(modimpl["module"]["code"] or "")
|
||||||
|
+ " "
|
||||||
|
+ (modimpl["module"]["abbrev"] or modimpl["module"]["titre"] or "")
|
||||||
|
)
|
||||||
|
modules.append({"moduleimpl_id": modimpl["moduleimpl_id"], "name": modname})
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"assiduites/widgets/moduleimpl_selector.j2", selected=selected, modules=modules
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _dynamic_module_selector():
|
||||||
|
return render_template("assiduites/widgets/moduleimpl_dynamic_selector.j2")
|
||||||
|
|
||||||
|
|
||||||
|
def _timeline(formsemestre_id=None) -> HTMLElement:
|
||||||
|
return render_template(
|
||||||
|
"assiduites/widgets/timeline.j2",
|
||||||
|
t_start=get_time("assi_morning_time", "08:00:00"),
|
||||||
|
t_end=get_time("assi_afternoon_time", "18:00:00"),
|
||||||
|
tick_time=ScoDocSiteConfig.get("assi_tick_time", 15),
|
||||||
|
periode_defaut=sco_preferences.get_preference(
|
||||||
|
"periode_defaut", formsemestre_id
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _mini_timeline() -> HTMLElement:
|
||||||
|
return render_template(
|
||||||
|
"assiduites/widgets/minitimeline.j2",
|
||||||
|
t_start=get_time("assi_morning_time", "08:00:00"),
|
||||||
|
t_end=get_time("assi_afternoon_time", "18:00:00"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _non_work_days():
|
||||||
|
non_travail = sco_preferences.get_preference("non_travail", None)
|
||||||
|
non_travail = non_travail.replace(" ", "").split(",")
|
||||||
|
return ",".join([f"'{i.lower()}'" for i in non_travail])
|
||||||
|
|
||||||
|
|
||||||
|
def _str_to_num(string: str):
|
||||||
|
parts = [*map(float, string.split(":"))]
|
||||||
|
hour = parts[0]
|
||||||
|
minutes = round(parts[1] / 60 * 4) / 4
|
||||||
|
return hour + minutes
|
||||||
|
|
||||||
|
|
||||||
|
def get_time(label: str, default: str):
|
||||||
|
return _str_to_num(ScoDocSiteConfig.get(label, default))
|
||||||
|
|
||||||
|
|
||||||
|
def _get_seuil():
|
||||||
|
return sco_preferences.get_preference("assi_seuil", dept_id=g.scodoc_dept_id)
|
@ -64,6 +64,7 @@ from app.forms.main import config_logos, config_main
|
|||||||
from app.forms.main.create_dept import CreateDeptForm
|
from app.forms.main.create_dept import CreateDeptForm
|
||||||
from app.forms.main.config_apo import CodesDecisionsForm
|
from app.forms.main.config_apo import CodesDecisionsForm
|
||||||
from app.forms.main.config_cas import ConfigCASForm
|
from app.forms.main.config_cas import ConfigCASForm
|
||||||
|
from app.forms.main.config_assiduites import ConfigAssiduitesForm
|
||||||
from app import models
|
from app import models
|
||||||
from app.models import Departement, Identite
|
from app.models import Departement, Identite
|
||||||
from app.models import departements
|
from app.models import departements
|
||||||
@ -188,6 +189,54 @@ def config_cas():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/ScoDoc/config_assiduites", methods=["GET", "POST"])
|
||||||
|
@admin_required
|
||||||
|
def config_assiduites():
|
||||||
|
"""Form config Assiduites"""
|
||||||
|
form = ConfigAssiduitesForm()
|
||||||
|
if request.method == "POST" and form.cancel.data: # cancel button
|
||||||
|
return redirect(url_for("scodoc.index"))
|
||||||
|
if form.validate_on_submit():
|
||||||
|
if ScoDocSiteConfig.set("assi_morning_time", form.data["morning_time"]):
|
||||||
|
flash("Heure du début de la journée enregistrée")
|
||||||
|
if ScoDocSiteConfig.set("assi_lunch_time", form.data["lunch_time"]):
|
||||||
|
flash("Heure de midi enregistrée")
|
||||||
|
if ScoDocSiteConfig.set("assi_afternoon_time", form.data["afternoon_time"]):
|
||||||
|
flash("Heure de fin de la journée enregistrée")
|
||||||
|
if (
|
||||||
|
form.data["tick_time"] > 0
|
||||||
|
and form.data["tick_time"] < 60
|
||||||
|
and ScoDocSiteConfig.set("assi_tick_time", float(form.data["tick_time"]))
|
||||||
|
):
|
||||||
|
flash("Granularité de la timeline enregistrée")
|
||||||
|
else:
|
||||||
|
flash("Erreur : Granularité invalide ou identique")
|
||||||
|
|
||||||
|
return redirect(url_for("scodoc.configuration"))
|
||||||
|
|
||||||
|
elif request.method == "GET":
|
||||||
|
form.morning_time.data = ScoDocSiteConfig.get(
|
||||||
|
"assi_morning_time", datetime.time(8, 0, 0)
|
||||||
|
)
|
||||||
|
form.lunch_time.data = ScoDocSiteConfig.get(
|
||||||
|
"assi_lunch_time", datetime.time(13, 0, 0)
|
||||||
|
)
|
||||||
|
form.afternoon_time.data = ScoDocSiteConfig.get(
|
||||||
|
"assi_afternoon_time", datetime.time(18, 0, 0)
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
form.tick_time.data = float(ScoDocSiteConfig.get("assi_tick_time", 15.0))
|
||||||
|
except ValueError:
|
||||||
|
form.tick_time.data = 15.0
|
||||||
|
ScoDocSiteConfig.set("assi_tick_time", 15.0)
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"assiduites/config_assiduites.j2",
|
||||||
|
form=form,
|
||||||
|
title="Configuration du module Assiduités",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/ScoDoc/config_codes_decisions", methods=["GET", "POST"])
|
@bp.route("/ScoDoc/config_codes_decisions", methods=["GET", "POST"])
|
||||||
@admin_required
|
@admin_required
|
||||||
def config_codes_decisions():
|
def config_codes_decisions():
|
||||||
|
71
migrations/versions/b555390780b2_assiduites_ajout_user_id_est_just.py
Executable file
@ -0,0 +1,71 @@
|
|||||||
|
"""assiduites ajout user_id,est_just
|
||||||
|
|
||||||
|
Revision ID: b555390780b2
|
||||||
|
Revises: dbcf2175e87f
|
||||||
|
Create Date: 2023-02-22 18:44:22.643275
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "b555390780b2"
|
||||||
|
down_revision = "dbcf2175e87f"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column(
|
||||||
|
"assiduites",
|
||||||
|
sa.Column(
|
||||||
|
"user_id",
|
||||||
|
sa.Integer(),
|
||||||
|
nullable=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
op.add_column(
|
||||||
|
"assiduites",
|
||||||
|
sa.Column("est_just", sa.Boolean(), server_default="false", nullable=False),
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
op.f("ix_assiduites_user_id"), "assiduites", ["user_id"], unique=False
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_assiduites_user_id",
|
||||||
|
"assiduites",
|
||||||
|
"user",
|
||||||
|
["user_id"],
|
||||||
|
["id"],
|
||||||
|
ondelete="SET NULL",
|
||||||
|
)
|
||||||
|
op.add_column(
|
||||||
|
"justificatifs",
|
||||||
|
sa.Column("user_id", sa.Integer(), nullable=True),
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
op.f("ix_justificatifs_user_id"), "justificatifs", ["user_id"], unique=False
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_justificatifs_user_id",
|
||||||
|
"justificatifs",
|
||||||
|
"user",
|
||||||
|
["user_id"],
|
||||||
|
["id"],
|
||||||
|
ondelete="SET NULL",
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint("fk_justificatifs_user_id", "justificatifs", type_="foreignkey")
|
||||||
|
op.drop_index(op.f("ix_justificatifs_user_id"), table_name="justificatifs")
|
||||||
|
op.drop_column("justificatifs", "user_id")
|
||||||
|
op.drop_constraint("fk_assiduites_user_id", "assiduites", type_="foreignkey")
|
||||||
|
op.drop_index(op.f("ix_assiduites_user_id"), table_name="assiduites")
|
||||||
|
op.drop_column("assiduites", "est_just")
|
||||||
|
op.drop_column("assiduites", "user_id")
|
||||||
|
# ### end Alembic commands ###
|
@ -11,7 +11,7 @@ from sqlalchemy.orm import sessionmaker # added by ev
|
|||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision = "c701224fa255"
|
revision = "c701224fa255"
|
||||||
down_revision = "d84bc592584e"
|
down_revision = "b555390780b2"
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
95
migrations/versions/dbcf2175e87f_modèles_assiduites_justificatifs.py
Executable file
@ -0,0 +1,95 @@
|
|||||||
|
"""modèles assiduites justificatifs
|
||||||
|
|
||||||
|
Revision ID: dbcf2175e87f
|
||||||
|
Revises: c701224fa255
|
||||||
|
Create Date: 2023-02-01 14:21:06.989190
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "dbcf2175e87f"
|
||||||
|
down_revision = "d84bc592584e"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table(
|
||||||
|
"justificatifs",
|
||||||
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"date_debut",
|
||||||
|
sa.DateTime(timezone=True),
|
||||||
|
server_default=sa.text("now()"),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"date_fin",
|
||||||
|
sa.DateTime(timezone=True),
|
||||||
|
server_default=sa.text("now()"),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column("etudid", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("etat", sa.Integer(), nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"entry_date",
|
||||||
|
sa.DateTime(timezone=True),
|
||||||
|
server_default=sa.text("now()"),
|
||||||
|
nullable=True,
|
||||||
|
),
|
||||||
|
sa.Column("raison", sa.Text(), nullable=True),
|
||||||
|
sa.Column("fichier", sa.Text(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(["etudid"], ["identite.id"], ondelete="CASCADE"),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
op.f("ix_justificatifs_etudid"), "justificatifs", ["etudid"], unique=False
|
||||||
|
)
|
||||||
|
op.create_table(
|
||||||
|
"assiduites",
|
||||||
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"date_debut",
|
||||||
|
sa.DateTime(timezone=True),
|
||||||
|
server_default=sa.text("now()"),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"date_fin",
|
||||||
|
sa.DateTime(timezone=True),
|
||||||
|
server_default=sa.text("now()"),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column("moduleimpl_id", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("etudid", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("etat", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("desc", sa.Text(), nullable=True),
|
||||||
|
sa.Column(
|
||||||
|
"entry_date",
|
||||||
|
sa.DateTime(timezone=True),
|
||||||
|
server_default=sa.text("now()"),
|
||||||
|
nullable=True,
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(["etudid"], ["identite.id"], ondelete="CASCADE"),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["moduleimpl_id"], ["notes_moduleimpl.id"], ondelete="SET NULL"
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
op.f("ix_assiduites_etudid"), "assiduites", ["etudid"], unique=False
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_index(op.f("ix_assiduites_etudid"), table_name="assiduites")
|
||||||
|
op.drop_table("assiduites")
|
||||||
|
op.drop_index(op.f("ix_justificatifs_etudid"), table_name="justificatifs")
|
||||||
|
op.drop_table("justificatifs")
|
||||||
|
# ### end Alembic commands ###
|
54
scodoc.py
@ -642,3 +642,57 @@ def profile(host, port, length, profile_dir):
|
|||||||
run_simple(
|
run_simple(
|
||||||
host, port, app, use_debugger=False
|
host, port, app, use_debugger=False
|
||||||
) # use run_simple instead of app.run()
|
) # use run_simple instead of app.run()
|
||||||
|
|
||||||
|
|
||||||
|
# <== Gestion de l'assiduité ==>
|
||||||
|
|
||||||
|
|
||||||
|
@app.cli.command()
|
||||||
|
@click.option(
|
||||||
|
"-d", "--dept", help="Restreint la migration au dept sélectionné (ACRONYME)"
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"-m",
|
||||||
|
"--morning",
|
||||||
|
help="Spécifie l'heure de début des cours format `hh:mm`",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"-n",
|
||||||
|
"--noon",
|
||||||
|
help="Spécifie l'heure de fin du matin (et donc début de l'après-midi) format `hh:mm`",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"-e",
|
||||||
|
"--evening",
|
||||||
|
help="Spécifie l'heure de fin des cours format `hh:mm`",
|
||||||
|
)
|
||||||
|
@with_appcontext
|
||||||
|
def migrate_abs_to_assiduites(
|
||||||
|
dept: str = None, morning: str = None, noon: str = None, evening: str = None
|
||||||
|
): # migrate-abs-to-assiduites
|
||||||
|
"""Permet de migrer les absences vers le nouveau module d'assiduités"""
|
||||||
|
tools.migrate_abs_to_assiduites(dept, morning, noon, evening)
|
||||||
|
|
||||||
|
|
||||||
|
@app.cli.command()
|
||||||
|
@click.option(
|
||||||
|
"-d", "--dept", help="Restreint la suppression au dept sélectionné (ACRONYME)"
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"-a",
|
||||||
|
"--assiduites",
|
||||||
|
is_flag=True,
|
||||||
|
help="Supprime les assiduités de scodoc",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"-j",
|
||||||
|
"--justificatifs",
|
||||||
|
is_flag=True,
|
||||||
|
help="Supprime les justificatifs de scodoc",
|
||||||
|
)
|
||||||
|
@with_appcontext
|
||||||
|
def downgrade_assiduites_module(
|
||||||
|
dept: str = None, assiduites: bool = False, justificatifs: bool = False
|
||||||
|
):
|
||||||
|
"""Supprime les assiduites et/ou les justificatifs de tous les départements ou du département sélectionné"""
|
||||||
|
tools.downgrade_module(dept, assiduites, justificatifs)
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
Usage:
|
Usage:
|
||||||
cd /opt/scodoc/tests/api
|
cd /opt/scodoc/tests/api
|
||||||
python make_samples.py [entry_names]
|
python make_samples.py [entry_names]
|
||||||
|
python make_samples.py -i <filepath> [entrynames]
|
||||||
|
|
||||||
si entry_names est spécifié, la génération est restreints aux exemples cités. expl: `python make_samples departements departement-formsemestres`
|
si entry_names est spécifié, la génération est restreints aux exemples cités. expl: `python make_samples departements departement-formsemestres`
|
||||||
doit être exécutée immédiatement apres une initialisation de la base pour test API! (car dépendant des identifiants générés lors de la création des objets)
|
doit être exécutée immédiatement apres une initialisation de la base pour test API! (car dépendant des identifiants générés lors de la création des objets)
|
||||||
@ -37,7 +38,6 @@ Quand la structure est complète, on génére tous les fichiers textes
|
|||||||
- le résultat
|
- le résultat
|
||||||
Le tout mis en forme au format markdown et rangé dans le répertoire DATA_DIR (/tmp/samples) qui est créé ou écrasé si déjà existant
|
Le tout mis en forme au format markdown et rangé dans le répertoire DATA_DIR (/tmp/samples) qui est créé ou écrasé si déjà existant
|
||||||
|
|
||||||
TODO: ajouter un argument au script permettant de ne générer qu'un seul fichier (exemple: `python make_samples.py nom_exemple`)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
@ -65,7 +65,7 @@ from setup_test_api import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
DATA_DIR = "/tmp/samples/"
|
DATA_DIR = "/tmp/samples/"
|
||||||
SAMPLES_FILENAME = "tests/ressources/samples.csv"
|
SAMPLES_FILENAME = "tests/ressources/samples/samples.csv"
|
||||||
|
|
||||||
|
|
||||||
class Sample:
|
class Sample:
|
||||||
@ -180,11 +180,13 @@ class Samples:
|
|||||||
file.close()
|
file.close()
|
||||||
|
|
||||||
|
|
||||||
def make_samples():
|
def make_samples(samples_filename):
|
||||||
if len(sys.argv) == 1:
|
if len(sys.argv) == 1:
|
||||||
entry_names = None
|
entry_names = None
|
||||||
else:
|
elif len(sys.argv) >= 3 and sys.argv[1] == "-i":
|
||||||
entry_names = sys.argv[1:]
|
samples_filename = sys.argv[2]
|
||||||
|
entry_names = sys.argv[3:] if len(sys.argv) > 3 else None
|
||||||
|
|
||||||
if os.path.exists(DATA_DIR):
|
if os.path.exists(DATA_DIR):
|
||||||
if not os.path.isdir(DATA_DIR):
|
if not os.path.isdir(DATA_DIR):
|
||||||
raise f"{DATA_DIR} existe déjà et n'est pas un répertoire"
|
raise f"{DATA_DIR} existe déjà et n'est pas un répertoire"
|
||||||
@ -197,7 +199,7 @@ def make_samples():
|
|||||||
|
|
||||||
samples = Samples(entry_names)
|
samples = Samples(entry_names)
|
||||||
df = read_csv(
|
df = read_csv(
|
||||||
SAMPLES_FILENAME,
|
samples_filename,
|
||||||
sep=";",
|
sep=";",
|
||||||
quotechar='"',
|
quotechar='"',
|
||||||
dtype={
|
dtype={
|
||||||
@ -217,4 +219,4 @@ def make_samples():
|
|||||||
|
|
||||||
if not CHECK_CERTIFICATE:
|
if not CHECK_CERTIFICATE:
|
||||||
urllib3.disable_warnings()
|
urllib3.disable_warnings()
|
||||||
make_samples()
|
make_samples(SAMPLES_FILENAME)
|
||||||
|
400
tests/api/test_api_assiduites.py
Normal file
@ -0,0 +1,400 @@
|
|||||||
|
"""
|
||||||
|
Test de l'api Assiduité
|
||||||
|
|
||||||
|
Ecrit par HARTMANN Matthias
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from random import randint
|
||||||
|
|
||||||
|
from tests.api.setup_test_api import (
|
||||||
|
GET,
|
||||||
|
POST_JSON,
|
||||||
|
APIError,
|
||||||
|
api_headers,
|
||||||
|
api_admin_headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
ETUDID = 1
|
||||||
|
FAUX = 42069
|
||||||
|
FORMSEMESTREID = 1
|
||||||
|
MODULE = 1
|
||||||
|
|
||||||
|
|
||||||
|
ASSIDUITES_FIELDS = {
|
||||||
|
"assiduite_id": int,
|
||||||
|
"etudid": int,
|
||||||
|
"moduleimpl_id": int,
|
||||||
|
"date_debut": str,
|
||||||
|
"date_fin": str,
|
||||||
|
"etat": str,
|
||||||
|
"desc": str,
|
||||||
|
"entry_date": str,
|
||||||
|
"user_id": str,
|
||||||
|
"est_just": bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
CREATE_FIELD = {"assiduite_id": int}
|
||||||
|
BATCH_FIELD = {"errors": dict, "success": dict}
|
||||||
|
|
||||||
|
COUNT_FIELDS = {"compte": int, "journee": int, "demi": int, "heure": float}
|
||||||
|
|
||||||
|
TO_REMOVE = []
|
||||||
|
|
||||||
|
|
||||||
|
def check_fields(data: dict, fields: dict = None):
|
||||||
|
"""
|
||||||
|
Cette fonction permet de vérifier que le dictionnaire data
|
||||||
|
contient les bonnes clés et les bons types de valeurs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data (dict): un dictionnaire (json de retour de l'api)
|
||||||
|
fields (dict, optional): Un dictionnaire représentant les clés et les types d'une réponse.
|
||||||
|
"""
|
||||||
|
if fields is None:
|
||||||
|
fields = ASSIDUITES_FIELDS
|
||||||
|
assert set(data.keys()) == set(fields.keys())
|
||||||
|
for key in data:
|
||||||
|
if key in ("moduleimpl_id", "desc", "user_id"):
|
||||||
|
assert isinstance(data[key], fields[key]) or data[key] is None
|
||||||
|
else:
|
||||||
|
assert isinstance(data[key], fields[key])
|
||||||
|
|
||||||
|
|
||||||
|
def check_failure_get(path: str, headers: dict, err: str = None):
|
||||||
|
"""
|
||||||
|
Cette fonction vérifiée que la requête GET renvoie bien un 404
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path (str): la route de l'api
|
||||||
|
headers (dict): le token d'auth de l'api
|
||||||
|
err (str, optional): L'erreur qui est sensée être fournie par l'api.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
APIError: Une erreur car la requête a fonctionné (mauvais comportement)
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
GET(path=path, headers=headers)
|
||||||
|
# ^ Renvoi un 404
|
||||||
|
except APIError as api_err:
|
||||||
|
if err is not None:
|
||||||
|
assert api_err.payload["message"] == err
|
||||||
|
else:
|
||||||
|
raise APIError("Le GET n'aurait pas du fonctionner")
|
||||||
|
|
||||||
|
|
||||||
|
def check_failure_post(path: str, headers: dict, data: dict, err: str = None):
|
||||||
|
"""
|
||||||
|
Cette fonction vérifiée que la requête POST renvoie bien un 404
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path (str): la route de l'api
|
||||||
|
headers (dict): le token d'auth
|
||||||
|
data (dict): un dictionnaire (json) à envoyer
|
||||||
|
err (str, optional): L'erreur qui est sensée être fournie par l'api.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
APIError: Une erreur car la requête a fonctionné (mauvais comportement)
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = POST_JSON(path=path, headers=headers, data=data)
|
||||||
|
# ^ Renvoi un 404
|
||||||
|
except APIError as api_err:
|
||||||
|
if err is not None:
|
||||||
|
assert api_err.payload["message"] == err
|
||||||
|
else:
|
||||||
|
raise APIError("Le GET n'aurait pas du fonctionner")
|
||||||
|
|
||||||
|
|
||||||
|
def create_data(etat: str, day: str, module: int = None, desc: str = None):
|
||||||
|
"""
|
||||||
|
Permet de créer un dictionnaire assiduité
|
||||||
|
|
||||||
|
Args:
|
||||||
|
etat (str): l'état de l'assiduité (PRESENT,ABSENT,RETARD)
|
||||||
|
day (str): Le jour de l'assiduité
|
||||||
|
module (int, optional): Le moduleimpl_id associé
|
||||||
|
desc (str, optional): Une description de l'assiduité (eg: motif retard )
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: la représentation d'une assiduité
|
||||||
|
"""
|
||||||
|
data = {
|
||||||
|
"date_debut": f"2022-01-{day}T08:00",
|
||||||
|
"date_fin": f"2022-01-{day}T10:00",
|
||||||
|
"etat": etat,
|
||||||
|
}
|
||||||
|
|
||||||
|
if module is not None:
|
||||||
|
data["moduleimpl_id"] = module
|
||||||
|
if desc is not None:
|
||||||
|
data["desc"] = desc
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def test_route_assiduite(api_headers):
|
||||||
|
"""test de la route /assiduite/<assiduite_id:int>"""
|
||||||
|
|
||||||
|
# Bon fonctionnement == id connu
|
||||||
|
data = GET(path="/assiduite/1", headers=api_headers)
|
||||||
|
check_fields(data)
|
||||||
|
|
||||||
|
# Mauvais Fonctionnement == id inconnu
|
||||||
|
|
||||||
|
check_failure_get(
|
||||||
|
f"/assiduite/{FAUX}",
|
||||||
|
api_headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_route_count_assiduites(api_headers):
|
||||||
|
"""test de la route /assiduites/<etudid:int>/count"""
|
||||||
|
|
||||||
|
# Bon fonctionnement
|
||||||
|
|
||||||
|
data = GET(path=f"/assiduites/{ETUDID}/count", headers=api_headers)
|
||||||
|
check_fields(data, COUNT_FIELDS)
|
||||||
|
|
||||||
|
metrics = {"heure", "compte"}
|
||||||
|
data = GET(
|
||||||
|
path=f"/assiduites/{ETUDID}/count/query?metric={','.join(metrics)}",
|
||||||
|
headers=api_headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert set(data.keys()) == metrics
|
||||||
|
|
||||||
|
# Mauvais fonctionnement
|
||||||
|
|
||||||
|
check_failure_get(f"/assiduites/{FAUX}/count", api_headers)
|
||||||
|
|
||||||
|
|
||||||
|
def test_route_assiduites(api_headers):
|
||||||
|
"""test de la route /assiduites/<etudid:int>"""
|
||||||
|
|
||||||
|
# Bon fonctionnement
|
||||||
|
|
||||||
|
data = GET(path=f"/assiduites/{ETUDID}", headers=api_headers)
|
||||||
|
assert isinstance(data, list)
|
||||||
|
for ass in data:
|
||||||
|
check_fields(ass, ASSIDUITES_FIELDS)
|
||||||
|
|
||||||
|
data = GET(path=f"/assiduites/{ETUDID}/query?", headers=api_headers)
|
||||||
|
assert isinstance(data, list)
|
||||||
|
for ass in data:
|
||||||
|
check_fields(ass, ASSIDUITES_FIELDS)
|
||||||
|
|
||||||
|
# Mauvais fonctionnement
|
||||||
|
check_failure_get(f"/assiduites/{FAUX}", api_headers)
|
||||||
|
check_failure_get(f"/assiduites/{FAUX}/query?", api_headers)
|
||||||
|
|
||||||
|
|
||||||
|
def test_route_formsemestre_assiduites(api_headers):
|
||||||
|
"""test de la route /assiduites/formsemestre/<formsemestre_id:int>"""
|
||||||
|
|
||||||
|
# Bon fonctionnement
|
||||||
|
|
||||||
|
data = GET(path=f"/assiduites/formsemestre/{FORMSEMESTREID}", headers=api_headers)
|
||||||
|
assert isinstance(data, list)
|
||||||
|
for ass in data:
|
||||||
|
check_fields(ass, ASSIDUITES_FIELDS)
|
||||||
|
|
||||||
|
data = GET(
|
||||||
|
path=f"/assiduites/formsemestre/{FORMSEMESTREID}/query?", headers=api_headers
|
||||||
|
)
|
||||||
|
assert isinstance(data, list)
|
||||||
|
for ass in data:
|
||||||
|
check_fields(ass, ASSIDUITES_FIELDS)
|
||||||
|
|
||||||
|
# Mauvais fonctionnement
|
||||||
|
check_failure_get(
|
||||||
|
f"/assiduites/formsemestre/{FAUX}",
|
||||||
|
api_headers,
|
||||||
|
err="le paramètre 'formsemestre_id' n'existe pas",
|
||||||
|
)
|
||||||
|
check_failure_get(
|
||||||
|
f"/assiduites/formsemestre/{FAUX}/query?",
|
||||||
|
api_headers,
|
||||||
|
err="le paramètre 'formsemestre_id' n'existe pas",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_route_count_formsemestre_assiduites(api_headers):
|
||||||
|
"""test de la route /assiduites/formsemestre/<formsemestre_id:int>/count"""
|
||||||
|
|
||||||
|
# Bon fonctionnement
|
||||||
|
|
||||||
|
data = GET(
|
||||||
|
path=f"/assiduites/formsemestre/{FORMSEMESTREID}/count", headers=api_headers
|
||||||
|
)
|
||||||
|
check_fields(data, COUNT_FIELDS)
|
||||||
|
metrics = {"heure", "compte"}
|
||||||
|
data = GET(
|
||||||
|
path=f"/assiduites/formsemestre/{FORMSEMESTREID}/count/query?metric={','.join(metrics)}",
|
||||||
|
headers=api_headers,
|
||||||
|
)
|
||||||
|
assert set(data.keys()) == metrics
|
||||||
|
|
||||||
|
# Mauvais fonctionnement
|
||||||
|
check_failure_get(
|
||||||
|
f"/assiduites/formsemestre/{FAUX}/count",
|
||||||
|
api_headers,
|
||||||
|
err="le paramètre 'formsemestre_id' n'existe pas",
|
||||||
|
)
|
||||||
|
check_failure_get(
|
||||||
|
f"/assiduites/formsemestre/{FAUX}/count/query?",
|
||||||
|
api_headers,
|
||||||
|
err="le paramètre 'formsemestre_id' n'existe pas",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_route_create(api_admin_headers):
|
||||||
|
"""test de la route /assiduite/<etudid:int>/create"""
|
||||||
|
|
||||||
|
# -== Unique ==-
|
||||||
|
|
||||||
|
# Bon fonctionnement
|
||||||
|
data = create_data("present", "01")
|
||||||
|
|
||||||
|
res = POST_JSON(f"/assiduite/{ETUDID}/create", [data], api_admin_headers)
|
||||||
|
check_fields(res, BATCH_FIELD)
|
||||||
|
assert len(res["success"]) == 1
|
||||||
|
|
||||||
|
TO_REMOVE.append(res["success"]["0"]["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"])
|
||||||
|
|
||||||
|
# Mauvais fonctionnement
|
||||||
|
check_failure_post(f"/assiduite/{FAUX}/create", api_admin_headers, [data])
|
||||||
|
|
||||||
|
res = POST_JSON(f"/assiduite/{ETUDID}/create", [data], api_admin_headers)
|
||||||
|
check_fields(res, BATCH_FIELD)
|
||||||
|
assert len(res["errors"]) == 1
|
||||||
|
assert (
|
||||||
|
res["errors"]["0"]
|
||||||
|
== "Duplication des assiduités (la période rentrée rentre en conflit avec une assiduité enregistrée)"
|
||||||
|
)
|
||||||
|
|
||||||
|
res = POST_JSON(
|
||||||
|
f"/assiduite/{ETUDID}/create",
|
||||||
|
[create_data("absent", "03", FAUX)],
|
||||||
|
api_admin_headers,
|
||||||
|
)
|
||||||
|
check_fields(res, BATCH_FIELD)
|
||||||
|
assert len(res["errors"]) == 1
|
||||||
|
assert res["errors"]["0"] == "param 'moduleimpl_id': invalide"
|
||||||
|
|
||||||
|
# -== Multiple ==-
|
||||||
|
|
||||||
|
# Bon Fonctionnement
|
||||||
|
|
||||||
|
etats = ["present", "absent", "retard"]
|
||||||
|
data = [
|
||||||
|
create_data(etats[d % 3], 10 + d, MODULE if d % 2 else None)
|
||||||
|
for d in range(randint(3, 5))
|
||||||
|
]
|
||||||
|
|
||||||
|
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"])
|
||||||
|
|
||||||
|
# Mauvais Fonctionnement
|
||||||
|
|
||||||
|
data2 = [
|
||||||
|
create_data("present", "01"),
|
||||||
|
create_data("present", "25", FAUX),
|
||||||
|
create_data("blabla", 26),
|
||||||
|
create_data("absent", 32),
|
||||||
|
]
|
||||||
|
|
||||||
|
res = POST_JSON(f"/assiduite/{ETUDID}/create", data2, api_admin_headers)
|
||||||
|
check_fields(res, BATCH_FIELD)
|
||||||
|
assert len(res["errors"]) == 4
|
||||||
|
|
||||||
|
assert (
|
||||||
|
res["errors"]["0"]
|
||||||
|
== "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"]["3"]
|
||||||
|
== "param 'date_debut': format invalide, param 'date_fin': format invalide"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_route_edit(api_admin_headers):
|
||||||
|
"""test de la route /assiduite/<assiduite_id:int>/edit"""
|
||||||
|
|
||||||
|
# Bon fonctionnement
|
||||||
|
|
||||||
|
data = {"etat": "retard", "moduleimpl_id": MODULE}
|
||||||
|
res = POST_JSON(f"/assiduite/{TO_REMOVE[0]}/edit", data, api_admin_headers)
|
||||||
|
assert res == {"OK": True}
|
||||||
|
|
||||||
|
data["moduleimpl_id"] = None
|
||||||
|
res = POST_JSON(f"/assiduite/{TO_REMOVE[1]}/edit", data, api_admin_headers)
|
||||||
|
assert res == {"OK": True}
|
||||||
|
|
||||||
|
# Mauvais fonctionnement
|
||||||
|
|
||||||
|
check_failure_post(f"/assiduite/{FAUX}/edit", api_admin_headers, data)
|
||||||
|
data["etat"] = "blabla"
|
||||||
|
check_failure_post(
|
||||||
|
f"/assiduite/{TO_REMOVE[2]}/edit",
|
||||||
|
api_admin_headers,
|
||||||
|
data,
|
||||||
|
err="param 'etat': invalide",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_route_delete(api_admin_headers):
|
||||||
|
"""test de la route /assiduite/delete"""
|
||||||
|
# -== Unique ==-
|
||||||
|
|
||||||
|
# Bon fonctionnement
|
||||||
|
data = TO_REMOVE[0]
|
||||||
|
|
||||||
|
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}
|
||||||
|
|
||||||
|
# Mauvais fonctionnement
|
||||||
|
res = POST_JSON("/assiduite/delete", [data], api_admin_headers)
|
||||||
|
check_fields(res, BATCH_FIELD)
|
||||||
|
assert len(res["errors"]) == 1
|
||||||
|
|
||||||
|
# -== Multiple ==-
|
||||||
|
|
||||||
|
# Bon Fonctionnement
|
||||||
|
|
||||||
|
data = TO_REMOVE[1:]
|
||||||
|
|
||||||
|
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}
|
||||||
|
|
||||||
|
# Mauvais Fonctionnement
|
||||||
|
|
||||||
|
data2 = [
|
||||||
|
FAUX,
|
||||||
|
FAUX + 1,
|
||||||
|
FAUX + 2,
|
||||||
|
]
|
||||||
|
|
||||||
|
res = POST_JSON("/assiduite/delete", data2, 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"]])
|
@ -713,6 +713,8 @@ def test_formsemestre_resultat(api_headers):
|
|||||||
) as f:
|
) as f:
|
||||||
json_reference = f.read()
|
json_reference = f.read()
|
||||||
ref = json.loads(json_reference)
|
ref = json.loads(json_reference)
|
||||||
|
with open("venv/res.json", "w", encoding="utf8") as f:
|
||||||
|
json.dump(res, f)
|
||||||
_compare_formsemestre_resultat(res, ref)
|
_compare_formsemestre_resultat(res, ref)
|
||||||
|
|
||||||
|
|
||||||
@ -724,4 +726,7 @@ def _compare_formsemestre_resultat(res: list[dict], ref: list[dict]):
|
|||||||
for res_d, ref_d in zip(res, ref):
|
for res_d, ref_d in zip(res, ref):
|
||||||
assert sorted(res_d.keys()) == sorted(ref_d.keys())
|
assert sorted(res_d.keys()) == sorted(ref_d.keys())
|
||||||
for k in res_d:
|
for k in res_d:
|
||||||
|
# On passe les absences pour le moment (TODO: mise à jour assiduité à faire)
|
||||||
|
if "nbabs" in k:
|
||||||
|
continue
|
||||||
assert res_d[k] == ref_d[k], f"values for key {k} differ."
|
assert res_d[k] == ref_d[k], f"values for key {k} differ."
|
||||||
|
1
tests/api/test_api_justificatif.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
test de l'importation des fichiers / archive justificatif
|
1
tests/api/test_api_justificatif2.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
test de l'importation des fichiers / archive justificatif
|
487
tests/api/test_api_justificatifs.py
Normal file
@ -0,0 +1,487 @@
|
|||||||
|
"""
|
||||||
|
Test de l'api justificatif
|
||||||
|
|
||||||
|
Ecrit par HARTMANN Matthias
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from random import randint
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from tests.api.setup_test_api import (
|
||||||
|
API_URL,
|
||||||
|
CHECK_CERTIFICATE,
|
||||||
|
GET,
|
||||||
|
POST_JSON,
|
||||||
|
APIError,
|
||||||
|
api_headers,
|
||||||
|
api_admin_headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
ETUDID = 1
|
||||||
|
FAUX = 42069
|
||||||
|
|
||||||
|
|
||||||
|
JUSTIFICATIFS_FIELDS = {
|
||||||
|
"justif_id": int,
|
||||||
|
"etudid": int,
|
||||||
|
"date_debut": str,
|
||||||
|
"date_fin": str,
|
||||||
|
"etat": str,
|
||||||
|
"raison": str,
|
||||||
|
"entry_date": str,
|
||||||
|
"fichier": str,
|
||||||
|
"user_id": int,
|
||||||
|
}
|
||||||
|
|
||||||
|
CREATE_FIELD = {"justif_id": int, "couverture": list}
|
||||||
|
BATCH_FIELD = {"errors": dict, "success": dict}
|
||||||
|
|
||||||
|
TO_REMOVE = []
|
||||||
|
|
||||||
|
|
||||||
|
def check_fields(data, fields: dict = None):
|
||||||
|
"""
|
||||||
|
Cette fonction permet de vérifier que le dictionnaire data
|
||||||
|
contient les bonnes clés et les bons types de valeurs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data (dict): un dictionnaire (json de retour de l'api)
|
||||||
|
fields (dict, optional): Un dictionnaire représentant les clés et les types d'une réponse.
|
||||||
|
"""
|
||||||
|
if fields is None:
|
||||||
|
fields = JUSTIFICATIFS_FIELDS
|
||||||
|
assert set(data.keys()) == set(fields.keys())
|
||||||
|
for key in data:
|
||||||
|
if key in ("raison", "fichier", "user_id"):
|
||||||
|
assert isinstance(data[key], fields[key]) or data[key] is None
|
||||||
|
else:
|
||||||
|
assert isinstance(data[key], fields[key])
|
||||||
|
|
||||||
|
|
||||||
|
def check_failure_get(path, headers, err=None):
|
||||||
|
"""
|
||||||
|
Cette fonction vérifiée que la requête GET renvoie bien un 404
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path (str): la route de l'api
|
||||||
|
headers (dict): le token d'auth de l'api
|
||||||
|
err (str, optional): L'erreur qui est sensée être fournie par l'api.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
APIError: Une erreur car la requête a fonctionné (mauvais comportement)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
GET(path=path, headers=headers)
|
||||||
|
# ^ Renvoi un 404
|
||||||
|
except APIError as api_err:
|
||||||
|
if err is not None:
|
||||||
|
assert api_err.payload["message"] == err
|
||||||
|
else:
|
||||||
|
raise APIError("Le GET n'aurait pas du fonctionner")
|
||||||
|
|
||||||
|
|
||||||
|
def check_failure_post(path, headers, data, err=None):
|
||||||
|
"""
|
||||||
|
Cette fonction vérifiée que la requête POST renvoie bien un 404
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path (str): la route de l'api
|
||||||
|
headers (dict): le token d'auth
|
||||||
|
data (dict): un dictionnaire (json) à envoyer
|
||||||
|
err (str, optional): L'erreur qui est sensée être fournie par l'api.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
APIError: Une erreur car la requête a fonctionné (mauvais comportement)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = POST_JSON(path=path, headers=headers, data=data)
|
||||||
|
# ^ Renvoi un 404
|
||||||
|
except APIError as api_err:
|
||||||
|
if err is not None:
|
||||||
|
assert api_err.payload["message"] == err
|
||||||
|
else:
|
||||||
|
raise APIError("Le POST n'aurait pas du fonctionner")
|
||||||
|
|
||||||
|
|
||||||
|
def create_data(etat: str, day: str, raison: str = None):
|
||||||
|
"""
|
||||||
|
Permet de créer un dictionnaire assiduité
|
||||||
|
|
||||||
|
Args:
|
||||||
|
etat (str): l'état du justificatif (VALIDE,NON_VALIDE,MODIFIE, ATTENTE)
|
||||||
|
day (str): Le jour du justificatif
|
||||||
|
raison (str, optional): Une description du justificatif (eg: motif retard )
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: la représentation d'une assiduité
|
||||||
|
"""
|
||||||
|
data = {
|
||||||
|
"date_debut": f"2022-01-{day}T08:00",
|
||||||
|
"date_fin": f"2022-01-{day}T10:00",
|
||||||
|
"etat": etat,
|
||||||
|
}
|
||||||
|
if raison is not None:
|
||||||
|
data["desc"] = raison
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def test_route_justificatif(api_headers):
|
||||||
|
"""test de la route /justificatif/<justif_id:int>"""
|
||||||
|
|
||||||
|
# Bon fonctionnement == id connu
|
||||||
|
data = GET(path="/justificatif/1", headers=api_headers)
|
||||||
|
check_fields(data)
|
||||||
|
|
||||||
|
# Mauvais Fonctionnement == id inconnu
|
||||||
|
|
||||||
|
check_failure_get(
|
||||||
|
f"/justificatif/{FAUX}",
|
||||||
|
api_headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_route_justificatifs(api_headers):
|
||||||
|
"""test de la route /justificatifs/<etudid:int>"""
|
||||||
|
# Bon fonctionnement
|
||||||
|
|
||||||
|
data = GET(path=f"/justificatifs/{ETUDID}", headers=api_headers)
|
||||||
|
assert isinstance(data, list)
|
||||||
|
for just in data:
|
||||||
|
check_fields(just, JUSTIFICATIFS_FIELDS)
|
||||||
|
|
||||||
|
data = GET(path=f"/justificatifs/{ETUDID}/query?", headers=api_headers)
|
||||||
|
assert isinstance(data, list)
|
||||||
|
for just in data:
|
||||||
|
check_fields(just, JUSTIFICATIFS_FIELDS)
|
||||||
|
|
||||||
|
# Mauvais fonctionnement
|
||||||
|
check_failure_get(f"/justificatifs/{FAUX}", api_headers)
|
||||||
|
check_failure_get(f"/justificatifs/{FAUX}/query?", api_headers)
|
||||||
|
|
||||||
|
|
||||||
|
def test_route_create(api_admin_headers):
|
||||||
|
"""test de la route /justificatif/<justif_id:int>/create"""
|
||||||
|
# -== Unique ==-
|
||||||
|
|
||||||
|
# Bon fonctionnement
|
||||||
|
data = create_data("valide", "01")
|
||||||
|
|
||||||
|
res = POST_JSON(f"/justificatif/{ETUDID}/create", [data], api_admin_headers)
|
||||||
|
check_fields(res, BATCH_FIELD)
|
||||||
|
assert len(res["success"]) == 1
|
||||||
|
|
||||||
|
TO_REMOVE.append(res["success"]["0"]["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"])
|
||||||
|
|
||||||
|
# Mauvais fonctionnement
|
||||||
|
check_failure_post(f"/justificatif/{FAUX}/create", api_admin_headers, [data])
|
||||||
|
|
||||||
|
res = POST_JSON(
|
||||||
|
f"/justificatif/{ETUDID}/create",
|
||||||
|
[create_data("absent", "03")],
|
||||||
|
api_admin_headers,
|
||||||
|
)
|
||||||
|
check_fields(res, BATCH_FIELD)
|
||||||
|
assert len(res["errors"]) == 1
|
||||||
|
assert res["errors"]["0"] == "param 'etat': invalide"
|
||||||
|
|
||||||
|
# -== Multiple ==-
|
||||||
|
|
||||||
|
# Bon Fonctionnement
|
||||||
|
|
||||||
|
etats = ["valide", "modifie", "non_valide", "attente"]
|
||||||
|
data = [
|
||||||
|
create_data(etats[d % 4], 10 + d, "raison" if d % 2 else None)
|
||||||
|
for d in range(randint(3, 5))
|
||||||
|
]
|
||||||
|
|
||||||
|
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"])
|
||||||
|
|
||||||
|
# Mauvais Fonctionnement
|
||||||
|
|
||||||
|
data2 = [
|
||||||
|
create_data(None, "25"),
|
||||||
|
create_data("blabla", 26),
|
||||||
|
create_data("valide", 32),
|
||||||
|
]
|
||||||
|
|
||||||
|
res = POST_JSON(f"/justificatif/{ETUDID}/create", data2, 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"]["2"]
|
||||||
|
== "param 'date_debut': format invalide, param 'date_fin': format invalide"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_route_edit(api_admin_headers):
|
||||||
|
"""test de la route /justificatif/<justif_id:int>/edit"""
|
||||||
|
# Bon fonctionnement
|
||||||
|
|
||||||
|
data = {"etat": "modifie", "raison": "test"}
|
||||||
|
res = POST_JSON(f"/justificatif/{TO_REMOVE[0]}/edit", data, api_admin_headers)
|
||||||
|
assert isinstance(res, dict) and "couverture" in res.keys()
|
||||||
|
|
||||||
|
data["raison"] = None
|
||||||
|
res = POST_JSON(f"/justificatif/{TO_REMOVE[1]}/edit", data, api_admin_headers)
|
||||||
|
assert isinstance(res, dict) and "couverture" in res.keys()
|
||||||
|
|
||||||
|
# Mauvais fonctionnement
|
||||||
|
|
||||||
|
check_failure_post(f"/justificatif/{FAUX}/edit", api_admin_headers, data)
|
||||||
|
data["etat"] = "blabla"
|
||||||
|
check_failure_post(
|
||||||
|
f"/justificatif/{TO_REMOVE[2]}/edit",
|
||||||
|
api_admin_headers,
|
||||||
|
data,
|
||||||
|
err="param 'etat': invalide",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_route_delete(api_admin_headers):
|
||||||
|
"""test de la route /justificatif/delete"""
|
||||||
|
# -== Unique ==-
|
||||||
|
|
||||||
|
# Bon fonctionnement
|
||||||
|
data = TO_REMOVE[0]
|
||||||
|
|
||||||
|
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}
|
||||||
|
|
||||||
|
# Mauvais fonctionnement
|
||||||
|
res = POST_JSON("/justificatif/delete", [data], api_admin_headers)
|
||||||
|
check_fields(res, BATCH_FIELD)
|
||||||
|
assert len(res["errors"]) == 1
|
||||||
|
|
||||||
|
# -== Multiple ==-
|
||||||
|
|
||||||
|
# Bon Fonctionnement
|
||||||
|
|
||||||
|
data = TO_REMOVE[1:]
|
||||||
|
|
||||||
|
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}
|
||||||
|
|
||||||
|
# Mauvais Fonctionnement
|
||||||
|
|
||||||
|
data2 = [
|
||||||
|
FAUX,
|
||||||
|
FAUX + 1,
|
||||||
|
FAUX + 2,
|
||||||
|
]
|
||||||
|
|
||||||
|
res = POST_JSON("/justificatif/delete", data2, 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"]])
|
||||||
|
|
||||||
|
|
||||||
|
# Gestion de l'archivage
|
||||||
|
|
||||||
|
|
||||||
|
def _send_file(justif_id: int, filename: str, headers):
|
||||||
|
"""
|
||||||
|
Envoi un fichier vers la route d'importation
|
||||||
|
"""
|
||||||
|
with open(filename, "rb") as file:
|
||||||
|
url: str = API_URL + f"/justificatif/{justif_id}/import"
|
||||||
|
req = requests.post(
|
||||||
|
url,
|
||||||
|
files={filename: file},
|
||||||
|
headers=headers,
|
||||||
|
verify=CHECK_CERTIFICATE,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
|
||||||
|
if req.status_code != 200:
|
||||||
|
raise APIError(f"erreur status={req.status_code} !", req.json())
|
||||||
|
|
||||||
|
return req.json()
|
||||||
|
|
||||||
|
|
||||||
|
def _check_failure_send(
|
||||||
|
justif_id: int,
|
||||||
|
headers,
|
||||||
|
filename: str = "tests/api/test_api_justificatif.txt",
|
||||||
|
err: str = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Vérifie si l'envoie d'un fichier renvoie bien un 404
|
||||||
|
|
||||||
|
Args:
|
||||||
|
justif_id (int): l'id du justificatif
|
||||||
|
headers (dict): token d'auth de l'api
|
||||||
|
filename (str, optional): le chemin vers le fichier.
|
||||||
|
Defaults to "tests/api/test_api_justificatif.txt".
|
||||||
|
err (str, optional): l'erreur attendue.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
APIError: Si l'envoie fonction (mauvais comportement)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
_send_file(justif_id, filename, headers)
|
||||||
|
# ^ Renvoi un 404
|
||||||
|
except APIError as api_err:
|
||||||
|
if err is not None:
|
||||||
|
assert api_err.payload["message"] == err
|
||||||
|
else:
|
||||||
|
raise APIError("Le POST n'aurait pas du fonctionner")
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_justificatif(api_admin_headers):
|
||||||
|
"""test de la route /justificatif/<justif_id:int>/import"""
|
||||||
|
|
||||||
|
# Bon fonctionnement
|
||||||
|
|
||||||
|
filename: str = "tests/api/test_api_justificatif.txt"
|
||||||
|
|
||||||
|
resp: dict = _send_file(1, filename, api_admin_headers)
|
||||||
|
assert "filename" in resp
|
||||||
|
assert resp["filename"] == "test_api_justificatif.txt"
|
||||||
|
|
||||||
|
filename: str = "tests/api/test_api_justificatif2.txt"
|
||||||
|
resp: dict = _send_file(1, filename, api_admin_headers)
|
||||||
|
assert "filename" in resp
|
||||||
|
assert resp["filename"] == "test_api_justificatif2.txt"
|
||||||
|
|
||||||
|
# Mauvais fonctionnement
|
||||||
|
|
||||||
|
_check_failure_send(FAUX, api_admin_headers)
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_justificatifs(api_admin_headers):
|
||||||
|
"""test de la route /justificatif/<justif_id:int>/list"""
|
||||||
|
|
||||||
|
# Bon fonctionnement
|
||||||
|
|
||||||
|
res: list = GET("/justificatif/1/list", api_admin_headers)
|
||||||
|
|
||||||
|
assert isinstance(res, dict)
|
||||||
|
assert len(res["filenames"]) == 2
|
||||||
|
assert res["total"] == 2
|
||||||
|
|
||||||
|
res: list = GET("/justificatif/2/list", api_admin_headers)
|
||||||
|
|
||||||
|
assert isinstance(res, dict)
|
||||||
|
assert len(res["filenames"]) == 0
|
||||||
|
assert res["total"] == 0
|
||||||
|
|
||||||
|
# Mauvais fonctionnement
|
||||||
|
|
||||||
|
check_failure_get(f"/justificatif/{FAUX}/list", api_admin_headers)
|
||||||
|
|
||||||
|
|
||||||
|
def _post_export(justif_id: int, fname: str, api_headers):
|
||||||
|
"""
|
||||||
|
Envoie une requête poste sans data et la retourne
|
||||||
|
|
||||||
|
Args:
|
||||||
|
id (int): justif_id
|
||||||
|
fname (str): nom du fichier (coté serv)
|
||||||
|
api_headers (dict): token auth de l'api
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
request: la réponse de l'api
|
||||||
|
"""
|
||||||
|
url: str = API_URL + f"/justificatif/{justif_id}/export/{fname}"
|
||||||
|
res = requests.post(url, headers=api_headers)
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def test_export(api_admin_headers):
|
||||||
|
"""test de la route /justificatif/<justif_id:int>/export/<filename:str>"""
|
||||||
|
|
||||||
|
# Bon fonctionnement
|
||||||
|
|
||||||
|
assert (
|
||||||
|
_post_export(1, "test_api_justificatif.txt", api_admin_headers).status_code
|
||||||
|
== 200
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mauvais fonctionnement
|
||||||
|
assert (
|
||||||
|
_post_export(FAUX, "test_api_justificatif.txt", api_admin_headers).status_code
|
||||||
|
== 404
|
||||||
|
)
|
||||||
|
assert _post_export(1, "blabla.txt", api_admin_headers).status_code == 404
|
||||||
|
assert _post_export(2, "blabla.txt", api_admin_headers).status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_justificatif(api_admin_headers):
|
||||||
|
"""test de la route /justificatif/<justif_id:int>/remove"""
|
||||||
|
|
||||||
|
# Bon fonctionnement
|
||||||
|
|
||||||
|
filename: str = "tests/api/test_api_justificatif.txt"
|
||||||
|
_send_file(2, filename, api_admin_headers)
|
||||||
|
filename: str = "tests/api/test_api_justificatif2.txt"
|
||||||
|
_send_file(2, filename, api_admin_headers)
|
||||||
|
|
||||||
|
res: dict = POST_JSON(
|
||||||
|
"/justificatif/1/remove", {"remove": "all"}, api_admin_headers
|
||||||
|
)
|
||||||
|
assert res == {"response": "removed"}
|
||||||
|
l = GET("/justificatif/1/list", api_admin_headers)
|
||||||
|
assert isinstance(l, dict)
|
||||||
|
assert l["total"] == 0
|
||||||
|
|
||||||
|
res: dict = POST_JSON(
|
||||||
|
"/justificatif/2/remove",
|
||||||
|
{"remove": "list", "filenames": ["test_api_justificatif2.txt"]},
|
||||||
|
api_admin_headers,
|
||||||
|
)
|
||||||
|
assert res == {"response": "removed"}
|
||||||
|
l = GET("/justificatif/2/list", api_admin_headers)
|
||||||
|
assert isinstance(l, dict)
|
||||||
|
assert l["total"] == 1
|
||||||
|
|
||||||
|
res: dict = POST_JSON(
|
||||||
|
"/justificatif/2/remove",
|
||||||
|
{"remove": "list", "filenames": ["test_api_justificatif.txt"]},
|
||||||
|
api_admin_headers,
|
||||||
|
)
|
||||||
|
assert res == {"response": "removed"}
|
||||||
|
l = GET("/justificatif/2/list", api_admin_headers)
|
||||||
|
assert isinstance(l, dict)
|
||||||
|
assert l["total"] == 0
|
||||||
|
|
||||||
|
# Mauvais fonctionnement
|
||||||
|
|
||||||
|
check_failure_post("/justificatif/2/remove", api_admin_headers, {})
|
||||||
|
check_failure_post(
|
||||||
|
f"/justificatif/{FAUX}/remove", api_admin_headers, {"remove": "all"}
|
||||||
|
)
|
||||||
|
check_failure_post("/justificatif/1/remove", api_admin_headers, {"remove": "all"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_justifies(api_admin_headers):
|
||||||
|
"""test la route /justificatif/<justif_id:int>/justifies"""
|
||||||
|
|
||||||
|
# Bon fonctionnement
|
||||||
|
|
||||||
|
res: list = GET("/justificatif/1/justifies", api_admin_headers)
|
||||||
|
assert isinstance(res, list)
|
||||||
|
|
||||||
|
# Mauvais fonctionnement
|
||||||
|
|
||||||
|
check_failure_get(f"/justificatif/{FAUX}/justifies", api_admin_headers)
|
15
tests/api/test_api_permissions.py
Normal file → Executable file
@ -62,12 +62,27 @@ def test_permissions(api_headers):
|
|||||||
"uid": 1,
|
"uid": 1,
|
||||||
"validation_id": 1,
|
"validation_id": 1,
|
||||||
"version": "long",
|
"version": "long",
|
||||||
|
"assiduite_id": 1,
|
||||||
|
"justif_id": 1,
|
||||||
|
"etudids": "1",
|
||||||
}
|
}
|
||||||
for rule in api_rules:
|
for rule in api_rules:
|
||||||
path = rule.build(args)[1]
|
path = rule.build(args)[1]
|
||||||
if not "GET" in rule.methods:
|
if not "GET" in rule.methods:
|
||||||
# skip all POST routes
|
# skip all POST routes
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if any(
|
||||||
|
path.startswith(p)
|
||||||
|
for p in [
|
||||||
|
"/ScoDoc/api/justificatif/1/list",
|
||||||
|
"/ScoDoc/api/justificatif/1/justifies",
|
||||||
|
]
|
||||||
|
):
|
||||||
|
# On passe la route "api/justificatif/<>/list" car elle nécessite la permission ScoJustifView
|
||||||
|
# On passe la route "api/justificatif/<>/justifies" car elle nécessite la permission ScoJustifChange
|
||||||
|
continue
|
||||||
|
|
||||||
r = requests.get(
|
r = requests.get(
|
||||||
SCODOC_URL + path,
|
SCODOC_URL + path,
|
||||||
headers=api_headers,
|
headers=api_headers,
|
||||||
|
26
tests/ressources/samples/assiduites_samples.csv
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
"entry_name";"url";"permission";"method";"content"
|
||||||
|
"assiduite";"/assiduite/1";"ScoView";"GET";
|
||||||
|
"assiduites";"/assiduites/1";"ScoView";"GET";
|
||||||
|
"assiduites";"/assiduites/1/query?etat=retard";"ScoView";"GET";
|
||||||
|
"assiduites";"/assiduites/1/query?moduleimpl_id=1";"ScoView";"GET";
|
||||||
|
"assiduites_count";"/assiduites/1/count";"ScoView";"GET";
|
||||||
|
"assiduites_count";"/assiduites/1/count/query?etat=retard";"ScoView";"GET";
|
||||||
|
"assiduites_count";"/assiduites/1/count/query?etat=present,retard&metric=compte,heure";"ScoView";"GET";
|
||||||
|
"assiduites_formsemestre";"/assiduites/formsemestre/1";"ScoView";"GET";
|
||||||
|
"assiduites_formsemestre";"/assiduites/formsemestre/1/query?etat=retard";"ScoView";"GET";
|
||||||
|
"assiduites_formsemestre";"/assiduites/formsemestre/1/query?moduleimpl_id=1";"ScoView";"GET";
|
||||||
|
"assiduites_formsemestre_count";"/assiduites/formsemestre/1/count";"ScoView";"GET";
|
||||||
|
"assiduites_formsemestre_count";"/assiduites/formsemestre/1/count/query?etat=retard";"ScoView";"GET";
|
||||||
|
"assiduites_formsemestre_count";"/assiduites/formsemestre/1/count/query?etat=present,retard&metric=compte,heure";"ScoView";"GET";
|
||||||
|
"assiduite_create";"/assiduite/1/create";"ScoView";"POST";"[{""date_debut"": ""2022-10-27T08:00"",""date_fin"": ""2022-10-27T10:00"",""etat"": ""absent""}]"
|
||||||
|
"assiduite_edit";"/assiduite/1/edit";"ScoView";"POST";"{""etat"":""absent""}"
|
||||||
|
"assiduite_edit";"/assiduite/1/edit";"ScoView";"POST";"{""moduleimpl_id"":2}"
|
||||||
|
"assiduite_edit";"/assiduite/1/edit";"ScoView";"POST";"{""etat"": ""retard"",""moduleimpl_id"":3}"
|
||||||
|
"assiduite_delete";"/assiduite/delete";"ScoView";"POST";"[2,2,3]"
|
||||||
|
"justificatif";"/justificatif/1";"ScoView";"GET";
|
||||||
|
"justificatifs";"/justificatifs/1";"ScoView";"GET";
|
||||||
|
"justificatifs";"/justificatifs/1/query?etat=attente";"ScoView";"GET";
|
||||||
|
"justificatif_create";"/justificatif/1/create";"ScoView";"POST";"[{""date_debut"": ""2022-10-27T08:00"",""date_fin"": ""2022-10-27T10:00"",""etat"": ""attente""}]"
|
||||||
|
"justificatif_edit";"/justificatif/1/edit";"ScoView";"POST";"{""etat"":""valide""}"
|
||||||
|
"justificatif_edit";"/justificatif/1/edit";"ScoView";"POST";"{""raison"":""MEDIC""}"
|
||||||
|
"justificatif_delete";"/justificatif/delete";"ScoView";"POST";"[2,2,3]"
|
|
@ -1,4 +1,24 @@
|
|||||||
"entry_name";"url";"permission";"method";"content"
|
"entry_name";"url";"permission";"method";"content"
|
||||||
|
"assiduite";"/assiduite/1";"ScoView";"GET";
|
||||||
|
"assiduites";"/assiduites/1";"ScoView";"GET";
|
||||||
|
"assiduites";"/assiduites/1/query?etat=retard";"ScoView";"GET";
|
||||||
|
"assiduites";"/assiduites/1/query?moduleimpl_id=1";"ScoView";"GET";
|
||||||
|
"assiduites_count";"/assiduites/1/count";"ScoView";"GET";
|
||||||
|
"assiduites_count";"/assiduites/1/count/query?etat=retard";"ScoView";"GET";
|
||||||
|
"assiduites_count";"/assiduites/1/count/query?etat=present,retard&metric=compte,heure";"ScoView";"GET";
|
||||||
|
"assiduites_formsemestre";"/assiduites/formsemestre/1";"ScoView";"GET";
|
||||||
|
"assiduites_formsemestre";"/assiduites/formsemestre/1/query?etat=retard";"ScoView";"GET";
|
||||||
|
"assiduites_formsemestre";"/assiduites/formsemestre/1/query?moduleimpl_id=1";"ScoView";"GET";
|
||||||
|
"assiduites_formsemestre_count";"/assiduites/formsemestre/1/count";"ScoView";"GET";
|
||||||
|
"assiduites_formsemestre_count";"/assiduites/formsemestre/1/count/query?etat=retard";"ScoView";"GET";
|
||||||
|
"assiduites_formsemestre_count";"/assiduites/formsemestre/1/count/query?etat=present,retard&metric=compte,heure";"ScoView";"GET";
|
||||||
|
"assiduite_create";"/assiduite/1/create";"ScoView";"POST";"{""date_debut"": ""2022-10-27T08:00"",""date_fin"": ""2022-10-27T10:00"",""etat"": ""absent""}"
|
||||||
|
"assiduite_create";"/assiduite/1/create/batch";"ScoView";"POST";"{""batch"":[{""date_debut"": ""2022-10-27T08:00"",""date_fin"": ""2022-10-27T10:00"",""etat"": ""absent""},{""date_debut"": ""2022-10-27T08:00"",""date_fin"": ""2022-10-27T10:00"",""etat"": ""retard""},{""date_debut"": ""2022-10-27T11:00"",""date_fin"": ""2022-10-27T13:00"",""etat"": ""present""}]}"
|
||||||
|
"assiduite_edit";"/assiduite/1/edit";"ScoView";"POST";"{""etat"":""absent""}"
|
||||||
|
"assiduite_edit";"/assiduite/1/edit";"ScoView";"POST";"{""moduleimpl_id"":2}"
|
||||||
|
"assiduite_edit";"/assiduite/1/edit";"ScoView";"POST";"{""etat"": ""retard"",""moduleimpl_id"":3}"
|
||||||
|
"assiduite_delete";"/assiduite/delete";"ScoView";"POST";"{""assiduite_id"": 1}"
|
||||||
|
"assiduite_delete";"/assiduite/delete/batch";"ScoView";"POST";"{""batch"":[2,2,3]}"
|
||||||
"departements";"/departements";"ScoView";"GET";
|
"departements";"/departements";"ScoView";"GET";
|
||||||
"departements-ids";"/departements_ids";"ScoView";"GET";
|
"departements-ids";"/departements_ids";"ScoView";"GET";
|
||||||
"departement";"/departement/TAPI";"ScoView";"GET";
|
"departement";"/departement/TAPI";"ScoView";"GET";
|
|
725
tests/unit/test_assiduites.py
Normal file
@ -0,0 +1,725 @@
|
|||||||
|
# -*- mode: python -*-
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Tests unitaires vérifiant le bon fonctionnement du modèle Assiduité et de
|
||||||
|
ses fonctions liées
|
||||||
|
|
||||||
|
Ecrit par HARTMANN Matthias (en s'inspirant de tests.unit.test_abs_count.py par Fares Amer )
|
||||||
|
"""
|
||||||
|
|
||||||
|
from tests.unit import sco_fake_gen
|
||||||
|
|
||||||
|
from app import db
|
||||||
|
|
||||||
|
from app.scodoc import sco_formsemestre
|
||||||
|
import app.scodoc.sco_assiduites as scass
|
||||||
|
from app.models import Assiduite, Justificatif, Identite, FormSemestre, ModuleImpl
|
||||||
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
|
import app.scodoc.sco_utils as scu
|
||||||
|
from app.scodoc.sco_abs import get_abs_count_in_interval
|
||||||
|
from app.scodoc import sco_abs_views
|
||||||
|
from tools import migrate_abs_to_assiduites, downgrade_module
|
||||||
|
|
||||||
|
|
||||||
|
class BiInt(int, scu.BiDirectionalEnum):
|
||||||
|
"""Classe pour tester la classe BiDirectionalEnum"""
|
||||||
|
|
||||||
|
A = 1
|
||||||
|
B = 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_bi_directional_enum(test_client):
|
||||||
|
"""Test le bon fonctionnement de la classe BiDirectionalEnum"""
|
||||||
|
|
||||||
|
assert BiInt.get("A") == BiInt.get("a") == BiInt.A == 1
|
||||||
|
assert BiInt.get("B") == BiInt.get("b") == BiInt.B == 2
|
||||||
|
assert BiInt.get("blabla") is None
|
||||||
|
assert BiInt.get("blabla", -1) == -1
|
||||||
|
assert isinstance(BiInt.inverse(), dict)
|
||||||
|
assert BiInt.inverse()[1] == BiInt.A and BiInt.inverse()[2] == BiInt.B
|
||||||
|
|
||||||
|
|
||||||
|
def test_general(test_client):
|
||||||
|
"""tests général du modèle assiduite"""
|
||||||
|
|
||||||
|
g_fake = sco_fake_gen.ScoFake(verbose=False)
|
||||||
|
|
||||||
|
# Création d'une formation (1)
|
||||||
|
|
||||||
|
formation_id = g_fake.create_formation()
|
||||||
|
ue_id = g_fake.create_ue(
|
||||||
|
formation_id=formation_id, acronyme="T1", titre="UE TEST 1"
|
||||||
|
)
|
||||||
|
matiere_id = g_fake.create_matiere(ue_id=ue_id, titre="test matière")
|
||||||
|
module_id_1 = g_fake.create_module(
|
||||||
|
matiere_id=matiere_id, code="Mo1", coefficient=1.0, titre="test module"
|
||||||
|
)
|
||||||
|
module_id_2 = g_fake.create_module(
|
||||||
|
matiere_id=matiere_id, code="Mo2", coefficient=1.0, titre="test module2"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Création semestre (2)
|
||||||
|
|
||||||
|
formsemestre_id_1 = g_fake.create_formsemestre(
|
||||||
|
formation_id=formation_id,
|
||||||
|
semestre_id=1,
|
||||||
|
date_debut="01/09/2022",
|
||||||
|
date_fin="31/12/2022",
|
||||||
|
)
|
||||||
|
formsemestre_id_2 = g_fake.create_formsemestre(
|
||||||
|
formation_id=formation_id,
|
||||||
|
semestre_id=2,
|
||||||
|
date_debut="01/01/2023",
|
||||||
|
date_fin="31/07/2023",
|
||||||
|
)
|
||||||
|
formsemestre_id_3 = g_fake.create_formsemestre(
|
||||||
|
formation_id=formation_id,
|
||||||
|
semestre_id=3,
|
||||||
|
date_debut="01/01/2024",
|
||||||
|
date_fin="31/07/2024",
|
||||||
|
)
|
||||||
|
|
||||||
|
formsemestre_1 = sco_formsemestre.get_formsemestre(formsemestre_id_1)
|
||||||
|
formsemestre_2 = sco_formsemestre.get_formsemestre(formsemestre_id_2)
|
||||||
|
formsemestre_3 = sco_formsemestre.get_formsemestre(formsemestre_id_3)
|
||||||
|
|
||||||
|
# Création des modulesimpls (4, 2 par semestre)
|
||||||
|
|
||||||
|
moduleimpl_1_1 = g_fake.create_moduleimpl(
|
||||||
|
module_id=module_id_1,
|
||||||
|
formsemestre_id=formsemestre_id_1,
|
||||||
|
)
|
||||||
|
moduleimpl_1_2 = g_fake.create_moduleimpl(
|
||||||
|
module_id=module_id_2,
|
||||||
|
formsemestre_id=formsemestre_id_1,
|
||||||
|
)
|
||||||
|
|
||||||
|
moduleimpl_2_1 = g_fake.create_moduleimpl(
|
||||||
|
module_id=module_id_1,
|
||||||
|
formsemestre_id=formsemestre_id_2,
|
||||||
|
)
|
||||||
|
moduleimpl_2_2 = g_fake.create_moduleimpl(
|
||||||
|
module_id=module_id_2,
|
||||||
|
formsemestre_id=formsemestre_id_2,
|
||||||
|
)
|
||||||
|
|
||||||
|
moduleimpls = [
|
||||||
|
moduleimpl_1_1,
|
||||||
|
moduleimpl_1_2,
|
||||||
|
moduleimpl_2_1,
|
||||||
|
moduleimpl_2_2,
|
||||||
|
]
|
||||||
|
|
||||||
|
moduleimpls = [
|
||||||
|
ModuleImpl.query.filter_by(id=mi_id).first() for mi_id in moduleimpls
|
||||||
|
]
|
||||||
|
|
||||||
|
# Création des étudiants (3)
|
||||||
|
|
||||||
|
etuds_dict = [
|
||||||
|
g_fake.create_etud(code_nip=None, prenom=f"etud{i}") for i in range(3)
|
||||||
|
]
|
||||||
|
|
||||||
|
etuds = []
|
||||||
|
for etud in etuds_dict:
|
||||||
|
g_fake.inscrit_etudiant(formsemestre_id=formsemestre_id_1, etud=etud)
|
||||||
|
g_fake.inscrit_etudiant(formsemestre_id=formsemestre_id_2, etud=etud)
|
||||||
|
|
||||||
|
etuds.append(Identite.query.filter_by(id=etud["id"]).first())
|
||||||
|
|
||||||
|
assert None not in etuds, "Problème avec la conversion en Identite"
|
||||||
|
|
||||||
|
# Etudiant faux
|
||||||
|
|
||||||
|
etud_faux_dict = g_fake.create_etud(code_nip=None, prenom="etudfaux")
|
||||||
|
etud_faux = Identite.query.filter_by(id=etud_faux_dict["id"]).first()
|
||||||
|
|
||||||
|
verif_migration_abs_assiduites()
|
||||||
|
|
||||||
|
ajouter_assiduites(etuds, moduleimpls, etud_faux)
|
||||||
|
justificatifs: list[Justificatif] = ajouter_justificatifs(etuds[0])
|
||||||
|
verifier_comptage_et_filtrage_assiduites(
|
||||||
|
etuds, moduleimpls, (formsemestre_1, formsemestre_2, formsemestre_3)
|
||||||
|
)
|
||||||
|
verifier_filtrage_justificatifs(etuds[0], justificatifs)
|
||||||
|
editer_supprimer_assiduites(etuds, moduleimpls)
|
||||||
|
editer_supprimer_justificatif(etuds[0])
|
||||||
|
|
||||||
|
|
||||||
|
def verif_migration_abs_assiduites():
|
||||||
|
"""Vérification que le script de migration fonctionne correctement"""
|
||||||
|
downgrade_module(assiduites=True, justificatifs=True)
|
||||||
|
|
||||||
|
etudid: int = 1
|
||||||
|
|
||||||
|
for debut, fin, demijournee in [
|
||||||
|
(
|
||||||
|
"02/01/2023",
|
||||||
|
"10/01/2023",
|
||||||
|
2,
|
||||||
|
), # 2 assiduités 02/01: 08h -> 06/01: 18h & assiduités 09/01: 08h -> 10/01: 18h | 14dj
|
||||||
|
("16/01/2023", "16/01/2023", 1), # 1 assiduité 16/01: 08h -> 16/01: 12h | 1dj
|
||||||
|
("19/01/2023", "19/01/2023", 0), # 1 assiduité 19/01: 12h -> 19/01: 18h | 1dj
|
||||||
|
("18/01/2023", "18/01/2023", 2), # 1 assiduité 18/01: 08h -> 18/01: 18h | 2dj
|
||||||
|
("23/01/2023", "23/01/2023", 0), # 1 assiduité 23/01: 12h -> 24/01: 18h | 3dj
|
||||||
|
("24/01/2023", "24/01/2023", 2),
|
||||||
|
]:
|
||||||
|
sco_abs_views.doSignaleAbsence(
|
||||||
|
datedebut=debut,
|
||||||
|
datefin=fin,
|
||||||
|
demijournee=demijournee,
|
||||||
|
etudid=etudid,
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Justification de certaines absences
|
||||||
|
|
||||||
|
for debut, fin, demijournee in [
|
||||||
|
(
|
||||||
|
"02/01/2023",
|
||||||
|
"10/01/2023",
|
||||||
|
2,
|
||||||
|
), # 2 justificatif 02/01: 08h -> 06/01: 18h & justificatif 09/01: 08h -> 10/01: 18h | 14dj
|
||||||
|
(
|
||||||
|
"19/01/2023",
|
||||||
|
"19/01/2023",
|
||||||
|
0,
|
||||||
|
), # 1 justificatif 19/01: 12h -> 19/01: 18h | 1dj
|
||||||
|
(
|
||||||
|
"18/01/2023",
|
||||||
|
"18/01/2023",
|
||||||
|
2,
|
||||||
|
), # 1 justificatif 18/01: 08h -> 18/01: 18h | 2dj
|
||||||
|
]:
|
||||||
|
sco_abs_views.doJustifAbsence(
|
||||||
|
datedebut=debut,
|
||||||
|
datefin=fin,
|
||||||
|
demijournee=demijournee,
|
||||||
|
etudid=etudid,
|
||||||
|
)
|
||||||
|
|
||||||
|
migrate_abs_to_assiduites()
|
||||||
|
|
||||||
|
assert Assiduite.query.count() == 6, "Erreur migration assiduites"
|
||||||
|
assert Justificatif.query.count() == 4, "Erreur migration justificatifs"
|
||||||
|
|
||||||
|
essais_cache(etudid)
|
||||||
|
|
||||||
|
downgrade_module(assiduites=True, justificatifs=True)
|
||||||
|
|
||||||
|
|
||||||
|
def essais_cache(etudid):
|
||||||
|
"""Vérification des fonctionnalités du cache TODO:WIP"""
|
||||||
|
|
||||||
|
date_deb: str = "2023-01-01T07:00"
|
||||||
|
date_fin: str = "2023-03-31T19:00"
|
||||||
|
|
||||||
|
abs_count_no_cache: int = get_abs_count_in_interval(etudid, date_deb, date_fin)
|
||||||
|
abs_count_cache = get_abs_count_in_interval(etudid, date_deb, date_fin)
|
||||||
|
assiduites_count_no_cache = scass.get_assiduites_count_in_interval(
|
||||||
|
etudid, date_deb, date_fin
|
||||||
|
)
|
||||||
|
assiduites_count_cache = scass.get_assiduites_count_in_interval(
|
||||||
|
etudid, date_deb, date_fin
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
abs_count_cache
|
||||||
|
== abs_count_no_cache
|
||||||
|
== assiduites_count_cache
|
||||||
|
== assiduites_count_no_cache
|
||||||
|
== (21, 17)
|
||||||
|
), "Erreur cache"
|
||||||
|
|
||||||
|
|
||||||
|
def ajouter_justificatifs(etud):
|
||||||
|
"""test de l'ajout des justificatifs"""
|
||||||
|
|
||||||
|
obj_justificatifs = [
|
||||||
|
{
|
||||||
|
"etat": scu.EtatJustificatif.ATTENTE,
|
||||||
|
"deb": "2022-09-03T08:00+01:00",
|
||||||
|
"fin": "2022-09-03T09:59:59+01:00",
|
||||||
|
"raison": None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"etat": scu.EtatJustificatif.VALIDE,
|
||||||
|
"deb": "2023-01-03T07:00+01:00",
|
||||||
|
"fin": "2023-01-03T11:00+01:00",
|
||||||
|
"raison": None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"etat": scu.EtatJustificatif.VALIDE,
|
||||||
|
"deb": "2022-09-03T10:00:00+01:00",
|
||||||
|
"fin": "2022-09-03T12:00+01:00",
|
||||||
|
"raison": None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"etat": scu.EtatJustificatif.NON_VALIDE,
|
||||||
|
"deb": "2022-09-03T14:00:00+01:00",
|
||||||
|
"fin": "2022-09-03T15:00+01:00",
|
||||||
|
"raison": "Description",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"etat": scu.EtatJustificatif.MODIFIE,
|
||||||
|
"deb": "2023-01-03T11:30+01:00",
|
||||||
|
"fin": "2023-01-03T12:00+01:00",
|
||||||
|
"raison": None,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
justificatifs = [
|
||||||
|
Justificatif.create_justificatif(
|
||||||
|
etud,
|
||||||
|
scu.is_iso_formated(just["deb"], True),
|
||||||
|
scu.is_iso_formated(just["fin"], True),
|
||||||
|
just["etat"],
|
||||||
|
just["raison"],
|
||||||
|
)
|
||||||
|
for just in obj_justificatifs
|
||||||
|
]
|
||||||
|
# Vérification de la création des justificatifs
|
||||||
|
assert [
|
||||||
|
justi for justi in justificatifs if not isinstance(justi, Justificatif)
|
||||||
|
] == [], "La création des justificatifs de base n'est pas OK"
|
||||||
|
|
||||||
|
# Vérification de la gestion des erreurs
|
||||||
|
|
||||||
|
test_assiduite = {
|
||||||
|
"etat": scu.EtatJustificatif.ATTENTE,
|
||||||
|
"deb": "2023-01-03T11:00:01+01:00",
|
||||||
|
"fin": "2023-01-03T12:00+01:00",
|
||||||
|
"raison": "Description",
|
||||||
|
}
|
||||||
|
return justificatifs
|
||||||
|
|
||||||
|
|
||||||
|
def verifier_filtrage_justificatifs(etud: Identite, justificatifs: list[Justificatif]):
|
||||||
|
"""
|
||||||
|
- vérifier le filtrage des justificatifs (etat, debut, fin)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Vérification du filtrage classique
|
||||||
|
|
||||||
|
# Etat
|
||||||
|
assert (
|
||||||
|
scass.filter_justificatifs_by_etat(etud.justificatifs, "valide").count() == 2
|
||||||
|
), "Filtrage de l'état 'valide' mauvais"
|
||||||
|
assert (
|
||||||
|
scass.filter_justificatifs_by_etat(etud.justificatifs, "attente").count() == 1
|
||||||
|
), "Filtrage de l'état 'attente' mauvais"
|
||||||
|
assert (
|
||||||
|
scass.filter_justificatifs_by_etat(etud.justificatifs, "modifie").count() == 1
|
||||||
|
), "Filtrage de l'état 'modifie' mauvais"
|
||||||
|
assert (
|
||||||
|
scass.filter_justificatifs_by_etat(etud.justificatifs, "non_valide").count()
|
||||||
|
== 1
|
||||||
|
), "Filtrage de l'état 'non_valide' mauvais"
|
||||||
|
assert (
|
||||||
|
scass.filter_justificatifs_by_etat(etud.justificatifs, "valide,modifie").count()
|
||||||
|
== 3
|
||||||
|
), "Filtrage de l'état 'valide,modifie' mauvais"
|
||||||
|
assert (
|
||||||
|
scass.filter_justificatifs_by_etat(
|
||||||
|
etud.justificatifs, "valide,modifie,attente"
|
||||||
|
).count()
|
||||||
|
== 4
|
||||||
|
), "Filtrage de l'état 'valide,modifie,attente' mauvais"
|
||||||
|
assert (
|
||||||
|
scass.filter_justificatifs_by_etat(
|
||||||
|
etud.justificatifs, "valide,modifie,attente,non_valide"
|
||||||
|
).count()
|
||||||
|
== 5
|
||||||
|
), "Filtrage de l'état 'valide,modifie,attente,_non_valide' mauvais"
|
||||||
|
|
||||||
|
assert (
|
||||||
|
scass.filter_justificatifs_by_etat(etud.justificatifs, "autre").count() == 0
|
||||||
|
), "Filtrage de l'état 'autre' mauvais"
|
||||||
|
|
||||||
|
# Dates
|
||||||
|
|
||||||
|
assert (
|
||||||
|
scass.filter_by_date(etud.justificatifs, Justificatif).count() == 5
|
||||||
|
), "Filtrage 'Toute Date' mauvais 1"
|
||||||
|
|
||||||
|
date = scu.localize_datetime("2022-09-01T10:00+01:00")
|
||||||
|
assert (
|
||||||
|
scass.filter_by_date(etud.justificatifs, Justificatif, date_deb=date).count()
|
||||||
|
== 5
|
||||||
|
), "Filtrage 'Toute Date' mauvais 2"
|
||||||
|
|
||||||
|
date = scu.localize_datetime("2022-09-03T08:00+01:00")
|
||||||
|
assert (
|
||||||
|
scass.filter_by_date(etud.justificatifs, Justificatif, date_deb=date).count()
|
||||||
|
== 5
|
||||||
|
), "Filtrage 'date début' mauvais 3"
|
||||||
|
|
||||||
|
date = scu.localize_datetime("2022-09-03T08:00:01+01:00")
|
||||||
|
assert (
|
||||||
|
scass.filter_by_date(etud.justificatifs, Justificatif, date_deb=date).count()
|
||||||
|
== 5
|
||||||
|
), "Filtrage 'date début' mauvais 4"
|
||||||
|
|
||||||
|
date = scu.localize_datetime("2022-09-03T10:00+01:00")
|
||||||
|
assert (
|
||||||
|
scass.filter_by_date(etud.justificatifs, Justificatif, date_deb=date).count()
|
||||||
|
== 4
|
||||||
|
), "Filtrage 'date début' mauvais 5"
|
||||||
|
|
||||||
|
date = scu.localize_datetime("2022-09-01T10:00+01:00")
|
||||||
|
assert (
|
||||||
|
scass.filter_by_date(etud.justificatifs, Justificatif, date_fin=date).count()
|
||||||
|
== 0
|
||||||
|
), "Filtrage 'Toute Date' mauvais 6"
|
||||||
|
|
||||||
|
date = scu.localize_datetime("2022-09-03T08:00+01:00")
|
||||||
|
assert (
|
||||||
|
scass.filter_by_date(etud.justificatifs, Justificatif, date_fin=date).count()
|
||||||
|
== 1
|
||||||
|
), "Filtrage 'date début' mauvais 7"
|
||||||
|
|
||||||
|
date = scu.localize_datetime("2022-09-03T10:00:01+01:00")
|
||||||
|
assert (
|
||||||
|
scass.filter_by_date(etud.justificatifs, Justificatif, date_fin=date).count()
|
||||||
|
== 2
|
||||||
|
), "Filtrage 'date début' mauvais 8"
|
||||||
|
|
||||||
|
date = scu.localize_datetime("2023-01-03T12:00+01:00")
|
||||||
|
assert (
|
||||||
|
scass.filter_by_date(etud.justificatifs, Justificatif, date_fin=date).count()
|
||||||
|
== 5
|
||||||
|
), "Filtrage 'date début' mauvais 9"
|
||||||
|
|
||||||
|
# Justifications des assiduites
|
||||||
|
|
||||||
|
assert len(scass.justifies(justificatifs[2])) == 2, "Justifications mauvais"
|
||||||
|
assert len(scass.justifies(justificatifs[0])) == 0, "Justifications mauvais"
|
||||||
|
|
||||||
|
|
||||||
|
def editer_supprimer_justificatif(etud: Identite):
|
||||||
|
"""
|
||||||
|
Troisième Partie:
|
||||||
|
- Vérification de l'édition des justificatifs
|
||||||
|
- Vérification de la suppression des justificatifs
|
||||||
|
"""
|
||||||
|
|
||||||
|
justi: Justificatif = etud.justificatifs.first()
|
||||||
|
|
||||||
|
# Modification de l'état
|
||||||
|
justi.etat = scu.EtatJustificatif.MODIFIE
|
||||||
|
# Modification du moduleimpl
|
||||||
|
justi.date_debut = scu.localize_datetime("2023-02-03T11:00:01+01:00")
|
||||||
|
justi.date_fin = scu.localize_datetime("2023-02-03T12:00:01+01:00")
|
||||||
|
|
||||||
|
db.session.add(justi)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Vérification du changement
|
||||||
|
assert (
|
||||||
|
scass.filter_justificatifs_by_etat(etud.justificatifs, "modifie").count() == 2
|
||||||
|
), "Edition de justificatif mauvais"
|
||||||
|
|
||||||
|
assert (
|
||||||
|
scass.filter_by_date(
|
||||||
|
etud.justificatifs,
|
||||||
|
Justificatif,
|
||||||
|
date_deb=scu.localize_datetime("2023-02-01T11:00:00+01:00"),
|
||||||
|
).count()
|
||||||
|
== 1
|
||||||
|
), "Edition de justificatif mauvais 2"
|
||||||
|
|
||||||
|
# Supression d'une assiduité
|
||||||
|
|
||||||
|
db.session.delete(justi)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
assert etud.justificatifs.count() == 4, "Supression de justificatif mauvais"
|
||||||
|
|
||||||
|
|
||||||
|
def editer_supprimer_assiduites(etuds: list[Identite], moduleimpls: list[int]):
|
||||||
|
"""
|
||||||
|
Troisième Partie:
|
||||||
|
- Vérification de l'édition des assiduitées
|
||||||
|
- Vérification de la suppression des assiduitées
|
||||||
|
"""
|
||||||
|
|
||||||
|
ass1: Assiduite = etuds[0].assiduites.first()
|
||||||
|
ass2: Assiduite = etuds[1].assiduites.first()
|
||||||
|
ass3: Assiduite = etuds[2].assiduites.first()
|
||||||
|
|
||||||
|
# Modification de l'état
|
||||||
|
ass1.etat = scu.EtatAssiduite.RETARD
|
||||||
|
db.session.add(ass1)
|
||||||
|
# Modification du moduleimpl
|
||||||
|
ass2.moduleimpl_id = moduleimpls[0].id
|
||||||
|
db.session.add(ass2)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Vérification du changement
|
||||||
|
assert (
|
||||||
|
scass.filter_assiduites_by_etat(etuds[0].assiduites, "retard").count() == 4
|
||||||
|
), "Edition d'assiduité mauvais"
|
||||||
|
assert (
|
||||||
|
scass.filter_by_module_impl(etuds[1].assiduites, moduleimpls[0].id).count() == 2
|
||||||
|
), "Edition d'assiduité mauvais"
|
||||||
|
|
||||||
|
# Supression d'une assiduité
|
||||||
|
|
||||||
|
db.session.delete(ass3)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
assert etuds[2].assiduites.count() == 6, "Supression d'assiduité mauvais"
|
||||||
|
|
||||||
|
|
||||||
|
def ajouter_assiduites(
|
||||||
|
etuds: list[Identite], moduleimpls: list[ModuleImpl], etud_faux: Identite
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Première partie:
|
||||||
|
- Ajoute 6 assiduités à chaque étudiant
|
||||||
|
- 2 présence (semestre 1 et 2)
|
||||||
|
- 2 retard (semestre 2)
|
||||||
|
- 2 absence (semestre 1)
|
||||||
|
- Vérifie la création des assiduités
|
||||||
|
"""
|
||||||
|
|
||||||
|
for etud in etuds:
|
||||||
|
obj_assiduites = [
|
||||||
|
{
|
||||||
|
"etat": scu.EtatAssiduite.PRESENT,
|
||||||
|
"deb": "2022-09-03T08:00+01:00",
|
||||||
|
"fin": "2022-09-03T10:00+01:00",
|
||||||
|
"moduleimpl": None,
|
||||||
|
"desc": None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"etat": scu.EtatAssiduite.PRESENT,
|
||||||
|
"deb": "2023-01-03T08:00+01:00",
|
||||||
|
"fin": "2023-01-03T10:00+01:00",
|
||||||
|
"moduleimpl": moduleimpls[2],
|
||||||
|
"desc": None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"etat": scu.EtatAssiduite.ABSENT,
|
||||||
|
"deb": "2022-09-03T10:00:01+01:00",
|
||||||
|
"fin": "2022-09-03T11:00+01:00",
|
||||||
|
"moduleimpl": moduleimpls[0],
|
||||||
|
"desc": None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"etat": scu.EtatAssiduite.ABSENT,
|
||||||
|
"deb": "2022-09-03T14:00:00+01:00",
|
||||||
|
"fin": "2022-09-03T15:00+01:00",
|
||||||
|
"moduleimpl": moduleimpls[1],
|
||||||
|
"desc": "Description",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"etat": scu.EtatAssiduite.RETARD,
|
||||||
|
"deb": "2023-01-03T11:00:01+01:00",
|
||||||
|
"fin": "2023-01-03T12:00+01:00",
|
||||||
|
"moduleimpl": moduleimpls[3],
|
||||||
|
"desc": None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"etat": scu.EtatAssiduite.RETARD,
|
||||||
|
"deb": "2023-01-04T11:00:01+01:00",
|
||||||
|
"fin": "2023-01-04T12:00+01:00",
|
||||||
|
"moduleimpl": moduleimpls[3],
|
||||||
|
"desc": "Description",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"etat": scu.EtatAssiduite.RETARD,
|
||||||
|
"deb": "2022-11-04T11:00:01+01:00",
|
||||||
|
"fin": "2022-12-05T12:00+01:00",
|
||||||
|
"moduleimpl": None,
|
||||||
|
"desc": "Description",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
assiduites = [
|
||||||
|
Assiduite.create_assiduite(
|
||||||
|
etud,
|
||||||
|
scu.is_iso_formated(ass["deb"], True),
|
||||||
|
scu.is_iso_formated(ass["fin"], True),
|
||||||
|
ass["etat"],
|
||||||
|
ass["moduleimpl"],
|
||||||
|
ass["desc"],
|
||||||
|
)
|
||||||
|
for ass in obj_assiduites
|
||||||
|
]
|
||||||
|
|
||||||
|
# Vérification de la création des assiduités
|
||||||
|
assert [
|
||||||
|
ass for ass in assiduites if not isinstance(ass, Assiduite)
|
||||||
|
] == [], "La création des assiduités de base n'est pas OK"
|
||||||
|
|
||||||
|
# Vérification de la gestion des erreurs
|
||||||
|
|
||||||
|
test_assiduite = {
|
||||||
|
"etat": scu.EtatAssiduite.RETARD,
|
||||||
|
"deb": "2023-01-04T11:00:01+01:00",
|
||||||
|
"fin": "2023-01-04T12:00+01:00",
|
||||||
|
"moduleimpl": moduleimpls[3],
|
||||||
|
"desc": "Description",
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
Assiduite.create_assiduite(
|
||||||
|
etuds[0],
|
||||||
|
scu.is_iso_formated(test_assiduite["deb"], True),
|
||||||
|
scu.is_iso_formated(test_assiduite["fin"], True),
|
||||||
|
test_assiduite["etat"],
|
||||||
|
test_assiduite["moduleimpl"],
|
||||||
|
test_assiduite["desc"],
|
||||||
|
)
|
||||||
|
except ScoValueError as excp:
|
||||||
|
assert (
|
||||||
|
excp.args[0]
|
||||||
|
== "Duplication des assiduités (la période rentrée rentre en conflit avec une assiduité enregistrée)"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
Assiduite.create_assiduite(
|
||||||
|
etud_faux,
|
||||||
|
scu.is_iso_formated(test_assiduite["deb"], True),
|
||||||
|
scu.is_iso_formated(test_assiduite["fin"], True),
|
||||||
|
test_assiduite["etat"],
|
||||||
|
test_assiduite["moduleimpl"],
|
||||||
|
test_assiduite["desc"],
|
||||||
|
)
|
||||||
|
except ScoValueError as excp:
|
||||||
|
assert excp.args[0] == "L'étudiant n'est pas inscrit au moduleimpl"
|
||||||
|
|
||||||
|
|
||||||
|
def verifier_comptage_et_filtrage_assiduites(
|
||||||
|
etuds: list[Identite], moduleimpls: list[int], formsemestres: tuple[int]
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Deuxième partie:
|
||||||
|
- vérifier les valeurs du comptage (compte, heure, journée, demi-journée)
|
||||||
|
- vérifier le filtrage des assiduites (etat, debut, fin, module, formsemestre)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
etu1, etu2, etu3 = etuds
|
||||||
|
|
||||||
|
mod11, mod12, mod21, mod22 = moduleimpls
|
||||||
|
|
||||||
|
# Vérification du comptage classique
|
||||||
|
comptage = scass.get_assiduites_stats(etu1.assiduites)
|
||||||
|
|
||||||
|
assert comptage["compte"] == 6 + 1, "la métrique 'Comptage' n'est pas bien calculée"
|
||||||
|
assert (
|
||||||
|
comptage["journee"] == 3 + 22
|
||||||
|
), "la métrique 'Journée' n'est pas bien calculée"
|
||||||
|
assert (
|
||||||
|
comptage["demi"] == 4 + 43
|
||||||
|
), "la métrique 'Demi-Journée' n'est pas bien calculée"
|
||||||
|
assert comptage["heure"] == float(
|
||||||
|
8 + 169
|
||||||
|
), "la métrique 'Heure' n'est pas bien calculée"
|
||||||
|
|
||||||
|
# Vérification du filtrage classique
|
||||||
|
|
||||||
|
# Etat
|
||||||
|
assert (
|
||||||
|
scass.filter_assiduites_by_etat(etu2.assiduites, "present").count() == 2
|
||||||
|
), "Filtrage de l'état 'présent' mauvais"
|
||||||
|
assert (
|
||||||
|
scass.filter_assiduites_by_etat(etu2.assiduites, "retard").count() == 3
|
||||||
|
), "Filtrage de l'état 'retard' mauvais"
|
||||||
|
assert (
|
||||||
|
scass.filter_assiduites_by_etat(etu2.assiduites, "absent").count() == 2
|
||||||
|
), "Filtrage de l'état 'absent' mauvais"
|
||||||
|
assert (
|
||||||
|
scass.filter_assiduites_by_etat(etu2.assiduites, "absent,retard").count() == 5
|
||||||
|
), "Filtrage de l'état 'absent,retard' mauvais"
|
||||||
|
assert (
|
||||||
|
scass.filter_assiduites_by_etat(
|
||||||
|
etu2.assiduites, "absent,retard,present"
|
||||||
|
).count()
|
||||||
|
== 7
|
||||||
|
), "Filtrage de l'état 'absent,retard,present' mauvais"
|
||||||
|
assert (
|
||||||
|
scass.filter_assiduites_by_etat(etu2.assiduites, "autre").count() == 0
|
||||||
|
), "Filtrage de l'état 'autre' mauvais"
|
||||||
|
|
||||||
|
# Module
|
||||||
|
assert (
|
||||||
|
scass.filter_by_module_impl(etu3.assiduites, mod11.id).count() == 1
|
||||||
|
), "Filtrage par 'Moduleimpl' mauvais"
|
||||||
|
assert (
|
||||||
|
scass.filter_by_module_impl(etu3.assiduites, mod12.id).count() == 1
|
||||||
|
), "Filtrage par 'Moduleimpl' mauvais"
|
||||||
|
assert (
|
||||||
|
scass.filter_by_module_impl(etu3.assiduites, mod21.id).count() == 1
|
||||||
|
), "Filtrage par 'Moduleimpl' mauvais"
|
||||||
|
assert (
|
||||||
|
scass.filter_by_module_impl(etu3.assiduites, mod22.id).count() == 2
|
||||||
|
), "Filtrage par 'Moduleimpl' mauvais"
|
||||||
|
assert (
|
||||||
|
scass.filter_by_module_impl(etu3.assiduites, None).count() == 2
|
||||||
|
), "Filtrage par 'Moduleimpl' mauvais"
|
||||||
|
assert (
|
||||||
|
scass.filter_by_module_impl(etu3.assiduites, 152).count() == 0
|
||||||
|
), "Filtrage par 'Moduleimpl' mauvais"
|
||||||
|
|
||||||
|
# Formsemestre
|
||||||
|
formsemestres = [
|
||||||
|
FormSemestre.query.filter_by(id=fms["id"]).first() for fms in formsemestres
|
||||||
|
]
|
||||||
|
assert (
|
||||||
|
scass.filter_by_formsemestre(etu1.assiduites, formsemestres[0]).count() == 4
|
||||||
|
), "Filtrage 'Formsemestre' mauvais"
|
||||||
|
assert (
|
||||||
|
scass.filter_by_formsemestre(etu1.assiduites, formsemestres[1]).count() == 3
|
||||||
|
), "Filtrage 'Formsemestre' mauvais"
|
||||||
|
assert (
|
||||||
|
scass.filter_by_formsemestre(etu1.assiduites, formsemestres[2]).count() == 0
|
||||||
|
), "Filtrage 'Formsemestre' mauvais"
|
||||||
|
|
||||||
|
# Date début
|
||||||
|
assert (
|
||||||
|
scass.filter_by_date(etu2.assiduites, Assiduite).count() == 7
|
||||||
|
), "Filtrage 'Date début' mauvais 1"
|
||||||
|
|
||||||
|
date = scu.localize_datetime("2022-09-01T10:00+01:00")
|
||||||
|
assert (
|
||||||
|
scass.filter_by_date(etu2.assiduites, Assiduite, date_deb=date).count() == 7
|
||||||
|
), "Filtrage 'Date début' mauvais 2"
|
||||||
|
|
||||||
|
date = scu.localize_datetime("2022-09-03T10:00+01:00")
|
||||||
|
assert (
|
||||||
|
scass.filter_by_date(etu2.assiduites, Assiduite, date_deb=date).count() == 7
|
||||||
|
), "Filtrage 'Date début' mauvais 3"
|
||||||
|
|
||||||
|
date = scu.localize_datetime("2022-09-03T16:00+01:00")
|
||||||
|
assert (
|
||||||
|
scass.filter_by_date(etu2.assiduites, Assiduite, date_deb=date).count() == 4
|
||||||
|
), "Filtrage 'Date début' mauvais 4"
|
||||||
|
|
||||||
|
# Date Fin
|
||||||
|
|
||||||
|
date = scu.localize_datetime("2022-09-01T10:00+01:00")
|
||||||
|
assert (
|
||||||
|
scass.filter_by_date(etu2.assiduites, Assiduite, date_fin=date).count() == 0
|
||||||
|
), "Filtrage 'Date fin' mauvais 1"
|
||||||
|
|
||||||
|
date = scu.localize_datetime("2022-09-03T10:00+01:00")
|
||||||
|
assert (
|
||||||
|
scass.filter_by_date(etu2.assiduites, Assiduite, date_fin=date).count() == 1
|
||||||
|
), "Filtrage 'Date fin' mauvais 2"
|
||||||
|
|
||||||
|
date = scu.localize_datetime("2022-09-03T10:00:01+01:00")
|
||||||
|
assert (
|
||||||
|
scass.filter_by_date(etu2.assiduites, Assiduite, date_fin=date).count() == 2
|
||||||
|
), "Filtrage 'Date fin' mauvais 3"
|
||||||
|
|
||||||
|
date = scu.localize_datetime("2022-09-03T16:00+01:00")
|
||||||
|
assert (
|
||||||
|
scass.filter_by_date(etu2.assiduites, Assiduite, date_fin=date).count() == 3
|
||||||
|
), "Filtrage 'Date fin' mauvais 4"
|
||||||
|
|
||||||
|
date = scu.localize_datetime("2023-01-04T16:00+01:00")
|
||||||
|
assert (
|
||||||
|
scass.filter_by_date(etu2.assiduites, Assiduite, date_fin=date).count() == 7
|
||||||
|
), "Filtrage 'Date fin' mauvais 5"
|
@ -8,3 +8,5 @@ from tools.import_scodoc7_user_db import import_scodoc7_user_db
|
|||||||
from tools.import_scodoc7_dept import import_scodoc7_dept
|
from tools.import_scodoc7_dept import import_scodoc7_dept
|
||||||
from tools.migrate_scodoc7_archives import migrate_scodoc7_dept_archives
|
from tools.migrate_scodoc7_archives import migrate_scodoc7_dept_archives
|
||||||
from tools.migrate_scodoc7_logos import migrate_scodoc7_dept_logos
|
from tools.migrate_scodoc7_logos import migrate_scodoc7_dept_logos
|
||||||
|
from tools.migrate_abs_to_assiduites import migrate_abs_to_assiduites
|
||||||
|
from tools.downgrade_assiduites import downgrade_module
|
||||||
|
71
tools/downgrade_assiduites.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
"""
|
||||||
|
Commande permettant de supprimer les assiduités et les justificatifs
|
||||||
|
|
||||||
|
Ecrit par Matthias HARTMANN
|
||||||
|
"""
|
||||||
|
|
||||||
|
from app import db
|
||||||
|
from app.models import Justificatif, Assiduite, Departement
|
||||||
|
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
|
||||||
|
from app.scodoc.sco_utils import TerminalColor
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade_module(
|
||||||
|
dept: str = None, assiduites: bool = False, justificatifs: bool = False
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Supprime les assiduités et/ou justificatifs du dept sélectionné ou de tous les départements
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dept (str, optional): l'acronym du département. Par défaut tous les départements.
|
||||||
|
assiduites (bool, optional): suppression des assiduités. Par défaut : Non
|
||||||
|
justificatifs (bool, optional): supression des justificatifs. Par défaut : Non
|
||||||
|
"""
|
||||||
|
|
||||||
|
dept_etudid: list[int] = None
|
||||||
|
dept_id: int = None
|
||||||
|
|
||||||
|
if dept is not None:
|
||||||
|
departement: Departement = Departement.query.filter_by(acronym=dept).first()
|
||||||
|
|
||||||
|
assert departement is not None, "Le département n'existe pas."
|
||||||
|
|
||||||
|
dept_etudid = [etud.id for etud in departement.etudiants]
|
||||||
|
dept_id = departement.id
|
||||||
|
|
||||||
|
if assiduites:
|
||||||
|
_remove_assiduites(dept_etudid)
|
||||||
|
|
||||||
|
if justificatifs:
|
||||||
|
_remove_justificatifs(dept_etudid)
|
||||||
|
_remove_justificatifs_archive(dept_id)
|
||||||
|
|
||||||
|
if dept is None:
|
||||||
|
if assiduites:
|
||||||
|
db.session.execute("ALTER SEQUENCE assiduites_id_seq RESTART WITH 1")
|
||||||
|
if justificatifs:
|
||||||
|
db.session.execute("ALTER SEQUENCE justificatifs_id_seq RESTART WITH 1")
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"{TerminalColor.GREEN}Le module assiduité a bien été remis à zero.{TerminalColor.RESET}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _remove_assiduites(dept_etudid: str = None):
|
||||||
|
if dept_etudid is None:
|
||||||
|
Assiduite.query.delete()
|
||||||
|
else:
|
||||||
|
Assiduite.query.filter(Assiduite.etudid.in_(dept_etudid)).delete()
|
||||||
|
|
||||||
|
|
||||||
|
def _remove_justificatifs(dept_etudid: str = None):
|
||||||
|
if dept_etudid is None:
|
||||||
|
Justificatif.query.delete()
|
||||||
|
else:
|
||||||
|
Justificatif.query.filter(Justificatif.etudid.in_(dept_etudid)).delete()
|
||||||
|
|
||||||
|
|
||||||
|
def _remove_justificatifs_archive(dept_id: int = None):
|
||||||
|
JustificatifArchiver().remove_dept_archive(dept_id)
|
@ -21,11 +21,13 @@ from app import models
|
|||||||
from app.models import departements
|
from app.models import departements
|
||||||
from app.models import (
|
from app.models import (
|
||||||
Absence,
|
Absence,
|
||||||
|
Assiduite,
|
||||||
Departement,
|
Departement,
|
||||||
Formation,
|
Formation,
|
||||||
FormSemestre,
|
FormSemestre,
|
||||||
FormSemestreEtape,
|
FormSemestreEtape,
|
||||||
Identite,
|
Identite,
|
||||||
|
Justificatif,
|
||||||
ModuleImpl,
|
ModuleImpl,
|
||||||
NotesNotes,
|
NotesNotes,
|
||||||
)
|
)
|
||||||
@ -37,6 +39,7 @@ from app.scodoc import (
|
|||||||
sco_groups,
|
sco_groups,
|
||||||
)
|
)
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
|
from app.scodoc.sco_utils import localize_datetime
|
||||||
from tools.fakeportal.gen_nomprenoms import nomprenom
|
from tools.fakeportal.gen_nomprenoms import nomprenom
|
||||||
|
|
||||||
random.seed(12345678) # tests reproductibles
|
random.seed(12345678) # tests reproductibles
|
||||||
@ -378,6 +381,56 @@ def create_logos():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def ajouter_assiduites_justificatifs(formsemestre: FormSemestre):
|
||||||
|
"""
|
||||||
|
Ajoute des assiduités semi-aléatoires à chaque étudiant du semestre
|
||||||
|
"""
|
||||||
|
MODS = [moduleimpl for moduleimpl in formsemestre.modimpls]
|
||||||
|
MODS.append(None)
|
||||||
|
|
||||||
|
for etud in formsemestre.etuds:
|
||||||
|
base_date = datetime.datetime(2022, 9, random.randint(1, 30), 8, 0, 0)
|
||||||
|
base_date = localize_datetime(base_date)
|
||||||
|
|
||||||
|
for i in range(random.randint(1, 5)):
|
||||||
|
etat = random.randint(0, 2)
|
||||||
|
moduleimpl = random.choice(MODS)
|
||||||
|
deb_date = base_date + datetime.timedelta(days=i)
|
||||||
|
fin_date = deb_date + datetime.timedelta(hours=i)
|
||||||
|
|
||||||
|
code = Assiduite.create_assiduite(
|
||||||
|
etud, deb_date, fin_date, etat, moduleimpl
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(
|
||||||
|
code, Assiduite
|
||||||
|
), "Erreur dans la génération des assiduités"
|
||||||
|
|
||||||
|
db.session.add(code)
|
||||||
|
|
||||||
|
for i in range(random.randint(0, 2)):
|
||||||
|
etat = random.randint(0, 3)
|
||||||
|
deb_date = base_date + datetime.timedelta(days=i)
|
||||||
|
fin_date = deb_date + datetime.timedelta(hours=8)
|
||||||
|
raison = random.choice(["raison", None])
|
||||||
|
|
||||||
|
code = Justificatif.create_justificatif(
|
||||||
|
etud=etud,
|
||||||
|
date_debut=deb_date,
|
||||||
|
date_fin=fin_date,
|
||||||
|
etat=etat,
|
||||||
|
raison=raison,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(
|
||||||
|
code, Justificatif
|
||||||
|
), "Erreur dans la génération des justificatifs"
|
||||||
|
|
||||||
|
db.session.add(code)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
def init_test_database():
|
def init_test_database():
|
||||||
"""Appelé par la commande `flask init-test-database`
|
"""Appelé par la commande `flask init-test-database`
|
||||||
|
|
||||||
@ -398,6 +451,7 @@ def init_test_database():
|
|||||||
saisie_notes_evaluations(formsemestre, user_lecteur)
|
saisie_notes_evaluations(formsemestre, user_lecteur)
|
||||||
add_absences(formsemestre)
|
add_absences(formsemestre)
|
||||||
create_etape_apo(formsemestre)
|
create_etape_apo(formsemestre)
|
||||||
|
ajouter_assiduites_justificatifs(formsemestre)
|
||||||
create_logos()
|
create_logos()
|
||||||
# à compléter
|
# à compléter
|
||||||
# - groupes
|
# - groupes
|
||||||
|
424
tools/migrate_abs_to_assiduites.py
Normal file
@ -0,0 +1,424 @@
|
|||||||
|
"""
|
||||||
|
Script de migration des données de la base "absences" -> "assiduites"/"justificatifs"
|
||||||
|
|
||||||
|
Ecrit par Matthias HARTMANN
|
||||||
|
"""
|
||||||
|
from datetime import date, datetime, time, timedelta
|
||||||
|
from json import dump, dumps
|
||||||
|
from sqlalchemy import not_
|
||||||
|
|
||||||
|
from app import db
|
||||||
|
from app.models import (
|
||||||
|
Absence,
|
||||||
|
Assiduite,
|
||||||
|
Departement,
|
||||||
|
Identite,
|
||||||
|
Justificatif,
|
||||||
|
ModuleImplInscription,
|
||||||
|
)
|
||||||
|
|
||||||
|
from app.models.config import ScoDocSiteConfig
|
||||||
|
|
||||||
|
from app.models.assiduites import (
|
||||||
|
compute_assiduites_justified,
|
||||||
|
)
|
||||||
|
from app.profiler import Profiler
|
||||||
|
from app.scodoc.sco_utils import (
|
||||||
|
EtatAssiduite,
|
||||||
|
EtatJustificatif,
|
||||||
|
TerminalColor,
|
||||||
|
localize_datetime,
|
||||||
|
print_progress_bar,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _Merger:
|
||||||
|
"""pour typage"""
|
||||||
|
|
||||||
|
|
||||||
|
class _glob:
|
||||||
|
"""variables globales du script"""
|
||||||
|
|
||||||
|
DEBUG: bool = False
|
||||||
|
PROBLEMS: dict[int, list[str]] = {}
|
||||||
|
CURRENT_ETU: list = []
|
||||||
|
MODULES: list[tuple[int, int]] = []
|
||||||
|
COMPTE: list[int, int] = []
|
||||||
|
ERR_ETU: list[int] = []
|
||||||
|
MERGER_ASSI: _Merger = None
|
||||||
|
MERGER_JUST: _Merger = None
|
||||||
|
|
||||||
|
MORNING: time = None
|
||||||
|
NOON: time = None
|
||||||
|
EVENING: time = None
|
||||||
|
|
||||||
|
|
||||||
|
class _Merger:
|
||||||
|
def __init__(self, abs_: Absence, est_abs: bool) -> None:
|
||||||
|
self.deb = (abs_.jour, abs_.matin)
|
||||||
|
self.fin = (abs_.jour, abs_.matin)
|
||||||
|
self.moduleimpl = abs_.moduleimpl_id
|
||||||
|
self.etudid = abs_.etudid
|
||||||
|
self.est_abs = est_abs
|
||||||
|
self.raison = abs_.description
|
||||||
|
self.entry_date = abs_.entry_date
|
||||||
|
|
||||||
|
def merge(self, abs_: Absence) -> bool:
|
||||||
|
"""Fusionne les absences"""
|
||||||
|
|
||||||
|
if self.etudid != abs_.etudid:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Cas d'une même absence enregistrée plusieurs fois
|
||||||
|
if self.fin == (abs_.jour, abs_.matin):
|
||||||
|
self.moduleimpl = None
|
||||||
|
else:
|
||||||
|
if self.fin[1]:
|
||||||
|
if abs_.jour != self.fin[0]:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
day_after: date = abs_.jour - timedelta(days=1) == self.fin[0]
|
||||||
|
if not (day_after and abs_.matin):
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.fin = (abs_.jour, abs_.matin)
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _tuple_to_date(couple: tuple[date, bool], end=False):
|
||||||
|
if couple[1]:
|
||||||
|
time_ = _glob.NOON if end else _glob.MORNING
|
||||||
|
date_ = datetime.combine(couple[0], time_)
|
||||||
|
else:
|
||||||
|
time_ = _glob.EVENING if end else _glob.NOON
|
||||||
|
date_ = datetime.combine(couple[0], time_)
|
||||||
|
d = localize_datetime(date_)
|
||||||
|
return d
|
||||||
|
|
||||||
|
def _to_justif(self):
|
||||||
|
date_deb = _Merger._tuple_to_date(self.deb)
|
||||||
|
date_fin = _Merger._tuple_to_date(self.fin, end=True)
|
||||||
|
|
||||||
|
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)
|
||||||
|
date_fin = _Merger._tuple_to_date(self.fin, end=True)
|
||||||
|
|
||||||
|
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
|
||||||
|
if self.est_abs:
|
||||||
|
_glob.COMPTE[0] += 1
|
||||||
|
obj = self._to_assi()
|
||||||
|
else:
|
||||||
|
_glob.COMPTE[1] += 1
|
||||||
|
obj = self._to_justif()
|
||||||
|
|
||||||
|
db.session.add(obj)
|
||||||
|
|
||||||
|
|
||||||
|
class _Statistics:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.object: dict[str, dict or int] = {"total": 0}
|
||||||
|
self.year: int = None
|
||||||
|
|
||||||
|
def __set_year(self, year: int):
|
||||||
|
if year not in self.object:
|
||||||
|
self.object[year] = {
|
||||||
|
"etuds_inexistant": [],
|
||||||
|
"abs_invalide": {},
|
||||||
|
}
|
||||||
|
self.year = year
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __add_etud(self, etudid: int):
|
||||||
|
if etudid not in self.object[self.year]["etuds_inexistant"]:
|
||||||
|
self.object[self.year]["etuds_inexistant"].append(etudid)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __add_abs(self, abs_: int, err: str):
|
||||||
|
if abs_ not in self.object[self.year]["abs_invalide"]:
|
||||||
|
self.object[self.year]["abs_invalide"][abs_] = [err]
|
||||||
|
else:
|
||||||
|
self.object[self.year]["abs_invalide"][abs_].append(err)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_problem(self, abs_: Absence, err: str):
|
||||||
|
"""Ajoute un nouveau problème dans les statistiques"""
|
||||||
|
abs_.jour: date
|
||||||
|
pivot: date = date(abs_.jour.year, 9, 15)
|
||||||
|
year: int = abs_.jour.year
|
||||||
|
if pivot < abs_.jour:
|
||||||
|
year += 1
|
||||||
|
self.__set_year(year)
|
||||||
|
|
||||||
|
if err == "Etudiant inexistant":
|
||||||
|
self.__add_etud(abs_.etudid)
|
||||||
|
else:
|
||||||
|
self.__add_abs(abs_.id, err)
|
||||||
|
|
||||||
|
self.object["total"] += 1
|
||||||
|
|
||||||
|
def compute_stats(self) -> dict:
|
||||||
|
"""Comptage des statistiques"""
|
||||||
|
stats: dict = {"total": self.object["total"]}
|
||||||
|
for year, item in self.object.items():
|
||||||
|
if year == "total":
|
||||||
|
continue
|
||||||
|
|
||||||
|
stats[year] = {}
|
||||||
|
stats[year]["etuds_inexistant"] = len(item["etuds_inexistant"])
|
||||||
|
stats[year]["abs_invalide"] = len(item["abs_invalide"])
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
def export(self, file):
|
||||||
|
"""Sérialise les statistiques dans un fichier"""
|
||||||
|
dump(self.object, file, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_abs_to_assiduites(
|
||||||
|
dept: str = None,
|
||||||
|
morning: str = None,
|
||||||
|
noon: str = None,
|
||||||
|
evening: str = None,
|
||||||
|
debug: bool = False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
une absence à 3 états:
|
||||||
|
|
||||||
|
|.estabs|.estjust|
|
||||||
|
|1|0| -> absence non justifiée
|
||||||
|
|1|1| -> absence justifiée
|
||||||
|
|0|1| -> justifié
|
||||||
|
|
||||||
|
dualité des temps :
|
||||||
|
|
||||||
|
.matin: bool (0:00 -> time_pref | time_pref->23:59:59)
|
||||||
|
.jour : date (jour de l'absence/justificatif)
|
||||||
|
.moduleimpl_id: relation -> moduleimpl_id
|
||||||
|
description:str -> motif abs / raision justif
|
||||||
|
|
||||||
|
.entry_date: datetime -> timestamp d'entrée de l'abs
|
||||||
|
.etudid: relation -> Identite
|
||||||
|
"""
|
||||||
|
Profiler.clear()
|
||||||
|
|
||||||
|
_glob.DEBUG = debug
|
||||||
|
|
||||||
|
if morning is None:
|
||||||
|
morning = ScoDocSiteConfig.get("assi_morning_time", time(8, 0))
|
||||||
|
|
||||||
|
morning: list[str] = str(morning).split(":")
|
||||||
|
_glob.MORNING = time(int(morning[0]), int(morning[1]))
|
||||||
|
|
||||||
|
if noon is None:
|
||||||
|
noon = ScoDocSiteConfig.get("assi_lunch_time", time(13, 0))
|
||||||
|
|
||||||
|
noon: list[str] = str(noon).split(":")
|
||||||
|
_glob.NOON = time(int(noon[0]), int(noon[1]))
|
||||||
|
|
||||||
|
if evening is None:
|
||||||
|
evening = ScoDocSiteConfig.get("assi_afternoon_time", time(18, 0))
|
||||||
|
|
||||||
|
evening: list[str] = str(evening).split(":")
|
||||||
|
_glob.EVENING = time(int(evening[0]), int(evening[1]))
|
||||||
|
|
||||||
|
if dept is None:
|
||||||
|
prof_total = Profiler("MigrationTotal")
|
||||||
|
prof_total.start()
|
||||||
|
depart: Departement
|
||||||
|
for depart in Departement.query.order_by(Departement.id):
|
||||||
|
migrate_dept(
|
||||||
|
depart.acronym, _Statistics(), Profiler(f"Migration_{depart.acronym}")
|
||||||
|
)
|
||||||
|
prof_total.stop()
|
||||||
|
|
||||||
|
print(
|
||||||
|
TerminalColor.GREEN
|
||||||
|
+ f"Fin de la migration, elle a durée {prof_total.elapsed():.2f}"
|
||||||
|
+ TerminalColor.RESET
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
migrate_dept(dept, _Statistics(), Profiler("Migration"))
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_dept(dept_name: str, stats: _Statistics, time_elapsed: Profiler):
|
||||||
|
time_elapsed.start()
|
||||||
|
|
||||||
|
absences_query = Absence.query
|
||||||
|
dept: Departement = Departement.query.filter_by(acronym=dept_name).first()
|
||||||
|
|
||||||
|
if dept is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
etuds_id: list[int] = [etud.id for etud in dept.etudiants]
|
||||||
|
absences_query = absences_query.filter(Absence.etudid.in_(etuds_id))
|
||||||
|
absences: Absence = absences_query.order_by(
|
||||||
|
Absence.etudid, Absence.jour, not_(Absence.matin)
|
||||||
|
)
|
||||||
|
|
||||||
|
absences_len: int = absences.count()
|
||||||
|
|
||||||
|
if absences_len == 0:
|
||||||
|
print(
|
||||||
|
f"{TerminalColor.BLUE}Le département {dept_name} ne possède aucune absence.{TerminalColor.RESET}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
_glob.CURRENT_ETU = []
|
||||||
|
_glob.MODULES = []
|
||||||
|
_glob.COMPTE = [0, 0]
|
||||||
|
_glob.ERR_ETU = []
|
||||||
|
_glob.MERGER_ASSI = None
|
||||||
|
_glob.MERGER_JUST = None
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"{TerminalColor.BLUE}{absences_len} absences du département {dept_name} vont être migrées{TerminalColor.RESET}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print_progress_bar(0, absences_len, "Progression", "effectué", autosize=True)
|
||||||
|
|
||||||
|
for i, abs_ in enumerate(absences):
|
||||||
|
try:
|
||||||
|
_from_abs_to_assiduite_justificatif(abs_)
|
||||||
|
except ValueError as e:
|
||||||
|
stats.add_problem(abs_, e.args[0])
|
||||||
|
|
||||||
|
if i % 10 == 0:
|
||||||
|
print_progress_bar(
|
||||||
|
i,
|
||||||
|
absences_len,
|
||||||
|
"Progression",
|
||||||
|
"effectué",
|
||||||
|
autosize=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if i % 1000 == 0:
|
||||||
|
print_progress_bar(
|
||||||
|
i,
|
||||||
|
absences_len,
|
||||||
|
"Progression",
|
||||||
|
"effectué",
|
||||||
|
autosize=True,
|
||||||
|
)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
_glob.MERGER_ASSI.export()
|
||||||
|
_glob.MERGER_JUST.export()
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
justifs: Justificatif = Justificatif.query
|
||||||
|
|
||||||
|
if dept_name is not None:
|
||||||
|
justifs.filter(Justificatif.etudid.in_(etuds_id))
|
||||||
|
|
||||||
|
print_progress_bar(
|
||||||
|
absences_len,
|
||||||
|
absences_len,
|
||||||
|
"Progression",
|
||||||
|
"effectué",
|
||||||
|
autosize=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
TerminalColor.RED
|
||||||
|
+ f"Justification des absences du département {dept_name}, veuillez patienter, ceci peut prendre un certain temps."
|
||||||
|
+ TerminalColor.RESET
|
||||||
|
)
|
||||||
|
|
||||||
|
compute_assiduites_justified(justifs, reset=True)
|
||||||
|
|
||||||
|
time_elapsed.stop()
|
||||||
|
|
||||||
|
statistiques: dict = stats.compute_stats()
|
||||||
|
print(
|
||||||
|
f"{TerminalColor.GREEN}La migration a pris {time_elapsed.elapsed():.2f} secondes {TerminalColor.RESET}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"{TerminalColor.RED}{statistiques['total']} absences qui n'ont pas pu être migrées."
|
||||||
|
)
|
||||||
|
|
||||||
|
filename = f"/opt/scodoc-data/log/{datetime.now().strftime('%Y-%m-%dT%H:%M:%S')}scodoc_migration_abs_{dept_name}.json"
|
||||||
|
print(
|
||||||
|
f"Vous retrouverez un fichier json {TerminalColor.GREEN}{filename}{TerminalColor.RED} contenant les problèmes de migrations"
|
||||||
|
)
|
||||||
|
with open(
|
||||||
|
filename,
|
||||||
|
"w",
|
||||||
|
encoding="utf-8",
|
||||||
|
) as file:
|
||||||
|
stats.export(file)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"{TerminalColor.CYAN}{_glob.COMPTE[0]} assiduités et {_glob.COMPTE[1]} justificatifs ont été générés pour le département {dept_name}.{TerminalColor.RESET}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if _glob.DEBUG:
|
||||||
|
print(dumps(statistiques, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def _from_abs_to_assiduite_justificatif(_abs: Absence):
|
||||||
|
if _abs.etudid not in _glob.CURRENT_ETU:
|
||||||
|
etud: Identite = Identite.query.filter_by(id=_abs.etudid).first()
|
||||||
|
if etud is None:
|
||||||
|
raise ValueError("Etudiant inexistant")
|
||||||
|
_glob.CURRENT_ETU.append(_abs.etudid)
|
||||||
|
|
||||||
|
if _abs.estabs:
|
||||||
|
moduleimpl_id: int = _abs.moduleimpl_id
|
||||||
|
if (
|
||||||
|
moduleimpl_id is not None
|
||||||
|
and (_abs.etudid, _abs.moduleimpl_id) not in _glob.MODULES
|
||||||
|
):
|
||||||
|
moduleimpl_inscription: ModuleImplInscription = (
|
||||||
|
ModuleImplInscription.query.filter_by(
|
||||||
|
moduleimpl_id=_abs.moduleimpl_id, etudid=_abs.etudid
|
||||||
|
).first()
|
||||||
|
)
|
||||||
|
if moduleimpl_inscription is None:
|
||||||
|
raise ValueError("Moduleimpl_id incorrect ou étudiant non inscrit")
|
||||||
|
|
||||||
|
if _glob.MERGER_ASSI is None:
|
||||||
|
_glob.MERGER_ASSI = _Merger(_abs, True)
|
||||||
|
return True
|
||||||
|
elif _glob.MERGER_ASSI.merge(_abs):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
_glob.MERGER_ASSI.export()
|
||||||
|
_glob.MERGER_ASSI = _Merger(_abs, True)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if _glob.MERGER_JUST is None:
|
||||||
|
_glob.MERGER_JUST = _Merger(_abs, False)
|
||||||
|
return True
|
||||||
|
elif _glob.MERGER_JUST.merge(_abs):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
_glob.MERGER_JUST.export()
|
||||||
|
_glob.MERGER_JUST = _Merger(_abs, False)
|
||||||
|
return False
|