diff --git a/app/api/justificatifs.py b/app/api/justificatifs.py index d1b7862d3..52ddee04e 100644 --- a/app/api/justificatifs.py +++ b/app/api/justificatifs.py @@ -18,7 +18,7 @@ from app.api import api_bp as bp from app.api import api_web_bp from app.api import get_model_api_object from app.decorators import permission_required, scodoc -from app.models import Identite, Justificatif +from app.models import Identite, Justificatif, Departement from app.models.assiduites import compute_assiduites_justified from app.scodoc.sco_archives_justificatifs import JustificatifArchiver from app.scodoc.sco_exceptions import ScoValueError @@ -105,6 +105,31 @@ def justificatifs(etudid: int = None, with_query: bool = False): return data_set +@api_web_bp.route("/justificatifs/dept/", defaults={"with_query": False}) +@api_web_bp.route( + "/justificatifs/dept//query", defaults={"with_query": True} +) +@login_required +@scodoc +@as_json +@permission_required(Permission.ScoView) +def justificatifs_dept(dept_id: int = None, with_query : bool = False): + """ """ + dept = Departement.query.get_or_404(dept_id) + etuds = [etud.id for etud in dept.etudiants] + + justificatifs_query = Justificatif.query.filter(Justificatif.etudid.in_(etuds)) + + if with_query: + justificatifs_query = _filter_manager(request, justificatifs_query) + data_set: list[dict] = [] + for just in justificatifs_query.all(): + data = just.to_dict(format_api=True) + data_set.append(data) + + return data_set + + @bp.route("/justificatif//create", methods=["POST"]) @api_web_bp.route("/justificatif//create", methods=["POST"]) @scodoc diff --git a/app/scodoc/html_sidebar.py b/app/scodoc/html_sidebar.py index cb93b15e1..ad720791a 100755 --- a/app/scodoc/html_sidebar.py +++ b/app/scodoc/html_sidebar.py @@ -54,7 +54,7 @@ def sidebar_common():

Scolarité

