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
6 changed files with 1020 additions and 45 deletions
Showing only changes of commit ea76dd702e - Show all commits

View File

@ -282,6 +282,8 @@ def validation_rcue_record(etudid: int):
) )
operation = "record" operation = "record"
db.session.add(validation) db.session.add(validation)
# invalider bulletins (les autres résultats ne dépendent pas des RCUEs):
sco_cache.invalidate_formsemestre_etud(etud)
db.session.commit() db.session.commit()
Scolog.logdb( Scolog.logdb(
method="validation_rcue_record", method="validation_rcue_record",

View File

@ -307,6 +307,17 @@ class FormSemestre(db.Model):
- et sont associées à l'un des parcours de ce formsemestre - et sont associées à l'un des parcours de ce formsemestre
(ou à aucun, donc tronc commun). (ou à aucun, donc tronc commun).
""" """
# per-request caching
key = (self.id, with_sport)
_cache = getattr(g, "_formsemestre_get_ues_cache", None)
if _cache:
result = _cache.get(key, False)
if result is not False:
return result
else:
g._formsemestre_get_ues_cache = {}
_cache = g._formsemestre_get_ues_cache
formation: Formation = self.formation formation: Formation = self.formation
if formation.is_apc(): if formation.is_apc():
# UEs de tronc commun (sans parcours indiqué) # UEs de tronc commun (sans parcours indiqué)
@ -326,8 +337,7 @@ class FormSemestre(db.Model):
).filter(UniteEns.semestre_idx == self.semestre_id) ).filter(UniteEns.semestre_idx == self.semestre_id)
} }
) )
ues = sem_ues.values() ues = sorted(sem_ues.values(), key=attrgetter("numero", "acronyme"))
return sorted(ues, key=attrgetter("numero", "acronyme"))
else: else:
sem_ues = db.session.query(UniteEns).filter( sem_ues = db.session.query(UniteEns).filter(
ModuleImpl.formsemestre_id == self.id, ModuleImpl.formsemestre_id == self.id,
@ -336,7 +346,9 @@ class FormSemestre(db.Model):
) )
if not with_sport: if not with_sport:
sem_ues = sem_ues.filter(UniteEns.type != codes_cursus.UE_SPORT) sem_ues = sem_ues.filter(UniteEns.type != codes_cursus.UE_SPORT)
return sem_ues.order_by(UniteEns.numero).all() ues = sem_ues.order_by(UniteEns.numero).all()
_cache[key] = ues
return ues
@cached_property @cached_property
def modimpls_sorted(self) -> list[ModuleImpl]: def modimpls_sorted(self) -> list[ModuleImpl]:

View File

@ -63,25 +63,32 @@ def dict_pvjury(
Si with_parcours_decisions: ajoute infos sur code decision jury de tous les semestre du parcours Si with_parcours_decisions: ajoute infos sur code decision jury de tous les semestre du parcours
Résultat: Résultat:
{ {
'date' : date de la decision la plus recente, 'date' : str = date de la decision la plus recente, format dd/mm/yyyy,
'formsemestre' : sem, 'formsemestre' : dict = formsemestre,
'is_apc' : bool, 'is_apc' : bool,
'formation' : { 'acronyme' :, 'titre': ... } 'formation' : { 'acronyme' :, 'titre': ... }
'decisions' : { [ { 'identite' : {'nom' :, 'prenom':, ...,}, 'decisions' : [
'etat' : I ou D ou DEF {
'decision_sem' : {'code':, 'code_prev': }, 'identite' : {'nom' :, 'prenom':, ...,},
'decisions_ue' : { ue_id : { 'code' : ADM|CMP|AJ, 'event_date' :, 'etat' : I ou D ou DEF
'acronyme', 'numero': } }, 'decision_sem' : {'code':, 'code_prev': },
'autorisations' : [ { 'semestre_id' : { ... } } ], 'decisions_ue' : {
'validation_parcours' : True si parcours validé (diplome obtenu) ue_id : {
'prev_code' : code (calculé slt si with_prev), 'code' : ADM|CMP|AJ,
'mention' : mention (en fct moy gen), 'ects' : float,
'sum_ects' : total ECTS acquis dans ce semestre (incluant les UE capitalisées) 'event_date' :str = "dd/mm/yyyy",
'sum_ects_capitalises' : somme des ECTS des UE capitalisees },
} },
] 'autorisations' : [ { 'semestre_id' : { ... } } ],
}, 'validation_parcours' : True si parcours validé (diplome obtenu)
'decisions_dict' : { etudid : decision (comme ci-dessus) }, 'prev_code' : code (calculé slt si with_prev),
'mention' : mention (en fct moy gen),
'sum_ects' : total ECTS acquis dans ce semestre (incluant les UE capitalisées)
'sum_ects_capitalises' : somme des ECTS des UE capitalisees
},
...
],
'decisions_dict' : { etudid : decision (comme ci-dessus) },
} }
""" """
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)

