Compare commits

...

15 Commits

Author SHA1 Message Date
c8ab9b9b6c Invalidation cache lors d'une erreur sur association UE/Niveau. Peut-être cause de #874. 2024-04-15 18:06:26 +02:00
ad7b48e110 Calendrier évaluations: fix #875 2024-04-15 17:53:02 +02:00
f2ce16f161 Archive PV: gzip large files 2024-04-15 03:21:32 +02:00
1ddf9b6ab8 Fix: création utilisateur si un seul département 2024-04-12 15:50:53 +02:00
0a2e39cae1 Ajoute aide sur édition parcours UEs 2024-04-12 01:10:42 +02:00
a194b4b6e0 Edition parcours UE: si tous cochés, tronc commun 2024-04-12 01:05:02 +02:00
cbe85dfb7d anonymize_users: ignore admin 2024-04-12 01:04:27 +02:00
beba69bfe4 Améliore/met à jour tests unitaires API 2024-04-11 06:00:00 +02:00
41fec29452 Bulletin BUT: ne mentionne pas les évaluations rattrapage/session2 sans notes. (c'est déjà le cas en classic) 2024-04-11 01:45:25 +02:00
9bd05ea241 Modify SCO_URL in all js: no trailing slash. 2024-04-11 01:44:17 +02:00
58b831513d Améliore traitement des erreurs lors de la génération des PDF 2024-04-10 15:29:30 +02:00
b861aba6a3 Tableaux génériques: possibilité de déclarer un colonne seulement pour excel. Assiduité: ajout etudid et NIP a visu_assi_group: closes #873. 2024-04-10 15:09:32 +02:00
c2443c361f Améliore page activation module entreprises. Implements #634 2024-04-09 00:36:46 +02:00
ab4731bd43 Suppression des anciennes fonctions ScoDoc7 donnant les URLs de base. 2024-04-08 18:57:00 +02:00
c17bc8b61b Fix: liste semestres avec code 2024-04-08 16:26:38 +02:00
70 changed files with 1926 additions and 1703 deletions

View File

@ -603,8 +603,19 @@ class Role(db.Model):
"""Create default roles if missing, then, if reset_permissions, """Create default roles if missing, then, if reset_permissions,
reset their permissions to default values. reset their permissions to default values.
""" """
Role.reset_roles_permissions(
SCO_ROLES_DEFAULTS, reset_permissions=reset_permissions
)
@staticmethod
def reset_roles_permissions(roles_perms: dict[str, tuple], reset_permissions=True):
"""Ajoute les permissions aux roles
roles_perms : { "role_name" : (permission, ...) }
reset_permissions : si vrai efface permissions déja existantes
Si le role n'existe pas, il est (re) créé.
"""
default_role = "Observateur" default_role = "Observateur"
for role_name, permissions in SCO_ROLES_DEFAULTS.items(): for role_name, permissions in roles_perms.items():
role = Role.query.filter_by(name=role_name).first() role = Role.query.filter_by(name=role_name).first()
if role is None: if role is None:
role = Role(name=role_name) role = Role(name=role_name)

View File

@ -37,7 +37,17 @@ def form_ue_choix_parcours(ue: UniteEns) -> str:
] ]
# Choix des parcours # Choix des parcours
ue_pids = [p.id for p in ue.parcours] ue_pids = [p.id for p in ue.parcours]
H.append("""<form id="choix_parcours">""") H.append(
"""
<div class="help">
Cocher tous les parcours dans lesquels cette UE est utilisée,
même si vous n'offrez pas ce parcours dans votre département.
Sans quoi, les UEs de Tronc Commun ne seront pas reconnues.
Ne cocher aucun parcours est équivalent à tous les cocher.
</div>
<form id="choix_parcours" style="margin-top: 12px;">
"""
)
ects_differents = { ects_differents = {
ue.get_ects(parcour, only_parcours=True) for parcour in ref_comp.parcours ue.get_ects(parcour, only_parcours=True) for parcour in ref_comp.parcours

View File

@ -9,12 +9,14 @@
import collections import collections
import datetime import datetime
import pandas as pd
import numpy as np import numpy as np
from flask import g, has_request_context, url_for from flask import g, has_request_context, url_for
from app import db from app import db
from app.comp.moy_mod import ModuleImplResults
from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_but import ResultatsSemestreBUT
from app.models import Evaluation, FormSemestre, Identite from app.models import Evaluation, FormSemestre, Identite, ModuleImpl
from app.models.groups import GroupDescr from app.models.groups import GroupDescr
from app.models.ues import UniteEns from app.models.ues import UniteEns
from app.scodoc import sco_bulletins, sco_utils as scu from app.scodoc import sco_bulletins, sco_utils as scu
@ -249,59 +251,88 @@ class BulletinBUT:
# "moy": fmt_note(moyennes_etuds.mean()), # "moy": fmt_note(moyennes_etuds.mean()),
}, },
"evaluations": ( "evaluations": (
[ self.etud_list_modimpl_evaluations(
self.etud_eval_results(etud, e) etud, modimpl, modimpl_results, version
for e in modimpl.evaluations )
if (e.visibulletin or version == "long")
and (e.id in modimpl_results.evaluations_etat)
and (
modimpl_results.evaluations_etat[e.id].is_complete
or self.prefs["bul_show_all_evals"]
)
]
if version != "short" if version != "short"
else [] else []
), ),
} }
return d return d
def etud_eval_results(self, etud, e: Evaluation) -> dict: def etud_list_modimpl_evaluations(
self,
etud: Identite,
modimpl: ModuleImpl,
modimpl_results: ModuleImplResults,
version: str,
) -> list[dict]:
"""Liste des résultats aux évaluations de ce modimpl à montrer pour cet étudiant"""
evaluation: Evaluation
eval_results = []
for evaluation in modimpl.evaluations:
if (
(evaluation.visibulletin or version == "long")
and (evaluation.id in modimpl_results.evaluations_etat)
and (
modimpl_results.evaluations_etat[evaluation.id].is_complete
or self.prefs["bul_show_all_evals"]
)
):
eval_notes = self.res.modimpls_results[modimpl.id].evals_notes[
evaluation.id
]
if (evaluation.evaluation_type == Evaluation.EVALUATION_NORMALE) or (
not np.isnan(eval_notes[etud.id])
):
eval_results.append(
self.etud_eval_results(etud, evaluation, eval_notes)
)
return eval_results
def etud_eval_results(
self, etud: Identite, evaluation: Evaluation, eval_notes: pd.DataFrame
) -> dict:
"dict resultats d'un étudiant à une évaluation" "dict resultats d'un étudiant à une évaluation"
# eval_notes est une pd.Series avec toutes les notes des étudiants inscrits # eval_notes est une pd.Series avec toutes les notes des étudiants inscrits
eval_notes = self.res.modimpls_results[e.moduleimpl_id].evals_notes[e.id]
notes_ok = eval_notes.where(eval_notes > scu.NOTES_ABSENCE).dropna() notes_ok = eval_notes.where(eval_notes > scu.NOTES_ABSENCE).dropna()
modimpls_evals_poids = self.res.modimpls_evals_poids[e.moduleimpl_id] modimpls_evals_poids = self.res.modimpls_evals_poids[evaluation.moduleimpl_id]
try: try:
etud_ues_ids = self.res.etud_ues_ids(etud.id) etud_ues_ids = self.res.etud_ues_ids(etud.id)
poids = { poids = {
ue.acronyme: modimpls_evals_poids[ue.id][e.id] ue.acronyme: modimpls_evals_poids[ue.id][evaluation.id]
for ue in self.res.ues for ue in self.res.ues
if (ue.type != UE_SPORT) and (ue.id in etud_ues_ids) if (ue.type != UE_SPORT) and (ue.id in etud_ues_ids)
} }
except KeyError: except KeyError:
poids = collections.defaultdict(lambda: 0.0) poids = collections.defaultdict(lambda: 0.0)
d = { d = {
"id": e.id, "id": evaluation.id,
"coef": ( "coef": (
fmt_note(e.coefficient) fmt_note(evaluation.coefficient)
if e.evaluation_type == Evaluation.EVALUATION_NORMALE if evaluation.evaluation_type == Evaluation.EVALUATION_NORMALE
else None else None
), ),
"date_debut": e.date_debut.isoformat() if e.date_debut else None, "date_debut": (
"date_fin": e.date_fin.isoformat() if e.date_fin else None, evaluation.date_debut.isoformat() if evaluation.date_debut else None
"description": e.description, ),
"evaluation_type": e.evaluation_type, "date_fin": (
evaluation.date_fin.isoformat() if evaluation.date_fin else None
),
"description": evaluation.description,
"evaluation_type": evaluation.evaluation_type,
"note": ( "note": (
{ {
"value": fmt_note( "value": fmt_note(
eval_notes[etud.id], eval_notes[etud.id],
note_max=e.note_max, note_max=evaluation.note_max,
), ),
"min": fmt_note(notes_ok.min(), note_max=e.note_max), "min": fmt_note(notes_ok.min(), note_max=evaluation.note_max),
"max": fmt_note(notes_ok.max(), note_max=e.note_max), "max": fmt_note(notes_ok.max(), note_max=evaluation.note_max),
"moy": fmt_note(notes_ok.mean(), note_max=e.note_max), "moy": fmt_note(notes_ok.mean(), note_max=evaluation.note_max),
} }
if not e.is_blocked() if not evaluation.is_blocked()
else {} else {}
), ),
"poids": poids, "poids": poids,
@ -309,17 +340,25 @@ class BulletinBUT:
url_for( url_for(
"notes.evaluation_listenotes", "notes.evaluation_listenotes",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
evaluation_id=e.id, evaluation_id=evaluation.id,
) )
if has_request_context() if has_request_context()
else "na" else "na"
), ),
# deprecated (supprimer avant #sco9.7) # deprecated (supprimer avant #sco9.7)
"date": e.date_debut.isoformat() if e.date_debut else None, "date": (
"heure_debut": ( evaluation.date_debut.isoformat() if evaluation.date_debut else None
e.date_debut.time().isoformat("minutes") if e.date_debut else None ),
"heure_debut": (
evaluation.date_debut.time().isoformat("minutes")
if evaluation.date_debut
else None
),
"heure_fin": (
evaluation.date_fin.time().isoformat("minutes")
if evaluation.date_fin
else None
), ),
"heure_fin": e.date_fin.time().isoformat("minutes") if e.date_fin else None,
} }
return d return d

View File

@ -59,3 +59,4 @@ def check_taxe_now(taxes):
from app.entreprises import routes from app.entreprises import routes
from app.entreprises.activate import activate_module

View File

@ -0,0 +1,31 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Activation du module entreprises
L'affichage du module est contrôlé par la config ScoDocConfig.enable_entreprises
Au moment de l'activation, il est en général utile de proposer de configurer les
permissions de rôles standards: AdminEntreprise UtilisateurEntreprise ObservateurEntreprise
Voir associations dans sco_roles_default
"""
from app.auth.models import Role
from app.models import ScoDocSiteConfig
from app.scodoc.sco_roles_default import SCO_ROLES_ENTREPRISES_DEFAULT
def activate_module(
enable: bool = True, set_default_roles_permission: bool = False
) -> bool:
"""Active le module et en option donne les permissions aux rôles standards.
True si l'état d'activation a changé.
"""
change = ScoDocSiteConfig.enable_entreprises(enable)
if enable and set_default_roles_permission:
Role.reset_roles_permissions(SCO_ROLES_ENTREPRISES_DEFAULT)
return change

View File

@ -0,0 +1,17 @@
"""
Formulaire activation module entreprises
"""
from flask_wtf import FlaskForm
from wtforms.fields.simple import BooleanField, SubmitField
from app.models import ScoDocSiteConfig
class ActivateEntreprisesForm(FlaskForm):
"Formulaire activation module entreprises"
set_default_roles_permission = BooleanField(
"(re)mettre les rôles 'Entreprise' à leurs valeurs par défaut"
)
submit = SubmitField("Valider")
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})

View File

@ -54,7 +54,6 @@ class BonusConfigurationForm(FlaskForm):
class ScoDocConfigurationForm(FlaskForm): class ScoDocConfigurationForm(FlaskForm):
"Panneau de configuration avancée" "Panneau de configuration avancée"
enable_entreprises = BooleanField("activer le module <em>entreprises</em>")
disable_passerelle = BooleanField( # disable car par défaut activée disable_passerelle = BooleanField( # disable car par défaut activée
"""cacher les fonctions liées à une passerelle de publication des résultats vers les étudiants ("œil"). N'affecte pas l'API, juste la présentation.""" """cacher les fonctions liées à une passerelle de publication des résultats vers les étudiants ("œil"). N'affecte pas l'API, juste la présentation."""
) )
@ -127,13 +126,6 @@ def configuration():
flash("Fonction bonus inchangée.") flash("Fonction bonus inchangée.")
return redirect(url_for("scodoc.index")) return redirect(url_for("scodoc.index"))
elif form_scodoc.submit_scodoc.data and form_scodoc.validate(): elif form_scodoc.submit_scodoc.data and form_scodoc.validate():
if ScoDocSiteConfig.enable_entreprises(
enabled=form_scodoc.data["enable_entreprises"]
):
flash(
"Module entreprise "
+ ("activé" if form_scodoc.data["enable_entreprises"] else "désactivé")
)
if ScoDocSiteConfig.disable_passerelle( if ScoDocSiteConfig.disable_passerelle(
disabled=form_scodoc.data["disable_passerelle"] disabled=form_scodoc.data["disable_passerelle"]
): ):
@ -182,6 +174,7 @@ def configuration():
return render_template( return render_template(
"configuration.j2", "configuration.j2",
is_entreprises_enabled=ScoDocSiteConfig.is_entreprises_enabled(),
form_bonus=form_bonus, form_bonus=form_bonus,
form_scodoc=form_scodoc, form_scodoc=form_scodoc,
scu=scu, scu=scu,

View File

@ -232,7 +232,9 @@ class ScolarNews(db.Model):
) )
# Transforme les URL en URL absolues # Transforme les URL en URL absolues
base = scu.ScoURL() base = url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)[
: -len("/index_html")
]
txt = re.sub('href=/.*?"', 'href="' + base + "/", txt) txt = re.sub('href=/.*?"', 'href="' + base + "/", txt)
# Transforme les liens HTML en texte brut: '<a href="url">texte</a>' devient 'texte: url' # Transforme les liens HTML en texte brut: '<a href="url">texte</a>' devient 'texte: url'

View File

@ -409,6 +409,14 @@ class UniteEns(models.ScoDocModel):
Renvoie (True, "") si ok, sinon (False, error_message) Renvoie (True, "") si ok, sinon (False, error_message)
""" """
msg = "" msg = ""
# Safety check
if self.formation.referentiel_competence is None:
return False, "pas de référentiel de compétence"
# Si tous les parcours, aucun (tronc commun)
if {p.id for p in parcours} == {
p.id for p in self.formation.referentiel_competence.parcours
}:
parcours = []
# Le niveau est-il dans tous ces parcours ? Sinon, l'enlève # Le niveau est-il dans tous ces parcours ? Sinon, l'enlève
prev_niveau = self.niveau_competence prev_niveau = self.niveau_competence
if ( if (
@ -424,6 +432,7 @@ class UniteEns(models.ScoDocModel):
self.niveau_competence, parcours self.niveau_competence, parcours
) )
if not ok: if not ok:
self.formation.invalidate_cached_sems()
self.niveau_competence = prev_niveau # restore self.niveau_competence = prev_niveau # restore
return False, error_message return False, error_message

View File

@ -48,6 +48,7 @@ from typing import Any
from urllib.parse import urlparse, urlencode, parse_qs, urlunparse from urllib.parse import urlparse, urlencode, parse_qs, urlunparse
from openpyxl.utils import get_column_letter from openpyxl.utils import get_column_letter
import reportlab
from reportlab.platypus import Paragraph, Spacer from reportlab.platypus import Paragraph, Spacer
from reportlab.platypus import Table, KeepInFrame from reportlab.platypus import Table, KeepInFrame
from reportlab.lib.colors import Color from reportlab.lib.colors import Color
@ -812,7 +813,10 @@ if __name__ == "__main__":
document, document,
) )
) )
document.build(objects) try:
document.build(objects)
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
raise ScoPDFFormatError(str(exc)) from exc
data = doc.getvalue() data = doc.getvalue()
with open("/tmp/gen_table.pdf", "wb") as f: with open("/tmp/gen_table.pdf", "wb") as f:
f.write(data) f.write(data)

View File

@ -30,7 +30,7 @@
import html import html
from flask import g, render_template from flask import g, render_template, url_for
from flask import request from flask import request
from flask_login import current_user from flask_login import current_user
@ -163,7 +163,7 @@ def sco_header(
params = { params = {
"page_title": page_title or sco_version.SCONAME, "page_title": page_title or sco_version.SCONAME,
"no_side_bar": no_side_bar, "no_side_bar": no_side_bar,
"ScoURL": scu.ScoURL(), "ScoURL": url_for("scolar.index_html", scodoc_dept=g.scodoc_dept),
"encoding": scu.SCO_ENCODING, "encoding": scu.SCO_ENCODING,
"titrebandeau_mkup": "<td>" + titrebandeau + "</td>", "titrebandeau_mkup": "<td>" + titrebandeau + "</td>",
"authuser": current_user.user_name, "authuser": current_user.user_name,
@ -220,7 +220,7 @@ def sco_header(
<script> <script>
window.onload=function(){{enableTooltips("gtrcontent")}}; window.onload=function(){{enableTooltips("gtrcontent")}};
const SCO_URL="{scu.ScoURL()}"; const SCO_URL="{url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)}";
const SCO_TIMEZONE="{scu.TIME_ZONE}"; const SCO_TIMEZONE="{scu.TIME_ZONE}";
</script>""" </script>"""
) )

View File

@ -102,25 +102,33 @@ def sidebar_common():
<a href="{home_link}" class="sidebar">Accueil</a> <br> <a href="{home_link}" class="sidebar">Accueil</a> <br>
<div id="authuser"><a id="authuserlink" href="{ <div id="authuser"><a id="authuserlink" href="{
url_for("users.user_info_page", url_for("users.user_info_page",
scodoc_dept=g.scodoc_dept, user_name=current_user.user_name) scodoc_dept=g.scodoc_dept, user_name=current_user.user_name)
}">{current_user.user_name}</a> }">{current_user.user_name}</a>
<br><a id="deconnectlink" href="{url_for("auth.logout")}">déconnexion</a> <br><a id="deconnectlink" href="{url_for("auth.logout")}">déconnexion</a>
</div> </div>
{sidebar_dept()} {sidebar_dept()}
<h2 class="insidebar">Scolarité</h2> <h2 class="insidebar">Scolarité</h2>
<a href="{scu.ScoURL()}" class="sidebar">Semestres</a> <br> <a href="{
<a href="{scu.NotesURL()}" class="sidebar">Formations</a> <br> url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
}" class="sidebar">Semestres</a> <br>
<a href="{
url_for("notes.index_html", scodoc_dept=g.scodoc_dept)
}" class="sidebar">Formations</a> <br>
""" """
] ]
if current_user.has_permission(Permission.AbsChange): if current_user.has_permission(Permission.AbsChange):
H.append( H.append(
f""" <a href="{scu.AssiduitesURL()}" class="sidebar">Assiduité</a> <br> """ f""" <a href="{
url_for("assiduites.bilan_dept", scodoc_dept=g.scodoc_dept)
}" class="sidebar">Assiduité</a> <br> """
) )
if current_user.has_permission( if current_user.has_permission(
Permission.UsersAdmin Permission.UsersAdmin
) or current_user.has_permission(Permission.UsersView): ) or current_user.has_permission(Permission.UsersView):
H.append( H.append(
f"""<a href="{scu.UsersURL()}" class="sidebar">Utilisateurs</a> <br>""" f"""<a href="{
url_for("users.index_html", scodoc_dept=g.scodoc_dept)
}" class="sidebar">Utilisateurs</a> <br>"""
) )
if current_user.has_permission(Permission.EditPreferences): if current_user.has_permission(Permission.EditPreferences):

