Compare commits

...

10 Commits

36 changed files with 1453 additions and 581 deletions

View File

@ -1,8 +1,8 @@
"""api.__init__ """api.__init__
""" """
from flask_json import as_json
from flask import Blueprint from flask import Blueprint
from flask import request, g, jsonify from flask import request, g
from app import db from app import db
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import ScoException from app.scodoc.sco_exceptions import ScoException
@ -35,6 +35,7 @@ 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): 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]" Retourne une réponse contenant la représentation api de l'objet "Model[model_id]"
@ -48,7 +49,7 @@ def get_model_api_object(model_cls: db.Model, model_id: int, join_cls: db.Model
query = query.join(join_cls).filter_by(dept_id=g.scodoc_dept_id) query = query.join(join_cls).filter_by(dept_id=g.scodoc_dept_id)
unique: model_cls = query.first_or_404() unique: model_cls = query.first_or_404()
return jsonify(unique.to_dict(format_api=True)) return unique.to_dict(format_api=True)
from app.api import tokens from app.api import tokens

View File

@ -6,7 +6,8 @@
"""ScoDoc 9 API : Assiduités """ScoDoc 9 API : Assiduités
""" """
from datetime import datetime from datetime import datetime
from flask import g, jsonify, request from flask_json import as_json
from flask import g, request
from flask_login import login_required, current_user from flask_login import login_required, current_user
import app.scodoc.sco_assiduites as scass import app.scodoc.sco_assiduites as scass
@ -52,6 +53,7 @@ def assiduite(assiduite_id: int = None):
@api_web_bp.route("/assiduites/<int:etudid>/count/query", defaults={"with_query": True}) @api_web_bp.route("/assiduites/<int:etudid>/count/query", defaults={"with_query": True})
@login_required @login_required
@scodoc @scodoc
@as_json
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def count_assiduites(etudid: int = None, with_query: bool = False): def count_assiduites(etudid: int = None, with_query: bool = False):
""" """
@ -109,10 +111,8 @@ def count_assiduites(etudid: int = None, with_query: bool = False):
if with_query: if with_query:
metric, filtered = _count_manager(request) metric, filtered = _count_manager(request)
return jsonify( return scass.get_assiduites_stats(
scass.get_assiduites_stats( assiduites=etud.assiduites, metric=metric, filtered=filtered
assiduites=etud.assiduites, metric=metric, filtered=filtered
)
) )
@ -122,6 +122,7 @@ def count_assiduites(etudid: int = None, with_query: bool = False):
@api_web_bp.route("/assiduites/<int:etudid>/query", defaults={"with_query": True}) @api_web_bp.route("/assiduites/<int:etudid>/query", defaults={"with_query": True})
@login_required @login_required
@scodoc @scodoc
@as_json
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def assiduites(etudid: int = None, with_query: bool = False): def assiduites(etudid: int = None, with_query: bool = False):
""" """
@ -178,13 +179,14 @@ def assiduites(etudid: int = None, with_query: bool = False):
data = ass.to_dict(format_api=True) data = ass.to_dict(format_api=True)
data_set.append(data) data_set.append(data)
return jsonify(data_set) return data_set
@bp.route("/assiduites/group/query", defaults={"with_query": True}) @bp.route("/assiduites/group/query", defaults={"with_query": True})
@api_web_bp.route("/assiduites/group/query", defaults={"with_query": True}) @api_web_bp.route("/assiduites/group/query", defaults={"with_query": True})
@login_required @login_required
@scodoc @scodoc
@as_json
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def assiduites_group(with_query: bool = False): def assiduites_group(with_query: bool = False):
""" """
@ -247,12 +249,11 @@ def assiduites_group(with_query: bool = False):
if with_query: if with_query:
assiduites_query = _filter_manager(request, assiduites_query) assiduites_query = _filter_manager(request, assiduites_query)
data_set: dict[list[dict]] = {key: [] for key in etuds} data_set: dict[list[dict]] = {str(key): [] for key in etuds}
for ass in assiduites_query.all(): for ass in assiduites_query.all():
data = ass.to_dict(format_api=True) data = ass.to_dict(format_api=True)
data_set.get(data["etudid"]).append(data) data_set.get(str(data["etudid"])).append(data)
return data_set
return jsonify(data_set)
@bp.route( @bp.route(
@ -271,6 +272,7 @@ def assiduites_group(with_query: bool = False):
) )
@login_required @login_required
@scodoc @scodoc
@as_json
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False): def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
"""Retourne toutes les assiduités du formsemestre""" """Retourne toutes les assiduités du formsemestre"""
@ -291,7 +293,7 @@ def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
data = ass.to_dict(format_api=True) data = ass.to_dict(format_api=True)
data_set.append(data) data_set.append(data)
return jsonify(data_set) return data_set
@bp.route( @bp.route(
@ -312,6 +314,7 @@ def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
) )
@login_required @login_required
@scodoc @scodoc
@as_json
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def count_assiduites_formsemestre( def count_assiduites_formsemestre(
formsemestre_id: int = None, with_query: bool = False formsemestre_id: int = None, with_query: bool = False
@ -334,12 +337,13 @@ def count_assiduites_formsemestre(
if with_query: if with_query:
metric, filtered = _count_manager(request) metric, filtered = _count_manager(request)
return jsonify(scass.get_assiduites_stats(assiduites_query, metric, filtered)) return scass.get_assiduites_stats(assiduites_query, metric, filtered)
@bp.route("/assiduite/<int:etudid>/create", methods=["POST"]) @bp.route("/assiduite/<int:etudid>/create", methods=["POST"])
@api_web_bp.route("/assiduite/<int:etudid>/create", methods=["POST"]) @api_web_bp.route("/assiduite/<int:etudid>/create", methods=["POST"])
@scodoc @scodoc
@as_json
@login_required @login_required
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
# @permission_required(Permission.ScoAssiduiteChange) # @permission_required(Permission.ScoAssiduiteChange)
@ -382,12 +386,13 @@ def assiduite_create(etudid: int = None):
db.session.commit() db.session.commit()
return jsonify({"errors": errors, "success": success}) return {"errors": errors, "success": success}
@bp.route("/assiduites/create", methods=["POST"]) @bp.route("/assiduites/create", methods=["POST"])
@api_web_bp.route("/assiduites/create", methods=["POST"]) @api_web_bp.route("/assiduites/create", methods=["POST"])
@scodoc @scodoc
@as_json
@login_required @login_required
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
# @permission_required(Permission.ScoAssiduiteChange) # @permission_required(Permission.ScoAssiduiteChange)
@ -435,7 +440,7 @@ def assiduites_create():
else: else:
success[i] = obj success[i] = obj
return jsonify({"errors": errors, "success": success}) return {"errors": errors, "success": success}
def _create_singular( def _create_singular(
@ -515,6 +520,7 @@ def _create_singular(
@api_web_bp.route("/assiduite/delete", methods=["POST"]) @api_web_bp.route("/assiduite/delete", methods=["POST"])
@login_required @login_required
@scodoc @scodoc
@as_json
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
# @permission_required(Permission.ScoAssiduiteChange) # @permission_required(Permission.ScoAssiduiteChange)
def assiduite_delete(): def assiduite_delete():
@ -543,7 +549,7 @@ def assiduite_delete():
else: else:
output["success"][f"{i}"] = {"OK": True} output["success"][f"{i}"] = {"OK": True}
db.session.commit() db.session.commit()
return jsonify(output) return output
def _delete_singular(assiduite_id: int, database): def _delete_singular(assiduite_id: int, database):
@ -558,6 +564,7 @@ def _delete_singular(assiduite_id: int, database):
@api_web_bp.route("/assiduite/<int:assiduite_id>/edit", methods=["POST"]) @api_web_bp.route("/assiduite/<int:assiduite_id>/edit", methods=["POST"])
@login_required @login_required
@scodoc @scodoc
@as_json
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
# @permission_required(Permission.ScoAssiduiteChange) # @permission_required(Permission.ScoAssiduiteChange)
def assiduite_edit(assiduite_id: int): def assiduite_edit(assiduite_id: int):
@ -625,13 +632,14 @@ def assiduite_edit(assiduite_id: int):
db.session.add(assiduite_unique) db.session.add(assiduite_unique)
db.session.commit() db.session.commit()
return jsonify({"OK": True}) return {"OK": True}
@bp.route("/assiduites/edit", methods=["POST"]) @bp.route("/assiduites/edit", methods=["POST"])
@api_web_bp.route("/assiduites/edit", methods=["POST"]) @api_web_bp.route("/assiduites/edit", methods=["POST"])
@login_required @login_required
@scodoc @scodoc
@as_json
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
# @permission_required(Permission.ScoAssiduiteChange) # @permission_required(Permission.ScoAssiduiteChange)
def assiduites_edit(): def assiduites_edit():
@ -666,7 +674,7 @@ def assiduites_edit():
db.session.commit() db.session.commit()
return jsonify({"errors": errors, "success": success}) return {"errors": errors, "success": success}
def _edit_singular(assiduite_unique, data): def _edit_singular(assiduite_unique, data):

View File

@ -7,6 +7,7 @@
""" """
from datetime import datetime from datetime import datetime
from flask_json import as_json
from flask import g, jsonify, request from flask import g, jsonify, request
from flask_login import login_required, current_user from flask_login import login_required, current_user
@ -57,6 +58,7 @@ def justificatif(justif_id: int = None):
@api_web_bp.route("/justificatifs/<int:etudid>/query", defaults={"with_query": True}) @api_web_bp.route("/justificatifs/<int:etudid>/query", defaults={"with_query": True})
@login_required @login_required
@scodoc @scodoc
@as_json
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def justificatifs(etudid: int = None, with_query: bool = False): def justificatifs(etudid: int = None, with_query: bool = False):
""" """
@ -100,13 +102,14 @@ def justificatifs(etudid: int = None, with_query: bool = False):
data = just.to_dict(format_api=True) data = just.to_dict(format_api=True)
data_set.append(data) data_set.append(data)
return jsonify(data_set) return data_set
@bp.route("/justificatif/<int:etudid>/create", methods=["POST"]) @bp.route("/justificatif/<int:etudid>/create", methods=["POST"])
@api_web_bp.route("/justificatif/<int:etudid>/create", methods=["POST"]) @api_web_bp.route("/justificatif/<int:etudid>/create", methods=["POST"])
@scodoc @scodoc
@login_required @login_required
@as_json
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
# @permission_required(Permission.ScoAssiduiteChange) # @permission_required(Permission.ScoAssiduiteChange)
def justif_create(etudid: int = None): def justif_create(etudid: int = None):
@ -145,7 +148,7 @@ def justif_create(etudid: int = None):
else: else:
success[i] = obj success[i] = obj
compute_assiduites_justified(Justificatif.query.filter_by(etudid=etudid), True) compute_assiduites_justified(Justificatif.query.filter_by(etudid=etudid), True)
return jsonify({"errors": errors, "success": success}) return {"errors": errors, "success": success}
def _create_singular( def _create_singular(
@ -221,6 +224,7 @@ def _create_singular(
@api_web_bp.route("/justificatif/<int:justif_id>/edit", methods=["POST"]) @api_web_bp.route("/justificatif/<int:justif_id>/edit", methods=["POST"])
@login_required @login_required
@scodoc @scodoc
@as_json
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
# @permission_required(Permission.ScoAssiduiteChange) # @permission_required(Permission.ScoAssiduiteChange)
def justif_edit(justif_id: int): def justif_edit(justif_id: int):
@ -296,23 +300,22 @@ def justif_edit(justif_id: int):
db.session.add(justificatif_unique) db.session.add(justificatif_unique)
db.session.commit() db.session.commit()
return jsonify( return {
{ "couverture": {
"couverture": { "avant": avant_ids,
"avant": avant_ids, "après": compute_assiduites_justified(
"après": compute_assiduites_justified( Justificatif.query.filter_by(etudid=justificatif_unique.etudid),
Justificatif.query.filter_by(etudid=justificatif_unique.etudid), True,
True, ),
),
}
} }
) }
@bp.route("/justificatif/delete", methods=["POST"]) @bp.route("/justificatif/delete", methods=["POST"])
@api_web_bp.route("/justificatif/delete", methods=["POST"]) @api_web_bp.route("/justificatif/delete", methods=["POST"])
@login_required @login_required
@scodoc @scodoc
@as_json
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
# @permission_required(Permission.ScoAssiduiteChange) # @permission_required(Permission.ScoAssiduiteChange)
def justif_delete(): def justif_delete():
@ -342,7 +345,7 @@ def justif_delete():
output["success"][f"{i}"] = {"OK": True} output["success"][f"{i}"] = {"OK": True}
db.session.commit() db.session.commit()
return jsonify(output) return output
def _delete_singular(justif_id: int, database): def _delete_singular(justif_id: int, database):
@ -371,6 +374,7 @@ def _delete_singular(justif_id: int, database):
@api_web_bp.route("/justificatif/<int:justif_id>/import", methods=["POST"]) @api_web_bp.route("/justificatif/<int:justif_id>/import", methods=["POST"])
@scodoc @scodoc
@login_required @login_required
@as_json
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
# @permission_required(Permission.ScoAssiduiteChange) # @permission_required(Permission.ScoAssiduiteChange)
def justif_import(justif_id: int = None): def justif_import(justif_id: int = None):
@ -407,7 +411,7 @@ def justif_import(justif_id: int = None):
db.session.add(justificatif_unique) db.session.add(justificatif_unique)
db.session.commit() db.session.commit()
return jsonify({"filename": fname}) return {"filename": fname}
except ScoValueError as err: except ScoValueError as err:
return json_error(404, err.args[0]) return json_error(404, err.args[0])
@ -447,6 +451,7 @@ def justif_export(justif_id: int = None, filename: str = None):
@api_web_bp.route("/justificatif/<int:justif_id>/remove", methods=["POST"]) @api_web_bp.route("/justificatif/<int:justif_id>/remove", methods=["POST"])
@scodoc @scodoc
@login_required @login_required
@as_json
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
# @permission_required(Permission.ScoAssiduiteChange) # @permission_required(Permission.ScoAssiduiteChange)
def justif_remove(justif_id: int = None): def justif_remove(justif_id: int = None):
@ -504,13 +509,14 @@ def justif_remove(justif_id: int = None):
except ScoValueError as err: except ScoValueError as err:
return json_error(404, err.args[0]) return json_error(404, err.args[0])
return jsonify({"response": "removed"}) return {"response": "removed"}
@bp.route("/justificatif/<int:justif_id>/list", methods=["GET"]) @bp.route("/justificatif/<int:justif_id>/list", methods=["GET"])
@api_web_bp.route("/justificatif/<int:justif_id>/list", methods=["GET"]) @api_web_bp.route("/justificatif/<int:justif_id>/list", methods=["GET"])
@scodoc @scodoc
@login_required @login_required
@as_json
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
# @permission_required(Permission.ScoAssiduiteChange) # @permission_required(Permission.ScoAssiduiteChange)
def justif_list(justif_id: int = None): def justif_list(justif_id: int = None):
@ -534,7 +540,7 @@ def justif_list(justif_id: int = None):
archive_name, justificatif_unique.etudid archive_name, justificatif_unique.etudid
) )
return jsonify(filenames) return filenames
# Partie justification # Partie justification
@ -542,6 +548,7 @@ def justif_list(justif_id: int = None):
@api_web_bp.route("/justificatif/<int:justif_id>/justifies", methods=["GET"]) @api_web_bp.route("/justificatif/<int:justif_id>/justifies", methods=["GET"])
@scodoc @scodoc
@login_required @login_required
@as_json
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
# @permission_required(Permission.ScoAssiduiteChange) # @permission_required(Permission.ScoAssiduiteChange)
def justif_justifies(justif_id: int = None): def justif_justifies(justif_id: int = None):
@ -557,7 +564,7 @@ def justif_justifies(justif_id: int = None):
assiduites_list: list[int] = scass.justifies(justificatif_unique) assiduites_list: list[int] = scass.justifies(justificatif_unique)
return jsonify(assiduites_list) return assiduites_list
# -- Utils -- # -- Utils --

View File

@ -386,6 +386,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"]:

View File

@ -30,7 +30,7 @@ Formulaire configuration Module Assiduités
""" """
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import SubmitField from wtforms import SubmitField, DecimalField
from wtforms.fields.simple import StringField from wtforms.fields.simple import StringField
from wtforms.widgets import TimeInput from wtforms.widgets import TimeInput
import datetime import datetime
@ -82,5 +82,7 @@ class ConfigAssiduitesForm(FlaskForm):
lunch_time = TimeField("Heure de midi (date pivot entre Matin et Après Midi)") lunch_time = TimeField("Heure de midi (date pivot entre Matin et Après Midi)")
afternoon_time = TimeField("Fin de la journée") afternoon_time = TimeField("Fin de la journée")
tick_time = DecimalField("Granularité de la Time Line (temps en minutes)", places=0)
submit = SubmitField("Valider") submit = SubmitField("Valider")
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True}) cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})

View File

@ -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
@ -678,8 +680,12 @@ class FormSemestre(db.Model):
""" """
from app.scodoc import sco_abs from app.scodoc import sco_abs
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_abs.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]:

View File

@ -116,10 +116,10 @@ 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_abs.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>")

View File

@ -1054,19 +1054,42 @@ def get_abs_count_in_interval(etudid, date_debut_iso, date_fin_iso):
return r return r
def get_assiduites_count_in_interval(etudid, date_debut_iso, date_fin_iso): 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: """Les comptes d'absences de cet étudiant entre ces deux dates, incluses:
tuple (nb abs, nb abs justifiées) tuple (nb abs, nb abs justifiées)
Utilise un cache. Utilise un cache.
""" """
key = str(etudid) + "_" + date_debut_iso + "_" + date_fin_iso + "_assiduites" key = (
str(etudid)
+ "_"
+ date_debut_iso
+ "_"
+ date_fin_iso
+ f"{metrique}_assiduites"
)
r = sco_cache.AbsSemEtudCache.get(key) r = sco_cache.AbsSemEtudCache.get(key)
if not r: if not r:
date_debut: datetime.datetime = scu.is_iso_formated(date_debut_iso, True) date_debut: datetime.datetime = scu.is_iso_formated(date_debut_iso, True)
date_fin: datetime.datetime = scu.is_iso_formated(date_fin_iso, True) date_fin: datetime.datetime = scu.is_iso_formated(date_fin_iso, True)
assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid) assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid)
assiduites = assiduites.filter(Assiduite.etat != 0)
justificatifs: Justificatif = Justificatif.query.filter_by(etudid=etudid) justificatifs: Justificatif = Justificatif.query.filter_by(etudid=etudid)
assiduites = scass.filter_by_date(assiduites, Assiduite, date_debut, date_fin) assiduites = scass.filter_by_date(assiduites, Assiduite, date_debut, date_fin)
@ -1076,7 +1099,7 @@ def get_assiduites_count_in_interval(etudid, date_debut_iso, date_fin_iso):
calculator: scass.CountCalculator = scass.CountCalculator() calculator: scass.CountCalculator = scass.CountCalculator()
calculator.compute_assiduites(assiduites) calculator.compute_assiduites(assiduites)
nb_abs: dict = calculator.to_dict()["demi"] nb_abs: dict = calculator.to_dict()[metrique]
abs_just: list[Assiduite] = scass.get_all_justified( abs_just: list[Assiduite] = scass.get_all_justified(
etudid, date_debut, date_fin etudid, date_debut, date_fin
@ -1084,7 +1107,7 @@ def get_assiduites_count_in_interval(etudid, date_debut_iso, date_fin_iso):
calculator.reset() calculator.reset()
calculator.compute_assiduites(abs_just) calculator.compute_assiduites(abs_just)
nb_abs_just: dict = calculator.to_dict()["demi"] nb_abs_just: dict = calculator.to_dict()[metrique]
r = (nb_abs, nb_abs_just) r = (nb_abs, nb_abs_just)
ans = sco_cache.AbsSemEtudCache.set(key, r) ans = sco_cache.AbsSemEtudCache.set(key, r)
@ -1101,6 +1124,15 @@ def invalidate_abs_count(etudid, sem):
sco_cache.AbsSemEtudCache.delete(key) sco_cache.AbsSemEtudCache.delete(key)
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_abs_count_sem(sem): def invalidate_abs_count_sem(sem):
"""Invalidate (clear) cached abs counts for all the students of this semestre""" """Invalidate (clear) cached abs counts for all the students of this semestre"""
inscriptions = ( inscriptions = (
@ -1112,6 +1144,17 @@ def invalidate_abs_count_sem(sem):
invalidate_abs_count(ins["etudid"], sem) invalidate_abs_count(ins["etudid"], sem)
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_abs_etud_date(etudid, date): # was invalidateAbsEtudDate def invalidate_abs_etud_date(etudid, date): # was invalidateAbsEtudDate
"""Doit etre appelé à chaque modification des absences pour cet étudiant et cette date. """Doit etre appelé à chaque modification des absences pour cet étudiant et cette date.
Invalide cache absence et caches semestre Invalide cache absence et caches semestre
@ -1145,3 +1188,38 @@ def invalidate_abs_etud_date(etudid, date): # was invalidateAbsEtudDate
# Inval cache compteurs absences: # Inval cache compteurs absences:
invalidate_abs_count_sem(sem) invalidate_abs_count_sem(sem)
def invalidate_assiduites_etud_date(etudid, date):
"""Doit etre appelé à chaque modification des assiduites pour cet étudiant et cette date.
Invalide cache absence et caches semestre
date: date au format ISO
"""
from app.scodoc import sco_compute_moy
# Semestres a cette date:
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
sems = [
sem
for sem in etud["sems"]
if sem["date_debut_iso"] <= date and sem["date_fin_iso"] >= date
]
# 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)

View 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):
@ -61,8 +62,15 @@ def abs_notify(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_abs.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

View File

@ -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_abs.get_assiduites_count(etudid, nt.sem)
# --- Decision Jury # --- Decision Jury
infos, dpv = etud_descr_situation_semestre( infos, dpv = etud_descr_situation_semestre(
@ -487,7 +487,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_abs.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:

View File

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

View File

@ -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_abs.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 (

View File

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

View File

@ -823,34 +823,20 @@ def _make_listes_sem(formsemestre: FormSemestre, with_absences=True):
first_monday = sco_abs.ddmmyyyy( first_monday = sco_abs.ddmmyyyy(
formsemestre.date_debut.strftime("%d/%m/%Y") formsemestre.date_debut.strftime("%d/%m/%Y")
).prev_monday() ).prev_monday()
form_abs_tmpl = f""" form_abs_tmpl = """
<td> <td>
<a href="%(url_etat)s">absences</a> <a class="btn" href="%(url_etat)s"><button>Voir l'assiduité</button></a>
</td> </td>
<td> <td>
<form action="{url_for(
"assiduites.signal_assiduites_group", scodoc_dept=g.scodoc_dept
)}" method="get">
<input type="hidden" name="date" 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="{
<a href="{
url_for("assiduites.signal_assiduites_group", scodoc_dept=g.scodoc_dept) url_for("assiduites.signal_assiduites_group", scodoc_dept=g.scodoc_dept)
}?group_ids=%(group_id)s&jour={datetime.date.today().isoformat()}&formsemestre_id={formsemestre.id}">saisie par semaine</a> }?group_ids=%(group_id)s&jour={datetime.date.today().isoformat()}&formsemestre_id={formsemestre.id}"><button>Saisie Journalière</button></a>
</form></td> <a class="btn" href="{
url_for("assiduites.signal_assiduites_diff", scodoc_dept=g.scodoc_dept)
}?group_ids=%(group_id)s&formsemestre_id={formsemestre.formsemestre_id}"><button>Saisie Différée</button></a>
</td>
""" """
else: else:
form_abs_tmpl = "" form_abs_tmpl = ""

View File

@ -696,7 +696,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_abs.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

View File

@ -57,6 +57,7 @@ from app.scodoc import sco_moduleimpl
from app.scodoc import sco_permissions_check from app.scodoc import sco_permissions_check
from app.tables import list_etuds from app.tables import list_etuds
# menu evaluation dans moduleimpl # menu evaluation dans moduleimpl
def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str: def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str:
"Menu avec actions sur une evaluation" "Menu avec actions sur une evaluation"
@ -139,8 +140,11 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str:
"title": "Absences ce jour", "title": "Absences ce jour",
"endpoint": "absences.EtatAbsencesDate", "endpoint": "absences.EtatAbsencesDate",
"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"],
}, },

View File

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

View File

@ -621,7 +621,18 @@ class BasePreferences(object):
}, },
), ),
( (
"etat_defaut", "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", "initvalue": "aucun",
"input_type": "menu", "input_type": "menu",
@ -631,6 +642,30 @@ class BasePreferences(object):
"category": "assi", "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é affichée dans la fiche étudiante et le bilan\n(J. = journée, H. = heure)",
"category": "assi",
"only_global": True,
},
),
# portal # portal
( (
"portal_url", "portal_url",

View File

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

View File

@ -251,6 +251,17 @@ def is_period_overlapping(
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."""

View File

@ -28,6 +28,9 @@
.infos { .infos {
position: relative; position: relative;
width: fit-content; width: fit-content;
display: flex;
justify-content: space-evenly;
align-content: center;
} }
#datestr { #datestr {
@ -36,6 +39,9 @@
border: 1px #444 solid; border: 1px #444 solid;
border-radius: 5px; border-radius: 5px;
padding: 5px; padding: 5px;
min-width: 100px;
display: inline-block;
min-height: 20px;
} }
#tl_slider { #tl_slider {