View File

@ -106,7 +106,7 @@ body:not(.editionActivated) .editing {
@keyframes boing { @keyframes boing {
100% { 100% {
transform: translateY(-20px) transform: translateY(-20px);
} }
} }
@ -152,6 +152,7 @@ body.editionActivated .filtres>div>div>div>div {
color: #000; color: #000;
border-radius: 4px; border-radius: 4px;
outline: 4px solid #FFF; outline: 4px solid #FFF;
padding: 2px;
} }
/* Suppression */ /* Suppression */
@ -355,7 +356,7 @@ body.editionActivated .filtres .nonEditable .move {
} }
.groupe:has(.etudiants:empty) { .groupe:has(.etudiants:empty) {
display: none; display: none;
} }
/* .filtres .unselect { /* .filtres .unselect {
@ -365,6 +366,23 @@ body.editionActivated .filtres .nonEditable .move {
/*****************************/ /*****************************/
/* Zone Etudiants */ /* Zone Etudiants */
/*****************************/ /*****************************/
#zoneChoix .autoAffectation>a {
text-decoration: underline;
}
#zoneChoix .dropZone {
background: #FFF;
border-radius: 8px;
border: 2px dashed #09C;
margin-bottom: 4px;
padding: 4px;
transition: 0.2s;
}
.fileOver {
transform: scale(0.9);
}
#zoneChoix>.autoAffectation { #zoneChoix>.autoAffectation {
background: #c9c9c9; background: #c9c9c9;
color: #141414; color: #141414;

View File