View File

@ -49,11 +49,13 @@
""" """
import datetime import datetime
import glob import glob
import gzip
import mimetypes import mimetypes
import os import os
import re import re
import shutil import shutil
import time import time
import zlib
import chardet import chardet
@ -241,11 +243,13 @@ class BaseArchiver:
filename: str, filename: str,
data: str | bytes, data: str | bytes,
dept_id: int = None, dept_id: int = None,
compress=False,
): ):
"""Store data in archive, under given filename. """Store data in archive, under given filename.
Filename may be modified (sanitized): return used filename Filename may be modified (sanitized): return used filename
The file is created or replaced. The file is created or replaced.
data may be str or bytes data may be str or bytes
If compress, data is gziped and filename suffix ".gz" added.
""" """
if isinstance(data, str): if isinstance(data, str):
data = data.encode(scu.SCO_ENCODING) data = data.encode(scu.SCO_ENCODING)
@ -255,8 +259,14 @@ class BaseArchiver:
try: try:
scu.GSL.acquire() scu.GSL.acquire()
fname = os.path.join(archive_id, filename) fname = os.path.join(archive_id, filename)
with open(fname, "wb") as f: if compress:
f.write(data) if not fname.endswith(".gz"):
fname += ".gz"
with gzip.open(fname, "wb") as f:
f.write(data)
else:
with open(fname, "wb") as f:
f.write(data)
except FileNotFoundError as exc: except FileNotFoundError as exc:
raise ScoValueError( raise ScoValueError(
f"Erreur stockage archive (dossier inexistant, chemin {fname})" f"Erreur stockage archive (dossier inexistant, chemin {fname})"
@ -274,8 +284,17 @@ class BaseArchiver:
fname = os.path.join(archive_id, filename) fname = os.path.join(archive_id, filename)
log(f"reading archive file {fname}") log(f"reading archive file {fname}")
try: try:
with open(fname, "rb") as f: if fname.endswith(".gz"):
data = f.read() try:
with gzip.open(fname) as f:
data = f.read()
except (OSError, EOFError, zlib.error) as exc:
raise ScoValueError(
f"Erreur lecture archive ({fname} invalide)"
) from exc
else:
with open(fname, "rb") as f:
data = f.read()
except FileNotFoundError as exc: except FileNotFoundError as exc:
raise ScoValueError( raise ScoValueError(
f"Erreur lecture archive (inexistant, chemin {fname})" f"Erreur lecture archive (inexistant, chemin {fname})"
@ -288,6 +307,8 @@ class BaseArchiver:
""" """
archive_id = self.get_id_from_name(oid, archive_name, dept_id=dept_id) archive_id = self.get_id_from_name(oid, archive_name, dept_id=dept_id)
data = self.get(archive_id, filename) data = self.get(archive_id, filename)
if filename.endswith(".gz"):
filename = filename[:-3]
mime = mimetypes.guess_type(filename)[0] mime = mimetypes.guess_type(filename)[0]
if mime is None: if mime is None:
mime = "application/octet-stream" mime = "application/octet-stream"

View File

@ -68,7 +68,7 @@ PV_ARCHIVER = SemsArchiver()
def do_formsemestre_archive( def do_formsemestre_archive(
formsemestre_id, formsemestre: FormSemestre,
group_ids: list[int] = None, # si indiqué, ne prend que ces groupes group_ids: list[int] = None, # si indiqué, ne prend que ces groupes
description="", description="",
date_jury="", date_jury="",
@ -92,9 +92,8 @@ def do_formsemestre_archive(
raise ScoValueError( raise ScoValueError(
"do_formsemestre_archive: version de bulletin demandée invalide" "do_formsemestre_archive: version de bulletin demandée invalide"
) )
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
sem_archive_id = formsemestre_id sem_archive_id = formsemestre.id
archive_id = PV_ARCHIVER.create_obj_archive( archive_id = PV_ARCHIVER.create_obj_archive(
sem_archive_id, description, formsemestre.dept_id sem_archive_id, description, formsemestre.dept_id
) )
@ -102,9 +101,9 @@ def do_formsemestre_archive(
if not group_ids: if not group_ids:
# tous les inscrits du semestre # tous les inscrits du semestre
group_ids = [sco_groups.get_default_group(formsemestre_id)] group_ids = [sco_groups.get_default_group(formsemestre.id)]
groups_infos = sco_groups_view.DisplayedGroupsInfos( groups_infos = sco_groups_view.DisplayedGroupsInfos(
group_ids, formsemestre_id=formsemestre_id group_ids, formsemestre_id=formsemestre.id
) )
groups_filename = "-" + groups_infos.groups_filename groups_filename = "-" + groups_infos.groups_filename
etudids = [m["etudid"] for m in groups_infos.members] etudids = [m["etudid"] for m in groups_infos.members]
@ -142,19 +141,23 @@ def do_formsemestre_archive(
) )
# Bulletins en JSON # Bulletins en JSON
data = gen_formsemestre_recapcomplet_json(formsemestre_id, xml_with_decisions=True) data = gen_formsemestre_recapcomplet_json(formsemestre.id, xml_with_decisions=True)
data_js = json.dumps(data, indent=1, cls=ScoDocJSONEncoder) data_js = json.dumps(data, indent=1, cls=ScoDocJSONEncoder)
if data: if data:
PV_ARCHIVER.store( PV_ARCHIVER.store(
archive_id, "Bulletins.json", data_js, dept_id=formsemestre.dept_id archive_id,
"Bulletins.json",
data_js,
dept_id=formsemestre.dept_id,
compress=True,
) )
# Décisions de jury, en XLS # Décisions de jury, en XLS
if formsemestre.formation.is_apc(): if formsemestre.formation.is_apc():
response = jury_but_pv.pvjury_page_but(formsemestre_id, fmt="xls") response = jury_but_pv.pvjury_page_but(formsemestre.id, fmt="xls")
data = response.get_data() data = response.get_data()
else: # formations classiques else: # formations classiques
data = sco_pv_forms.formsemestre_pvjury( data = sco_pv_forms.formsemestre_pvjury(
formsemestre_id, fmt="xls", publish=False formsemestre.id, fmt="xls", publish=False
) )
if data: if data:
PV_ARCHIVER.store( PV_ARCHIVER.store(
@ -165,7 +168,7 @@ def do_formsemestre_archive(
) )
# Classeur bulletins (PDF) # Classeur bulletins (PDF)
data, _ = sco_bulletins_pdf.get_formsemestre_bulletins_pdf( data, _ = sco_bulletins_pdf.get_formsemestre_bulletins_pdf(
formsemestre_id, version=bul_version formsemestre.id, version=bul_version
) )
if data: if data:
PV_ARCHIVER.store( PV_ARCHIVER.store(
@ -173,10 +176,11 @@ def do_formsemestre_archive(
"Bulletins.pdf", "Bulletins.pdf",
data, data,
dept_id=formsemestre.dept_id, dept_id=formsemestre.dept_id,
compress=True,
) )
# Lettres individuelles (PDF): # Lettres individuelles (PDF):
data = sco_pv_lettres_inviduelles.pdf_lettres_individuelles( data = sco_pv_lettres_inviduelles.pdf_lettres_individuelles(
formsemestre_id, formsemestre.id,
etudids=etudids, etudids=etudids,
date_jury=date_jury, date_jury=date_jury,
date_commission=date_commission, date_commission=date_commission,
@ -217,7 +221,7 @@ def formsemestre_archive(formsemestre_id, group_ids: list[int] = None):
"""Make and store new archive for this formsemestre. """Make and store new archive for this formsemestre.
(all students or only selected groups) (all students or only selected groups)
""" """
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
if not formsemestre.can_edit_pv(): if not formsemestre.can_edit_pv():
raise ScoPermissionDenied( raise ScoPermissionDenied(
dest_url=url_for( dest_url=url_for(
@ -320,7 +324,7 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement.
else: else:
tf[2]["anonymous"] = False tf[2]["anonymous"] = False
do_formsemestre_archive( do_formsemestre_archive(
formsemestre_id, formsemestre,
group_ids=group_ids, group_ids=group_ids,
description=tf[2]["description"], description=tf[2]["description"],
date_jury=tf[2]["date_jury"], date_jury=tf[2]["date_jury"],
@ -352,7 +356,7 @@ def formsemestre_list_archives(formsemestre_id):
"""Page listing archives""" """Page listing archives"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
sem_archive_id = formsemestre_id sem_archive_id = formsemestre_id
L = [] archives_descr = []
for archive_id in PV_ARCHIVER.list_obj_archives( for archive_id in PV_ARCHIVER.list_obj_archives(
sem_archive_id, dept_id=formsemestre.dept_id sem_archive_id, dept_id=formsemestre.dept_id
): ):
@ -366,28 +370,30 @@ def formsemestre_list_archives(formsemestre_id):
archive_id, dept_id=formsemestre.dept_id archive_id, dept_id=formsemestre.dept_id
), ),
} }
L.append(a) archives_descr.append(a)
H = [html_sco_header.html_sem_header("Archive des PV et résultats ")] H = [html_sco_header.html_sem_header("Archive des PV et résultats ")]
if not L: if not archives_descr:
H.append("<p>aucune archive enregistrée</p>") H.append("<p>aucune archive enregistrée</p>")
else: else:
H.append("<ul>") H.append("<ul>")
for a in L: for a in archives_descr:
archive_name = PV_ARCHIVER.get_archive_name(a["archive_id"]) archive_name = PV_ARCHIVER.get_archive_name(a["archive_id"])
H.append( H.append(
'<li>%s : <em>%s</em> (<a href="formsemestre_delete_archive?formsemestre_id=%s&archive_name=%s">supprimer</a>)<ul>' f"""<li>{a["date"].strftime("%d/%m/%Y %H:%M")} : <em>{a["description"]}</em>
% ( (<a href="{ url_for( "notes.formsemestre_delete_archive", scodoc_dept=g.scodoc_dept,
a["date"].strftime("%d/%m/%Y %H:%M"), formsemestre_id=formsemestre_id, archive_name=archive_name
a["description"], )}">supprimer</a>)
formsemestre_id, <ul>"""
archive_name,
)
) )
for filename in a["content"]: for filename in a["content"]:
H.append( H.append(
'<li><a href="formsemestre_get_archived_file?formsemestre_id=%s&archive_name=%s&filename=%s">%s</a></li>' f"""<li><a href="{
% (formsemestre_id, archive_name, filename, filename) url_for( "notes.formsemestre_get_archived_file", scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
archive_name=archive_name,
filename=filename
)}">{filename[:-3] if filename.endswith(".gz") else filename}</a></li>"""
) )
if not a["content"]: if not a["content"]:
H.append("<li><em>aucun fichier !</em></li>") H.append("<li><em>aucun fichier !</em></li>")
@ -399,7 +405,7 @@ def formsemestre_list_archives(formsemestre_id):
def formsemestre_get_archived_file(formsemestre_id, archive_name, filename): def formsemestre_get_archived_file(formsemestre_id, archive_name, filename):
"""Send file to client.""" """Send file to client."""
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
sem_archive_id = formsemestre.id sem_archive_id = formsemestre.id
return PV_ARCHIVER.get_archived_file( return PV_ARCHIVER.get_archived_file(
sem_archive_id, archive_name, filename, dept_id=formsemestre.dept_id sem_archive_id, archive_name, filename, dept_id=formsemestre.dept_id

View File

@ -446,7 +446,8 @@ def _ue_mod_bulletin(
): ):
"""Infos sur les modules (et évaluations) dans une UE """Infos sur les modules (et évaluations) dans une UE
(ajoute les informations aux modimpls) (ajoute les informations aux modimpls)
Result: liste de modules de l'UE avec les infos dans chacun (seulement ceux où l'étudiant est inscrit). Result: liste de modules de l'UE avec les infos dans chacun (seulement
ceux l'étudiant est inscrit).
""" """
bul_show_mod_rangs = sco_preferences.get_preference( bul_show_mod_rangs = sco_preferences.get_preference(
"bul_show_mod_rangs", formsemestre_id "bul_show_mod_rangs", formsemestre_id

View File

@ -61,7 +61,7 @@ from flask_login import current_user
from app.models import FormSemestre, Identite, ScoDocSiteConfig from app.models import FormSemestre, Identite, ScoDocSiteConfig
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import NoteProcessError from app.scodoc.sco_exceptions import NoteProcessError, ScoPDFFormatError
from app import log from app import log
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_pdf from app.scodoc import sco_pdf
@ -228,7 +228,15 @@ class BulletinGenerator:
preferences=sco_preferences.SemPreferences(formsemestre_id), preferences=sco_preferences.SemPreferences(formsemestre_id),
) )
) )
document.build(story) try:
document.build(story)
except (
ValueError,
KeyError,
reportlab.platypus.doctemplate.LayoutError,
) as exc:
raise ScoPDFFormatError(str(exc)) from exc
data = report.getvalue() data = report.getvalue()
return data return data

View File

@ -230,41 +230,41 @@ def next_iso_day(date):
def YearTable( def YearTable(
year, year,
events=[], events_by_day: dict[str, list[dict]],
firstmonth=9, firstmonth=9,
lastmonth=7, lastmonth=7,
halfday=0,
dayattributes="", dayattributes="",
pad_width=8,
): ):
# Code simplifié en 2024: utilisé seulement pour calendrier évaluations
"""Generate a calendar table """Generate a calendar table
events = list of tuples (date, text, color, href [,halfday]) events = list of tuples (date, text, color, href [,halfday])
where date is a string in ISO format (yyyy-mm-dd) where date is a string in ISO format (yyyy-mm-dd)
halfday is boolean (true: morning, false: afternoon) halfday is boolean (true: morning, false: afternoon)
text = text to put in calendar (must be short, 1-5 cars) (optional) text = text to put in calendar (must be short, 1-5 cars) (optional)
if halfday, generate 2 cells per day (morning, afternoon)
""" """
T = [ T = [
'<table id="maincalendar" class="maincalendar" border="3" cellpadding="1" cellspacing="1" frame="box">' """<table id="maincalendar" class="maincalendar"
border="3" cellpadding="1" cellspacing="1" frame="box">"""
] ]
T.append("<tr>") T.append("<tr>")
month = firstmonth month = firstmonth
while 1: while True:
T.append('<td valign="top">') T.append('<td valign="top">')
T.append(MonthTableHead(month)) T.append(_month_table_head(month))
T.append( T.append(
MonthTableBody( _month_table_body(
month, month,
year, year,
events, events_by_day,
halfday,
dayattributes, dayattributes,
is_work_saturday(), is_work_saturday(),
pad_width=pad_width,
) )
) )
T.append(MonthTableTail()) T.append(
T.append("</td>") """
</table>
</td>"""
)
if month == lastmonth: if month == lastmonth:
break break
month = month + 1 month = month + 1
@ -322,29 +322,32 @@ WEEKDAYCOLOR = GRAY1
WEEKENDCOLOR = GREEN3 WEEKENDCOLOR = GREEN3
def MonthTableHead(month): def _month_table_head(month):
color = WHITE color = WHITE
return """<table class="monthcalendar" border="0" cellpadding="0" cellspacing="0" frame="box"> return f"""<table class="monthcalendar" border="0" cellpadding="0" cellspacing="0" frame="box">
<tr bgcolor="%s"><td class="calcol" colspan="2" align="center">%s</td></tr>\n""" % ( <tr bgcolor="{color}">
color, <td class="calcol" colspan="2" align="center">{MONTHNAMES_ABREV[month - 1]}</td>
MONTHNAMES_ABREV[month - 1], </tr>\n"""
)
def MonthTableTail(): def _month_table_body(
return "</table>\n" month,
year,
events_by_day: dict[str, list[dict]],
def MonthTableBody( trattributes="",
month, year, events=[], halfday=0, trattributes="", work_saturday=False, pad_width=8 work_saturday=False,
): ) -> str:
"""
events : [event]
event = [ yyyy-mm-dd, legend, href, color, descr ] XXX
"""
firstday, nbdays = calendar.monthrange(year, month) firstday, nbdays = calendar.monthrange(year, month)
localtime = time.localtime() localtime = time.localtime()
current_weeknum = time.strftime("%U", localtime) current_weeknum = time.strftime("%U", localtime)
current_year = localtime[0] current_year = localtime[0]
T = [] rows = []
# cherche date du lundi de la 1ere semaine de ce mois # cherche date du lundi de la 1ere semaine de ce mois
monday = ddmmyyyy("1/%d/%d" % (month, year)) monday = ddmmyyyy(f"1/{month}/{year}")
while monday.weekday != 0: while monday.weekday != 0:
monday = monday.prev() monday = monday.prev()
@ -353,158 +356,51 @@ def MonthTableBody(
else: else:
weekend = ("S", "D") weekend = ("S", "D")
if not halfday: for d in range(1, nbdays + 1):
for d in range(1, nbdays + 1): weeknum = time.strftime(
weeknum = time.strftime( "%U", time.strptime("%d/%d/%d" % (d, month, year), scu.DATE_FMT)
"%U", time.strptime("%d/%d/%d" % (d, month, year), scu.DATE_FMT) )
) day = DAYNAMES_ABREV[(firstday + d - 1) % 7]
day = DAYNAMES_ABREV[(firstday + d - 1) % 7] if day in weekend:
if day in weekend: bgcolor = WEEKENDCOLOR
bgcolor = WEEKENDCOLOR weekclass = "wkend"
weekclass = "wkend" attrs = ""
attrs = "" else:
else: bgcolor = WEEKDAYCOLOR
bgcolor = WEEKDAYCOLOR weekclass = "wk" + str(monday).replace("/", "_")
weekclass = "wk" + str(monday).replace("/", "_") attrs = trattributes
attrs = trattributes # events this day ?
color = None events = events_by_day.get(f"{year}-{month:02}-{d:02}", [])
legend = "" color = None
href = "" ev_txts = []
descr = "" for ev in events:
# event this day ? color = ev.get("color")
# each event is a tuple (date, text, color, href) href = ev.get("href", "")
# where date is a string in ISO format (yyyy-mm-dd) description = ev.get("description", "")
for ev in events:
ev_year = int(ev[0][:4])
ev_month = int(ev[0][5:7])
ev_day = int(ev[0][8:10])
if year == ev_year and month == ev_month and ev_day == d:
if ev[1]:
legend = ev[1]
if ev[2]:
color = ev[2]
if ev[3]:
href = ev[3]
if len(ev) > 4 and ev[4]:
descr = ev[4]
#
cc = []
if color is not None:
cc.append('<td bgcolor="%s" class="calcell">' % color)
else:
cc.append('<td class="calcell">')
if href: if href:
href = 'href="%s"' % href href = f'href="{href}"'
if descr: if description:
descr = 'title="%s"' % html.escape(descr, quote=True) description = f"""title="{html.escape(description, quote=True)}" """
if href or descr: if href or description:
cc.append("<a %s %s>" % (href, descr)) ev_txts.append(f"""<a {href} {description}>{ev.get("title", "")}</a>""")
if legend or d == 1:
if pad_width is not None:
n = pad_width - len(legend) # pad to 8 cars
if n > 0:
legend = (
"&nbsp;" * (n // 2) + legend + "&nbsp;" * ((n + 1) // 2)
)
else: else:
legend = "&nbsp;" # empty cell ev_txts.append(ev.get("title", "&nbsp;"))
cc.append(legend) #
if href or descr: cc = []
cc.append("</a>") if color is not None:
cc.append("</td>") cc.append(f'<td bgcolor="{color}" class="calcell">')
cell = "".join(cc) else:
if day == "D": cc.append('<td class="calcell">')
monday = monday.next_day(7)
if (
weeknum == current_weeknum
and current_year == year
and weekclass != "wkend"
):
weekclass += " currentweek"
T.append(
'<tr bgcolor="%s" class="%s" %s><td class="calday">%d%s</td>%s</tr>'
% (bgcolor, weekclass, attrs, d, day, cell)
)
else:
# Calendar with 2 cells / day
for d in range(1, nbdays + 1):
weeknum = time.strftime(
"%U", time.strptime("%d/%d/%d" % (d, month, year), scu.DATE_FMT)
)
day = DAYNAMES_ABREV[(firstday + d - 1) % 7]
if day in weekend:
bgcolor = WEEKENDCOLOR
weekclass = "wkend"
attrs = ""
else:
bgcolor = WEEKDAYCOLOR
weekclass = "wk" + str(monday).replace("/", "_")
attrs = trattributes
if (
weeknum == current_weeknum
and current_year == year
and weekclass != "wkend"
):
weeknum += " currentweek"
if day == "D": cc.append(f"{', '.join(ev_txts)}</td>")
monday = monday.next_day(7) cells = "".join(cc)
T.append( if day == "D":
'<tr bgcolor="%s" class="wk%s" %s><td class="calday">%d%s</td>' monday = monday.next_day(7)
% (bgcolor, weekclass, attrs, d, day) if weeknum == current_weeknum and current_year == year and weekclass != "wkend":
) weekclass += " currentweek"
cc = [] rows.append(
for morning in (True, False): f"""<tr bgcolor="{bgcolor}" class="{weekclass}" {attrs}>
color = None <td class="calday">{d}{day}</td>{cells}</tr>"""
legend = "" )
href = ""
descr = "" return "\n".join(rows)
for ev in events:
ev_year = int(ev[0][:4])
ev_month = int(ev[0][5:7])
ev_day = int(ev[0][8:10])
if ev[4] is not None:
ev_half = int(ev[4])
else:
ev_half = 0
if (
year == ev_year
and month == ev_month
and ev_day == d
and morning == ev_half
):
if ev[1]:
legend = ev[1]
if ev[2]:
color = ev[2]
if ev[3]:
href = ev[3]
if len(ev) > 5 and ev[5]:
descr = ev[5]
#
if color is not None:
cc.append('<td bgcolor="%s" class="calcell">' % (color))
else:
cc.append('<td class="calcell">')
if href:
href = 'href="%s"' % href
if descr:
descr = 'title="%s"' % html.escape(descr, quote=True)
if href or descr:
cc.append("<a %s %s>" % (href, descr))
if legend or d == 1:
n = 3 - len(legend) # pad to 3 cars
if n > 0:
legend = (
"&nbsp;" * (n // 2) + legend + "&nbsp;" * ((n + 1) // 2)
)
else:
legend = "&nbsp;&nbsp;&nbsp;" # empty cell
cc.append(legend)
if href or descr:
cc.append("</a>")
cc.append("</td>\n")
T.append("".join(cc) + "</tr>")
return "\n".join(T)

View File

@ -192,7 +192,7 @@ def _sem_table_gt(formsemestres: Query, showcodes=False, fmt="html") -> GenTable
"elt_sem_apo", "elt_sem_apo",
] ]
if showcodes: if showcodes:
columns_ids = ("formsemestre_id",) + columns_ids columns_ids.insert(0, "formsemestre_id") # prepend
html_class = "stripe cell-border compact hover order-column table_leftalign semlist" html_class = "stripe cell-border compact hover order-column table_leftalign semlist"
if current_user.has_permission(Permission.EditApogee): if current_user.has_permission(Permission.EditApogee):

View File

@ -58,21 +58,20 @@ def formation_delete(formation_id=None, dialog_confirmed=False):
html_sco_header.sco_header(page_title="Suppression d'une formation"), html_sco_header.sco_header(page_title="Suppression d'une formation"),
f"""<h2>Suppression de la formation {formation.titre} ({formation.acronyme})</h2>""", f"""<h2>Suppression de la formation {formation.titre} ({formation.acronyme})</h2>""",
] ]
formsemestres = formation.formsemestres.all()
sems = sco_formsemestre.do_formsemestre_list({"formation_id": formation_id}) if formsemestres:
if sems:
H.append( H.append(
"""<p class="warning">Impossible de supprimer cette formation, """<p class="warning">Impossible de supprimer cette formation,
car les sessions suivantes l'utilisent:</p> car les sessions suivantes l'utilisent:</p>
<ul>""" <ul>"""
) )
for sem in sems: for formsemestre in formsemestres:
H.append( H.append(f"""<li>{formsemestre.html_link_status()}</li>""")
'<li><a class="stdlink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titremois)s</a></li>'
% sem
)
H.append( H.append(
'</ul><p><a class="stdlink" href="%s">Revenir</a></p>' % scu.NotesURL() f"""</ul>
<p><a class="stdlink" href="{
url_for("notes.index_html", scodoc_dept=g.scodoc_dept)
}">Revenir</a></p>"""
) )
else: else:
if not dialog_confirmed: if not dialog_confirmed:
@ -85,14 +84,16 @@ def formation_delete(formation_id=None, dialog_confirmed=False):
</p> </p>
""", """,
OK="Supprimer cette formation", OK="Supprimer cette formation",
cancel_url=scu.NotesURL(), cancel_url=url_for("notes.index_html", scodoc_dept=g.scodoc_dept),
parameters={"formation_id": formation_id}, parameters={"formation_id": formation_id},
) )
else: else:
do_formation_delete(formation_id) do_formation_delete(formation_id)
H.append( H.append(
f"""<p>OK, formation supprimée.</p> f"""<p>OK, formation supprimée.</p>
<p><a class="stdlink" href="{scu.NotesURL()}">continuer</a></p>""" <p><a class="stdlink" href="{
url_for("notes.index_html", scodoc_dept=g.scodoc_dept)
}">continuer</a></p>"""
) )
H.append(html_sco_header.sco_footer()) H.append(html_sco_header.sco_footer())
@ -252,7 +253,7 @@ def formation_edit(formation_id=None, create=False):
if tf[0] == 0: if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer() return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
elif tf[0] == -1: elif tf[0] == -1:
return flask.redirect(scu.NotesURL()) return flask.redirect(url_for("notes.index_html", scodoc_dept=g.scodoc_dept))
else: else:
# check unicity : constraint UNIQUE(acronyme,titre,version) # check unicity : constraint UNIQUE(acronyme,titre,version)
if create: if create:

View File

@ -360,6 +360,7 @@ def do_evaluation_etat_in_mod(nt, modimpl: ModuleImpl):
return etat return etat
# View
def formsemestre_evaluations_cal(formsemestre_id): def formsemestre_evaluations_cal(formsemestre_id):
"""Page avec calendrier de toutes les evaluations de ce semestre""" """Page avec calendrier de toutes les evaluations de ce semestre"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
@ -373,22 +374,17 @@ def formsemestre_evaluations_cal(formsemestre_id):
color_futur = "#70E0FF" color_futur = "#70E0FF"
year = formsemestre.annee_scolaire() year = formsemestre.annee_scolaire()
events = {} # (day, halfday) : event events_by_day = collections.defaultdict(list) # date_iso : event
for e in evaluations: for e in evaluations:
if e.date_debut is None: if e.date_debut is None:
continue # éval. sans date continue # éval. sans date
txt = e.moduleimpl.module.code or e.moduleimpl.module.abbrev or "éval."
if e.date_debut == e.date_fin: if e.date_debut == e.date_fin:
heure_debut_txt, heure_fin_txt = "?", "?" heure_debut_txt, heure_fin_txt = "", ""
else: else:
heure_debut_txt = ( heure_debut_txt = (
e.date_debut.strftime(scu.TIME_FMT) if e.date_debut else "?" e.date_debut.strftime(scu.TIME_FMT) if e.date_debut else ""
) )
heure_fin_txt = e.date_fin.strftime(scu.TIME_FMT) if e.date_fin else "?" heure_fin_txt = e.date_fin.strftime(scu.TIME_FMT) if e.date_fin else ""
description = f"""{
e.moduleimpl.module.titre
}, de {heure_debut_txt} à {heure_fin_txt}"""
# Etat (notes completes) de l'évaluation: # Etat (notes completes) de l'évaluation:
modimpl_result = nt.modimpls_results[e.moduleimpl.id] modimpl_result = nt.modimpls_results[e.moduleimpl.id]
@ -398,28 +394,27 @@ def formsemestre_evaluations_cal(formsemestre_id):
color = color_incomplete color = color_incomplete
if e.date_debut > datetime.datetime.now(scu.TIME_ZONE): if e.date_debut > datetime.datetime.now(scu.TIME_ZONE):
color = color_futur color = color_futur
href = url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=e.moduleimpl_id,
)
day = e.date_debut.date().isoformat() # yyyy-mm-dd day = e.date_debut.date().isoformat() # yyyy-mm-dd
event = events.get(day) event = {
if not event: "color": color,
events[day] = [day, txt, color, href, description, e.moduleimpl] "date_iso": day,
else: "title": e.moduleimpl.module.code or e.moduleimpl.module.abbrev or "éval.",
if event[-1].id != e.moduleimpl.id: "description": f"""{e.description or e.moduleimpl.module.titre_str()}"""
# plusieurs evals de modules differents a la meme date + (
event[1] += ", " + txt f""" de {heure_debut_txt} à {heure_fin_txt}"""
event[4] += ", " + description if heure_debut_txt
if color == color_incomplete: else ""
event[2] = color_incomplete ),
if color == color_futur: "href": url_for(
event[2] = color_futur "notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=e.moduleimpl_id,
),
"modimpl": e.moduleimpl,
}
events_by_day[day].append(event)
cal_html = sco_cal.YearTable( cal_html = sco_cal.YearTable(year, events_by_day=events_by_day)
year, events=list(events.values()), halfday=False, pad_width=None
)
return f""" return f"""
{ {
@ -541,7 +536,9 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, fmt="html"):
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
moduleimpl_id=e.moduleimpl.id, moduleimpl_id=e.moduleimpl.id,
), ),
"module_titre": e.moduleimpl.module.abbrev or e.moduleimpl.module.titre, "module_titre": e.moduleimpl.module.abbrev
or e.moduleimpl.module.titre
or "",
"responsable_id": e.moduleimpl.responsable_id, "responsable_id": e.moduleimpl.responsable_id,
"responsable_nomplogin": sco_users.user_info( "responsable_nomplogin": sco_users.user_info(
e.moduleimpl.responsable_id e.moduleimpl.responsable_id

View File

@ -103,7 +103,7 @@ class ScoPDFFormatError(ScoValueError):
super().__init__( super().__init__(
f"""Erreur dans un format pdf: f"""Erreur dans un format pdf:
<p>{msg}</p> <p>{msg}</p>
<p>Vérifiez les paramètres (polices de caractères, balisage) <p>Vérifiez les paramètres (polices de caractères, balisage, réglages bulletins...)
dans les paramètres ou préférences. dans les paramètres ou préférences.
</p> </p>
""", """,

View File

@ -1431,18 +1431,25 @@ Ceci n'est possible que si :
def formsemestre_delete2(formsemestre_id, dialog_confirmed=False): def formsemestre_delete2(formsemestre_id, dialog_confirmed=False):
"""Delete a formsemestre (confirmation)""" """Delete a formsemestre (confirmation)"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
# Confirmation dialog # Confirmation dialog
if not dialog_confirmed: if not dialog_confirmed:
return scu.confirm_dialog( return scu.confirm_dialog(
"""<h2>Vous voulez vraiment supprimer ce semestre ???</h2><p>(opération irréversible)</p>""", """<h2>Vous voulez vraiment supprimer ce semestre ???</h2>
<p>(opération irréversible)</p>
""",
dest_url="", dest_url="",
cancel_url="formsemestre_status?formsemestre_id=%s" % formsemestre_id, cancel_url=url_for(
parameters={"formsemestre_id": formsemestre_id}, "notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
),
parameters={"formsemestre_id": formsemestre.id},
) )
# Bon, s'il le faut... # Bon, s'il le faut...
do_formsemestre_delete(formsemestre_id) do_formsemestre_delete(formsemestre.id)
flash("Semestre supprimé !") flash("Semestre supprimé !")
return flask.redirect(scu.ScoURL()) return flask.redirect(url_for("scolar.index_html", scodoc_dept=g.scodoc_dept))
def formsemestre_has_decisions_or_compensations( def formsemestre_has_decisions_or_compensations(

View File

@ -634,7 +634,7 @@ def formsemestre_description_table(
"UE": modimpl.module.ue.acronyme, "UE": modimpl.module.ue.acronyme,
"_UE_td_attrs": ue_info.get("_UE_td_attrs", ""), "_UE_td_attrs": ue_info.get("_UE_td_attrs", ""),
"Code": modimpl.module.code or "", "Code": modimpl.module.code or "",
"Module": modimpl.module.abbrev or modimpl.module.titre, "Module": modimpl.module.abbrev or modimpl.module.titre or "",
"_Module_class": "scotext", "_Module_class": "scotext",
"Inscrits": mod_nb_inscrits, "Inscrits": mod_nb_inscrits,
"Responsable": sco_users.user_info(modimpl.responsable_id)["nomprenom"], "Responsable": sco_users.user_info(modimpl.responsable_id)["nomprenom"],

View File

@ -180,7 +180,7 @@ def fiche_etud(etudid=None):
) )
else: else:
info["etat_civil"] = "" info["etat_civil"] = ""
info["ScoURL"] = scu.ScoURL() info["ScoURL"] = url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
info["authuser"] = current_user info["authuser"] = current_user
if restrict_etud_data: if restrict_etud_data:
info["info_naissance"] = "" info["info_naissance"] = ""

View File

@ -458,7 +458,12 @@ def pdf_basic_page(
if title: if title:
head = Paragraph(SU(title), StyleSheet["Heading3"]) head = Paragraph(SU(title), StyleSheet["Heading3"])
objects = [head] + objects objects = [head] + objects
document.build(objects)
try:
document.build(objects)
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
raise ScoPDFFormatError(str(exc)) from exc
data = report.getvalue() data = report.getvalue()
return data return data

View File

@ -2260,16 +2260,17 @@ class BasePreferences:
before_table="<details><summary>{title}</summary>", before_table="<details><summary>{title}</summary>",
after_table="</details>", after_table="</details>",
) )
dest_url = url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
if tf[0] == 0: if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer() return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
elif tf[0] == -1: if tf[0] == -1:
return flask.redirect(scu.ScoURL()) # cancel return flask.redirect(dest_url) # cancel
else: #
for pref in self.prefs_definition: for pref in self.prefs_definition:
self.prefs[None][pref[0]] = tf[2][pref[0]] self.prefs[None][pref[0]] = tf[2][pref[0]]
self.save() self.save()
flash("Préférences modifiées") flash("Préférences modifiées")
return flask.redirect(scu.ScoURL()) return flask.redirect(dest_url)
def build_tf_form(self, categories: list[str] = None, formsemestre_id: int = None): def build_tf_form(self, categories: list[str] = None, formsemestre_id: int = None):
"""Build list of elements for TrivialFormulator. """Build list of elements for TrivialFormulator.
@ -2433,10 +2434,12 @@ function set_global_pref(el, pref_name) {
before_table="<details><summary>{title}</summary>", before_table="<details><summary>{title}</summary>",
after_table="</details>", after_table="</details>",
) )
dest_url = ( dest_url = url_for(
scu.NotesURL() "notes.formsemestre_status",
+ "/formsemestre_status?formsemestre_id=%s" % self.formsemestre_id scodoc_dept=g.scodoc_dept,
formsemestre_id=self.formsemestre_id,
) )
if tf[0] == 0: if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer() return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
elif tf[0] == -1: elif tf[0] == -1:
@ -2482,7 +2485,9 @@ function set_global_pref(el, pref_name) {
request.base_url + "?formsemestre_id=" + str(self.formsemestre_id) request.base_url + "?formsemestre_id=" + str(self.formsemestre_id)
) )
elif destination == "global": elif destination == "global":
return flask.redirect(scu.ScoURL() + "/edit_preferences") return flask.redirect(
url_for("scolar.edit_preferences", scodoc_dept=g.scodoc_dept)
)
# #

View File

@ -50,7 +50,7 @@ from app.scodoc import sco_bulletins_pdf
from app.scodoc import sco_pv_dict from app.scodoc import sco_pv_dict
from app.scodoc import sco_pdf from app.scodoc import sco_pdf
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoPDFFormatError, ScoValueError
from app.scodoc.sco_cursus_dut import SituationEtudCursus from app.scodoc.sco_cursus_dut import SituationEtudCursus
from app.scodoc.sco_pv_templates import CourrierIndividuelTemplate, jury_titres from app.scodoc.sco_pv_templates import CourrierIndividuelTemplate, jury_titres
import sco_version import sco_version
@ -132,7 +132,11 @@ def pdf_lettres_individuelles(
) )
) )
document.build(objects) try:
document.build(objects)
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
raise ScoPDFFormatError(str(exc)) from exc
data = report.getvalue() data = report.getvalue()
return data return data
@ -241,13 +245,14 @@ def pdf_lettre_individuelle(sem, decision, etud: Identite, params, signature=Non
titre_jury_court = "s" titre_jury_court = "s"
else: else:
titre_jury_court = "" titre_jury_court = ""
params[ params["autorisations_txt"] = (
"autorisations_txt" """Vous êtes autorisé%s à continuer dans le%s semestre%s : <b>%s</b>"""
] = """Vous êtes autorisé%s à continuer dans le%s semestre%s : <b>%s</b>""" % ( % (
etud.e, etud.e,
titre_jury_court, titre_jury_court,
titre_jury_court, titre_jury_court,
decision["autorisations_descr"], decision["autorisations_descr"],
)
) )
else: else:
params["autorisations_txt"] = "" params["autorisations_txt"] = ""

View File

@ -126,7 +126,11 @@ def pvjury_pdf(
) )
) )
document.build(objects) try:
document.build(objects)
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
raise ScoPDFFormatError(str(exc)) from exc
data = report.getvalue() data = report.getvalue()
return data return data

View File

@ -51,7 +51,24 @@ SCO_ROLES_DEFAULTS = {
p.UsersView, p.UsersView,
p.ViewEtudData, p.ViewEtudData,
), ),
# Rôles pour l'application relations entreprises # LecteurAPI peut utiliser l'API en lecture
"LecteurAPI": (p.ScoView,),
"Observateur": (p.Observateur,),
# RespPE est le responsable poursuites d'études
# il peut ajouter des tags sur les formations:
# (doit avoir un rôle Ens en plus !)
"RespPe": (p.EditFormationTags,),
# Super Admin est un root: création/suppression de départements
# _tous_ les droits
# Afin d'avoir tous les droits, il ne doit pas être asscoié à un département
"SuperAdmin": p.ALL_PERMISSIONS,
}
# Rôles pour l'application relations entreprises
# séparés pour pouvoir les réinitialiser lors de l'activation du module Entreprises
# Note: Admin (chef de dept n'a par défaut aucun rôle lié à ce module)
SCO_ROLES_ENTREPRISES_DEFAULT = {
# ObservateurEntreprise est un observateur de l'application entreprise # ObservateurEntreprise est un observateur de l'application entreprise
"ObservateurEntreprise": (p.RelationsEntrepView,), "ObservateurEntreprise": (p.RelationsEntrepView,),
# UtilisateurEntreprise est un utilisateur de l'application entreprise (droit de modification) # UtilisateurEntreprise est un utilisateur de l'application entreprise (droit de modification)
@ -70,19 +87,10 @@ SCO_ROLES_DEFAULTS = {
p.RelationsEntrepValidate, p.RelationsEntrepValidate,
p.RelationsEntrepViewCorrs, p.RelationsEntrepViewCorrs,
), ),
# LecteurAPI peut utiliser l'API en lecture
"LecteurAPI": (p.ScoView,),
"Observateur": (p.Observateur,),
# RespPE est le responsable poursuites d'études
# il peut ajouter des tags sur les formations:
# (doit avoir un rôle Ens en plus !)
"RespPe": (p.EditFormationTags,),
# Super Admin est un root: création/suppression de départements
# _tous_ les droits
# Afin d'avoir tous les droits, il ne doit pas être asscoié à un département
"SuperAdmin": p.ALL_PERMISSIONS,
} }
SCO_ROLES_DEFAULTS.update(SCO_ROLES_ENTREPRISES_DEFAULT)
# Les rôles accessibles via la page d'admin utilisateurs # Les rôles accessibles via la page d'admin utilisateurs
# - associés à un département: # - associés à un département:
ROLES_ATTRIBUABLES_DEPT = ("Ens", "Secr", "Admin", "RespPe") ROLES_ATTRIBUABLES_DEPT = ("Ens", "Secr", "Admin", "RespPe")

