Compare commits

...

28 Commits

Author SHA1 Message Date
Emmanuel Viennet baa0412071 Merge pull request 'Mise à jour du README' (#881) from lyanis/ScoDoc:readme into master
Reviewed-on: #881
2024-05-13 18:23:34 +02:00
Emmanuel Viennet d51a47b71a Fix: formulaire creation étudiant (date naissance vide) 2024-05-13 17:31:54 +02:00
Lyanis Souidi f21ef41de6 README: Mise en forme des blocs de code 2024-05-13 14:54:52 +02:00
Lyanis Souidi 2d673e7a5d Mise à jour du README 2024-05-13 11:16:10 +02:00
Emmanuel Viennet 3e43495831 Fix: bulletins pdf, notes évaluations sans notes. 2024-05-07 18:17:13 +02:00
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
78 changed files with 2184 additions and 1905 deletions

152
README.md
View File

@ -2,7 +2,7 @@
(c) Emmanuel Viennet 1999 - 2024 (voir LICENCE.txt). (c) Emmanuel Viennet 1999 - 2024 (voir LICENCE.txt).
Installation: voir instructions à jour sur <https://scodoc.org/GuideInstallDebian11> Installation: voir instructions à jour sur <https://scodoc.org/GuideInstallDebian12>
Documentation utilisateur: <https://scodoc.org> Documentation utilisateur: <https://scodoc.org>
@ -23,7 +23,7 @@ Flask, SQLAlchemy, au lien de Python2/Zope dans les versions précédentes).
### Lignes de commandes ### Lignes de commandes
Voir [https://scodoc.org/GuideConfig](le guide de configuration). Voir [le guide de configuration](https://scodoc.org/GuideConfig).
## Organisation des fichiers ## Organisation des fichiers
@ -41,45 +41,41 @@ Ils ne doivent pas être modifiés à la main, sauf certains fichiers de configu
Le répertoire `/opt/scodoc-data` doit être régulièrement sauvegardé. Le répertoire `/opt/scodoc-data` doit être régulièrement sauvegardé.
Principaux contenus: Principaux contenus:
```
/opt/scodoc-data /opt/scodoc-data
/opt/scodoc-data/log # Fichiers de log ScoDoc /opt/scodoc-data/log # Fichiers de log ScoDoc
/opt/scodoc-data/config # Fichiers de configuration /opt/scodoc-data/config # Fichiers de configuration
.../config/logos # Logos de l'établissement .../config/logos # Logos de l'établissement
.../config/depts # un fichier par département .../config/depts # un fichier par département
/opt/scodoc-data/photos # Photos des étudiants /opt/scodoc-data/photos # Photos des étudiants
/opt/scodoc-data/archives # Archives: PV de jury, maquettes Apogée, fichiers étudiants /opt/scodoc-data/archives # Archives: PV de jury, maquettes Apogée, fichiers étudiants
```
## Pour les développeurs ## Pour les développeurs
### Installation du code ### Installation du code
Installer ScoDoc 9 normalement ([voir la doc](https://scodoc.org/GuideInstallDebian11)). Installer ScoDoc 9 normalement ([voir la doc](https://scodoc.org/GuideInstallDebian12)).
Puis remplacer `/opt/scodoc` par un clone du git. Puis remplacer `/opt/scodoc` par un clone du git.
```bash
sudo su
mv /opt/scodoc /opt/off-scodoc # ou ce que vous voulez
apt-get install git # si besoin
git clone https://scodoc.org/git/ScoDoc/ScoDoc.git /opt/scodoc
# (ou bien utiliser votre clone gitea si vous l'avez déjà créé !)
sudo su # Donner ce répertoire à l'utilisateur scodoc:
mv /opt/scodoc /opt/off-scodoc # ou ce que vous voulez chown -R scodoc:scodoc /opt/scodoc
apt-get install git # si besoin ```
cd /opt
git clone https://scodoc.org/git/viennet/ScoDoc.git
# (ou bien utiliser votre clone gitea si vous l'avez déjà créé !)
# Renommer le répertoire:
mv ScoDoc scodoc
# Et donner ce répertoire à l'utilisateur scodoc:
chown -R scodoc.scodoc /opt/scodoc
Il faut ensuite installer l'environnement et le fichier de configuration: Il faut ensuite installer l'environnement et le fichier de configuration:
```bash
# Le plus simple est de piquer le virtualenv configuré par l'installeur: # Le plus simple est de piquer le virtualenv configuré par l'installeur:
mv /opt/off-scodoc/venv /opt/scodoc mv /opt/off-scodoc/venv /opt/scodoc
```
Et la config: Et la config:
```bash
ln -s /opt/scodoc-data/.env /opt/scodoc ln -s /opt/scodoc-data/.env /opt/scodoc
```
Cette dernière commande utilise le `.env` crée lors de l'install, ce qui Cette dernière commande utilise le `.env` crée lors de l'install, ce qui
n'est pas toujours le plus judicieux: vous pouvez modifier son contenu, par n'est pas toujours le plus judicieux: vous pouvez modifier son contenu, par
exemple pour travailler en mode "développement" avec `FLASK_ENV=development`. exemple pour travailler en mode "développement" avec `FLASK_ENV=development`.
@ -88,11 +84,11 @@ exemple pour travailler en mode "développement" avec `FLASK_ENV=development`.
Les tests unitaires utilisent normalement la base postgresql `SCODOC_TEST`. Les tests unitaires utilisent normalement la base postgresql `SCODOC_TEST`.
Avant le premier lancement, créer cette base ainsi: Avant le premier lancement, créer cette base ainsi:
```bash
./tools/create_database.sh SCODOC_TEST ./tools/create_database.sh SCODOC_TEST
export FLASK_ENV=test export FLASK_ENV=test
flask db upgrade flask db upgrade
```
Cette commande n'est nécessaire que la première fois (le contenu de la base Cette commande n'est nécessaire que la première fois (le contenu de la base
est effacé au début de chaque test, mais son schéma reste) et aussi si des est effacé au début de chaque test, mais son schéma reste) et aussi si des
migrations (changements de schéma) ont eu lieu dans le code. migrations (changements de schéma) ont eu lieu dans le code.
@ -100,17 +96,17 @@ migrations (changements de schéma) ont eu lieu dans le code.
Certains tests ont besoin d'un département déjà créé, qui n'est pas créé par les Certains tests ont besoin d'un département déjà créé, qui n'est pas créé par les
scripts de tests: scripts de tests:
Lancer au préalable: Lancer au préalable:
```bash
flask delete-dept -fy TEST00 && flask create-dept TEST00 flask delete-dept -fy TEST00 && flask create-dept TEST00
```
Puis dérouler les tests unitaires: Puis dérouler les tests unitaires:
```bash
pytest tests/unit pytest tests/unit
```
Ou avec couverture (`pip install pytest-cov`) Ou avec couverture (`pip install pytest-cov`)
```bash
pytest --cov=app --cov-report=term-missing --cov-branch tests/unit/* pytest --cov=app --cov-report=term-missing --cov-branch tests/unit/*
```
#### Utilisation des tests unitaires pour initialiser la base de dev #### Utilisation des tests unitaires pour initialiser la base de dev
On peut aussi utiliser les tests unitaires pour mettre la base de données de On peut aussi utiliser les tests unitaires pour mettre la base de données de
@ -119,43 +115,43 @@ développement dans un état connu, par exemple pour éviter de recréer à la m
Il suffit de positionner une variable d'environnement indiquant la BD utilisée Il suffit de positionner une variable d'environnement indiquant la BD utilisée
par les tests: par les tests:
```bash
export SCODOC_TEST_DATABASE_URI=postgresql:///SCODOC_DEV export SCODOC_TEST_DATABASE_URI=postgresql:///SCODOC_DEV
```
(si elle n'existe pas, voir plus loin pour la créer) puis de les lancer (si elle n'existe pas, voir plus loin pour la créer) puis de les lancer
normalement, par exemple: normalement, par exemple:
```bash
pytest tests/unit/test_sco_basic.py pytest tests/unit/test_sco_basic.py
```
Il est en général nécessaire d'affecter ensuite un mot de passe à (au moins) un Il est en général nécessaire d'affecter ensuite un mot de passe à (au moins) un
utilisateur: utilisateur:
```bash
flask user-password admin flask user-password admin
```
**Attention:** les tests unitaires **effacent** complètement le contenu de la **Attention:** les tests unitaires **effacent** complètement le contenu de la
base de données (tous les départements, et les utilisateurs) avant de commencer ! base de données (tous les départements, et les utilisateurs) avant de commencer !
#### Modification du schéma de la base #### Modification du schéma de la base
On utilise SQLAlchemy avec Alembic et Flask-Migrate. On utilise SQLAlchemy avec Alembic et Flask-Migrate.
```bash
flask db migrate -m "message explicatif....." flask db migrate -m "message explicatif....."
flask db upgrade flask db upgrade
```
Ne pas oublier de d'ajouter le script de migration à git (`git add migrations/...`). Ne pas oublier de d'ajouter le script de migration à git (`git add migrations/...`).
**Mémo**: séquence re-création d'une base (vérifiez votre `.env` **Mémo**: séquence re-création d'une base (vérifiez votre `.env`
ou variables d'environnement pour interroger la bonne base !). ou variables d'environnement pour interroger la bonne base !).
```bash
dropdb SCODOC_DEV
tools/create_database.sh SCODOC_DEV # créé base SQL
flask db upgrade # créé les tables à partir des migrations
flask sco-db-init # ajoute au besoin les constantes (fait en migration 0)
dropdb SCODOC_DEV # puis imports:
tools/create_database.sh SCODOC_DEV # créé base SQL flask import-scodoc7-users
flask db upgrade # créé les tables à partir des migrations flask import-scodoc7-dept STID SCOSTID
flask sco-db-init # ajoute au besoin les constantes (fait en migration 0) ```
# puis imports:
flask import-scodoc7-users
flask import-scodoc7-dept STID SCOSTID
Si la base utilisée pour les dev n'est plus en phase avec les scripts de Si la base utilisée pour les dev n'est plus en phase avec les scripts de
migration, utiliser les commandes `flask db history`et `flask db stamp`pour se migration, utiliser les commandes `flask db history`et `flask db stamp`pour se
positionner à la bonne étape. positionner à la bonne étape.
@ -163,23 +159,23 @@ positionner à la bonne étape.
### Profiling ### Profiling
Sur une machine de DEV, lancer Sur une machine de DEV, lancer
```bash
flask profile --host 0.0.0.0 --length 32 --profile-dir /opt/scodoc-data flask profile --host 0.0.0.0 --length 32 --profile-dir /opt/scodoc-data
```
le fichier `.prof` sera alors écrit dans `/opt/scodoc-data` (on peut aussi utiliser `/tmp`). le fichier `.prof` sera alors écrit dans `/opt/scodoc-data` (on peut aussi utiliser `/tmp`).
Pour la visualisation, [snakeviz](https://jiffyclub.github.io/snakeviz/) est bien: Pour la visualisation, [snakeviz](https://jiffyclub.github.io/snakeviz/) est bien:
```bash
pip install snakeviz pip install snakeviz
```
puis puis
```bash
snakeviz -s --hostname 0.0.0.0 -p 5555 /opt/scodoc-data/GET.ScoDoc......prof snakeviz -s --hostname 0.0.0.0 -p 5555 /opt/scodoc-data/GET.ScoDoc......prof
```
## Paquet Debian 12 ## Paquet Debian 12
Les scripts associés au paquet Debian (.deb) sont dans `tools/debian`. Le plus Les scripts associés au paquet Debian (.deb) sont dans `tools/debian`. Le plus
important est `postinst`qui se charge de configurer le système (install ou important est `postinst` qui se charge de configurer le système (install ou
upgrade de scodoc9). upgrade de scodoc9).
La préparation d'une release se fait à l'aide du script La préparation d'une release se fait à l'aide du script

View File

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

View File

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

View File

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

View File

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

View File

@ -427,12 +427,11 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
if e["evaluation_type"] == Evaluation.EVALUATION_NORMALE if e["evaluation_type"] == Evaluation.EVALUATION_NORMALE
else "*" else "*"
) )
note_value = e["note"].get("value", "")
t = { t = {
"titre": f"{e['description'] or ''}", "titre": f"{e['description'] or ''}",
"moyenne": e["note"]["value"], "moyenne": note_value,
"_moyenne_pdf": Paragraph( "_moyenne_pdf": Paragraph(f"""<para align=right>{note_value}</para>"""),
f"""<para align=right>{e["note"]["value"]}</para>"""
),
"coef": coef, "coef": coef,
"_coef_pdf": Paragraph( "_coef_pdf": Paragraph(
f"""<para align=right fontSize={self.small_fontsize}><i>{ f"""<para align=right fontSize={self.small_fontsize}><i>{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,16 +4,17 @@
# See LICENSE # See LICENSE
############################################################################## ##############################################################################
"""Liste simple d'étudiants """Liste simple d'étudiants"""
"""
import datetime
from flask import g, url_for from flask import g, url_for
from app import log from app import log
from app.models import FormSemestre, Identite, Justificatif from app.models import FormSemestre, Identite, Justificatif
from app.tables import table_builder as tb from app.tables import table_builder as tb
import app.scodoc.sco_assiduites as scass
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
import app.scodoc.sco_assiduites as scass
from app.scodoc.sco_exceptions import ScoValueError
class TableAssi(tb.Table): class TableAssi(tb.Table):
@ -39,7 +40,13 @@ class TableAssi(tb.Table):
): ):
self.rows: list["RowAssi"] = [] # juste pour que VSCode nous aide sur .rows self.rows: list["RowAssi"] = [] # juste pour que VSCode nous aide sur .rows
classes = ["gt_table"] classes = ["gt_table"]
self.dates = [str(dates[0]) + "T00:00", str(dates[1]) + "T23:59"] try:
self.dates = [
datetime.datetime.fromisoformat(str(dates[0]) + "T00:00"),
datetime.datetime.fromisoformat(str(dates[1]) + "T00:00"),
]
except ValueError as exc:
raise ScoValueError("invalid dates") from exc
self.formsemestre = formsemestre self.formsemestre = formsemestre
self.formsemestre_modimpls = formsemestre_modimpls self.formsemestre_modimpls = formsemestre_modimpls
if convert_values: if convert_values:
@ -97,6 +104,20 @@ class RowAssi(tb.Row):
bilan_etud = url_for( bilan_etud = url_for(
"assiduites.bilan_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id "assiduites.bilan_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id
) )
self.add_cell(
"etudid",
"etudid",
etud.etudid,
"etudinfo",
only_excel=True,
)
self.add_cell(
"code_nip",
"code_nip",
etud.code_nip,
"etudinfo",
only_excel=True,
)
self.add_cell( self.add_cell(
"nom_disp", "nom_disp",
"Nom", "Nom",
@ -119,6 +140,13 @@ class RowAssi(tb.Row):
) )
stats = self._get_etud_stats(etud) stats = self._get_etud_stats(etud)
for key, value in stats.items(): 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") self.add_cell(key, value[0], fmt_num(value[1] - value[2]), "assi_stats")
if key != "present": if key != "present":
self.add_cell( 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">
<div class="dayline-title"> <div class="dayline-title">
<span>Assiduité du</span>
<br>
<span>{{jour.get_date()}}</span> <span>{{jour.get_date()}}</span>
{{jour.generate_minitimeline() | safe}} {{jour.generate_minitimeline() | safe}}
</div> </div>
@ -77,36 +75,7 @@ Calendrier de l'assiduité
<div class="help"> <div class="help">
<h3>Calendrier</h3> <h3>Calendrier</h3>
<p>Code couleur</p> {% include "assiduites/widgets/legende_couleur.j2" %}
<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>
</div> </div>
<ul class="couleurs print"> <ul class="couleurs print">
<li><span title="Vert" class="present demo"></span> présence <li><span title="Vert" class="present demo"></span> présence
@ -149,7 +118,7 @@ Calendrier de l'assiduité
list-style-type: none; list-style-type: none;
} }
.pageContent { .pageContent {
margin-top: 1vh; margin-top: 1vh;
@ -158,7 +127,7 @@ Calendrier de l'assiduité
.calendrier { .calendrier {
display: flex; display: flex;
justify-content: start; justify-content: center;
overflow-x: scroll; overflow-x: scroll;
border: 1px solid #444; border: 1px solid #444;
border-radius: 12px; border-radius: 12px;
@ -182,21 +151,8 @@ Calendrier de l'assiduité
justify-content: start; 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 { .demi .day.nonwork>span {
@ -335,7 +291,7 @@ Calendrier de l'assiduité
document.querySelectorAll('[assi_id]').forEach((el, i) => { document.querySelectorAll('[assi_id]').forEach((el, i) => {
el.addEventListener('click', () => { el.addEventListener('click', () => {
const assi_id = el.getAttribute('assi_id'); const assi_id = el.getAttribute('assi_id');
window.open(`${SCO_URL}/Assiduites/tableau_assiduite_actions?type=assiduite&action=details&obj_id=${assi_id}`); window.open(`${SCO_URL}Assiduites/tableau_assiduite_actions?type=assiduite&action=details&obj_id=${assi_id}`);
}) })
}); });

View File

@ -67,7 +67,7 @@
text-align: center; text-align: center;
border: 1px solid #ddd; border: 1px solid #ddd;
} }
.cell, .header { .cell, .header {
border: 1px solid #ddd; border: 1px solid #ddd;
padding: 10px; padding: 10px;
@ -111,7 +111,7 @@
display: flex; display: flex;
gap: 4px; gap: 4px;
} }
.pointer{ .pointer{
cursor: pointer; cursor: pointer;
} }
@ -220,7 +220,7 @@ async function nouvellePeriode(period = null) {
let periodeDiv = document.createElement("div"); let periodeDiv = document.createElement("div");
periodeDiv.classList.add("cell", "header"); periodeDiv.classList.add("cell", "header");
periodeDiv.id = `periode-${periodId}`; periodeDiv.id = `periode-${periodId}`;
const periodP = document.createElement("p"); const periodP = document.createElement("p");
periodP.textContent = `Plage du ${date} de ${debut} à ${fin}`; periodP.textContent = `Plage du ${date} de ${debut} à ${fin}`;
@ -310,8 +310,13 @@ async function nouvellePeriode(period = null) {
const assi_btns = document.createElement('div'); const assi_btns = document.createElement('div');
assi_btns.classList.add('assi-btns'); 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"); const cbox = document.createElement("input");
cbox.type = "checkbox"; cbox.type = "checkbox";
cbox.value = value; cbox.value = value;
@ -402,12 +407,12 @@ function sauvegarderAssiduites() {
await nouvellePeriode(periode); await nouvellePeriode(periode);
} }
// Si il y n'a pas d'erreur, on affiche un message de succès // Si il n'y a pas d'erreur, on affiche un message de succès
if (data.errors.length == 0) { if (data.errors.length == 0) {
const span = document.createElement("span"); const span = document.createElement("span");
span.textContent = "Les assiduités ont bien été sauvegardées."; span.textContent = "Le relevé d'assiduité a été enregistré.";
openAlertModal( openAlertModal(
"Sauvegarde des assiduités", "Enregistrement de l'assiduité",
span, span,
null, null,
"var(--color-present)" "var(--color-present)"
@ -499,6 +504,8 @@ const moduleimpls = new Map();
const inscriptionsModules = new Map(); const inscriptionsModules = new Map();
const nonWorkDays = [{{ nonworkdays| safe }}]; const nonWorkDays = [{{ nonworkdays| safe }}];
window.nonPresent = {{ 'true' if non_present else 'false' }};
// Vérification du forçage de module // Vérification du forçage de module
window.forceModule = "{{ forcer_module }}" == "True"; window.forceModule = "{{ forcer_module }}" == "True";
if (window.forceModule) { if (window.forceModule) {
@ -518,12 +525,29 @@ if (window.forceModule) {
} }
}); });
} }
const defaultPlage = {{ nouv_plage | safe}} || [];
/** /**
* Fonction exécutée au lancement de la page * Fonction exécutée au lancement de la page
* - On affiche ou non les photos des étudiants * - On affiche ou non les photos des étudiants
* - On vérifie si la date est un jour travaillé * - On vérifie si la date est un jour travaillé
*/ */
async function main() { 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"; const checked = localStorage.getItem("scodoc-etud-pdp") == "true";
afficherPDP(checked); afficherPDP(checked);
$("#date").on("change", async function (d) { $("#date").on("change", async function (d) {
@ -532,7 +556,7 @@ async function main() {
}); });
} }
main(); window.addEventListener("load", main);
</script> </script>
@ -597,21 +621,23 @@ main();
</label> </label>
<label for="etatDef"> <label for="etatDef">
Intialiser les étudiants comme : Intialiser les étudiants comme :
<select name="etatDef" id="etatDef"> <select name="etatDef" id="etatDef">
<option value="">-</option> <option value="">-</option>
{% if not non_present %}
<option value="present">présents</option> <option value="present">présents</option>
{% endif %}
<option value="retard">en retard</option> <option value="retard">en retard</option>
<option value="absent">absents</option> <option value="absent">absents</option>
</select> </select>
</label> </label>
</div> </div>
<!-- Tableau à double entrée <!-- Tableau à double entrée
Colonne : Etudiants (Header = Nom, Prénom, Photo (si actif)) Colonne : Etudiants (Header = Nom, Prénom, Photo (si actif))
Ligne : Période (Header = Jour, Heure de début, Heure de fin, ModuleImplId) Ligne : Période (Header = Jour, Heure de début, Heure de fin, ModuleImplId)
Contenu : Contenu :
- bouton assiduité (présent, retard, absent) - bouton assiduité (présent, retard, absent)
- Bouton conflit si conflit de période - Bouton conflit si conflit de période
---> --->

View File

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

View File

@ -1,12 +1,28 @@
<li><span title="Vert" class="present demo"></span> &rightarrow; présence de l'étudiant lors de la période <p>Code couleur</p>
</li> <ul class="couleurs">
<li><span title="Orange" class="retard demo"></span> &rightarrow; retard de l'étudiant lors de la période <li><span title="Vert" class="present demo"></span> &rightarrow; présence de l'étudiant lors de la
</li> période
<li><span title="Rouge" class="absent demo"></span> &rightarrow; absence de l'étudiant lors de la période </li>
</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 <li><span title="Quart Bleu" class="est_just demo color"></span> &rightarrow; la période est couverte par un
justificatif valide</li> justificatif valide</li>
<li><span title="Hachure Rouge" class="invalid_justified demo"></span> &rightarrow; l'assiduité est <li><span title="Justif. non valide" class="invalide demo color "></span> &rightarrow; la période est
justifiée par un justificatif non valide / en attente de validation couverte par un justificatif non valide
</li> </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é); 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()); block.classList.add(assiduité.etat.toLowerCase());
if(assiduité.etat != "CRENEAU") block.classList.add("color"); if(assiduité.etat != "CRENEAU") block.classList.add("color");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@
<h2>Erreur !</h2> <h2>Erreur !</h2>
{{ exc | safe }} {{ exc }}
<div style="margin-top: 16px;"> <div style="margin-top: 16px;">
{% if g.scodoc_dept %} {% 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 <input class=groupe type=checkbox ${partition.show_in_lists ? "checked" : ""} data-attr=show_in_lists> Afficher sur bulletins et tableaux
</label> </label>
<label> <label>
<a class="stdlink" href="{{scu.ScoURL() <a class="stdlink" href="{{url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
}}/groups_auto_repartition/${partition.id}">Répartir les étudiants</a> }}/groups_auto_repartition/${partition.id}">Répartir les étudiants</a>
</label> </label>
</div> </div>

View File

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

View File

@ -1132,6 +1132,11 @@ def signal_assiduites_group():
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
dept_id=g.scodoc_dept_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_debut=str(formsemestre.date_debut),
formsemestre_date_fin=str(formsemestre.date_fin), formsemestre_date_fin=str(formsemestre.date_fin),
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
@ -1440,7 +1445,6 @@ def visu_assi_group():
formsemestre_modimpls=formsemestre_modimpls, formsemestre_modimpls=formsemestre_modimpls,
convert_values=(fmt == "html"), convert_values=(fmt == "html"),
) )
# Export en XLS # Export en XLS
if fmt.startswith("xls"): if fmt.startswith("xls"):
return scu.send_file( return scu.send_file(
@ -1915,8 +1919,29 @@ def _preparer_objet(
@scodoc @scodoc
@permission_required(Permission.AbsChange) @permission_required(Permission.AbsChange)
def signal_assiduites_diff(): def signal_assiduites_diff():
"""TODO documenter """
Utilisé notamment par "Saisie différée" sur tableau de bord semetstre" 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 # Récupération des paramètres de la requête
group_ids: list[int] = request.args.get("group_ids", None) 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>" grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>"
) )
# Pré-remplissage des sélecteurs
moduleimpl_id = request.args.get("moduleimpl_id", -1) moduleimpl_id = request.args.get("moduleimpl_id", -1)
try: try:
moduleimpl_id = int(moduleimpl_id) moduleimpl_id = int(moduleimpl_id)
except ValueError: except ValueError:
moduleimpl_id = -1 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( return render_template(
"assiduites/pages/signal_assiduites_diff.j2", "assiduites/pages/signal_assiduites_diff.j2",
@ -1978,6 +2015,12 @@ def signal_assiduites_diff():
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
dept_id=g.scodoc_dept_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("/")
@bp.route("/index_html") @bp.route("/index_html", alias=True)
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def index_html(): def index_html():
@ -807,7 +807,7 @@ def formation_import_xml_form():
{ html_sco_header.sco_footer() } { html_sco_header.sco_footer() }
""" """
elif tf[0] == -1: elif tf[0] == -1:
return flask.redirect(scu.NotesURL()) return flask.redirect(url_for("notes.index_html", scodoc_dept=g.scodoc_dept))
else: else:
formation_id, _, _ = sco_formations.formation_import_xml( formation_id, _, _ = sco_formations.formation_import_xml(
tf[2]["xmlfile"].read() tf[2]["xmlfile"].read()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -96,7 +96,7 @@ mkdir -p "$optdir" || die "mkdir failure for $optdir"
archive="$FACTORY_DIR"/"$PACKAGE_NAME-$RELEASE_TAG".tar.gz archive="$FACTORY_DIR"/"$PACKAGE_NAME-$RELEASE_TAG".tar.gz
echo "Downloading $GIT_RELEASE_URL ..." echo "Downloading $GIT_RELEASE_URL ..."
# curl -o "$archive" "$GIT_RELEASE_URL" || die "curl failure for $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 # -nv
# On décomprime # On décomprime

File diff suppressed because it is too large Load Diff