@ -22,6 +22,23 @@
<section id="zoneChoix"> <section id="zoneChoix">
<h2>Étudiants</h2> <h2>Étudiants</h2>
<div class="autoAffectation">
<a href="students_groups_auto_assignment?formsemestre_id={{formsemestre.id}}"><svg
xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
stroke="#0b0b0b" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4M10 17l5-5-5-5M13.8 12H3" />
</svg> Aide à l'affectation dans les parcours</a>
<div>Importer les résultats :
<form class=dropZone>
<div>
Déposez le fichier .xlsx ou <br>
<label>
<input type=file accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet">
</label>
</div>
</form>
</div>
</div>
<div class="autoAffectation"> <div class="autoAffectation">
Affecter automatiquement les étudiants du groupe<br> Affecter automatiquement les étudiants du groupe<br>
<select name="affectationFrom" id="affectationFrom"></select> <select name="affectationFrom" id="affectationFrom"></select>
@ -38,6 +55,7 @@
</section> </section>
</main> </main>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx-populate/1.21.0/xlsx-populate.min.js"></script>
<script> <script>
go(); go();
@ -125,7 +143,7 @@
output = ""; output = "";
etudiants.forEach(etudiant => { etudiants.forEach(etudiant => {
output += ` output += `
<div> <div data-etudid="${etudiant.etudid}" >
<div class=nom data-etudid="${etudiant.etudid}" data-nom="${etudiant.nom_disp}" data-prenom="${etudiant.prenom}"><a href="ficheEtud?etudid=${etudiant.etudid}">${etudiant.nom_disp} ${etudiant.prenom}</a><div class=small>${etudiant.bac}</div></div> <div class=nom data-etudid="${etudiant.etudid}" data-nom="${etudiant.nom_disp}" data-prenom="${etudiant.prenom}"><a href="ficheEtud?etudid=${etudiant.etudid}">${etudiant.nom_disp} ${etudiant.prenom}</a><div class=small>${etudiant.bac}</div></div>
${(() => { ${(() => {
let output = "<div class=grpPartitions>"; let output = "<div class=grpPartitions>";
@ -141,7 +159,7 @@
}) })
arrayGroups.forEach((groupe) => { arrayGroups.forEach((groupe) => {
output += ` output += `
<label><input type=radio name="${partition.id}-${etudiant.etudid}" value="${groupe.id}" ${(etudiant.partitions[partition.id] == groupe.id) ? "checked" : ""}><span>${groupe.group_name}</span></label>`; <label><input type=radio name="${partition.id}-${etudiant.etudid}" value="${groupe.id}" ${(etudiant.partitions[partition.id] == groupe.id) ? "checked" : ""}><span data-grpname="${groupe.group_name}">${groupe.group_name}</span></label>`;
if (etudiant.partitions[partition.id] == groupe.id) { if (etudiant.partitions[partition.id] == groupe.id) {
affected = true; affected = true;
@ -192,10 +210,10 @@
Configuration Configuration
<hr> <hr>
<label title="Calculer et afficher les rangs dans les groupes de cette partition ?"> <label title="Calculer et afficher les rangs dans les groupes de cette partition ?">
<input class=rang type=checkbox ${partition.bul_show_rank?"checked":""} data-attr=bul_show_rank> Rang bulletins <input class=rang type=checkbox ${partition.bul_show_rank ? "checked" : ""} data-attr=bul_show_rank> Rang bulletins
</label> </label>
<label title="Doit-on afficher les groupes de cette partition dans les listes ?"> <label title="Doit-on afficher les groupes de cette partition dans les listes ?">
<input class=groupe type=checkbox ${partition.show_in_lists?"checked":""} data-attr=show_in_lists> Afficher sur bulletins et tableaux <input class=groupe type=checkbox ${partition.show_in_lists ? "checked" : ""} data-attr=show_in_lists> Afficher sur bulletins et tableaux
</label> </label>
<label> <label>
<a class="stdlink" href="/ScoDoc/{{formsemestre.departement.acronym}}/Scolarite/groups_auto_repartition?partition_id=${partition.id}">Répartir les étudiants</a> <a class="stdlink" href="/ScoDoc/{{formsemestre.departement.acronym}}/Scolarite/groups_auto_repartition?partition_id=${partition.id}">Répartir les étudiants</a>
@ -353,24 +371,76 @@
} }
}) })
} }
/*****************************************/
/* Import des résultats auto affectation */
/*****************************************/
document.querySelector(".dropZone").addEventListener("drop", dropFile);
document.querySelector(".dropZone input").addEventListener("change", dropFile);
document.querySelector(".dropZone").addEventListener("dragover", dragOver);
document.querySelector(".dropZone").addEventListener("dragleave", dragLeave);
function dropFile(event) {
event.preventDefault();
this.classList.remove("fileOver");
if (event.target.files?.[0] || event.dataTransfer.items[0].type.match('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')) {
let file = event.target.files?.[0] || event.dataTransfer.items[0].getAsFile();
let reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onloadend = () => {
manageFile(reader.result)
}
}
}
function manageFile(file) {
XlsxPopulate.fromDataAsync(file)
.then(function (workbook) {
const sheet = workbook.sheet(0);
let ligne = 1;
let colonne = 1;
let parcours;
let etudid;
while (parcours = sheet.row(ligne).cell(colonne).value()) {
ligne += 2;
while (etudid = sheet.row(ligne).cell(colonne).value()) {
console.log(parcours, etudid);
document.querySelector(`#zoneChoix .etudiants [data-etudid="${etudid}"] [data-grpname="${parcours}"]`)?.click();
ligne++;
}
ligne = 1;
colonne += 3;
}
})
}
function dragOver(event) {
event.preventDefault();
this.classList.add("fileOver")
}
function dragLeave() {
this.classList.remove("fileOver")
}
/****************************/ /****************************/
/* Affectation à un groupe */ /* Affectation à un groupe */
/****************************/ /****************************/
function affectationGo(){ function affectationGo() {
let from = document.querySelector("#affectationFrom").value; let from = document.querySelector("#affectationFrom").value;
let to = document.querySelector("#affectationTo").value; let to = document.querySelector("#affectationTo").value;
if(!from || !to){ if (!from || !to) {
return; return;
} }
let elements = []; let elements = [];
if(from[0] != "n"){ if (from[0] != "n") {
elements = document.querySelectorAll(`#zoneChoix .etudiants [value="${from}"]:checked`) elements = document.querySelectorAll(`#zoneChoix .etudiants [value="${from}"]:checked`)
} else { } else {
document.querySelectorAll(`#zoneChoix .etudiants [data-idpartition="${from.split("-")[1]}"]`).forEach(element=>{ document.querySelectorAll(`#zoneChoix .etudiants [data-idpartition="${from.split("-")[1]}"]`).forEach(element => {
if(!element.querySelector('input:not([value="aucun"]):checked')){ if (!element.querySelector('input:not([value="aucun"]):checked')) {
elements.push(element); elements.push(element);
} }
}) })
@ -378,10 +448,10 @@
console.log(elements); console.log(elements);
elements.forEach(groupeSelected=>{ elements.forEach(groupeSelected => {
if(to[0] != "n"){ if (to[0] != "n") {
groupeSelected.closest(".grpPartitions").querySelector(`[value="${to}"]`).click(); groupeSelected.closest(".grpPartitions").querySelector(`[value="${to}"]`).click();
}else{ } else {
groupeSelected.closest(".grpPartitions").querySelector(`[value="aucun"]`).click(); groupeSelected.closest(".grpPartitions").querySelector(`[value="aucun"]`).click();
} }
@ -609,6 +679,15 @@
} }
function saveEditing(obj) { function saveEditing(obj) {
// Vérification que le champ est non vide
if (obj.innerText == "") {
event.preventDefault();
sco_message("Ce champ ne peut rester vide.");
obj.focus();
return;
}
// Fin de l'édition
obj.classList.remove("editingText"); obj.classList.remove("editingText");
obj.setAttribute("contenteditable", "false"); obj.setAttribute("contenteditable", "false");
obj.removeEventListener("keydown", writing); obj.removeEventListener("keydown", writing);
@ -829,14 +908,14 @@
headers: { headers: {
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8" "Content-type": "application/x-www-form-urlencoded; charset=UTF-8"
}, },
body: `partition_id=${this.closest("[data-idpartition]").dataset.idpartition}&attr=${this.dataset.attr}&value=${this.checked?1:0}` body: `partition_id=${this.closest("[data-idpartition]").dataset.idpartition}&attr=${this.dataset.attr}&value=${this.checked ? 1 : 0}`
} }
).then(function(response) { ).then(function (response) {
return response.text(); return response.text();
}) })
.then(function(txt) { .then(function (txt) {
sco_message(txt); sco_message(txt);
}); });
} }

View File

@ -1 +1,858 @@
{# Pour Sébastien #} <style>
.wait {
position: fixed;
width: 50px;
height: 10px;
background: #424242;
top: calc(50% - 50px);
left: 50%;
margin-left: -25px;
animation: wait 0.6s ease-out alternate infinite;
}
@keyframes wait {
100% {
transform: translateY(-30px) rotate(360deg)
}
}
main h2 {
border-bottom: 4px solid #0069d9;
grid-column: 1 / -1;
margin-bottom: 2px;
}
main {
font-family: Verdana, Geneva, Tahoma, sans-serif;
margin-right: 16px;
padding: 8px;
border-radius: 12px;
background: #717171;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
section {
background: #dbccb8;
padding: 8px;
border-radius: 8px;
}
.moitemoite {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
@media screen and (max-width: 1000px) {
.moitemoite {
grid-template-columns: 1fr;
}
}
.notes {
opacity: 0.4;
pointer-events: none;
}
.parcours,
.notes {
display: inline-grid;
grid-template-columns: auto auto;
align-items: center;
grid-row-gap: 4px;
grid-column-gap: 16px;
margin-bottom: 16px;
background: #FFF;
border: 1px solid #0069d9;
border-radius: 8px;
padding: 8px 16px;
}
h3,
.resultats small {
grid-column: 1 / -1;
margin: 4px 0;
}
.parcours form {
display: inline;
}
.parcours input {
padding: 4px 8px;
border: 1px solid #aaa;
border-radius: 4px;
width: 40px;
}
.dropZone {
border: 2px dashed #0069d9;
padding: 8px 16px;
text-align: center;
}
.notes select {
padding: 4px 8px;
}
.donnees,
.resultats {
display: inline-grid;
}
.donnees {
background: #aaa;
padding: 4px;
border-radius: 4px;
}
.donnees>* {
border-radius: 4px;
border: 1px solid #aaa;
}
.donnees>*,
.resultats>*:not(h2, button) {
background: #fff;
outline: 1px solid #aaa;
padding: 4px 8px;
}
.entete {
background: #0069d9 !important;
color: #FFF;
}
.criteres {
text-align: right;
}
.criteres:not(.criteres+.criteres),
.voeux:not(.voeux+.voeux),
.entete+.entete {
margin-left: 8px;
}
small {
display: block;
margin-bottom: 4px;
padding: 2px 4px;
background: #fff;
border-radius: 4px;
font-style: italic;
}
button {
background: rgb(134, 179, 0);
color: #FFF;
font-size: 16px;
padding: 16px 32px;
border: none;
border-radius: 4px;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.26);
cursor: pointer;
}
button:hover {
color: #ddd;
}
button:active {
transform: translateY(2px);
box-shadow: initial;
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px;
}
button.autre {
background: #0c9;
}
.resultats button {
background: #90c;
margin-bottom: 4px;
}
b {
color: #000;
}
.moitemoite input:invalid,
[contenteditable]:empty {
animation: focus .5s alternate infinite;
}
@keyframes focus {
0% {
outline: 1px solid rgba(204, 0, 153, 0)
}
100% {
outline: 1px solid rgba(204, 0, 153, 1)
}
}
[data-actif] {
cursor: pointer;
}
[data-actif=false] {
background: #ccc;
}
[data-highlight=true] {
outline: 2px solid #90c !important;
border-radius: 4px;
z-index: 1;
}
</style>
<main class="moitemoite">
<div class="wait"></div>
<section>
<h2>Données</h2>
<div class="parcours"></div>
<div class="notes"></div>
<div style="display:flex; gap: 8px;">
<button onclick="exportData()">Exporter les données</button>
<!--<form class="dropZone">
<div><b>Importer les données</b></div>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#0099cc" stroke-width="2" stroke-linecap="round"><path pathlength="100" d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path></svg>
Déposez une fichier ou <br>
<label><input type="file" accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"></label>
</form>-->
</div>
<small style="margin-top: 4px">💡 Vous pouvez ensuite copier / coller, dans les cases blanches ci-dessous, les
"notes" et "voeux", directement depuis le tableur.</small>
<small>💡 Les voeux doivent être une série d'entiers en partant de 1 - exemple : 3, 1 et 2.</small>
<small>💡 Cliquez sur un étudiant pour qu'il ne fasse pas parti du tri.</small>
<div class="donnees"></div>
</section>
<section>
<h2>Résultats</h2>
<div class=grid>
<button class="actionTri" data-tri="notes">Trier uniquement par <b>notes</b></button>
<button class="actionTri" data-tri="voeux">Trier par <b>voeux</b> puis par notes</button>
</div>
<small>
💡 Vous pouvez exporter les résultats pour les utiliser dans l'éditeur de groupes/partitions.
</small>
<div class="resultats"></div>
</section>
</main>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx-populate/1.21.0/xlsx-populate.min.js"></script>
<script>
/************************/
/* A générer par Scodoc */
/************************/
let choixNotes = [
"RCUE1",
"RCUE2",
"RCUE3",
"RCUE4",
"RCUE5"
]
let formsemestre;
let parcours = {};
let nbParcours;
/*let parcours = {
1448: {
nom: "Communication",
places: 28, // Modifiable par l'utilisateur
etudiants: [] // Résultat du tri
},
1449: {
nom: "Création numérique",
places: 28, // Modifiable par l'utilisateur
etudiants: [] // Résultat du tri
},
1450: {
nom: "Développement Web",
places: 28, // Modifiable par l'utilisateur
etudiants: [] // Résultat du tri
}
}*/
let etudiants = {};
/*let etudiants = {
123: {
actif: true,
nom: "Jean Bono",
criteres: {
1448: 10, // Modifiable par l'utilisateur
1449: 12, // Modifiable par l'utilisateur
1450: 8 // Modifiable par l'utilisateur
},
voeux: {
1448: 1, // Modifiable par l'utilisateur
1449: 3, // Modifiable par l'utilisateur
1450: 2 // Modifiable par l'utilisateur
}
},
124: {
actif: true,
nom: "Etudiant 2",
criteres: {
1448: 11, // Modifiable par l'utilisateur
1449: 12.5, // Modifiable par l'utilisateur
1450: 8 // Modifiable par l'utilisateur
},
voeux: {
1448: 2, // Modifiable par l'utilisateur
1449: 1, // Modifiable par l'utilisateur
1450: 3 // Modifiable par l'utilisateur
}
},
125: {
actif: true,
nom: "Etudiant 3",
criteres: {
1448: 10, // Modifiable par l'utilisateur
1449: 12.5, // Modifiable par l'utilisateur
1450: 15 // Modifiable par l'utilisateur
},
voeux: {
1448: 3, // Modifiable par l'utilisateur
1449: 2, // Modifiable par l'utilisateur
1450: 1 // Modifiable par l'utilisateur
}
}
}*/
/************************/
/* Chargement des datas */
/************************/
go();
async function go() {
document.querySelector('.wait').style.display = "";
let params = (new URL(document.location)).searchParams;
formsemestre = params.get('formsemestre_id');
let parcoursRaw = await fetchData("/ScoDoc/{{formsemestre.departement.acronym}}/api/formsemestre/" + formsemestre + "/partitions");
let etudiantsRaw = await fetchData("/ScoDoc/{{formsemestre.departement.acronym}}/api/formsemestre/" + formsemestre + "/etudiants");
//let decisionsRaw = await fetchData("/ScoDoc/{{formsemestre.departement.acronym}}/api/formsemestre/" + formsemestre + "/decisions_jury");
let savedData = await getFromScodoc();
etudiantsRaw.sort((a, b) => {
return (a.nom + a.prenom).localeCompare(b.nom + b.prenom)
});
processParcours(parcoursRaw, savedData);
processNotes();
processEtudiants(etudiantsRaw, savedData);
processEvents();
document.querySelector("body").classList.add("loaded");
document.querySelector('.wait').style.display = "none";
}
function fetchData(request) {
return fetch(request)
.then(r => { return r.json() })
.then(data => {
return data;
}).catch(error => {
document.querySelector("main").innerHTML = "<h2>Une erreur s'est produite lors du transfert des données.</h2>";
throw 'Fin du script - données invalides';
})
}
/************************/
/* Génération du front */
/************************/
function processParcours(parcoursRaw, savedData) {
Object.values(parcoursRaw).forEach(partition => {
if (partition.partition_name == "Parcours") {
Object.values(partition.groups).forEach(group => {
parcours[group.id] = {
nom: group.group_name,
places: savedData.parcours[group.id].places || 28,
etudiants: [] // Résultat du tri
}
})
}
})
nbParcours = Object.keys(parcours).length;
/* Zones parcours */
let output = "<h3>Places</h3>";
Object.entries(parcours).forEach(([idParcours, dataParcours]) => {
output += `
<div data-idparcours=${idParcours}>${dataParcours.nom}</div>
<div><form onsubmit="return false;"><input type=number required min=0 data-idparcours="${idParcours}" value="${dataParcours.places}"></form> places</div>
`;
})
document.querySelector(".parcours").innerHTML = output;
}
function processNotes() {
/* Zones critère de notes */
let output = "<h3>Appliquer les notes</h3>";
Object.entries(parcours).forEach(([idParcours, dataParcours]) => {
output += `
<div data-idparcours=${idParcours}>${dataParcours.nom}</div>
<div>
<select data-idparcours=${idParcours}>
${(
() => {
let output = "<option>-</option>";
choixNotes.forEach(notes => {
output += `<option value="${notes}">${notes}</option>`;
})
return output;
}
)()}
</select>
</div>
`;
})
document.querySelector(".notes").innerHTML = output;
}
function processEtudiants(etudiantsRaw, savedData) {
/*id123: {
actif: true,
nom: "Jean Bono",
criteres: {
1448: 10, // Modifiable par l'utilisateur
1449: 12, // Modifiable par l'utilisateur
1450: 8 // Modifiable par l'utilisateur
},
voeux: {
1448: 1, // Modifiable par l'utilisateur
1449: 3, // Modifiable par l'utilisateur
1450: 2 // Modifiable par l'utilisateur
}
},*/
etudiantsRaw.forEach(etudiant => {
saved = savedData.etudiants['id' + etudiant.id];
etudiants['id' + etudiant.id] = {
actif: saved.actif ? true : false,
nom: etudiant.nom + " " + etudiant.prenom,
criteres: saved.criteres || {},
voeux: saved.voeux || {}
};
/*Object.keys(parcours).forEach((idParcours, index) => {
etudiants['id' + etudiant.id].criteres[idParcours] = 10;
etudiants['id' + etudiant.id].voeux[idParcours] = index + 1;
})*/
})
/* Zone étudiants */
let output = `
<div class=entete>Etudiants</div>
<div class=entete style="grid-column: span ${nbParcours}">Notes</div>
<div class=entete style="grid-column: span ${nbParcours}">Voeux</div>`;
Object.entries(etudiants).forEach(([id, etudiant]) => {
output += `
<div data-actif=${etudiant.actif}>${etudiant.nom}</div>
${display(id, etudiant, "criteres", true)}
${display(id, etudiant, "voeux", true)}
`;
})
document.querySelector(".donnees").style.gridTemplateColumns = `repeat(${1 + nbParcours * 2}, auto)`;
document.querySelector(".donnees").innerHTML = output;
}
/**********************************/
/* Affichage des critères / voeux */
/**********************************/
function display(id, datas, key, editable) {
let output = "";
Object.entries(datas[key]).forEach(([idParcours, data]) => {
output += `<div class=${key} data-id=${id} data-idParcours=${idParcours} contenteditable=${editable}>${data}</div>`;
})
return output;
}
/**********************************/
/* Gestion des événements */
/**********************************/
function processEvents() {
document.querySelectorAll(".parcours input").forEach(e => e.addEventListener("change", changeNbParcours));
document.querySelectorAll(".notes select").forEach(e => e.addEventListener("change", changeNotes));
document.querySelectorAll(".actionTri").forEach(e => e.addEventListener("click", processData));
document.querySelectorAll(".donnees>.criteres, .donnees>.voeux").forEach(e => e.addEventListener("paste", pasteData));
document.querySelectorAll("[data-actif]").forEach(e => e.addEventListener("mousedown", event => { event.preventDefault() }));
document.querySelectorAll("[data-actif]").forEach(e => e.addEventListener("click", changeActif));
document.querySelectorAll("[contenteditable]").forEach(e => e.addEventListener("keypress", changeListe));
document.querySelectorAll("[contenteditable]").forEach(e => e.addEventListener("keyup", saveLocal));
document.querySelectorAll("[contenteditable]").forEach(e => e.addEventListener("keyup", saveToScodoc));
document.querySelectorAll("[data-idparcours]").forEach(e => e.addEventListener("mouseover", setHighlight));
document.querySelectorAll("[data-idparcours]").forEach(e => e.addEventListener("mouseleave", resetHighlight));
}
/***************************************/
/* Traitement des entrées utilisateurs */
/***************************************/
function changeNbParcours() {
let idParcours = this.dataset.idparcours;
let places = this.value;
parcours[idParcours].places = places;
saveToScodoc();
}
function changeNotes() {
let idParcours = this.dataset.idparcours;
let notes = this.value;
/******* SCODOC - récupérer et modifier les valeurs des notes ******/
}
function changeActif() {
let statut = this.dataset.actif;
if (statut == "true") {
statut = false;
} else {
statut = true;
}
this.dataset.actif = statut;
etudiants[this.nextElementSibling.dataset.id].actif = statut;
saveToScodoc();
}
function changeListe(event) {
let type = this.className;
/* Vérification des touches */
if (type == "criteres") {
if (!/[0-9.]/.test(event.key)) {
event.preventDefault();
}
} else {
if (!/[0-9]/.test(event.key)) {
event.preventDefault();
}
}
if (event.key == "." && /[.]/.test(this.innerText)) {
event.preventDefault();
}
}
function saveLocal(event) {
/* Enregistement des données */
let type = this.className;
let parcours = this.dataset.idparcours;
let id = this.dataset.id;
etudiants[id][type][parcours] = parseFloat(this.innerText);
}
/*function importList(semestre) {
console.log(parcours);
/******* SCODOC - enregistrer la répartition des étudiants dans le parcours ******/
/*document.querySelector(".resultats>h2").innerHTML += `<div>✔️ Groupes importés dans le ${semestre}</div>`;
}*/
function setHighlight() {
document.querySelectorAll(`[data-idparcours="${this.dataset.idparcours}"]`).forEach(e => {
e.dataset.highlight = true;
})
}
function resetHighlight() {
document.querySelectorAll(`[data-highlight=true]`).forEach(e => {
e.dataset.highlight = false;
})
}
function saveToScodoc() {
let data = {
parcours: parcours,
etudiants: etudiants
};
fetch(
"/ScoDoc/{{formsemestre.departement.acronym}}/api/formsemestre/" + formsemestre + "/save_groups_auto_assignment",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data)
}
);
}
async function getFromScodoc() {
let dataRaw = await fetch("/ScoDoc/{{formsemestre.departement.acronym}}/api/formsemestre/" + formsemestre + "/get_groups_auto_assignment");
let data = await dataRaw.text();
if (data == "") {
return {};
}
return JSON.parse(data);
}
/***************/
/* Algo de tri */
/***************/
function processData() {
let typeTri = this.dataset.tri;
// Vérifications nombre de place
let placesTotales = 0;
Object.values(parcours).forEach(p => { placesTotales += p.places })
if (placesTotales < Object.keys(etudiants).length) {
document.querySelector(".resultats").innerHTML = "<h2>⚠️ Le nombre de places est inférieur au nombre d'étudiants ⚠️</h2>";
return;
}
// Vérification champs non remplis
if (typeTri == "notes") {
var recherche = ".moitemoite input:invalid";
} else {
var recherche = ".moitemoite input:invalid, [contenteditable]:empty";
}
if (document.querySelector(recherche)) {
document.querySelector(".resultats").innerHTML = "<h2>⚠️ Veuillez remplir tous les champs ⚠️</h2>";
return;
}
// Remise à 0 des résultats
Object.values(parcours).map(p => p.etudiants = []);
// Liste des étudiants à traiter
let etudiantsID = Object.keys(etudiants);
/* Pour chaque niveau de voeux */
for (let numVoeux = 1; numVoeux < nbParcours + 1; numVoeux++) {
/* Ce qu'il restera à traiter au round suivant */
let aTraiter = [];
etudiantsID.forEach(id => {
if (etudiants[id].actif == false) return;
if (typeTri == "notes") {
// Récupère le parcours en fonction des notes et du round
var groupe = Object.entries(etudiants[id].criteres).sort(([, a], [, b]) => b - a)[numVoeux - 1][0];
} else {
// Trouve le parcours qui correspond au numéro du voeux
var groupe = Object.keys(etudiants[id].voeux).find(key => etudiants[id].voeux[key] == numVoeux);
}
if (!groupe) {
sco_message("Il semblerait que les voeux de " + etudiants[id].nom + " ne soit pas consécutifs en partant de 1.");
}
if (parcours[groupe].places == 0) {
aTraiter.push(id);
} else if (parcours[groupe].etudiants.length < parcours[groupe].places) {
// Le groupe n'est pas plein, on y ajoute l'étudiant
parcours[groupe].etudiants.push(id);
} else {
aTraiter.push(groupePlein(groupe, id));
}
})
etudiantsID = aTraiter;
}
showResults();
}
function groupePlein(groupe, id) {
// Trie des étudiants dans le parcours par rapport au critère
parcours[groupe].etudiants = parcours[groupe].etudiants.sort((a, b) => {
return etudiants[a].criteres[groupe] - etudiants[b].criteres[groupe];
})
// Si les deux étudiants ont le même niveau : aléatoire
let diff = etudiants[parcours[groupe].etudiants[0]].criteres[groupe] - etudiants[id].criteres[groupe];
if (diff == 0) {
if (Math.random() < 0.5) {
diff = 1;
} else {
diff = -1;
}
}
// On ajoute dans le parcours le meilleur et on met à traiter l'autre
if (diff < 0) {
parcours[groupe].etudiants.push(id);
return parcours[groupe].etudiants.shift();
} else {
return id;
}
}
/***************************/
/* Affichage des résultats */
/***************************/
function showResults() {
let output = `
<button class="autre" onclick="exportResults()">Exporter les résultats</button>`;
/*`
<h2>
Importer les résultats dans Scodoc<br>
<button onclick='importList("S1")'>S1</button>
<button onclick='importList("S2")'>S2</button>
<button onclick='importList("S3")'>S3</button>
<button onclick='importList("S4")'>S4</button>
<button onclick='importList("S5")'>S5</button>
<button onclick='importList("S6")'>S6</button>
</h2>
`*/;
Object.values(parcours).forEach(p => {
output += `<h2>${p.nom} - ${p.etudiants.length}</h2>
<div class=entete>Etudiants</div>
<div class=entete style="grid-column: span ${nbParcours}">Notes</div>
<div class=entete style="grid-column: span ${nbParcours}">Voeux</div>`;
p.etudiants.forEach(id => {
output += `<div>${etudiants[id].nom}</div>
${display(id, etudiants[id], "criteres", false)}
${display(id, etudiants[id], "voeux", false)}
`;
})
})
document.querySelector(".resultats").style.gridTemplateColumns = `repeat(${1 + nbParcours * 2}, auto)`;
document.querySelector(".resultats").innerHTML = output;
}
/*****************************/
/* Copier / coller des datas */
/*****************************/
function pasteData(event) {
event.preventDefault();
let data = event.clipboardData.getData('Text');
let lignes = data.split(/\r\n|\r|\n/g);
currentTarget = this;
// Calcul du décalage dans la grille
let offset = -1;
do {
offset++;
currentTarget = currentTarget.previousElementSibling;
} while (currentTarget.className != "")
// Collage des données
currentTarget = this;
lignes.forEach(ligne => {
let zone = ligne.split(/\t/g);
for (let i = 0; i < zone.length; i++) {
if (!currentTarget) return;
currentTarget.innerText = zone[i];
currentTarget = currentTarget.nextElementSibling;
if (currentTarget?.className == "") {
for (let j = 0; j <= offset; j++) {
currentTarget = currentTarget.nextElementSibling;
}
return;
}
}
})
updateStudentsDatas();
}
function updateStudentsDatas() {
document.querySelectorAll(".criteres, .voeux").forEach(e => {
etudiants[e.dataset.id][e.className][e.dataset.idparcours] = parseFloat(e.innerText);
})
saveStudentsData();
}
function saveStudentsData() {
/******* SCODOC - enregistrer les données étudiantes ******/
console.log(etudiants);
}
/***************/
/* Export xlsx */
/***************/
function saveFile(name, workbook) {
workbook.outputAsync()
.then(function (blob) {
var url = window.URL.createObjectURL(blob);
var a = document.createElement("a");
document.body.appendChild(a);
a.href = url;
a.download = name + ".xlsx";
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
});
}
function changeChar(char, nb) {
return String.fromCharCode(char.charCodeAt(0) + nb);
}
async function exportData() {
XlsxPopulate.fromBlankAsync()
.then(workbook => {
const sheet = workbook.sheet(0);
sheet.name("Données");
sheet.cell("A1").value("id");
sheet.cell("B1").value("Actif");
sheet.cell("C1").value("Nom");
Object.values(parcours).forEach((p, index) => {
sheet.cell(changeChar("D", index) + "1").value("Note " + p.nom);
sheet.cell(changeChar("D", index + nbParcours) + "1").value("Voeu " + p.nom);
sheet.cell(changeChar("D", index + nbParcours) + "2").value(p.places);
})
Object.entries(etudiants).forEach(([id, etudiant], index) => {
sheet.cell("A" + (index + 3)).value(id.slice(2));
sheet.cell("B" + (index + 3)).value(etudiant.actif);
sheet.cell("C" + (index + 3)).value(etudiant.nom);
let offset = 0;
Object.values(etudiant.criteres).forEach(critere => {
sheet.cell(changeChar("D", offset) + (index + 3)).value(critere);
offset++;
})
Object.values(etudiant.voeux).forEach(voeux => {
sheet.cell(changeChar("D", offset) + (index + 3)).value(voeux);
offset++;
})
})
sheet.column("A").width(5);
sheet.column("B").width(5);
sheet.column("C").width(20);
sheet.column("D").width(20);
sheet.column("E").width(20);
sheet.column("F").width(20);
sheet.column("G").width(20);
sheet.column("H").width(20);
sheet.column("I").width(20);
sheet.column("J").width(20);
sheet.column("K").width(20);
sheet.column("L").width(20);
saveFile("Données groupes - " + formsemestre, workbook);
});
}
async function exportResults() {
XlsxPopulate.fromBlankAsync()
.then(workbook => {
const sheet = workbook.sheet(0);
sheet.name("Résultats");
let colonne = 1;
Object.values(parcours).forEach((dataParcours, index) => {
let ligne = 1;
sheet.row(ligne++).cell(colonne).value(dataParcours.nom).style("bold", true);
sheet.row(ligne).cell(colonne).value("id").style("bold", true);
sheet.row(ligne++).cell(colonne + 1).value("Nom").style("bold", true).column().width(30);
dataParcours.etudiants.forEach(etudiant => {
sheet.row(ligne).cell(colonne).value(etudiant.slice(2));
sheet.row(ligne++).cell(colonne + 1).value(etudiants[etudiant].nom);
})
colonne += 3;
})
saveFile("Résultats groupes - " + formsemestre, workbook);
});
}
</script>