View File

@ -439,9 +439,9 @@ function hideLoader() {
*/ */
function toTime(time) { function toTime(time) {
let heure = Math.floor(time); let heure = Math.floor(time);
let minutes = (time - heure) * 60; let minutes = Math.round((time - heure) * 60);
if (minutes < 1) { if (minutes < 10) {
minutes = "00"; minutes = `0${minutes}`;
} }
if (heure < 10) { if (heure < 10) {
heure = `0${heure}`; heure = `0${heure}`;
@ -467,7 +467,18 @@ function updateDate() {
const date = dateInput.valueAsDate; const date = dateInput.valueAsDate;
$("#datestr").text(formatDate(date).capitalize()); if (!verifyNonWorkDays(date.getDay(), nonWorkDays)) {
$("#datestr").text(formatDate(date).capitalize());
dateInput.setAttribute("value", date.toISOString().split("T")[0]);
return true;
} else {
const att = document.createTextNode(
"Le jour sélectionné n'est pas un jour travaillé."
);
openAlertModal("Erreur", att, "", "crimson");
dateInput.value = dateInput.getAttribute("value");
return false;
}
} }
function verifyDateInSemester() { function verifyDateInSemester() {
@ -519,6 +530,39 @@ function formatDateModal(str, separator = "·") {
return new moment.tz(str, TIMEZONE).format(`DD/MM/Y ${separator} HH:mm`); return new moment.tz(str, TIMEZONE).format(`DD/MM/Y ${separator} HH:mm`);
} }
/**
* Vérifie si la date sélectionnée n'est pas un jour non travaillé
* Renvoie Vrai si le jour est non travaillé
*/
function verifyNonWorkDays(day, nonWorkdays) {
let d = "";
switch (day) {
case 0:
d = "dim";
break;
case 1:
d = "lun";
break;
case 2:
d = "mar";
break;
case 3:
d = "mer";
break;
case 4:
d = "jeu";
break;
case 5:
d = "ven";
break;
case 6:
d = "sam";
break;
}
return nonWorkdays.indexOf(d) != -1;
}
/** /**
* Fonction qui vérifie si une période est dans un interval * Fonction qui vérifie si une période est dans un interval
* Objet période / interval * Objet période / interval
@ -573,7 +617,9 @@ function isConflictSameAsTimeLine(conflict) {
* @returns {Date} la date sélectionnée * @returns {Date} la date sélectionnée
*/ */
function getDate() { function getDate() {
const date = document.querySelector("#tl_date").valueAsDate; const date = new Date(
document.querySelector("#tl_date").getAttribute("value")
);
date.setHours(0, 0, 0, 0); date.setHours(0, 0, 0, 0);
return date; return date;
} }
@ -997,7 +1043,6 @@ function generateEtudRow(
</div> </div>
<div class="assiduites_bar"> <div class="assiduites_bar">
<div id="prevDateAssi" class="${assiduite.prevAssiduites?.etat?.toLowerCase()}"> <div id="prevDateAssi" class="${assiduite.prevAssiduites?.etat?.toLowerCase()}">
<span class="mini_tick">13h</span>
</div> </div>
</div> </div>
<fieldset class="btns_field single" etudid="${etud.id}" assiduite_id="${ <fieldset class="btns_field single" etudid="${etud.id}" assiduite_id="${
@ -1065,172 +1110,6 @@ function insertEtudRow(etud, index, output = false) {
} }
} }
/**
* Création de la minitiline d'un étudiant
* @param {Array[Assiduité]} assiduitesArray
* @returns {HTMLElement} l'élément correspondant à la mini timeline
*/
function createMiniTimeline(assiduitesArray) {
const array = [...assiduitesArray];
const dateiso = document.getElementById("tl_date").value;
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(8, "hours");
const dayEnd = timelineDate.clone().add(18, "hours");
const dayDuration = moment.duration(dayEnd.diff(dayStart)).asMinutes();
const tlTimes = getTimeLineTimes();
const period_assi = {
date_debut: tlTimes.deb.format(),
date_fin: tlTimes.fin.format(),
etat: "CRENEAU",
};
array.push(period_assi);
array.forEach((assiduité) => {
const startDate = moment(assiduité.date_debut);
const endDate = moment(assiduité.date_fin);
if (startDate.isBefore(dayStart)) {
startDate.startOf("day").add(8, "hours");
}
if (endDate.isAfter(dayEnd)) {
endDate.startOf("day").add(18, "hours");
}
const block = document.createElement("div");
block.className = "mini-timeline-block";
const startOffset = moment.duration(startDate.diff(dayStart)).asMinutes();
const duration = moment.duration(endDate.diff(startDate)).asMinutes();
const leftPercentage = (startOffset / dayDuration) * 100;
const widthPercentage = (duration / dayDuration) * 100;
block.style.left = `${leftPercentage}%`;
block.style.width = `${widthPercentage}%`;
if (assiduité.etat != "CRENEAU") {
if (isSingleEtud()) {
block.addEventListener("click", () => {
let deb = startDate.hours() + startDate.minutes() / 60;
let fin = endDate.hours() + endDate.minutes() / 60;
deb = Math.max(8, deb);
fin = Math.min(18, fin);
setPeriodValues(deb, fin);
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 ${getUserFromId(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");
});
}
/** /**
* Mise à jour d'une ligne étudiant * Mise à jour d'une ligne étudiant
* @param {String | Number} etudid l'identifiant de l'étudiant * @param {String | Number} etudid l'identifiant de l'étudiant
@ -1263,7 +1142,9 @@ function actualizeEtud(etudid) {
*/ */
function generateAllEtudRow() { function generateAllEtudRow() {
if (isSingleEtud()) { if (isSingleEtud()) {
actualizeEtud(etudid); try {
actualizeEtud(etudid);
} catch (ignored) {}
return; return;
} }

View File

@ -1,70 +1,79 @@
/* Module par Seb. L. */ /* Module par Seb. L. */
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(
} else { "href",
styles.setAttribute('href', '/assets/styles/releve-but.css'); // Passerelle removeLastTwoComponents(getCurrentScriptPath()) + "/css/releve-but.css"
} ); // Scodoc
this.shadow.appendChild(styles); } else {
} styles.setAttribute("href", "/assets/styles/releve-but.css"); // Passerelle
listeOnOff() { }
this.parentElement.parentElement.classList.toggle("listeOff"); this.shadow.appendChild(styles);
this.parentElement.parentElement.querySelectorAll(".moduleOnOff").forEach(e => { }
e.classList.remove("moduleOnOff") listeOnOff() {
}) this.parentElement.parentElement.classList.toggle("listeOff");
} this.parentElement.parentElement
moduleOnOff() { .querySelectorAll(".moduleOnOff")
this.parentElement.classList.toggle("moduleOnOff"); .forEach((e) => {
} e.classList.remove("moduleOnOff");
goTo() { });
let module = this.dataset.module; }
this.parentElement.parentElement.parentElement.parentElement.querySelector("#Module_" + module).scrollIntoView(); moduleOnOff() {
} this.parentElement.classList.toggle("moduleOnOff");
}
goTo() {
let module = this.dataset.module;
this.parentElement.parentElement.parentElement.parentElement
.querySelector("#Module_" + module)
.scrollIntoView();
}
set setConfig(config) { set setConfig(config) {
this.config.showURL = config.showURL ?? this.config.showURL; this.config.showURL = config.showURL ?? this.config.showURL;
} }
set showData(data) { set showData(data) {
// this.showInformations(data); // this.showInformations(data);
this.showSemestre(data); this.showSemestre(data);
this.showSynthese(data); this.showSynthese(data);
this.showEvaluations(data); this.showEvaluations(data);
this.showCustom(data); this.showCustom(data);
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");
} }
template() { template() {
return ` return `
<div> <div>
<div class="wait"></div> <div class="wait"></div>
<main class="releve"> <main class="releve">
@ -140,33 +149,36 @@ class releveBUT extends HTMLElement {
</main> </main>
</div>`; </div>`;
} }
/********************************/ /********************************/
/* 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>`;
} else { } else {
output += `<div class=info_etudiant>`; output += `<div class=info_etudiant>`;
} }
output += ` output += `
<div class=civilite> <div class=civilite>
${this.civilite(data.etudiant.civilite)} ${this.civilite(data.etudiant.civilite)}
${data.etudiant.nom} ${data.etudiant.nom}
${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 += `
</div> </div>
<div class=numerosEtudiant> <div class=numerosEtudiant>
Numéro étudiant : ${data.etudiant.code_nip || "~"} - Numéro étudiant : ${data.etudiant.code_nip || "~"} -
@ -174,46 +186,51 @@ class releveBUT extends HTMLElement {
</div> </div>
<div>${data.formation.titre}</div> <div>${data.formation.titre}</div>
`; `;
if (this.config.showURL) { if (this.config.showURL) {
output += `</a>`; output += `</a>`;
} else { } else {
output += `</div>`; output += `</div>`;
} }
this.shadow.querySelector(".infoEtudiant").innerHTML = output; this.shadow.querySelector(".infoEtudiant").innerHTML = output;
} }
/*******************************/ /*******************************/
/* Affichage local */ /* Affichage local */
/*******************************/ /*******************************/
showCustom(data) { showCustom(data) {
this.shadow.querySelector(".custom").innerHTML = data.custom || ""; this.shadow.querySelector(".custom").innerHTML = data.custom || "";
} }
/*******************************/ /*******************************/
/* Information sur le semestre */ /* Information sur le semestre */
/*******************************/ /*******************************/
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> `;
if (!data.options.block_moyenne_generale) { this.shadow.querySelector(".dateInscription").innerHTML += this.ISOToDate(
output += ` data.semestre.inscription
);
let output = "";
if (!data.options.block_moyenne_generale) {
output += `
<div> <div>
<div class=enteteSemestre>Moyenne</div><div class=enteteSemestre>${data.semestre.notes.value}</div> <div class=enteteSemestre>Moyenne</div><div class=enteteSemestre>${data.semestre.notes.value}</div>
<div class=rang>Rang :</div><div class=rang>${data.semestre.rang.value} / ${data.semestre.rang.total}</div> <div class=rang>Rang :</div><div class=rang>${data.semestre.rang.value} / ${data.semestre.rang.total}</div>
@ -222,64 +239,72 @@ class releveBUT extends HTMLElement {
<div>Min. promo. :</div><div>${data.semestre.notes.min}</div> <div>Min. promo. :</div><div>${data.semestre.notes.min}</div>
</div> </div>
`; `;
} }
output += ` output += `
${(() => { ${(() => {
if ((!data.semestre.rang.groupes) || if (
(Object.keys(data.semestre.rang.groupes).length == 0)) { !data.semestre.rang.groupes ||
return ""; Object.keys(data.semestre.rang.groupes).length == 0
} ) {
let output = ""; return "";
let [idGroupe, dataGroupe] = Object.entries(data.semestre.rang.groupes)[0]; }
output += `<div> let output = "";
let [idGroupe, dataGroupe] = Object.entries(
data.semestre.rang.groupes
)[0];
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>
</div>`; </div>`;
// <div>Max. promo. :</div><div>${dataGroupe.max || "-"}</div> // <div>Max. promo. :</div><div>${dataGroupe.max || "-"}</div>
// <div>Moy. promo. :</div><div>${dataGroupe.moy || "-"}</div> // <div>Moy. promo. :</div><div>${dataGroupe.moy || "-"}</div>
// <div>Min. promo. :</div><div>${dataGroupe.min || "-"}</div> // <div>Min. promo. :</div><div>${dataGroupe.min || "-"}</div>
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>
</div>`; </div>`;
if (data.semestre.decision_rcue?.length) { if (data.semestre.decision_rcue?.length) {
output += ` output += `
<div> <div>
<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 += `
<div> <div>
<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 `
<div> <div>
<div class=enteteSemestre>Groupe</div><div class=enteteSemestre>${groupe.nom}</div> <div class=enteteSemestre>Groupe</div><div class=enteteSemestre>${groupe.nom}</div>
@ -291,37 +316,42 @@ class releveBUT extends HTMLElement {
`; `;
}).join("") }).join("")
}*/ }*/
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 =
/*if (data.semestre.decision?.code) { data.semestre.situation || "";
/*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&nbsp;:&nbsp;" + (data.semestre.ECTS?.acquis ?? "-") + "&nbsp;/&nbsp;" + (data.semestre.ECTS?.total ?? "-"); this.shadow.querySelector("#ects_tot").innerHTML =
} "ECTS&nbsp;:&nbsp;" +
(data.semestre.ECTS?.acquis ?? "-") +
"&nbsp;/&nbsp;" +
(data.semestre.ECTS?.total ?? "-");
}
/*******************************/ /*******************************/
/* Synthèse */ /* Synthèse */
/*******************************/ /*******************************/
showSynthese(data) { showSynthese(data) {
let output = ``; let output = ``;
/* 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) {
output += ` // UE Sport / Bonus
output += `
<div> <div>
<div class="ue ueBonus"> <div class="ue ueBonus">
<h3>Bonus</h3> <h3>Bonus</h3>
@ -330,52 +360,60 @@ class releveBUT extends HTMLElement {
${this.ueSport(dataUE.modules)} ${this.ueSport(dataUE.modules)}
</div> </div>
`; `;
} else { } else {
output += ` output += `
<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&nbsp;:&nbsp;${dataUE.moyenne?.value || dataUE.moyenne || "-"}</div> <div class=moyenne>Moyenne&nbsp;:&nbsp;${
<div class=ue_rang>Rang&nbsp;:&nbsp;${dataUE.moyenne?.rang}&nbsp;/&nbsp;${dataUE.moyenne?.total}</div> dataUE.moyenne?.value || dataUE.moyenne || "-"
}</div>
<div class=ue_rang>Rang&nbsp;:&nbsp;${dataUE.moyenne?.rang}&nbsp;/&nbsp;${
dataUE.moyenne?.total
}</div>
<div class=info>`; <div class=info>`;
if (!dataUE.date_capitalisation) { if (!dataUE.date_capitalisation) {
output += ` Bonus&nbsp;:&nbsp;${dataUE.bonus || 0}&nbsp;- output += ` Bonus&nbsp;:&nbsp;${dataUE.bonus || 0}&nbsp;-
Malus&nbsp;:&nbsp;${dataUE.malus || 0}`; Malus&nbsp;:&nbsp;${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>&nbsp;- output += ` <span class=ects>&nbsp;-
ECTS&nbsp;:&nbsp;${dataUE.ECTS?.acquis ?? "-"}&nbsp;/&nbsp;${dataUE.ECTS?.total ?? "-"} ECTS&nbsp;:&nbsp;${dataUE.ECTS?.acquis ?? "-"}&nbsp;/&nbsp;${
dataUE.ECTS?.total ?? "-"
}
</span> </span>
</div> </div>
</div>`; </div>`;
/*<div class=absences> /*<div class=absences>
<div>Abs&nbsp;N.J.</div><div>${dataUE.absences?.injustifie || 0}</div> <div>Abs&nbsp;N.J.</div><div>${dataUE.absences?.injustifie || 0}</div>
<div>Total</div><div>${dataUE.absences?.total || 0}</div> <div>Total</div><div>${dataUE.absences?.total || 0}</div>
</div>*/ </div>*/
output += "</div>"; output += "</div>";
if (!dataUE.date_capitalisation) { if (!dataUE.date_capitalisation) {
output += output +=
this.synthese(data, dataUE.ressources) + this.synthese(data, dataUE.ressources) +
this.synthese(data, dataUE.saes); this.synthese(data, dataUE.saes);
} }
output += "</div>"; output += "</div>";
} }
}); });
this.shadow.querySelector(".synthese").innerHTML = output; this.shadow.querySelector(".synthese").innerHTML = output;
} }
synthese(data, modules) { synthese(data, modules) {
let output = ""; let output = "";
Object.entries(modules).forEach(([module, dataModule]) => { Object.entries(modules).forEach(([module, dataModule]) => {
let titre = data.ressources[module]?.titre || data.saes[module]?.titre; let titre = data.ressources[module]?.titre || data.saes[module]?.titre;
//let url = data.ressources[module]?.url || data.saes[module]?.url; //let url = data.ressources[module]?.url || data.saes[module]?.url;
output += ` output += `
<div class=syntheseModule data-module="${module.replace(/[^a-zA-Z0-9]/g, "")}"> <div class=syntheseModule data-module="${module.replace(/[^a-zA-Z0-9]/g, "")}">
<div>${module}&nbsp;- ${titre}</div> <div>${module}&nbsp;- ${titre}</div>
<div> <div>
@ -384,14 +422,14 @@ class releveBUT extends HTMLElement {
</div> </div>
</div> </div>
`; `;
}) });
return output; return output;
} }
ueSport(modules) { ueSport(modules) {
let output = ""; let output = "";
Object.values(modules).forEach((module) => { Object.values(modules).forEach((module) => {
Object.values(module.evaluations).forEach((evaluation) => { Object.values(module.evaluations).forEach((evaluation) => {
output += ` output += `
<div class=syntheseModule> <div class=syntheseModule>
<div>${module.titre} - ${evaluation.description || "Note"}</div> <div>${module.titre} - ${evaluation.description || "Note"}</div>
<div> <div>
@ -400,27 +438,31 @@ class releveBUT extends HTMLElement {
</div> </div>
</div> </div>
`; `;
}) });
}) });
return output; return output;
} }
/*******************************/ /*******************************/
/* Evaluations */ /* Evaluations */
/*******************************/ /*******************************/
showEvaluations(data) { showEvaluations(data) {
this.shadow.querySelector(".evaluations").innerHTML = this.module(data.ressources); this.shadow.querySelector(".evaluations").innerHTML = this.module(
this.shadow.querySelector(".sae").innerHTML += this.module(data.saes); data.ressources
} );
module(module) { this.shadow.querySelector(".sae").innerHTML += this.module(data.saes);
let output = ""; }
Object.entries(module).forEach(([numero, content]) => { module(module) {
output += ` let output = "";
Object.entries(module).forEach(([numero, content]) => {
output += `
<div id="Module_${numero.replace(/[^a-zA-Z0-9]/g, "")}"> <div id="Module_${numero.replace(/[^a-zA-Z0-9]/g, "")}">
<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&nbsp;indicative&nbsp;:&nbsp;${content.moyenne.value}</div> <div class=moyenne>Moyenne&nbsp;indicative&nbsp;:&nbsp;${
content.moyenne.value
}</div>
<div class=info> <div class=info>
Classe&nbsp;:&nbsp;${content.moyenne.moy}&nbsp;- Classe&nbsp;:&nbsp;${content.moyenne.moy}&nbsp;-
Max&nbsp;:&nbsp;${content.moyenne.max}&nbsp;- Max&nbsp;:&nbsp;${content.moyenne.max}&nbsp;-
@ -435,14 +477,14 @@ class releveBUT extends HTMLElement {
${this.evaluation(content.evaluations)} ${this.evaluation(content.evaluations)}
</div> </div>
`; `;
}) });
return output; return output;
} }
evaluation(evaluations) { evaluation(evaluations) {
let output = ""; let output = "";
evaluations.forEach((evaluation) => { evaluations.forEach((evaluation) => {
output += ` output += `
<div class=eval> <div class=eval>
<div>${this.URL(evaluation.url, evaluation.description || "Évaluation")}</div> <div>${this.URL(evaluation.url, evaluation.description || "Évaluation")}</div>
<div> <div>
@ -454,52 +496,55 @@ 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)
return ` .map(([UE, poids]) => {
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;
} }
/********************/ /********************/
/* Options */ /* Options */
/********************/ /********************/
setOptions(options) { setOptions(options) {
Object.entries(options).forEach(([option, value]) => { Object.entries(options).forEach(([option, value]) => {
if (value === false) { if (value === false) {
this.shadow.children[0].classList.add(option.replace("show", "hide")); this.shadow.children[0].classList.add(option.replace("show", "hide"));
} }
}); });
} }
/********************/
/* Fonctions d'aide */
/********************/
URL(href, content) {
if (this.config.showURL) {
return `<a href=${href}>${content}</a>`;
} else {
return content;
}
}
civilite(txt) {
switch (txt) {
case "M":
return "M.";
case "F":
return "Mme";
default:
return "";
}
}
/********************/ ISOToDate(ISO) {
/* Fonctions d'aide */ return ISO.split("-").reverse().join("/");
/********************/ }
URL(href, content) {
if (this.config.showURL) {
return `<a href=${href}>${content}</a>`;
} else {
return content;
}
}
civilite(txt) {
switch (txt) {
case "M": return "M.";
case "F": return "Mme";
default: return "";
}
}
ISOToDate(ISO) {
return ISO.split("-").reverse().join("/");
}
} }
customElements.define('releve-but', releveBUT); customElements.define("releve-but", releveBUT);

View File

@ -14,6 +14,7 @@
{{ wtf.form_field(form.morning_time) }} {{ wtf.form_field(form.morning_time) }}
{{ wtf.form_field(form.lunch_time) }} {{ wtf.form_field(form.lunch_time) }}
{{ wtf.form_field(form.afternoon_time) }} {{ wtf.form_field(form.afternoon_time) }}
{{ wtf.form_field(form.tick_time) }}
<div class="form-group"> <div class="form-group">
{{ wtf.form_field(form.submit) }} {{ wtf.form_field(form.submit) }}
{{ wtf.form_field(form.cancel) }} {{ wtf.form_field(form.cancel) }}

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

View File

@ -3,6 +3,189 @@
</div> </div>
<script> <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) {
const array = [...assiduitesArray];
const dateiso = document.getElementById("tl_date").value;
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));
const tlTimes = getTimeLineTimes();
const period_assi = {
date_debut: tlTimes.deb.format(),
date_fin: tlTimes.fin.format(),
etat: "CRENEAU",
};
array.push(period_assi);
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") {
if (isSingleEtud()) {
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);
setPeriodValues(deb, fin);
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 ${getUserFromId(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;
console.log(endDate, duration, widthPercentage)
const tick = document.createElement('span');
tick.className = "mini_tick"
tick.textContent = "13h"
tick.style.left = `${widthPercentage}%`
return tick
}
</script> </script>
<style> <style>
@ -87,10 +270,9 @@
.mini_tick { .mini_tick {
position: absolute; position: absolute;
text-align: center; text-align: start;
top: -16px; top: -40px;
left: 50%; transform: translateX(-50%)
} }
.mini_tick::after { .mini_tick::after {

View File

@ -0,0 +1,372 @@
<h2>Signalement différé des assiduités {{gr |safe}}</h2>
<h3>{{sem | safe }}</h3>
<button onclick="getAndVerify()">Valider les assiduités</button>
<div id="studentTable">
<div class="thead">
<div class="tr">
<div class="th sticky">Noms</div>
<button id="addColumn" class="floating-button">+</button>
</div>
</div>
<div class="tbody">
{% for etud in etudiants %}
<div class="tr" etudid="{{etud.etudid}}">
<div class="td sticky">{{etud.nomprenom}}</div>
</div>
{% endfor %}
</div>
</div>
{% include "assiduites/alert.j2" %}
{% include "assiduites/prompt.j2" %}
<style>
button {
margin: 10px 0;
}
.err-assi {
display: flex;
justify-content: flex-start;
align-items: center;
gap: 15px;
}
.table-container {
overflow: auto;
position: relative;
max-width: 100%;
margin: 0 auto;
box-shadow: 0 0 1rem 0 rgba(0, 0, 0, .2);
}
.table {
border-collapse: collapse;
}
.thead .tr {
display: flex;
align-items: center;
}
.thead .tr .th {
height: 125px;
display: flex;
justify-content: center;
align-items: center;
font-size: larger;
}
.th.sticky {
z-index: 5;
}
.th,
.td {
padding: 10px;
text-align: center;
width: 200px;
border: 1px solid #ddd;
display: inline-block;
}
.tr {
display: flex;
justify-content: flex-start;
align-items: center;
width: max-content;
}
.sticky {
position: sticky;
left: 0;
background-color: #fafafa;
border-right: 1px solid #ddd;
}
.mini-form {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.mini-form input,
.mini-form select {
display: block;
margin: 5px;
padding: 5px;
border-radius: 5px;
border: 1px solid #ddd;
}
#addColumn {
font-size: 24px;
width: 50px;
height: 50px;
border-radius: 50%;
right: -60px;
top: calc(50% - 50px /2);
background-color: #007BFF;
color: white;
border: none;
outline: none;
cursor: pointer;
transition: background-color 0.3s;
}
#addColumn:hover {
background-color: #0056b3;
}
.th {
background-color: #007BFF;
color: white;
}
.tbody .tr:nth-child(even) {
background-color: #f2f2f2;
}
.tbody .tr:hover {
background-color: #ddd;
}
.etat {
display: grid;
grid-template-columns: 33% 33% 33%;
grid-template-rows: 50% 50%;
font-size: small;
}
#moduleimpl_select {
max-width: 175px;
}
</style>
<script>
let verified = false;
const etatDef = "{{etat_def}}";
moment.tz.setDefault("Etc/UTC");
function createColumn() {
let table = document.getElementById("studentTable");
let th = document.createElement("div");
th.classList.add("th");
const col_id = `${document.querySelectorAll("[col]").length + 1}`;
th.setAttribute("col", col_id);
th.innerHTML = `
<div class="mini-form">
<input type="datetime-local" id="dateStart">
<input type="datetime-local" id="dateEnd">
{{moduleimpl_select|safe}}
</div>
`;
table
.querySelector(".thead .tr")
.insertBefore(th, document.querySelector("#addColumn"));
const last = [...document.querySelectorAll("#dateStart")].pop();
defaultDate(last);
let rows = table.querySelector(".tbody").querySelectorAll(".tr");
for (let i = 0; i < rows.length; i++) {
let td = document.createElement("div");
td.setAttribute("colid", col_id)
td.classList.add("td", "etat");
const etudid = rows[i].getAttribute("etudid");
td.innerHTML = `
<input type="radio" name="etat_${col_id}_${etudid}" value="present">
<input type="radio" name="etat_${col_id}_${etudid}" value="retard">
<input type="radio" name="etat_${col_id}_${etudid}" value="absent">
<span>Present</span>
<span>Retard</span>
<span>Absent</span>
`;
rows[i].appendChild(td);
if (etatDef != "" && etatDef != "aucun") {
const inp = td.querySelector(`[value='${etatDef}']`).checked = true;
}
}
}
function defaultDate(element) {
const num = element.parentElement.parentElement.getAttribute("col") - 1;
const last = [...document.querySelectorAll(`[col='${num}'] #dateEnd`)].pop();
let date = undefined;
if (last == undefined) {
date = moment().tz("Europe/Paris").format("YYYY-MM-DDTHH:mm");
} else {
date = last.value;
}
element.value = date;
element.addEventListener(
"focusout",
() => {
const el = element.parentElement.querySelector("#dateEnd");
const el2 = element.parentElement.querySelector("#dateStart");
el.value = moment(el2.valueAsDate)
.add(2, "hours")
.format("YYYY-MM-DDTHH:mm");
},
{ once: true }
);
}
function getEtatCol(colId) {
const etats = {};
const tds = [...document.querySelectorAll(`.td[colid='${colId}']`)]
tds.forEach((td) => {
const tr = td.parentElement
const etudid = tr.getAttribute("etudid");
let inputs = [...td.querySelectorAll("input")]
etatInput = inputs.filter((e) => e.checked).pop()
if (etatInput == undefined) {
etats[etudid] = "";
} else {
etats[etudid] = etatInput.value;
}
})
return etats;
}
function _createAssiduites(inputDeb, inputFin, moduleSelect, etudid, etat, colId) {
if (moduleSelect == "") {
return {
"date_debut": inputDeb,
"date_fin": inputFin,
"etudid": etudid,
"etat": etat,
"colid": colId,
}
} else {
return {
"date_debut": inputDeb,
"date_fin": inputFin,
"etudid": etudid,
"moduleimpl_id": moduleSelect,
"etat": etat,
"colid": colId,
}
}
}
function getAndVerify() {
const assiduites = [];
const cols = [...document.querySelectorAll("[col]")];
const errors = [];
cols.forEach((col) => {
const col_id = col.getAttribute("col");
const etats = getEtatCol(col_id);
const inputDeb = col.querySelector("#dateStart").value;
const inputFin = col.querySelector("#dateEnd").value;
const moduleSelect = col.querySelector("#moduleimpl_select").value;
if (inputDeb == "" || inputFin == "") {
errors.push(`La colonne n°${col_id} n'est pas valide`);
return;
}
// TODO Mettre une erreur lorsque moduleimpl forcé (pref)
// TODO Mettre une erreur lorsque assiduité forcé (pref)
Object.keys(etats).forEach((key) => {
const etat = etats[key];
if (etat != "") {
assiduites.push(_createAssiduites(inputDeb, inputFin, moduleSelect, key, etat, col_id))
}
})
});
if (errors.length > 0) {
const texte = document.createElement("div");
errors.map((err) => document.createTextNode(err)).forEach((err) => {
texte.appendChild(err);
texte.appendChild(document.createElement('br'));
})
openAlertModal("Erreur(s) détéctée(s)", texte)
} else {
createAllAssiduites(assiduites);
}
}
function createAllAssiduites(createQueue) {
if (createQueue.length < 0)
return;
const path = getUrl() + `/api/assiduites/create`;
sync_post(
path,
createQueue,
(data, status) => {
verified = true;
const { success, errors } = data;
const indexes = [...Object.keys(errors)];
if (indexes.length > 0) {
const incriminated = indexes.map((i) => {
return createQueue[Number.parseInt(i)];
})
const error_message = document.createElement('div');
for (let i = 0; i < incriminated.length; i++) {
const err = errors[indexes[i]];
const crimi = incriminated[i];
const nom = document.querySelector(`[etudid='${crimi.etudid}']`).firstElementChild.textContent.trim();
const col = crimi.colid;
const div = document.createElement('div');
div.classList.add("err-assi")
const span = document.createElement("span");
span.setAttribute("title", err);
span.textContent = ""
const span2 = document.createElement("span");
span2.textContent = `L'assiduité (Colonne n°${col}) de ${nom} n'a pas pu être enregistrée`;
div.appendChild(span2);
div.appendChild(span);
error_message.appendChild(div);
}
openAlertModal("Certaines assiduités non pas été enregistrées", error_message)
} else {
openAlertModal("Tous les assiduités ont bien été enregistrée", document.createTextNode(""), null, "#09AD2A")
}
},
(data, status) => {
//error
console.error(data, status);
}
);
}
document.getElementById("addColumn").addEventListener("click", () => {
createColumn();
});
const onConfirmRefresh = function (event) {
if (!verified)
return event.returnValue = "Attention, certaines données n'ont pas été enregistrées";
}
window.addEventListener("beforeunload", onConfirmRefresh, { capture: true });
createColumn();
</script>

View File

@ -37,7 +37,7 @@
<div class="infos"> <div class="infos">
Date: <span id="datestr"></span> Date: <span id="datestr"></span>
<input type="date" name="tl_date" id="tl_date" value="{{ date }}" onchange="updateDate()"> <input type="date" name="tl_date" id="tl_date" value="{{ date }}">
</div> </div>
{{timeline|safe}} {{timeline|safe}}
@ -70,11 +70,16 @@
<script> <script>
const etudid = {{ sco.etud.id }}; const etudid = {{ sco.etud.id }};
const nonWorkDays = [{{ nonworkdays| safe }}];
setupDate(() => { setupDate(() => {
actualizeEtud(etudid); if (updateDate()) {
updateSelect() actualizeEtud(etudid);
updateSelect()
}
}); });
setupTimeLine(() => { setupTimeLine(() => {
updateJustifyBtn(); updateJustifyBtn();
}); });

View File

@ -70,6 +70,8 @@
{% include "assiduites/prompt.j2" %} {% include "assiduites/prompt.j2" %}
<script> <script>
const nonWorkDays = [{{ nonworkdays| safe }}];
updateDate(); updateDate();
setupDate(); setupDate();
setupTimeLine(); setupTimeLine();

View File

@ -9,11 +9,16 @@
const timelineContainer = document.querySelector(".timeline-container"); const timelineContainer = document.querySelector(".timeline-container");
const periodTimeLine = document.querySelector(".period"); const periodTimeLine = document.querySelector(".period");
const t_start = {{ t_start }} const t_start = {{ t_start }};
const t_end = {{ t_end }} const t_end = {{ t_end }};
const tick_time = 60 / {{ tick_time }};
const tick_delay = 1 / tick_time;
const period_default = {{ periode_defaut }};
function createTicks() { function createTicks() {
let i = t_start let i = t_start;
while (i <= t_end) { while (i <= t_end) {
const hourTick = document.createElement("div"); const hourTick = document.createElement("div");
@ -28,10 +33,10 @@
timelineContainer.appendChild(tickLabel); timelineContainer.appendChild(tickLabel);
if (i < t_end) { if (i < t_end) {
let j = Math.floor(i + 1) let j = Math.floor(i + 1);
while (i < j) { while (i < j) {
i += 0.25; i += tick_delay;
if (i <= t_end) { if (i <= t_end) {
const quarterTick = document.createElement("div"); const quarterTick = document.createElement("div");
@ -41,34 +46,39 @@
} }
} }
i = j;
} else {
i++;
} }
} }
} }
function numberToTime(num) { function numberToTime(num) {
const integer = Math.floor(num) const integer = Math.floor(num);
const decimal = (num % 1) * 60 const decimal = (num % 1) * 60;
let dec = `:${decimal}` let dec = `:${decimal}`;
if (decimal < 10) { if (decimal < 10) {
dec = `:0${decimal}` dec = `:0${decimal}`;
} }
let int = `${integer}` let int = `${integer}`;
if (integer < 10) { if (integer < 10) {
int = `0${integer}` int = `0${integer}`;
} }
return int + dec return int + dec;
} }
function snapToQuarter(value) { function snapToQuarter(value) {
return Math.round(value * 4) / 4;
return Math.round(value * tick_time) / tick_time;
} }
function setupTimeLine(callback) { function setupTimeLine(callback) {
const func_call = callback ? callback : () => { } const func_call = callback ? callback : () => { };
timelineContainer.addEventListener("mousedown", (event) => { timelineContainer.addEventListener("mousedown", (event) => {
const startX = event.clientX; const startX = event.clientX;
@ -88,9 +98,9 @@
"mouseup", "mouseup",
() => { () => {
generateAllEtudRow(); generateAllEtudRow();
snapHandlesToQuarters() snapHandlesToQuarters();
document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mousemove", onMouseMove);
func_call() func_call();
}, },
{ once: true } { once: true }
); );
@ -117,12 +127,12 @@
document.addEventListener( document.addEventListener(
"mouseup", "mouseup",
() => { () => {
snapHandlesToQuarters() snapHandlesToQuarters();
generateAllEtudRow(); generateAllEtudRow();
document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mousemove", onMouseMove);
func_call() func_call();
}, },
{ once: true } { once: true }
@ -151,41 +161,43 @@
const startHour = (leftPercentage / 100) * (t_end - t_start) + t_start; const startHour = (leftPercentage / 100) * (t_end - t_start) + t_start;
const endHour = ((leftPercentage + widthPercentage) / 100) * (t_end - t_start) + t_start; const endHour = ((leftPercentage + widthPercentage) / 100) * (t_end - t_start) + t_start;
const startValue = Math.round(startHour * 4) / 4; const startValue = snapToQuarter(startHour);
const endValue = Math.round(endHour * 4) / 4; const endValue = snapToQuarter(endHour);
const computedValues = [Math.max(startValue, t_start), Math.min(t_end, endValue)] const computedValues = [Math.max(startValue, t_start), Math.min(t_end, endValue)];
if (computedValues[0] > t_end || computedValues[1] < t_start) { if (computedValues[0] > t_end || computedValues[1] < t_start) {
return [8, 10] return [t_start, min(t_end, t_start + period_default)];
} }
if (computedValues[1] - computedValues[0] <= 0.25 && computedValues[1] < t_end - 0.25) { if (computedValues[1] - computedValues[0] <= tick_delay && computedValues[1] < t_end - tick_delay) {
computedValues[1] += 0.25; computedValues[1] += tick_delay;
} }
return computedValues return computedValues;
} }
function setPeriodValues(deb, fin) { function setPeriodValues(deb, fin) {
let leftPercentage = (deb - t_start) / (t_end - t_start) * 100 deb = snapToQuarter(deb);
let widthPercentage = (fin - deb) / (t_end - t_start) * 100 fin = snapToQuarter(fin);
periodTimeLine.style.left = `${leftPercentage}%` let leftPercentage = (deb - t_start) / (t_end - t_start) * 100;
periodTimeLine.style.width = `${widthPercentage}%` let widthPercentage = (fin - deb) / (t_end - t_start) * 100;
periodTimeLine.style.left = `${leftPercentage}%`;
periodTimeLine.style.width = `${widthPercentage}%`;
snapHandlesToQuarters() snapHandlesToQuarters();
generateAllEtudRow(); generateAllEtudRow();
} }
function snapHandlesToQuarters() { function snapHandlesToQuarters() {
const periodValues = getPeriodValues(); const periodValues = getPeriodValues();
let lef = Math.min(computePercentage(periodValues[0], t_start), computePercentage(t_end, 0.25)) let lef = Math.min(computePercentage(periodValues[0], t_start), computePercentage(t_end, tick_delay));
if (lef < 0) { if (lef < 0) {
lef = 0; lef = 0;
} }
const left = `${lef}%` const left = `${lef}%`;
let wid = Math.max(computePercentage(periodValues[1], periodValues[0]), computePercentage(0.25, 0)) let wid = Math.max(computePercentage(periodValues[1], periodValues[0]), computePercentage(tick_delay, 0));
if (wid > 100) { if (wid > 100) {
wid = 100; wid = 100;
} }
@ -195,11 +207,11 @@
} }
function computePercentage(a, b) { function computePercentage(a, b) {
return ((a - b) / (t_end - t_start)) * 100 return ((a - b) / (t_end - t_start)) * 100;
} }
createTicks(); createTicks();
setPeriodValues(8, 9) setPeriodValues(t_start, t_start + period_default);
</script> </script>
<style> <style>

View File

@ -55,7 +55,7 @@
<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>

View File

@ -72,10 +72,13 @@ 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_abs.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:

View File

@ -10,7 +10,7 @@ from app.decorators import (
scodoc, scodoc,
permission_required, permission_required,
) )
from app.models import FormSemestre, Identite, ScoDocSiteConfig from app.models import FormSemestre, Identite, ScoDocSiteConfig, Assiduite
from app.views import assiduites_bp as bp from app.views import assiduites_bp as bp
from app.views import ScoData from app.views import ScoData
@ -23,6 +23,8 @@ from app.scodoc import sco_groups_view
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc import sco_find_etud from app.scodoc import sco_find_etud
from flask_login import current_user from flask_login import current_user
from app.scodoc import sco_utils as scu
from app.scodoc import sco_assiduites as scass
CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
@ -198,7 +200,7 @@ def signal_assiduites_etud():
return HTMLBuilder( return HTMLBuilder(
header, header,
render_template("assiduites/minitimeline.j2"), _mini_timeline(),
render_template( render_template(
"assiduites/signal_assiduites_etud.j2", "assiduites/signal_assiduites_etud.j2",
sco=ScoData(etud), sco=ScoData(etud),
@ -207,6 +209,7 @@ def signal_assiduites_etud():
lunch=lunch, lunch=lunch,
timeline=_timeline(), timeline=_timeline(),
afternoon=afternoon, afternoon=afternoon,
nonworkdays=_non_work_days(),
forcer_module=sco_preferences.get_preference( forcer_module=sco_preferences.get_preference(
"forcer_module", dept_id=g.scodoc_dept_id "forcer_module", dept_id=g.scodoc_dept_id
), ),
@ -214,6 +217,12 @@ def signal_assiduites_etud():
).build() ).build()
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): def _str_to_num(string: str):
parts = [*map(float, string.split(":"))] parts = [*map(float, string.split(":"))]
hour = parts[0] hour = parts[0]
@ -277,10 +286,6 @@ def signal_assiduites_group():
Returns: Returns:
str: l'html généré str: l'html généré
""" """
formsemestre_id: int = request.args.get("formsemestre_id", -1)
moduleimpl_id: int = request.args.get("moduleimpl_id")
group_ids: list[int] = request.args.get("group_ids", None)
formsemestre_id: int = request.args.get("formsemestre_id", -1) formsemestre_id: int = request.args.get("formsemestre_id", -1)
moduleimpl_id: int = request.args.get("moduleimpl_id") moduleimpl_id: int = request.args.get("moduleimpl_id")
date: str = request.args.get("jour", datetime.date.today().isoformat()) date: str = request.args.get("jour", datetime.date.today().isoformat())
@ -307,7 +312,6 @@ def signal_assiduites_group():
group_ids, moduleimpl_id=moduleimpl_id, formsemestre_id=formsemestre_id group_ids, moduleimpl_id=moduleimpl_id, formsemestre_id=formsemestre_id
) )
# Aucun étudiant WIP
if not groups_infos.members: if not groups_infos.members:
return ( return (
html_sco_header.sco_header(page_title="Saisie journalière des Assiduités") html_sco_header.sco_header(page_title="Saisie journalière des Assiduités")
@ -321,6 +325,7 @@ def signal_assiduites_group():
# --- Filtrage par formsemestre --- # --- Filtrage par formsemestre ---
formsemestre_id = groups_infos.formsemestre_id formsemestre_id = groups_infos.formsemestre_id
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
if formsemestre.dept_id != g.scodoc_dept_id: if formsemestre.dept_id != g.scodoc_dept_id:
abort(404, "groupes inexistants dans ce département") abort(404, "groupes inexistants dans ce département")
@ -385,7 +390,7 @@ def signal_assiduites_group():
return HTMLBuilder( return HTMLBuilder(
header, header,
render_template("assiduites/minitimeline.j2"), _mini_timeline(),
render_template( render_template(
"assiduites/signal_assiduites_group.j2", "assiduites/signal_assiduites_group.j2",
gr_tit=gr_tit, gr_tit=gr_tit,
@ -395,6 +400,7 @@ def signal_assiduites_group():
grp=sco_groups_view.menu_groups_choice(groups_infos), grp=sco_groups_view.menu_groups_choice(groups_infos),
moduleimpl_select=_module_selector(formsemestre, moduleimpl_id), moduleimpl_select=_module_selector(formsemestre, moduleimpl_id),
timeline=_timeline(), timeline=_timeline(),
nonworkdays=_non_work_days(),
formsemestre_date_debut=str(formsemestre.date_debut), formsemestre_date_debut=str(formsemestre.date_debut),
formsemestre_date_fin=str(formsemestre.date_fin), formsemestre_date_fin=str(formsemestre.date_fin),
forcer_module=sco_preferences.get_preference( forcer_module=sco_preferences.get_preference(
@ -407,6 +413,154 @@ def signal_assiduites_group():
).build() ).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("absences.CalAbs", 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/etat_absence_date.j2", etudiants=etudiants, eval=evaluation
),
html_sco_header.sco_footer(),
).build()
@bp.route("/SignalAssiduiteDifferee")
@scodoc
@permission_required(Permission.ScoAbsChange)
def signal_assiduites_diff():
group_ids: list[int] = request.args.get("group_ids", None)
etudid: int = request.args.get("etudid", None)
formsemestre_id: int = request.args.get("formsemestre_id", -1)
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)
if etudid is not None:
etudiants.append(sco_etud.get_etud_info(etudid=int(etudid), filled=True)[0])
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)
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,
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/signal_assiduites_diff.j2",
etudiants=etudiants,
etat_def=sco_preferences.get_preference("assi_etat_defaut"),
moduleimpl_select=_module_selector(formsemestre),
gr=gr_tit,
sem=sem["titre_num"],
),
html_sco_header.sco_footer(),
).build()
def _module_selector( def _module_selector(
formsemestre: FormSemestre, moduleimpl_id: int = None formsemestre: FormSemestre, moduleimpl_id: int = None
) -> HTMLElement: ) -> HTMLElement:
@ -444,9 +598,21 @@ def _module_selector(
) )
def _timeline() -> HTMLElement: def _timeline(formsemestre_id=None) -> HTMLElement:
return render_template( return render_template(
"assiduites/timeline.j2", "assiduites/timeline.j2",
t_start=get_time("assi_morning_time", "08:00:00"), t_start=get_time("assi_morning_time", "08:00:00"),
t_end=get_time("assi_afternoon_time", "18: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/minitimeline.j2",
t_start=get_time("assi_morning_time", "08:00:00"),
t_end=get_time("assi_afternoon_time", "18:00:00"),
) )

View File

@ -203,6 +203,8 @@ def config_assiduites():
flash("Heure de midi enregistrée") flash("Heure de midi enregistrée")
if ScoDocSiteConfig.set("assi_afternoon_time", form.data["afternoon_time"]): if ScoDocSiteConfig.set("assi_afternoon_time", form.data["afternoon_time"]):
flash("Heure de fin de la journée enregistrée") flash("Heure de fin de la journée enregistrée")
if ScoDocSiteConfig.set("assi_tick_time", form.data["tick_time"]):
flash("Granularité de la timeline enregistrée")
return redirect(url_for("scodoc.configuration")) return redirect(url_for("scodoc.configuration"))
elif request.method == "GET": elif request.method == "GET":
@ -215,6 +217,7 @@ def config_assiduites():
form.afternoon_time.data = ScoDocSiteConfig.get( form.afternoon_time.data = ScoDocSiteConfig.get(
"assi_afternoon_time", datetime.time(18, 0, 0) "assi_afternoon_time", datetime.time(18, 0, 0)
) )
form.tick_time.data = float(ScoDocSiteConfig.get("assi_tick_time", 15))
return render_template( return render_template(
"assiduites/config_assiduites.j2", "assiduites/config_assiduites.j2",
form=form, form=form,

View File

@ -1,7 +1,7 @@
"""modèles assiduites justificatifs """modèles assiduites justificatifs
Revision ID: dbcf2175e87f Revision ID: dbcf2175e87f
Revises: 5c7b208355df Revises: d84bc592584e
Create Date: 2023-02-01 14:21:06.989190 Create Date: 2023-02-01 14:21:06.989190
""" """
@ -11,7 +11,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = "dbcf2175e87f" revision = "dbcf2175e87f"
down_revision = "6520faf67508" down_revision = "d84bc592584e"
branch_labels = None branch_labels = None
depends_on = None depends_on = None

View File

@ -395,7 +395,7 @@ def verifier_filtrage_justificatifs(etud: Identite, justificatifs: list[Justific
# Justifications des assiduites # Justifications des assiduites
assert len(scass.justifies(justificatifs[2])) == 1, "Justifications mauvais" assert len(scass.justifies(justificatifs[2])) == 2, "Justifications mauvais"
assert len(scass.justifies(justificatifs[0])) == 0, "Justifications mauvais" assert len(scass.justifies(justificatifs[0])) == 0, "Justifications mauvais"