Compare commits

...

23 Commits

Author SHA1 Message Date
Emmanuel Viennet 18b1f00586 version 9.6.964 2024-04-24 20:58:02 +02:00
Iziram 6b985620e9 Assiduité : signal_assiduites_diff : plage depuis args url closes #741 2024-04-24 20:55:21 +02:00
Iziram 4d234ba353 Assiduité : désactiver saisie présence closes #793 2024-04-24 20:55:21 +02:00
Iziram 5d45fcf656 Assiduité : signal_assiduites_group : bug fix suppr assi autre 2024-04-24 20:55:21 +02:00
Iziram 0a5919b788 Assiduité : pseudo-fix (catch + http error) #872 2024-04-24 20:55:21 +02:00
Iziram 09f4525e66 Assiduité : maj couleurs minitimeline + legende 2024-04-24 20:55:21 +02:00
Emmanuel Viennet 0bc57807de release build script using gitea again 2024-04-24 20:37:03 +02:00
Emmanuel Viennet 87aaf12d27 Protect against Reflected XSS on home page (and other exception-handling pages) 2024-04-23 18:28:00 +02:00
Emmanuel Viennet c8ab9b9b6c Invalidation cache lors d'une erreur sur association UE/Niveau. Peut-être cause de #874. 2024-04-15 18:06:26 +02:00
Emmanuel Viennet ad7b48e110 Calendrier évaluations: fix #875 2024-04-15 17:53:02 +02:00
Emmanuel Viennet f2ce16f161 Archive PV: gzip large files 2024-04-15 03:21:32 +02:00
Emmanuel Viennet 1ddf9b6ab8 Fix: création utilisateur si un seul département 2024-04-12 15:50:53 +02:00
Emmanuel Viennet 0a2e39cae1 Ajoute aide sur édition parcours UEs 2024-04-12 01:10:42 +02:00
Emmanuel Viennet a194b4b6e0 Edition parcours UE: si tous cochés, tronc commun 2024-04-12 01:05:02 +02:00
Emmanuel Viennet cbe85dfb7d anonymize_users: ignore admin 2024-04-12 01:04:27 +02:00
Emmanuel Viennet beba69bfe4 Améliore/met à jour tests unitaires API 2024-04-11 06:00:00 +02:00
Emmanuel Viennet 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
Emmanuel Viennet 9bd05ea241 Modify SCO_URL in all js: no trailing slash. 2024-04-11 01:44:17 +02:00
Emmanuel Viennet 58b831513d Améliore traitement des erreurs lors de la génération des PDF 2024-04-10 15:29:30 +02:00
Emmanuel Viennet 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
Emmanuel Viennet c2443c361f Améliore page activation module entreprises. Implements #634 2024-04-09 00:36:46 +02:00
Emmanuel Viennet ab4731bd43 Suppression des anciennes fonctions ScoDoc7 donnant les URLs de base. 2024-04-08 18:57:00 +02:00
Emmanuel Viennet c17bc8b61b Fix: liste semestres avec code 2024-04-08 16:26:38 +02:00
76 changed files with 2099 additions and 1821 deletions

View File

@ -3,14 +3,15 @@
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""ScoDoc 9 API : Assiduités
"""
"""ScoDoc 9 API : Assiduités"""
from datetime import datetime
from flask import g, request
from flask_json import as_json
from flask_login import current_user, login_required
from flask_sqlalchemy.query import Query
from sqlalchemy.orm.exc import ObjectDeletedError
from app import db, log, set_sco_dept
import app.scodoc.sco_assiduites as scass
@ -858,7 +859,10 @@ def assiduite_edit(assiduite_id: int):
msg=f"assiduite: modif {assiduite_unique}",
)
db.session.commit()
scass.simple_invalidate_cache(assiduite_unique.to_dict())
try:
scass.simple_invalidate_cache(assiduite_unique.to_dict())
except ObjectDeletedError:
return json_error(404, "Assiduité supprimée / inexistante")
return {"OK": True}

View File

@ -603,8 +603,19 @@ class Role(db.Model):
"""Create default roles if missing, then, if reset_permissions,
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"
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()
if role is None:
role = Role(name=role_name)

View File

@ -37,7 +37,17 @@ def form_ue_choix_parcours(ue: UniteEns) -> str:
]
# Choix des 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 = {
ue.get_ects(parcour, only_parcours=True) for parcour in ref_comp.parcours

View File