View File

@ -47,12 +47,11 @@ from app import db, log
from app.models import Identite from app.models import Identite
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoPDFFormatError, ScoValueError
from app.scodoc.sco_pdf import SU from app.scodoc.sco_pdf import SU
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import htmlutils from app.scodoc import htmlutils
from app.scodoc import sco_import_etuds from app.scodoc import sco_import_etuds
from app.scodoc import sco_etud
from app.scodoc import sco_excel from app.scodoc import sco_excel
from app.scodoc import sco_groups_view from app.scodoc import sco_groups_view
from app.scodoc import sco_pdf from app.scodoc import sco_pdf
@ -388,7 +387,10 @@ def _trombino_pdf(groups_infos):
preferences=sco_preferences.SemPreferences(sem["formsemestre_id"]), preferences=sco_preferences.SemPreferences(sem["formsemestre_id"]),
) )
) )
document.build(objects) try:
document.build(objects)
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
raise ScoPDFFormatError(str(exc)) from exc
report.seek(0) report.seek(0)
return send_file( return send_file(
report, report,
@ -465,7 +467,10 @@ def _listeappel_photos_pdf(groups_infos):
preferences=sco_preferences.SemPreferences(sem["formsemestre_id"]), preferences=sco_preferences.SemPreferences(sem["formsemestre_id"]),
) )
) )
document.build(objects) try:
document.build(objects)
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
raise ScoPDFFormatError(str(exc)) from exc
data = report.getvalue() data = report.getvalue()
return scu.sendPDFFile(data, filename) return scu.sendPDFFile(data, filename)