Semestres
Programmes
- Absences
+ Assiduités
""" ] if current_user.has_permission( @@ -138,6 +138,7 @@ def sidebar(etudid: int = None): f"""
  • Calendrier
  • Liste
  • +
  • Bilan
  • """ ) diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 05167c664..ea5d29231 100755 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -213,13 +213,14 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str: "enabled": True, "helpmsg": "", }, - { - "title": "Vérifier absences aux évaluations", - "endpoint": "notes.formsemestre_check_absences_html", - "args": {"formsemestre_id": formsemestre_id}, - "enabled": True, - "helpmsg": "", - }, + # TODO: Mettre à jour avec module Assiduités + # { + # "title": "Vérifier absences aux évaluations", + # "endpoint": "notes.formsemestre_check_absences_html", + # "args": {"formsemestre_id": formsemestre_id}, + # "enabled": True, + # "helpmsg": "", + # }, { "title": "Lister tous les enseignants", "endpoint": "notes.formsemestre_enseignants_list", diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py index fefeacbef..56cbd4ab7 100644 --- a/app/scodoc/sco_moduleimpl_status.py +++ b/app/scodoc/sco_moduleimpl_status.py @@ -138,7 +138,7 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str: }, { "title": "Absences ce jour", - "endpoint": "absences.EtatAbsencesDate", + "endpoint": "assiduites.get_etat_abs_date", "args": { "group_ids": group_id, "desc": E["description"], diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py index b7099dabb..b6a064ed7 100644 --- a/app/scodoc/sco_preferences.py +++ b/app/scodoc/sco_preferences.py @@ -661,7 +661,19 @@ class BasePreferences(object): "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)", + "explanation": "Unité utilisée dans la fiche étudiante, le bilan, et dans les calculs (J. = journée, H. = heure)", + "category": "assi", + "only_global": True, + }, + ), + ( + "assi_seuil", + { + "initvalue": 3.0, + "size": 10, + "title": "Seuil d'alerte des absences", + "type": "float", + "explanation": "Nombres d'absences limite avant alerte dans le bilan (utilisation de l'unité métrique ↑ )", "category": "assi", "only_global": True, }, diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 73e056995..c163806ee 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -619,6 +619,13 @@ def AbsencesURL(): ] +def AssiduitesURL(): + """URL of Assiduités""" + return url_for("assiduites.index_html", scodoc_dept=g.scodoc_dept)[ + : -len("/index_html") + ] + + def UsersURL(): """URL of Users e.g. https://scodoc.xxx.fr/ScoDoc/DEPT/Scolarite/Users diff --git a/app/templates/assiduites/pages/bilan_dept.j2 b/app/templates/assiduites/pages/bilan_dept.j2 new file mode 100644 index 000000000..f6afdf610 --- /dev/null +++ b/app/templates/assiduites/pages/bilan_dept.j2 @@ -0,0 +1,160 @@ +{% include "assiduites/widgets/tableau_base.j2" %} + + +
    + +

    Justificatifs en attente (ou modifiés)

    + {% include "assiduites/widgets/tableau_justi.j2" %} +
    + +
    + Année scolaire 2022-2023 Changer année: + +
    + +
    + +
    + + \ No newline at end of file diff --git a/app/templates/assiduites/pages/bilan_etud.j2 b/app/templates/assiduites/pages/bilan_etud.j2 new file mode 100644 index 000000000..a282c3c5f --- /dev/null +++ b/app/templates/assiduites/pages/bilan_etud.j2 @@ -0,0 +1,342 @@ +{% block app_content %} +{% include "assiduites/widgets/tableau_base.j2" %} +
    + +

    Bilan de l'assiduité de {{sco.etud.nomprenom}}

    + + + +
    + +

    Statistiques d'assiduité

    +
    + + + +
    + +
    + +
    +
    + +
    + +

    Assiduités non justifiées (Uniquement les retards et les absences)

    + {% include "assiduites/widgets/tableau_assi.j2" %} + +

    Justificatifs en attente (ou modifiés)

    + {% include "assiduites/widgets/tableau_justi.j2" %} + +
    + +
    +

    Boutons de suppresions (toute suppression est définitive)

    + + +
    + +
    + +
    + +
    +{% endblock app_content %} + + + \ No newline at end of file diff --git a/app/templates/assiduites/widgets/tableau_assi.j2 b/app/templates/assiduites/widgets/tableau_assi.j2 index 52114f24d..6d1ea2d33 100644 --- a/app/templates/assiduites/widgets/tableau_assi.j2 +++ b/app/templates/assiduites/widgets/tableau_assi.j2 @@ -59,6 +59,8 @@ assi = filterArray(assi, filterAssiduites.filters) renderTableAssiduites(currentPageAssiduites, assi); renderPaginationButtons(assi); + + try { stats() } catch (_) { } } const moduleimpls = {} @@ -109,6 +111,7 @@ row.appendChild(td) }) + row.addEventListener("contextmenu", openContext); tableBodyAssiduites.appendChild(row); diff --git a/app/templates/assiduites/widgets/tableau_base.j2 b/app/templates/assiduites/widgets/tableau_base.j2 index 5121cb9ac..242d2c044 100644 --- a/app/templates/assiduites/widgets/tableau_base.j2 +++ b/app/templates/assiduites/widgets/tableau_base.j2 @@ -132,6 +132,11 @@ function renderPaginationButtons(array, assi = true) { const totalPages = Math.ceil(array.length / itemsPerPage); if (totalPages <= 1) { + if (assi) { + paginationContainerAssiduites.innerHTML = "" + } else { + paginationContainerJustificatifs.innerHTML = "" + } return; } @@ -139,14 +144,15 @@ paginationContainerAssiduites.innerHTML = "" paginationContainerAssiduites.querySelector('#paginationAssi')?.addEventListener('change', (e) => { currentPageAssiduites = e.target.value; - renderTableAssiduites(currentPageAssiduites, array); + assiduiteCallBack(array); }) paginationContainerAssiduites.querySelector('.pagination_moins').addEventListener('click', () => { if (currentPageAssiduites > 1) { currentPageAssiduites--; paginationContainerAssiduites.querySelector('#paginationAssi').value = currentPageAssiduites - renderTableAssiduites(currentPageAssiduites, array); + assiduiteCallBack(array); + } }) @@ -154,21 +160,21 @@ if (currentPageAssiduites < totalPages) { currentPageAssiduites++; paginationContainerAssiduites.querySelector('#paginationAssi').value = currentPageAssiduites - renderTableAssiduites(currentPageAssiduites, array); + assiduiteCallBack(array); } }) } else { paginationContainerJustificatifs.innerHTML = "" paginationContainerJustificatifs.querySelector('#paginationJusti')?.addEventListener('change', (e) => { currentPageJustificatifs = e.target.value; - renderTableJustificatifs(currentPageJustificatifs, array); + justificatifCallBack(array); }) paginationContainerJustificatifs.querySelector('.pagination_moins').addEventListener('click', () => { if (currentPageJustificatifs > 1) { currentPageJustificatifs--; paginationContainerJustificatifs.querySelector('#paginationJusti').value = currentPageAssiduites - renderTableJustificatifs(currentPageJustificatifs, array); + justificatifCallBack(array); } }) @@ -176,7 +182,7 @@ if (currentPageJustificatifs < totalPages) { currentPageJustificatifs++; paginationContainerJustificatifs.querySelector('#paginationJusti').value = currentPageAssiduites - renderTableJustificatifs(currentPageJustificatifs, array); + justificatifCallBack(array); } }) } @@ -624,6 +630,8 @@ return "Raison"; case "fichier": return "Fichier"; + case "etudid": + return "Etudiant"; } } @@ -776,7 +784,7 @@ margin-left: 2px !important; } - label { + .filter-body label { display: flex; justify-content: center; align-items: center; diff --git a/app/templates/assiduites/widgets/tableau_justi.j2 b/app/templates/assiduites/widgets/tableau_justi.j2 index c1808640e..a10041eb8 100644 --- a/app/templates/assiduites/widgets/tableau_justi.j2 +++ b/app/templates/assiduites/widgets/tableau_justi.j2 @@ -57,6 +57,17 @@ renderPaginationButtons(justi, false); } + + function getEtudiant(id) { + if (id in etuds) { + return etuds[id]; + } + getSingleEtud(id); + + return etuds[id]; + + } + function renderTableJustificatifs(page, justificatifs) { generateTableHead(filterJustificatifs.columns, false) @@ -85,9 +96,13 @@ td.textContent = moment.tz(justificatif[k], TIMEZONE).format(`DD/MM/Y HH:mm`) } else if (k.indexOf('fichier') != -1) { td.textContent = justificatif.fichier ? "Oui" : "Non"; + } else if (k.indexOf('etudid') != -1) { + const e = getEtudiant(justificatif.etudid); + + td.textContent = `${e.prenom.capitalize()} ${e.nom.toUpperCase()}`; } else { - td.textContent = justificatif[k].capitalize() + td.textContent = `${justificatif[k]}`.capitalize() } row.appendChild(td) diff --git a/app/templates/sidebar.j2 b/app/templates/sidebar.j2 index 6a607db79..8b8d559f4 100755 --- a/app/templates/sidebar.j2 +++ b/app/templates/sidebar.j2 @@ -24,7 +24,7 @@

    Scolarité

    Semestres
    Programmes
    - Absences
    + Assiduités
    {% if current_user.has_permission(sco.Permission.ScoUsersAdmin) or current_user.has_permission(sco.Permission.ScoUsersView) @@ -73,6 +73,8 @@ etudid=sco.etud.id) }}">Calendrier
  • Liste
  • +
  • Bilan
  • {% endif %} {# /etud-insidebar #} diff --git a/app/views/assiduites.py b/app/views/assiduites.py index 464b4cc53..8c2fed0c5 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -10,7 +10,7 @@ from app.decorators import ( scodoc, permission_required, ) -from app.models import FormSemestre, Identite, ScoDocSiteConfig, Assiduite +from app.models import FormSemestre, Identite, ScoDocSiteConfig, Assiduite, Departement from app.views import assiduites_bp as bp from app.views import ScoData @@ -125,36 +125,49 @@ class HTMLBuilder: @permission_required(Permission.ScoView) def index_html(): """Gestionnaire assiduités, page principale""" - H = [ html_sco_header.sco_header( page_title="Saisie des assiduités", - cssstyles=["css/calabs.css"], - javascripts=["js/calabs.js"], + javascripts=[ + "js/assiduites.js", + "libjs/moment.new.min.js", + "libjs/moment-timezone.js", + ], + cssstyles=[ + "css/assiduites.css", + ], ), """

    Traitement des assiduités

    Pour saisir des assiduités ou consulter les états, il est recommandé par passer par - le semestre concerné (saisie par jours nommés ou par semaines). + le semestre concerné (saisie par jour ou saisie différée).

    """, ] H.append( """

    Pour signaler, annuler ou justifier une assiduité pour un seul étudiant, - choisissez d'abord concerné:

    """ + choisissez d'abord le concerné:

    """ ) H.append(sco_find_etud.form_search_etud()) - if current_user.has_permission( - Permission.ScoAbsChange - ) and sco_preferences.get_preference("handle_billets_abs"): - H.append( - f""" -

    Billets d'absence

    - - """ - ) + # if current_user.has_permission( + # Permission.ScoAbsChange + # ) and sco_preferences.get_preference("handle_billets_abs"): + # H.append( + # f""" + #

    Billets d'absence

    + # + # """ + # ) + + H.append( + render_template( + "assiduites/pages/bilan_dept.j2", + dept_id=g.scodoc_dept_id, + annee=scu.annee_scolaire(), + ), + ) H.append(html_sco_header.sco_footer()) return "\n".join(H) @@ -269,6 +282,60 @@ def liste_assiduites_etud(): ).build() +@bp.route("/BilanEtud") +@scodoc +@permission_required(Permission.ScoAbsChange) +def bilan_etud(): + """ + bilan_etud Affichage de toutes les assiduites et justificatifs d'un etudiant + Args: + etudid (int): l'identifiant de l'étudiant + + Returns: + str: l'html généré + """ + + etudid = request.args.get("etudid", -1) + etud: Identite = Identite.query.get_or_404(etudid) + if etud.dept_id != g.scodoc_dept_id: + abort(404, "étudiant inexistant dans ce département") + + header: str = html_sco_header.sco_header( + page_title="Bilan de l'assiduité étudiante", + init_qtip=True, + javascripts=[ + "js/assiduites.js", + "libjs/moment.new.min.js", + "libjs/moment-timezone.js", + ], + cssstyles=CSSSTYLES + + [ + "css/assiduites.css", + ], + ) + + date_debut: str = f"{scu.annee_scolaire()}-09-01" + date_fin: str = f"{scu.annee_scolaire()+1}-06-30" + + assi_metric = { + "H.": "heure", + "J.": "journee", + "1/2 J.": "demi", + }.get(sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id)) + + return HTMLBuilder( + header, + render_template( + "assiduites/pages/bilan_etud.j2", + sco=ScoData(etud), + date_debut=date_debut, + date_fin=date_fin, + assi_metric=assi_metric, + assi_seuil=_get_seuil(), + ), + ).build() + + @bp.route("/AjoutJustificatifEtud") @scodoc @permission_required(Permission.ScoAbsChange) @@ -549,7 +616,7 @@ def get_etat_abs_date(): etat = scu.EtatAssiduite.inverse().get(assi.etat).name etudiant = { - "nom": f'{etud["nomprenom"]}', + "nom": f'{etud["nomprenom"]}', "etat": etat, } @@ -777,3 +844,7 @@ def _str_to_num(string: str): def get_time(label: str, default: str): return _str_to_num(ScoDocSiteConfig.get(label, default)) + + +def _get_seuil(): + return sco_preferences.get_preference("assi_seuil", dept_id=g.scodoc_dept_id) diff --git a/scodoc.py b/scodoc.py index ee1a8edf8..21a66cbcb 100755 --- a/scodoc.py +++ b/scodoc.py @@ -655,22 +655,16 @@ def profile(host, port, length, profile_dir): "-m", "--morning", help="Spécifie l'heure de début des cours format `hh:mm`", - default="Heure configurée dans la configuration générale / 08:00 sinon", - show_default=True, ) @click.option( "-n", "--noon", help="Spécifie l'heure de fin du matin (et donc début de l'après-midi) format `hh:mm`", - default="Heure configurée dans la configuration générale / 13:00 sinon", - show_default=True, ) @click.option( "-e", "--evening", help="Spécifie l'heure de fin des cours format `hh:mm`", - default="Heure configurée dans la configuration générale / 18:00 sinon", - show_default=True, ) @with_appcontext def migrate_abs_to_assiduites( diff --git a/tools/migrate_abs_to_assiduites.py b/tools/migrate_abs_to_assiduites.py index b64586098..3860e18aa 100644 --- a/tools/migrate_abs_to_assiduites.py +++ b/tools/migrate_abs_to_assiduites.py @@ -228,22 +228,22 @@ def migrate_abs_to_assiduites( _glob.DEBUG = debug if morning is None: - _glob.MORNING = ScoDocSiteConfig.get("assi_morning_time", time(8, 0)) - else: - morning: list[str] = morning.split(":") - _glob.MORNING = time(int(morning[0]), int(morning[1])) + morning = ScoDocSiteConfig.get("assi_morning_time", time(8, 0)) + + morning: list[str] = morning.split(":") + _glob.MORNING = time(int(morning[0]), int(morning[1])) if noon is None: - _glob.NOON = ScoDocSiteConfig.get("assi_lunch_time", time(13, 0)) - else: - noon: list[str] = noon.split(":") - _glob.NOON = time(int(noon[0]), int(noon[1])) + noon = ScoDocSiteConfig.get("assi_lunch_time", time(13, 0)) + + noon: list[str] = noon.split(":") + _glob.NOON = time(int(noon[0]), int(noon[1])) if evening is None: - _glob.EVENING = ScoDocSiteConfig.get("assi_afternoon_time", time(18, 0)) - else: - evening: list[str] = evening.split(":") - _glob.EVENING = time(int(evening[0]), int(evening[1])) + evening = ScoDocSiteConfig.get("assi_afternoon_time", time(18, 0)) + + evening: list[str] = evening.split(":") + _glob.EVENING = time(int(evening[0]), int(evening[1])) if dept is None: prof_total = Profiler("MigrationTotal")