Update opolka/ScoDoc from ScoDoc/ScoDoc #2

Merged
opolka merged 1272 commits from ScoDoc/ScoDoc:master into master 2024-05-27 09:11:04 +02:00
7 changed files with 221 additions and 185 deletions
Showing only changes of commit cfa209a24b - Show all commits

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

@ -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}`;
@ -997,7 +997,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 +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 * Mise à jour d'une ligne étudiant
* @param {String | Number} etudid l'identifiant de l'étudiant * @param {String | Number} etudid l'identifiant de l'étudiant

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

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

@ -12,6 +12,9 @@
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 }}; const period_default = {{ periode_defaut }};
function createTicks() { function createTicks() {
@ -33,7 +36,7 @@
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");
@ -43,6 +46,7 @@
} }
} }
i = j
} }
} }
} }
@ -66,7 +70,9 @@
} }
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) {
@ -153,8 +159,8 @@
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)]
@ -162,8 +168,8 @@
return [t_start, min(t_end, t_start + period_default)] 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
@ -183,13 +189,13 @@
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;
} }

View File

@ -198,7 +198,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),
@ -385,7 +385,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,
@ -449,7 +449,16 @@ def _timeline(formsemestre_id=None) -> HTMLElement:
"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", 0.25),
periode_defaut=sco_preferences.get_preference( periode_defaut=sco_preferences.get_preference(
"periode_defaut", formsemestre_id "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", 0.25))
return render_template( return render_template(
"assiduites/config_assiduites.j2", "assiduites/config_assiduites.j2",
form=form, form=form,