View File

@ -31,7 +31,7 @@
""" """
import io import io
import reportlab
from reportlab.lib import colors from reportlab.lib import colors
from reportlab.lib.colors import black from reportlab.lib.colors import black
from reportlab.lib.pagesizes import A4, A3 from reportlab.lib.pagesizes import A4, A3
@ -277,10 +277,12 @@ def pdf_trombino_tours(
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
) )
) )
try: try:
document.build(objects) document.build(objects)
except (ValueError, KeyError) as exc: except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
raise ScoPDFFormatError(str(exc)) from exc raise ScoPDFFormatError(str(exc)) from exc
data = report.getvalue() data = report.getvalue()
return scu.sendPDFFile(data, filename) return scu.sendPDFFile(data, filename)
@ -470,7 +472,10 @@ def pdf_feuille_releve_absences(
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
) )
) )
document.build(objects) try:
document.build(objects)
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
raise ScoPDFFormatError(str(exc)) from exc
data = report.getvalue() data = report.getvalue()
return scu.sendPDFFile(data, filename) return scu.sendPDFFile(data, filename)

View File

@ -785,51 +785,6 @@ BULLETINS_VERSIONS_BUT = BULLETINS_VERSIONS | {
"butcourt": "Version courte spéciale BUT" "butcourt": "Version courte spéciale BUT"
} }
# ----- Support for ScoDoc7 compatibility
def ScoURL():
"""base URL for this sco instance.
e.g. https://scodoc.xxx.fr/ScoDoc/DEPT/Scolarite
= page accueil département
"""
return url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)[
: -len("/index_html")
]
def NotesURL():
"""URL of Notes
e.g. https://scodoc.xxx.fr/ScoDoc/DEPT/Scolarite/Notes
= url de base des méthodes de notes
(page accueil programmes).
"""
return url_for("notes.index_html", scodoc_dept=g.scodoc_dept)[: -len("/index_html")]
def AbsencesURL():
"""URL of Absences"""
return url_for("absences.index_html", scodoc_dept=g.scodoc_dept)[
: -len("/index_html")
]
def AssiduitesURL():
"""URL of Assiduités"""
return url_for("assiduites.bilan_dept", scodoc_dept=g.scodoc_dept)[
: -len("/BilanDept")
]
def UsersURL():
"""URL of Users
e.g. https://scodoc.xxx.fr/ScoDoc/DEPT/Scolarite/Users
= url de base des requêtes ZScoUsers
et page accueil users
"""
return url_for("users.index_html", scodoc_dept=g.scodoc_dept)[: -len("/index_html")]
# ---- Simple python utilities # ---- Simple python utilities

View File

