Edition en ligne des codes étapes Apogée des semestre

This commit is contained in:
Emmanuel Viennet 2022-04-16 15:34:40 +02:00
parent 2dbaacf460
commit 2e6e7675bf
13 changed files with 299 additions and 47 deletions

View File

@ -146,6 +146,7 @@ class Formation(db.Model):
db.session.add(ue) db.session.add(ue)
db.session.commit() db.session.commit()
if change:
app.clear_scodoc_cache() app.clear_scodoc_cache()

View File

@ -286,7 +286,7 @@ class FormSemestre(db.Model):
""" """
if not self.etapes: if not self.etapes:
return "" return ""
return ", ".join([str(x.etape_apo) for x in self.etapes]) return ", ".join(sorted([str(x.etape_apo) for x in self.etapes]))
def responsables_str(self, abbrev_prenom=True) -> str: def responsables_str(self, abbrev_prenom=True) -> str:
"""chaîne "J. Dupond, X. Martin" """chaîne "J. Dupond, X. Martin"
@ -433,7 +433,7 @@ notes_formsemestre_responsables = db.Table(
class FormSemestreEtape(db.Model): class FormSemestreEtape(db.Model):
"""Étape Apogée associées au semestre""" """Étape Apogée associée au semestre"""
__tablename__ = "notes_formsemestre_etapes" __tablename__ = "notes_formsemestre_etapes"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)

View File

@ -121,6 +121,7 @@ class GenTable(object):
html_with_td_classes=False, # put class=column_id in each <td> html_with_td_classes=False, # put class=column_id in each <td>
html_before_table="", # html snippet to put before the <table> in the page html_before_table="", # html snippet to put before the <table> in the page
html_empty_element="", # replace table when empty html_empty_element="", # replace table when empty
html_table_attrs="", # for html
base_url=None, base_url=None,
origin=None, # string added to excel and xml versions origin=None, # string added to excel and xml versions
filename="table", # filename, without extension filename="table", # filename, without extension
@ -146,6 +147,7 @@ class GenTable(object):
self.html_header = html_header self.html_header = html_header
self.html_before_table = html_before_table self.html_before_table = html_before_table
self.html_empty_element = html_empty_element self.html_empty_element = html_empty_element
self.html_table_attrs = html_table_attrs
self.page_title = page_title self.page_title = page_title
self.pdf_link = pdf_link self.pdf_link = pdf_link
self.xls_link = xls_link self.xls_link = xls_link
@ -413,8 +415,7 @@ class GenTable(object):
cls = ' class="%s"' % " ".join(tablclasses) cls = ' class="%s"' % " ".join(tablclasses)
else: else:
cls = "" cls = ""
H = [self.html_before_table, f"<table{hid}{cls} {self.html_table_attrs}>"]
H = [self.html_before_table, "<table%s%s>" % (hid, cls)]
line_num = 0 line_num = 0
# thead # thead

View File

