diff --git a/app/forms/main/config_assiduites.py b/app/forms/main/config_assiduites.py new file mode 100644 index 00000000..26126f2e --- /dev/null +++ b/app/forms/main/config_assiduites.py @@ -0,0 +1,86 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# ScoDoc +# +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Emmanuel Viennet emmanuel.viennet@viennet.net +# +############################################################################## + +""" +Formulaire configuration Module Assiduités +""" + +from flask_wtf import FlaskForm +from wtforms import SubmitField +from wtforms.fields.simple import StringField +from wtforms.widgets import TimeInput +import datetime + + +class TimeField(StringField): + """HTML5 time input.""" + + widget = TimeInput() + + def __init__(self, label=None, validators=None, fmt="%H:%M:%S", **kwargs): + super(TimeField, self).__init__(label, validators, **kwargs) + self.fmt = fmt + self.data = None + + def _value(self): + if self.raw_data: + return " ".join(self.raw_data) + if self.data and isinstance(self.data, str): + self.data = datetime.time(*map(int, self.data.split(":"))) + return self.data and self.data.strftime(self.fmt) or "" + + def process_formdata(self, valuelist): + if valuelist: + time_str = " ".join(valuelist) + try: + components = time_str.split(":") + hour = 0 + minutes = 0 + seconds = 0 + if len(components) in range(2, 4): + hour = int(components[0]) + minutes = int(components[1]) + + if len(components) == 3: + seconds = int(components[2]) + else: + raise ValueError + self.data = datetime.time(hour, minutes, seconds) + except ValueError: + self.data = None + raise ValueError(self.gettext("Not a valid time string")) + + +class ConfigAssiduitesForm(FlaskForm): + "Formulaire paramétrage Module Assiduités" + + morning_time = TimeField("Début de la journée") + lunch_time = TimeField("Heure de midi (date pivot entre Matin et Après Midi)") + afternoon_time = TimeField("Fin de la journée") + + submit = SubmitField("Valider") + cancel = SubmitField("Annuler", render_kw={"formnovalidate": True}) diff --git a/app/models/config.py b/app/models/config.py index 71b462bc..b989781c 100644 --- a/app/models/config.py +++ b/app/models/config.py @@ -8,6 +8,8 @@ from app import current_app, db, log from app.comp import bonus_spo from app.scodoc import sco_utils as scu +from datetime import time + from app.scodoc.codes_cursus import ( ABAN, ABL, @@ -94,6 +96,10 @@ class ScoDocSiteConfig(db.Model): "cas_logout_route": str, "cas_validate_route": str, "cas_attribute_id": str, + # Assiduités + "morning_time": str, + "lunch_time": str, + "afternoon_time": str, } def __init__(self, name, value): diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py index b77ee34b..4131e7f7 100644 --- a/app/scodoc/sco_preferences.py +++ b/app/scodoc/sco_preferences.py @@ -206,6 +206,7 @@ PREF_CATEGORIES = ( ("misc", {"title": "Divers"}), ("apc", {"title": "BUT et Approches par Compétences"}), ("abs", {"title": "Suivi des absences", "related": ("bul",)}), + ("assi", {"title": "Gestion de l'assiduité"}), ("portal", {"title": "Liaison avec portail (Apogée, etc)"}), ( "pdf", @@ -588,6 +589,38 @@ class BasePreferences(object): "category": "abs", }, ), + # Assiduités + ( + "forcer_module", + { + "initvalue": 0, + "title": "Forcer la déclaration du module.", + "input_type": "boolcheckbox", + "labels": ["non", "oui"], + "category": "assi", + }, + ), + ( + "forcer_present", + { + "initvalue": 0, + "title": "Forcer l'appel des présents", + "input_type": "boolcheckbox", + "labels": ["non", "oui"], + "category": "assi", + }, + ), + ( + "etat_defaut", + { + "initvalue": "aucun", + "input_type": "menu", + "labels": ["aucun", "present", "retard", "absent"], + "allowed_values": ["aucun", "present", "retard", "absent"], + "title": "Définir l'état par défaut", + "category": "assi", + }, + ), # portal ( "portal_url", @@ -1678,7 +1711,7 @@ class BasePreferences(object): ( "feuille_releve_abs_taille", { - "initvalue": "A3", + "initvalue": "A4", "input_type": "menu", "labels": ["A3", "A4"], "allowed_values": ["A3", "A4"], diff --git a/app/static/js/assiduites.js b/app/static/js/assiduites.js index 94efe5f9..bf3d821b 100644 --- a/app/static/js/assiduites.js +++ b/app/static/js/assiduites.js @@ -88,6 +88,20 @@ function validateSelectors() { ); }); + if (getModuleImplId() == null && forceModule) { + const HTML = ` +

