forked from ScoDoc/ScoDoc
Update opolka/ScoDoc from ScoDoc/ScoDoc #2
@ -30,7 +30,7 @@ Formulaire configuration Module Assiduités
|
||||
"""
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import SubmitField
|
||||
from wtforms import SubmitField, DecimalField
|
||||
from wtforms.fields.simple import StringField
|
||||
from wtforms.widgets import TimeInput
|
||||
import datetime
|
||||
@ -82,5 +82,7 @@ class ConfigAssiduitesForm(FlaskForm):
|
||||
lunch_time = TimeField("Heure de midi (date pivot entre Matin et Après Midi)")
|
||||
afternoon_time = TimeField("Fin de la journée")
|
||||
|
||||
tick_time = DecimalField("Granularité de la Time Line (temps en minutes)", places=0)
|
||||
|
||||
submit = SubmitField("Valider")
|
||||
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
||||
|
@ -439,9 +439,9 @@ function hideLoader() {
|
||||
*/
|
||||
function toTime(time) {
|
||||
let heure = Math.floor(time);
|
||||
let minutes = (time - heure) * 60;
|
||||
if (minutes < 1) {
|
||||
minutes = "00";
|
||||
let minutes = Math.round((time - heure) * 60);
|
||||
if (minutes < 10) {
|
||||
minutes = `0${minutes}`;
|
||||
}
|
||||
if (heure < 10) {
|
||||
heure = `0${heure}`;
|
||||
@ -997,7 +997,6 @@ function generateEtudRow(
|
||||
</div>
|
||||
<div class="assiduites_bar">
|
||||
<div id="prevDateAssi" class="${assiduite.prevAssiduites?.etat?.toLowerCase()}">
|
||||
<span class="mini_tick">13h</span>
|
||||
</div>
|
||||
</div>
|
||||
<fieldset class="btns_field single" etudid="${etud.id}" assiduite_id="${
|
||||
@ -1065,172 +1064,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
|
||||
* @param {String | Number} etudid l'identifiant de l'étudiant
|
||||
|
@ -14,6 +14,7 @@
|
||||
{{ wtf.form_field(form.morning_time) }}
|
||||
{{ wtf.form_field(form.lunch_time) }}
|
||||
{{ wtf.form_field(form.afternoon_time) }}
|
||||
{{ wtf.form_field(form.tick_time) }}
|
||||
<div class="form-group">
|
||||
{{ wtf.form_field(form.submit) }}
|
||||
{{ wtf.form_field(form.cancel) }}
|
||||
|
@ -3,6 +3,189 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
const mt_start = {{ t_start }};
|
||||
const mt_end = {{ t_end }};
|
||||
/**
|
||||
* Création de la minitiline d'un étudiant
|
||||
* @param {Array[Assiduité]} assiduitesArray
|
||||
* @returns {HTMLElement} l'élément correspondant à la mini timeline
|
||||
*/
|
||||
function createMiniTimeline(assiduitesArray) {
|
||||
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>
|
||||
|
||||
<style>
|
||||
@ -87,10 +270,9 @@
|
||||
|
||||
.mini_tick {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
top: -16px;
|
||||
left: 50%;
|
||||
|
||||
text-align: start;
|
||||
top: -40px;
|
||||
transform: translateX(-50%)
|
||||
}
|
||||
|
||||
.mini_tick::after {
|
||||
|
@ -12,6 +12,9 @@
|
||||
const t_start = {{ t_start }};
|
||||
const t_end = {{ t_end }};
|
||||
|
||||
const tick_time = 60 / {{ tick_time }}
|
||||
const tick_delay = 1 / tick_time
|
||||
|
||||
const period_default = {{ periode_defaut }};
|
||||
|
||||
function createTicks() {
|
||||
@ -33,7 +36,7 @@
|
||||
let j = Math.floor(i + 1)
|
||||
|
||||
while (i < j) {
|
||||
i += 0.25;
|
||||
i += tick_delay;
|
||||
|
||||
if (i <= t_end) {
|
||||
const quarterTick = document.createElement("div");
|
||||
@ -43,6 +46,7 @@
|
||||
}
|
||||
|
||||
}
|
||||
i = j
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -66,7 +70,9 @@
|
||||
}
|
||||
|
||||
function snapToQuarter(value) {
|
||||
return Math.round(value * 4) / 4;
|
||||
|
||||
|
||||
return Math.round(value * tick_time) / tick_time;
|
||||
}
|
||||
|
||||
function setupTimeLine(callback) {
|
||||
@ -153,8 +159,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;
|
||||
const startValue = snapToQuarter(startHour);
|
||||
const endValue = snapToQuarter(endHour);
|
||||
|
||||
const computedValues = [Math.max(startValue, t_start), Math.min(t_end, endValue)]
|
||||
|
||||
@ -162,8 +168,8 @@
|
||||
return [t_start, min(t_end, t_start + period_default)]
|
||||
}
|
||||
|
||||
if (computedValues[1] - computedValues[0] <= 0.25 && computedValues[1] < t_end - 0.25) {
|
||||
computedValues[1] += 0.25;
|
||||
if (computedValues[1] - computedValues[0] <= tick_delay && computedValues[1] < t_end - tick_delay) {
|
||||
computedValues[1] += tick_delay;
|
||||
}
|
||||
|
||||
return computedValues
|
||||
@ -183,13 +189,13 @@
|
||||
|
||||
function snapHandlesToQuarters() {
|
||||
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) {
|
||||
lef = 0;
|
||||
}
|
||||
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) {
|
||||
wid = 100;
|
||||
}
|
||||
|
@ -198,7 +198,7 @@ def signal_assiduites_etud():
|
||||
|
||||
return HTMLBuilder(
|
||||
header,
|
||||
render_template("assiduites/minitimeline.j2"),
|
||||
_mini_timeline(),
|
||||
render_template(
|
||||
"assiduites/signal_assiduites_etud.j2",
|
||||
sco=ScoData(etud),
|
||||
@ -385,7 +385,7 @@ def signal_assiduites_group():
|
||||
|
||||
return HTMLBuilder(
|
||||
header,
|
||||
render_template("assiduites/minitimeline.j2"),
|
||||
_mini_timeline(),
|
||||
render_template(
|
||||
"assiduites/signal_assiduites_group.j2",
|
||||
gr_tit=gr_tit,
|
||||
@ -449,7 +449,16 @@ def _timeline(formsemestre_id=None) -> HTMLElement:
|
||||
"assiduites/timeline.j2",
|
||||
t_start=get_time("assi_morning_time", "08:00:00"),
|
||||
t_end=get_time("assi_afternoon_time", "18:00:00"),
|
||||
tick_time=ScoDocSiteConfig.get("assi_tick_time", 0.25),
|
||||
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"),
|
||||
)
|
||||
|
@ -203,6 +203,8 @@ def config_assiduites():
|
||||
flash("Heure de midi enregistrée")
|
||||
if ScoDocSiteConfig.set("assi_afternoon_time", form.data["afternoon_time"]):
|
||||
flash("Heure de fin de la journée enregistrée")
|
||||
if ScoDocSiteConfig.set("assi_tick_time", form.data["tick_time"]):
|
||||
flash("Granularité de la timeline enregistrée")
|
||||
return redirect(url_for("scodoc.configuration"))
|
||||
|
||||
elif request.method == "GET":
|
||||
@ -215,6 +217,7 @@ def config_assiduites():
|
||||
form.afternoon_time.data = ScoDocSiteConfig.get(
|
||||
"assi_afternoon_time", datetime.time(18, 0, 0)
|
||||
)
|
||||
form.tick_time.data = float(ScoDocSiteConfig.get("assi_tick_time", 0.25))
|
||||
return render_template(
|
||||
"assiduites/config_assiduites.j2",
|
||||
form=form,
|
||||
|
Loading…
Reference in New Issue
Block a user