@ -9,12 +9,14 @@
import collections
import datetime
import pandas as pd
import numpy as np
from flask import g, has_request_context, url_for
from app import db
from app.comp.moy_mod import ModuleImplResults
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.ues import UniteEns
from app.scodoc import sco_bulletins, sco_utils as scu
@ -249,59 +251,88 @@ class BulletinBUT:
# "moy": fmt_note(moyennes_etuds.mean()),
},
"evaluations": (
[
self.etud_eval_results(etud, e)
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"]
)
]
self.etud_list_modimpl_evaluations(
etud, modimpl, modimpl_results, version
)
if version != "short"
else []
),
}
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"
# 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()
modimpls_evals_poids = self.res.modimpls_evals_poids[e.moduleimpl_id]
modimpls_evals_poids = self.res.modimpls_evals_poids[evaluation.moduleimpl_id]
try:
etud_ues_ids = self.res.etud_ues_ids(etud.id)
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
if (ue.type != UE_SPORT) and (ue.id in etud_ues_ids)
}
except KeyError:
poids = collections.defaultdict(lambda: 0.0)
d = {
"id": e.id,
"id": evaluation.id,
"coef": (
fmt_note(e.coefficient)
if e.evaluation_type == Evaluation.EVALUATION_NORMALE
fmt_note(evaluation.coefficient)
if evaluation.evaluation_type == Evaluation.EVALUATION_NORMALE
else None
),
"date_debut": e.date_debut.isoformat() if e.date_debut else None,
"date_fin": e.date_fin.isoformat() if e.date_fin else None,
"description": e.description,
"evaluation_type": e.evaluation_type,
"date_debut": (
evaluation.date_debut.isoformat() if evaluation.date_debut else None
),
"date_fin": (
evaluation.date_fin.isoformat() if evaluation.date_fin else None
),
"description": evaluation.description,
"evaluation_type": evaluation.evaluation_type,
"note": (
{
"value": fmt_note(
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),
"max": fmt_note(notes_ok.max(), note_max=e.note_max),
"moy": fmt_note(notes_ok.mean(), note_max=e.note_max),
"min": fmt_note(notes_ok.min(), note_max=evaluation.note_max),
"max": fmt_note(notes_ok.max(), note_max=evaluation.note_max),
"moy": fmt_note(notes_ok.mean(), note_max=evaluation.note_max),
}
if not e.is_blocked()
if not evaluation.is_blocked()
else {}
),
"poids": poids,
@ -309,17 +340,25 @@ class BulletinBUT:
url_for(
"notes.evaluation_listenotes",
scodoc_dept=g.scodoc_dept,
evaluation_id=e.id,
evaluation_id=evaluation.id,
)
if has_request_context()
else "na"
),
# deprecated (supprimer avant #sco9.7)
"date": e.date_debut.isoformat() if e.date_debut else None,
"heure_debut": (
e.date_debut.time().isoformat("minutes") if e.date_debut else None
"date": (
evaluation.date_debut.isoformat() if evaluation.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

View File

@ -59,3 +59,4 @@ def check_taxe_now(taxes):
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):
"Panneau de configuration avancée"
enable_entreprises = BooleanField("activer le module <em>entreprises</em>")
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."""
)
@ -127,13 +126,6 @@ def configuration():
flash("Fonction bonus inchangée.")
return redirect(url_for("scodoc.index"))
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(
disabled=form_scodoc.data["disable_passerelle"]
):
@ -182,6 +174,7 @@ def configuration():
return render_template(
"configuration.j2",
is_entreprises_enabled=ScoDocSiteConfig.is_entreprises_enabled(),
form_bonus=form_bonus,
form_scodoc=form_scodoc,
scu=scu,

View File

@ -232,7 +232,9 @@ class ScolarNews(db.Model):
)
# 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)
# 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)
"""
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
prev_niveau = self.niveau_competence
if (
@ -424,6 +432,7 @@ class UniteEns(models.ScoDocModel):
self.niveau_competence, parcours
)
if not ok:
self.formation.invalidate_cached_sems()
self.niveau_competence = prev_niveau # restore
return False, error_message

View File

@ -48,6 +48,7 @@ from typing import Any
from urllib.parse import urlparse, urlencode, parse_qs, urlunparse
from openpyxl.utils import get_column_letter
import reportlab
from reportlab.platypus import Paragraph, Spacer
from reportlab.platypus import Table, KeepInFrame
from reportlab.lib.colors import Color
@ -812,7 +813,10 @@ if __name__ == "__main__":
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()
with open("/tmp/gen_table.pdf", "wb") as f:
f.write(data)

View File

@ -30,7 +30,7 @@
import html
from flask import g, render_template
from flask import g, render_template, url_for
from flask import request
from flask_login import current_user
@ -163,7 +163,7 @@ def sco_header(
params = {
"page_title": page_title or sco_version.SCONAME,
"no_side_bar": no_side_bar,
"ScoURL": scu.ScoURL(),
"ScoURL": url_for("scolar.index_html", scodoc_dept=g.scodoc_dept),
"encoding": scu.SCO_ENCODING,
"titrebandeau_mkup": "<td>" + titrebandeau + "</td>",
"authuser": current_user.user_name,
@ -220,7 +220,7 @@ def sco_header(
<script>
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}";
</script>"""
)

View File

@ -102,25 +102,33 @@ def sidebar_common():
<a href="{home_link}" class="sidebar">Accueil</a> <br>
<div id="authuser"><a id="authuserlink" href="{
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>
<br><a id="deconnectlink" href="{url_for("auth.logout")}">déconnexion</a>
</div>
{sidebar_dept()}
<h2 class="insidebar">Scolarité</h2>
<a href="{scu.ScoURL()}" class="sidebar">Semestres</a> <br>
<a href="{scu.NotesURL()}" class="sidebar">Formations</a> <br>
<a href="{
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):
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(
Permission.UsersAdmin
) or current_user.has_permission(Permission.UsersView):
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):

View File

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

View File

@ -68,7 +68,7 @@ PV_ARCHIVER = SemsArchiver()
def do_formsemestre_archive(
formsemestre_id,
formsemestre: FormSemestre,
group_ids: list[int] = None, # si indiqué, ne prend que ces groupes
description="",
date_jury="",
@ -92,9 +92,8 @@ def do_formsemestre_archive(
raise ScoValueError(
"do_formsemestre_archive: version de bulletin demandée invalide"
)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
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(
sem_archive_id, description, formsemestre.dept_id
)
@ -102,9 +101,9 @@ def do_formsemestre_archive(
if not group_ids:
# 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(
group_ids, formsemestre_id=formsemestre_id
group_ids, formsemestre_id=formsemestre.id
)
groups_filename = "-" + groups_infos.groups_filename
etudids = [m["etudid"] for m in groups_infos.members]
@ -142,19 +141,23 @@ def do_formsemestre_archive(
)
# 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)
if data:
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
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()
else: # formations classiques
data = sco_pv_forms.formsemestre_pvjury(
formsemestre_id, fmt="xls", publish=False
formsemestre.id, fmt="xls", publish=False
)
if data:
PV_ARCHIVER.store(
@ -165,7 +168,7 @@ def do_formsemestre_archive(
)
# Classeur bulletins (PDF)
data, _ = sco_bulletins_pdf.get_formsemestre_bulletins_pdf(
formsemestre_id, version=bul_version
formsemestre.id, version=bul_version
)
if data:
PV_ARCHIVER.store(
@ -173,10 +176,11 @@ def do_formsemestre_archive(
"Bulletins.pdf",
data,
dept_id=formsemestre.dept_id,
compress=True,
)
# Lettres individuelles (PDF):
data = sco_pv_lettres_inviduelles.pdf_lettres_individuelles(
formsemestre_id,
formsemestre.id,
etudids=etudids,
date_jury=date_jury,
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.
(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():
raise ScoPermissionDenied(
dest_url=url_for(
@ -320,7 +324,7 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement.
else:
tf[2]["anonymous"] = False
do_formsemestre_archive(
formsemestre_id,
formsemestre,
group_ids=group_ids,
description=tf[2]["description"],
date_jury=tf[2]["date_jury"],
@ -352,7 +356,7 @@ def formsemestre_list_archives(formsemestre_id):
"""Page listing archives"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
sem_archive_id = formsemestre_id
L = []
archives_descr = []
for archive_id in PV_ARCHIVER.list_obj_archives(
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
),
}
L.append(a)
archives_descr.append(a)
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>")
else:
H.append("<ul>")
for a in L:
for a in archives_descr:
archive_name = PV_ARCHIVER.get_archive_name(a["archive_id"])
H.append(
'<li>%s : <em>%s</em> (<a href="formsemestre_delete_archive?formsemestre_id=%s&archive_name=%s">supprimer</a>)<ul>'
% (
a["date"].strftime("%d/%m/%Y %H:%M"),
a["description"],
formsemestre_id,
archive_name,
)
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,
formsemestre_id=formsemestre_id, archive_name=archive_name
)}">supprimer</a>)
<ul>"""
)
for filename in a["content"]:
H.append(
'<li><a href="formsemestre_get_archived_file?formsemestre_id=%s&archive_name=%s&filename=%s">%s</a></li>'
% (formsemestre_id, archive_name, filename, filename)
f"""<li><a href="{
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"]:
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):
"""Send file to client."""
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
sem_archive_id = formsemestre.id
return PV_ARCHIVER.get_archived_file(
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
(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", formsemestre_id

View File

@ -61,7 +61,7 @@ from flask_login import current_user
from app.models import FormSemestre, Identite, ScoDocSiteConfig
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.scodoc import sco_formsemestre
from app.scodoc import sco_pdf
@ -228,7 +228,15 @@ class BulletinGenerator:
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()
return data

View File

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

View File

@ -192,7 +192,7 @@ def _sem_table_gt(formsemestres: Query, showcodes=False, fmt="html") -> GenTable
"elt_sem_apo",
]
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"
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"),
f"""<h2>Suppression de la formation {formation.titre} ({formation.acronyme})</h2>""",
]
sems = sco_formsemestre.do_formsemestre_list({"formation_id": formation_id})
if sems:
formsemestres = formation.formsemestres.all()
if formsemestres:
H.append(
"""<p class="warning">Impossible de supprimer cette formation,
car les sessions suivantes l'utilisent:</p>
<ul>"""
)
for sem in sems:
H.append(
'<li><a class="stdlink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titremois)s</a></li>'
% sem
)
for formsemestre in formsemestres:
H.append(f"""<li>{formsemestre.html_link_status()}</li>""")
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:
if not dialog_confirmed:
@ -85,14 +84,16 @@ def formation_delete(formation_id=None, dialog_confirmed=False):
</p>
""",
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},
)
else:
do_formation_delete(formation_id)
H.append(
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())
@ -252,7 +253,7 @@ def formation_edit(formation_id=None, create=False):
if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
elif tf[0] == -1:
return flask.redirect(scu.NotesURL())
return flask.redirect(url_for("notes.index_html", scodoc_dept=g.scodoc_dept))
else:
# check unicity : constraint UNIQUE(acronyme,titre,version)
if create:

View File

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

View File

@ -103,7 +103,7 @@ class ScoPDFFormatError(ScoValueError):
super().__init__(
f"""Erreur dans un format pdf:
<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.
</p>
""",

View File

@ -1431,18 +1431,25 @@ Ceci n'est possible que si :
def formsemestre_delete2(formsemestre_id, dialog_confirmed=False):
"""Delete a formsemestre (confirmation)"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
# Confirmation dialog
if not dialog_confirmed:
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="",
cancel_url="formsemestre_status?formsemestre_id=%s" % formsemestre_id,
parameters={"formsemestre_id": formsemestre_id},
cancel_url=url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
),
parameters={"formsemestre_id": formsemestre.id},
)
# Bon, s'il le faut...
do_formsemestre_delete(formsemestre_id)
do_formsemestre_delete(formsemestre.id)
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(

View File

@ -634,7 +634,7 @@ def formsemestre_description_table(
"UE": modimpl.module.ue.acronyme,
"_UE_td_attrs": ue_info.get("_UE_td_attrs", ""),
"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",
"Inscrits": mod_nb_inscrits,
"Responsable": sco_users.user_info(modimpl.responsable_id)["nomprenom"],

View File

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

View File

@ -458,7 +458,12 @@ def pdf_basic_page(
if title:
head = Paragraph(SU(title), StyleSheet["Heading3"])
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()
return data

View File

@ -611,16 +611,17 @@ class BasePreferences:
"explanation": "toute saisie d'absence doit indiquer le module concerné",
},
),
# (
# "forcer_present",
# {
# "initvalue": 0,
# "title": "Forcer l'appel des présents",
# "input_type": "boolcheckbox",
# "labels": ["non", "oui"],
# "category": "assi",
# },
# ),
(
"non_present",
{
"initvalue": 0,
"title": "Désactiver la saisie des présences",
"input_type": "boolcheckbox",
"labels": ["non", "oui"],
"category": "assi",
"explanation": "Désactive la saisie et l'affichage des présences",
},
),
(
"periode_defaut",
{
@ -644,18 +645,18 @@ class BasePreferences:
"category": "assi",
},
),
(
"assi_etat_defaut",
{
"explanation": "⚠ non fonctionnel, travaux en cours !",
"initvalue": "aucun",
"input_type": "menu",
"labels": ["aucun", "present", "retard", "absent"],
"allowed_values": ["aucun", "present", "retard", "absent"],
"title": "Définir l'état par défaut",
"category": "assi",
},
),
# (
# "assi_etat_defaut",
# {
# "explanation": "⚠ non fonctionnel, travaux en cours !",
# "initvalue": "aucun",
# "input_type": "menu",
# "labels": ["aucun", "present", "retard", "absent"],
# "allowed_values": ["aucun", "present", "retard", "absent"],
# "title": "Définir l'état par défaut",
# "category": "assi",
# },
# ),
(
"non_travail",
{
@ -2260,16 +2261,17 @@ class BasePreferences:
before_table="<details><summary>{title}</summary>",
after_table="</details>",
)
dest_url = url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
elif tf[0] == -1:
return flask.redirect(scu.ScoURL()) # cancel
else:
for pref in self.prefs_definition:
self.prefs[None][pref[0]] = tf[2][pref[0]]
self.save()
flash("Préférences modifiées")
return flask.redirect(scu.ScoURL())
if tf[0] == -1:
return flask.redirect(dest_url) # cancel
#
for pref in self.prefs_definition:
self.prefs[None][pref[0]] = tf[2][pref[0]]
self.save()
flash("Préférences modifiées")
return flask.redirect(dest_url)
def build_tf_form(self, categories: list[str] = None, formsemestre_id: int = None):
"""Build list of elements for TrivialFormulator.
@ -2433,10 +2435,12 @@ function set_global_pref(el, pref_name) {
before_table="<details><summary>{title}</summary>",
after_table="</details>",
)
dest_url = (
scu.NotesURL()
+ "/formsemestre_status?formsemestre_id=%s" % self.formsemestre_id
dest_url = url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=self.formsemestre_id,
)
if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
elif tf[0] == -1:
@ -2482,7 +2486,9 @@ function set_global_pref(el, pref_name) {
request.base_url + "?formsemestre_id=" + str(self.formsemestre_id)
)
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_pdf
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_pv_templates import CourrierIndividuelTemplate, jury_titres
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()
return data
@ -241,13 +245,14 @@ def pdf_lettre_individuelle(sem, decision, etud: Identite, params, signature=Non
titre_jury_court = "s"
else:
titre_jury_court = ""
params[
"autorisations_txt"
] = """Vous êtes autorisé%s à continuer dans le%s semestre%s : <b>%s</b>""" % (
etud.e,
titre_jury_court,
titre_jury_court,
decision["autorisations_descr"],
params["autorisations_txt"] = (
"""Vous êtes autorisé%s à continuer dans le%s semestre%s : <b>%s</b>"""
% (
etud.e,
titre_jury_court,
titre_jury_court,
decision["autorisations_descr"],
)
)
else:
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()
return data

View File

@ -51,7 +51,24 @@ SCO_ROLES_DEFAULTS = {
p.UsersView,
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": (p.RelationsEntrepView,),
# UtilisateurEntreprise est un utilisateur de l'application entreprise (droit de modification)
@ -70,19 +87,10 @@ SCO_ROLES_DEFAULTS = {
p.RelationsEntrepValidate,
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
# - associés à un département:
ROLES_ATTRIBUABLES_DEPT = ("Ens", "Secr", "Admin", "RespPe")

View File

@ -47,12 +47,11 @@ from app import db, log
from app.models import Identite
import app.scodoc.sco_utils as scu
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 import html_sco_header
from app.scodoc import htmlutils
from app.scodoc import sco_import_etuds
from app.scodoc import sco_etud
from app.scodoc import sco_excel
from app.scodoc import sco_groups_view
from app.scodoc import sco_pdf
@ -388,7 +387,10 @@ def _trombino_pdf(groups_infos):
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)
return send_file(
report,
@ -465,7 +467,10 @@ def _listeappel_photos_pdf(groups_infos):
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()
return scu.sendPDFFile(data, filename)

View File

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

View File

@ -785,51 +785,6 @@ BULLETINS_VERSIONS_BUT = BULLETINS_VERSIONS | {
"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

View File

@ -485,6 +485,10 @@
cursor: pointer;
}
.mass-selection em {
margin-left: 16px;
}
.fieldsplit {
display: flex;
justify-content: flex-start;
@ -726,31 +730,11 @@ tr.row-justificatif.non_valide td.assi-type {
background-color: var(--color-defaut) !important;
}
.color.est_just.sans_etat::before {
content: "";
position: absolute;
width: 25%;
height: 100%;
background-color: var(--color-justi) !important;
right: 0;
}
.color.invalide::before {
content: "";
position: absolute;
width: 25%;
height: 100%;
right: 0;
.color.invalide {
background-color: var(--color-justi-invalide) !important;
}
.color.attente::before,
.color.modifie::before {
content: "";
position: absolute;
width: 25%;
height: 100%;
right: 0;
.color.attente {
background: repeating-linear-gradient(to bottom,
var(--color-justi-attente-stripe) 0px,
var(--color-justi-attente-stripe) 4px,
@ -758,6 +742,10 @@ tr.row-justificatif.non_valide td.assi-type {
var(--color-justi-attente) 7px) !important;
}
.color.est_just {
background-color: var(--color-justi) !important;
}
#gtrcontent .pdp {
display: none;
}

View File

@ -296,7 +296,13 @@ function creerLigneEtudiant(etud, index) {
// Création des boutons d'assiduités
if (readOnly) {
} else if (currentAssiduite.type != "conflit") {
["present", "retard", "absent"].forEach((abs) => {
const etats = ["retard", "absent"];
if (!window.nonPresent) {
etats.splice(0, 0, "present");
}
etats.forEach((abs) => {
const btn = document.createElement("input");
btn.type = "checkbox";
btn.value = abs;
@ -425,7 +431,7 @@ async function getModuleImpl(assiduite) {
return res.json();
})
.then((data) => {
moduleimpls[id] = `${data.module.code} ${data.module.abbrev}`;
moduleimpls[id] = `${data.module.code} ${data.module.abbrev || ""}`;
return moduleimpls[id];
})
.catch((_) => {
@ -531,12 +537,7 @@ async function MiseAJourLigneEtud(etud) {
async function actionAssiduite(etud, etat, type, assiduite = null) {
const modimpl_id = $("#moduleimpl_select").val();
if (
assiduite &&
assiduite.etat.toLowerCase() === etat &&
assiduite.moduleimpl_id == modimpl_id
)
type = "suppression";
if (assiduite && assiduite.etat.toLowerCase() === etat) type = "suppression";
const { deb, fin } = getPeriodAsDate();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@ $(function () {
delay: 300, // wait 300ms before suggestions
minLength: 2, // min nb of chars before suggest
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) {
$(".in-expnom").val(ui.item.value);
$("#form-chercheetud").submit();

View File

@ -5,6 +5,6 @@ $().ready(function () {
for (var i = 0; i < spans.size(); i++) {
var sp = spans[i];
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) {
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",
}
@ -38,7 +38,7 @@ async function delete_validation(etudid, validation_type, validation_id) {
function update_ue_list() {
var ue_id = $("#tf_ue_id")[0].value;
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) {
$("#ue_list_code").html(data);
});

View File

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

View File

@ -4,16 +4,17 @@
# See LICENSE
##############################################################################
"""Liste simple d'étudiants
"""
"""Liste simple d'étudiants"""
import datetime
from flask import g, url_for
from app import log
from app.models import FormSemestre, Identite, Justificatif
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_utils as scu
import app.scodoc.sco_assiduites as scass
from app.scodoc.sco_exceptions import ScoValueError
class TableAssi(tb.Table):
@ -39,7 +40,13 @@ class TableAssi(tb.Table):
):
self.rows: list["RowAssi"] = [] # juste pour que VSCode nous aide sur .rows
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_modimpls = formsemestre_modimpls
if convert_values:
@ -97,6 +104,20 @@ class RowAssi(tb.Row):
bilan_etud = url_for(
"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(
"nom_disp",
"Nom",
@ -119,6 +140,13 @@ class RowAssi(tb.Row):
)
stats = self._get_etud_stats(etud)
for key, value in stats.items():
if key == "present" and sco_preferences.get_preference(
"non_present",
dept_id=g.scodoc_dept_id,
formsemestre_id=self.table.formsemestre.id,
):
continue
self.add_cell(key, value[0], fmt_num(value[1] - value[2]), "assi_stats")
if key != "present":
self.add_cell(

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-title">
<span>Assiduité du</span>
<br>
<span>{{jour.get_date()}}</span>
{{jour.generate_minitimeline() | safe}}
</div>
@ -77,36 +75,7 @@ Calendrier de l'assiduité
<div class="help">
<h3>Calendrier</h3>
<p>Code couleur</p>
<ul class="couleurs">
<li><span title="Vert" class="present demo"></span> &rightarrow; présence de l'étudiant lors de la
période
</li>
<li><span title="Bleu clair" class="nonwork demo"></span> &rightarrow; la période n'est pas travaillée
</li>
<li><span title="Rouge" class="absent demo"></span> &rightarrow; absence de l'étudiant lors de la
période
</li>
<li><span title="Rose" class="demo color absent est_just"></span> &rightarrow; absence justifiée
</li>
<li><span title="Orange" class="retard demo"></span> &rightarrow; retard de l'étudiant lors de la
période
</li>
<li><span title="Jaune clair" class="demo color retard est_just"></span> &rightarrow; retard justifié
</li>
<li><span title="Quart Bleu" class="est_just demo"></span> &rightarrow; la période est couverte par un
justificatif valide</li>
<li><span title="Justif. non valide" class="invalide demo"></span> &rightarrow; la période est
couverte par un justificatif non valide
</li>
<li><span title="Justif. en attente" class="attente demo"></span> &rightarrow; la période
a un justificatif en attente de validation
</li>
</ul>
<p>Vous pouvez passer le curseur sur les jours colorés afin de voir les informations supplémentaires</p>
{% include "assiduites/widgets/legende_couleur.j2" %}
</div>
<ul class="couleurs print">
<li><span title="Vert" class="present demo"></span> présence
@ -149,7 +118,7 @@ Calendrier de l'assiduité
list-style-type: none;
}
.pageContent {
margin-top: 1vh;
@ -158,7 +127,7 @@ Calendrier de l'assiduité
.calendrier {
display: flex;
justify-content: start;
justify-content: center;
overflow-x: scroll;
border: 1px solid #444;
border-radius: 12px;
@ -182,21 +151,8 @@ Calendrier de l'assiduité
justify-content: start;
}
.demo.invalide {
background-color: var(--color-justi-invalide) !important;
}
.demo.attente {
background: repeating-linear-gradient(to bottom,
var(--color-justi-attente-stripe) 0px,
var(--color-justi-attente-stripe) 4px,
var(--color-justi-attente) 4px,
var(--color-justi-attente) 7px) !important;
}
.demo.est_just {
background-color: var(--color-justi) !important;
}
.demi .day.nonwork>span {
@ -335,7 +291,7 @@ Calendrier de l'assiduité
document.querySelectorAll('[assi_id]').forEach((el, i) => {
el.addEventListener('click', () => {
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;
border: 1px solid #ddd;
}
.cell, .header {
border: 1px solid #ddd;
padding: 10px;
@ -111,7 +111,7 @@
display: flex;
gap: 4px;
}
.pointer{
cursor: pointer;
}
@ -220,7 +220,7 @@ async function nouvellePeriode(period = null) {
let periodeDiv = document.createElement("div");
periodeDiv.classList.add("cell", "header");
periodeDiv.id = `periode-${periodId}`;
const periodP = document.createElement("p");
periodP.textContent = `Plage du ${date} de ${debut} à ${fin}`;
@ -310,8 +310,13 @@ async function nouvellePeriode(period = null) {
const assi_btns = document.createElement('div');
assi_btns.classList.add('assi-btns');
const etats = ["retard", "absent"];
["present", "retard", "absent"].forEach((value) => {
if(!window.nonPresent){
etats.splice(0,0,"present");
}
etats.forEach((value) => {
const cbox = document.createElement("input");
cbox.type = "checkbox";
cbox.value = value;
@ -402,12 +407,12 @@ function sauvegarderAssiduites() {
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) {
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(
"Sauvegarde des assiduités",
"Enregistrement de l'assiduité",
span,
null,
"var(--color-present)"
@ -499,6 +504,8 @@ const moduleimpls = new Map();
const inscriptionsModules = new Map();
const nonWorkDays = [{{ nonworkdays| safe }}];
window.nonPresent = {{ 'true' if non_present else 'false' }};
// Vérification du forçage de module
window.forceModule = "{{ forcer_module }}" == "True";
if (window.forceModule) {
@ -518,12 +525,29 @@ if (window.forceModule) {
}
});
}
const defaultPlage = {{ nouv_plage | safe}} || [];
/**
* Fonction exécutée au lancement de la page
* - On affiche ou non les photos des étudiants
* - On vérifie si la date est un jour travaillé
*/
async function main() {
// On initialise les sélecteurs avec les valeurs par défaut (si elles existent)
if (defaultPlage.every((e) => e)) {
$("#date").datepicker("setDate", defaultPlage[0]);
$("#debut").val(defaultPlage[1]);
$("#fin").val(defaultPlage[2]);
// On ajoute la période si la date est un jour travaillé
if(dateCouranteEstTravaillee()){
await nouvellePeriode();
}
}
const checked = localStorage.getItem("scodoc-etud-pdp") == "true";
afficherPDP(checked);
$("#date").on("change", async function (d) {
@ -532,7 +556,7 @@ async function main() {
});
}
main();
window.addEventListener("load", main);
</script>
@ -597,21 +621,23 @@ main();
</label>
<label for="etatDef">
Intialiser les étudiants comme :
Intialiser les étudiants comme :
<select name="etatDef" id="etatDef">
<option value="">-</option>
{% if not non_present %}
<option value="present">présents</option>
{% endif %}
<option value="retard">en retard</option>
<option value="absent">absents</option>
</select>
</label>
</div>
<!-- Tableau à double entrée
<!-- Tableau à double entrée
Colonne : Etudiants (Header = Nom, Prénom, Photo (si actif))
Ligne : Période (Header = Jour, Heure de début, Heure de fin, ModuleImplId)
Contenu :
Contenu :
- bouton assiduité (présent, retard, absent)
- Bouton conflit si conflit de période
--->

View File

@ -25,12 +25,13 @@
setupTimeLine(()=>{creerTousLesEtudiants(etuds)})
{% endif %}
const nonWorkDays = [{{ nonworkdays| safe }}];
const readOnly = {{ readonly }};
window.forceModule = "{{ forcer_module }}" == "True"
window.nonPresent = {{ 'true' if non_present else 'false' }};
const etudsDefDem = {{ defdem | safe }}
@ -61,7 +62,7 @@
$('#date').on('change', async function(d) {
// On vérifie si la date est un jour travaillé
dateCouranteEstTravaillee();
await recupAssiduites(etuds, $("#date").datepicker("getDate"));
@ -87,7 +88,7 @@
await recupAssiduites(etuds, $("#date").datepicker("getDate"));
}
creerTousLesEtudiants(etuds);
// affichage ou non des PDP
afficherPDP(localStorage.getItem("scodoc-etud-pdp") == "true" )
}
@ -159,8 +160,10 @@
<div class="mass-selection">
<span>Mettre tout le monde :</span>
<fieldset class="btns_field mass">
{% if not non_present %}
<input type="checkbox" value="present" name="mass_btn_assiduites" id="mass_rbtn_present"
class="rbtn present" onclick="mettreToutLeMonde('present', this)" title="Present">
{% endif %}
<input type="checkbox" value="retard" name="mass_btn_assiduites" id="mass_rbtn_retard"
class="rbtn retard" onclick="mettreToutLeMonde('retard', this)" title="Retard">
<input type="checkbox" value="absent" name="mass_btn_assiduites" id="mass_rbtn_absent"
@ -168,20 +171,26 @@
<input type="checkbox" value="remove" name="mass_btn_assiduites" id="mass_rbtn_aucun"
class="rbtn aucun" onclick="mettreToutLeMonde('vide', this)" title="Supprimer">
</fieldset>
<em>Les saisies ci-dessous sont enregistrées au fur et à mesure.</em>
</div>
{% endif %}
<div class="etud_holder">
<p class="placeholder">
</p>
</div>
<div class="help">
<h3>Calendrier</h3>
{% include "assiduites/widgets/legende_couleur.j2" %}
</div>
{% include "assiduites/widgets/toast.j2" %}
{% include "assiduites/widgets/alert.j2" %}
{% include "assiduites/widgets/prompt.j2" %}
{% include "assiduites/widgets/conflict.j2" %}
</section>

View File

@ -1,12 +1,28 @@
<li><span title="Vert" class="present demo"></span> &rightarrow; présence de l'étudiant lors de la période
</li>
<li><span title="Orange" class="retard demo"></span> &rightarrow; retard de l'étudiant lors de la période
</li>
<li><span title="Rouge" class="absent demo"></span> &rightarrow; absence de l'étudiant lors de la période
</li>
<p>Code couleur</p>
<ul class="couleurs">
<li><span title="Vert" class="present demo"></span> &rightarrow; présence de l'étudiant lors de la
période
</li>
<li><span title="Bleu clair" class="nonwork demo"></span> &rightarrow; la période n'est pas travaillée
</li>
<li><span title="Rouge" class="absent demo"></span> &rightarrow; absence de l'étudiant lors de la
période
</li>
<li><span title="Rose" class="demo color absent est_just"></span> &rightarrow; absence justifiée
</li>
<li><span title="Orange" class="retard demo"></span> &rightarrow; retard de l'étudiant lors de la
période
</li>
<li><span title="Jaune clair" class="demo color retard est_just"></span> &rightarrow; retard justifié
</li>
<li><span title="Hachure Bleue" class="justified demo"></span> &rightarrow; l'assiduité est justifiée par un
justificatif valide</li>
<li><span title="Hachure Rouge" class="invalid_justified demo"></span> &rightarrow; l'assiduité est
justifiée par un justificatif non valide / en attente de validation
</li>
<li><span title="Quart Bleu" class="est_just demo color"></span> &rightarrow; la période est couverte par un
justificatif valide</li>
<li><span title="Justif. non valide" class="invalide demo color "></span> &rightarrow; la période est
couverte par un justificatif non valide
</li>
<li><span title="Justif. en attente" class="attente demo color"></span> &rightarrow; la période
a un justificatif en attente de validation
</li>
</ul>
<p>Vous pouvez passer le curseur sur les jours colorés afin de voir les informations supplémentaires</p>

View File

@ -74,7 +74,13 @@
setupAssiduiteBubble(block, assiduité);
}
// TODO: ajout couleur justificatif
// ajout couleur justificatif
const justificatifs = assiduité.justificatifs || [];
const justified = justificatifs.some(
(justificatif) => justificatif.etat === "VALIDE"
)
if(justified) block.classList.add("est_just");
block.classList.add(assiduité.etat.toLowerCase());
if(assiduité.etat != "CRENEAU") block.classList.add("color");

View File

@ -102,6 +102,6 @@
<script src="{{scu.STATIC_DIR}}/js/scodoc.js"></script>
<script>
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>
{% 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-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_scodoc.hidden_tag() }}
<div class="row">

View File

@ -6,7 +6,7 @@
<h2>Accès non autorisé</h2>
{{ exc | safe }}
{{ exc }}
<p class="footer">
{% if g.scodoc_dept %}

View File

@ -152,7 +152,7 @@ document.addEventListener('DOMContentLoaded', function() {
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(events=>{
if (typeof events == 'string') {

View File

@ -17,8 +17,8 @@ et permet de les effacer une par une.
<p class="help">
<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>.
Aucune annulation n'est ici possible (vous devrez re-saisir les décisions via les
pages de saisie de jury habituelles).
Aucune annulation n'est ici possible (vous devrez re-saisir les décisions via les
pages de saisie de jury habituelles).
</p>
{% if sem_vals.first() %}
<div class="jury_decisions_list jury_decisions_sems">
@ -27,7 +27,7 @@ pages de saisie de jury habituelles).
{% for v in sem_vals %}
<li>{{v.html()|safe}}
<form>
<button
<button
data-v_id="{{v.id}}" data-type="validation_formsemestre" data-etudid="{{etud.id}}"
>effacer</button></form>
</li>
@ -101,8 +101,8 @@ pages de saisie de jury habituelles).
{% endif %}
{% if not(
sem_vals.first() or ue_vals.first() or rcue_vals.first()
or annee_but_vals.first() or autorisations.first())
sem_vals.first() or ue_vals.first() or rcue_vals.first()
or annee_but_vals.first() or autorisations.first())
%}
<div>
<p class="fontred">aucune décision enregistrée</p>
@ -123,7 +123,7 @@ pages de saisie de jury habituelles).
<script>
document.addEventListener('DOMContentLoaded', () => {
const buttons = document.querySelectorAll('.jury_decisions_list button');
buttons.forEach(button => {
button.addEventListener('click', (event) => {
// Handle button click event here
@ -132,10 +132,10 @@ document.addEventListener('DOMContentLoaded', () => {
const v_id = event.target.dataset.v_id;
const validation_type = event.target.dataset.type;
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",
}).then(response => {
}).then(response => {
// Handle the response
if (response.ok) {
location.reload();

View File

@ -51,7 +51,7 @@
<script>
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>
{% endblock %}

View File

@ -5,7 +5,7 @@
<h2>Erreur !</h2>
{{ exc | safe }}
{{ exc }}
<div style="margin-top: 16px;">
{% if g.scodoc_dept %}

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
</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>
</label>
</div>

View File

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

View File

@ -1132,6 +1132,11 @@ def signal_assiduites_group():
formsemestre_id=formsemestre_id,
dept_id=g.scodoc_dept_id,
),
non_present=sco_preferences.get_preference(
"non_present",
formsemestre_id=formsemestre_id,
dept_id=g.scodoc_dept_id,
),
formsemestre_date_debut=str(formsemestre.date_debut),
formsemestre_date_fin=str(formsemestre.date_fin),
formsemestre_id=formsemestre_id,
@ -1440,7 +1445,6 @@ def visu_assi_group():
formsemestre_modimpls=formsemestre_modimpls,
convert_values=(fmt == "html"),
)
# Export en XLS
if fmt.startswith("xls"):
return scu.send_file(
@ -1915,8 +1919,29 @@ def _preparer_objet(
@scodoc
@permission_required(Permission.AbsChange)
def signal_assiduites_diff():
"""TODO documenter
"""
Utilisé notamment par "Saisie différée" sur tableau de bord semetstre"
Arguments de la requête:
- group_ids : liste des groupes
example : group_ids=1,2,3
- formsemestre_id : id du formsemestre
example : formsemestre_id=1
- moduleimpl_id : id du moduleimpl
example : moduleimpl_id=1
(Permet de pré-générer une plage. Si non renseigné, la plage sera vide)
(Les trois valeurs suivantes doivent être renseignées ensemble)
- date
example : date=01/01/2021
- heure_debut
example : heure_debut=08:00
- heure_fin
example : heure_fin=10:00
Exemple de requête :
signal_assiduites_diff?formsemestre_id=67&group_ids=400&moduleimpl_id=1229&date=15/04/2024&heure_debut=12:34&heure_fin=12:55
"""
# Récupération des paramètres de la requête
group_ids: list[int] = request.args.get("group_ids", None)
@ -1958,11 +1983,23 @@ def signal_assiduites_diff():
grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>"
)
# Pré-remplissage des sélecteurs
moduleimpl_id = request.args.get("moduleimpl_id", -1)
try:
moduleimpl_id = int(moduleimpl_id)
except ValueError:
moduleimpl_id = -1
# date fra (dd/mm/yyyy)
date = request.args.get("date", "")
# heures (hh:mm)
heure_deb = request.args.get("heure_debut", "")
heure_fin = request.args.get("heure_fin", "")
# vérifications des sélecteurs
date = date if re.match(r"^\d{2}\/\d{2}\/\d{4}$", date) else ""
heure_deb = heure_deb if re.match(r"^[0-2]\d:[0-5]\d$", heure_deb) else ""
heure_fin = heure_fin if re.match(r"^[0-2]\d:[0-5]\d$", heure_fin) else ""
nouv_plage: list[str] = [date, heure_deb, heure_fin]
return render_template(
"assiduites/pages/signal_assiduites_diff.j2",
@ -1978,6 +2015,12 @@ def signal_assiduites_diff():
formsemestre_id=formsemestre_id,
dept_id=g.scodoc_dept_id,
),
non_present=sco_preferences.get_preference(
"non_present",
formsemestre_id=formsemestre_id,
dept_id=g.scodoc_dept_id,
),
nouv_plage=nouv_plage,
)

View File

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

View File

@ -53,6 +53,7 @@ from werkzeug.exceptions import BadRequest, NotFound
from app import db, log
from app import entreprises
from app.auth.models import User, Role
from app.auth.cas import set_cas_configuration
from app.decorators import (
@ -62,6 +63,7 @@ from app.decorators import (
)
from app.forms.generic import SimpleConfirmationForm
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_apo import CodesDecisionsForm
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"])
@login_required
def table_etud_in_accessible_depts():

View File

@ -340,8 +340,8 @@ def showEtudLog(etudid, fmt="html"):
# ---------- PAGE ACCUEIL (listes) --------------
@bp.route("/", alias=True)
@bp.route("/index_html")
@bp.route("/")
@bp.route("/index_html", alias=True)
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
@ -1954,7 +1954,7 @@ def etudident_delete(etudid: int = -1, dialog_confirmed=False):
for formsemestre_id in formsemestre_ids_to_inval:
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id)
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")
@ -2148,7 +2148,7 @@ def form_students_import_excel(formsemestre_id=None):
)
else:
sem = None
dest_url = scu.ScoURL()
dest_url = url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
if sem and not sem["etat"]:
raise ScoValueError("Modification impossible: semestre verrouille")
H = [
@ -2183,13 +2183,15 @@ def form_students_import_excel(formsemestre_id=None):
)
else:
H.append(
"""
f"""
<p>Pour inscrire directement les étudiants dans un semestre de
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>
"""
% (scu.ScoURL())
)
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
elif tf[0] == -1:
return flask.redirect(
scu.ScoURL()
+ "/formsemestre_status?formsemestre_id="
+ str(formsemestre_id)
url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
)
else:
return sco_import_etuds.students_import_admission(

View File

@ -132,7 +132,7 @@ class Mode(IntEnum):
@bp.route("/")
@bp.route("/index_html")
@bp.route("/index_html", alias=True)
@scodoc
@permission_required(Permission.UsersView)
@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
else (auth_dept or "")
)
if len(selectable_dept_acronyms) > 1:
if len(selectable_dept_acronyms) > 0:
selectable_dept_acronyms = sorted(list(selectable_dept_acronyms))
descr.append(
(
@ -529,7 +529,7 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
{
"input_type": "separator",
"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",
{
"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:
return "\n".join(H) + "\n" + tf[1] + F
elif tf[0] == -1:
return flask.redirect(scu.UsersURL())
return flask.redirect(url_for("users.index_html", scodoc_dept=g.scodoc_dept))
else:
vals = tf[2]
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
# le droit d'acceder aux feuilles de style
H.append(
"""<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>"""
)
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">
return f"""<?xml version="1.0" encoding="{scu.SCO_ENCODING}"?>
<!DOCTYPE html>
<html>
<head>
<title>Mot de passe changé</title>
<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

View File

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

View File

@ -255,8 +255,20 @@ def test_etudiants_by_name(api_headers):
etuds = r.json()
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(
API_URL + "/etudiants/name/REG",
API_URL + "/etudiants/name/REC",
headers=api_headers,
verify=CHECK_CERTIFICATE,
timeout=scu.SCO_TEST_API_TIMEOUT,
@ -264,7 +276,7 @@ def test_etudiants_by_name(api_headers):
assert r.status_code == 200
etuds = r.json()
assert len(etuds) == 1
assert etuds[0]["nom"] == "GNIER"
assert etuds[0]["nom"] == "ÇIER"
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)
# 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
r = requests.get(
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.scodoc import notesdb as ndb
import app.scodoc.sco_utils as scu
from app.views import ScoData
RESOURCES_DIR = "/opt/scodoc/tests/ressources"

View File

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

View File

@ -157,10 +157,11 @@ def anonymize_users(cursor):
cursor.execute("""UPDATE "user" SET token = NULL;""")
cursor.execute("""UPDATE "user" SET token_expiration = NULL;""")
# 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
nb_users = len(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"]
nom, prenom = random.choice(NOMS), random.choice(PRENOMS)
new_name = (prenom[0] + nom).lower()
@ -168,7 +169,7 @@ def anonymize_users(cursor):
while new_name in used_user_names:
new_name += "x"
used_user_names.add(new_name)
print(f"{user_name} > {new_name}")
print(f"{i}/{nb_users}\t{user_name} > {new_name}")
cursor.execute(
"""UPDATE "user"
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)
anonymize_db(cursor)
rename_students(cursor)
if PROCESS_USERS:
anonymize_users(cursor)

View File

@ -96,7 +96,7 @@ mkdir -p "$optdir" || die "mkdir failure for $optdir"
archive="$FACTORY_DIR"/"$PACKAGE_NAME-$RELEASE_TAG".tar.gz
echo "Downloading $GIT_RELEASE_URL ..."
# curl -o "$archive" "$GIT_RELEASE_URL" || die "curl failure for $GIT_RELEASE_URL"
#wget --progress=dot -O "$archive" "$GIT_RELEASE_URL" || die "wget failure for $GIT_RELEASE_URL"
wget --progress=dot -O "$archive" "$GIT_RELEASE_URL" || die "wget failure for $GIT_RELEASE_URL"
# -nv
# On décomprime

File diff suppressed because it is too large Load Diff