@ -485,6 +485,10 @@
cursor: pointer; cursor: pointer;
} }
.mass-selection em {
margin-left: 16px;
}
.fieldsplit { .fieldsplit {
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;

View File

@ -425,7 +425,7 @@ async function getModuleImpl(assiduite) {
return res.json(); return res.json();
}) })
.then((data) => { .then((data) => {
moduleimpls[id] = `${data.module.code} ${data.module.abbrev}`; moduleimpls[id] = `${data.module.code} ${data.module.abbrev || ''}`;
return moduleimpls[id]; return moduleimpls[id];
}) })
.catch((_) => { .catch((_) => {

View File

@ -1,6 +1,6 @@
function _partition_set_attr(partition_id, attr_name, attr_value) { function _partition_set_attr(partition_id, attr_name, attr_value) {
$.post( $.post(
SCO_URL + "/partition_set_attr", SCO_URL + "partition_set_attr",
{ {
partition_id: partition_id, partition_id: partition_id,
attr: attr_name, attr: attr_name,

View File

@ -33,7 +33,7 @@ function update_ue_list() {
let ue_code = $("#tf_ue_code")[0].value; let ue_code = $("#tf_ue_code")[0].value;
let query = let query =
SCO_URL + SCO_URL +
"/Notes/ue_sharing_code?ue_code=" + "Notes/ue_sharing_code?ue_code=" +
ue_code + ue_code +
"&hide_ue_id=" + "&hide_ue_id=" +
ue_id + ue_id +

View File

@ -16,7 +16,7 @@ function display_itemsuivis(active) {
.off("click") .off("click")
.click(function (e) { .click(function (e) {
e.preventDefault(); e.preventDefault();
$.post(SCO_URL + "/itemsuivi_create", { $.post(SCO_URL + "itemsuivi_create", {
etudid: etudid, etudid: etudid,
fmt: "json", fmt: "json",
}).done(item_insert_new); }).done(item_insert_new);
@ -26,7 +26,7 @@ function display_itemsuivis(active) {
} }
// add existing items // add existing items
$.get( $.get(
SCO_URL + "/itemsuivi_list_etud", SCO_URL + "itemsuivi_list_etud",
{ etudid: etudid, fmt: "json" }, { etudid: etudid, fmt: "json" },
function (L) { function (L) {
for (var i in L) { for (var i in L) {
@ -95,7 +95,7 @@ function item_nodes(itemsuivi_id, item_date, situation, tags, readonly) {
dp.blur(function (e) { dp.blur(function (e) {
var date = this.value; var date = this.value;
// console.log('selected text: ' + date); // console.log('selected text: ' + date);
$.post(SCO_URL + "/itemsuivi_set_date", { $.post(SCO_URL + "itemsuivi_set_date", {
item_date: date, item_date: date,
itemsuivi_id: itemsuivi_id, itemsuivi_id: itemsuivi_id,
}); });
@ -103,7 +103,7 @@ function item_nodes(itemsuivi_id, item_date, situation, tags, readonly) {
dp.datepicker({ dp.datepicker({
onSelect: function (date, instance) { onSelect: function (date, instance) {
// console.log('selected: ' + date + 'for itemsuivi_id ' + itemsuivi_id); // console.log('selected: ' + date + 'for itemsuivi_id ' + itemsuivi_id);
$.post(SCO_URL + "/itemsuivi_set_date", { $.post(SCO_URL + "itemsuivi_set_date", {
item_date: date, item_date: date,
itemsuivi_id: itemsuivi_id, itemsuivi_id: itemsuivi_id,
}); });
@ -161,7 +161,7 @@ function Date2DMY(date) {
} }
function itemsuivi_suppress(itemsuivi_id) { function itemsuivi_suppress(itemsuivi_id) {
$.post(SCO_URL + "/itemsuivi_suppress", { itemsuivi_id: itemsuivi_id }); $.post(SCO_URL + "itemsuivi_suppress", { itemsuivi_id: itemsuivi_id });
// Clear items and rebuild: // Clear items and rebuild:
$("ul.listdebouches li.itemsuivi").remove(); $("ul.listdebouches li.itemsuivi").remove();
display_itemsuivis(0); display_itemsuivis(0);

View File

@ -37,7 +37,7 @@ $().ready(function () {
ajax: { ajax: {
url: url:
SCO_URL + SCO_URL +
"/etud_info_html?etudid=" + "etud_info_html?etudid=" +
get_etudid_from_elem(elems[i]) + get_etudid_from_elem(elems[i]) +
qs, qs,
type: "GET", type: "GET",

View File

@ -19,7 +19,7 @@ function loadGroupes() {
$("#gmsg")[0].style.display = "block"; $("#gmsg")[0].style.display = "block";
var partition_id = document.formGroup.partition_id.value; var partition_id = document.formGroup.partition_id.value;
$.get(SCO_URL + "/XMLgetGroupsInPartition", { $.get(SCO_URL + "XMLgetGroupsInPartition", {
partition_id: partition_id, partition_id: partition_id,
}).done(function (data) { }).done(function (data) {
var nodes = data.getElementsByTagName("group"); var nodes = data.getElementsByTagName("group");
@ -384,7 +384,7 @@ function handleError(msg) {
} }
function submitGroups() { function submitGroups() {
var url = SCO_URL + "/setGroups"; var url = SCO_URL + "setGroups";
// build post request body: groupname \n etudid; ... // build post request body: groupname \n etudid; ...
var groupsLists = ""; var groupsLists = "";
var groupsToCreate = ""; var groupsToCreate = "";
@ -443,7 +443,7 @@ function GotoAnother() {
} else } else
document.location = document.location =
SCO_URL + SCO_URL +
"/affect_groups?partition_id=" + "affect_groups?partition_id=" +
document.formGroup.other_partition_id.value; document.formGroup.other_partition_id.value;
} }

View File

@ -5,7 +5,7 @@ $().ready(function () {
for (var i = 0; i < spans.length; i++) { for (var i = 0; i < spans.length; i++) {
var sp = spans[i]; var sp = spans[i];
var etudid = sp.id; var etudid = sp.id;
$(sp).load(SCO_URL + "/etud_photo_html?etudid=" + etudid); $(sp).load(SCO_URL + "etud_photo_html?etudid=" + etudid);
} }
}); });
@ -194,7 +194,7 @@ $().ready(function () {
ajax: { ajax: {
url: url:
SCO_URL + SCO_URL +
"/etud_info_html?with_photo=0&etudid=" + "etud_info_html?with_photo=0&etudid=" +
get_etudid_from_elem(elems[i]), get_etudid_from_elem(elems[i]),
}, },
text: "Loading...", text: "Loading...",

View File

@ -34,7 +34,7 @@ function get_notes_and_draw(formsemestre_id, etudid) {
*/ */
var query = var query =
SCO_URL + SCO_URL +
"/Notes/formsemestre_bulletinetud?formsemestre_id=" + "Notes/formsemestre_bulletinetud?formsemestre_id=" +
formsemestre_id + formsemestre_id +
"&etudid=" + "&etudid=" +
etudid + etudid +

View File

@ -42,7 +42,7 @@ async function save_note(elem, v, etudid) {
$("#sco_msg").html("en cours...").show(); $("#sco_msg").html("en cours...").show();
try { try {
const response = await fetch( const response = await fetch(
SCO_URL + "/../api/evaluation/" + evaluation_id + "/notes/set", SCO_URL + "../api/evaluation/" + evaluation_id + "/notes/set",
{ {
method: "POST", method: "POST",
headers: { headers: {

View File

@ -6,7 +6,7 @@ $(function () {
delay: 300, // wait 300ms before suggestions delay: 300, // wait 300ms before suggestions
minLength: 2, // min nb of chars before suggest minLength: 2, // min nb of chars before suggest
position: { collision: "flip" }, // automatic menu position up/down position: { collision: "flip" }, // automatic menu position up/down
source: SCO_URL + "/search_etud_by_name", source: SCO_URL + "search_etud_by_name",
select: function (event, ui) { select: function (event, ui) {
$(".in-expnom").val(ui.item.value); $(".in-expnom").val(ui.item.value);
$("#form-chercheetud").submit(); $("#form-chercheetud").submit();

View File

@ -5,6 +5,6 @@ $().ready(function () {
for (var i = 0; i < spans.size(); i++) { for (var i = 0; i < spans.size(); i++) {
var sp = spans[i]; var sp = spans[i];
var etudid = sp.id; var etudid = sp.id;
$(sp).load(SCO_URL + "/etud_photo_html?etudid=" + etudid); $(sp).load(SCO_URL + "etud_photo_html?etudid=" + etudid);
} }
}); });

View File

@ -22,7 +22,7 @@ document.addEventListener("DOMContentLoaded", () => {
async function delete_validation(etudid, validation_type, validation_id) { async function delete_validation(etudid, validation_type, validation_id) {
const response = await fetch( const response = await fetch(
`${SCO_URL}/../api/etudiant/${etudid}/jury/${validation_type}/${validation_id}/delete`, `${SCO_URL}../api/etudiant/${etudid}/jury/${validation_type}/${validation_id}/delete`,
{ {
method: "POST", method: "POST",
} }
@ -38,7 +38,7 @@ async function delete_validation(etudid, validation_type, validation_id) {
function update_ue_list() { function update_ue_list() {
var ue_id = $("#tf_ue_id")[0].value; var ue_id = $("#tf_ue_id")[0].value;
if (ue_id) { if (ue_id) {
var query = SCO_URL + "/Notes/ue_sharing_code?ue_id=" + ue_id; var query = SCO_URL + "Notes/ue_sharing_code?ue_id=" + ue_id;
$.get(query, "", function (data) { $.get(query, "", function (data) {
$("#ue_list_code").html(data); $("#ue_list_code").html(data);
}); });

View File

@ -265,6 +265,8 @@ class Table(Element):
title: str = None, title: str = None,
classes: list[str] = None, classes: list[str] = None,
raw_title: str = None, raw_title: str = None,
no_excel: bool = False,
only_excel: bool = False,
) -> tuple["Cell", "Cell"]: ) -> tuple["Cell", "Cell"]:
"""Record this title, """Record this title,
and create cells for footer and header if they don't already exist. and create cells for footer and header if they don't already exist.
@ -282,6 +284,8 @@ class Table(Element):
classes=classes, classes=classes,
group=self.column_group.get(col_id), group=self.column_group.get(col_id),
raw_content=raw_title or title, raw_content=raw_title or title,
no_excel=no_excel,
only_excel=only_excel,
) )
if self.foot_title_row: if self.foot_title_row:
self.foot_title_row.cells[col_id] = self.foot_title_row.add_cell( self.foot_title_row.cells[col_id] = self.foot_title_row.add_cell(
@ -370,6 +374,7 @@ class Row(Element):
target_attrs: dict = None, target_attrs: dict = None,
target: str = None, target: str = None,
column_classes: set[str] = None, column_classes: set[str] = None,
only_excel: bool = False,
no_excel: bool = False, no_excel: bool = False,
) -> "Cell": ) -> "Cell":
"""Create cell and add it to the row. """Create cell and add it to the row.
@ -397,6 +402,7 @@ class Row(Element):
column_group=group, column_group=group,
title=title, title=title,
raw_title=raw_title, raw_title=raw_title,
only_excel=only_excel,
no_excel=no_excel, no_excel=no_excel,
) )
@ -406,6 +412,7 @@ class Row(Element):
cell: "Cell", cell: "Cell",
column_group: str | None = None, column_group: str | None = None,
title: str | None = None, title: str | None = None,
only_excel: bool = False,
no_excel: bool = False, no_excel: bool = False,
raw_title: str | None = None, raw_title: str | None = None,
) -> "Cell": ) -> "Cell":
@ -414,10 +421,10 @@ class Row(Element):
""" """
cell.data["group"] = column_group or "" cell.data["group"] = column_group or ""
self.cells[col_id] = cell self.cells[col_id] = cell
if col_id not in self.table.column_ids: if not only_excel and col_id not in self.table.column_ids:
self.table.column_ids.append(col_id) self.table.column_ids.append(col_id)
if not no_excel: if not no_excel and col_id not in self.table.raw_column_ids:
self.table.raw_column_ids.append(col_id) self.table.raw_column_ids.append(col_id)
self.table.insert_group(column_group) self.table.insert_group(column_group)
if column_group is not None: if column_group is not None:
@ -425,7 +432,12 @@ class Row(Element):
if title is not None: if title is not None:
self.table.add_title( self.table.add_title(
col_id, title, classes=cell.classes, raw_title=raw_title col_id,
title,
classes=cell.classes,
raw_title=raw_title,
no_excel=no_excel,
only_excel=only_excel,
) )
return cell return cell

View File

@ -6,14 +6,15 @@
"""Liste simple d'étudiants """Liste simple d'étudiants
""" """
import datetime
from flask import g, url_for from flask import g, url_for
from app import log from app import log
from app.models import FormSemestre, Identite, Justificatif from app.models import FormSemestre, Identite, Justificatif
from app.tables import table_builder as tb from app.tables import table_builder as tb
import app.scodoc.sco_assiduites as scass
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
import app.scodoc.sco_assiduites as scass
from app.scodoc.sco_exceptions import ScoValueError
class TableAssi(tb.Table): class TableAssi(tb.Table):
@ -39,7 +40,13 @@ class TableAssi(tb.Table):
): ):
self.rows: list["RowAssi"] = [] # juste pour que VSCode nous aide sur .rows self.rows: list["RowAssi"] = [] # juste pour que VSCode nous aide sur .rows
classes = ["gt_table"] classes = ["gt_table"]
self.dates = [str(dates[0]) + "T00:00", str(dates[1]) + "T23:59"] try:
self.dates = [
datetime.datetime.fromisoformat(str(dates[0]) + "T00:00"),
datetime.datetime.fromisoformat(str(dates[1]) + "T00:00"),
]
except ValueError as exc:
raise ScoValueError("invalid dates") from exc
self.formsemestre = formsemestre self.formsemestre = formsemestre
self.formsemestre_modimpls = formsemestre_modimpls self.formsemestre_modimpls = formsemestre_modimpls
if convert_values: if convert_values:
@ -97,6 +104,20 @@ class RowAssi(tb.Row):
bilan_etud = url_for( bilan_etud = url_for(
"assiduites.bilan_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id "assiduites.bilan_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id
) )
self.add_cell(
"etudid",
"etudid",
etud.etudid,
"etudinfo",
only_excel=True,
)
self.add_cell(
"code_nip",
"code_nip",
etud.code_nip,
"etudinfo",
only_excel=True,
)
self.add_cell( self.add_cell(
"nom_disp", "nom_disp",
"Nom", "Nom",

View File

@ -0,0 +1,51 @@
{% extends "base.j2" %}
{% import 'wtf.j2' as wtf %}
{% block app_content %}
<h1>{{title}}</h1>
<div class="help">
<p>
</p>
<p>
</p>
</div>
<div class="row">
<div class="col-md-8">
<form class="form form-horizontal spacediv" method="post" enctype="multipart/form-data" role="form">
{{ form.hidden_tag() }}
{{ wtf.form_errors(form, hiddens="only") }}
{% if is_enabled %}
<p>Le module <em>relations entreprises</em> est actuellement activé.</p>
<p>Il peut être activé ou désactivé à tout moment sans aucune perte de
données (la désactivation le fait simplement disparaitre des pages
utilisateurs).
<p>
{% else %}
<p>Le module <em>relations entreprises</em> est actuellement désactivé.
</p>
<p>Il peut être activé ou désactivé à tout moment sans aucune perte de
données (la désactivation le fait simplement disparaitre des pages
utilisateurs).
<p>
<p>
Lors de son activation, vous pouvez (re)positionner les rôles qu'il utilise
à leurs valeurs par défaut en cochant la case ci-dessous.
</p>
{{ wtf.form_field(form.set_default_roles_permission) }}
{% endif %}
<div class="form-group spacediv">
{{ wtf.form_field(form.submit) }}
{{ wtf.form_field(form.cancel) }}
</div>
</div>
</div>
<style>
.spacediv {
margin-top: 16px;
}
</style>
{% endblock %}

View File

@ -51,8 +51,6 @@ Calendrier de l'assiduité
<div class="dayline"> <div class="dayline">
<div class="dayline-title"> <div class="dayline-title">
<span>Assiduité du</span>
<br>
<span>{{jour.get_date()}}</span> <span>{{jour.get_date()}}</span>
{{jour.generate_minitimeline() | safe}} {{jour.generate_minitimeline() | safe}}
</div> </div>
@ -149,7 +147,7 @@ Calendrier de l'assiduité
list-style-type: none; list-style-type: none;
} }
.pageContent { .pageContent {
margin-top: 1vh; margin-top: 1vh;
@ -158,7 +156,7 @@ Calendrier de l'assiduité
.calendrier { .calendrier {
display: flex; display: flex;
justify-content: start; justify-content: center;
overflow-x: scroll; overflow-x: scroll;
border: 1px solid #444; border: 1px solid #444;
border-radius: 12px; border-radius: 12px;
@ -335,7 +333,7 @@ Calendrier de l'assiduité
document.querySelectorAll('[assi_id]').forEach((el, i) => { document.querySelectorAll('[assi_id]').forEach((el, i) => {
el.addEventListener('click', () => { el.addEventListener('click', () => {
const assi_id = el.getAttribute('assi_id'); const assi_id = el.getAttribute('assi_id');
window.open(`${SCO_URL}/Assiduites/tableau_assiduite_actions?type=assiduite&action=details&obj_id=${assi_id}`); window.open(`${SCO_URL}Assiduites/tableau_assiduite_actions?type=assiduite&action=details&obj_id=${assi_id}`);
}) })
}); });

View File

@ -67,7 +67,7 @@
text-align: center; text-align: center;
border: 1px solid #ddd; border: 1px solid #ddd;
} }
.cell, .header { .cell, .header {
border: 1px solid #ddd; border: 1px solid #ddd;
padding: 10px; padding: 10px;
@ -111,7 +111,7 @@
display: flex; display: flex;
gap: 4px; gap: 4px;
} }
.pointer{ .pointer{
cursor: pointer; cursor: pointer;
} }
@ -220,7 +220,7 @@ async function nouvellePeriode(period = null) {
let periodeDiv = document.createElement("div"); let periodeDiv = document.createElement("div");
periodeDiv.classList.add("cell", "header"); periodeDiv.classList.add("cell", "header");
periodeDiv.id = `periode-${periodId}`; periodeDiv.id = `periode-${periodId}`;
const periodP = document.createElement("p"); const periodP = document.createElement("p");
periodP.textContent = `Plage du ${date} de ${debut} à ${fin}`; periodP.textContent = `Plage du ${date} de ${debut} à ${fin}`;
@ -402,12 +402,12 @@ function sauvegarderAssiduites() {
await nouvellePeriode(periode); await nouvellePeriode(periode);
} }
// Si il y n'a pas d'erreur, on affiche un message de succès // Si il n'y a pas d'erreur, on affiche un message de succès
if (data.errors.length == 0) { if (data.errors.length == 0) {
const span = document.createElement("span"); const span = document.createElement("span");
span.textContent = "Les assiduités ont bien été sauvegardées."; span.textContent = "Le relevé d'assiduité a été enregistré.";
openAlertModal( openAlertModal(
"Sauvegarde des assiduités", "Enregistrement de l'assiduité",
span, span,
null, null,
"var(--color-present)" "var(--color-present)"
@ -597,7 +597,7 @@ main();
</label> </label>
<label for="etatDef"> <label for="etatDef">
Intialiser les étudiants comme : Intialiser les étudiants comme :
<select name="etatDef" id="etatDef"> <select name="etatDef" id="etatDef">
<option value="">-</option> <option value="">-</option>
<option value="present">présents</option> <option value="present">présents</option>
@ -605,13 +605,13 @@ main();
<option value="absent">absents</option> <option value="absent">absents</option>
</select> </select>
</label> </label>
</div> </div>
<!-- Tableau à double entrée <!-- Tableau à double entrée
Colonne : Etudiants (Header = Nom, Prénom, Photo (si actif)) Colonne : Etudiants (Header = Nom, Prénom, Photo (si actif))
Ligne : Période (Header = Jour, Heure de début, Heure de fin, ModuleImplId) Ligne : Période (Header = Jour, Heure de début, Heure de fin, ModuleImplId)
Contenu : Contenu :
- bouton assiduité (présent, retard, absent) - bouton assiduité (présent, retard, absent)
- Bouton conflit si conflit de période - Bouton conflit si conflit de période
---> --->

View File

@ -25,7 +25,7 @@
setupTimeLine(()=>{creerTousLesEtudiants(etuds)}) setupTimeLine(()=>{creerTousLesEtudiants(etuds)})
{% endif %} {% endif %}
const nonWorkDays = [{{ nonworkdays| safe }}]; const nonWorkDays = [{{ nonworkdays| safe }}];
const readOnly = {{ readonly }}; const readOnly = {{ readonly }};
@ -61,7 +61,7 @@
$('#date').on('change', async function(d) { $('#date').on('change', async function(d) {
// On vérifie si la date est un jour travaillé // On vérifie si la date est un jour travaillé
dateCouranteEstTravaillee(); dateCouranteEstTravaillee();
await recupAssiduites(etuds, $("#date").datepicker("getDate")); await recupAssiduites(etuds, $("#date").datepicker("getDate"));
@ -87,7 +87,7 @@
await recupAssiduites(etuds, $("#date").datepicker("getDate")); await recupAssiduites(etuds, $("#date").datepicker("getDate"));
} }
creerTousLesEtudiants(etuds); creerTousLesEtudiants(etuds);
// affichage ou non des PDP // affichage ou non des PDP
afficherPDP(localStorage.getItem("scodoc-etud-pdp") == "true" ) afficherPDP(localStorage.getItem("scodoc-etud-pdp") == "true" )
} }
@ -168,9 +168,10 @@
<input type="checkbox" value="remove" name="mass_btn_assiduites" id="mass_rbtn_aucun" <input type="checkbox" value="remove" name="mass_btn_assiduites" id="mass_rbtn_aucun"
class="rbtn aucun" onclick="mettreToutLeMonde('vide', this)" title="Supprimer"> class="rbtn aucun" onclick="mettreToutLeMonde('vide', this)" title="Supprimer">
</fieldset> </fieldset>
<em>Les saisies ci-dessous sont enregistrées au fur et à mesure.</em>
</div> </div>
{% endif %} {% endif %}
<div class="etud_holder"> <div class="etud_holder">
<p class="placeholder"> <p class="placeholder">
@ -181,7 +182,7 @@
{% include "assiduites/widgets/alert.j2" %} {% include "assiduites/widgets/alert.j2" %}
{% include "assiduites/widgets/prompt.j2" %} {% include "assiduites/widgets/prompt.j2" %}
{% include "assiduites/widgets/conflict.j2" %} {% include "assiduites/widgets/conflict.j2" %}
</section> </section>

View File

@ -102,6 +102,6 @@
<script src="{{scu.STATIC_DIR}}/js/scodoc.js"></script> <script src="{{scu.STATIC_DIR}}/js/scodoc.js"></script>
<script> <script>
const SCO_URL = "{% if g.scodoc_dept %}{{ const SCO_URL = "{% if g.scodoc_dept %}{{
url_for('scolar.index_html', scodoc_dept=g.scodoc_dept)[:-11] }}{% endif %}"; url_for('scolar.index_html', scodoc_dept=g.scodoc_dept)}}{% endif %}";
</script> </script>
{% endblock %} {% endblock %}

View File

@ -39,6 +39,18 @@ Heure: <b><tt>{{ time.strftime("%d/%m/%Y %H:%M") }}</tt></b>
<div class="scobox"> <div class="scobox">
<div class="scobox-title">ScoDoc : paramètres généraux</div> <div class="scobox-title">ScoDoc : paramètres généraux</div>
<div style="margin-top: 16px;">
Le module <em>Relations Entreprises</em>
{% if is_entreprises_enabled %}
est <b>activé</b>
{% else %}
n'est pas activé
{% endif %}
: <a class="stdlink" href="{{url_for('scodoc.activate_entreprises')
}}">{% if is_entreprises_enabled %}le désactiver{%else%}l'activer{%endif%}</a>
</div>
<form id="configuration_form_scodoc" class="sco-form" action="" method="post" enctype="multipart/form-data" novalidate> <form id="configuration_form_scodoc" class="sco-form" action="" method="post" enctype="multipart/form-data" novalidate>
{{ form_scodoc.hidden_tag() }} {{ form_scodoc.hidden_tag() }}
<div class="row"> <div class="row">

View File

@ -152,7 +152,7 @@ document.addEventListener('DOMContentLoaded', function() {
calendar = new Calendar(container, options); calendar = new Calendar(container, options);
fetch(`${SCO_URL}/../api/formsemestre/{{formsemestre.id}}/edt?{{groups_query_args|safe}}&show_modules_titles={{show_modules_titles}}`) fetch(`${SCO_URL}../api/formsemestre/{{formsemestre.id}}/edt?{{groups_query_args|safe}}&show_modules_titles={{show_modules_titles}}`)
.then(r=>{return r.json()}) .then(r=>{return r.json()})
.then(events=>{ .then(events=>{
if (typeof events == 'string') { if (typeof events == 'string') {

View File

@ -17,8 +17,8 @@ et permet de les effacer une par une.
<p class="help"> <p class="help">
<b>Attention</b>, il vous appartient de vérifier la cohérence du résultat ! <b>Attention</b>, il vous appartient de vérifier la cohérence du résultat !
En principe, <b>l'usage de cette page devrait rester exceptionnel</b>. En principe, <b>l'usage de cette page devrait rester exceptionnel</b>.
Aucune annulation n'est ici possible (vous devrez re-saisir les décisions via les Aucune annulation n'est ici possible (vous devrez re-saisir les décisions via les
pages de saisie de jury habituelles). pages de saisie de jury habituelles).
</p> </p>
{% if sem_vals.first() %} {% if sem_vals.first() %}
<div class="jury_decisions_list jury_decisions_sems"> <div class="jury_decisions_list jury_decisions_sems">
@ -27,7 +27,7 @@ pages de saisie de jury habituelles).
{% for v in sem_vals %} {% for v in sem_vals %}
<li>{{v.html()|safe}} <li>{{v.html()|safe}}
<form> <form>
<button <button
data-v_id="{{v.id}}" data-type="validation_formsemestre" data-etudid="{{etud.id}}" data-v_id="{{v.id}}" data-type="validation_formsemestre" data-etudid="{{etud.id}}"
>effacer</button></form> >effacer</button></form>
</li> </li>
@ -101,8 +101,8 @@ pages de saisie de jury habituelles).
{% endif %} {% endif %}
{% if not( {% if not(
sem_vals.first() or ue_vals.first() or rcue_vals.first() sem_vals.first() or ue_vals.first() or rcue_vals.first()
or annee_but_vals.first() or autorisations.first()) or annee_but_vals.first() or autorisations.first())
%} %}
<div> <div>
<p class="fontred">aucune décision enregistrée</p> <p class="fontred">aucune décision enregistrée</p>
@ -123,7 +123,7 @@ pages de saisie de jury habituelles).
<script> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const buttons = document.querySelectorAll('.jury_decisions_list button'); const buttons = document.querySelectorAll('.jury_decisions_list button');
buttons.forEach(button => { buttons.forEach(button => {
button.addEventListener('click', (event) => { button.addEventListener('click', (event) => {
// Handle button click event here // Handle button click event here
@ -132,10 +132,10 @@ document.addEventListener('DOMContentLoaded', () => {
const v_id = event.target.dataset.v_id; const v_id = event.target.dataset.v_id;
const validation_type = event.target.dataset.type; const validation_type = event.target.dataset.type;
if (confirm("Supprimer cette validation ?")) { if (confirm("Supprimer cette validation ?")) {
fetch(`${SCO_URL}/../api/etudiant/${etudid}/jury/${validation_type}/${v_id}/delete`, fetch(`${SCO_URL}../api/etudiant/${etudid}/jury/${validation_type}/${v_id}/delete`,
{ {
method: "POST", method: "POST",
}).then(response => { }).then(response => {
// Handle the response // Handle the response
if (response.ok) { if (response.ok) {
location.reload(); location.reload();

View File

@ -51,7 +51,7 @@
<script> <script>
window.onload = function () { enableTooltips("gtrcontent") }; window.onload = function () { enableTooltips("gtrcontent") };
const SCO_URL = "{{ url_for('scolar.index_html', scodoc_dept=g.scodoc_dept)[:-11] }}"; const SCO_URL = "{{ url_for('scolar.index_html', scodoc_dept=g.scodoc_dept) }}";
</script> </script>
{% endblock %} {% endblock %}

View File

@ -237,7 +237,7 @@ span.calendarEdit {
<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="{{scu.ScoURL() <a class="stdlink" href="{{url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
}}/groups_auto_repartition/${partition.id}">Répartir les étudiants</a> }}/groups_auto_repartition/${partition.id}">Répartir les étudiants</a>
</label> </label>
</div> </div>

View File

@ -181,7 +181,7 @@ def add_billets_absence_form(etudid):
if tf[0] == 0: if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer() return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
elif tf[0] == -1: elif tf[0] == -1:
return flask.redirect(scu.ScoURL()) return flask.redirect(url_for("scolar.index_html", scodoc_dept=g.scodoc_dept))
else: else:
e = tf[2]["begin"].split("/") e = tf[2]["begin"].split("/")
begin = e[2] + "-" + e[1] + "-" + e[0] + " 00:00:00" begin = e[2] + "-" + e[1] + "-" + e[0] + " 00:00:00"
@ -407,7 +407,7 @@ def process_billet_absence_form(billet_id: int):
return "\n".join(H) + "<br>" + tf[1] + F + html_sco_header.sco_footer() return "\n".join(H) + "<br>" + tf[1] + F + html_sco_header.sco_footer()
elif tf[0] == -1: elif tf[0] == -1:
return flask.redirect(scu.ScoURL()) return flask.redirect(url_for("scolar.index_html", scodoc_dept=g.scodoc_dept))
else: else:
n = _ProcessBilletAbsence(billet, tf[2]["estjust"], tf[2]["description"]) n = _ProcessBilletAbsence(billet, tf[2]["estjust"], tf[2]["description"])
if tf[2]["estjust"]: if tf[2]["estjust"]:

View File

@ -1440,7 +1440,6 @@ def visu_assi_group():
formsemestre_modimpls=formsemestre_modimpls, formsemestre_modimpls=formsemestre_modimpls,
convert_values=(fmt == "html"), convert_values=(fmt == "html"),
) )
# Export en XLS # Export en XLS
if fmt.startswith("xls"): if fmt.startswith("xls"):
return scu.send_file( return scu.send_file(

View File

@ -685,7 +685,7 @@ def module_clone():
# #
@bp.route("/") @bp.route("/")
@bp.route("/index_html") @bp.route("/index_html", alias=True)
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def index_html(): def index_html():
@ -807,7 +807,7 @@ def formation_import_xml_form():
{ html_sco_header.sco_footer() } { html_sco_header.sco_footer() }
""" """
elif tf[0] == -1: elif tf[0] == -1:
return flask.redirect(scu.NotesURL()) return flask.redirect(url_for("notes.index_html", scodoc_dept=g.scodoc_dept))
else: else:
formation_id, _, _ = sco_formations.formation_import_xml( formation_id, _, _ = sco_formations.formation_import_xml(
tf[2]["xmlfile"].read() tf[2]["xmlfile"].read()

View File

@ -53,6 +53,7 @@ from werkzeug.exceptions import BadRequest, NotFound
from app import db, log from app import db, log
from app import entreprises
from app.auth.models import User, Role from app.auth.models import User, Role
from app.auth.cas import set_cas_configuration from app.auth.cas import set_cas_configuration
from app.decorators import ( from app.decorators import (
@ -62,6 +63,7 @@ from app.decorators import (
) )
from app.forms.generic import SimpleConfirmationForm from app.forms.generic import SimpleConfirmationForm
from app.forms.main import config_logos, config_main from app.forms.main import config_logos, config_main
from app.forms.main.activate_entreprises import ActivateEntreprisesForm
from app.forms.main.config_assiduites import ConfigAssiduitesForm from app.forms.main.config_assiduites import ConfigAssiduitesForm
from app.forms.main.config_apo import CodesDecisionsForm from app.forms.main.config_apo import CodesDecisionsForm
from app.forms.main.config_cas import ConfigCASForm from app.forms.main.config_cas import ConfigCASForm
@ -484,6 +486,38 @@ def config_personalized_links():
) )
@bp.route("/ScoDoc/activate_entreprises", methods=["GET", "POST"])
@admin_required
def activate_entreprises():
"""Form activation module entreprises"""
is_enabled = ScoDocSiteConfig.is_entreprises_enabled()
form = ActivateEntreprisesForm(
data={
"set_default_roles_permission": True,
}
)
if request.method == "POST" and form.cancel.data: # cancel button
return redirect(url_for("scodoc.configuration"))
if form.validate_on_submit():
if entreprises.activate_module(
enable=not is_enabled,
set_default_roles_permission=form.data["set_default_roles_permission"],
):
flash("Module entreprise " + ("activé" if not is_enabled else "désactivé"))
return redirect(url_for("scodoc.configuration"))
if is_enabled:
form.submit.label.text = "Désactiver le module relations entreprises"
else:
form.submit.label.text = "Activer le module relations entreprises"
return render_template(
"activate_entreprises.j2",
form=form,
is_enabled=is_enabled,
title="Activation module Relations Entreprises",
)
@bp.route("/ScoDoc/table_etud_in_accessible_depts", methods=["POST"]) @bp.route("/ScoDoc/table_etud_in_accessible_depts", methods=["POST"])
@login_required @login_required
def table_etud_in_accessible_depts(): def table_etud_in_accessible_depts():

View File

@ -340,8 +340,8 @@ def showEtudLog(etudid, fmt="html"):
# ---------- PAGE ACCUEIL (listes) -------------- # ---------- PAGE ACCUEIL (listes) --------------
@bp.route("/", alias=True) @bp.route("/")
@bp.route("/index_html") @bp.route("/index_html", alias=True)
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@scodoc7func @scodoc7func
@ -1954,7 +1954,7 @@ def etudident_delete(etudid: int = -1, dialog_confirmed=False):
for formsemestre_id in formsemestre_ids_to_inval: for formsemestre_id in formsemestre_ids_to_inval:
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id) sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id)
flash("Étudiant supprimé !") flash("Étudiant supprimé !")
return flask.redirect(scu.ScoURL()) return flask.redirect(url_for("scolar.index_html", scodoc_dept=g.scodoc_dept))
@bp.route("/check_group_apogee") @bp.route("/check_group_apogee")
@ -2148,7 +2148,7 @@ def form_students_import_excel(formsemestre_id=None):
) )
else: else:
sem = None sem = None
dest_url = scu.ScoURL() dest_url = url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
if sem and not sem["etat"]: if sem and not sem["etat"]:
raise ScoValueError("Modification impossible: semestre verrouille") raise ScoValueError("Modification impossible: semestre verrouille")
H = [ H = [
@ -2183,13 +2183,15 @@ def form_students_import_excel(formsemestre_id=None):
) )
else: else:
H.append( H.append(
""" f"""
<p>Pour inscrire directement les étudiants dans un semestre de <p>Pour inscrire directement les étudiants dans un semestre de
formation, il suffit d'indiquer le code de ce semestre formation, il suffit d'indiquer le code de ce semestre
(qui doit avoir été créé au préalable). <a class="stdlink" href="%s?showcodes=1">Cliquez ici pour afficher les codes</a> (qui doit avoir été créé au préalable).
<a class="stdlink" href="{
url_for("scolar.index_html", showcodes=1, scodoc_dept=g.scodoc_dept)
}">Cliquez ici pour afficher les codes</a>
</p> </p>
""" """
% (scu.ScoURL())
) )
H.append("""<ol><li>""") H.append("""<ol><li>""")
@ -2414,9 +2416,11 @@ def form_students_import_infos_admissions(formsemestre_id=None):
return "\n".join(H) + tf[1] + help_text + F return "\n".join(H) + tf[1] + help_text + F
elif tf[0] == -1: elif tf[0] == -1:
return flask.redirect( return flask.redirect(
scu.ScoURL() url_for(
+ "/formsemestre_status?formsemestre_id=" "notes.formsemestre_status",
+ str(formsemestre_id) scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
) )
else: else:
return sco_import_etuds.students_import_admission( return sco_import_etuds.students_import_admission(

View File

@ -132,7 +132,7 @@ class Mode(IntEnum):
@bp.route("/") @bp.route("/")
@bp.route("/index_html") @bp.route("/index_html", alias=True)
@scodoc @scodoc
@permission_required(Permission.UsersView) @permission_required(Permission.UsersView)
@scodoc7func @scodoc7func
@ -504,7 +504,7 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
if g.scodoc_dept in selectable_dept_acronyms if g.scodoc_dept in selectable_dept_acronyms
else (auth_dept or "") else (auth_dept or "")
) )
if len(selectable_dept_acronyms) > 1: if len(selectable_dept_acronyms) > 0:
selectable_dept_acronyms = sorted(list(selectable_dept_acronyms)) selectable_dept_acronyms = sorted(list(selectable_dept_acronyms))
descr.append( descr.append(
( (
@ -529,7 +529,7 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
{ {
"input_type": "separator", "input_type": "separator",
"title": f"""L'utilisateur appartient au département { "title": f"""L'utilisateur appartient au département {
the_user.dept or "(tous)"}""", the_user.dept or "(tous/aucun)"}""",
}, },
) )
) )
@ -539,7 +539,7 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
"d", "d",
{ {
"input_type": "separator", "input_type": "separator",
"title": f"L'utilisateur sera créé dans le département {auth_dept}", "title": f"L'utilisateur sera créé dans le département {auth_dept or 'aucun'}",
}, },
) )
) )
@ -605,7 +605,7 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
if tf[0] == 0: if tf[0] == 0:
return "\n".join(H) + "\n" + tf[1] + F return "\n".join(H) + "\n" + tf[1] + F
elif tf[0] == -1: elif tf[0] == -1:
return flask.redirect(scu.UsersURL()) return flask.redirect(url_for("users.index_html", scodoc_dept=g.scodoc_dept))
else: else:
vals = tf[2] vals = tf[2]
roles = set(vals["roles"]).intersection(editable_roles_strings) roles = set(vals["roles"]).intersection(editable_roles_strings)
@ -1080,28 +1080,28 @@ def change_password(user_name, password, password2):
# #
# ici page simplifiee car on peut ne plus avoir # ici page simplifiee car on peut ne plus avoir
# le droit d'acceder aux feuilles de style # le droit d'acceder aux feuilles de style
H.append( return f"""<?xml version="1.0" encoding="{scu.SCO_ENCODING}"?>
"""<h2>Changement effectué !</h2> <!DOCTYPE html>
<p>Ne notez pas ce mot de passe, mais mémorisez le !</p>
<p>Rappel: il est <b>interdit</b> de communiquer son mot de passe à
un tiers, même si c'est un collègue de confiance !</p>
<p><b>Si vous n'êtes pas administrateur, le système va vous redemander
votre login et nouveau mot de passe au prochain accès.</b>
</p>"""
)
return (
f"""<?xml version="1.0" encoding="{scu.SCO_ENCODING}"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html> <html>
<head> <head>
<title>Mot de passe changé</title> <title>Mot de passe changé</title>
<meta http-equiv="Content-Type" content="text/html; charset={scu.SCO_ENCODING}" /> <meta http-equiv="Content-Type" content="text/html; charset={scu.SCO_ENCODING}" />
<body><h1>Mot de passe changé !</h1> <body>
<h1>Mot de passe changé !</h1>
<h2>Changement effectué</h2>
<p>Ne notez pas ce mot de passe, mais mémorisez le !</p>
<p>Rappel: il est <b>interdit</b> de communiquer son mot de passe à
un tiers, même si c'est un collègue de confiance !</p>
<p><b>Si vous n'êtes pas administrateur, le système va vous redemander
votre login et nouveau mot de passe au prochain accès.</b>
</p>
<a href="{
url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
}" class="stdlink">Continuer</a>
</body>
</html>
""" """
+ "\n".join(H)
+ f'<a href="{scu.ScoURL()}" class="stdlink">Continuer</a></body></html>'
)
return html_sco_header.sco_header() + "\n".join(H) + F return html_sco_header.sco_header() + "\n".join(H) + F

View File

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

View File

@ -255,8 +255,20 @@ def test_etudiants_by_name(api_headers):
etuds = r.json() etuds = r.json()
assert etuds == [] assert etuds == []
# #
admin_header = get_auth_headers(API_USER_ADMIN, API_PASSWORD_ADMIN)
args = {
"prenom": "Prénom",
"nom": "Réçier",
"dept": DEPT_ACRONYM,
"civilite": "X",
}
_ = POST_JSON(
"/etudiant/create",
args,
headers=admin_header,
)
r = requests.get( r = requests.get(
API_URL + "/etudiants/name/REG", API_URL + "/etudiants/name/REC",
headers=api_headers, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
timeout=scu.SCO_TEST_API_TIMEOUT, timeout=scu.SCO_TEST_API_TIMEOUT,
@ -264,7 +276,7 @@ def test_etudiants_by_name(api_headers):
assert r.status_code == 200 assert r.status_code == 200
etuds = r.json() etuds = r.json()
assert len(etuds) == 1 assert len(etuds) == 1
assert etuds[0]["nom"] == "GNIER" assert etuds[0]["nom"] == "ÇIER"
def test_etudiant_annotations(api_headers): def test_etudiant_annotations(api_headers):

View File

@ -708,6 +708,7 @@ def test_formsemestre_resultat(api_headers):
""" """
# Test brutal: compare les texts des json (après suppression des espaces et tabs) # Test brutal: compare les texts des json (après suppression des espaces et tabs)
# ce test cassera à la moindre modification :-) # ce test cassera à la moindre modification :-)
# Pour regénérer le fichier de référence, récupérer venv/res.json
formsemestre_id = 1 formsemestre_id = 1
r = requests.get( r = requests.get(
f"{API_URL}/formsemestre/{formsemestre_id}/resultats", f"{API_URL}/formsemestre/{formsemestre_id}/resultats",

View File

@ -12,6 +12,7 @@ from app.auth.models import User, Role
from app.auth.models import get_super_admin from app.auth.models import get_super_admin
from app.scodoc import notesdb as ndb from app.scodoc import notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.views import ScoData
RESOURCES_DIR = "/opt/scodoc/tests/ressources" RESOURCES_DIR = "/opt/scodoc/tests/ressources"

View File

@ -4,13 +4,14 @@
"code_nip": "11", "code_nip": "11",
"rang": "1", "rang": "1",
"civilite_str": "Mme", "civilite_str": "Mme",
"nom_disp": "FLEURY", "nom_disp": "BONHOMME",
"prenom": "MADELEINE", "prenom": "MADELEINE",
"code_cursus": "S1", "nom_short": "BONHOMME Ma.",
"ues_validables": "3/3", "partitions": {
"1": 1
},
"sort_key": "bonhomme;madeleine",
"moy_gen": "14.36", "moy_gen": "14.36",
"nbabs": 5,
"nbabsjust": 1,
"moy_ue_1": "14.94", "moy_ue_1": "14.94",
"moy_res_1_1": "~", "moy_res_1_1": "~",
"moy_res_3_1": "11.97", "moy_res_3_1": "11.97",
@ -48,27 +49,28 @@
"moy_res_21_3": "~", "moy_res_21_3": "~",
"moy_sae_14_3": "17.83", "moy_sae_14_3": "17.83",
"moy_sae_15_3": "~", "moy_sae_15_3": "~",
"ues_validables": "3/3",
"nbabs": 0,
"nbabsjust": 0,
"code_cursus": "S1",
"bac": "", "bac": "",
"specialite": "", "specialite": "",
"sort_key": "fleury;madeleine",
"type_admission": "", "type_admission": "",
"classement": "", "classement": ""
"partitions": {
"1": 1
}
}, },
{ {
"etudid": 8, "etudid": 8,
"code_nip": "NIP8", "code_nip": "NIP8",
"rang": "2", "rang": "2",
"civilite_str": "M.", "civilite_str": "M.",
"nom_disp": "SAUNIER", "nom_disp": "JAMES",
"prenom": "JACQUES", "prenom": "JACQUES",
"code_cursus": "S1", "nom_short": "JAMES Ja.",
"ues_validables": "3/3", "partitions": {
"1": 1
},
"sort_key": "james;jacques",
"moy_gen": "12.67", "moy_gen": "12.67",
"nbabs": 3,
"nbabsjust": 1,
"moy_ue_1": "13.51", "moy_ue_1": "13.51",
"moy_res_1_1": "~", "moy_res_1_1": "~",
"moy_res_3_1": "03.27", "moy_res_3_1": "03.27",
@ -106,27 +108,28 @@
"moy_res_21_3": "~", "moy_res_21_3": "~",
"moy_sae_14_3": "10.74", "moy_sae_14_3": "10.74",
"moy_sae_15_3": "~", "moy_sae_15_3": "~",
"ues_validables": "3/3",
"nbabs": 2,
"nbabsjust": 0,
"code_cursus": "S1",
"bac": "", "bac": "",
"specialite": "", "specialite": "",
"sort_key": "saunier;jacques",
"type_admission": "", "type_admission": "",
"classement": "", "classement": ""
"partitions": {
"1": 1
}
}, },
{ {
"etudid": 6, "etudid": 6,
"code_nip": "NIP6", "code_nip": "NIP6",
"rang": "3", "rang": "3",
"civilite_str": "", "civilite_str": "",
"nom_disp": "LENFANT", "nom_disp": "THIBAUD",
"prenom": "MAXIME", "prenom": "MAXIME",
"code_cursus": "S1", "nom_short": "THIBAUD Ma.",
"ues_validables": "2/3", "partitions": {
"1": 1
},
"sort_key": "thibaud;maxime",
"moy_gen": "12.02", "moy_gen": "12.02",
"nbabs": 0,
"nbabsjust": 0,
"moy_ue_1": "14.34", "moy_ue_1": "14.34",
"moy_res_1_1": "~", "moy_res_1_1": "~",
"moy_res_3_1": "17.68", "moy_res_3_1": "17.68",
@ -164,27 +167,28 @@
"moy_res_21_3": "~", "moy_res_21_3": "~",
"moy_sae_14_3": "05.70", "moy_sae_14_3": "05.70",
"moy_sae_15_3": "~", "moy_sae_15_3": "~",
"ues_validables": "2/3",
"nbabs": 0,
"nbabsjust": 0,
"code_cursus": "S1",
"bac": "", "bac": "",
"sort_key": "lenfant;maxime",
"specialite": "", "specialite": "",
"type_admission": "", "type_admission": "",
"classement": "", "classement": ""
"partitions": {
"1": 1
}
}, },
{ {
"etudid": 7, "etudid": 7,
"code_nip": "7", "code_nip": "7",
"rang": "4", "rang": "4",
"civilite_str": "", "civilite_str": "",
"nom_disp": "CUNY", "nom_disp": "ROYER",
"prenom": "CAMILLE", "prenom": "CAMILLE",
"code_cursus": "S1", "nom_short": "ROYER Ca.",
"ues_validables": "2/3", "partitions": {
"1": 1
},
"sort_key": "royer;camille",
"moy_gen": "11.88", "moy_gen": "11.88",
"nbabs": 4,
"nbabsjust": 4,
"moy_ue_1": "07.09", "moy_ue_1": "07.09",
"moy_res_1_1": "~", "moy_res_1_1": "~",
"moy_res_3_1": "04.07", "moy_res_3_1": "04.07",
@ -222,27 +226,28 @@
"moy_res_21_3": "~", "moy_res_21_3": "~",
"moy_sae_14_3": "10.52", "moy_sae_14_3": "10.52",
"moy_sae_15_3": "~", "moy_sae_15_3": "~",
"ues_validables": "2/3",
"nbabs": 0,
"nbabsjust": 0,
"code_cursus": "S1",
"bac": "", "bac": "",
"specialite": "", "specialite": "",
"sort_key": "cuny;camille",
"type_admission": "", "type_admission": "",
"classement": "", "classement": ""
"partitions": {
"1": 1
}
}, },
{ {
"etudid": 12, "etudid": 12,
"code_nip": "NIP12", "code_nip": "NIP12",
"rang": "5", "rang": "5",
"civilite_str": "M.", "civilite_str": "M.",
"nom_disp": "MOUTON", "nom_disp": "GODIN",
"prenom": "CLAUDE", "prenom": "CLAUDE",
"code_cursus": "S1", "nom_short": "GODIN Cl.",
"ues_validables": "1/3", "partitions": {
"1": 1
},
"sort_key": "godin;claude",
"moy_gen": "10.52", "moy_gen": "10.52",
"nbabs": 1,
"nbabsjust": 0,
"moy_ue_1": "08.93", "moy_ue_1": "08.93",
"moy_res_1_1": "~", "moy_res_1_1": "~",
"moy_res_3_1": "07.77", "moy_res_3_1": "07.77",
@ -280,27 +285,28 @@
"moy_res_21_3": "~", "moy_res_21_3": "~",
"moy_sae_14_3": "11.09", "moy_sae_14_3": "11.09",
"moy_sae_15_3": "~", "moy_sae_15_3": "~",
"ues_validables": "1/3",
"nbabs": 3,
"nbabsjust": 0,
"code_cursus": "S1",
"bac": "", "bac": "",
"sort_key": "mouton;claude",
"specialite": "", "specialite": "",
"type_admission": "", "type_admission": "",
"classement": "", "classement": ""
"partitions": {
"1": 1
}
}, },
{ {
"etudid": 3, "etudid": 3,
"code_nip": "3", "code_nip": "3",
"rang": "6", "rang": "6",
"civilite_str": "M.", "civilite_str": "M.",
"nom_disp": "R\u00c9GNIER", "nom_disp": "CONSTANT",
"prenom": "PATRICK", "prenom": "PATRICK",
"code_cursus": "S1", "nom_short": "CONSTANT Pa.",
"ues_validables": "2/3", "partitions": {
"1": 1
},
"sort_key": "constant;patrick",
"moy_gen": "10.04", "moy_gen": "10.04",
"nbabs": 0,
"nbabsjust": 0,
"moy_ue_1": "13.06", "moy_ue_1": "13.06",
"moy_res_1_1": "~", "moy_res_1_1": "~",
"moy_res_3_1": "05.84", "moy_res_3_1": "05.84",
@ -338,27 +344,28 @@
"moy_res_21_3": "~", "moy_res_21_3": "~",
"moy_sae_14_3": "01.55", "moy_sae_14_3": "01.55",
"moy_sae_15_3": "~", "moy_sae_15_3": "~",
"ues_validables": "2/3",
"nbabs": 0,
"nbabsjust": 0,
"code_cursus": "S1",
"bac": "", "bac": "",
"specialite": "", "specialite": "",
"sort_key": "regnier;patrick",
"type_admission": "", "type_admission": "",
"classement": "", "classement": ""
"partitions": {
"1": 1
}
}, },
{ {
"etudid": 13, "etudid": 13,
"code_nip": "13", "code_nip": "13",
"rang": "7", "rang": "7",
"civilite_str": "", "civilite_str": "",
"nom_disp": "ESTEVE", "nom_disp": "TOUSSAINT",
"prenom": "ALIX", "prenom": "ALIX",
"code_cursus": "S1", "nom_short": "TOUSSAINT Al.",
"ues_validables": "1/3", "partitions": {
"1": 1
},
"sort_key": "toussaint;alix",
"moy_gen": "08.59", "moy_gen": "08.59",
"nbabs": 0,
"nbabsjust": 0,
"moy_ue_1": "07.24", "moy_ue_1": "07.24",
"moy_res_1_1": "~", "moy_res_1_1": "~",
"moy_res_3_1": "11.90", "moy_res_3_1": "11.90",
@ -396,27 +403,28 @@
"moy_res_21_3": "~", "moy_res_21_3": "~",
"moy_sae_14_3": "05.17", "moy_sae_14_3": "05.17",
"moy_sae_15_3": "~", "moy_sae_15_3": "~",
"ues_validables": "1/3",
"nbabs": 3,
"nbabsjust": 0,
"code_cursus": "S1",
"bac": "", "bac": "",
"sort_key": "esteve;alix",
"specialite": "", "specialite": "",
"type_admission": "", "type_admission": "",
"classement": "", "classement": ""
"partitions": {
"1": 1
}
}, },
{ {
"etudid": 16, "etudid": 16,
"code_nip": "NIP16", "code_nip": "NIP16",
"rang": "8", "rang": "8",
"civilite_str": "", "civilite_str": "",
"nom_disp": "GILLES", "nom_disp": "DENIS",
"prenom": "MAXIME", "prenom": "MAXIME",
"code_cursus": "S1", "nom_short": "DENIS Ma.",
"ues_validables": "0/3", "partitions": {
"1": 1
},
"sort_key": "denis;maxime",
"moy_gen": "07.21", "moy_gen": "07.21",
"nbabs": 1,
"nbabsjust": 1,
"moy_ue_1": "06.86", "moy_ue_1": "06.86",
"moy_res_1_1": "~", "moy_res_1_1": "~",
"moy_res_3_1": "~", "moy_res_3_1": "~",
@ -454,27 +462,28 @@
"moy_res_21_3": "~", "moy_res_21_3": "~",
"moy_sae_14_3": "03.32", "moy_sae_14_3": "03.32",
"moy_sae_15_3": "~", "moy_sae_15_3": "~",
"ues_validables": "0/3",
"nbabs": 0,
"nbabsjust": 0,
"code_cursus": "S1",
"bac": "", "bac": "",
"sort_key": "gilles;maxime",
"specialite": "", "specialite": "",
"type_admission": "", "type_admission": "",
"classement": "", "classement": ""
"partitions": {
"1": 1
}
}, },
{ {
"etudid": 2, "etudid": 2,
"code_nip": "NIP2", "code_nip": "NIP2",
"rang": "9", "rang": "9",
"civilite_str": "Mme", "civilite_str": "Mme",
"nom_disp": "NAUDIN", "nom_disp": "WALTER",
"prenom": "SIMONE", "prenom": "SIMONE",
"code_cursus": "S1", "nom_short": "WALTER Si.",
"ues_validables": "0/3", "partitions": {
"1": 1
},
"sort_key": "walter;simone",
"moy_gen": "07.02", "moy_gen": "07.02",
"nbabs": 5,
"nbabsjust": 3,
"moy_ue_1": "06.82", "moy_ue_1": "06.82",
"moy_res_1_1": "~", "moy_res_1_1": "~",
"moy_res_3_1": "16.91", "moy_res_3_1": "16.91",
@ -512,27 +521,28 @@
"moy_res_21_3": "~", "moy_res_21_3": "~",
"moy_sae_14_3": "02.10", "moy_sae_14_3": "02.10",
"moy_sae_15_3": "~", "moy_sae_15_3": "~",
"ues_validables": "0/3",
"nbabs": 0,
"nbabsjust": 0,
"code_cursus": "S1",
"bac": "", "bac": "",
"sort_key": "naudin;simone",
"specialite": "", "specialite": "",
"type_admission": "", "type_admission": "",
"classement": "", "classement": ""
"partitions": {
"1": 1
}
}, },
{ {
"etudid": 1, "etudid": 1,
"code_nip": "1", "code_nip": "1",
"rang": "10", "rang": "10",
"civilite_str": "", "civilite_str": "",
"nom_disp": "COSTA", "nom_disp": "GROSS",
"prenom": "SACHA", "prenom": "SACHA",
"code_cursus": "S1", "nom_short": "GROSS Sa.",
"ues_validables": "0/3", "partitions": {
"1": 1
},
"sort_key": "gross;sacha",
"moy_gen": "05.31", "moy_gen": "05.31",
"nbabs": 2,
"nbabsjust": 1,
"moy_ue_1": "03.73", "moy_ue_1": "03.73",
"moy_res_1_1": "~", "moy_res_1_1": "~",
"moy_res_3_1": "~", "moy_res_3_1": "~",
@ -570,27 +580,28 @@
"moy_res_21_3": "~", "moy_res_21_3": "~",
"moy_sae_14_3": "07.17", "moy_sae_14_3": "07.17",
"moy_sae_15_3": "~", "moy_sae_15_3": "~",
"ues_validables": "0/3",
"nbabs": 0,
"nbabsjust": 0,
"code_cursus": "S1",
"bac": "", "bac": "",
"sort_key": "costa;sacha",
"specialite": "", "specialite": "",
"type_admission": "", "type_admission": "",
"classement": "", "classement": ""
"partitions": {
"1": 1
}
}, },
{ {
"etudid": 4, "etudid": 4,
"code_nip": "NIP4", "code_nip": "NIP4",
"rang": "11 ex", "rang": "11 ex",
"civilite_str": "M.", "civilite_str": "M.",
"nom_disp": "GAUTIER", "nom_disp": "BARTHELEMY",
"prenom": "G\u00c9RARD", "prenom": "G\u00c9RARD",
"code_cursus": "S1", "nom_short": "BARTHELEMY G\u00e9.",
"ues_validables": "", "partitions": {
"1": 1
},
"sort_key": "barthelemy;gerard",
"moy_gen": "", "moy_gen": "",
"nbabs": 3,
"nbabsjust": 1,
"moy_ue_1": "", "moy_ue_1": "",
"moy_res_1_1": "", "moy_res_1_1": "",
"moy_res_3_1": "", "moy_res_3_1": "",
@ -628,27 +639,28 @@
"moy_res_21_3": "", "moy_res_21_3": "",
"moy_sae_14_3": "", "moy_sae_14_3": "",
"moy_sae_15_3": "", "moy_sae_15_3": "",
"ues_validables": "",
"nbabs": 2,
"nbabsjust": 0,
"code_cursus": "S1",
"bac": "", "bac": "",
"sort_key": "gautier;gerard",
"specialite": "", "specialite": "",
"type_admission": "", "type_admission": "",
"classement": "", "classement": ""
"partitions": {
"1": 1
}
}, },
{ {
"etudid": 5, "etudid": 5,
"code_nip": "5", "code_nip": "5",
"rang": "11 ex", "rang": "11 ex",
"civilite_str": "Mme", "civilite_str": "Mme",
"nom_disp": "VILLENEUVE", "nom_disp": "MILLOT",
"prenom": "FRAN\u00c7OISE", "prenom": "FRAN\u00c7OISE",
"code_cursus": "S1", "nom_short": "MILLOT Fr.",
"ues_validables": "", "partitions": {
"1": 1
},
"sort_key": "millot;francoise",
"moy_gen": "", "moy_gen": "",
"nbabs": 0,
"nbabsjust": 0,
"moy_ue_1": "", "moy_ue_1": "",
"moy_res_1_1": "", "moy_res_1_1": "",
"moy_res_3_1": "", "moy_res_3_1": "",
@ -686,27 +698,28 @@
"moy_res_21_3": "", "moy_res_21_3": "",
"moy_sae_14_3": "", "moy_sae_14_3": "",
"moy_sae_15_3": "", "moy_sae_15_3": "",
"ues_validables": "",
"nbabs": 2,
"nbabsjust": 0,
"code_cursus": "S1",
"bac": "", "bac": "",
"sort_key": "villeneuve;francoise",
"specialite": "", "specialite": "",
"type_admission": "", "type_admission": "",
"classement": "", "classement": ""
"partitions": {
"1": 1
}
}, },
{ {
"etudid": 9, "etudid": 9,
"code_nip": "9", "code_nip": "9",
"rang": "11 ex", "rang": "11 ex",
"civilite_str": "M.", "civilite_str": "M.",
"nom_disp": "SCHMITT", "nom_disp": "BENOIT",
"prenom": "EMMANUEL", "prenom": "EMMANUEL",
"code_cursus": "S1", "nom_short": "BENOIT Em.",
"ues_validables": "", "partitions": {
"1": 1
},
"sort_key": "benoit;emmanuel",
"moy_gen": "", "moy_gen": "",
"nbabs": 0,
"nbabsjust": 0,
"moy_ue_1": "", "moy_ue_1": "",
"moy_res_1_1": "", "moy_res_1_1": "",
"moy_res_3_1": "", "moy_res_3_1": "",
@ -744,27 +757,28 @@
"moy_res_21_3": "", "moy_res_21_3": "",
"moy_sae_14_3": "", "moy_sae_14_3": "",
"moy_sae_15_3": "", "moy_sae_15_3": "",
"ues_validables": "",
"nbabs": 0,
"nbabsjust": 0,
"code_cursus": "S1",
"bac": "", "bac": "",
"sort_key": "schmitt;emmanuel",
"specialite": "", "specialite": "",
"type_admission": "", "type_admission": "",
"classement": "", "classement": ""
"partitions": {
"1": 1
}
}, },
{ {
"etudid": 10, "etudid": 10,
"code_nip": "NIP10", "code_nip": "NIP10",
"rang": "11 ex", "rang": "11 ex",
"civilite_str": "Mme", "civilite_str": "Mme",
"nom_disp": "BOUTET", "nom_disp": "LECOCQ",
"prenom": "MARGUERITE", "prenom": "MARGUERITE",
"code_cursus": "S1", "nom_short": "LECOCQ Ma.",
"ues_validables": "", "partitions": {
"1": 1
},
"sort_key": "lecocq;marguerite",
"moy_gen": "", "moy_gen": "",
"nbabs": 0,
"nbabsjust": 0,
"moy_ue_1": "", "moy_ue_1": "",
"moy_res_1_1": "", "moy_res_1_1": "",
"moy_res_3_1": "", "moy_res_3_1": "",
@ -802,27 +816,28 @@
"moy_res_21_3": "", "moy_res_21_3": "",
"moy_sae_14_3": "", "moy_sae_14_3": "",
"moy_sae_15_3": "", "moy_sae_15_3": "",
"ues_validables": "",
"nbabs": 1,
"nbabsjust": 0,
"code_cursus": "S1",
"bac": "", "bac": "",
"sort_key": "boutet;marguerite",
"specialite": "", "specialite": "",
"type_admission": "", "type_admission": "",
"classement": "", "classement": ""
"partitions": {
"1": 1
}
}, },
{ {
"etudid": 14, "etudid": 14,
"code_nip": "NIP14", "code_nip": "NIP14",
"rang": "11 ex", "rang": "11 ex",
"civilite_str": "M.", "civilite_str": "M.",
"nom_disp": "ROLLIN", "nom_disp": "ROUSSET",
"prenom": "DERC'HEN", "prenom": "DERC'HEN",
"code_cursus": "S1", "nom_short": "ROUSSET De.",
"ues_validables": "", "partitions": {
"1": 1
},
"sort_key": "rousset;derchen",
"moy_gen": "", "moy_gen": "",
"nbabs": 0,
"nbabsjust": 0,
"moy_ue_1": "", "moy_ue_1": "",
"moy_res_1_1": "", "moy_res_1_1": "",
"moy_res_3_1": "", "moy_res_3_1": "",
@ -860,27 +875,28 @@
"moy_res_21_3": "", "moy_res_21_3": "",
"moy_sae_14_3": "", "moy_sae_14_3": "",
"moy_sae_15_3": "", "moy_sae_15_3": "",
"ues_validables": "",
"nbabs": 0,
"nbabsjust": 0,
"code_cursus": "S1",
"bac": "", "bac": "",
"specialite": "", "specialite": "",
"sort_key": "rollin;derchen",
"type_admission": "", "type_admission": "",
"classement": "", "classement": ""
"partitions": {
"1": 1
}
}, },
{ {
"etudid": 15, "etudid": 15,
"code_nip": "15", "code_nip": "15",
"rang": "11 ex", "rang": "11 ex",
"civilite_str": "", "civilite_str": "",
"nom_disp": "DIOT", "nom_disp": "MORAND",
"prenom": "CAMILLE", "prenom": "CAMILLE",
"code_cursus": "S1", "nom_short": "MORAND Ca.",
"ues_validables": "", "partitions": {
"1": 1
},
"sort_key": "morand;camille",
"moy_gen": "", "moy_gen": "",
"nbabs": 4,
"nbabsjust": 2,
"moy_ue_1": "", "moy_ue_1": "",
"moy_res_1_1": "", "moy_res_1_1": "",
"moy_res_3_1": "", "moy_res_3_1": "",
@ -918,13 +934,13 @@
"moy_res_21_3": "", "moy_res_21_3": "",
"moy_sae_14_3": "", "moy_sae_14_3": "",
"moy_sae_15_3": "", "moy_sae_15_3": "",
"ues_validables": "",
"nbabs": 0,
"nbabsjust": 0,
"code_cursus": "S1",
"bac": "", "bac": "",
"sort_key": "diot;camille",
"specialite": "", "specialite": "",
"type_admission": "", "type_admission": "",
"classement": "", "classement": ""
"partitions": {
"1": 1
}
} }
] ]

View File

@ -157,10 +157,11 @@ def anonymize_users(cursor):
cursor.execute("""UPDATE "user" SET token = NULL;""") cursor.execute("""UPDATE "user" SET token = NULL;""")
cursor.execute("""UPDATE "user" SET token_expiration = NULL;""") cursor.execute("""UPDATE "user" SET token_expiration = NULL;""")
# Change les noms/prenoms/mail # Change les noms/prenoms/mail
cursor.execute("""SELECT * FROM "user";""") cursor.execute("""SELECT * FROM "user" WHERE user_name <> 'admin';""")
users = cursor.fetchall() # fetch tout car modifie cette table ds la boucle users = cursor.fetchall() # fetch tout car modifie cette table ds la boucle
nb_users = len(users)
used_user_names = {u["user_name"] for u in users} used_user_names = {u["user_name"] for u in users}
for user in users: for i, user in enumerate(users):
user_name = user["user_name"] user_name = user["user_name"]
nom, prenom = random.choice(NOMS), random.choice(PRENOMS) nom, prenom = random.choice(NOMS), random.choice(PRENOMS)
new_name = (prenom[0] + nom).lower() new_name = (prenom[0] + nom).lower()
@ -168,7 +169,7 @@ def anonymize_users(cursor):
while new_name in used_user_names: while new_name in used_user_names:
new_name += "x" new_name += "x"
used_user_names.add(new_name) used_user_names.add(new_name)
print(f"{user_name} > {new_name}") print(f"{i}/{nb_users}\t{user_name} > {new_name}")
cursor.execute( cursor.execute(
"""UPDATE "user" """UPDATE "user"
SET nom=%(nom)s, prenom=%(prenom)s, email=%(email)s, user_name=%(new_name)s SET nom=%(nom)s, prenom=%(prenom)s, email=%(email)s, user_name=%(new_name)s
@ -234,6 +235,7 @@ if __name__ == "__main__":
cursor = cnx.cursor(cursor_factory=psycopg2.extras.DictCursor) cursor = cnx.cursor(cursor_factory=psycopg2.extras.DictCursor)
anonymize_db(cursor) anonymize_db(cursor)
rename_students(cursor)
if PROCESS_USERS: if PROCESS_USERS:
anonymize_users(cursor) anonymize_users(cursor)

File diff suppressed because it is too large Load Diff