@ -29,6 +29,7 @@
""" """
from flask import g, request from flask import g, request
from flask import url_for
from flask_login import current_user from flask_login import current_user
import app import app
@ -79,7 +80,7 @@ def index_html(showcodes=0, showsemtable=0):
sco_formsemestre.sem_set_responsable_name(sem) sco_formsemestre.sem_set_responsable_name(sem)
if showcodes: if showcodes:
sem["tmpcode"] = "<td><tt>%s</tt></td>" % sem["formsemestre_id"] sem["tmpcode"] = f"<td><tt>{sem['formsemestre_id']}</tt></td>"
else: else:
sem["tmpcode"] = "" sem["tmpcode"] = ""
# Nombre d'inscrits: # Nombre d'inscrits:
@ -121,26 +122,27 @@ def index_html(showcodes=0, showsemtable=0):
if showsemtable: if showsemtable:
H.append( H.append(
"""<hr/> f"""<hr>
<h2>Semestres de %s</h2> <h2>Semestres de {sco_preferences.get_preference("DeptName")}</h2>
""" """
% sco_preferences.get_preference("DeptName")
) )
H.append(_sem_table_gt(sems, showcodes=showcodes).html()) H.append(_sem_table_gt(sems, showcodes=showcodes).html())
H.append("</table>") H.append("</table>")
if not showsemtable: if not showsemtable:
H.append( H.append(
'<hr/><p><a href="%s?showsemtable=1">Voir tous les semestres</a></p>' f"""<hr>
% request.base_url <p><a class="stdlink" href="{url_for('scolar.index_html', scodoc_dept=g.scodoc_dept, showsemtable=1)
}">Voir tous les semestres ({len(othersems)} verrouillés)</a>
</p>"""
) )
H.append( H.append(
"""<p><form action="%s/view_formsemestre_by_etape"> f"""<p>
Chercher étape courante: <input name="etape_apo" type="text" size="8" spellcheck="false"></input> <form action="{url_for('notes.view_formsemestre_by_etape', scodoc_dept=g.scodoc_dept)}">
</form Chercher étape courante:
</p> <input name="etape_apo" type="text" size="8" spellcheck="false"></input>
""" </form>
% scu.NotesURL() </p>"""
) )
# #
if current_user.has_permission(Permission.ScoEtudInscrit): if current_user.has_permission(Permission.ScoEtudInscrit):
@ -148,23 +150,26 @@ Chercher étape courante: <input name="etape_apo" type="text" size="8" spellchec
"""<hr> """<hr>
<h3>Gestion des étudiants</h3> <h3>Gestion des étudiants</h3>
<ul> <ul>
<li><a class="stdlink" href="etudident_create_form">créer <em>un</em> nouvel étudiant</a></li> <li><a class="stdlink" href="etudident_create_form">créer <em>un</em> nouvel étudiant</a>
<li><a class="stdlink" href="form_students_import_excel">importer de nouveaux étudiants</a> (ne pas utiliser sauf cas particulier, utilisez plutôt le lien dans </li>
<li><a class="stdlink" href="form_students_import_excel">importer de nouveaux étudiants</a>
(ne pas utiliser sauf cas particulier, utilisez plutôt le lien dans
le tableau de bord semestre si vous souhaitez inscrire les le tableau de bord semestre si vous souhaitez inscrire les
étudiants importés à un semestre)</li> étudiants importés à un semestre)
</li>
</ul> </ul>
""" """
) )
# #
if current_user.has_permission(Permission.ScoEditApo): if current_user.has_permission(Permission.ScoEditApo):
H.append( H.append(
"""<hr> f"""<hr>
<h3>Exports Apogée</h3> <h3>Exports Apogée</h3>
<ul> <ul>
<li><a class="stdlink" href="%s/semset_page">Années scolaires / exports Apogée</a></li> <li><a class="stdlink" href="{url_for('notes.semset_page', scodoc_dept=g.scodoc_dept)
}">Années scolaires / exports Apogée</a></li>
</ul> </ul>
""" """
% scu.NotesURL()
) )
# #
H.append( H.append(
@ -176,7 +181,13 @@ Chercher étape courante: <input name="etape_apo" type="text" size="8" spellchec
""" """
) )
# #
return html_sco_header.sco_header() + "\n".join(H) + html_sco_header.sco_footer() return (
html_sco_header.sco_header(
page_title=f"ScoDoc {g.scodoc_dept}", javascripts=["js/scolar_index.js"]
)
+ "\n".join(H)
+ html_sco_header.sco_footer()
)
def _sem_table(sems): def _sem_table(sems):
@ -213,7 +224,9 @@ def _sem_table(sems):
def _sem_table_gt(sems, showcodes=False): def _sem_table_gt(sems, showcodes=False):
"""Nouvelle version de la table des semestres""" """Nouvelle version de la table des semestres
Utilise une datatables.
"""
_style_sems(sems) _style_sems(sems)
columns_ids = ( columns_ids = (
"lockimg", "lockimg",
@ -236,14 +249,16 @@ def _sem_table_gt(sems, showcodes=False):
"mois_debut": "Début", "mois_debut": "Début",
"dash_mois_fin": "Année", "dash_mois_fin": "Année",
"titre_resp": "Semestre", "titre_resp": "Semestre",
"nb_inscrits": "N", # groupicon, "nb_inscrits": "N",
}, },
columns_ids=columns_ids, columns_ids=columns_ids,
rows=sems, rows=sems,
html_class="table_leftalign semlist", table_id="semlist",
html_class_ignore_default=True,
html_class="stripe cell-border compact hover order-column table_leftalign semlist",
html_sortable=True, html_sortable=True,
# base_url = '%s?formsemestre_id=%s' % (request.base_url, formsemestre_id), html_table_attrs=f"""data-apo_save_url="{url_for('notes.formsemestre_set_apo_etapes', scodoc_dept=g.scodoc_dept)}" """,
# caption='Maquettes enregistrées', html_with_td_classes=True,
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
) )
@ -276,6 +291,10 @@ def _style_sems(sems):
sem["semestre_id_n"] = "" sem["semestre_id_n"] = ""
else: else:
sem["semestre_id_n"] = sem["semestre_id"] sem["semestre_id_n"] = sem["semestre_id"]
# pour édition codes Apogée:
sem[
"_etapes_apo_str_td_attrs"
] = f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['etapes_apo_str']}" """
def delete_dept(dept_id: int): def delete_dept(dept_id: int):

