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
5 changed files with 465 additions and 29 deletions
Showing only changes of commit d5f01e0628 - Show all commits

View File

@ -390,7 +390,6 @@ class BulletinBUT:
"H.": "Heure(s)", "H.": "Heure(s)",
"J.": "Journée(s)", "J.": "Journée(s)",
"1/2 J.": "1/2 Jour.", "1/2 J.": "1/2 Jour.",
"N.": "Nombre",
}.get(sco_preferences.get_preference("assi_metrique")), }.get(sco_preferences.get_preference("assi_metrique")),
} }
decisions_ues = self.res.get_etud_decisions_ue(etud.id) or {} decisions_ues = self.res.get_etud_decisions_ue(etud.id) or {}

View File

@ -823,34 +823,20 @@ def _make_listes_sem(formsemestre: FormSemestre, with_absences=True):
first_monday = sco_abs.ddmmyyyy( first_monday = sco_abs.ddmmyyyy(
formsemestre.date_debut.strftime("%d/%m/%Y") formsemestre.date_debut.strftime("%d/%m/%Y")
).prev_monday() ).prev_monday()
form_abs_tmpl = f""" form_abs_tmpl = """
<td> <td>
<a href="%(url_etat)s">absences</a> <a class="btn" href="%(url_etat)s"><button>Voir l'assiduité</button></a>
</td> </td>
<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""" form_abs_tmpl += f"""
</select> <a class="btn" href="{
<a href="{
url_for("assiduites.signal_assiduites_group", scodoc_dept=g.scodoc_dept) 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> }?group_ids=%(group_id)s&jour={datetime.date.today().isoformat()}&formsemestre_id={formsemestre.id}"><button>Saisie Journalière</button></a>
</form></td> <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: else:
form_abs_tmpl = "" form_abs_tmpl = ""

View File

@ -632,7 +632,7 @@ class BasePreferences(object):
}, },
), ),
( (
"etat_defaut", "assi_etat_defaut",
{ {
"initvalue": "aucun", "initvalue": "aucun",
"input_type": "menu", "input_type": "menu",
@ -658,10 +658,10 @@ class BasePreferences(object):
{ {
"initvalue": "1/2 J.", "initvalue": "1/2 J.",
"input_type": "menu", "input_type": "menu",
"labels": ["1/2 J.", "J.", "H.", "N."], "labels": ["1/2 J.", "J.", "H."],
"allowed_values": ["1/2 J.", "J.", "H.", "N."], "allowed_values": ["1/2 J.", "J.", "H."],
"title": "Métrique de l'assiduité", "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", "category": "assi",
"only_global": True, "only_global": True,
}, },

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

View File

@ -325,6 +325,7 @@ def signal_assiduites_group():
# --- Filtrage par formsemestre --- # --- Filtrage par formsemestre ---
formsemestre_id = groups_infos.formsemestre_id formsemestre_id = groups_infos.formsemestre_id
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
if formsemestre.dept_id != g.scodoc_dept_id: if formsemestre.dept_id != g.scodoc_dept_id:
abort(404, "groupes inexistants dans ce département") abort(404, "groupes inexistants dans ce département")
@ -414,7 +415,7 @@ def signal_assiduites_group():
@bp.route("/EtatAbsencesDate") @bp.route("/EtatAbsencesDate")
@scodoc @scodoc
@permission_required(Permission.ScoAbsChange) @permission_required(Permission.ScoView)
def get_etat_abs_date(): def get_etat_abs_date():
evaluation = { evaluation = {
"jour": request.args.get("jour"), "jour": request.args.get("jour"),
@ -483,6 +484,84 @@ def get_etat_abs_date():
).build() ).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( def _module_selector(
formsemestre: FormSemestre, moduleimpl_id: int = None formsemestre: FormSemestre, moduleimpl_id: int = None
) -> HTMLElement: ) -> HTMLElement: