Accepte codage boursiers O/N (USPN), fonction de resynchro globale, table de tous les étudiants courants avec état boursier.

This commit is contained in:
Emmanuel Viennet 2023-11-26 18:28:56 +01:00
parent 60109bb513
commit 568c8681ba
8 changed files with 397 additions and 139 deletions

View File

@ -271,23 +271,11 @@ def dept_formsemestres_courants(acronym: str):
"""
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
date_courante = request.args.get("date_courante")
if date_courante:
test_date = datetime.fromisoformat(date_courante)
else:
test_date = app.db.func.now()
# Les semestres en cours de ce département
formsemestres = FormSemestre.query.filter(
FormSemestre.dept_id == dept.id,
FormSemestre.date_debut <= test_date,
FormSemestre.date_fin >= test_date,
)
date_courante = datetime.fromisoformat(date_courante) if date_courante else None
return [
d.to_dict_api()
for d in formsemestres.order_by(
FormSemestre.date_debut.desc(),
FormSemestre.modalite,
FormSemestre.semestre_id,
FormSemestre.titre,
formsemestre.to_dict_api()
for formsemestre in FormSemestre.get_dept_formsemestres_courants(
dept, date_courante
)
]

View File

@ -30,6 +30,7 @@ from app.models.but_refcomp import (
parcours_formsemestre,
)
from app.models.config import ScoDocSiteConfig
from app.models.departements import Departement
from app.models.etudiants import Identite
from app.models.evaluations import Evaluation
from app.models.formations import Formation
@ -521,7 +522,7 @@ class FormSemestre(db.Model):
mois_pivot_periode=scu.MONTH_DEBUT_PERIODE2,
jour_pivot_annee=1,
jour_pivot_periode=1,
):
) -> tuple[int, int]:
"""Calcule la session associée à un formsemestre commençant en date_debut
sous la forme (année, période)
année: première année de l'année scolaire
@ -571,6 +572,26 @@ class FormSemestre(db.Model):
mois_pivot_periode=ScoDocSiteConfig.get_month_debut_periode2(),
)
@classmethod
def get_dept_formsemestres_courants(
cls, dept: Departement, date_courante: datetime.datetime | None = None
) -> db.Query:
"""Liste (query) ordonnée des formsemestres courants, c'est
à dire contenant la date courant (si None, la date actuelle)"""
date_courante = date_courante or db.func.now()
# Les semestres en cours de ce département
formsemestres = FormSemestre.query.filter(
FormSemestre.dept_id == dept.id,
FormSemestre.date_debut <= date_courante,
FormSemestre.date_fin >= date_courante,
)
return formsemestres.order_by(
FormSemestre.date_debut.desc(),
FormSemestre.modalite,
FormSemestre.semestre_id,
FormSemestre.titre,
)
def etapes_apo_vdi(self) -> list[ApoEtapeVDI]:
"Liste des vdis"
# was read_formsemestre_etapes

View File

@ -149,21 +149,49 @@ def index_html(showcodes=0, showsemtable=0):
</p>"""
)
#
if current_user.has_permission(Permission.EtudInscrit):
H.append(
"""<hr>
<h3>Gestion des étudiants</h3>
<ul>
<li><a class="stdlink" href="etudident_create_form">créer <em>un</em> nouvel étudiant</a>
"""
)
if current_user.has_permission(Permission.EtudInscrit):
H.append(
f"""
<li><a class="stdlink" href="{
url_for("scolar.etudident_create_form", scodoc_dept=g.scodoc_dept)
}">créer <em>un</em> nouvel étudiant</a>
</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
<li><a class="stdlink" href="{
url_for("scolar.form_students_import_excel", scodoc_dept=g.scodoc_dept)
}">importer de nouveaux étudiants</a>
(<em>ne pas utiliser</em> sauf cas particulier&nbsp;: utilisez plutôt le lien dans
le tableau de bord semestre si vous souhaitez inscrire les
étudiants importés à un semestre)
</li>
</ul>
"""
)
H.append(
f"""
<li><a class="stdlink" href="{
url_for("scolar.export_etudiants_courants", scodoc_dept=g.scodoc_dept)
}">exporter tableau des étudiants des semestres en cours</a>
</li>
"""
)
if current_user.has_permission(
Permission.EtudInscrit
) and sco_preferences.get_preference("portal_url"):
H.append(
f"""
<li><a class="stdlink" href="{
url_for("scolar.formsemestre_import_etud_admission",
scodoc_dept=g.scodoc_dept, tous_courants=1)
}">resynchroniser les données étudiants des semestres en cours depuis le portail</a>
</li>
"""
)
H.append("</ul>")
#
if current_user.has_permission(Permission.EditApogee):
H.append(

View File

@ -793,21 +793,25 @@ def update_etape_formsemestre_inscription(ins, etud):
def formsemestre_import_etud_admission(
formsemestre_id, import_identite=True, import_email=False
):
formsemestre_id: int, import_identite=True, import_email=False
) -> tuple[list[Identite], list[Identite], list[tuple[Identite, str]]]:
"""Tente d'importer les données admission depuis le portail
pour tous les étudiants du semestre.
Si import_identite==True, recopie l'identité (nom/prenom/sexe/date_naissance)
de chaque étudiant depuis le portail.
N'affecte pas les etudiants inconnus sur le portail.
Renvoie:
- etuds_no_nip: liste d'étudiants sans code NIP
- etuds_unknown: etudiants avec NIP mais inconnus du portail
- changed_mails: (etudiant, old_mail) pour ceux dont le mail a changé
"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
{"formsemestre_id": formsemestre_id}
)
log(f"formsemestre_import_etud_admission: {formsemestre_id} ({len(ins)} etuds)")
no_nip = [] # liste d'etudids sans code NIP
unknowns = [] # etudiants avec NIP mais inconnus du portail
etuds_no_nip: list[Identite] = []
etuds_unknown: list[Identite] = []
changed_mails: list[tuple[Identite, str]] = [] # modification d'adresse mails
# Essaie de recuperer les etudiants des étapes, car
@ -828,7 +832,7 @@ def formsemestre_import_etud_admission(
etud: Identite = Identite.query.get_or_404(etudid)
code_nip = etud.code_nip
if not code_nip:
no_nip.append(etudid)
etuds_no_nip.append(etud)
else:
data_apo = apo_etuds.get(code_nip)
if not data_apo:
@ -865,7 +869,7 @@ def formsemestre_import_etud_admission(
if adresse.email != data_apo["mail"]:
changed_mails.append((etud, old_mail))
else:
unknowns.append(code_nip)
etuds_unknown.append(etud)
db.session.commit()
sco_cache.invalidate_formsemestre(formsemestre_id=sem["formsemestre_id"])
return no_nip, unknowns, changed_mails
return etuds_no_nip, etuds_unknown, changed_mails

View File

@ -419,7 +419,13 @@ table.dataTable td {
color: #333333 !important;
border: 1px solid #979797;
background-color: white;
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, white), color-stop(100%, gainsboro));
background: -webkit-gradient(
linear,
left top,
left bottom,
color-stop(0%, white),
color-stop(100%, gainsboro)
);
/* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, white 0%, gainsboro 100%);
/* Chrome10+,Safari5.1+ */
@ -447,7 +453,13 @@ table.dataTable td {
color: white !important;
border: 1px solid #111111;
background-color: #585858;
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #585858), color-stop(100%, #111111));
background: -webkit-gradient(
linear,
left top,
left bottom,
color-stop(0%, #585858),
color-stop(100%, #111111)
);
/* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, #585858 0%, #111111 100%);
/* Chrome10+,Safari5.1+ */
@ -464,7 +476,13 @@ table.dataTable td {
.dataTables_wrapper .dataTables_paginate .paginate_button:active {
outline: none;
background-color: #2b2b2b;
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #2b2b2b), color-stop(100%, #0c0c0c));
background: -webkit-gradient(
linear,
left top,
left bottom,
color-stop(0%, #2b2b2b),
color-stop(100%, #0c0c0c)
);
/* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
/* Chrome10+,Safari5.1+ */
@ -495,12 +513,50 @@ table.dataTable td {
text-align: center;
font-size: 1.2em;
background-color: white;
background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255, 255, 255, 0)), color-stop(25%, rgba(255, 255, 255, 0.9)), color-stop(75%, rgba(255, 255, 255, 0.9)), color-stop(100%, rgba(255, 255, 255, 0)));
background: -webkit-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
background: -moz-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
background: -ms-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
background: -o-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
background: -webkit-gradient(
linear,
left top,
right top,
color-stop(0%, rgba(255, 255, 255, 0)),
color-stop(25%, rgba(255, 255, 255, 0.9)),
color-stop(75%, rgba(255, 255, 255, 0.9)),
color-stop(100%, rgba(255, 255, 255, 0))
);
background: -webkit-linear-gradient(
left,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.9) 25%,
rgba(255, 255, 255, 0.9) 75%,
rgba(255, 255, 255, 0) 100%
);
background: -moz-linear-gradient(
left,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.9) 25%,
rgba(255, 255, 255, 0.9) 75%,
rgba(255, 255, 255, 0) 100%
);
background: -ms-linear-gradient(
left,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.9) 25%,
rgba(255, 255, 255, 0.9) 75%,
rgba(255, 255, 255, 0) 100%
);
background: -o-linear-gradient(
left,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.9) 25%,
rgba(255, 255, 255, 0.9) 75%,
rgba(255, 255, 255, 0) 100%
);
background: linear-gradient(
to right,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.9) 25%,
rgba(255, 255, 255, 0.9) 75%,
rgba(255, 255, 255, 0) 100%
);
}
.dataTables_wrapper .dataTables_length,
@ -520,17 +576,69 @@ table.dataTable td {
-webkit-overflow-scrolling: touch;
}
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th,
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td,
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th,
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td {
.dataTables_wrapper
.dataTables_scroll
div.dataTables_scrollBody
> table
> thead
> tr
> th,
.dataTables_wrapper
.dataTables_scroll
div.dataTables_scrollBody
> table
> thead
> tr
> td,
.dataTables_wrapper
.dataTables_scroll
div.dataTables_scrollBody
> table
> tbody
> tr
> th,
.dataTables_wrapper
.dataTables_scroll
div.dataTables_scrollBody
> table
> tbody
> tr
> td {
vertical-align: middle;
}
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th>div.dataTables_sizing,
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td>div.dataTables_sizing,
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th>div.dataTables_sizing,
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td>div.dataTables_sizing {
.dataTables_wrapper
.dataTables_scroll
div.dataTables_scrollBody
> table
> thead
> tr
> th
> div.dataTables_sizing,
.dataTables_wrapper
.dataTables_scroll
div.dataTables_scrollBody
> table
> thead
> tr
> td
> div.dataTables_sizing,
.dataTables_wrapper
.dataTables_scroll
div.dataTables_scrollBody
> table
> tbody
> tr
> th
> div.dataTables_sizing,
.dataTables_wrapper
.dataTables_scroll
div.dataTables_scrollBody
> table
> tbody
> tr
> td
> div.dataTables_sizing {
height: 0;
overflow: hidden;
margin: 0 !important;
@ -555,7 +663,6 @@ table.dataTable td {
}
@media screen and (max-width: 767px) {
.dataTables_wrapper .dataTables_info,
.dataTables_wrapper .dataTables_paginate {
float: none;
@ -568,7 +675,6 @@ table.dataTable td {
}
@media screen and (max-width: 640px) {
.dataTables_wrapper .dataTables_length,
.dataTables_wrapper .dataTables_filter {
float: none;
@ -580,7 +686,6 @@ table.dataTable td {
}
}
/* ------ Ajouts spécifiques pour ScoDoc:
*/
@ -624,7 +729,6 @@ table.dataTable.stripe.hover tbody tr.odd:hover td,
table.dataTable.stripe.hover tbody tr.odd:hover td.sorting_1,
table.dataTable.order-column.stripe.hover tbody tr.odd:hover td.sorting_1 {
background-color: rgb(80%, 85%, 80%);
;
}
/* Lignes paires */
@ -638,7 +742,6 @@ table.dataTable.stripe.hover tbody tr.even:hover td,
table.dataTable.stripe.hover tbody tr.even:hover td.sorting_1,
table.dataTable.order-column.stripe.hover tbody tr.even:hover td.sorting_1 {
background-color: rgb(85%, 85%, 85%);
;
}
/* Reglage largeur de la table */
@ -653,3 +756,8 @@ table.dataTable.gt_table {
table.dataTable.gt_table.gt_left {
margin-left: 16px;
}
table.dataTable.gt_table.gt_left td,
table.dataTable.gt_table.gt_left th {
text-align: left;
}
scodoc;css

View File

@ -1133,7 +1133,8 @@ a.redlink:hover {
text-decoration: underline;
}
a.discretelink {
a.discretelink,
a:discretelink:visited {
color: black;
text-decoration: none;
}
@ -1567,6 +1568,7 @@ h2.formsemestre,
#gtrcontent h2 {
margin-top: 2px;
font-size: 130%;
font-weight: bold;
}
.formsemestre_page_title table.semtitle,

View File

@ -7,8 +7,7 @@
"""Liste simple d'étudiants
"""
from flask import g, url_for
from app.models import Identite
from app.models import FormSemestre, FormSemestreInscription, Identite
from app.tables import table_builder as tb
@ -26,6 +25,7 @@ class TableEtud(tb.Table):
with_foot_titles=False,
**kwargs,
):
etuds = etuds or []
self.rows: list["RowEtud"] = [] # juste pour que VSCode nous aide sur .rows
classes = classes or ["gt_table", "gt_left"]
super().__init__(
@ -46,10 +46,12 @@ class TableEtud(tb.Table):
class RowEtud(tb.Row):
"Ligne de la table d'étudiants"
# pour le moment très simple, extensible (codes, liens bulletins, ...)
def __init__(self, table: TableEtud, etud: Identite, *args, **kwargs):
super().__init__(table, etud.id, *args, **kwargs)
self.etud = etud
self.target_url = etud.url_fiche()
def add_etud_cols(self):
"""Ajoute colonnes étudiant: codes, noms"""
@ -85,7 +87,7 @@ class RowEtud(tb.Row):
etud.nom_disp(),
"identite_detail",
data={"order": etud.sort_key},
target=url_bulletin,
target=self.target_url,
target_attrs={"class": "etudinfo discretelink", "id": str(etud.id)},
)
self.add_cell("prenom", "Prénom", etud.prenom, "identite_detail")
@ -115,3 +117,70 @@ def html_table_etuds(etudids) -> str:
etuds = etuds_sorted_from_ids(etudids)
table = TableEtud(etuds)
return table.html()
class RowEtudWithInfos(RowEtud):
"""Ligne de la table d'étudiants avec plus d'informations:
département, formsemestre, codes, boursier
"""
def __init__(
self,
table: TableEtud,
etud: Identite,
formsemestre: FormSemestre,
inscription: FormSemestreInscription,
*args,
**kwargs,
):
super().__init__(table, etud, *args, **kwargs)
self.formsemestre = formsemestre
self.inscription = inscription
def add_etud_cols(self):
"""Ajoute colonnes étudiant: codes, noms"""
self.add_cell("dept", "Dépt.", self.etud.departement.acronym, "identite_detail")
self.add_cell(
"formsemestre",
"Semestre",
f"""{self.formsemestre.titre_formation()} {
('S'+str(self.formsemestre.semestre_id))
if self.formsemestre.semestre_id > 0 else ''}
""",
"identite_detail",
)
super().add_etud_cols()
self.add_cell(
"etat",
"État",
self.inscription.etat,
"inscription",
)
self.add_cell(
"boursier",
"Boursier",
"O" if self.etud.boursier else "N",
"identite_infos",
)
class TableEtudWithInfos(TableEtud):
"""Table d'étudiants avec formsemestre et inscription"""
def add_formsemestre(self, formsemestre: FormSemestre):
"Ajoute les étudiants de ce semestre à la table"
etuds = formsemestre.get_inscrits(order=True, include_demdef=True)
for etud in etuds:
row = self.row_class(
self, etud, formsemestre, formsemestre.etuds_inscriptions.get(etud.id)
)
row.add_etud_cols()
self.add_row(row)
def table_etudiants_courants(formsemestres: list[FormSemestre]) -> TableEtud:
"""Table des étudiants des formsemestres indiqués"""
table = TableEtudWithInfos(row_class=RowEtudWithInfos)
for formsemestre in formsemestres:
table.add_formsemestre(formsemestre)
return table

View File

@ -105,6 +105,7 @@ from app.scodoc import sco_synchro_etuds
from app.scodoc import sco_trombino
from app.scodoc import sco_trombino_tours
from app.scodoc import sco_up_to_date
from app.tables import list_etuds
def sco_publish(route, function, permission, methods=["GET"]):
@ -2071,6 +2072,21 @@ def check_group_apogee(group_id, etat=None, fix=False, fixmail=False):
return "\n".join(H) + html_sco_header.sco_footer()
@bp.route("/export_etudiants_courants")
@scodoc
@permission_required(Permission.ScoView)
def export_etudiants_courants():
"""Table export de tous les étudiants des formsemestres en cours."""
departement = Departement.query.get(g.scodoc_dept_id)
if not departement:
raise ScoValueError("département invalide")
formsemestres = FormSemestre.get_dept_formsemestres_courants(departement)
table = list_etuds.table_etudiants_courants(formsemestres)
return render_template(
"scolar/export_etudiants_courants.j2", sco=ScoData(), table=table
)
@bp.route("/form_students_import_excel", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.EtudInscrit)
@ -2361,35 +2377,57 @@ def form_students_import_infos_admissions(formsemestre_id=None):
@scodoc
@permission_required(Permission.EtudChangeAdr)
@scodoc7func
def formsemestre_import_etud_admission(formsemestre_id, import_email=True):
"""Ré-importe donnees admissions par synchro Portail Apogée"""
def formsemestre_import_etud_admission(
formsemestre_id=None, import_email=True, tous_courants=False
):
"""Ré-importe donnees admissions par synchro Portail Apogée.
Si tous_courants, le fait pour tous les formsemestres courants du département
"""
if tous_courants:
departement = Departement.query.get(g.scodoc_dept_id)
formsemestres = FormSemestre.get_dept_formsemestres_courants(departement)
else:
formsemestres = [FormSemestre.get_formsemestre(formsemestre_id)]
diag_by_sem = {}
for formsemestre in formsemestres:
(
no_nip,
unknowns,
etuds_no_nip,
etuds_unknown,
changed_mails,
) = sco_synchro_etuds.formsemestre_import_etud_admission(
formsemestre_id, import_identite=True, import_email=import_email
)
H = [
html_sco_header.html_sem_header("Ré-import données admission"),
"<h3>Opération effectuée</h3>",
]
if no_nip:
H.append("<p>Attention: étudiants sans NIP: " + str(no_nip) + "</p>")
if unknowns:
H.append(
"<p>Attention: étudiants inconnus du portail: codes NIP="
+ str(unknowns)
+ "</p>"
formsemestre.id, import_identite=True, import_email=import_email
)
diag = ""
if etuds_no_nip:
diag += f"""<p>Attention: étudiants sans NIP:
{', '.join([e.html_link_fiche() for e in etuds_no_nip])}
</p>"""
if etuds_unknown:
diag += f"""<p>Attention: étudiants inconnus du portail:
{', '.join([(e.html_link_fiche() + ' (nip= ' + e.code_nip + ')')
for e in etuds_unknown])}
</p>"""
if changed_mails:
H.append("<h3>Adresses mails modifiées:</h3><ul>")
diag += """<p>Adresses mails modifiées:</p><ul>"""
for etud, old_mail in changed_mails:
H.append(
f"""<li>{etud.nom}: <tt>{old_mail}</tt> devient <tt>{etud.email}</tt></li>"""
)
H.append("</ul>")
return "\n".join(H) + html_sco_header.sco_footer()
diag += f"""<li>{etud.nom}: <tt>{old_mail}</tt> devient <tt>{etud.email}</tt></li>"""
diag += "</ul>"
diag_by_sem[formsemestre.id] = diag
return f"""
{ html_sco_header.html_sem_header("Ré-import données admission") }
<h3>Opération effectuée</h3>
<p>Sur le(s) semestres(s):</p>
<ul>
<li>
{ '</li><li>'.join( [(s.html_link_status() + diag_by_sem[s.id]) for s in formsemestres ]) }
</li>
</ul>
{ html_sco_header.sco_footer() }
"""
sco_publish(