View File

@ -34,8 +34,6 @@ from zipfile import ZipFile
import flask import flask
from flask import url_for, g, send_file, request from flask import url_for, g, send_file, request
# from werkzeug.utils import send_file
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app import log from app import log
from app.scodoc import html_sco_header from app.scodoc import html_sco_header

View File

@ -141,7 +141,6 @@ def _formsemestre_enrich(sem):
"""Ajoute champs souvent utiles: titre + annee et dateord (pour tris)""" """Ajoute champs souvent utiles: titre + annee et dateord (pour tris)"""
# imports ici pour eviter refs circulaires # imports ici pour eviter refs circulaires
from app.scodoc import sco_formsemestre_edit from app.scodoc import sco_formsemestre_edit
from app.scodoc import sco_etud
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0] F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"]) parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
@ -350,6 +349,7 @@ def read_formsemestre_etapes(formsemestre_id): # OBSOLETE
"""SELECT etape_apo """SELECT etape_apo
FROM notes_formsemestre_etapes FROM notes_formsemestre_etapes
WHERE formsemestre_id = %(formsemestre_id)s WHERE formsemestre_id = %(formsemestre_id)s
ORDER BY etape_apo
""", """,
{"formsemestre_id": formsemestre_id}, {"formsemestre_id": formsemestre_id},
) )

View File