Attention, le module doit obligatoirement être renseigné.

+

Cela vient de la configuration du semestre ou plus largement du département.

+

Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.

+ `; + + const content = document.createElement("div"); + content.innerHTML = HTML; + + openAlertModal("Sélection du module", content); + return; + } + getAssiduitesFromEtuds(true); document.querySelector(".selectors").disabled = true; @@ -1133,14 +1147,16 @@ function createMiniTimeline(assiduitesArray) { } }; - getJustificatifFromPeriod( - { - deb: new moment.tz(assiduité.date_debut, TIMEZONE), - fin: new moment.tz(assiduité.date_fin, TIMEZONE), - }, - assiduité.etudid, - action - ); + 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": @@ -1905,7 +1921,9 @@ function fastJustify(assiduite) { ); } }; - getJustificatifFromPeriod(period, assiduite.etudid, action); + if (assiduite.etudid) { + getJustificatifFromPeriod(period, assiduite.etudid, action); + } } function justifyAssiduite(assiduite_id, justified) { diff --git a/app/templates/assiduites/config_assiduites.j2 b/app/templates/assiduites/config_assiduites.j2 new file mode 100644 index 00000000..92d9dd24 --- /dev/null +++ b/app/templates/assiduites/config_assiduites.j2 @@ -0,0 +1,28 @@ +{% extends "base.j2" %} +{% import 'bootstrap/wtf.html' as wtf %} + +{% block app_content %} +

Configuration du Module d'assiduité

+ +
+
+ +
+ {{ form.hidden_tag() }} + {{ wtf.form_errors(form, hiddens="only") }} + + {{ wtf.form_field(form.morning_time) }} + {{ wtf.form_field(form.lunch_time) }} + {{ wtf.form_field(form.afternoon_time) }} +
+ {{ wtf.form_field(form.submit) }} + {{ wtf.form_field(form.cancel) }} +
+
+
+
+ + + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/assiduites/signal_assiduites_etud.j2 b/app/templates/assiduites/signal_assiduites_etud.j2 index bd5de04e..d44ccacc 100644 --- a/app/templates/assiduites/signal_assiduites_etud.j2 +++ b/app/templates/assiduites/signal_assiduites_etud.j2 @@ -49,9 +49,9 @@
- - - + + +
@@ -94,6 +94,9 @@ } + let forceModule = "{{ forcer_module }}" + forceModule = forceModule == "True" ? true : false + diff --git a/app/templates/assiduites/signal_assiduites_group.j2 b/app/templates/assiduites/signal_assiduites_group.j2 index 27f13874..0446606c 100644 --- a/app/templates/assiduites/signal_assiduites_group.j2 +++ b/app/templates/assiduites/signal_assiduites_group.j2 @@ -73,5 +73,8 @@ updateDate(); setupDate(); setupTimeLine(); + + let forceModule = "{{ forcer_module }}" + forceModule = forceModule == "True" ? true : false \ No newline at end of file diff --git a/app/templates/assiduites/timeline.j2 b/app/templates/assiduites/timeline.j2 index f1977df4..b67fd0b6 100644 --- a/app/templates/assiduites/timeline.j2 +++ b/app/templates/assiduites/timeline.j2 @@ -9,31 +9,60 @@ const timelineContainer = document.querySelector(".timeline-container"); const periodTimeLine = document.querySelector(".period"); + const t_start = {{ t_start }} + const t_end = {{ t_end }} function createTicks() { - for (let i = 8; i <= 18; i++) { + let i = t_start + + while (i <= t_end) { const hourTick = document.createElement("div"); hourTick.classList.add("tick", "hour"); - hourTick.style.left = `${((i - 8) / 10) * 100}%`; + hourTick.style.left = `${((i - t_start) / (t_end - t_start)) * 100}%`; timelineContainer.appendChild(hourTick); const tickLabel = document.createElement("div"); tickLabel.classList.add("tick-label"); - tickLabel.style.left = `${((i - 8) / 10) * 100}%`; - tickLabel.textContent = i < 10 ? `0${i}:00` : `${i}:00`; + tickLabel.style.left = `${((i - t_start) / (t_end - t_start)) * 100}%`; + tickLabel.textContent = numberToTime(i); timelineContainer.appendChild(tickLabel); - if (i < 18) { - for (let j = 1; j < 4; j++) { - const quarterTick = document.createElement("div"); - quarterTick.classList.add("tick", "quarter"); - quarterTick.style.left = `${((i - 8 + j / 4) / 10) * 100}%`; - timelineContainer.appendChild(quarterTick); + if (i < t_end) { + let j = Math.floor(i + 1) + + while (i < j) { + i += 0.25; + + if (i <= t_end) { + const quarterTick = document.createElement("div"); + quarterTick.classList.add("tick", "quarter"); + quarterTick.style.left = `${computePercentage(i, t_start)}%`; + timelineContainer.appendChild(quarterTick); + } + } } } } + function numberToTime(num) { + const integer = Math.floor(num) + const decimal = (num % 1) * 60 + + let dec = `:${decimal}` + if (decimal < 10) { + dec = `:0${decimal}` + } + + let int = `${integer}` + if (integer < 10) { + int = `0${integer}` + } + + return int + dec + + } + function snapToQuarter(value) { return Math.round(value * 4) / 4; } @@ -119,18 +148,28 @@ const leftPercentage = parseFloat(periodTimeLine.style.left); const widthPercentage = parseFloat(periodTimeLine.style.width); - const startHour = (leftPercentage / 100) * 10 + 8; - const endHour = ((leftPercentage + widthPercentage) / 100) * 10 + 8; + const startHour = (leftPercentage / 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 endValue = Math.round(endHour * 4) / 4; - return [startValue, endValue] + const computedValues = [Math.max(startValue, t_start), Math.min(t_end, endValue)] + + if (computedValues[0] > t_end || computedValues[1] < t_start) { + return [8, 10] + } + + if (computedValues[1] - computedValues[0] <= 0.25 && computedValues[1] < t_end - 0.25) { + computedValues[1] += 0.25; + } + + return computedValues } function setPeriodValues(deb, fin) { - let leftPercentage = (deb - 8) / 10 * 100 - let widthPercentage = (fin - deb) / 10 * 100 + let leftPercentage = (deb - t_start) / (t_end - t_start) * 100 + let widthPercentage = (fin - deb) / (t_end - t_start) * 100 periodTimeLine.style.left = `${leftPercentage}%` periodTimeLine.style.width = `${widthPercentage}%` @@ -140,12 +179,13 @@ function snapHandlesToQuarters() { const periodValues = getPeriodValues(); - let lef = Math.min((periodValues[0] - 8) * 10, 97.5) + let lef = Math.min(computePercentage(periodValues[0], t_start), computePercentage(t_end, 0.25)) if (lef < 0) { lef = 0; } const left = `${lef}%` - let wid = Math.max((periodValues[1] - periodValues[0]) * 10, 2.5) + + let wid = Math.max(computePercentage(periodValues[1], periodValues[0]), computePercentage(0.25, 0)) if (wid > 100) { wid = 100; } @@ -154,7 +194,12 @@ periodTimeLine.style.width = width; } + function computePercentage(a, b) { + return ((a - b) / (t_end - t_start)) * 100 + } + createTicks(); + setPeriodValues(8, 9)