forked from ScoDoc/ScoDoc
Assiduites : Signalement différé
This commit is contained in:
parent
36bc67fffc
commit
d5f01e0628
@ -390,7 +390,6 @@ class BulletinBUT:
|
||||
"H.": "Heure(s)",
|
||||
"J.": "Journée(s)",
|
||||
"1/2 J.": "1/2 Jour.",
|
||||
"N.": "Nombre",
|
||||
}.get(sco_preferences.get_preference("assi_metrique")),
|
||||
}
|
||||
decisions_ues = self.res.get_etud_decisions_ue(etud.id) or {}
|
||||
|
@ -823,34 +823,20 @@ def _make_listes_sem(formsemestre: FormSemestre, with_absences=True):
|
||||
first_monday = sco_abs.ddmmyyyy(
|
||||
formsemestre.date_debut.strftime("%d/%m/%Y")
|
||||
).prev_monday()
|
||||
form_abs_tmpl = f"""
|
||||
form_abs_tmpl = """
|
||||
<td>
|
||||
<a href="%(url_etat)s">absences</a>
|
||||
<a class="btn" href="%(url_etat)s"><button>Voir l'assiduité</button></a>
|
||||
</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"""
|
||||
</select>
|
||||
|
||||
<a href="{
|
||||
<a class="btn" href="{
|
||||
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>
|
||||
</form></td>
|
||||
}?group_ids=%(group_id)s&jour={datetime.date.today().isoformat()}&formsemestre_id={formsemestre.id}"><button>Saisie Journalière</button></a>
|
||||
<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:
|
||||
form_abs_tmpl = ""
|
||||
|
@ -632,7 +632,7 @@ class BasePreferences(object):
|
||||
},
|
||||
),
|
||||
(
|
||||
"etat_defaut",
|
||||
"assi_etat_defaut",
|
||||
{
|
||||
"initvalue": "aucun",
|
||||
"input_type": "menu",
|
||||
@ -658,12 +658,12 @@ class BasePreferences(object):
|
||||
{
|
||||
"initvalue": "1/2 J.",
|
||||
"input_type": "menu",
|
||||
"labels": ["1/2 J.", "J.", "H.", "N."],
|
||||
"allowed_values": ["1/2 J.", "J.", "H.", "N."],
|
||||
"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, N. = nombre)",
|
||||
"explanation": "Unité affichée dans la fiche étudiante et le bilan\n(J. = journée, H. = heure)",
|
||||
"category": "assi",
|
||||
"only_global" : True,
|
||||
"only_global": True,
|
||||
},
|
||||
),
|
||||
# portal
|
||||
|
372
app/templates/assiduites/signal_assiduites_diff.j2
Normal file
372
app/templates/assiduites/signal_assiduites_diff.j2
Normal 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>
|
@ -325,6 +325,7 @@ def signal_assiduites_group():
|
||||
|
||||
# --- Filtrage par formsemestre ---
|
||||
formsemestre_id = groups_infos.formsemestre_id
|
||||
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
if formsemestre.dept_id != g.scodoc_dept_id:
|
||||
abort(404, "groupes inexistants dans ce département")
|
||||
@ -414,7 +415,7 @@ def signal_assiduites_group():
|
||||
|
||||
@bp.route("/EtatAbsencesDate")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoAbsChange)
|
||||
@permission_required(Permission.ScoView)
|
||||
def get_etat_abs_date():
|
||||
evaluation = {
|
||||
"jour": request.args.get("jour"),
|
||||
@ -483,6 +484,84 @@ def get_etat_abs_date():
|
||||
).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(
|
||||
formsemestre: FormSemestre, moduleimpl_id: int = None
|
||||
) -> HTMLElement:
|
||||
|
Loading…
Reference in New Issue
Block a user