@ -728,15 +728,13 @@ def sendResult(
def send_file(data, filename="", suffix="", mime=None, attached=None): def send_file(data, filename="", suffix="", mime=None, attached=None):
"""Build Flask Response for file download of given type """Build Flask Response for file download of given type
By default (attached is None), json and xml are inlined and otrher types are attached. By default (attached is None), json and xml are inlined and other types are attached.
""" """
if attached is None: if attached is None:
if mime == XML_MIMETYPE or mime == JSON_MIMETYPE: if mime == XML_MIMETYPE or mime == JSON_MIMETYPE:
attached = False attached = False
else: else:
attached = True attached = True
# if attached and not filename:
# raise ValueError("send_file: missing attachement filename")
if filename: if filename:
if suffix: if suffix:
filename += suffix filename += suffix
@ -755,7 +753,7 @@ def send_docx(document, filename):
buffer.seek(0) buffer.seek(0)
return flask.send_file( return flask.send_file(
buffer, buffer,
attachment_filename=sanitize_filename(filename), download_name=sanitize_filename(filename),
mimetype=DOCX_MIMETYPE, mimetype=DOCX_MIMETYPE,
) )

View File

@ -427,8 +427,8 @@ table.semlist tr td {
border: none; border: none;
} }
table.semlist tr a.stdlink, table.semlist tbody tr a.stdlink,
table.semlist tr a.stdlink:visited { table.semlist tbody tr a.stdlink:visited {
color: navy; color: navy;
text-decoration: none; text-decoration: none;
} }
@ -442,32 +442,86 @@ table.semlist tr td.semestre_id {
text-align: right; text-align: right;
} }
table.semlist tr td.modalite { table.semlist tbody tr td.modalite {
text-align: left; text-align: left;
padding-right: 1em; padding-right: 1em;
} }
div#gtrcontent table.semlist tr.css_S-1 { /***************************/
/* Statut des cellules */
/***************************/
.sco_selected {
outline: 1px solid #c09;
}
.sco_modifying {
outline: 2px dashed #c09;
background-color: white !important;
}
.sco_wait {
outline: 2px solid #c90;
}
.sco_good {
outline: 2px solid #9c0;
}
.sco_modified {
font-weight: bold;
color: indigo
}
/***************************/
/* Message */
/***************************/
.message {
position: fixed;
bottom: 100%;
left: 50%;
z-index: 10;
padding: 20px;
border-radius: 0 0 10px 10px;
background: #ec7068;
background: #90c;
color: #FFF;
font-size: 24px;
animation: message 3s;
transform: translate(-50%, 0);
}
@keyframes message {
20% {
transform: translate(-50%, 100%)
}
80% {
transform: translate(-50%, 100%)
}
}
div#gtrcontent table.semlist tbody tr.css_S-1 td {
background-color: rgb(251, 250, 216); background-color: rgb(251, 250, 216);
} }
div#gtrcontent table.semlist tr.css_S1 { div#gtrcontent table.semlist tbody tr.css_S1 td {
background-color: rgb(92%, 95%, 94%); background-color: rgb(92%, 95%, 94%);
} }
div#gtrcontent table.semlist tr.css_S2 { div#gtrcontent table.semlist tbody tr.css_S2 td {
background-color: rgb(214, 223, 236); background-color: rgb(214, 223, 236);
} }
div#gtrcontent table.semlist tr.css_S3 { div#gtrcontent table.semlist tbody tr.css_S3 td {
background-color: rgb(167, 216, 201); background-color: rgb(167, 216, 201);
} }
div#gtrcontent table.semlist tr.css_S4 { div#gtrcontent table.semlist tbody tr.css_S4 td {
background-color: rgb(131, 225, 140); background-color: rgb(131, 225, 140);
} }
div#gtrcontent table.semlist tr.css_MEXT { div#gtrcontent table.semlist tbody tr.css_MEXT td {
color: #0b6e08; color: #0b6e08;
} }

View File

@ -133,3 +133,134 @@ function readOnlyTags(nodes) {
node.after('<span class="ro_tags"><span class="ro_tag">' + tags.join('</span><span class="ro_tag">') + '</span></span>'); node.after('<span class="ro_tags"><span class="ro_tag">' + tags.join('</span><span class="ro_tag">') + '</span></span>');
} }
} }
/* Editeur pour champs
* Usage: créer un élément avec data-oid (object id)
* La méthode d'URL save sera appelée en POST avec deux arguments: oid et value,
* value contenant la valeur du champs.
* Inspiré par les codes et conseils de Seb. L.
*/
class ScoFieldEditor {
constructor(selector, save_url, read_only) {
this.save_url = save_url;
this.read_only = read_only;
this.selector = selector;
this.installListeners();
}
// Enregistre l'élément obj
save(obj) {
var value = obj.innerText.trim();
if (value.length == 0) {
value = "";
}
if (value == obj.dataset.value) {
return true; // Aucune modification, pas d'enregistrement mais on continue normalement
}
obj.classList.add("sco_wait");
// DEBUG
// console.log(`
// data : ${value},
// id: ${obj.dataset.oid}
// `);
$.post(this.save_url,
{
oid: obj.dataset.oid,
value: value,
},
function (result) {
obj.classList.remove("sco_wait");
obj.classList.add("sco_modified");
}
);
return true;
}
/*****************************/
/* Gestion des évènements */
/*****************************/
installListeners() {
if (this.read_only) {
return;
}
document.body.addEventListener("keydown", this.key);
let editor = this;
this.handleSelectCell = (event) => { editor.selectCell(event) };
this.handleModifCell = (event) => { editor.modifCell(event) };
this.handleBlur = (event) => { editor.blurCell(event) };
this.handleKeyCell = (event) => { editor.keyCell(event) };
document.querySelectorAll(this.selector).forEach(cellule => {
cellule.addEventListener("click", this.handleSelectCell);
cellule.addEventListener("dblclick", this.handleModifCell);
cellule.addEventListener("blur", this.handleBlur);
});
}
/*********************************/
/* Interaction avec les cellules */
/*********************************/
blurCell(event) {
let currentModif = document.querySelector(".sco_modifying");
if (currentModif) {
if (!this.save(currentModif)) {
return;
}
}
}
selectCell(event) {
let obj = event.currentTarget;
if (obj) {
if (obj.classList.contains("sco_modifying")) {
return; // Cellule en cours de modification, ne pas sélectionner.
}
let currentModif = document.querySelector(".sco_modifying");
if (currentModif) {
if (!this.save(currentModif)) {
return;
}
}
this.unselectCell();
obj.classList.add("sco_selected");
}
}
unselectCell() {
document.querySelectorAll(".sco_selected, .sco_modifying").forEach(cellule => {
cellule.classList.remove("sco_selected", "sco_modifying");
cellule.removeAttribute("contentEditable");
cellule.removeEventListener("keydown", this.handleKeyCell);
});
}
modifCell(event) {
let obj = event.currentTarget;
if (obj) {
obj.classList.add("sco_modifying");
obj.contentEditable = true;
obj.addEventListener("keydown", this.handleKeyCell);
obj.focus();
}
}
key(event) {
switch (event.key) {
case "Enter":
this.modifCell(document.querySelector(".sco_selected"));
event.preventDefault();
break;
}
}
keyCell(event) {
let obj = event.currentTarget;
if (obj) {
if (event.key == "Enter") {
event.preventDefault();
event.stopPropagation();
if (!this.save(obj)) {
return
}
obj.classList.remove("sco_modifying");
// ArrowMove(0, 1);
// modifCell(document.querySelector(".sco_selected"));
this.unselectCell();
}
}
}
}

View File

@ -0,0 +1,22 @@
/* Page accueil département */
var apo_editor = null;
$(document).ready(function () {
var table_options = {
"paging": false,
"searching": false,
"info": false,
/* "autoWidth" : false, */
"fixedHeader": {
"header": true,
"footer": true
},
"orderCellsTop": true, // cellules ligne 1 pour tri
"aaSorting": [], // Prevent initial sorting
};
$('table.semlist').DataTable(table_options);
let apo_save_url = document.querySelector("table#semlist").dataset.apo_save_url;
apo_editor = new ScoFieldEditor(".etapes_apo_str", apo_save_url, false);
});

View File

@ -2410,6 +2410,33 @@ sco_publish(
Permission.ScoEditApo, Permission.ScoEditApo,
) )
@bp.route("/formsemestre_set_apo_etapes", methods=["POST"])
@scodoc
@permission_required(Permission.ScoEditApo)
def formsemestre_set_apo_etapes():
"""Change les codes étapes du semestre indiqué.
Args: oid=formsemestre_id, value=chaine "V1RT, V1RT2", codes séparés par des virgules
"""
formsemestre_id = int(request.form.get("oid"))
etapes_apo_str = request.form.get("value")
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
current_etapes = {e.etape_apo for e in formsemestre.etapes}
new_etapes = {s.strip() for s in etapes_apo_str.split(",")}
if new_etapes != current_etapes:
formsemestre.etapes = []
for etape_apo in new_etapes:
etape = models.FormSemestreEtape(
formsemestre_id=formsemestre_id, etape_apo=etape_apo
)
formsemestre.etapes.append(etape)
db.session.add(formsemestre)
db.session.commit()
return ("", 204)
# sco_semset # sco_semset
sco_publish("/semset_page", sco_semset.semset_page, Permission.ScoEditApo) sco_publish("/semset_page", sco_semset.semset_page, Permission.ScoEditApo)
sco_publish( sco_publish(

View File

@ -327,6 +327,7 @@ def showEtudLog(etudid, format="html"):
@bp.route("/") @bp.route("/")
@bp.route("/index_html") @bp.route("/index_html")
@bp.route("/index")
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@scodoc7func @scodoc7func

View File

@ -1,7 +1,7 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.2.2" SCOVERSION = "9.2.3"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"