Compare commits
134 Commits
Author | SHA1 | Date | |
---|---|---|---|
320cfbebc8 | |||
e0208d0650 | |||
f18fbe284a | |||
|
6acf72c0c9 | ||
b675a57678 | |||
ecd753fae7 | |||
|
c55e02497e | ||
|
7c7697631e | ||
0fe60aaa64 | |||
|
09f697941d | ||
|
cefef8a89a | ||
b7cbfbae67 | |||
|
0fee2e612b | ||
1167c13787 | |||
963c09976b | |||
|
dbb5ac8946 | ||
|
93bb9d598e | ||
|
9801cf7936 | ||
|
db44e8e5f4 | ||
|
ffdaf2a19a | ||
|
ba77b155c5 | ||
fed84559fc | |||
02a5b00ecf | |||
dcdf6a8012 | |||
912a213dcd | |||
3575e89dc0 | |||
|
675eccd6b6 | ||
|
07a8658672 | ||
|
80bd02114e | ||
|
65a4b31fbd | ||
|
7bdae70d38 | ||
21c0625147 | |||
e18c1d8fd0 | |||
5867d0f430 | |||
9897ccc659 | |||
|
7575959bd4 | ||
|
2aafbad9e2 | ||
50f2cd7a0f | |||
fd8fbb9e02 | |||
|
ebcef76950 | ||
|
13349776af | ||
|
f275286b71 | ||
|
f4f6c13d79 | ||
e7f23efe65 | |||
e44d3fd5dc | |||
fac36fa11c | |||
9289535359 | |||
|
d73b925006 | ||
6749ca70d6 | |||
|
dea403b03d | ||
|
ab9543c310 | ||
|
f94998f66b | ||
|
eb88a8ca83 | ||
7042650fd9 | |||
2745ffd687 | |||
9a882ea41d | |||
ea6003e812 | |||
5c6935337e | |||
60998d2e20 | |||
29b877d9ed | |||
|
6834c19015 | ||
|
f47fc4ba46 | ||
5894c6f952 | |||
af1d1884c7 | |||
|
881bf82000 | ||
|
2ed4516a97 | ||
|
75ce1ccd31 | ||
|
f8d5f6ea11 | ||
|
70995fbd7e | ||
dc095765f2 | |||
|
1cec3fa703 | ||
|
032454aefd | ||
|
e3344cf424 | ||
|
d7acff9d35 | ||
|
decdf59e20 | ||
|
42fc08a3a3 | ||
|
f3770fb5c7 | ||
63b28a3277 | |||
bb23cdcea7 | |||
3ca5636454 | |||
42882154d5 | |||
489acb26d2 | |||
8ee373db7d | |||
8e56dc2418 | |||
b3331bd886 | |||
89afb672af | |||
8f25284038 | |||
f29002a57d | |||
69780b3f24 | |||
fbff151be0 | |||
3b436fa0f3 | |||
8847a1f008 | |||
ac882e9ccd | |||
000e016985 | |||
22d90215a0 | |||
043985bff6 | |||
d20ada1797 | |||
|
778fecabb6 | ||
|
fa6f83722e | ||
baa0412071 | |||
d51a47b71a | |||
f21ef41de6 | |||
2d673e7a5d | |||
3e43495831 | |||
|
a4db8c4ff8 | ||
|
1ac35d04c2 | ||
|
687ac3cf13 | ||
18b1f00586 | |||
|
6b985620e9 | ||
|
4d234ba353 | ||
|
5d45fcf656 | ||
|
0a5919b788 | ||
|
09f4525e66 | ||
0bc57807de | |||
87aaf12d27 | |||
c8ab9b9b6c | |||
ad7b48e110 | |||
f2ce16f161 | |||
1ddf9b6ab8 | |||
0a2e39cae1 | |||
a194b4b6e0 | |||
cbe85dfb7d | |||
beba69bfe4 | |||
41fec29452 | |||
9bd05ea241 | |||
58b831513d | |||
b861aba6a3 | |||
c2443c361f | |||
ab4731bd43 | |||
c17bc8b61b | |||
e44a5ee55d | |||
a747ed22e2 | |||
5d0a932634 | |||
2b150cf521 |
148
README.md
148
README.md
|
@ -2,7 +2,7 @@
|
|||
|
||||
(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>
|
||||
|
||||
|
@ -23,7 +23,7 @@ Flask, SQLAlchemy, au lien de Python2/Zope dans les versions précédentes).
|
|||
|
||||
### Lignes de commandes
|
||||
|
||||
Voir [https://scodoc.org/GuideConfig](le guide de configuration).
|
||||
Voir [le guide de configuration](https://scodoc.org/GuideConfig).
|
||||
|
||||
## 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é.
|
||||
|
||||
Principaux contenus:
|
||||
|
||||
/opt/scodoc-data
|
||||
/opt/scodoc-data/log # Fichiers de log ScoDoc
|
||||
/opt/scodoc-data/config # Fichiers de configuration
|
||||
```
|
||||
/opt/scodoc-data
|
||||
/opt/scodoc-data/log # Fichiers de log ScoDoc
|
||||
/opt/scodoc-data/config # Fichiers de configuration
|
||||
.../config/logos # Logos de l'établissement
|
||||
.../config/depts # un fichier par département
|
||||
/opt/scodoc-data/photos # Photos des étudiants
|
||||
/opt/scodoc-data/archives # Archives: PV de jury, maquettes Apogée, fichiers étudiants
|
||||
|
||||
/opt/scodoc-data/photos # Photos des étudiants
|
||||
/opt/scodoc-data/archives # Archives: PV de jury, maquettes Apogée, fichiers étudiants
|
||||
```
|
||||
## Pour les développeurs
|
||||
|
||||
### 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.
|
||||
```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
|
||||
mv /opt/scodoc /opt/off-scodoc # ou ce que vous voulez
|
||||
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
|
||||
|
||||
# Donner ce répertoire à l'utilisateur scodoc:
|
||||
chown -R scodoc:scodoc /opt/scodoc
|
||||
```
|
||||
Il faut ensuite installer l'environnement et le fichier de configuration:
|
||||
|
||||
# Le plus simple est de piquer le virtualenv configuré par l'installeur:
|
||||
mv /opt/off-scodoc/venv /opt/scodoc
|
||||
|
||||
```bash
|
||||
# Le plus simple est de piquer le virtualenv configuré par l'installeur:
|
||||
mv /opt/off-scodoc/venv /opt/scodoc
|
||||
```
|
||||
Et la config:
|
||||
|
||||
ln -s /opt/scodoc-data/.env /opt/scodoc
|
||||
|
||||
```bash
|
||||
ln -s /opt/scodoc-data/.env /opt/scodoc
|
||||
```
|
||||
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
|
||||
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`.
|
||||
Avant le premier lancement, créer cette base ainsi:
|
||||
|
||||
./tools/create_database.sh SCODOC_TEST
|
||||
export FLASK_ENV=test
|
||||
flask db upgrade
|
||||
|
||||
```bash
|
||||
./tools/create_database.sh SCODOC_TEST
|
||||
export FLASK_ENV=test
|
||||
flask db upgrade
|
||||
```
|
||||
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
|
||||
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
|
||||
scripts de tests:
|
||||
Lancer au préalable:
|
||||
|
||||
flask delete-dept -fy TEST00 && flask create-dept TEST00
|
||||
|
||||
```bash
|
||||
flask delete-dept -fy TEST00 && flask create-dept TEST00
|
||||
```
|
||||
Puis dérouler les tests unitaires:
|
||||
|
||||
pytest tests/unit
|
||||
|
||||
```bash
|
||||
pytest tests/unit
|
||||
```
|
||||
Ou avec couverture (`pip install pytest-cov`)
|
||||
|
||||
pytest --cov=app --cov-report=term-missing --cov-branch tests/unit/*
|
||||
|
||||
```bash
|
||||
pytest --cov=app --cov-report=term-missing --cov-branch tests/unit/*
|
||||
```
|
||||
#### 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
|
||||
|
@ -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
|
||||
par les tests:
|
||||
|
||||
export SCODOC_TEST_DATABASE_URI=postgresql:///SCODOC_DEV
|
||||
|
||||
```bash
|
||||
export SCODOC_TEST_DATABASE_URI=postgresql:///SCODOC_DEV
|
||||
```
|
||||
(si elle n'existe pas, voir plus loin pour la créer) puis de les lancer
|
||||
normalement, par exemple:
|
||||
|
||||
pytest tests/unit/test_sco_basic.py
|
||||
|
||||
```bash
|
||||
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
|
||||
utilisateur:
|
||||
|
||||
flask user-password admin
|
||||
|
||||
```bash
|
||||
flask user-password admin
|
||||
```
|
||||
**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 !
|
||||
|
||||
#### Modification du schéma de la base
|
||||
|
||||
On utilise SQLAlchemy avec Alembic et Flask-Migrate.
|
||||
|
||||
flask db migrate -m "message explicatif....."
|
||||
flask db upgrade
|
||||
|
||||
```bash
|
||||
flask db migrate -m "message explicatif....."
|
||||
flask db upgrade
|
||||
```
|
||||
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`
|
||||
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
|
||||
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)
|
||||
|
||||
# puis imports:
|
||||
flask import-scodoc7-users
|
||||
flask import-scodoc7-dept STID SCOSTID
|
||||
|
||||
# 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
|
||||
migration, utiliser les commandes `flask db history`et `flask db stamp`pour se
|
||||
positionner à la bonne étape.
|
||||
|
@ -163,23 +159,23 @@ positionner à la bonne étape.
|
|||
### Profiling
|
||||
|
||||
Sur une machine de DEV, lancer
|
||||
|
||||
flask profile --host 0.0.0.0 --length 32 --profile-dir /opt/scodoc-data
|
||||
|
||||
```bash
|
||||
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`).
|
||||
|
||||
Pour la visualisation, [snakeviz](https://jiffyclub.github.io/snakeviz/) est bien:
|
||||
|
||||
pip install snakeviz
|
||||
|
||||
```bash
|
||||
pip install snakeviz
|
||||
```
|
||||
puis
|
||||
|
||||
snakeviz -s --hostname 0.0.0.0 -p 5555 /opt/scodoc-data/GET.ScoDoc......prof
|
||||
|
||||
```bash
|
||||
snakeviz -s --hostname 0.0.0.0 -p 5555 /opt/scodoc-data/GET.ScoDoc......prof
|
||||
```
|
||||
## Paquet Debian 12
|
||||
|
||||
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).
|
||||
|
||||
La préparation d'une release se fait à l'aide du script
|
||||
|
|
|
@ -315,12 +315,6 @@ def create_app(config_class=DevConfig):
|
|||
app.register_error_handler(503, postgresql_server_error)
|
||||
app.register_error_handler(APIInvalidParams, handle_invalid_usage)
|
||||
|
||||
# Add some globals
|
||||
# previously in Flask-Bootstrap:
|
||||
app.jinja_env.globals["bootstrap_is_hidden_field"] = lambda field: isinstance(
|
||||
field, HiddenField
|
||||
)
|
||||
|
||||
from app.auth import bp as auth_bp
|
||||
|
||||
app.register_blueprint(auth_bp, url_prefix="/auth")
|
||||
|
@ -338,8 +332,15 @@ def create_app(config_class=DevConfig):
|
|||
from app.api import api_bp
|
||||
from app.api import api_web_bp
|
||||
|
||||
# Jinja2 configuration
|
||||
# Enable autoescaping of all templates, including .j2
|
||||
app.jinja_env.autoescape = select_autoescape(default_for_string=True, default=True)
|
||||
app.jinja_env.trim_blocks = True
|
||||
app.jinja_env.lstrip_blocks = True
|
||||
# previously in Flask-Bootstrap:
|
||||
app.jinja_env.globals["bootstrap_is_hidden_field"] = lambda field: isinstance(
|
||||
field, HiddenField
|
||||
)
|
||||
|
||||
# https://scodoc.fr/ScoDoc
|
||||
app.register_blueprint(scodoc_bp)
|
||||
|
@ -636,14 +637,12 @@ def critical_error(msg):
|
|||
import app.scodoc.sco_utils as scu
|
||||
|
||||
log(f"\n*** CRITICAL ERROR: {msg}")
|
||||
send_scodoc_alarm(f"CRITICAL ERROR: {msg}", msg)
|
||||
subject = f"CRITICAL ERROR: {msg}".strip()[:68]
|
||||
send_scodoc_alarm(subject, msg)
|
||||
clear_scodoc_cache()
|
||||
raise ScoValueError(
|
||||
f"""
|
||||
Une erreur est survenue.
|
||||
|
||||
Si le problème persiste, merci de contacter le support ScoDoc via
|
||||
{scu.SCO_DISCORD_ASSISTANCE}
|
||||
Une erreur est survenue, veuillez ré-essayer.
|
||||
|
||||
{msg}
|
||||
"""
|
||||
|
|
|
@ -3,14 +3,15 @@
|
|||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
"""ScoDoc 9 API : Assiduités
|
||||
"""
|
||||
"""ScoDoc 9 API : Assiduités"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from flask import g, request
|
||||
from flask_json import as_json
|
||||
from flask_login import current_user, login_required
|
||||
from flask_sqlalchemy.query import Query
|
||||
from sqlalchemy.orm.exc import ObjectDeletedError
|
||||
|
||||
from app import db, log, set_sco_dept
|
||||
import app.scodoc.sco_assiduites as scass
|
||||
|
@ -858,7 +859,10 @@ def assiduite_edit(assiduite_id: int):
|
|||
msg=f"assiduite: modif {assiduite_unique}",
|
||||
)
|
||||
db.session.commit()
|
||||
try:
|
||||
scass.simple_invalidate_cache(assiduite_unique.to_dict())
|
||||
except ObjectDeletedError:
|
||||
return json_error(404, "Assiduité supprimée / inexistante")
|
||||
|
||||
return {"OK": True}
|
||||
|
||||
|
|
|
@ -603,8 +603,19 @@ class Role(db.Model):
|
|||
"""Create default roles if missing, then, if reset_permissions,
|
||||
reset their permissions to default values.
|
||||
"""
|
||||
Role.reset_roles_permissions(
|
||||
SCO_ROLES_DEFAULTS, reset_permissions=reset_permissions
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def reset_roles_permissions(roles_perms: dict[str, tuple], reset_permissions=True):
|
||||
"""Ajoute les permissions aux roles
|
||||
roles_perms : { "role_name" : (permission, ...) }
|
||||
reset_permissions : si vrai efface permissions déja existantes
|
||||
Si le role n'existe pas, il est (re) créé.
|
||||
"""
|
||||
default_role = "Observateur"
|
||||
for role_name, permissions in SCO_ROLES_DEFAULTS.items():
|
||||
for role_name, permissions in roles_perms.items():
|
||||
role = Role.query.filter_by(name=role_name).first()
|
||||
if role is None:
|
||||
role = Role(name=role_name)
|
||||
|
|
|
@ -37,7 +37,17 @@ def form_ue_choix_parcours(ue: UniteEns) -> str:
|
|||
]
|
||||
# Choix des parcours
|
||||
ue_pids = [p.id for p in ue.parcours]
|
||||
H.append("""<form id="choix_parcours">""")
|
||||
H.append(
|
||||
"""
|
||||
<div class="help">
|
||||
Cocher tous les parcours dans lesquels cette UE est utilisée,
|
||||
même si vous n'offrez pas ce parcours dans votre département.
|
||||
Sans quoi, les UEs de Tronc Commun ne seront pas reconnues.
|
||||
Ne cocher aucun parcours est équivalent à tous les cocher.
|
||||
</div>
|
||||
<form id="choix_parcours" style="margin-top: 12px;">
|
||||
"""
|
||||
)
|
||||
|
||||
ects_differents = {
|
||||
ue.get_ects(parcour, only_parcours=True) for parcour in ref_comp.parcours
|
||||
|
|
|
@ -9,12 +9,14 @@
|
|||
|
||||
import collections
|
||||
import datetime
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from flask import g, has_request_context, url_for
|
||||
|
||||
from app import db
|
||||
from app.comp.moy_mod import ModuleImplResults
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.models import Evaluation, FormSemestre, Identite
|
||||
from app.models import Evaluation, FormSemestre, Identite, ModuleImpl
|
||||
from app.models.groups import GroupDescr
|
||||
from app.models.ues import UniteEns
|
||||
from app.scodoc import sco_bulletins, sco_utils as scu
|
||||
|
@ -229,7 +231,7 @@ class BulletinBUT:
|
|||
if res.modimpl_inscr_df[modimpl.id][etud.id]: # si inscrit
|
||||
d[modimpl.module.code] = {
|
||||
"id": modimpl.id,
|
||||
"titre": modimpl.module.titre,
|
||||
"titre": modimpl.module.titre_str(),
|
||||
"code_apogee": modimpl.module.code_apogee,
|
||||
"url": (
|
||||
url_for(
|
||||
|
@ -249,59 +251,88 @@ class BulletinBUT:
|
|||
# "moy": fmt_note(moyennes_etuds.mean()),
|
||||
},
|
||||
"evaluations": (
|
||||
[
|
||||
self.etud_eval_results(etud, e)
|
||||
for e in modimpl.evaluations
|
||||
if (e.visibulletin or version == "long")
|
||||
and (e.id in modimpl_results.evaluations_etat)
|
||||
and (
|
||||
modimpl_results.evaluations_etat[e.id].is_complete
|
||||
or self.prefs["bul_show_all_evals"]
|
||||
self.etud_list_modimpl_evaluations(
|
||||
etud, modimpl, modimpl_results, version
|
||||
)
|
||||
]
|
||||
if version != "short"
|
||||
else []
|
||||
),
|
||||
}
|
||||
return d
|
||||
|
||||
def etud_eval_results(self, etud, e: Evaluation) -> dict:
|
||||
def etud_list_modimpl_evaluations(
|
||||
self,
|
||||
etud: Identite,
|
||||
modimpl: ModuleImpl,
|
||||
modimpl_results: ModuleImplResults,
|
||||
version: str,
|
||||
) -> list[dict]:
|
||||
"""Liste des résultats aux évaluations de ce modimpl à montrer pour cet étudiant"""
|
||||
evaluation: Evaluation
|
||||
eval_results = []
|
||||
for evaluation in modimpl.evaluations:
|
||||
if (
|
||||
(evaluation.visibulletin or version == "long")
|
||||
and (evaluation.id in modimpl_results.evaluations_etat)
|
||||
and (
|
||||
modimpl_results.evaluations_etat[evaluation.id].is_complete
|
||||
or self.prefs["bul_show_all_evals"]
|
||||
)
|
||||
):
|
||||
eval_notes = self.res.modimpls_results[modimpl.id].evals_notes[
|
||||
evaluation.id
|
||||
]
|
||||
|
||||
if (evaluation.evaluation_type == Evaluation.EVALUATION_NORMALE) or (
|
||||
not np.isnan(eval_notes[etud.id])
|
||||
):
|
||||
eval_results.append(
|
||||
self.etud_eval_results(etud, evaluation, eval_notes)
|
||||
)
|
||||
return eval_results
|
||||
|
||||
def etud_eval_results(
|
||||
self, etud: Identite, evaluation: Evaluation, eval_notes: pd.DataFrame
|
||||
) -> dict:
|
||||
"dict resultats d'un étudiant à une évaluation"
|
||||
# eval_notes est une pd.Series avec toutes les notes des étudiants inscrits
|
||||
eval_notes = self.res.modimpls_results[e.moduleimpl_id].evals_notes[e.id]
|
||||
notes_ok = eval_notes.where(eval_notes > scu.NOTES_ABSENCE).dropna()
|
||||
modimpls_evals_poids = self.res.modimpls_evals_poids[e.moduleimpl_id]
|
||||
modimpls_evals_poids = self.res.modimpls_evals_poids[evaluation.moduleimpl_id]
|
||||
try:
|
||||
etud_ues_ids = self.res.etud_ues_ids(etud.id)
|
||||
poids = {
|
||||
ue.acronyme: modimpls_evals_poids[ue.id][e.id]
|
||||
ue.acronyme: modimpls_evals_poids[ue.id][evaluation.id]
|
||||
for ue in self.res.ues
|
||||
if (ue.type != UE_SPORT) and (ue.id in etud_ues_ids)
|
||||
}
|
||||
except KeyError:
|
||||
poids = collections.defaultdict(lambda: 0.0)
|
||||
d = {
|
||||
"id": e.id,
|
||||
"id": evaluation.id,
|
||||
"coef": (
|
||||
fmt_note(e.coefficient)
|
||||
if e.evaluation_type == Evaluation.EVALUATION_NORMALE
|
||||
fmt_note(evaluation.coefficient)
|
||||
if evaluation.evaluation_type == Evaluation.EVALUATION_NORMALE
|
||||
else None
|
||||
),
|
||||
"date_debut": e.date_debut.isoformat() if e.date_debut else None,
|
||||
"date_fin": e.date_fin.isoformat() if e.date_fin else None,
|
||||
"description": e.description,
|
||||
"evaluation_type": e.evaluation_type,
|
||||
"date_debut": (
|
||||
evaluation.date_debut.isoformat() if evaluation.date_debut else None
|
||||
),
|
||||
"date_fin": (
|
||||
evaluation.date_fin.isoformat() if evaluation.date_fin else None
|
||||
),
|
||||
"description": evaluation.description,
|
||||
"evaluation_type": evaluation.evaluation_type,
|
||||
"note": (
|
||||
{
|
||||
"value": fmt_note(
|
||||
eval_notes[etud.id],
|
||||
note_max=e.note_max,
|
||||
note_max=evaluation.note_max,
|
||||
),
|
||||
"min": fmt_note(notes_ok.min(), note_max=e.note_max),
|
||||
"max": fmt_note(notes_ok.max(), note_max=e.note_max),
|
||||
"moy": fmt_note(notes_ok.mean(), note_max=e.note_max),
|
||||
"min": fmt_note(notes_ok.min(), note_max=evaluation.note_max),
|
||||
"max": fmt_note(notes_ok.max(), note_max=evaluation.note_max),
|
||||
"moy": fmt_note(notes_ok.mean(), note_max=evaluation.note_max),
|
||||
}
|
||||
if not e.is_blocked()
|
||||
if not evaluation.is_blocked()
|
||||
else {}
|
||||
),
|
||||
"poids": poids,
|
||||
|
@ -309,17 +340,25 @@ class BulletinBUT:
|
|||
url_for(
|
||||
"notes.evaluation_listenotes",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
evaluation_id=e.id,
|
||||
evaluation_id=evaluation.id,
|
||||
)
|
||||
if has_request_context()
|
||||
else "na"
|
||||
),
|
||||
# deprecated (supprimer avant #sco9.7)
|
||||
"date": e.date_debut.isoformat() if e.date_debut else None,
|
||||
"heure_debut": (
|
||||
e.date_debut.time().isoformat("minutes") if e.date_debut else None
|
||||
"date": (
|
||||
evaluation.date_debut.isoformat() if evaluation.date_debut else None
|
||||
),
|
||||
"heure_debut": (
|
||||
evaluation.date_debut.time().isoformat("minutes")
|
||||
if evaluation.date_debut
|
||||
else None
|
||||
),
|
||||
"heure_fin": (
|
||||
evaluation.date_fin.time().isoformat("minutes")
|
||||
if evaluation.date_fin
|
||||
else None
|
||||
),
|
||||
"heure_fin": e.date_fin.time().isoformat("minutes") if e.date_fin else None,
|
||||
}
|
||||
return d
|
||||
|
||||
|
@ -540,9 +579,9 @@ class BulletinBUT:
|
|||
|
||||
d.update(infos)
|
||||
# --- Rangs
|
||||
d[
|
||||
"rang_nt"
|
||||
] = f"{d['semestre']['rang']['value']} / {d['semestre']['rang']['total']}"
|
||||
d["rang_nt"] = (
|
||||
f"{d['semestre']['rang']['value']} / {d['semestre']['rang']['total']}"
|
||||
)
|
||||
d["rang_txt"] = "Rang " + d["rang_nt"]
|
||||
|
||||
d.update(sco_bulletins.make_context_dict(self.res.formsemestre, d["etud"]))
|
||||
|
|
|
@ -124,7 +124,9 @@ def _build_bulletin_but_infos(
|
|||
formsemestre, bulletins_sem.res
|
||||
)
|
||||
if warn_html:
|
||||
raise ScoValueError("<b>Formation mal configurée pour le BUT</b>" + warn_html)
|
||||
raise ScoValueError(
|
||||
"<b>Formation mal configurée pour le BUT</b>" + warn_html, safe=True
|
||||
)
|
||||
ue_validation_by_niveau = validations_view.get_ue_validation_by_niveau(
|
||||
refcomp, etud
|
||||
)
|
||||
|
|
|
@ -73,6 +73,7 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
|||
html_class="notes_bulletin",
|
||||
html_class_ignore_default=True,
|
||||
html_with_td_classes=True,
|
||||
table_id="bul-table",
|
||||
)
|
||||
table_objects = table.gen(fmt=fmt)
|
||||
objects += table_objects
|
||||
|
@ -427,12 +428,11 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
|||
if e["evaluation_type"] == Evaluation.EVALUATION_NORMALE
|
||||
else "*"
|
||||
)
|
||||
note_value = e["note"].get("value", "")
|
||||
t = {
|
||||
"titre": f"{e['description'] or ''}",
|
||||
"moyenne": e["note"]["value"],
|
||||
"_moyenne_pdf": Paragraph(
|
||||
f"""<para align=right>{e["note"]["value"]}</para>"""
|
||||
),
|
||||
"moyenne": note_value,
|
||||
"_moyenne_pdf": Paragraph(f"""<para align=right>{note_value}</para>"""),
|
||||
"coef": coef,
|
||||
"_coef_pdf": Paragraph(
|
||||
f"""<para align=right fontSize={self.small_fontsize}><i>{
|
||||
|
|
|
@ -44,7 +44,7 @@ from app.scodoc import sco_cursus_dut
|
|||
class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
|
||||
"""Pour compat ScoDoc 7: à revoir pour le BUT"""
|
||||
|
||||
def __init__(self, etud: dict, formsemestre_id: int, res: ResultatsSemestreBUT):
|
||||
def __init__(self, etud: Identite, formsemestre_id: int, res: ResultatsSemestreBUT):
|
||||
super().__init__(etud, formsemestre_id, res)
|
||||
# Ajustements pour le BUT
|
||||
self.can_compensate_with_prev = False # jamais de compensation à la mode DUT
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
|
||||
"""caches pour tables APC
|
||||
"""
|
||||
from flask import g
|
||||
|
||||
from app.scodoc import sco_cache
|
||||
|
||||
|
@ -47,3 +48,27 @@ class EvaluationsPoidsCache(sco_cache.ScoDocCache):
|
|||
"""
|
||||
|
||||
prefix = "EPC"
|
||||
|
||||
@classmethod
|
||||
def invalidate_all(cls):
|
||||
"delete all cached evaluations poids (in current dept)"
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
|
||||
moduleimpl_ids = [
|
||||
mi.id
|
||||
for mi in ModuleImpl.query.join(FormSemestre).filter_by(
|
||||
dept_id=g.scodoc_dept_id
|
||||
)
|
||||
]
|
||||
cls.delete_many(moduleimpl_ids)
|
||||
|
||||
@classmethod
|
||||
def invalidate_sem(cls, formsemestre_id):
|
||||
"delete cached evaluations poids for this formsemestre from cache"
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
|
||||
moduleimpl_ids = [
|
||||
mi.id for mi in ModuleImpl.query.filter_by(formsemestre_id=formsemestre_id)
|
||||
]
|
||||
cls.delete_many(moduleimpl_ids)
|
||||
|
|
|
@ -45,7 +45,6 @@ from app.models import Evaluation, EvaluationUEPoids, ModuleImpl
|
|||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.codes_cursus import UE_SPORT
|
||||
from app.scodoc.sco_exceptions import ScoBugCatcher
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
|
||||
|
@ -113,6 +112,8 @@ class ModuleImplResults:
|
|||
"""
|
||||
self.evals_etudids_sans_note = {}
|
||||
"""dict: evaluation_id : set des etudids non notés dans cette eval, sans les démissions."""
|
||||
self.evals_type = {}
|
||||
"""Type de chaque eval { evaluation.id : evaluation.evaluation_type }"""
|
||||
self.load_notes(etudids, etudids_actifs)
|
||||
self.etuds_use_session2 = pd.Series(False, index=self.evals_notes.index)
|
||||
"""1 bool par etud, indique si sa moyenne de module vient de la session2"""
|
||||
|
@ -164,7 +165,10 @@ class ModuleImplResults:
|
|||
self.evaluations_completes = []
|
||||
self.evaluations_completes_dict = {}
|
||||
self.etudids_attente = set() # empty
|
||||
self.evals_type = {}
|
||||
evaluation: Evaluation
|
||||
for evaluation in moduleimpl.evaluations:
|
||||
self.evals_type[evaluation.id] = evaluation.evaluation_type
|
||||
eval_df = self._load_evaluation_notes(evaluation)
|
||||
# is_complete ssi
|
||||
# tous les inscrits (non dem) au module ont une note
|
||||
|
@ -270,6 +274,24 @@ class ModuleImplResults:
|
|||
* self.evaluations_completes
|
||||
).reshape(-1, 1)
|
||||
|
||||
def get_evaluations_special_coefs(
|
||||
self, modimpl: ModuleImpl, evaluation_type=Evaluation.EVALUATION_SESSION2
|
||||
) -> np.array:
|
||||
"""Coefficients des évaluations de session 2 ou rattrapage.
|
||||
Les évals de session 2 et rattrapage sont réputées "complètes": elles sont toujours
|
||||
prises en compte mais seules les notes numériques et ABS sont utilisées.
|
||||
Résultat: 2d-array of floats, shape (nb_evals, 1)
|
||||
"""
|
||||
return (
|
||||
np.array(
|
||||
[
|
||||
(e.coefficient if e.evaluation_type == evaluation_type else 0.0)
|
||||
for e in modimpl.evaluations
|
||||
],
|
||||
dtype=float,
|
||||
)
|
||||
).reshape(-1, 1)
|
||||
|
||||
# was _list_notes_evals_titles
|
||||
def get_evaluations_completes(self, moduleimpl: ModuleImpl) -> list[Evaluation]:
|
||||
"Liste des évaluations complètes"
|
||||
|
@ -296,32 +318,26 @@ class ModuleImplResults:
|
|||
for (etudid, x) in self.evals_notes[evaluation_id].items()
|
||||
}
|
||||
|
||||
def get_evaluation_rattrapage(self, moduleimpl: ModuleImpl) -> Evaluation | None:
|
||||
"""L'évaluation de rattrapage de ce module, ou None s'il n'en a pas.
|
||||
def get_evaluations_rattrapage(self, moduleimpl: ModuleImpl) -> list[Evaluation]:
|
||||
"""Les évaluations de rattrapage de ce module.
|
||||
Rattrapage: la moyenne du module est la meilleure note entre moyenne
|
||||
des autres évals et la note eval rattrapage.
|
||||
des autres évals et la moyenne des notes de rattrapage.
|
||||
"""
|
||||
eval_list = [
|
||||
return [
|
||||
e
|
||||
for e in moduleimpl.evaluations
|
||||
if e.evaluation_type == Evaluation.EVALUATION_RATTRAPAGE
|
||||
]
|
||||
if eval_list:
|
||||
return eval_list[0]
|
||||
return None
|
||||
|
||||
def get_evaluation_session2(self, moduleimpl: ModuleImpl) -> Evaluation | None:
|
||||
"""L'évaluation de deuxième session de ce module, ou None s'il n'en a pas.
|
||||
Session 2: remplace la note de moyenne des autres évals.
|
||||
def get_evaluations_session2(self, moduleimpl: ModuleImpl) -> list[Evaluation]:
|
||||
"""Les évaluations de deuxième session de ce module, ou None s'il n'en a pas.
|
||||
La moyenne des notes de Session 2 remplace la note de moyenne des autres évals.
|
||||
"""
|
||||
eval_list = [
|
||||
return [
|
||||
e
|
||||
for e in moduleimpl.evaluations
|
||||
if e.evaluation_type == Evaluation.EVALUATION_SESSION2
|
||||
]
|
||||
if eval_list:
|
||||
return eval_list[0]
|
||||
return None
|
||||
|
||||
def get_evaluations_bonus(self, modimpl: ModuleImpl) -> list[Evaluation]:
|
||||
"""Les évaluations bonus de ce module, ou liste vide s'il n'en a pas."""
|
||||
|
@ -344,12 +360,13 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
|||
"Calcul des moyennes de modules à la mode BUT"
|
||||
|
||||
def compute_module_moy(
|
||||
self,
|
||||
evals_poids_df: pd.DataFrame,
|
||||
self, evals_poids_df: pd.DataFrame, modimpl_coefs_df: pd.DataFrame
|
||||
) -> pd.DataFrame:
|
||||
"""Calcule les moyennes des étudiants dans ce module
|
||||
|
||||
Argument: evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs
|
||||
Argument:
|
||||
evals_poids: DataFrame, colonnes: UEs, lignes: EVALs
|
||||
modimpl_coefs_df: DataFrame, colonnes: modimpl_id, lignes: ue_id
|
||||
|
||||
Résultat: DataFrame, colonnes UE, lignes etud
|
||||
= la note de l'étudiant dans chaque UE pour ce module.
|
||||
|
@ -370,6 +387,7 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
|||
return pd.DataFrame(index=[], columns=evals_poids_df.columns)
|
||||
if nb_ues == 0:
|
||||
return pd.DataFrame(index=self.evals_notes.index, columns=[])
|
||||
# coefs des évals complètes normales (pas rattr., session 2 ni bonus):
|
||||
evals_coefs = self.get_evaluations_coefs(modimpl)
|
||||
evals_poids = evals_poids_df.values * evals_coefs
|
||||
# -> evals_poids shape : (nb_evals, nb_ues)
|
||||
|
@ -398,6 +416,47 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
|||
) / np.sum(evals_poids_etuds, axis=1)
|
||||
# etuds_moy_module shape: nb_etuds x nb_ues
|
||||
|
||||
evals_session2 = self.get_evaluations_session2(modimpl)
|
||||
evals_rat = self.get_evaluations_rattrapage(modimpl)
|
||||
if evals_session2:
|
||||
# Session2 : quand elle existe, remplace la note de module
|
||||
# Calcul moyenne notes session2 et remplace (si la note session 2 existe)
|
||||
etuds_moy_module_s2 = self._compute_moy_special(
|
||||
modimpl,
|
||||
evals_notes_stacked,
|
||||
evals_poids_df,
|
||||
Evaluation.EVALUATION_SESSION2,
|
||||
)
|
||||
|
||||
# Vrai si toutes les UEs avec coef non nul ont bien une note de session 2 calculée:
|
||||
mod_coefs = modimpl_coefs_df[modimpl.id]
|
||||
etuds_use_session2 = np.all(
|
||||
np.isfinite(etuds_moy_module_s2[:, mod_coefs != 0]), axis=1
|
||||
)
|
||||
etuds_moy_module = np.where(
|
||||
etuds_use_session2[:, np.newaxis],
|
||||
etuds_moy_module_s2,
|
||||
etuds_moy_module,
|
||||
)
|
||||
self.etuds_use_session2 = pd.Series(
|
||||
etuds_use_session2, index=self.evals_notes.index
|
||||
)
|
||||
elif evals_rat:
|
||||
etuds_moy_module_rat = self._compute_moy_special(
|
||||
modimpl,
|
||||
evals_notes_stacked,
|
||||
evals_poids_df,
|
||||
Evaluation.EVALUATION_RATTRAPAGE,
|
||||
)
|
||||
etuds_ue_use_rattrapage = (
|
||||
etuds_moy_module_rat > etuds_moy_module
|
||||
) # etud x UE
|
||||
etuds_moy_module = np.where(
|
||||
etuds_ue_use_rattrapage, etuds_moy_module_rat, etuds_moy_module
|
||||
)
|
||||
self.etuds_use_rattrapage = pd.Series(
|
||||
np.any(etuds_ue_use_rattrapage, axis=1), index=self.evals_notes.index
|
||||
)
|
||||
# Application des évaluations bonus:
|
||||
etuds_moy_module = self.apply_bonus(
|
||||
etuds_moy_module,
|
||||
|
@ -405,47 +464,6 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
|||
evals_poids_df,
|
||||
evals_notes_stacked,
|
||||
)
|
||||
|
||||
# Session2 : quand elle existe, remplace la note de module
|
||||
eval_session2 = self.get_evaluation_session2(modimpl)
|
||||
if eval_session2:
|
||||
notes_session2 = self.evals_notes[eval_session2.id].values
|
||||
# n'utilise que les notes valides (pas ATT, EXC, ABS, NaN)
|
||||
etuds_use_session2 = notes_session2 > scu.NOTES_ABSENCE
|
||||
etuds_moy_module = np.where(
|
||||
etuds_use_session2[:, np.newaxis],
|
||||
np.tile(
|
||||
(notes_session2 / (eval_session2.note_max / 20.0))[:, np.newaxis],
|
||||
nb_ues,
|
||||
),
|
||||
etuds_moy_module,
|
||||
)
|
||||
self.etuds_use_session2 = pd.Series(
|
||||
etuds_use_session2, index=self.evals_notes.index
|
||||
)
|
||||
else:
|
||||
# Rattrapage: remplace la note de module ssi elle est supérieure
|
||||
eval_rat = self.get_evaluation_rattrapage(modimpl)
|
||||
if eval_rat:
|
||||
notes_rat = self.evals_notes[eval_rat.id].values
|
||||
# remplace les notes invalides (ATT, EXC...) par des NaN
|
||||
notes_rat = np.where(
|
||||
notes_rat > scu.NOTES_ABSENCE,
|
||||
notes_rat / (eval_rat.note_max / 20.0),
|
||||
np.nan,
|
||||
)
|
||||
# "Étend" le rattrapage sur les UE: la note de rattrapage est la même
|
||||
# pour toutes les UE mais ne remplace que là où elle est supérieure
|
||||
notes_rat_ues = np.stack([notes_rat] * nb_ues, axis=1)
|
||||
# prend le max
|
||||
etuds_use_rattrapage = notes_rat_ues > etuds_moy_module
|
||||
etuds_moy_module = np.where(
|
||||
etuds_use_rattrapage, notes_rat_ues, etuds_moy_module
|
||||
)
|
||||
# Serie indiquant que l'étudiant utilise une note de rattrapage sur l'une des UE:
|
||||
self.etuds_use_rattrapage = pd.Series(
|
||||
etuds_use_rattrapage.any(axis=1), index=self.evals_notes.index
|
||||
)
|
||||
self.etuds_moy_module = pd.DataFrame(
|
||||
etuds_moy_module,
|
||||
index=self.evals_notes.index,
|
||||
|
@ -453,6 +471,34 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
|||
)
|
||||
return self.etuds_moy_module
|
||||
|
||||
def _compute_moy_special(
|
||||
self,
|
||||
modimpl: ModuleImpl,
|
||||
evals_notes_stacked: np.array,
|
||||
evals_poids_df: pd.DataFrame,
|
||||
evaluation_type: int,
|
||||
) -> np.array:
|
||||
"""Calcul moyenne APC sur évals rattrapage ou session2"""
|
||||
nb_etuds = self.evals_notes.shape[0]
|
||||
nb_ues = evals_poids_df.shape[1]
|
||||
evals_coefs_s2 = self.get_evaluations_special_coefs(
|
||||
modimpl, evaluation_type=evaluation_type
|
||||
)
|
||||
evals_poids_s2 = evals_poids_df.values * evals_coefs_s2
|
||||
poids_stacked_s2 = np.stack(
|
||||
[evals_poids_s2] * nb_etuds
|
||||
) # nb_etuds, nb_evals, nb_ues
|
||||
evals_poids_etuds_s2 = np.where(
|
||||
np.stack([self.evals_notes.values] * nb_ues, axis=2) > scu.NOTES_NEUTRALISE,
|
||||
poids_stacked_s2,
|
||||
0,
|
||||
)
|
||||
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
|
||||
etuds_moy_module_s2 = np.sum(
|
||||
evals_poids_etuds_s2 * evals_notes_stacked, axis=1
|
||||
) / np.sum(evals_poids_etuds_s2, axis=1)
|
||||
return etuds_moy_module_s2
|
||||
|
||||
def apply_bonus(
|
||||
self,
|
||||
etuds_moy_module: pd.DataFrame,
|
||||
|
@ -525,6 +571,7 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
|
|||
return evals_poids, ues
|
||||
|
||||
|
||||
# appelé par ModuleImpl.check_apc_conformity()
|
||||
def moduleimpl_is_conforme(
|
||||
moduleimpl, evals_poids: pd.DataFrame, modimpl_coefs_df: pd.DataFrame
|
||||
) -> bool:
|
||||
|
@ -546,12 +593,12 @@ def moduleimpl_is_conforme(
|
|||
if len(modimpl_coefs_df) != nb_ues:
|
||||
# il arrive (#bug) que le cache ne soit pas à jour...
|
||||
sco_cache.invalidate_formsemestre()
|
||||
raise ScoBugCatcher("moduleimpl_is_conforme: nb ue incoherent")
|
||||
return app.critical_error("moduleimpl_is_conforme: err 1")
|
||||
|
||||
if moduleimpl.id not in modimpl_coefs_df:
|
||||
# soupçon de bug cache coef ?
|
||||
sco_cache.invalidate_formsemestre()
|
||||
raise ScoBugCatcher("Erreur 454 - merci de ré-essayer")
|
||||
return app.critical_error("moduleimpl_is_conforme: err 2")
|
||||
|
||||
module_evals_poids = evals_poids.transpose().sum(axis=1) != 0
|
||||
return all((modimpl_coefs_df[moduleimpl.id] != 0).eq(module_evals_poids))
|
||||
|
@ -593,46 +640,43 @@ class ModuleImplResultsClassic(ModuleImplResults):
|
|||
evals_coefs_etuds * evals_notes_20, axis=1
|
||||
) / np.sum(evals_coefs_etuds, axis=1)
|
||||
|
||||
# Application des évaluations bonus:
|
||||
etuds_moy_module = self.apply_bonus(
|
||||
etuds_moy_module,
|
||||
modimpl,
|
||||
evals_notes_20,
|
||||
)
|
||||
|
||||
evals_session2 = self.get_evaluations_session2(modimpl)
|
||||
evals_rat = self.get_evaluations_rattrapage(modimpl)
|
||||
if evals_session2:
|
||||
# Session2 : quand elle existe, remplace la note de module
|
||||
eval_session2 = self.get_evaluation_session2(modimpl)
|
||||
if eval_session2:
|
||||
notes_session2 = self.evals_notes[eval_session2.id].values
|
||||
# n'utilise que les notes valides (pas ATT, EXC, ABS, NaN)
|
||||
etuds_use_session2 = notes_session2 > scu.NOTES_ABSENCE
|
||||
# Calcule la moyenne des évaluations de session2
|
||||
etuds_moy_module_s2 = self._compute_moy_special(
|
||||
modimpl, evals_notes_20, Evaluation.EVALUATION_SESSION2
|
||||
)
|
||||
etuds_use_session2 = np.isfinite(etuds_moy_module_s2)
|
||||
etuds_moy_module = np.where(
|
||||
etuds_use_session2,
|
||||
notes_session2 / (eval_session2.note_max / 20.0),
|
||||
etuds_moy_module_s2,
|
||||
etuds_moy_module,
|
||||
)
|
||||
self.etuds_use_session2 = pd.Series(
|
||||
etuds_use_session2, index=self.evals_notes.index
|
||||
)
|
||||
else:
|
||||
elif evals_rat:
|
||||
# Rattrapage: remplace la note de module ssi elle est supérieure
|
||||
eval_rat = self.get_evaluation_rattrapage(modimpl)
|
||||
if eval_rat:
|
||||
notes_rat = self.evals_notes[eval_rat.id].values
|
||||
# remplace les notes invalides (ATT, EXC...) par des NaN
|
||||
notes_rat = np.where(
|
||||
notes_rat > scu.NOTES_ABSENCE,
|
||||
notes_rat / (eval_rat.note_max / 20.0),
|
||||
np.nan,
|
||||
# Calcule la moyenne des évaluations de rattrapage
|
||||
etuds_moy_module_rat = self._compute_moy_special(
|
||||
modimpl, evals_notes_20, Evaluation.EVALUATION_RATTRAPAGE
|
||||
)
|
||||
# prend le max
|
||||
etuds_use_rattrapage = notes_rat > etuds_moy_module
|
||||
etuds_use_rattrapage = etuds_moy_module_rat > etuds_moy_module
|
||||
etuds_moy_module = np.where(
|
||||
etuds_use_rattrapage, notes_rat, etuds_moy_module
|
||||
etuds_use_rattrapage, etuds_moy_module_rat, etuds_moy_module
|
||||
)
|
||||
self.etuds_use_rattrapage = pd.Series(
|
||||
etuds_use_rattrapage, index=self.evals_notes.index
|
||||
)
|
||||
|
||||
# Application des évaluations bonus:
|
||||
etuds_moy_module = self.apply_bonus(
|
||||
etuds_moy_module,
|
||||
modimpl,
|
||||
evals_notes_20,
|
||||
)
|
||||
self.etuds_moy_module = pd.Series(
|
||||
etuds_moy_module,
|
||||
index=self.evals_notes.index,
|
||||
|
@ -640,6 +684,28 @@ class ModuleImplResultsClassic(ModuleImplResults):
|
|||
|
||||
return self.etuds_moy_module
|
||||
|
||||
def _compute_moy_special(
|
||||
self, modimpl: ModuleImpl, evals_notes_20: np.array, evaluation_type: int
|
||||
) -> np.array:
|
||||
"""Calcul moyenne sur évals rattrapage ou session2"""
|
||||
# n'utilise que les notes valides et ABS (0).
|
||||
# Même calcul que pour les évals normales, mais avec seulement les
|
||||
# coefs des évals de session 2 ou rattrapage:
|
||||
nb_etuds = self.evals_notes.shape[0]
|
||||
evals_coefs = self.get_evaluations_special_coefs(
|
||||
modimpl, evaluation_type=evaluation_type
|
||||
).reshape(-1)
|
||||
coefs_stacked = np.stack([evals_coefs] * nb_etuds)
|
||||
# zéro partout sauf si une note ou ABS:
|
||||
evals_coefs_etuds = np.where(
|
||||
self.evals_notes.values > scu.NOTES_NEUTRALISE, coefs_stacked, 0
|
||||
)
|
||||
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
|
||||
etuds_moy_module = np.sum(
|
||||
evals_coefs_etuds * evals_notes_20, axis=1
|
||||
) / np.sum(evals_coefs_etuds, axis=1)
|
||||
return etuds_moy_module # array 1d (nb_etuds)
|
||||
|
||||
def apply_bonus(
|
||||
self,
|
||||
etuds_moy_module: np.ndarray,
|
||||
|
|
|
@ -183,7 +183,9 @@ def notes_sem_assemble_cube(modimpls_notes: list[pd.DataFrame]) -> np.ndarray:
|
|||
return modimpls_notes.swapaxes(0, 1)
|
||||
|
||||
|
||||
def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
|
||||
def notes_sem_load_cube(
|
||||
formsemestre: FormSemestre, modimpl_coefs_df: pd.DataFrame
|
||||
) -> tuple:
|
||||
"""Construit le "cube" (tenseur) des notes du semestre.
|
||||
Charge toutes les notes (sql), calcule les moyennes des modules
|
||||
et assemble le cube.
|
||||
|
@ -207,8 +209,8 @@ def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
|
|||
etudids, etudids_actifs = formsemestre.etudids_actifs()
|
||||
for modimpl in formsemestre.modimpls_sorted:
|
||||
mod_results = moy_mod.ModuleImplResultsAPC(modimpl, etudids, etudids_actifs)
|
||||
evals_poids, _ = moy_mod.load_evaluations_poids(modimpl.id)
|
||||
etuds_moy_module = mod_results.compute_module_moy(evals_poids)
|
||||
evals_poids = modimpl.get_evaluations_poids()
|
||||
etuds_moy_module = mod_results.compute_module_moy(evals_poids, modimpl_coefs_df)
|
||||
modimpls_results[modimpl.id] = mod_results
|
||||
modimpls_evals_poids[modimpl.id] = evals_poids
|
||||
modimpls_notes.append(etuds_moy_module)
|
||||
|
|
|
@ -59,16 +59,17 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||
|
||||
def compute(self):
|
||||
"Charge les notes et inscriptions et calcule les moyennes d'UE et gen."
|
||||
self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs(
|
||||
self.formsemestre, modimpls=self.formsemestre.modimpls_sorted
|
||||
)
|
||||
(
|
||||
self.sem_cube,
|
||||
self.modimpls_evals_poids,
|
||||
self.modimpls_results,
|
||||
) = moy_ue.notes_sem_load_cube(self.formsemestre)
|
||||
) = moy_ue.notes_sem_load_cube(self.formsemestre, self.modimpl_coefs_df)
|
||||
self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre)
|
||||
self.ues_inscr_parcours_df = self.load_ues_inscr_parcours()
|
||||
self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs(
|
||||
self.formsemestre, modimpls=self.formsemestre.modimpls_sorted
|
||||
)
|
||||
|
||||
# l'idx de la colonne du mod modimpl.id est
|
||||
# modimpl_coefs_df.columns.get_loc(modimpl.id)
|
||||
# idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id)
|
||||
|
|
|
@ -242,7 +242,8 @@ class ResultatsSemestreClassic(NotesTableCompat):
|
|||
)
|
||||
}">saisir le coefficient de cette UE avant de continuer</a></p>
|
||||
</div>
|
||||
"""
|
||||
""",
|
||||
safe=True,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -518,7 +518,8 @@ class ResultatsSemestre(ResultatsCache):
|
|||
Corrigez ou faite corriger le programme
|
||||
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
|
||||
formation_id=ue_capitalized.formation_id)}">via cette page</a>.
|
||||
"""
|
||||
""",
|
||||
safe=True,
|
||||
)
|
||||
else:
|
||||
# Coefs de l'UE capitalisée en formation classique:
|
||||
|
|
12
app/email.py
12
app/email.py
|
@ -9,9 +9,9 @@ import datetime
|
|||
from threading import Thread
|
||||
|
||||
from flask import current_app, g
|
||||
from flask_mail import Message
|
||||
from flask_mail import BadHeaderError, Message
|
||||
|
||||
from app import mail
|
||||
from app import log, mail
|
||||
from app.models.departements import Departement
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
from app.scodoc import sco_preferences
|
||||
|
@ -20,7 +20,15 @@ from app.scodoc import sco_preferences
|
|||
def send_async_email(app, msg):
|
||||
"Send an email, async"
|
||||
with app.app_context():
|
||||
try:
|
||||
mail.send(msg)
|
||||
except BadHeaderError:
|
||||
log(
|
||||
f"""send_async_email: BadHeaderError
|
||||
msg={msg}
|
||||
"""
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
def send_email(
|
||||
|
|
|
@ -59,3 +59,4 @@ def check_taxe_now(taxes):
|
|||
|
||||
|
||||
from app.entreprises import routes
|
||||
from app.entreprises.activate import activate_module
|
||||
|
|
31
app/entreprises/activate.py
Normal file
31
app/entreprises/activate.py
Normal 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
|
|
@ -338,9 +338,11 @@ def add_entreprise():
|
|||
if form.validate_on_submit():
|
||||
entreprise = Entreprise(
|
||||
nom=form.nom_entreprise.data.strip(),
|
||||
siret=form.siret.data.strip()
|
||||
siret=(
|
||||
form.siret.data.strip()
|
||||
if form.siret.data.strip()
|
||||
else f"{SIRET_PROVISOIRE_START}{datetime.now().strftime('%d%m%y%H%M%S')}", # siret provisoire
|
||||
else f"{SIRET_PROVISOIRE_START}{datetime.now().strftime('%d%m%y%H%M%S')}"
|
||||
), # siret provisoire
|
||||
siret_provisoire=False if form.siret.data.strip() else True,
|
||||
association=form.association.data,
|
||||
adresse=form.adresse.data.strip(),
|
||||
|
@ -352,7 +354,7 @@ def add_entreprise():
|
|||
db.session.add(entreprise)
|
||||
db.session.commit()
|
||||
db.session.refresh(entreprise)
|
||||
except:
|
||||
except Exception:
|
||||
db.session.rollback()
|
||||
flash("Une erreur est survenue veuillez réessayer.")
|
||||
return render_template(
|
||||
|
@ -804,9 +806,9 @@ def add_offre(entreprise_id):
|
|||
missions=form.missions.data.strip(),
|
||||
duree=form.duree.data.strip(),
|
||||
expiration_date=form.expiration_date.data,
|
||||
correspondant_id=form.correspondant.data
|
||||
if form.correspondant.data != ""
|
||||
else None,
|
||||
correspondant_id=(
|
||||
form.correspondant.data if form.correspondant.data != "" else None
|
||||
),
|
||||
)
|
||||
db.session.add(offre)
|
||||
db.session.commit()
|
||||
|
@ -1328,9 +1330,11 @@ def add_contact(entreprise_id):
|
|||
).first_or_404(description=f"entreprise {entreprise_id} inconnue")
|
||||
form = ContactCreationForm(
|
||||
date=f"{datetime.now().strftime('%Y-%m-%dT%H:%M')}",
|
||||
utilisateur=f"{current_user.nom} {current_user.prenom} ({current_user.user_name})"
|
||||
utilisateur=(
|
||||
f"{current_user.nom} {current_user.prenom} ({current_user.user_name})"
|
||||
if current_user.nom and current_user.prenom
|
||||
else "",
|
||||
else ""
|
||||
),
|
||||
)
|
||||
if request.method == "POST" and form.cancel.data:
|
||||
return redirect(url_for("entreprises.contacts", entreprise_id=entreprise_id))
|
||||
|
@ -1496,9 +1500,9 @@ def add_stage_apprentissage(entreprise_id):
|
|||
date_debut=form.date_debut.data,
|
||||
date_fin=form.date_fin.data,
|
||||
formation_text=formation.formsemestre.titre if formation else None,
|
||||
formation_scodoc=formation.formsemestre.formsemestre_id
|
||||
if formation
|
||||
else None,
|
||||
formation_scodoc=(
|
||||
formation.formsemestre.formsemestre_id if formation else None
|
||||
),
|
||||
notes=form.notes.data.strip(),
|
||||
)
|
||||
db.session.add(stage_apprentissage)
|
||||
|
@ -1802,7 +1806,7 @@ def import_donnees():
|
|||
db.session.add(entreprise)
|
||||
db.session.commit()
|
||||
db.session.refresh(entreprise)
|
||||
except:
|
||||
except Exception:
|
||||
db.session.rollback()
|
||||
flash("Une erreur est survenue veuillez réessayer.")
|
||||
return render_template(
|
||||
|
|
|
@ -62,6 +62,11 @@ class AjoutAssiOrJustForm(FlaskForm):
|
|||
if field:
|
||||
field.errors.append(err_msg)
|
||||
|
||||
def disable_all(self):
|
||||
"Disable all fields"
|
||||
for field in self:
|
||||
field.render_kw = {"disabled": True}
|
||||
|
||||
date_debut = StringField(
|
||||
"Date de début",
|
||||
validators=[validators.Length(max=10)],
|
||||
|
@ -175,36 +180,3 @@ class AjoutJustificatifEtudForm(AjoutAssiOrJustForm):
|
|||
validators=[DataRequired(message="This field is required.")],
|
||||
)
|
||||
fichiers = MultipleFileField(label="Ajouter des fichiers")
|
||||
|
||||
|
||||
class ChoixDateForm(FlaskForm):
|
||||
"""
|
||||
Formulaire de choix de date
|
||||
(utilisé par la page de choix de date
|
||||
si la date courante n'est pas dans le semestre)
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"Init form, adding a filed for our error messages"
|
||||
super().__init__(*args, **kwargs)
|
||||
self.ok = True
|
||||
self.error_messages: list[str] = [] # used to report our errors
|
||||
|
||||
def set_error(self, err_msg, field=None):
|
||||
"Set error message both in form and field"
|
||||
self.ok = False
|
||||
self.error_messages.append(err_msg)
|
||||
if field:
|
||||
field.errors.append(err_msg)
|
||||
|
||||
date = StringField(
|
||||
"Date",
|
||||
validators=[validators.Length(max=10)],
|
||||
render_kw={
|
||||
"class": "datepicker",
|
||||
"size": 10,
|
||||
"id": "date",
|
||||
},
|
||||
)
|
||||
submit = SubmitField("Enregistrer")
|
||||
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
||||
|
|
122
app/forms/assiduite/edit_assiduite_etud.py
Normal file
122
app/forms/assiduite/edit_assiduite_etud.py
Normal file
|
@ -0,0 +1,122 @@
|
|||
""" """
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import (
|
||||
StringField,
|
||||
SelectField,
|
||||
RadioField,
|
||||
TextAreaField,
|
||||
validators,
|
||||
SubmitField,
|
||||
)
|
||||
from app.scodoc.sco_utils import EtatAssiduite
|
||||
|
||||
|
||||
class EditAssiForm(FlaskForm):
|
||||
"""
|
||||
Formulaire de modification d'une assiduité
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"Init form, adding a filed for our error messages"
|
||||
super().__init__(*args, **kwargs)
|
||||
self.ok = True
|
||||
self.error_messages: list[str] = [] # used to report our errors
|
||||
|
||||
def set_error(self, err_msg, field=None):
|
||||
"Set error message both in form and field"
|
||||
self.ok = False
|
||||
self.error_messages.append(err_msg)
|
||||
if field:
|
||||
field.errors.append(err_msg)
|
||||
|
||||
def disable_all(self):
|
||||
"Disable all fields"
|
||||
for field in self:
|
||||
field.render_kw = {"disabled": True}
|
||||
|
||||
assi_etat = RadioField(
|
||||
"État:",
|
||||
choices=[
|
||||
(EtatAssiduite.ABSENT.value, EtatAssiduite.ABSENT.version_lisible()),
|
||||
(EtatAssiduite.RETARD.value, EtatAssiduite.RETARD.version_lisible()),
|
||||
(EtatAssiduite.PRESENT.value, EtatAssiduite.PRESENT.version_lisible()),
|
||||
],
|
||||
default="absent",
|
||||
validators=[
|
||||
validators.DataRequired("spécifiez le type d'évènement à signaler"),
|
||||
],
|
||||
)
|
||||
modimpl = SelectField(
|
||||
"Module",
|
||||
choices={}, # will be populated dynamically
|
||||
)
|
||||
description = TextAreaField(
|
||||
"Description",
|
||||
render_kw={
|
||||
"id": "description",
|
||||
"cols": 75,
|
||||
"rows": 4,
|
||||
"maxlength": 500,
|
||||
},
|
||||
)
|
||||
date_debut = StringField(
|
||||
"Date de début",
|
||||
validators=[validators.Length(max=10)],
|
||||
render_kw={
|
||||
"class": "datepicker",
|
||||
"size": 10,
|
||||
"id": "assi_date_debut",
|
||||
},
|
||||
)
|
||||
heure_debut = StringField(
|
||||
"Heure début",
|
||||
default="",
|
||||
validators=[validators.Length(max=5)],
|
||||
render_kw={
|
||||
"class": "timepicker",
|
||||
"size": 5,
|
||||
"id": "assi_heure_debut",
|
||||
},
|
||||
)
|
||||
heure_fin = StringField(
|
||||
"Heure fin",
|
||||
default="",
|
||||
validators=[validators.Length(max=5)],
|
||||
render_kw={
|
||||
"class": "timepicker",
|
||||
"size": 5,
|
||||
"id": "assi_heure_fin",
|
||||
},
|
||||
)
|
||||
date_fin = StringField(
|
||||
"Date de fin",
|
||||
validators=[validators.Length(max=10)],
|
||||
render_kw={
|
||||
"class": "datepicker",
|
||||
"size": 10,
|
||||
"id": "assi_date_fin",
|
||||
},
|
||||
)
|
||||
entry_date = StringField(
|
||||
"Date de dépôt ou saisie",
|
||||
validators=[validators.Length(max=10)],
|
||||
render_kw={
|
||||
"class": "datepicker",
|
||||
"size": 10,
|
||||
"id": "entry_date",
|
||||
},
|
||||
)
|
||||
entry_time = StringField(
|
||||
"Heure dépôt",
|
||||
default="",
|
||||
validators=[validators.Length(max=5)],
|
||||
render_kw={
|
||||
"class": "timepicker",
|
||||
"size": 5,
|
||||
"id": "assi_heure_fin",
|
||||
},
|
||||
)
|
||||
|
||||
submit = SubmitField("Enregistrer")
|
||||
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
17
app/forms/main/activate_entreprises.py
Normal file
17
app/forms/main/activate_entreprises.py
Normal 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})
|
|
@ -54,7 +54,6 @@ class BonusConfigurationForm(FlaskForm):
|
|||
|
||||
class ScoDocConfigurationForm(FlaskForm):
|
||||
"Panneau de configuration avancée"
|
||||
enable_entreprises = BooleanField("activer le module <em>entreprises</em>")
|
||||
disable_passerelle = BooleanField( # disable car par défaut activée
|
||||
"""cacher les fonctions liées à une passerelle de publication des résultats vers les étudiants ("œil"). N'affecte pas l'API, juste la présentation."""
|
||||
)
|
||||
|
@ -127,13 +126,6 @@ def configuration():
|
|||
flash("Fonction bonus inchangée.")
|
||||
return redirect(url_for("scodoc.index"))
|
||||
elif form_scodoc.submit_scodoc.data and form_scodoc.validate():
|
||||
if ScoDocSiteConfig.enable_entreprises(
|
||||
enabled=form_scodoc.data["enable_entreprises"]
|
||||
):
|
||||
flash(
|
||||
"Module entreprise "
|
||||
+ ("activé" if form_scodoc.data["enable_entreprises"] else "désactivé")
|
||||
)
|
||||
if ScoDocSiteConfig.disable_passerelle(
|
||||
disabled=form_scodoc.data["disable_passerelle"]
|
||||
):
|
||||
|
@ -182,6 +174,7 @@ def configuration():
|
|||
|
||||
return render_template(
|
||||
"configuration.j2",
|
||||
is_entreprises_enabled=ScoDocSiteConfig.is_entreprises_enabled(),
|
||||
form_bonus=form_bonus,
|
||||
form_scodoc=form_scodoc,
|
||||
scu=scu,
|
||||
|
|
66
app/forms/main/create_bug_report.py
Normal file
66
app/forms/main/create_bug_report.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# ScoDoc
|
||||
#
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
Formulaire création de ticket de bug
|
||||
"""
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import SubmitField, validators
|
||||
from wtforms.fields.simple import StringField, TextAreaField, BooleanField
|
||||
from app.scodoc import sco_preferences
|
||||
|
||||
|
||||
class CreateBugReport(FlaskForm):
|
||||
"""Formulaire permettant la création d'un ticket de bug"""
|
||||
|
||||
title = StringField(
|
||||
label="Titre du ticket",
|
||||
validators=[
|
||||
validators.DataRequired("titre du ticket requis"),
|
||||
],
|
||||
)
|
||||
message = TextAreaField(
|
||||
label="Message",
|
||||
id="ticket_message",
|
||||
validators=[
|
||||
validators.DataRequired("message du ticket requis"),
|
||||
],
|
||||
)
|
||||
etab = StringField(label="Etablissement")
|
||||
include_dump = BooleanField(
|
||||
"""Inclure une copie anonymisée de la base de données ?
|
||||
Ces données faciliteront le traitement du problème et resteront strictement confidentielles.
|
||||
""",
|
||||
default=False,
|
||||
)
|
||||
submit = SubmitField("Envoyer")
|
||||
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CreateBugReport, self).__init__(*args, **kwargs)
|
||||
self.etab.data = sco_preferences.get_preference("InstituteName") or ""
|
|
@ -353,12 +353,22 @@ class Assiduite(ScoDocModel):
|
|||
|
||||
elif self.external_data is not None and "module" in self.external_data:
|
||||
return (
|
||||
"Tout module"
|
||||
"Autre module (pas dans la liste)"
|
||||
if self.external_data["module"] == "Autre"
|
||||
else self.external_data["module"]
|
||||
)
|
||||
|
||||
return "Non spécifié" if traduire else None
|
||||
return "Module non spécifié" if traduire else None
|
||||
|
||||
def get_moduleimpl_id(self) -> int | str | None:
|
||||
"""
|
||||
Retourne le ModuleImpl associé à l'assiduité
|
||||
"""
|
||||
if self.moduleimpl_id is not None:
|
||||
return self.moduleimpl_id
|
||||
if self.external_data is not None and "module" in self.external_data:
|
||||
return self.external_data["module"]
|
||||
return None
|
||||
|
||||
def get_saisie(self) -> str:
|
||||
"""
|
||||
|
@ -395,6 +405,14 @@ class Assiduite(ScoDocModel):
|
|||
if force:
|
||||
raise ScoValueError("Module non renseigné")
|
||||
|
||||
@classmethod
|
||||
def get_assiduite(cls, assiduite_id: int) -> "Assiduite":
|
||||
"""Assiduité ou 404, cherche uniquement dans le département courant"""
|
||||
query = Assiduite.query.filter_by(id=assiduite_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||
return query.first_or_404()
|
||||
|
||||
|
||||
class Justificatif(ScoDocModel):
|
||||
"""
|
||||
|
@ -685,10 +703,14 @@ def is_period_conflicting(
|
|||
date_fin: datetime,
|
||||
collection: Query,
|
||||
collection_cls: Assiduite | Justificatif,
|
||||
obj_id: int = -1,
|
||||
) -> bool:
|
||||
"""
|
||||
Vérifie si une date n'entre pas en collision
|
||||
avec les justificatifs ou assiduites déjà présentes
|
||||
|
||||
On peut donner un objet_id pour exclure un objet de la vérification
|
||||
(utile pour les modifications)
|
||||
"""
|
||||
|
||||
# On s'assure que les dates soient avec TimeZone
|
||||
|
@ -696,7 +718,9 @@ def is_period_conflicting(
|
|||
date_fin = localize_datetime(date_fin)
|
||||
|
||||
count: int = collection.filter(
|
||||
collection_cls.date_debut < date_fin, collection_cls.date_fin > date_debut
|
||||
collection_cls.date_debut < date_fin,
|
||||
collection_cls.date_fin > date_debut,
|
||||
collection_cls.id != obj_id,
|
||||
).count()
|
||||
|
||||
return count > 0
|
||||
|
|
|
@ -274,6 +274,11 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
|||
return "type_departement mismatch"
|
||||
# Table d'équivalences entre refs:
|
||||
equiv = self._load_config_equivalences()
|
||||
# Même specialité (ou alias) ?
|
||||
if self.specialite != other.specialite and other.specialite not in equiv.get(
|
||||
"alias", []
|
||||
):
|
||||
return "specialite mismatch"
|
||||
# mêmes parcours ?
|
||||
eq_parcours = equiv.get("parcours", {})
|
||||
parcours_by_code_1 = {eq_parcours.get(p.code, p.code): p for p in self.parcours}
|
||||
|
@ -317,6 +322,9 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
|||
def _load_config_equivalences(self) -> dict:
|
||||
"""Load config file ressources/referentiels/equivalences.yaml
|
||||
used to define equivalences between distinct referentiels
|
||||
return a dict, with optional keys:
|
||||
alias: list of equivalent names for speciality (eg SD == STID)
|
||||
parcours: dict with equivalent parcours acronyms
|
||||
"""
|
||||
try:
|
||||
with open(REFCOMP_EQUIVALENCE_FILENAME, encoding="utf-8") as f:
|
||||
|
|
|
@ -199,6 +199,11 @@ class Identite(models.ScoDocModel):
|
|||
@classmethod
|
||||
def get_etud(cls, etudid: int) -> "Identite":
|
||||
"""Etudiant ou 404, cherche uniquement dans le département courant"""
|
||||
if not isinstance(etudid, int):
|
||||
try:
|
||||
etudid = int(etudid)
|
||||
except (TypeError, ValueError):
|
||||
abort(404, "etudid invalide")
|
||||
if g.scodoc_dept:
|
||||
return cls.query.filter_by(
|
||||
id=etudid, dept_id=g.scodoc_dept_id
|
||||
|
@ -299,9 +304,10 @@ class Identite(models.ScoDocModel):
|
|||
|
||||
@property
|
||||
def nomprenom(self, reverse=False) -> str:
|
||||
"""Civilité/nom/prenom pour affichages: "M. Pierre Dupont"
|
||||
"""DEPRECATED
|
||||
Civilité/prénom/nom pour affichages: "M. Pierre Dupont"
|
||||
Si reverse, "Dupont Pierre", sans civilité.
|
||||
Prend l'identité courant et non celle de l'état civile si elles diffèrent.
|
||||
Prend l'identité courante et non celle de l'état civil si elles diffèrent.
|
||||
"""
|
||||
nom = self.nom_usuel or self.nom
|
||||
prenom = self.prenom_str
|
||||
|
@ -309,6 +315,12 @@ class Identite(models.ScoDocModel):
|
|||
return f"{nom} {prenom}".strip()
|
||||
return f"{self.civilite_str} {prenom} {nom}".strip()
|
||||
|
||||
def nom_prenom(self) -> str:
|
||||
"""Civilite NOM Prénom
|
||||
Prend l'identité courante et non celle de l'état civil si elles diffèrent.
|
||||
"""
|
||||
return f"{self.civilite_str} {(self.nom_usuel or self.nom).upper()} {self.prenom_str}"
|
||||
|
||||
@property
|
||||
def prenom_str(self):
|
||||
"""Prénom à afficher. Par exemple: "Jean-Christophe" """
|
||||
|
@ -347,14 +359,15 @@ class Identite(models.ScoDocModel):
|
|||
"Le mail associé à la première adresse de l'étudiant, ou None"
|
||||
return getattr(self.adresses[0], field) if self.adresses.count() > 0 else None
|
||||
|
||||
def get_formsemestres(self) -> list:
|
||||
def get_formsemestres(self, recent_first=True) -> list:
|
||||
"""Liste des formsemestres dans lesquels l'étudiant est (a été) inscrit,
|
||||
triée par date_debut
|
||||
triée par date_debut, le plus récent d'abord (comme "sems" de scodoc7)
|
||||
(si recent_first=False, le plus ancien en tête)
|
||||
"""
|
||||
return sorted(
|
||||
[ins.formsemestre for ins in self.formsemestre_inscriptions],
|
||||
key=attrgetter("date_debut"),
|
||||
reverse=True,
|
||||
reverse=recent_first,
|
||||
)
|
||||
|
||||
def get_modimpls_by_formsemestre(
|
||||
|
@ -393,6 +406,18 @@ class Identite(models.ScoDocModel):
|
|||
modimpls_by_formsemestre[formsemestre.id] = modimpls_sem
|
||||
return modimpls_by_formsemestre
|
||||
|
||||
def get_modimpls_from_formsemestre(
|
||||
self, formsemestre: "FormSemestre"
|
||||
) -> list["ModuleImpl"]:
|
||||
"""
|
||||
Liste des ModuleImpl auxquels l'étudiant est inscrit dans le formsemestre.
|
||||
"""
|
||||
modimpls = ModuleImpl.query.join(ModuleImplInscription).filter(
|
||||
ModuleImplInscription.etudid == self.id,
|
||||
ModuleImpl.formsemestre_id == formsemestre.id,
|
||||
)
|
||||
return modimpls.all()
|
||||
|
||||
@classmethod
|
||||
def convert_dict_fields(cls, args: dict) -> dict:
|
||||
"""Convert fields in the given dict. No other side effect.
|
||||
|
@ -551,7 +576,7 @@ class Identite(models.ScoDocModel):
|
|||
.all()
|
||||
)
|
||||
|
||||
def inscription_courante(self):
|
||||
def inscription_courante(self) -> "FormSemestreInscription | None":
|
||||
"""La première inscription à un formsemestre _actuellement_ en cours.
|
||||
None s'il n'y en a pas (ou plus, ou pas encore).
|
||||
"""
|
||||
|
|
|
@ -71,6 +71,15 @@ class Evaluation(models.ScoDocModel):
|
|||
EVALUATION_BONUS,
|
||||
}
|
||||
|
||||
def type_abbrev(self) -> str:
|
||||
"Le nom abrégé du type de cette éval."
|
||||
return {
|
||||
self.EVALUATION_NORMALE: "std",
|
||||
self.EVALUATION_RATTRAPAGE: "rattrapage",
|
||||
self.EVALUATION_SESSION2: "session 2",
|
||||
self.EVALUATION_BONUS: "bonus",
|
||||
}.get(self.evaluation_type, "?")
|
||||
|
||||
def __repr__(self):
|
||||
return f"""<Evaluation {self.id} {
|
||||
self.date_debut.isoformat() if self.date_debut else ''} "{
|
||||
|
@ -417,12 +426,13 @@ class Evaluation(models.ScoDocModel):
|
|||
return modified
|
||||
|
||||
def set_ue_poids(self, ue, poids: float) -> None:
|
||||
"""Set poids évaluation vers cette UE"""
|
||||
"""Set poids évaluation vers cette UE. Commit."""
|
||||
self.update_ue_poids_dict({ue.id: poids})
|
||||
|
||||
def set_ue_poids_dict(self, ue_poids_dict: dict) -> None:
|
||||
"""set poids vers les UE (remplace existants)
|
||||
ue_poids_dict = { ue_id : poids }
|
||||
Commit session.
|
||||
"""
|
||||
from app.models.ues import UniteEns
|
||||
|
||||
|
@ -432,9 +442,12 @@ class Evaluation(models.ScoDocModel):
|
|||
if ue is None:
|
||||
raise ScoValueError("poids vers une UE inexistante")
|
||||
ue_poids = EvaluationUEPoids(evaluation=self, ue=ue, poids=poids)
|
||||
L.append(ue_poids)
|
||||
db.session.add(ue_poids)
|
||||
L.append(ue_poids)
|
||||
|
||||
self.ue_poids = L # backref # pylint:disable=attribute-defined-outside-init
|
||||
|
||||
db.session.commit()
|
||||
self.moduleimpl.invalidate_evaluations_poids() # inval cache
|
||||
|
||||
def update_ue_poids_dict(self, ue_poids_dict: dict) -> None:
|
||||
|
|
|
@ -232,7 +232,9 @@ class ScolarNews(db.Model):
|
|||
)
|
||||
|
||||
# Transforme les URL en URL absolues
|
||||
base = scu.ScoURL()
|
||||
base = url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)[
|
||||
: -len("/index_html")
|
||||
]
|
||||
txt = re.sub('href=/.*?"', 'href="' + base + "/", txt)
|
||||
|
||||
# Transforme les liens HTML en texte brut: '<a href="url">texte</a>' devient 'texte: url'
|
||||
|
|
|
@ -6,6 +6,7 @@ from flask import abort, g
|
|||
from flask_login import current_user
|
||||
from flask_sqlalchemy.query import Query
|
||||
|
||||
import app
|
||||
from app import db
|
||||
from app.auth.models import User
|
||||
from app.comp import df_cache
|
||||
|
@ -78,7 +79,9 @@ class ModuleImpl(ScoDocModel):
|
|||
] or self.module.get_edt_ids()
|
||||
|
||||
def get_evaluations_poids(self) -> pd.DataFrame:
|
||||
"""Les poids des évaluations vers les UE (accès via cache)"""
|
||||
"""Les poids des évaluations vers les UEs (accès via cache redis).
|
||||
Toutes les évaluations sont considérées (normales, bonus, rattr., etc.)
|
||||
"""
|
||||
evaluations_poids = df_cache.EvaluationsPoidsCache.get(self.id)
|
||||
if evaluations_poids is None:
|
||||
from app.comp import moy_mod
|
||||
|
@ -108,20 +111,37 @@ class ModuleImpl(ScoDocModel):
|
|||
"""Invalide poids cachés"""
|
||||
df_cache.EvaluationsPoidsCache.delete(self.id)
|
||||
|
||||
def check_apc_conformity(self, res: "ResultatsSemestreBUT") -> bool:
|
||||
"""true si les poids des évaluations du module permettent de satisfaire
|
||||
les coefficients du PN.
|
||||
def check_apc_conformity(
|
||||
self, res: "ResultatsSemestreBUT", evaluation_type=Evaluation.EVALUATION_NORMALE
|
||||
) -> bool:
|
||||
"""true si les poids des évaluations du type indiqué (normales par défaut)
|
||||
du module permettent de satisfaire les coefficients du PN.
|
||||
"""
|
||||
# appelé par formsemestre_status, liste notes, et moduleimpl_status
|
||||
if not self.module.formation.get_cursus().APC_SAE or (
|
||||
self.module.module_type != scu.ModuleType.RESSOURCE
|
||||
and self.module.module_type != scu.ModuleType.SAE
|
||||
self.module.module_type
|
||||
not in {scu.ModuleType.RESSOURCE, scu.ModuleType.SAE}
|
||||
):
|
||||
return True # Non BUT, toujours conforme
|
||||
from app.comp import moy_mod
|
||||
|
||||
mod_results = res.modimpls_results.get(self.id)
|
||||
if mod_results is None:
|
||||
app.critical_error("check_apc_conformity: err 1")
|
||||
|
||||
selected_evaluations_ids = [
|
||||
eval_id
|
||||
for eval_id, eval_type in mod_results.evals_type.items()
|
||||
if eval_type == evaluation_type
|
||||
]
|
||||
if not selected_evaluations_ids:
|
||||
return True # conforme si pas d'évaluations
|
||||
selected_evaluations_poids = self.get_evaluations_poids().loc[
|
||||
selected_evaluations_ids
|
||||
]
|
||||
return moy_mod.moduleimpl_is_conforme(
|
||||
self,
|
||||
self.get_evaluations_poids(),
|
||||
selected_evaluations_poids,
|
||||
res.modimpl_coefs_df,
|
||||
)
|
||||
|
||||
|
|
|
@ -340,6 +340,21 @@ class Module(models.ScoDocModel):
|
|||
# Liste seulement les coefs définis:
|
||||
return [(c.ue, c.coef) for c in self.get_ue_coefs_sorted()]
|
||||
|
||||
def get_ue_coefs_descr(self) -> str:
|
||||
"""Description des coefficients vers les UEs (APC)"""
|
||||
coefs_descr = ", ".join(
|
||||
[
|
||||
f"{ue.acronyme}: {co}"
|
||||
for ue, co in self.ue_coefs_list()
|
||||
if isinstance(co, float) and co > 0
|
||||
]
|
||||
)
|
||||
if coefs_descr:
|
||||
descr = "Coefs: " + coefs_descr
|
||||
else:
|
||||
descr = "(pas de coefficients) "
|
||||
return descr
|
||||
|
||||
def get_codes_apogee(self) -> set[str]:
|
||||
"""Les codes Apogée (codés en base comme "VRT1,VRT2")"""
|
||||
if self.code_apogee:
|
||||
|
|
|
@ -46,6 +46,8 @@ class UniteEns(models.ScoDocModel):
|
|||
# coef UE, utilise seulement si l'option use_ue_coefs est activée:
|
||||
coefficient = db.Column(db.Float)
|
||||
|
||||
# id de l'élément Apogée du RCUE (utilisé pour les UEs de sem. pair du BUT)
|
||||
code_apogee_rcue = db.Column(db.String(APO_CODE_STR_LEN))
|
||||
# coef. pour le calcul de moyennes de RCUE. Par défaut, 1.
|
||||
coef_rcue = db.Column(db.Float, nullable=False, default=1.0, server_default="1.0")
|
||||
|
||||
|
@ -409,6 +411,14 @@ class UniteEns(models.ScoDocModel):
|
|||
Renvoie (True, "") si ok, sinon (False, error_message)
|
||||
"""
|
||||
msg = ""
|
||||
# Safety check
|
||||
if self.formation.referentiel_competence is None:
|
||||
return False, "pas de référentiel de compétence"
|
||||
# Si tous les parcours, aucun (tronc commun)
|
||||
if {p.id for p in parcours} == {
|
||||
p.id for p in self.formation.referentiel_competence.parcours
|
||||
}:
|
||||
parcours = []
|
||||
# Le niveau est-il dans tous ces parcours ? Sinon, l'enlève
|
||||
prev_niveau = self.niveau_competence
|
||||
if (
|
||||
|
@ -424,6 +434,7 @@ class UniteEns(models.ScoDocModel):
|
|||
self.niveau_competence, parcours
|
||||
)
|
||||
if not ok:
|
||||
self.formation.invalidate_cached_sems()
|
||||
self.niveau_competence = prev_niveau # restore
|
||||
return False, error_message
|
||||
|
||||
|
|
|
@ -48,6 +48,7 @@ from typing import Any
|
|||
from urllib.parse import urlparse, urlencode, parse_qs, urlunparse
|
||||
|
||||
from openpyxl.utils import get_column_letter
|
||||
import reportlab
|
||||
from reportlab.platypus import Paragraph, Spacer
|
||||
from reportlab.platypus import Table, KeepInFrame
|
||||
from reportlab.lib.colors import Color
|
||||
|
@ -175,6 +176,7 @@ class GenTable:
|
|||
self.xml_link = xml_link
|
||||
# HTML parameters:
|
||||
if not table_id: # random id
|
||||
log("Warning: GenTable() called without table_id")
|
||||
self.table_id = "gt_" + str(random.randint(0, 1000000))
|
||||
else:
|
||||
self.table_id = table_id
|
||||
|
@ -812,7 +814,10 @@ if __name__ == "__main__":
|
|||
document,
|
||||
)
|
||||
)
|
||||
try:
|
||||
document.build(objects)
|
||||
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
|
||||
raise ScoPDFFormatError(str(exc)) from exc
|
||||
data = doc.getvalue()
|
||||
with open("/tmp/gen_table.pdf", "wb") as f:
|
||||
f.write(data)
|
||||
|
|
|
@ -25,12 +25,11 @@
|
|||
#
|
||||
##############################################################################
|
||||
|
||||
"""HTML Header/Footer for ScoDoc pages
|
||||
"""
|
||||
"""HTML Header/Footer for ScoDoc pages"""
|
||||
|
||||
import html
|
||||
|
||||
from flask import g, render_template
|
||||
from flask import g, render_template, url_for
|
||||
from flask import request
|
||||
from flask_login import current_user
|
||||
|
||||
|
@ -101,7 +100,7 @@ _HTML_BEGIN = f"""<!DOCTYPE html>
|
|||
<script src="{scu.STATIC_DIR}/libjs/menu.js"></script>
|
||||
<script src="{scu.STATIC_DIR}/libjs/bubble.js"></script>
|
||||
<script>
|
||||
window.onload=function(){{enableTooltips("gtrcontent")}};
|
||||
window.onload=function(){{enableTooltips("gtrcontent"); enableTooltips("sidebar");}};
|
||||
</script>
|
||||
|
||||
<script src="{scu.STATIC_DIR}/jQuery/jquery.js"></script>
|
||||
|
@ -163,7 +162,7 @@ def sco_header(
|
|||
params = {
|
||||
"page_title": page_title or sco_version.SCONAME,
|
||||
"no_side_bar": no_side_bar,
|
||||
"ScoURL": scu.ScoURL(),
|
||||
"ScoURL": url_for("scolar.index_html", scodoc_dept=g.scodoc_dept),
|
||||
"encoding": scu.SCO_ENCODING,
|
||||
"titrebandeau_mkup": "<td>" + titrebandeau + "</td>",
|
||||
"authuser": current_user.user_name,
|
||||
|
@ -179,6 +178,7 @@ def sco_header(
|
|||
|
||||
H = [
|
||||
"""<!DOCTYPE html><html lang="fr">
|
||||
<!-- ScoDoc legacy -->
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>%(page_title)s</title>
|
||||
|
@ -217,9 +217,9 @@ def sco_header(
|
|||
<script src="{scu.STATIC_DIR}/libjs/menu.js"></script>
|
||||
<script src="{scu.STATIC_DIR}/libjs/bubble.js"></script>
|
||||
<script>
|
||||
window.onload=function(){{enableTooltips("gtrcontent")}};
|
||||
window.onload=function(){{enableTooltips("gtrcontent"); enableTooltips("sidebar");}};
|
||||
|
||||
const SCO_URL="{scu.ScoURL()}";
|
||||
const SCO_URL="{url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)}";
|
||||
const SCO_TIMEZONE="{scu.TIME_ZONE}";
|
||||
</script>"""
|
||||
)
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
"""
|
||||
Génération de la "sidebar" (marge gauche des pages HTML)
|
||||
"""
|
||||
|
||||
from flask import render_template, url_for
|
||||
from flask import g, request
|
||||
from flask_login import current_user
|
||||
|
@ -108,19 +109,27 @@ def sidebar_common():
|
|||
</div>
|
||||
{sidebar_dept()}
|
||||
<h2 class="insidebar">Scolarité</h2>
|
||||
<a href="{scu.ScoURL()}" class="sidebar">Semestres</a> <br>
|
||||
<a href="{scu.NotesURL()}" class="sidebar">Formations</a> <br>
|
||||
<a href="{
|
||||
url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
|
||||
}" class="sidebar">Semestres</a> <br>
|
||||
<a href="{
|
||||
url_for("notes.index_html", scodoc_dept=g.scodoc_dept)
|
||||
}" class="sidebar">Formations</a> <br>
|
||||
"""
|
||||
]
|
||||
if current_user.has_permission(Permission.AbsChange):
|
||||
H.append(
|
||||
f""" <a href="{scu.AssiduitesURL()}" class="sidebar">Assiduité</a> <br> """
|
||||
f""" <a href="{
|
||||
url_for("assiduites.bilan_dept", scodoc_dept=g.scodoc_dept)
|
||||
}" class="sidebar">Assiduité</a> <br> """
|
||||
)
|
||||
if current_user.has_permission(
|
||||
Permission.UsersAdmin
|
||||
) or current_user.has_permission(Permission.UsersView):
|
||||
H.append(
|
||||
f"""<a href="{scu.UsersURL()}" class="sidebar">Utilisateurs</a> <br>"""
|
||||
f"""<a href="{
|
||||
url_for("users.index_html", scodoc_dept=g.scodoc_dept)
|
||||
}" class="sidebar">Utilisateurs</a> <br>"""
|
||||
)
|
||||
|
||||
if current_user.has_permission(Permission.EditPreferences):
|
||||
|
@ -141,7 +150,9 @@ def sidebar(etudid: int = None):
|
|||
params = {}
|
||||
|
||||
H = [
|
||||
f"""<div class="sidebar">
|
||||
f"""
|
||||
<!-- sidebar py -->
|
||||
<div class="sidebar" id="sidebar">
|
||||
{ sidebar_common() }
|
||||
<div class="box-chercheetud">Chercher étudiant:<br>
|
||||
<form method="get" id="form-chercheetud"
|
||||
|
@ -183,7 +194,7 @@ def sidebar(etudid: int = None):
|
|||
formsemestre.date_debut.strftime(scu.DATE_FMT)
|
||||
} au {
|
||||
formsemestre.date_fin.strftime(scu.DATE_FMT)
|
||||
}">({
|
||||
}" data-tooltip>({
|
||||
sco_preferences.get_preference("assi_metrique", None)})
|
||||
<br>{nbabsjust:1g} J., {nbabsnj:1g} N.J.</span>"""
|
||||
)
|
||||
|
@ -217,12 +228,9 @@ def sidebar(etudid: int = None):
|
|||
<li><a href="{ url_for('assiduites.calendrier_assi_etud',
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">Calendrier</a></li>
|
||||
<li><a href="{ url_for('assiduites.liste_assiduites_etud',
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">Liste</a></li>
|
||||
<li><a href="{ url_for('assiduites.bilan_etud',
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">Bilan</a></li>
|
||||
}" title="Les pages bilan et liste ont été fusionnées">Liste/Bilan</a></li>
|
||||
</ul>
|
||||
"""
|
||||
)
|
||||
|
|
|
@ -157,5 +157,6 @@ def table_billets(
|
|||
rows=rows,
|
||||
html_sortable=True,
|
||||
html_class="table_leftalign",
|
||||
table_id="table_billets",
|
||||
)
|
||||
return tab
|
||||
|
|
|
@ -288,6 +288,7 @@ def apo_table_compare_etud_results(A, B):
|
|||
html_class="table_leftalign",
|
||||
html_with_td_classes=True,
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
table_id="apo_table_compare_etud_results",
|
||||
)
|
||||
return T
|
||||
|
||||
|
|
|
@ -48,9 +48,9 @@ import numpy as np
|
|||
|
||||
|
||||
from app import log
|
||||
from app.but import jury_but
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.models import (
|
||||
ApcValidationAnnee,
|
||||
ApcValidationRCUE,
|
||||
|
@ -132,7 +132,7 @@ class ApoEtud(dict):
|
|||
"Vrai si BUT"
|
||||
self.col_elts = {}
|
||||
"{'V1RT': {'R': 'ADM', 'J': '', 'B': 20, 'N': '12.14'}}"
|
||||
self.etud: Identite = None
|
||||
self.etud: Identite | None = None
|
||||
"etudiant ScoDoc associé"
|
||||
self.etat = None # ETUD_OK, ...
|
||||
self.is_nar = False
|
||||
|
@ -171,24 +171,18 @@ class ApoEtud(dict):
|
|||
Sinon, cherche le semestre, et met l'état à ETUD_OK ou ETUD_NON_INSCRIT.
|
||||
"""
|
||||
|
||||
# futur: #WIP
|
||||
# etud: Identite = Identite.query.filter_by(code_nip=self["nip"], dept_id=g.scodoc_dept_id).first()
|
||||
# self.etud = etud
|
||||
etuds = sco_etud.get_etud_info(code_nip=self["nip"], filled=True)
|
||||
if not etuds:
|
||||
self.etud = Identite.query.filter_by(
|
||||
code_nip=self["nip"], dept_id=g.scodoc_dept_id
|
||||
).first()
|
||||
if not self.etud:
|
||||
# pas dans ScoDoc
|
||||
self.etud = None
|
||||
self.log.append("non inscrit dans ScoDoc")
|
||||
self.etat = ETUD_ORPHELIN
|
||||
else:
|
||||
# futur: #WIP
|
||||
# formsemestre_ids = {
|
||||
# ins.formsemestre_id for ins in etud.formsemestre_inscriptions
|
||||
# }
|
||||
# in_formsemestre_ids = formsemestre_ids.intersection(etape_formsemestre_ids)
|
||||
self.etud = etuds[0]
|
||||
# cherche le semestre ScoDoc correspondant à l'un de ceux de l'etape:
|
||||
formsemestre_ids = {s["formsemestre_id"] for s in self.etud["sems"]}
|
||||
formsemestre_ids = {
|
||||
ins.formsemestre_id for ins in self.etud.formsemestre_inscriptions
|
||||
}
|
||||
in_formsemestre_ids = formsemestre_ids.intersection(etape_formsemestre_ids)
|
||||
if not in_formsemestre_ids:
|
||||
self.log.append(
|
||||
|
@ -267,13 +261,17 @@ class ApoEtud(dict):
|
|||
Args:
|
||||
code (str): code apo de l'element cherché
|
||||
sem (dict): semestre dans lequel on cherche l'élément
|
||||
|
||||
Utilise notamment:
|
||||
cur_sem (dict): semestre "courant" pour résultats annuels (VET)
|
||||
autre_sem (dict): autre semestre utilisé pour calculer les résultats annuels (VET)
|
||||
|
||||
Returns:
|
||||
dict: with N, B, J, R keys, ou None si elt non trouvé
|
||||
"""
|
||||
etudid = self.etud["etudid"]
|
||||
if not self.etud:
|
||||
return None
|
||||
etudid = self.etud.id
|
||||
if not self.cur_res:
|
||||
log("search_elt_in_sem: no cur_res !")
|
||||
return None
|
||||
|
@ -377,6 +375,8 @@ class ApoEtud(dict):
|
|||
|
||||
if module_code_found:
|
||||
return VOID_APO_RES
|
||||
# RCUE du BUT
|
||||
deca = jury_but.DecisionsProposeesAnnee(self.etud, formsemestre)
|
||||
#
|
||||
return None # element Apogee non trouvé dans ce semestre
|
||||
|
||||
|
@ -917,6 +917,7 @@ class ApoData:
|
|||
columns_ids=columns_ids,
|
||||
titles=dict(zip(columns_ids, columns_ids)),
|
||||
rows=rows,
|
||||
table_id="build_cr_table",
|
||||
xls_sheet_name="Decisions ScoDoc",
|
||||
)
|
||||
return T
|
||||
|
@ -969,6 +970,7 @@ class ApoData:
|
|||
"rcue": "RCUE",
|
||||
},
|
||||
rows=rows,
|
||||
table_id="adsup_table",
|
||||
xls_sheet_name="ADSUPs",
|
||||
)
|
||||
|
||||
|
@ -1054,6 +1056,7 @@ def nar_etuds_table(apo_data, nar_etuds):
|
|||
columns_ids=columns_ids,
|
||||
titles=dict(zip(columns_ids, columns_ids)),
|
||||
rows=rows,
|
||||
table_id="nar_etuds_table",
|
||||
xls_sheet_name="NAR ScoDoc",
|
||||
)
|
||||
return table.excel()
|
||||
|
|
|
@ -49,11 +49,13 @@
|
|||
"""
|
||||
import datetime
|
||||
import glob
|
||||
import gzip
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import time
|
||||
import zlib
|
||||
|
||||
import chardet
|
||||
|
||||
|
@ -241,11 +243,13 @@ class BaseArchiver:
|
|||
filename: str,
|
||||
data: str | bytes,
|
||||
dept_id: int = None,
|
||||
compress=False,
|
||||
):
|
||||
"""Store data in archive, under given filename.
|
||||
Filename may be modified (sanitized): return used filename
|
||||
The file is created or replaced.
|
||||
data may be str or bytes
|
||||
If compress, data is gziped and filename suffix ".gz" added.
|
||||
"""
|
||||
if isinstance(data, str):
|
||||
data = data.encode(scu.SCO_ENCODING)
|
||||
|
@ -255,6 +259,12 @@ class BaseArchiver:
|
|||
try:
|
||||
scu.GSL.acquire()
|
||||
fname = os.path.join(archive_id, filename)
|
||||
if compress:
|
||||
if not fname.endswith(".gz"):
|
||||
fname += ".gz"
|
||||
with gzip.open(fname, "wb") as f:
|
||||
f.write(data)
|
||||
else:
|
||||
with open(fname, "wb") as f:
|
||||
f.write(data)
|
||||
except FileNotFoundError as exc:
|
||||
|
@ -274,6 +284,15 @@ class BaseArchiver:
|
|||
fname = os.path.join(archive_id, filename)
|
||||
log(f"reading archive file {fname}")
|
||||
try:
|
||||
if fname.endswith(".gz"):
|
||||
try:
|
||||
with gzip.open(fname) as f:
|
||||
data = f.read()
|
||||
except (OSError, EOFError, zlib.error) as exc:
|
||||
raise ScoValueError(
|
||||
f"Erreur lecture archive ({fname} invalide)"
|
||||
) from exc
|
||||
else:
|
||||
with open(fname, "rb") as f:
|
||||
data = f.read()
|
||||
except FileNotFoundError as exc:
|
||||
|
@ -288,6 +307,8 @@ class BaseArchiver:
|
|||
"""
|
||||
archive_id = self.get_id_from_name(oid, archive_name, dept_id=dept_id)
|
||||
data = self.get(archive_id, filename)
|
||||
if filename.endswith(".gz"):
|
||||
filename = filename[:-3]
|
||||
mime = mimetypes.guess_type(filename)[0]
|
||||
if mime is None:
|
||||
mime = "application/octet-stream"
|
||||
|
|
|
@ -68,7 +68,7 @@ PV_ARCHIVER = SemsArchiver()
|
|||
|
||||
|
||||
def do_formsemestre_archive(
|
||||
formsemestre_id,
|
||||
formsemestre: FormSemestre,
|
||||
group_ids: list[int] = None, # si indiqué, ne prend que ces groupes
|
||||
description="",
|
||||
date_jury="",
|
||||
|
@ -92,9 +92,8 @@ def do_formsemestre_archive(
|
|||
raise ScoValueError(
|
||||
"do_formsemestre_archive: version de bulletin demandée invalide"
|
||||
)
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
sem_archive_id = formsemestre_id
|
||||
sem_archive_id = formsemestre.id
|
||||
archive_id = PV_ARCHIVER.create_obj_archive(
|
||||
sem_archive_id, description, formsemestre.dept_id
|
||||
)
|
||||
|
@ -102,9 +101,9 @@ def do_formsemestre_archive(
|
|||
|
||||
if not group_ids:
|
||||
# tous les inscrits du semestre
|
||||
group_ids = [sco_groups.get_default_group(formsemestre_id)]
|
||||
group_ids = [sco_groups.get_default_group(formsemestre.id)]
|
||||
groups_infos = sco_groups_view.DisplayedGroupsInfos(
|
||||
group_ids, formsemestre_id=formsemestre_id
|
||||
group_ids, formsemestre_id=formsemestre.id
|
||||
)
|
||||
groups_filename = "-" + groups_infos.groups_filename
|
||||
etudids = [m["etudid"] for m in groups_infos.members]
|
||||
|
@ -142,19 +141,23 @@ def do_formsemestre_archive(
|
|||
)
|
||||
|
||||
# Bulletins en JSON
|
||||
data = gen_formsemestre_recapcomplet_json(formsemestre_id, xml_with_decisions=True)
|
||||
data = gen_formsemestre_recapcomplet_json(formsemestre.id, xml_with_decisions=True)
|
||||
data_js = json.dumps(data, indent=1, cls=ScoDocJSONEncoder)
|
||||
if data:
|
||||
PV_ARCHIVER.store(
|
||||
archive_id, "Bulletins.json", data_js, dept_id=formsemestre.dept_id
|
||||
archive_id,
|
||||
"Bulletins.json",
|
||||
data_js,
|
||||
dept_id=formsemestre.dept_id,
|
||||
compress=True,
|
||||
)
|
||||
# Décisions de jury, en XLS
|
||||
if formsemestre.formation.is_apc():
|
||||
response = jury_but_pv.pvjury_page_but(formsemestre_id, fmt="xls")
|
||||
response = jury_but_pv.pvjury_page_but(formsemestre.id, fmt="xls")
|
||||
data = response.get_data()
|
||||
else: # formations classiques
|
||||
data = sco_pv_forms.formsemestre_pvjury(
|
||||
formsemestre_id, fmt="xls", publish=False
|
||||
formsemestre.id, fmt="xls", publish=False
|
||||
)
|
||||
if data:
|
||||
PV_ARCHIVER.store(
|
||||
|
@ -165,7 +168,7 @@ def do_formsemestre_archive(
|
|||
)
|
||||
# Classeur bulletins (PDF)
|
||||
data, _ = sco_bulletins_pdf.get_formsemestre_bulletins_pdf(
|
||||
formsemestre_id, version=bul_version
|
||||
formsemestre.id, version=bul_version
|
||||
)
|
||||
if data:
|
||||
PV_ARCHIVER.store(
|
||||
|
@ -173,10 +176,11 @@ def do_formsemestre_archive(
|
|||
"Bulletins.pdf",
|
||||
data,
|
||||
dept_id=formsemestre.dept_id,
|
||||
compress=True,
|
||||
)
|
||||
# Lettres individuelles (PDF):
|
||||
data = sco_pv_lettres_inviduelles.pdf_lettres_individuelles(
|
||||
formsemestre_id,
|
||||
formsemestre.id,
|
||||
etudids=etudids,
|
||||
date_jury=date_jury,
|
||||
date_commission=date_commission,
|
||||
|
@ -217,7 +221,7 @@ def formsemestre_archive(formsemestre_id, group_ids: list[int] = None):
|
|||
"""Make and store new archive for this formsemestre.
|
||||
(all students or only selected groups)
|
||||
"""
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
if not formsemestre.can_edit_pv():
|
||||
raise ScoPermissionDenied(
|
||||
dest_url=url_for(
|
||||
|
@ -320,7 +324,7 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement.
|
|||
else:
|
||||
tf[2]["anonymous"] = False
|
||||
do_formsemestre_archive(
|
||||
formsemestre_id,
|
||||
formsemestre,
|
||||
group_ids=group_ids,
|
||||
description=tf[2]["description"],
|
||||
date_jury=tf[2]["date_jury"],
|
||||
|
@ -352,7 +356,7 @@ def formsemestre_list_archives(formsemestre_id):
|
|||
"""Page listing archives"""
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
sem_archive_id = formsemestre_id
|
||||
L = []
|
||||
archives_descr = []
|
||||
for archive_id in PV_ARCHIVER.list_obj_archives(
|
||||
sem_archive_id, dept_id=formsemestre.dept_id
|
||||
):
|
||||
|
@ -366,28 +370,30 @@ def formsemestre_list_archives(formsemestre_id):
|
|||
archive_id, dept_id=formsemestre.dept_id
|
||||
),
|
||||
}
|
||||
L.append(a)
|
||||
archives_descr.append(a)
|
||||
|
||||
H = [html_sco_header.html_sem_header("Archive des PV et résultats ")]
|
||||
if not L:
|
||||
if not archives_descr:
|
||||
H.append("<p>aucune archive enregistrée</p>")
|
||||
else:
|
||||
H.append("<ul>")
|
||||
for a in L:
|
||||
for a in archives_descr:
|
||||
archive_name = PV_ARCHIVER.get_archive_name(a["archive_id"])
|
||||
H.append(
|
||||
'<li>%s : <em>%s</em> (<a href="formsemestre_delete_archive?formsemestre_id=%s&archive_name=%s">supprimer</a>)<ul>'
|
||||
% (
|
||||
a["date"].strftime("%d/%m/%Y %H:%M"),
|
||||
a["description"],
|
||||
formsemestre_id,
|
||||
archive_name,
|
||||
)
|
||||
f"""<li>{a["date"].strftime("%d/%m/%Y %H:%M")} : <em>{a["description"]}</em>
|
||||
(<a href="{ url_for( "notes.formsemestre_delete_archive", scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id, archive_name=archive_name
|
||||
)}">supprimer</a>)
|
||||
<ul>"""
|
||||
)
|
||||
for filename in a["content"]:
|
||||
H.append(
|
||||
'<li><a href="formsemestre_get_archived_file?formsemestre_id=%s&archive_name=%s&filename=%s">%s</a></li>'
|
||||
% (formsemestre_id, archive_name, filename, filename)
|
||||
f"""<li><a href="{
|
||||
url_for( "notes.formsemestre_get_archived_file", scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
archive_name=archive_name,
|
||||
filename=filename
|
||||
)}">{filename[:-3] if filename.endswith(".gz") else filename}</a></li>"""
|
||||
)
|
||||
if not a["content"]:
|
||||
H.append("<li><em>aucun fichier !</em></li>")
|
||||
|
@ -399,7 +405,7 @@ def formsemestre_list_archives(formsemestre_id):
|
|||
|
||||
def formsemestre_get_archived_file(formsemestre_id, archive_name, filename):
|
||||
"""Send file to client."""
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
sem_archive_id = formsemestre.id
|
||||
return PV_ARCHIVER.get_archived_file(
|
||||
sem_archive_id, archive_name, filename, dept_id=formsemestre.dept_id
|
||||
|
|
|
@ -751,7 +751,7 @@ def formsemestre_get_assiduites_count(
|
|||
) -> tuple[int, int, int]:
|
||||
"""Les comptes d'absences de cet étudiant dans ce semestre:
|
||||
tuple (nb abs non justifiées, nb abs justifiées, nb abs total)
|
||||
Utilise un cache.
|
||||
Utilise un cache (si moduleimpl_id n'est pas spécifié).
|
||||
"""
|
||||
metrique = sco_preferences.get_preference("assi_metrique", formsemestre.id)
|
||||
return get_assiduites_count_in_interval(
|
||||
|
@ -779,7 +779,7 @@ def get_assiduites_count_in_interval(
|
|||
"""Les comptes d'absences de cet étudiant entre ces deux dates, incluses:
|
||||
tuple (nb abs non justifiées, nb abs justifiées, nb abs total)
|
||||
On peut spécifier les dates comme datetime ou iso.
|
||||
Utilise un cache.
|
||||
Utilise un cache (si moduleimpl_id n'est pas spécifié).
|
||||
"""
|
||||
date_debut_iso = date_debut_iso or date_debut.strftime("%Y-%m-%d")
|
||||
date_fin_iso = date_fin_iso or date_fin.strftime("%Y-%m-%d")
|
||||
|
|
102
app/scodoc/sco_bug_report.py
Normal file
102
app/scodoc/sco_bug_report.py
Normal file
|
@ -0,0 +1,102 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Rapport de bug ScoDoc
|
||||
|
||||
Permet de créer un rapport de bug (ticket) sur la plateforme git scodoc.org.
|
||||
|
||||
Le principe est le suivant:
|
||||
1- Si l'utilisateur le demande, on dump la base de données et on l'envoie
|
||||
|
||||
2- ScoDoc envoie une requête POST à scodoc.org pour qu'un ticket git soit créé avec les
|
||||
informations fournies par l'utilisateur + quelques métadonnées.
|
||||
|
||||
"""
|
||||
from flask import g
|
||||
from flask_login import current_user
|
||||
import requests
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
import sco_version
|
||||
|
||||
from app import log
|
||||
from app.scodoc.sco_dump_db import sco_dump_and_send_db
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
|
||||
def sco_bug_report(
|
||||
title: str = "", message: str = "", etab: str = "", include_dump: bool = False
|
||||
) -> requests.Response:
|
||||
"""Envoi d'un bug report (ticket)"""
|
||||
dump_id = None
|
||||
|
||||
if include_dump:
|
||||
dump = sco_dump_and_send_db()
|
||||
|
||||
try:
|
||||
dump_id = dump.json()["dump_id"]
|
||||
except (requests.exceptions.JSONDecodeError, KeyError):
|
||||
dump_id = "inconnu (erreur)"
|
||||
|
||||
log(f"sco_bug_report: {scu.SCO_BUG_REPORT_URL} by {current_user.user_name}")
|
||||
try:
|
||||
r = requests.post(
|
||||
scu.SCO_BUG_REPORT_URL,
|
||||
json={
|
||||
"ticket": {
|
||||
"title": title,
|
||||
"message": message,
|
||||
"etab": etab,
|
||||
"dept": getattr(g, "scodoc_dept", "-"),
|
||||
},
|
||||
"user": {
|
||||
"name": current_user.get_nomcomplet(),
|
||||
"email": current_user.email,
|
||||
},
|
||||
"dump": {
|
||||
"included": include_dump,
|
||||
"id": dump_id,
|
||||
},
|
||||
"scodoc": {
|
||||
"version": sco_version.SCOVERSION,
|
||||
},
|
||||
},
|
||||
timeout=scu.SCO_ORG_TIMEOUT,
|
||||
)
|
||||
|
||||
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as exc:
|
||||
log("ConnectionError: Impossible de joindre le serveur d'assistance")
|
||||
raise ScoValueError(
|
||||
"""
|
||||
Impossible de joindre le serveur d'assistance (scodoc.org).
|
||||
Veuillez contacter le service informatique de votre établissement pour
|
||||
corriger la configuration de ScoDoc. Dans la plupart des cas, il
|
||||
s'agit d'un proxy mal configuré.
|
||||
"""
|
||||
) from exc
|
||||
|
||||
return r
|
|
@ -446,7 +446,8 @@ def _ue_mod_bulletin(
|
|||
):
|
||||
"""Infos sur les modules (et évaluations) dans une UE
|
||||
(ajoute les informations aux modimpls)
|
||||
Result: liste de modules de l'UE avec les infos dans chacun (seulement ceux où l'étudiant est inscrit).
|
||||
Result: liste de modules de l'UE avec les infos dans chacun (seulement
|
||||
ceux où l'étudiant est inscrit).
|
||||
"""
|
||||
bul_show_mod_rangs = sco_preferences.get_preference(
|
||||
"bul_show_mod_rangs", formsemestre_id
|
||||
|
|
|
@ -61,7 +61,7 @@ from flask_login import current_user
|
|||
|
||||
from app.models import FormSemestre, Identite, ScoDocSiteConfig
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import NoteProcessError
|
||||
from app.scodoc.sco_exceptions import NoteProcessError, ScoPDFFormatError
|
||||
from app import log
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_pdf
|
||||
|
@ -226,9 +226,18 @@ class BulletinGenerator:
|
|||
server_name=self.server_name,
|
||||
filigranne=self.filigranne,
|
||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
with_page_numbers=self.multi_pages,
|
||||
)
|
||||
)
|
||||
try:
|
||||
document.build(story)
|
||||
except (
|
||||
ValueError,
|
||||
KeyError,
|
||||
reportlab.platypus.doctemplate.LayoutError,
|
||||
) as exc:
|
||||
raise ScoPDFFormatError(str(exc)) from exc
|
||||
|
||||
data = report.getvalue()
|
||||
return data
|
||||
|
||||
|
|
|
@ -106,6 +106,7 @@ def assemble_bulletins_pdf(
|
|||
pagesbookmarks=pagesbookmarks,
|
||||
filigranne=filigranne,
|
||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
with_page_numbers=False, # on ne veut pas de no de pages sur les bulletins imprimés en masse
|
||||
)
|
||||
)
|
||||
document.multiBuild(story)
|
||||
|
@ -122,7 +123,8 @@ def replacement_function(match) -> str:
|
|||
return r'<img %s src="%s"%s/>' % (match.group(2), logo.filepath, match.group(4))
|
||||
raise ScoValueError(
|
||||
'balise "%s": logo "%s" introuvable'
|
||||
% (pydoc.html.escape(balise), pydoc.html.escape(name))
|
||||
% (pydoc.html.escape(balise), pydoc.html.escape(name)),
|
||||
safe=True,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -114,6 +114,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
|||
html_class="notes_bulletin",
|
||||
html_class_ignore_default=True,
|
||||
html_with_td_classes=True,
|
||||
table_id="std_bul_table",
|
||||
)
|
||||
|
||||
return T.gen(fmt=fmt)
|
||||
|
|
|
@ -274,6 +274,7 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
|
|||
"""expire cache pour un semestre (ou tous ceux du département si formsemestre_id non spécifié).
|
||||
Si pdfonly, n'expire que les bulletins pdf cachés.
|
||||
"""
|
||||
from app.comp import df_cache
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.scodoc import sco_cursus
|
||||
|
||||
|
@ -315,12 +316,14 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
|
|||
and fid in g.formsemestre_results_cache
|
||||
):
|
||||
del g.formsemestre_results_cache[fid]
|
||||
|
||||
df_cache.EvaluationsPoidsCache.invalidate_sem(formsemestre_id)
|
||||
else:
|
||||
# optimization when we invalidate all evaluations:
|
||||
EvaluationCache.invalidate_all_sems()
|
||||
df_cache.EvaluationsPoidsCache.invalidate_all()
|
||||
if hasattr(g, "formsemestre_results_cache"):
|
||||
del g.formsemestre_results_cache
|
||||
|
||||
SemInscriptionsCache.delete_many(formsemestre_ids)
|
||||
ResultatsSemestreCache.delete_many(formsemestre_ids)
|
||||
ValidationsSemestreCache.delete_many(formsemestre_ids)
|
||||
|
|
|
@ -230,41 +230,41 @@ def next_iso_day(date):
|
|||
|
||||
def YearTable(
|
||||
year,
|
||||
events=[],
|
||||
events_by_day: dict[str, list[dict]],
|
||||
firstmonth=9,
|
||||
lastmonth=7,
|
||||
halfday=0,
|
||||
dayattributes="",
|
||||
pad_width=8,
|
||||
):
|
||||
# Code simplifié en 2024: utilisé seulement pour calendrier évaluations
|
||||
"""Generate a calendar table
|
||||
events = list of tuples (date, text, color, href [,halfday])
|
||||
where date is a string in ISO format (yyyy-mm-dd)
|
||||
halfday is boolean (true: morning, false: afternoon)
|
||||
text = text to put in calendar (must be short, 1-5 cars) (optional)
|
||||
if halfday, generate 2 cells per day (morning, afternoon)
|
||||
"""
|
||||
T = [
|
||||
'<table id="maincalendar" class="maincalendar" border="3" cellpadding="1" cellspacing="1" frame="box">'
|
||||
"""<table id="maincalendar" class="maincalendar"
|
||||
border="3" cellpadding="1" cellspacing="1" frame="box">"""
|
||||
]
|
||||
T.append("<tr>")
|
||||
month = firstmonth
|
||||
while 1:
|
||||
while True:
|
||||
T.append('<td valign="top">')
|
||||
T.append(MonthTableHead(month))
|
||||
T.append(_month_table_head(month))
|
||||
T.append(
|
||||
MonthTableBody(
|
||||
_month_table_body(
|
||||
month,
|
||||
year,
|
||||
events,
|
||||
halfday,
|
||||
events_by_day,
|
||||
dayattributes,
|
||||
is_work_saturday(),
|
||||
pad_width=pad_width,
|
||||
)
|
||||
)
|
||||
T.append(MonthTableTail())
|
||||
T.append("</td>")
|
||||
T.append(
|
||||
"""
|
||||
</table>
|
||||
</td>"""
|
||||
)
|
||||
if month == lastmonth:
|
||||
break
|
||||
month = month + 1
|
||||
|
@ -322,29 +322,32 @@ WEEKDAYCOLOR = GRAY1
|
|||
WEEKENDCOLOR = GREEN3
|
||||
|
||||
|
||||
def MonthTableHead(month):
|
||||
def _month_table_head(month):
|
||||
color = WHITE
|
||||
return """<table class="monthcalendar" border="0" cellpadding="0" cellspacing="0" frame="box">
|
||||
<tr bgcolor="%s"><td class="calcol" colspan="2" align="center">%s</td></tr>\n""" % (
|
||||
color,
|
||||
MONTHNAMES_ABREV[month - 1],
|
||||
)
|
||||
return f"""<table class="monthcalendar" border="0" cellpadding="0" cellspacing="0" frame="box">
|
||||
<tr bgcolor="{color}">
|
||||
<td class="calcol" colspan="2" align="center">{MONTHNAMES_ABREV[month - 1]}</td>
|
||||
</tr>\n"""
|
||||
|
||||
|
||||
def MonthTableTail():
|
||||
return "</table>\n"
|
||||
|
||||
|
||||
def MonthTableBody(
|
||||
month, year, events=[], halfday=0, trattributes="", work_saturday=False, pad_width=8
|
||||
):
|
||||
def _month_table_body(
|
||||
month,
|
||||
year,
|
||||
events_by_day: dict[str, list[dict]],
|
||||
trattributes="",
|
||||
work_saturday=False,
|
||||
) -> str:
|
||||
"""
|
||||
events : [event]
|
||||
event = [ yyyy-mm-dd, legend, href, color, descr ] XXX
|
||||
"""
|
||||
firstday, nbdays = calendar.monthrange(year, month)
|
||||
localtime = time.localtime()
|
||||
current_weeknum = time.strftime("%U", localtime)
|
||||
current_year = localtime[0]
|
||||
T = []
|
||||
rows = []
|
||||
# cherche date du lundi de la 1ere semaine de ce mois
|
||||
monday = ddmmyyyy("1/%d/%d" % (month, year))
|
||||
monday = ddmmyyyy(f"1/{month}/{year}")
|
||||
while monday.weekday != 0:
|
||||
monday = monday.prev()
|
||||
|
||||
|
@ -353,7 +356,6 @@ def MonthTableBody(
|
|||
else:
|
||||
weekend = ("S", "D")
|
||||
|
||||
if not halfday:
|
||||
for d in range(1, nbdays + 1):
|
||||
weeknum = time.strftime(
|
||||
"%U", time.strptime("%d/%d/%d" % (d, month, year), scu.DATE_FMT)
|
||||
|
@ -367,144 +369,38 @@ def MonthTableBody(
|
|||
bgcolor = WEEKDAYCOLOR
|
||||
weekclass = "wk" + str(monday).replace("/", "_")
|
||||
attrs = trattributes
|
||||
# events this day ?
|
||||
events = events_by_day.get(f"{year}-{month:02}-{d:02}", [])
|
||||
color = None
|
||||
legend = ""
|
||||
href = ""
|
||||
descr = ""
|
||||
# event this day ?
|
||||
# each event is a tuple (date, text, color, href)
|
||||
# where date is a string in ISO format (yyyy-mm-dd)
|
||||
ev_txts = []
|
||||
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]
|
||||
color = ev.get("color")
|
||||
href = ev.get("href", "")
|
||||
description = ev.get("description", "")
|
||||
if href:
|
||||
href = f'href="{href}"'
|
||||
if description:
|
||||
description = f"""title="{html.escape(description, quote=True)}" """
|
||||
if href or description:
|
||||
ev_txts.append(f"""<a {href} {description}>{ev.get("title", "")}</a>""")
|
||||
else:
|
||||
ev_txts.append(ev.get("title", " "))
|
||||
#
|
||||
cc = []
|
||||
if color is not None:
|
||||
cc.append('<td bgcolor="%s" class="calcell">' % color)
|
||||
cc.append(f'<td bgcolor="{color}" class="calcell">')
|
||||
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:
|
||||
if pad_width is not None:
|
||||
n = pad_width - len(legend) # pad to 8 cars
|
||||
if n > 0:
|
||||
legend = (
|
||||
" " * (n // 2) + legend + " " * ((n + 1) // 2)
|
||||
)
|
||||
else:
|
||||
legend = " " # empty cell
|
||||
cc.append(legend)
|
||||
if href or descr:
|
||||
cc.append("</a>")
|
||||
cc.append("</td>")
|
||||
cell = "".join(cc)
|
||||
cc.append(f"{', '.join(ev_txts)}</td>")
|
||||
cells = "".join(cc)
|
||||
if day == "D":
|
||||
monday = monday.next_day(7)
|
||||
if (
|
||||
weeknum == current_weeknum
|
||||
and current_year == year
|
||||
and weekclass != "wkend"
|
||||
):
|
||||
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)
|
||||
rows.append(
|
||||
f"""<tr bgcolor="{bgcolor}" class="{weekclass}" {attrs}>
|
||||
<td class="calday">{d}{day}</td>{cells}</tr>"""
|
||||
)
|
||||
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":
|
||||
monday = monday.next_day(7)
|
||||
T.append(
|
||||
'<tr bgcolor="%s" class="wk%s" %s><td class="calday">%d%s</td>'
|
||||
% (bgcolor, weekclass, attrs, d, day)
|
||||
)
|
||||
cc = []
|
||||
for morning in (True, False):
|
||||
color = None
|
||||
legend = ""
|
||||
href = ""
|
||||
descr = ""
|
||||
for ev in events:
|
||||
ev_year = int(ev[0][:4])
|
||||
ev_month = int(ev[0][5:7])
|
||||
ev_day = int(ev[0][8:10])
|
||||
if ev[4] is not None:
|
||||
ev_half = int(ev[4])
|
||||
else:
|
||||
ev_half = 0
|
||||
if (
|
||||
year == ev_year
|
||||
and month == ev_month
|
||||
and ev_day == d
|
||||
and morning == ev_half
|
||||
):
|
||||
if ev[1]:
|
||||
legend = ev[1]
|
||||
if ev[2]:
|
||||
color = ev[2]
|
||||
if ev[3]:
|
||||
href = ev[3]
|
||||
if len(ev) > 5 and ev[5]:
|
||||
descr = ev[5]
|
||||
#
|
||||
if color is not None:
|
||||
cc.append('<td bgcolor="%s" class="calcell">' % (color))
|
||||
else:
|
||||
cc.append('<td class="calcell">')
|
||||
if href:
|
||||
href = 'href="%s"' % href
|
||||
if descr:
|
||||
descr = 'title="%s"' % html.escape(descr, quote=True)
|
||||
if href or descr:
|
||||
cc.append("<a %s %s>" % (href, descr))
|
||||
if legend or d == 1:
|
||||
n = 3 - len(legend) # pad to 3 cars
|
||||
if n > 0:
|
||||
legend = (
|
||||
" " * (n // 2) + legend + " " * ((n + 1) // 2)
|
||||
)
|
||||
else:
|
||||
legend = " " # 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)
|
||||
return "\n".join(rows)
|
||||
|
|
|
@ -141,6 +141,7 @@ def formsemestre_table_estim_cost(
|
|||
""",
|
||||
origin=f"""Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}""",
|
||||
filename=f"EstimCout-S{formsemestre.semestre_id}",
|
||||
table_id="formsemestre_table_estim_cost",
|
||||
)
|
||||
return tab
|
||||
|
||||
|
|
|
@ -34,13 +34,13 @@ from app.scodoc import sco_cursus_dut
|
|||
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.comp import res_sem
|
||||
from app.models import FormSemestre
|
||||
from app.models import FormSemestre, Identite
|
||||
import app.scodoc.notesdb as ndb
|
||||
|
||||
|
||||
# SituationEtudParcours -> get_situation_etud_cursus
|
||||
def get_situation_etud_cursus(
|
||||
etud: dict, formsemestre_id: int
|
||||
etud: Identite, formsemestre_id: int
|
||||
) -> sco_cursus_dut.SituationEtudCursus:
|
||||
"""renvoie une instance de SituationEtudCursus (ou sous-classe spécialisée)"""
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
from app import db
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre, UniteEns, ScolarAutorisationInscription
|
||||
from app.models import FormSemestre, Identite, ScolarAutorisationInscription, UniteEns
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
|
@ -115,14 +115,22 @@ class SituationEtudCursus:
|
|||
class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
"Semestre dans un parcours"
|
||||
|
||||
def __init__(self, etud: dict, formsemestre_id: int, nt: NotesTableCompat):
|
||||
def __init__(self, etud: Identite, formsemestre_id: int, nt: NotesTableCompat):
|
||||
"""
|
||||
etud: dict filled by fill_etuds_info()
|
||||
"""
|
||||
assert formsemestre_id == nt.formsemestre.id
|
||||
self.etud = etud
|
||||
self.etudid = etud["etudid"]
|
||||
self.etudid = etud.id
|
||||
self.formsemestre_id = formsemestre_id
|
||||
self.sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
self.formsemestres: list[FormSemestre] = []
|
||||
"les semestres parcourus, le plus ancien en tête"
|
||||
self.sem = sco_formsemestre.get_formsemestre(
|
||||
formsemestre_id
|
||||
) # TODO utiliser formsemestres
|
||||
self.cur_sem: FormSemestre = nt.formsemestre
|
||||
self.can_compensate: set[int] = set()
|
||||
"les formsemestre_id qui peuvent compenser le courant"
|
||||
self.nt: NotesTableCompat = nt
|
||||
self.formation = self.nt.formsemestre.formation
|
||||
self.parcours = self.nt.parcours
|
||||
|
@ -130,18 +138,20 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
|||
# pour le DUT, le dernier est toujours S4.
|
||||
# Ici: terminal si semestre == NB_SEM ou bien semestre_id==-1
|
||||
# (licences et autres formations en 1 seule session))
|
||||
self.semestre_non_terminal = self.sem["semestre_id"] != self.parcours.NB_SEM
|
||||
if self.sem["semestre_id"] == NO_SEMESTRE_ID:
|
||||
self.semestre_non_terminal = self.cur_sem.semestre_id != self.parcours.NB_SEM
|
||||
if self.cur_sem.semestre_id == NO_SEMESTRE_ID:
|
||||
self.semestre_non_terminal = False
|
||||
# Liste des semestres du parcours de cet étudiant:
|
||||
self._comp_semestres()
|
||||
# Determine le semestre "precedent"
|
||||
self.prev_formsemestre_id = self._search_prev()
|
||||
self._search_prev()
|
||||
# Verifie barres
|
||||
self._comp_barres()
|
||||
# Verifie compensation
|
||||
if self.prev and self.sem["gestion_compensation"]:
|
||||
self.can_compensate_with_prev = self.prev["can_compensate"]
|
||||
if self.prev_formsemestre and self.cur_sem.gestion_compensation:
|
||||
self.can_compensate_with_prev = (
|
||||
self.prev_formsemestre.id in self.can_compensate
|
||||
)
|
||||
else:
|
||||
self.can_compensate_with_prev = False
|
||||
|
||||
|
@ -170,14 +180,14 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
|||
if rule.conclusion[0] in self.parcours.UNUSED_CODES:
|
||||
continue
|
||||
# Saute regles REDOSEM si pas de semestres decales:
|
||||
if (not self.sem["gestion_semestrielle"]) and rule.conclusion[
|
||||
if (not self.cur_sem.gestion_semestrielle) and rule.conclusion[
|
||||
3
|
||||
] == "REDOSEM":
|
||||
continue
|
||||
if rule.match(state):
|
||||
if rule.conclusion[0] == ADC:
|
||||
# dans les regles on ne peut compenser qu'avec le PRECEDENT:
|
||||
fiduc = self.prev_formsemestre_id
|
||||
fiduc = self.prev_formsemestre.id
|
||||
assert fiduc
|
||||
else:
|
||||
fiduc = None
|
||||
|
@ -203,8 +213,8 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
|||
"Phrase d'explication pour le code devenir"
|
||||
if not devenir:
|
||||
return ""
|
||||
s = self.sem["semestre_id"] # numero semestre courant
|
||||
if s < 0: # formation sans semestres (eg licence)
|
||||
s_idx = self.cur_sem.semestre_id # numero semestre courant
|
||||
if s_idx < 0: # formation sans semestres (eg licence)
|
||||
next_s = 1
|
||||
else:
|
||||
next_s = self._get_next_semestre_id()
|
||||
|
@ -219,27 +229,27 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
|||
elif devenir == REO:
|
||||
return "Réorienté"
|
||||
elif devenir == REDOANNEE:
|
||||
return "Redouble année (recommence %s%s)" % (SA, (s - 1))
|
||||
return "Redouble année (recommence %s%s)" % (SA, (s_idx - 1))
|
||||
elif devenir == REDOSEM:
|
||||
return "Redouble semestre (recommence en %s%s)" % (SA, s)
|
||||
return "Redouble semestre (recommence en %s%s)" % (SA, s_idx)
|
||||
elif devenir == RA_OR_NEXT:
|
||||
return passage + ", ou redouble année (en %s%s)" % (SA, (s - 1))
|
||||
return passage + ", ou redouble année (en %s%s)" % (SA, (s_idx - 1))
|
||||
elif devenir == RA_OR_RS:
|
||||
return "Redouble semestre %s%s, ou redouble année (en %s%s)" % (
|
||||
SA,
|
||||
s,
|
||||
s_idx,
|
||||
SA,
|
||||
s - 1,
|
||||
s_idx - 1,
|
||||
)
|
||||
elif devenir == RS_OR_NEXT:
|
||||
return passage + ", ou semestre %s%s" % (SA, s)
|
||||
return passage + ", ou semestre %s%s" % (SA, s_idx)
|
||||
elif devenir == NEXT_OR_NEXT2:
|
||||
return passage + ", ou en semestre %s%s" % (
|
||||
SA,
|
||||
s + 2,
|
||||
s_idx + 2,
|
||||
) # coherent avec get_next_semestre_ids
|
||||
elif devenir == NEXT2:
|
||||
return "Passe en %s%s" % (SA, s + 2)
|
||||
return "Passe en %s%s" % (SA, s_idx + 2)
|
||||
else:
|
||||
log("explique_devenir: code devenir inconnu: %s" % devenir)
|
||||
return "Code devenir inconnu !"
|
||||
|
@ -258,7 +268,7 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
|||
|
||||
def _sems_validated(self, exclude_current=False):
|
||||
"True si semestres du parcours validés"
|
||||
if self.sem["semestre_id"] == NO_SEMESTRE_ID:
|
||||
if self.cur_sem.semestre_id == NO_SEMESTRE_ID:
|
||||
# mono-semestre: juste celui ci
|
||||
decision = self.nt.get_etud_decision_sem(self.etudid)
|
||||
return decision and code_semestre_validant(decision["code"])
|
||||
|
@ -266,8 +276,8 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
|||
to_validate = set(
|
||||
range(1, self.parcours.NB_SEM + 1)
|
||||
) # ensemble des indices à valider
|
||||
if exclude_current and self.sem["semestre_id"] in to_validate:
|
||||
to_validate.remove(self.sem["semestre_id"])
|
||||
if exclude_current and self.cur_sem.semestre_id in to_validate:
|
||||
to_validate.remove(self.cur_sem.semestre_id)
|
||||
return self._sem_list_validated(to_validate)
|
||||
|
||||
def can_jump_to_next2(self):
|
||||
|
@ -275,14 +285,14 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
|||
Il faut donc que tous les semestres 1...n-1 soient validés et que n+1 soit en attente.
|
||||
(et que le sem courant n soit validé, ce qui n'est pas testé ici)
|
||||
"""
|
||||
n = self.sem["semestre_id"]
|
||||
if not self.sem["gestion_semestrielle"]:
|
||||
s_idx = self.cur_sem.semestre_id
|
||||
if not self.cur_sem.gestion_semestrielle:
|
||||
return False # pas de semestre décalés
|
||||
if n == NO_SEMESTRE_ID or n > self.parcours.NB_SEM - 2:
|
||||
if s_idx == NO_SEMESTRE_ID or s_idx > self.parcours.NB_SEM - 2:
|
||||
return False # n+2 en dehors du parcours
|
||||
if self._sem_list_validated(set(range(1, n))):
|
||||
if self._sem_list_validated(set(range(1, s_idx))):
|
||||
# antérieurs validé, teste suivant
|
||||
n1 = n + 1
|
||||
n1 = s_idx + 1
|
||||
for sem in self.get_semestres():
|
||||
if (
|
||||
sem["semestre_id"] == n1
|
||||
|
@ -315,19 +325,17 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
|||
return not sem_idx_set
|
||||
|
||||
def _comp_semestres(self):
|
||||
# etud['sems'] est trie par date decroissante (voir fill_etuds_info)
|
||||
if not "sems" in self.etud:
|
||||
self.etud["sems"] = sco_etud.etud_inscriptions_infos(
|
||||
self.etud["etudid"], self.etud["ne"]
|
||||
)["sems"]
|
||||
sems = self.etud["sems"][:] # copy
|
||||
sems.reverse()
|
||||
# plus ancien en tête:
|
||||
self.formsemestres = self.etud.get_formsemestres(recent_first=False)
|
||||
|
||||
# Nb max d'UE et acronymes
|
||||
ue_acros = {} # acronyme ue : 1
|
||||
nb_max_ue = 0
|
||||
for sem in sems:
|
||||
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
|
||||
sems = []
|
||||
for formsemestre in self.formsemestres: # plus ancien en tête
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
sem = formsemestre.to_dict()
|
||||
sems.append(sem)
|
||||
ues = nt.get_ues_stat_dict(filter_sport=True)
|
||||
for ue in ues:
|
||||
ue_acros[ue["acronyme"]] = 1
|
||||
|
@ -338,37 +346,48 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
|||
sem["formation_code"] = formsemestre.formation.formation_code
|
||||
# si sem peut servir à compenser le semestre courant, positionne
|
||||
# can_compensate
|
||||
sem["can_compensate"] = self.check_compensation_dut(sem, nt)
|
||||
if self.check_compensation_dut(sem, nt):
|
||||
self.can_compensate.add(formsemestre.id)
|
||||
|
||||
self.ue_acros = list(ue_acros.keys())
|
||||
self.ue_acros.sort()
|
||||
self.nb_max_ue = nb_max_ue
|
||||
self.sems = sems
|
||||
|
||||
def get_semestres(self):
|
||||
def get_semestres(self) -> list[dict]:
|
||||
"""Liste des semestres dans lesquels a été inscrit
|
||||
l'étudiant (quelle que soit la formation), le plus ancien en tête"""
|
||||
return self.sems
|
||||
|
||||
def get_cursus_descr(self, filter_futur=False):
|
||||
def get_cursus_descr(self, filter_futur=False, filter_formation_code=False):
|
||||
"""Description brève du parcours: "S1, S2, ..."
|
||||
Si filter_futur, ne mentionne pas les semestres qui sont après le semestre courant.
|
||||
Si filter_formation_code, restreint aux semestres de même code formation que le courant.
|
||||
"""
|
||||
cur_begin_date = self.sem["dateord"]
|
||||
cur_begin_date = self.cur_sem.date_debut
|
||||
cur_formation_code = self.cur_sem.formation.formation_code
|
||||
p = []
|
||||
for s in self.sems:
|
||||
if s["ins"]["etat"] == scu.DEMISSION:
|
||||
for formsemestre in self.formsemestres:
|
||||
inscription = formsemestre.etuds_inscriptions.get(self.etud.id)
|
||||
if inscription is None:
|
||||
raise ValueError("Etudiant non inscrit au semestre") # bug
|
||||
if inscription.etat == scu.DEMISSION:
|
||||
dem = " (dem.)"
|
||||
else:
|
||||
dem = ""
|
||||
if filter_futur and s["dateord"] > cur_begin_date:
|
||||
if filter_futur and formsemestre.date_debut > cur_begin_date:
|
||||
continue # skip semestres demarrant apres le courant
|
||||
SA = self.parcours.SESSION_ABBRV # 'S' ou 'A'
|
||||
if s["semestre_id"] < 0:
|
||||
SA = "A" # force, cas des DUT annuels par exemple
|
||||
p.append("%s%d%s" % (SA, -s["semestre_id"], dem))
|
||||
if (
|
||||
filter_formation_code
|
||||
and formsemestre.formation.formation_code != cur_formation_code
|
||||
):
|
||||
continue # restreint aux semestres de la formation courante (pour les PV)
|
||||
session_abbrv = self.parcours.SESSION_ABBRV # 'S' ou 'A'
|
||||
if formsemestre.semestre_id < 0:
|
||||
session_abbrv = "A" # force, cas des DUT annuels par exemple
|
||||
p.append("%s%d%s" % (session_abbrv, -formsemestre.semestre_id, dem))
|
||||
else:
|
||||
p.append("%s%d%s" % (SA, s["semestre_id"], dem))
|
||||
p.append("%s%d%s" % (session_abbrv, formsemestre.semestre_id, dem))
|
||||
return ", ".join(p)
|
||||
|
||||
def get_parcours_decisions(self):
|
||||
|
@ -377,7 +396,7 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
|||
Returns: { semestre_id : code }
|
||||
"""
|
||||
r = {}
|
||||
if self.sem["semestre_id"] == NO_SEMESTRE_ID:
|
||||
if self.cur_sem.semestre_id == NO_SEMESTRE_ID:
|
||||
indices = [NO_SEMESTRE_ID]
|
||||
else:
|
||||
indices = list(range(1, self.parcours.NB_SEM + 1))
|
||||
|
@ -420,22 +439,22 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
|||
"true si ce semestre pourrait etre compensé par un autre (e.g. barres UE > 8)"
|
||||
return self.barres_ue_ok
|
||||
|
||||
def _search_prev(self):
|
||||
def _search_prev(self) -> FormSemestre | None:
|
||||
"""Recherche semestre 'precedent'.
|
||||
return prev_formsemestre_id
|
||||
positionne .prev_decision
|
||||
"""
|
||||
self.prev = None
|
||||
self.prev_formsemestre = None
|
||||
self.prev_decision = None
|
||||
if len(self.sems) < 2:
|
||||
if len(self.formsemestres) < 2:
|
||||
return None
|
||||
# Cherche sem courant dans la liste triee par date_debut
|
||||
cur = None
|
||||
icur = -1
|
||||
for cur in self.sems:
|
||||
for cur in self.formsemestres:
|
||||
icur += 1
|
||||
if cur["formsemestre_id"] == self.formsemestre_id:
|
||||
if cur.id == self.formsemestre_id:
|
||||
break
|
||||
if not cur or cur["formsemestre_id"] != self.formsemestre_id:
|
||||
if not cur or cur.id != self.formsemestre_id:
|
||||
log(
|
||||
f"*** SituationEtudCursus: search_prev: cur not found (formsemestre_id={self.formsemestre_id}, etudid={self.etudid})"
|
||||
)
|
||||
|
@ -443,60 +462,59 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
|||
# Cherche semestre antérieur de même formation (code) et semestre_id precedent
|
||||
#
|
||||
# i = icur - 1 # part du courant, remonte vers le passé
|
||||
i = len(self.sems) - 1 # par du dernier, remonte vers le passé
|
||||
prev = None
|
||||
i = len(self.formsemestres) - 1 # par du dernier, remonte vers le passé
|
||||
prev_formsemestre = None
|
||||
while i >= 0:
|
||||
if (
|
||||
self.sems[i]["formation_code"] == self.formation.formation_code
|
||||
and self.sems[i]["semestre_id"] == cur["semestre_id"] - 1
|
||||
self.formsemestres[i].formation.formation_code
|
||||
== self.formation.formation_code
|
||||
and self.formsemestres[i].semestre_id == cur.semestre_id - 1
|
||||
):
|
||||
prev = self.sems[i]
|
||||
prev_formsemestre = self.formsemestres[i]
|
||||
break
|
||||
i -= 1
|
||||
if not prev:
|
||||
if not prev_formsemestre:
|
||||
return None # pas de precedent trouvé
|
||||
self.prev = prev
|
||||
self.prev_formsemestre = prev_formsemestre
|
||||
# Verifications basiques:
|
||||
# ?
|
||||
# Code etat du semestre precedent:
|
||||
formsemestre = FormSemestre.query.get_or_404(prev["formsemestre_id"])
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(prev_formsemestre)
|
||||
self.prev_decision = nt.get_etud_decision_sem(self.etudid)
|
||||
self.prev_moy_gen = nt.get_etud_moy_gen(self.etudid)
|
||||
self.prev_barres_ue_ok = nt.etud_check_conditions_ues(self.etudid)[0]
|
||||
return self.prev["formsemestre_id"]
|
||||
|
||||
def get_next_semestre_ids(self, devenir):
|
||||
def get_next_semestre_ids(self, devenir: str) -> list[int]:
|
||||
"""Liste des numeros de semestres autorises avec ce devenir
|
||||
Ne vérifie pas que le devenir est possible (doit être fait avant),
|
||||
juste que le rang du semestre est dans le parcours [1..NB_SEM]
|
||||
"""
|
||||
s = self.sem["semestre_id"]
|
||||
s_idx = self.cur_sem.semestre_id
|
||||
if devenir == NEXT:
|
||||
ids = [self._get_next_semestre_id()]
|
||||
elif devenir == REDOANNEE:
|
||||
ids = [s - 1]
|
||||
ids = [s_idx - 1]
|
||||
elif devenir == REDOSEM:
|
||||
ids = [s]
|
||||
ids = [s_idx]
|
||||
elif devenir == RA_OR_NEXT:
|
||||
ids = [s - 1, self._get_next_semestre_id()]
|
||||
ids = [s_idx - 1, self._get_next_semestre_id()]
|
||||
elif devenir == RA_OR_RS:
|
||||
ids = [s - 1, s]
|
||||
ids = [s_idx - 1, s_idx]
|
||||
elif devenir == RS_OR_NEXT:
|
||||
ids = [s, self._get_next_semestre_id()]
|
||||
ids = [s_idx, self._get_next_semestre_id()]
|
||||
elif devenir == NEXT_OR_NEXT2:
|
||||
ids = [
|
||||
self._get_next_semestre_id(),
|
||||
s + 2,
|
||||
s_idx + 2,
|
||||
] # cohérent avec explique_devenir()
|
||||
elif devenir == NEXT2:
|
||||
ids = [s + 2]
|
||||
ids = [s_idx + 2]
|
||||
else:
|
||||
ids = [] # reoriente ou autre: pas de next !
|
||||
# clip [1..NB_SEM]
|
||||
r = []
|
||||
for idx in ids:
|
||||
if idx > 0 and idx <= self.parcours.NB_SEM:
|
||||
if 0 < idx <= self.parcours.NB_SEM:
|
||||
r.append(idx)
|
||||
return r
|
||||
|
||||
|
@ -504,27 +522,27 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
|||
"""Indice du semestre suivant non validé.
|
||||
S'il n'y en a pas, ramène NB_SEM+1
|
||||
"""
|
||||
s = self.sem["semestre_id"]
|
||||
if s >= self.parcours.NB_SEM:
|
||||
s_idx = self.cur_sem.semestre_id
|
||||
if s_idx >= self.parcours.NB_SEM:
|
||||
return self.parcours.NB_SEM + 1
|
||||
validated = True
|
||||
while validated and (s < self.parcours.NB_SEM):
|
||||
s = s + 1
|
||||
while validated and (s_idx < self.parcours.NB_SEM):
|
||||
s_idx = s_idx + 1
|
||||
# semestre s validé ?
|
||||
validated = False
|
||||
for sem in self.sems:
|
||||
for formsemestre in self.formsemestres:
|
||||
if (
|
||||
sem["formation_code"] == self.formation.formation_code
|
||||
and sem["semestre_id"] == s
|
||||
formsemestre.formation.formation_code
|
||||
== self.formation.formation_code
|
||||
and formsemestre.semestre_id == s_idx
|
||||
):
|
||||
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(
|
||||
formsemestre
|
||||
)
|
||||
decision = nt.get_etud_decision_sem(self.etudid)
|
||||
if decision and code_semestre_validant(decision["code"]):
|
||||
validated = True
|
||||
return s
|
||||
return s_idx
|
||||
|
||||
def valide_decision(self, decision):
|
||||
"""Enregistre la decision (instance de DecisionSem)
|
||||
|
@ -539,8 +557,11 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
|||
fsid = decision.formsemestre_id_utilise_pour_compenser
|
||||
if fsid:
|
||||
ok = False
|
||||
for sem in self.sems:
|
||||
if sem["formsemestre_id"] == fsid and sem["can_compensate"]:
|
||||
for formsemestre in self.formsemestres:
|
||||
if (
|
||||
formsemestre.id == fsid
|
||||
and formsemestre.id in self.can_compensate
|
||||
):
|
||||
ok = True
|
||||
break
|
||||
if not ok:
|
||||
|
@ -581,7 +602,7 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
|||
decision.assiduite,
|
||||
)
|
||||
# -- modification du code du semestre precedent
|
||||
if self.prev and decision.new_code_prev:
|
||||
if self.prev_formsemestre and decision.new_code_prev:
|
||||
if decision.new_code_prev == ADC:
|
||||
# ne compense le prec. qu'avec le sem. courant
|
||||
fsid = self.formsemestre_id
|
||||
|
@ -589,7 +610,7 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
|||
fsid = None
|
||||
to_invalidate += formsemestre_update_validation_sem(
|
||||
cnx,
|
||||
self.prev["formsemestre_id"],
|
||||
self.prev_formsemestre.id,
|
||||
self.etudid,
|
||||
decision.new_code_prev,
|
||||
assidu=True,
|
||||
|
@ -601,18 +622,18 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
|||
etudid=self.etudid,
|
||||
commit=False,
|
||||
msg="formsemestre_id=%s code=%s"
|
||||
% (self.prev["formsemestre_id"], decision.new_code_prev),
|
||||
% (self.prev_formsemestre.id, decision.new_code_prev),
|
||||
)
|
||||
# modifs des codes d'UE (pourraient passer de ADM a CMP, meme sans modif des notes)
|
||||
formsemestre_validate_ues(
|
||||
self.prev["formsemestre_id"],
|
||||
self.prev_formsemestre.id,
|
||||
self.etudid,
|
||||
decision.new_code_prev,
|
||||
decision.assiduite, # attention: en toute rigueur il faudrait utiliser une indication de l'assiduite au sem. precedent, que nous n'avons pas...
|
||||
)
|
||||
|
||||
sco_cache.invalidate_formsemestre(
|
||||
formsemestre_id=self.prev["formsemestre_id"]
|
||||
formsemestre_id=self.prev_formsemestre.id
|
||||
) # > modif decisions jury (sem, UE)
|
||||
|
||||
try:
|
||||
|
@ -694,7 +715,7 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
|||
class SituationEtudCursusECTS(SituationEtudCursusClassic):
|
||||
"""Gestion parcours basés sur ECTS"""
|
||||
|
||||
def __init__(self, etud, formsemestre_id, nt):
|
||||
def __init__(self, etud: Identite, formsemestre_id: int, nt):
|
||||
SituationEtudCursusClassic.__init__(self, etud, formsemestre_id, nt)
|
||||
|
||||
def could_be_compensated(self):
|
||||
|
|
|
@ -222,6 +222,7 @@ def table_debouche_etudids(etudids, keep_numeric=True):
|
|||
html_sortable=True,
|
||||
html_class="table_leftalign table_listegroupe",
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
table_id="table_debouche_etudids",
|
||||
)
|
||||
return tab
|
||||
|
||||
|
|
|
@ -192,12 +192,24 @@ def _sem_table_gt(formsemestres: Query, showcodes=False, fmt="html") -> GenTable
|
|||
"elt_sem_apo",
|
||||
]
|
||||
if showcodes:
|
||||
columns_ids = ("formsemestre_id",) + columns_ids
|
||||
columns_ids.insert(0, "formsemestre_id") # prepend
|
||||
|
||||
html_class = "stripe cell-border compact hover order-column table_leftalign semlist"
|
||||
if current_user.has_permission(Permission.EditApogee):
|
||||
html_class += " apo_editable"
|
||||
tab = GenTable(
|
||||
columns_ids=columns_ids,
|
||||
html_class_ignore_default=True,
|
||||
html_class=html_class,
|
||||
html_sortable=True,
|
||||
html_table_attrs=f"""
|
||||
data-apo_save_url="{url_for('notes.formsemestre_set_apo_etapes', scodoc_dept=g.scodoc_dept)}"
|
||||
data-elt_annee_apo_save_url="{url_for('notes.formsemestre_set_elt_annee_apo', scodoc_dept=g.scodoc_dept)}"
|
||||
data-elt_sem_apo_save_url="{url_for('notes.formsemestre_set_elt_sem_apo', scodoc_dept=g.scodoc_dept)}"
|
||||
""",
|
||||
html_with_td_classes=True,
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
rows=sems,
|
||||
titles={
|
||||
"formsemestre_id": "id",
|
||||
"semestre_id_n": "S#",
|
||||
|
@ -211,19 +223,7 @@ def _sem_table_gt(formsemestres: Query, showcodes=False, fmt="html") -> GenTable
|
|||
"elt_sem_apo": "Elt. sem. Apo.",
|
||||
"formation": "Formation",
|
||||
},
|
||||
columns_ids=columns_ids,
|
||||
rows=sems,
|
||||
table_id="semlist",
|
||||
html_class_ignore_default=True,
|
||||
html_class=html_class,
|
||||
html_sortable=True,
|
||||
html_table_attrs=f"""
|
||||
data-apo_save_url="{url_for('notes.formsemestre_set_apo_etapes', scodoc_dept=g.scodoc_dept)}"
|
||||
data-elt_annee_apo_save_url="{url_for('notes.formsemestre_set_elt_annee_apo', scodoc_dept=g.scodoc_dept)}"
|
||||
data-elt_sem_apo_save_url="{url_for('notes.formsemestre_set_elt_sem_apo', scodoc_dept=g.scodoc_dept)}"
|
||||
""",
|
||||
html_with_td_classes=True,
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
)
|
||||
|
||||
return tab
|
||||
|
|
|
@ -67,7 +67,7 @@ SCO_DUMP_LOCK = "/tmp/scodump.lock"
|
|||
|
||||
def sco_dump_and_send_db(
|
||||
message: str = "", request_url: str = "", traceback_str_base64: str = ""
|
||||
):
|
||||
) -> requests.Response:
|
||||
"""Dump base de données et l'envoie anonymisée pour debug"""
|
||||
traceback_str = base64.urlsafe_b64decode(traceback_str_base64).decode(
|
||||
scu.SCO_ENCODING
|
||||
|
@ -97,7 +97,6 @@ def sco_dump_and_send_db(
|
|||
|
||||
# Send
|
||||
r = _send_db(ano_db_name, message, request_url, traceback_str=traceback_str)
|
||||
code = r.status_code
|
||||
|
||||
finally:
|
||||
# Drop anonymized database
|
||||
|
@ -107,7 +106,7 @@ def sco_dump_and_send_db(
|
|||
|
||||
log("sco_dump_and_send_db: done.")
|
||||
|
||||
return code
|
||||
return r
|
||||
|
||||
|
||||
def _duplicate_db(db_name, ano_db_name):
|
||||
|
@ -216,11 +215,11 @@ def _drop_ano_db(ano_db_name):
|
|||
log("_drop_ano_db: no temp db, nothing to drop")
|
||||
return
|
||||
cmd = ["dropdb", ano_db_name]
|
||||
log("sco_dump_and_send_db: {}".format(cmd))
|
||||
log(f"sco_dump_and_send_db: {cmd}")
|
||||
try:
|
||||
_ = subprocess.check_output(cmd)
|
||||
except subprocess.CalledProcessError as e:
|
||||
log("sco_dump_and_send_db: exception dropdb {}".format(e))
|
||||
except subprocess.CalledProcessError as exc:
|
||||
log(f"sco_dump_and_send_db: exception dropdb {exc}")
|
||||
raise ScoValueError(
|
||||
"erreur lors de la suppression de la base {}".format(ano_db_name)
|
||||
)
|
||||
f"erreur lors de la suppression de la base {ano_db_name}"
|
||||
) from exc
|
||||
|
|
|
@ -58,21 +58,20 @@ def formation_delete(formation_id=None, dialog_confirmed=False):
|
|||
html_sco_header.sco_header(page_title="Suppression d'une formation"),
|
||||
f"""<h2>Suppression de la formation {formation.titre} ({formation.acronyme})</h2>""",
|
||||
]
|
||||
|
||||
sems = sco_formsemestre.do_formsemestre_list({"formation_id": formation_id})
|
||||
if sems:
|
||||
formsemestres = formation.formsemestres.all()
|
||||
if formsemestres:
|
||||
H.append(
|
||||
"""<p class="warning">Impossible de supprimer cette formation,
|
||||
car les sessions suivantes l'utilisent:</p>
|
||||
<ul>"""
|
||||
)
|
||||
for sem in sems:
|
||||
for formsemestre in formsemestres:
|
||||
H.append(f"""<li>{formsemestre.html_link_status()}</li>""")
|
||||
H.append(
|
||||
'<li><a class="stdlink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titremois)s</a></li>'
|
||||
% sem
|
||||
)
|
||||
H.append(
|
||||
'</ul><p><a class="stdlink" href="%s">Revenir</a></p>' % scu.NotesURL()
|
||||
f"""</ul>
|
||||
<p><a class="stdlink" href="{
|
||||
url_for("notes.index_html", scodoc_dept=g.scodoc_dept)
|
||||
}">Revenir</a></p>"""
|
||||
)
|
||||
else:
|
||||
if not dialog_confirmed:
|
||||
|
@ -85,14 +84,16 @@ def formation_delete(formation_id=None, dialog_confirmed=False):
|
|||
</p>
|
||||
""",
|
||||
OK="Supprimer cette formation",
|
||||
cancel_url=scu.NotesURL(),
|
||||
cancel_url=url_for("notes.index_html", scodoc_dept=g.scodoc_dept),
|
||||
parameters={"formation_id": formation_id},
|
||||
)
|
||||
else:
|
||||
do_formation_delete(formation_id)
|
||||
H.append(
|
||||
f"""<p>OK, formation supprimée.</p>
|
||||
<p><a class="stdlink" href="{scu.NotesURL()}">continuer</a></p>"""
|
||||
<p><a class="stdlink" href="{
|
||||
url_for("notes.index_html", scodoc_dept=g.scodoc_dept)
|
||||
}">continuer</a></p>"""
|
||||
)
|
||||
|
||||
H.append(html_sco_header.sco_footer())
|
||||
|
@ -252,7 +253,7 @@ def formation_edit(formation_id=None, create=False):
|
|||
if tf[0] == 0:
|
||||
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
|
||||
elif tf[0] == -1:
|
||||
return flask.redirect(scu.NotesURL())
|
||||
return flask.redirect(url_for("notes.index_html", scodoc_dept=g.scodoc_dept))
|
||||
else:
|
||||
# check unicity : constraint UNIQUE(acronyme,titre,version)
|
||||
if create:
|
||||
|
@ -325,6 +326,7 @@ def do_formation_create(args: dict) -> Formation:
|
|||
scodoc_dept=g.scodoc_dept,
|
||||
formation_id=formation.id,
|
||||
),
|
||||
safe=True,
|
||||
) from exc
|
||||
|
||||
ScolarNews.add(
|
||||
|
|
|
@ -448,7 +448,7 @@ def module_edit(
|
|||
(
|
||||
"titre",
|
||||
{
|
||||
"size": 30,
|
||||
"size": 64,
|
||||
"explanation": """nom du module. Exemple:
|
||||
<em>Introduction à la démarche ergonomique</em>""",
|
||||
},
|
||||
|
@ -456,8 +456,8 @@ def module_edit(
|
|||
(
|
||||
"abbrev",
|
||||
{
|
||||
"size": 20,
|
||||
"explanation": """nom abrégé (pour bulletins).
|
||||
"size": 32,
|
||||
"explanation": """(optionnel) nom abrégé pour bulletins.
|
||||
Exemple: <em>Intro. à l'ergonomie</em>""",
|
||||
},
|
||||
),
|
||||
|
|
|
@ -837,8 +837,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
|
|||
<a href="{url_for('notes.refcomp_show',
|
||||
scodoc_dept=g.scodoc_dept, refcomp_id=formation.referentiel_competence.id)}"
|
||||
class="stdlink">
|
||||
{formation.referentiel_competence.type_titre}
|
||||
{formation.referentiel_competence.specialite_long}
|
||||
{formation.referentiel_competence.get_title()}
|
||||
</a> """
|
||||
msg_refcomp = "changer"
|
||||
H.append(f"""<ul><li>{descr_refcomp}""")
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
Lecture et conversion des ics.
|
||||
|
||||
"""
|
||||
|
||||
from datetime import timezone
|
||||
import glob
|
||||
import os
|
||||
|
@ -229,7 +230,7 @@ def translate_calendar(
|
|||
heure_deb=event["heure_deb"],
|
||||
heure_fin=event["heure_fin"],
|
||||
moduleimpl_id=modimpl.id,
|
||||
jour=event["jour"],
|
||||
day=event["jour"],
|
||||
)
|
||||
if modimpl and group
|
||||
else None
|
||||
|
|
|
@ -490,6 +490,7 @@ def table_apo_csv_list(semset):
|
|||
# base_url = '%s?formsemestre_id=%s' % (request.base_url, formsemestre_id),
|
||||
# caption='Maquettes enregistrées',
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
table_id="apo_csv_list",
|
||||
)
|
||||
|
||||
return tab
|
||||
|
@ -582,6 +583,7 @@ def _view_etuds_page(
|
|||
html_class="table_leftalign",
|
||||
filename="students_apo",
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
table_id="view_etuds_page",
|
||||
)
|
||||
if fmt != "html":
|
||||
return tab.make_page(fmt=fmt)
|
||||
|
@ -798,6 +800,7 @@ def view_apo_csv(etape_apo="", semset_id="", fmt="html"):
|
|||
filename="students_" + etape_apo,
|
||||
caption="Étudiants Apogée en " + etape_apo,
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
table_id="view_apo_csv",
|
||||
)
|
||||
|
||||
if fmt != "html":
|
||||
|
|
|
@ -666,7 +666,9 @@ class EtapeBilan:
|
|||
col_ids,
|
||||
self.titres,
|
||||
html_class="repartition",
|
||||
html_sortable=True,
|
||||
html_with_td_classes=True,
|
||||
table_id="apo-repartition",
|
||||
).gen(fmt="html")
|
||||
)
|
||||
return "\n".join(H)
|
||||
|
@ -762,9 +764,9 @@ class EtapeBilan:
|
|||
rows,
|
||||
col_ids,
|
||||
titles,
|
||||
table_id="detail",
|
||||
html_class="table_leftalign",
|
||||
html_sortable=True,
|
||||
table_id="apo-detail",
|
||||
).gen(fmt="html")
|
||||
)
|
||||
return "\n".join(H)
|
||||
|
|
|
@ -367,6 +367,9 @@ def evaluation_create_form(
|
|||
+ "\n".join(H)
|
||||
+ "\n"
|
||||
+ tf[1]
|
||||
+ render_template(
|
||||
"scodoc/forms/evaluation_edit.j2",
|
||||
)
|
||||
+ render_template(
|
||||
"scodoc/help/evaluations.j2", is_apc=is_apc, modimpl=modimpl
|
||||
)
|
||||
|
|
|
@ -70,8 +70,8 @@ def evaluations_recap_table(formsemestre: FormSemestre) -> list[dict]:
|
|||
Colonnes:
|
||||
- code (UE ou module),
|
||||
- titre
|
||||
- type évaluation
|
||||
- complete
|
||||
- publiée
|
||||
- inscrits (non dem. ni def.)
|
||||
- nb notes manquantes
|
||||
- nb ATT
|
||||
|
@ -81,9 +81,10 @@ def evaluations_recap_table(formsemestre: FormSemestre) -> list[dict]:
|
|||
rows = []
|
||||
titles = {
|
||||
"type": "",
|
||||
"code": "Code",
|
||||
"code": "Module",
|
||||
"titre": "",
|
||||
"date": "Date",
|
||||
"type_evaluation": "Type",
|
||||
"complete": "Comptée",
|
||||
"inscrits": "Inscrits",
|
||||
"manquantes": "Manquantes", # notes eval non entrées
|
||||
|
@ -114,7 +115,9 @@ def evaluations_recap_table(formsemestre: FormSemestre) -> list[dict]:
|
|||
rows.append(row)
|
||||
line_idx += 1
|
||||
for evaluation_id in modimpl_results.evals_notes:
|
||||
e = db.session.get(Evaluation, evaluation_id)
|
||||
e: Evaluation = db.session.get(Evaluation, evaluation_id)
|
||||
if e is None:
|
||||
continue # ignore errors (rare race conditions?)
|
||||
eval_etat = modimpl_results.evaluations_etat[evaluation_id]
|
||||
row = {
|
||||
"type": "",
|
||||
|
@ -128,6 +131,7 @@ def evaluations_recap_table(formsemestre: FormSemestre) -> list[dict]:
|
|||
"_titre_target_attrs": 'class="discretelink"',
|
||||
"date": e.date_debut.strftime(scu.DATE_FMT) if e.date_debut else "",
|
||||
"_date_order": e.date_debut.isoformat() if e.date_debut else "",
|
||||
"type_evaluation": e.type_abbrev(),
|
||||
"complete": "oui" if eval_etat.is_complete else "non",
|
||||
"_complete_target": "#",
|
||||
"_complete_target_attrs": (
|
||||
|
|
|
@ -25,8 +25,8 @@
|
|||
#
|
||||
##############################################################################
|
||||
|
||||
"""Evaluations
|
||||
"""
|
||||
"""Evaluations"""
|
||||
|
||||
import collections
|
||||
import datetime
|
||||
import operator
|
||||
|
@ -50,6 +50,7 @@ from app.scodoc import sco_cal
|
|||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_gen_cal
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_users
|
||||
|
@ -360,6 +361,108 @@ def do_evaluation_etat_in_mod(nt, modimpl: ModuleImpl):
|
|||
return etat
|
||||
|
||||
|
||||
class JourEval(sco_gen_cal.Jour):
|
||||
"""
|
||||
Représentation d'un jour dans un calendrier d'évaluations
|
||||
"""
|
||||
|
||||
COLOR_INCOMPLETE = "#FF6060"
|
||||
COLOR_COMPLETE = "#A0FFA0"
|
||||
COLOR_FUTUR = "#70E0FF"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
date: datetime.date,
|
||||
evaluations: list[Evaluation],
|
||||
parent: "CalendrierEval",
|
||||
):
|
||||
super().__init__(date)
|
||||
|
||||
self.evaluations: list[Evaluation] = evaluations
|
||||
self.evaluations.sort(key=lambda e: e.date_debut)
|
||||
|
||||
self.parent: "CalendrierEval" = parent
|
||||
|
||||
def get_html(self) -> str:
|
||||
htmls = []
|
||||
|
||||
for e in self.evaluations:
|
||||
url: str = url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=e.moduleimpl_id,
|
||||
)
|
||||
title: str = (
|
||||
e.moduleimpl.module.code or e.moduleimpl.module.abbrev or "éval."
|
||||
)
|
||||
htmls.append(
|
||||
f"""<a
|
||||
href="{url}"
|
||||
style="{self._get_eval_style(e)}"
|
||||
title="{self._get_eval_title(e)}"
|
||||
class="stdlink"
|
||||
>{title}</a>"""
|
||||
)
|
||||
|
||||
return ", ".join(htmls)
|
||||
|
||||
def _get_eval_style(self, e: Evaluation) -> str:
|
||||
color: str = ""
|
||||
# Etat (notes completes) de l'évaluation:
|
||||
modimpl_result = self.parent.nt.modimpls_results[e.moduleimpl.id]
|
||||
if modimpl_result.evaluations_etat[e.id].is_complete:
|
||||
color = JourEval.COLOR_COMPLETE
|
||||
else:
|
||||
color = JourEval.COLOR_INCOMPLETE
|
||||
if e.date_debut > datetime.datetime.now(scu.TIME_ZONE):
|
||||
color = JourEval.COLOR_FUTUR
|
||||
|
||||
return f"background-color: {color};"
|
||||
|
||||
def _get_eval_title(self, e: Evaluation) -> str:
|
||||
heure_debut_txt, heure_fin_txt = "", ""
|
||||
if e.date_debut != e.date_fin:
|
||||
heure_debut_txt = (
|
||||
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 ""
|
||||
|
||||
title = f"{e.moduleimpl.module.titre_str()}"
|
||||
if e.description:
|
||||
title += f" : {e.description}"
|
||||
if heure_debut_txt:
|
||||
title += f" de {heure_debut_txt} à {heure_fin_txt}"
|
||||
|
||||
return title
|
||||
|
||||
|
||||
class CalendrierEval(sco_gen_cal.Calendrier):
|
||||
"""
|
||||
Représentation des évaluations d'un semestre dans un calendrier
|
||||
"""
|
||||
|
||||
def __init__(self, year: int, evals: list[Evaluation], nt: NotesTableCompat):
|
||||
# On prend du 01/09 au 31/08
|
||||
date_debut: datetime.datetime = datetime.datetime(year, 9, 1, 0, 0)
|
||||
date_fin: datetime.datetime = datetime.datetime(year + 1, 8, 31, 23, 59)
|
||||
super().__init__(date_debut, date_fin)
|
||||
|
||||
# évalutions du semestre
|
||||
self.evals: dict[datetime.date, list[Evaluation]] = {}
|
||||
for e in evals:
|
||||
if e.date_debut is not None:
|
||||
day = e.date_debut.date()
|
||||
if day not in self.evals:
|
||||
self.evals[day] = []
|
||||
self.evals[day].append(e)
|
||||
|
||||
self.nt: NotesTableCompat = nt
|
||||
|
||||
def instanciate_jour(self, date: datetime.date) -> JourEval:
|
||||
return JourEval(date, self.evals.get(date, []), parent=self)
|
||||
|
||||
|
||||
# View
|
||||
def formsemestre_evaluations_cal(formsemestre_id):
|
||||
"""Page avec calendrier de toutes les evaluations de ce semestre"""
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
|
@ -368,58 +471,9 @@ def formsemestre_evaluations_cal(formsemestre_id):
|
|||
evaluations = formsemestre.get_evaluations()
|
||||
nb_evals = len(evaluations)
|
||||
|
||||
color_incomplete = "#FF6060"
|
||||
color_complete = "#A0FFA0"
|
||||
color_futur = "#70E0FF"
|
||||
|
||||
year = formsemestre.annee_scolaire()
|
||||
events = {} # (day, halfday) : event
|
||||
for e in evaluations:
|
||||
if e.date_debut is None:
|
||||
continue # éval. sans date
|
||||
txt = e.moduleimpl.module.code or e.moduleimpl.module.abbrev or "éval."
|
||||
if e.date_debut == e.date_fin:
|
||||
heure_debut_txt, heure_fin_txt = "?", "?"
|
||||
else:
|
||||
heure_debut_txt = (
|
||||
e.date_debut.strftime(scu.TIME_FMT) if e.date_debut else "?"
|
||||
)
|
||||
heure_fin_txt = e.date_fin.strftime(scu.TIME_FMT) if e.date_fin else "?"
|
||||
|
||||
description = f"""{
|
||||
e.moduleimpl.module.titre
|
||||
}, de {heure_debut_txt} à {heure_fin_txt}"""
|
||||
|
||||
# Etat (notes completes) de l'évaluation:
|
||||
modimpl_result = nt.modimpls_results[e.moduleimpl.id]
|
||||
if modimpl_result.evaluations_etat[e.id].is_complete:
|
||||
color = color_complete
|
||||
else:
|
||||
color = color_incomplete
|
||||
if e.date_debut > datetime.datetime.now(scu.TIME_ZONE):
|
||||
color = color_futur
|
||||
href = url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=e.moduleimpl_id,
|
||||
)
|
||||
day = e.date_debut.date().isoformat() # yyyy-mm-dd
|
||||
event = events.get(day)
|
||||
if not event:
|
||||
events[day] = [day, txt, color, href, description, e.moduleimpl]
|
||||
else:
|
||||
if event[-1].id != e.moduleimpl.id:
|
||||
# plusieurs evals de modules differents a la meme date
|
||||
event[1] += ", " + txt
|
||||
event[4] += ", " + description
|
||||
if color == color_incomplete:
|
||||
event[2] = color_incomplete
|
||||
if color == color_futur:
|
||||
event[2] = color_futur
|
||||
|
||||
cal_html = sco_cal.YearTable(
|
||||
year, events=list(events.values()), halfday=False, pad_width=None
|
||||
)
|
||||
cal = CalendrierEval(year, evaluations, nt)
|
||||
cal_html = cal.get_html()
|
||||
|
||||
return f"""
|
||||
{
|
||||
|
@ -435,15 +489,15 @@ def formsemestre_evaluations_cal(formsemestre_id):
|
|||
</p>
|
||||
<ul>
|
||||
<li>en <span style=
|
||||
"background-color: {color_incomplete}">rouge</span>
|
||||
"background-color: {JourEval.COLOR_INCOMPLETE}">rouge</span>
|
||||
les évaluations passées auxquelles il manque des notes
|
||||
</li>
|
||||
<li>en <span style=
|
||||
"background-color: {color_complete}">vert</span>
|
||||
"background-color: {JourEval.COLOR_COMPLETE}">vert</span>
|
||||
les évaluations déjà notées
|
||||
</li>
|
||||
<li>en <span style=
|
||||
"background-color: {color_futur}">bleu</span>
|
||||
"background-color: {JourEval.COLOR_FUTUR}">bleu</span>
|
||||
les évaluations futures
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -541,7 +595,9 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, fmt="html"):
|
|||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=e.moduleimpl.id,
|
||||
),
|
||||
"module_titre": e.moduleimpl.module.abbrev or e.moduleimpl.module.titre,
|
||||
"module_titre": e.moduleimpl.module.abbrev
|
||||
or e.moduleimpl.module.titre
|
||||
or "",
|
||||
"responsable_id": e.moduleimpl.responsable_id,
|
||||
"responsable_nomplogin": sco_users.user_info(
|
||||
e.moduleimpl.responsable_id
|
||||
|
@ -579,6 +635,7 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, fmt="html"):
|
|||
base_url="%s?formsemestre_id=%s" % (request.base_url, formsemestre_id),
|
||||
origin=f"""Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}""",
|
||||
filename=scu.make_filename("evaluations_delais_" + formsemestre.titre_annee()),
|
||||
table_id="formsemestre_evaluations_delai_correction",
|
||||
)
|
||||
return tab.make_page(fmt=fmt)
|
||||
|
||||
|
|
|
@ -45,13 +45,17 @@ class ScoInvalidCSRF(ScoException):
|
|||
|
||||
|
||||
class ScoValueError(ScoException):
|
||||
"Exception avec page d'erreur utilisateur, et qui stoque dest_url"
|
||||
"""Exception avec page d'erreur utilisateur
|
||||
- dest_url : url où aller après la page d'erreur
|
||||
- safe (default False): si vrai, affiche le message non html quoté.
|
||||
"""
|
||||
|
||||
# mal nommée: super classe de toutes les exceptions avec page
|
||||
# d'erreur gentille.
|
||||
def __init__(self, msg, dest_url=None):
|
||||
def __init__(self, msg, dest_url=None, safe=False):
|
||||
super().__init__(msg)
|
||||
self.dest_url = dest_url
|
||||
self.safe = safe # utilisé par template sco_value_error.j2
|
||||
|
||||
|
||||
class ScoPermissionDenied(ScoValueError):
|
||||
|
@ -103,7 +107,7 @@ class ScoPDFFormatError(ScoValueError):
|
|||
super().__init__(
|
||||
f"""Erreur dans un format pdf:
|
||||
<p>{msg}</p>
|
||||
<p>Vérifiez les paramètres (polices de caractères, balisage)
|
||||
<p>Vérifiez les paramètres (polices de caractères, balisage, réglages bulletins...)
|
||||
dans les paramètres ou préférences.
|
||||
</p>
|
||||
""",
|
||||
|
|
|
@ -106,6 +106,7 @@ def _build_results_table(start_date=None, end_date=None, types_parcours=[]):
|
|||
html_class="table_leftalign",
|
||||
html_sortable=True,
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
table_id="export_result_table",
|
||||
)
|
||||
return tab, semlist
|
||||
|
||||
|
|
|
@ -32,8 +32,7 @@ from flask import url_for, g, request
|
|||
from flask_login import current_user
|
||||
|
||||
import app
|
||||
from app.models import Departement
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.models import Departement, Identite
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc import html_sco_header
|
||||
|
@ -55,7 +54,9 @@ def form_search_etud(
|
|||
"form recherche par nom"
|
||||
H = []
|
||||
H.append(
|
||||
f"""<form action="{ url_for("scolar.search_etud_in_dept", scodoc_dept=g.scodoc_dept) }" method="POST">
|
||||
f"""<form action="{
|
||||
url_for("scolar.search_etud_in_dept", scodoc_dept=g.scodoc_dept)
|
||||
}" method="POST">
|
||||
<b>{title}</b>
|
||||
<input type="text" name="expnom" class="in-expnom" width="12" spellcheck="false" value="">
|
||||
<input type="submit" value="Chercher">
|
||||
|
@ -100,9 +101,9 @@ def form_search_etud(
|
|||
return "\n".join(H)
|
||||
|
||||
|
||||
def search_etuds_infos_from_exp(expnom: str = "") -> list[dict]:
|
||||
def search_etuds_infos_from_exp(expnom: str = "") -> list[Identite]:
|
||||
"""Cherche étudiants, expnom peut être, dans cet ordre:
|
||||
un etudid (int), un code NIP, ou le début d'un nom.
|
||||
un etudid (int), un code NIP, ou une partie d'un nom (case insensitive).
|
||||
"""
|
||||
if not isinstance(expnom, int) and len(expnom) <= 1:
|
||||
return [] # si expnom est trop court, n'affiche rien
|
||||
|
@ -111,13 +112,22 @@ def search_etuds_infos_from_exp(expnom: str = "") -> list[dict]:
|
|||
except ValueError:
|
||||
etudid = None
|
||||
if etudid is not None:
|
||||
etuds = sco_etud.get_etud_info(filled=True, etudid=expnom)
|
||||
if len(etuds) == 1:
|
||||
return etuds
|
||||
etud = Identite.query.filter_by(dept_id=g.scodoc_dept_id, id=etudid).first()
|
||||
if etud:
|
||||
return [etud]
|
||||
expnom_str = str(expnom)
|
||||
if scu.is_valid_code_nip(expnom_str):
|
||||
return search_etuds_infos(code_nip=expnom_str)
|
||||
return search_etuds_infos(expnom=expnom_str)
|
||||
etuds = Identite.query.filter_by(
|
||||
dept_id=g.scodoc_dept_id, code_nip=expnom_str
|
||||
).all()
|
||||
if etuds:
|
||||
return etuds
|
||||
|
||||
return (
|
||||
Identite.query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
.filter(Identite.nom.op("~*")(expnom_str))
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
def search_etud_in_dept(expnom=""):
|
||||
|
@ -152,7 +162,7 @@ def search_etud_in_dept(expnom=""):
|
|||
|
||||
if len(etuds) == 1:
|
||||
# va directement a la fiche
|
||||
url_args["etudid"] = etuds[0]["etudid"]
|
||||
url_args["etudid"] = etuds[0].id
|
||||
return flask.redirect(url_for(endpoint, **url_args))
|
||||
|
||||
H = [
|
||||
|
@ -179,14 +189,39 @@ def search_etud_in_dept(expnom=""):
|
|||
)
|
||||
if len(etuds) > 0:
|
||||
# Choix dans la liste des résultats:
|
||||
rows = []
|
||||
e: Identite
|
||||
for e in etuds:
|
||||
url_args["etudid"] = e["etudid"]
|
||||
url_args["etudid"] = e.id
|
||||
target = url_for(endpoint, **url_args)
|
||||
e["_nomprenom_target"] = target
|
||||
e["inscription_target"] = target
|
||||
e["_nomprenom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"])
|
||||
sco_groups.etud_add_group_infos(
|
||||
e, e["cursem"]["formsemestre_id"] if e["cursem"] else None
|
||||
cur_inscription = e.inscription_courante()
|
||||
inscription = (
|
||||
e.inscription_descr().get("inscription_str", "")
|
||||
if cur_inscription
|
||||
else ""
|
||||
)
|
||||
groupes = (
|
||||
", ".join(
|
||||
gr.group_name
|
||||
for gr in sco_groups.get_etud_formsemestre_groups(
|
||||
e, cur_inscription.formsemestre
|
||||
)
|
||||
)
|
||||
if cur_inscription
|
||||
else ""
|
||||
)
|
||||
|
||||
rows.append(
|
||||
{
|
||||
"code_nip": e.code_nip or "",
|
||||
"etudid": e.id,
|
||||
"inscription": inscription,
|
||||
"inscription_target": target,
|
||||
"groupes": groupes,
|
||||
"nomprenom": e.nomprenom,
|
||||
"_nomprenom_target": target,
|
||||
"_nomprenom_td_attrs": f'id="{e.id}" class="etudinfo"',
|
||||
}
|
||||
)
|
||||
|
||||
tab = GenTable(
|
||||
|
@ -197,10 +232,11 @@ def search_etud_in_dept(expnom=""):
|
|||
"inscription": "Inscription",
|
||||
"groupes": "Groupes",
|
||||
},
|
||||
rows=etuds,
|
||||
rows=rows,
|
||||
html_sortable=True,
|
||||
html_class="table_leftalign",
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
table_id="search_etud_in_dept",
|
||||
)
|
||||
H.append(tab.html())
|
||||
if len(etuds) > 20: # si la page est grande
|
||||
|
@ -213,15 +249,16 @@ def search_etud_in_dept(expnom=""):
|
|||
)
|
||||
)
|
||||
else:
|
||||
H.append('<h2 style="color: red;">Aucun résultat pour "%s".</h2>' % expnom)
|
||||
H.append(f'<h2 style="color: red;">Aucun résultat pour "{expnom}".</h2>')
|
||||
H.append(
|
||||
"""<p class="help">La recherche porte sur tout ou partie du NOM ou du NIP de l'étudiant. Saisir au moins deux caractères.</p>"""
|
||||
"""<p class="help">La recherche porte sur tout ou partie du NOM ou du NIP
|
||||
de l'étudiant. Saisir au moins deux caractères.</p>"""
|
||||
)
|
||||
return "\n".join(H) + html_sco_header.sco_footer()
|
||||
|
||||
|
||||
# Was chercheEtudsInfo()
|
||||
def search_etuds_infos(expnom=None, code_nip=None):
|
||||
def search_etuds_infos(expnom=None, code_nip=None) -> list[dict]:
|
||||
"""recherche les étudiants correspondants à expnom ou au code_nip
|
||||
et ramene liste de mappings utilisables en DTML.
|
||||
"""
|
||||
|
@ -264,7 +301,7 @@ def search_etud_by_name(term: str) -> list:
|
|||
FROM identite
|
||||
WHERE
|
||||
dept_id = %(dept_id)s
|
||||
AND code_nip LIKE %(beginning)s
|
||||
AND code_nip ILIKE %(beginning)s
|
||||
ORDER BY nom
|
||||
""",
|
||||
{"beginning": term + "%", "dept_id": g.scodoc_dept_id},
|
||||
|
@ -283,7 +320,7 @@ def search_etud_by_name(term: str) -> list:
|
|||
FROM identite
|
||||
WHERE
|
||||
dept_id = %(dept_id)s
|
||||
AND nom LIKE %(beginning)s
|
||||
AND nom ILIKE %(beginning)s
|
||||
ORDER BY nom
|
||||
""",
|
||||
{"beginning": term + "%", "dept_id": g.scodoc_dept_id},
|
||||
|
@ -348,6 +385,7 @@ def table_etud_in_accessible_depts(expnom=None):
|
|||
rows=etuds,
|
||||
html_sortable=True,
|
||||
html_class="table_leftalign",
|
||||
table_id="etud_in_accessible_depts",
|
||||
)
|
||||
|
||||
H.append('<div class="table_etud_in_dept">')
|
||||
|
@ -383,13 +421,13 @@ def search_inscr_etud_by_nip(code_nip, fmt="json"):
|
|||
"""
|
||||
result, _ = search_etud_in_accessible_depts(code_nip=code_nip)
|
||||
|
||||
T = []
|
||||
rows = []
|
||||
for etuds in result:
|
||||
if etuds:
|
||||
dept_id = etuds[0]["dept"]
|
||||
for e in etuds:
|
||||
for sem in e["sems"]:
|
||||
T.append(
|
||||
rows.append(
|
||||
{
|
||||
"dept": dept_id,
|
||||
"etudid": e["etudid"],
|
||||
|
@ -414,6 +452,6 @@ def search_inscr_etud_by_nip(code_nip, fmt="json"):
|
|||
"date_debut_iso",
|
||||
"date_fin_iso",
|
||||
)
|
||||
tab = GenTable(columns_ids=columns_ids, rows=T)
|
||||
tab = GenTable(columns_ids=columns_ids, rows=rows, table_id="inscr_etud_by_nip")
|
||||
|
||||
return tab.make_page(fmt=fmt, with_html_headers=False, publish=True)
|
||||
|
|
|
@ -649,20 +649,20 @@ def formation_list_table(detail: bool) -> GenTable:
|
|||
"semestres_ues": "Semestres avec UEs",
|
||||
}
|
||||
return GenTable(
|
||||
columns_ids=columns_ids,
|
||||
rows=rows,
|
||||
titles=titles,
|
||||
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
|
||||
caption=title,
|
||||
html_caption=title,
|
||||
table_id="formation_list_table",
|
||||
html_class="formation_list_table table_leftalign",
|
||||
html_with_td_classes=True,
|
||||
html_sortable=True,
|
||||
base_url=f"{request.base_url}" + ("?detail=on" if detail else ""),
|
||||
caption=title,
|
||||
columns_ids=columns_ids,
|
||||
html_caption=title,
|
||||
html_class="formation_list_table table_leftalign",
|
||||
html_sortable=True,
|
||||
html_with_td_classes=True,
|
||||
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
|
||||
page_title=title,
|
||||
pdf_title=title,
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
rows=rows,
|
||||
table_id="formation_list_table",
|
||||
titles=titles,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -527,15 +527,16 @@ def table_formsemestres(
|
|||
preferences = sco_preferences.SemPreferences()
|
||||
tab = GenTable(
|
||||
columns_ids=columns_ids,
|
||||
rows=sems,
|
||||
titles=titles,
|
||||
html_class="table_leftalign",
|
||||
html_empty_element="<p><em>aucun résultat</em></p>",
|
||||
html_next_section=html_next_section,
|
||||
html_sortable=True,
|
||||
html_title=html_title,
|
||||
html_next_section=html_next_section,
|
||||
html_empty_element="<p><em>aucun résultat</em></p>",
|
||||
page_title="Semestres",
|
||||
preferences=preferences,
|
||||
rows=sems,
|
||||
table_id="table_formsemestres",
|
||||
titles=titles,
|
||||
)
|
||||
return tab
|
||||
|
||||
|
|
|
@ -1431,18 +1431,25 @@ Ceci n'est possible que si :
|
|||
|
||||
def formsemestre_delete2(formsemestre_id, dialog_confirmed=False):
|
||||
"""Delete a formsemestre (confirmation)"""
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
# Confirmation dialog
|
||||
if not dialog_confirmed:
|
||||
return scu.confirm_dialog(
|
||||
"""<h2>Vous voulez vraiment supprimer ce semestre ???</h2><p>(opération irréversible)</p>""",
|
||||
"""<h2>Vous voulez vraiment supprimer ce semestre ???</h2>
|
||||
<p>(opération irréversible)</p>
|
||||
""",
|
||||
dest_url="",
|
||||
cancel_url="formsemestre_status?formsemestre_id=%s" % formsemestre_id,
|
||||
parameters={"formsemestre_id": formsemestre_id},
|
||||
cancel_url=url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
),
|
||||
parameters={"formsemestre_id": formsemestre.id},
|
||||
)
|
||||
# Bon, s'il le faut...
|
||||
do_formsemestre_delete(formsemestre_id)
|
||||
do_formsemestre_delete(formsemestre.id)
|
||||
flash("Semestre supprimé !")
|
||||
return flask.redirect(scu.ScoURL())
|
||||
return flask.redirect(url_for("scolar.index_html", scodoc_dept=g.scodoc_dept))
|
||||
|
||||
|
||||
def formsemestre_has_decisions_or_compensations(
|
||||
|
|
|
@ -143,8 +143,10 @@ def _build_menu_stats(formsemestre: FormSemestre):
|
|||
]
|
||||
|
||||
|
||||
def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
|
||||
def formsemestre_status_menubar(formsemestre: FormSemestre | None) -> str:
|
||||
"""HTML to render menubar"""
|
||||
if formsemestre is None:
|
||||
return ""
|
||||
formsemestre_id = formsemestre.id
|
||||
if formsemestre.etat:
|
||||
change_lock_msg = "Verrouiller"
|
||||
|
@ -632,7 +634,7 @@ def formsemestre_description_table(
|
|||
"UE": modimpl.module.ue.acronyme,
|
||||
"_UE_td_attrs": ue_info.get("_UE_td_attrs", ""),
|
||||
"Code": modimpl.module.code or "",
|
||||
"Module": modimpl.module.abbrev or modimpl.module.titre,
|
||||
"Module": modimpl.module.abbrev or modimpl.module.titre or "",
|
||||
"_Module_class": "scotext",
|
||||
"Inscrits": mod_nb_inscrits,
|
||||
"Responsable": sco_users.user_info(modimpl.responsable_id)["nomprenom"],
|
||||
|
@ -724,20 +726,21 @@ def formsemestre_description_table(
|
|||
rows.append(sums)
|
||||
|
||||
return GenTable(
|
||||
columns_ids=columns_ids,
|
||||
rows=rows,
|
||||
titles=titles,
|
||||
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
|
||||
base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}&with_evals={with_evals}",
|
||||
caption=title,
|
||||
columns_ids=columns_ids,
|
||||
html_caption=title,
|
||||
html_class="table_leftalign formsemestre_description",
|
||||
base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}&with_evals={with_evals}",
|
||||
page_title=title,
|
||||
html_title=html_sco_header.html_sem_header(
|
||||
"Description du semestre", with_page_header=False
|
||||
),
|
||||
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
|
||||
page_title=title,
|
||||
pdf_title=title,
|
||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
rows=rows,
|
||||
table_id="formsemestre_description_table",
|
||||
titles=titles,
|
||||
)
|
||||
|
||||
|
||||
|
@ -819,6 +822,55 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
|
|||
</div>
|
||||
</div>
|
||||
<div class="sem-groups-assi">
|
||||
|
||||
"""
|
||||
)
|
||||
if can_edit_abs:
|
||||
H.append(
|
||||
f"""
|
||||
<div>
|
||||
<a class="stdlink" href="{
|
||||
url_for("assiduites.signal_assiduites_group",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
day=datetime.date.today().isoformat(),
|
||||
formsemestre_id=formsemestre.id,
|
||||
group_ids=group.id,
|
||||
)}">
|
||||
Saisir l'assiduité</a>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
# YYYY-Www (ISO 8601) :
|
||||
current_week: str = datetime.datetime.now().strftime("%G-W%V")
|
||||
H.append(
|
||||
f"""
|
||||
<div>
|
||||
<a class="stdlink" href="{
|
||||
url_for("assiduites.signal_assiduites_hebdo",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
group_ids=group.id,
|
||||
week=current_week,
|
||||
)}">Saisie hebdomadaire</a>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
if can_edit_abs:
|
||||
H.append(
|
||||
f"""
|
||||
<div>
|
||||
<a class="stdlink" href="{
|
||||
url_for("assiduites.bilan_dept",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
group_ids=group.id,
|
||||
)}">
|
||||
Justificatifs en attente</a>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
H.append(
|
||||
f"""
|
||||
<div>
|
||||
<a class="stdlink" href="{
|
||||
url_for("assiduites.visu_assi_group",
|
||||
|
@ -831,49 +883,6 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
|
|||
</div>
|
||||
"""
|
||||
)
|
||||
if can_edit_abs:
|
||||
H.append(
|
||||
f"""
|
||||
<div>
|
||||
<a class="stdlink" href="{
|
||||
url_for("assiduites.visu_assiduites_group",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
jour = datetime.date.today().isoformat(),
|
||||
group_ids=group.id,
|
||||
)}">
|
||||
Visualiser</a>
|
||||
</div>
|
||||
<div>
|
||||
<a class="stdlink" href="{
|
||||
url_for("assiduites.signal_assiduites_group",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
jour=datetime.date.today().isoformat(),
|
||||
formsemestre_id=formsemestre.id,
|
||||
group_ids=group.id,
|
||||
)}">
|
||||
Saisie journalière</a>
|
||||
</div>
|
||||
<div>
|
||||
<a class="stdlink" href="{
|
||||
url_for("assiduites.signal_assiduites_diff",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
group_ids=group.id,
|
||||
)}">
|
||||
Saisie différée</a>
|
||||
</div>
|
||||
<div>
|
||||
<a class="stdlink" href="{
|
||||
url_for("assiduites.bilan_dept",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
group_ids=group.id,
|
||||
)}">
|
||||
Justificatifs en attente</a>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
|
||||
H.append("</div>") # /sem-groups-assi
|
||||
if partition_is_empty:
|
||||
|
@ -1184,17 +1193,7 @@ def formsemestre_tableau_modules(
|
|||
mod_descr = "Module " + (mod.titre or "")
|
||||
is_apc = mod.is_apc() # SAE ou ressource
|
||||
if is_apc:
|
||||
coef_descr = ", ".join(
|
||||
[
|
||||
f"{ue.acronyme}: {co}"
|
||||
for ue, co in mod.ue_coefs_list()
|
||||
if isinstance(co, float) and co > 0
|
||||
]
|
||||
)
|
||||
if coef_descr:
|
||||
mod_descr += " Coefs: " + coef_descr
|
||||
else:
|
||||
mod_descr += " (pas de coefficients) "
|
||||
mod_descr += " " + mod.get_ue_coefs_descr()
|
||||
else:
|
||||
mod_descr += ", coef. " + str(mod.coefficient)
|
||||
mod_ens = sco_users.user_info(modimpl.responsable_id)["nomcomplet"]
|
||||
|
|
|
@ -116,7 +116,7 @@ def formsemestre_validation_etud_form(
|
|||
check = True
|
||||
|
||||
etud_d = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
Se = sco_cursus.get_situation_etud_cursus(etud_d, formsemestre_id)
|
||||
Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
|
||||
if not Se.sem["etat"]:
|
||||
raise ScoValueError("validation: semestre verrouille")
|
||||
|
||||
|
@ -262,8 +262,8 @@ def formsemestre_validation_etud_form(
|
|||
return "\n".join(H + footer)
|
||||
|
||||
# Infos si pas de semestre précédent
|
||||
if not Se.prev:
|
||||
if Se.sem["semestre_id"] == 1:
|
||||
if not Se.prev_formsemestre:
|
||||
if Se.cur_sem.semestre_id == 1:
|
||||
H.append("<p>Premier semestre (pas de précédent)</p>")
|
||||
else:
|
||||
H.append("<p>Pas de semestre précédent !</p>")
|
||||
|
@ -274,7 +274,7 @@ def formsemestre_validation_etud_form(
|
|||
f"""Le jury n'a pas statué sur le semestre précédent ! (<a href="{
|
||||
url_for("notes.formsemestre_validation_etud_form",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=Se.prev["formsemestre_id"],
|
||||
formsemestre_id=Se.prev_formsemestre.id,
|
||||
etudid=etudid)
|
||||
}">le faire maintenant</a>)
|
||||
"""
|
||||
|
@ -310,9 +310,9 @@ def formsemestre_validation_etud_form(
|
|||
H.append("</p>")
|
||||
|
||||
# Cas particulier pour ATJ: corriger precedent avant de continuer
|
||||
if Se.prev_decision and Se.prev_decision["code"] == ATJ:
|
||||
if Se.prev_formsemestre and Se.prev_decision and Se.prev_decision["code"] == ATJ:
|
||||
H.append(
|
||||
"""<div class="sfv_warning"><p>La décision du semestre précédent est en
|
||||
f"""<div class="sfv_warning"><p>La décision du semestre précédent est en
|
||||
<b>attente</b> à cause d\'un <b>problème d\'assiduité<b>.</p>
|
||||
<p>Vous devez la corriger avant de continuer ce jury. Soit vous considérez que le
|
||||
problème d'assiduité n'est pas réglé et choisissez de ne pas valider le semestre
|
||||
|
@ -320,14 +320,16 @@ def formsemestre_validation_etud_form(
|
|||
l'assiduité.</p>
|
||||
<form method="get" action="formsemestre_validation_etud_form">
|
||||
<input type="submit" value="Statuer sur le semestre précédent"/>
|
||||
<input type="hidden" name="formsemestre_id" value="%s"/>
|
||||
<input type="hidden" name="etudid" value="%s"/>
|
||||
<input type="hidden" name="desturl" value="formsemestre_validation_etud_form?etudid=%s&formsemestre_id=%s"/>
|
||||
<input type="hidden" name="formsemestre_id" value="{Se.prev_formsemestre.id}"/>
|
||||
<input type="hidden" name="etudid" value="{etudid}"/>
|
||||
<input type="hidden" name="desturl" value="{
|
||||
url_for("notes.formsemestre_validation_etud_form",
|
||||
etudid=etudid, formsemestre_id=formsemestre_id, scodoc_dept=g.scodoc_dept
|
||||
)}"/>
|
||||
"""
|
||||
% (Se.prev["formsemestre_id"], etudid, etudid, formsemestre_id)
|
||||
)
|
||||
if sortcol:
|
||||
H.append('<input type="hidden" name="sortcol" value="%s"/>' % sortcol)
|
||||
H.append(f"""<input type="hidden" name="sortcol" value="{sortcol}"/>""")
|
||||
H.append("</form></div>")
|
||||
|
||||
H.append(html_sco_header.sco_footer())
|
||||
|
@ -405,7 +407,7 @@ def formsemestre_validation_etud(
|
|||
sortcol=None,
|
||||
):
|
||||
"""Enregistre validation"""
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
etud = Identite.get_etud(etudid)
|
||||
Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
|
||||
# retrouve la decision correspondant au code:
|
||||
choices = Se.get_possible_choices(assiduite=True)
|
||||
|
@ -438,7 +440,7 @@ def formsemestre_validation_etud_manu(
|
|||
"""Enregistre validation"""
|
||||
if assidu:
|
||||
assidu = True
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
etud = Identite.get_etud(etudid)
|
||||
Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
|
||||
if code_etat in Se.parcours.UNUSED_CODES:
|
||||
raise ScoValueError("code decision invalide dans ce parcours")
|
||||
|
@ -494,32 +496,35 @@ def decisions_possible_rows(Se, assiduite, subtitle="", trclass=""):
|
|||
choices = Se.get_possible_choices(assiduite=assiduite)
|
||||
if not choices:
|
||||
return ""
|
||||
TitlePrev = ""
|
||||
if Se.prev:
|
||||
if Se.prev["semestre_id"] >= 0:
|
||||
TitlePrev = "%s%d" % (Se.parcours.SESSION_ABBRV, Se.prev["semestre_id"])
|
||||
prev_title = ""
|
||||
if Se.prev_formsemestre:
|
||||
if Se.prev_formsemestre.semestre_id >= 0:
|
||||
prev_title = "%s%d" % (
|
||||
Se.parcours.SESSION_ABBRV,
|
||||
Se.prev_formsemestre.semestre_id,
|
||||
)
|
||||
else:
|
||||
TitlePrev = "Prec."
|
||||
prev_title = "Prec."
|
||||
|
||||
if Se.sem["semestre_id"] >= 0:
|
||||
TitleCur = "%s%d" % (Se.parcours.SESSION_ABBRV, Se.sem["semestre_id"])
|
||||
if Se.cur_sem.semestre_id >= 0:
|
||||
cur_title = "%s%d" % (Se.parcours.SESSION_ABBRV, Se.cur_sem.semestre_id)
|
||||
else:
|
||||
TitleCur = Se.parcours.SESSION_NAME
|
||||
cur_title = Se.parcours.SESSION_NAME
|
||||
|
||||
H = [
|
||||
'<tr class="%s titles"><th class="sfv_subtitle">%s</em></th>'
|
||||
% (trclass, subtitle)
|
||||
]
|
||||
if Se.prev:
|
||||
H.append("<th>Code %s</th>" % TitlePrev)
|
||||
H.append("<th>Code %s</th><th>Devenir</th></tr>" % TitleCur)
|
||||
if Se.prev_formsemestre:
|
||||
H.append(f"<th>Code {prev_title}</th>")
|
||||
H.append(f"<th>Code {cur_title}</th><th>Devenir</th></tr>")
|
||||
for ch in choices:
|
||||
H.append(
|
||||
"""<tr class="%s"><td title="règle %s"><input type="radio" name="codechoice" value="%s" onClick="document.getElementById('subut').disabled=false;">"""
|
||||
% (trclass, ch.rule_id, ch.codechoice)
|
||||
)
|
||||
H.append("%s </input></td>" % ch.explication)
|
||||
if Se.prev:
|
||||
if Se.prev_formsemestre:
|
||||
H.append('<td class="centercell">%s</td>' % _dispcode(ch.new_code_prev))
|
||||
H.append(
|
||||
'<td class="centercell">%s</td><td>%s</td>'
|
||||
|
@ -535,7 +540,6 @@ def formsemestre_recap_parcours_table(
|
|||
etudid,
|
||||
with_links=False,
|
||||
with_all_columns=True,
|
||||
a_url="",
|
||||
sem_info=None,
|
||||
show_details=False,
|
||||
):
|
||||
|
@ -576,14 +580,14 @@ def formsemestre_recap_parcours_table(
|
|||
H.append("<th></th></tr>")
|
||||
|
||||
num_sem = 0
|
||||
for sem in situation_etud_cursus.get_semestres():
|
||||
is_prev = situation_etud_cursus.prev and (
|
||||
situation_etud_cursus.prev["formsemestre_id"] == sem["formsemestre_id"]
|
||||
for formsemestre in situation_etud_cursus.formsemestres:
|
||||
is_prev = situation_etud_cursus.prev_formsemestre and (
|
||||
situation_etud_cursus.prev_formsemestre.id == formsemestre.id
|
||||
)
|
||||
is_cur = situation_etud_cursus.formsemestre_id == sem["formsemestre_id"]
|
||||
is_cur = situation_etud_cursus.formsemestre_id == formsemestre.id
|
||||
num_sem += 1
|
||||
|
||||
dpv = sco_pv_dict.dict_pvjury(sem["formsemestre_id"], etudids=[etudid])
|
||||
dpv = sco_pv_dict.dict_pvjury(formsemestre.id, etudids=[etudid])
|
||||
pv = dpv["decisions"][0]
|
||||
decision_sem = pv["decision_sem"]
|
||||
decisions_ue = pv["decisions_ue"]
|
||||
|
@ -592,7 +596,6 @@ def formsemestre_recap_parcours_table(
|
|||
else:
|
||||
ass = ""
|
||||
|
||||
formsemestre = db.session.get(FormSemestre, sem["formsemestre_id"])
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
if is_cur:
|
||||
type_sem = "*" # now unused
|
||||
|
@ -603,20 +606,24 @@ def formsemestre_recap_parcours_table(
|
|||
else:
|
||||
type_sem = ""
|
||||
class_sem = "sem_autre"
|
||||
if sem["formation_code"] != situation_etud_cursus.formation.formation_code:
|
||||
if (
|
||||
formsemestre.formation.formation_code
|
||||
!= situation_etud_cursus.formation.formation_code
|
||||
):
|
||||
class_sem += " sem_autre_formation"
|
||||
if sem["bul_bgcolor"]:
|
||||
bgcolor = sem["bul_bgcolor"]
|
||||
else:
|
||||
bgcolor = "background-color: rgb(255,255,240)"
|
||||
bgcolor = (
|
||||
formsemestre.bul_bgcolor
|
||||
if formsemestre.bul_bgcolor
|
||||
else "background-color: rgb(255,255,240)"
|
||||
)
|
||||
# 1ere ligne: titre sem, decision, acronymes UE
|
||||
H.append('<tr class="%s rcp_l1 sem_%s">' % (class_sem, sem["formsemestre_id"]))
|
||||
H.append('<tr class="%s rcp_l1 sem_%s">' % (class_sem, formsemestre.id))
|
||||
if is_cur:
|
||||
pm = ""
|
||||
elif is_prev:
|
||||
pm = minuslink % sem["formsemestre_id"]
|
||||
pm = minuslink % formsemestre.id
|
||||
else:
|
||||
pm = plusminus % sem["formsemestre_id"]
|
||||
pm = plusminus % formsemestre.id
|
||||
|
||||
inscr = formsemestre.etuds_inscriptions.get(etudid)
|
||||
parcours_name = ""
|
||||
|
@ -638,9 +645,12 @@ def formsemestre_recap_parcours_table(
|
|||
H.append(
|
||||
f"""
|
||||
<td class="rcp_type_sem" style="background-color:{bgcolor};">{num_sem}{pm}</td>
|
||||
<td class="datedebut">{sem['mois_debut']}</td>
|
||||
<td class="datedebut">{formsemestre.mois_debut()}</td>
|
||||
<td class="rcp_titre_sem"><a class="formsemestre_status_link"
|
||||
href="{a_url}formsemestre_bulletinetud?formsemestre_id={formsemestre.id}&etudid={etudid}"
|
||||
href="{
|
||||
url_for("notes.formsemestre_bulletinetud", scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id, etudid=etudid
|
||||
)}"
|
||||
title="Bulletin de notes">{formsemestre.titre_annee()}{parcours_name}</a>
|
||||
"""
|
||||
)
|
||||
|
@ -675,7 +685,7 @@ def formsemestre_recap_parcours_table(
|
|||
ues = [
|
||||
ue
|
||||
for ue in ues
|
||||
if etud_est_inscrit_ue(cnx, etudid, sem["formsemestre_id"], ue.id)
|
||||
if etud_est_inscrit_ue(cnx, etudid, formsemestre.id, ue.id)
|
||||
or etud_ue_status[ue.id]["is_capitalized"]
|
||||
]
|
||||
|
||||
|
@ -697,7 +707,7 @@ def formsemestre_recap_parcours_table(
|
|||
H.append("<td></td>")
|
||||
H.append("</tr>")
|
||||
# 2eme ligne: notes
|
||||
H.append(f"""<tr class="{class_sem} rcp_l2 sem_{sem["formsemestre_id"]}">""")
|
||||
H.append(f"""<tr class="{class_sem} rcp_l2 sem_{formsemestre.id}">""")
|
||||
H.append(
|
||||
f"""<td class="rcp_type_sem"
|
||||
style="background-color:{bgcolor};"> </td>"""
|
||||
|
@ -706,21 +716,28 @@ def formsemestre_recap_parcours_table(
|
|||
default_sem_info = '<span class="fontred">[sem. précédent]</span>'
|
||||
else:
|
||||
default_sem_info = ""
|
||||
if not sem["etat"]: # locked
|
||||
if not formsemestre.etat: # locked
|
||||
lockicon = scu.icontag("lock32_img", title="verrouillé", border="0")
|
||||
default_sem_info += lockicon
|
||||
if sem["formation_code"] != situation_etud_cursus.formation.formation_code:
|
||||
default_sem_info += f"""Autre formation: {sem["formation_code"]}"""
|
||||
if (
|
||||
formsemestre.formation.formation_code
|
||||
!= situation_etud_cursus.formation.formation_code
|
||||
):
|
||||
default_sem_info += (
|
||||
f"""Autre formation: {formsemestre.formation.formation_code}"""
|
||||
)
|
||||
H.append(
|
||||
'<td class="datefin">%s</td><td class="sem_info">%s</td>'
|
||||
% (sem["mois_fin"], sem_info.get(sem["formsemestre_id"], default_sem_info))
|
||||
% (formsemestre.mois_fin(), sem_info.get(formsemestre.id, default_sem_info))
|
||||
)
|
||||
# Moy Gen (sous le code decision)
|
||||
H.append(
|
||||
f"""<td class="rcp_moy">{scu.fmt_note(nt.get_etud_moy_gen(etudid))}</td>"""
|
||||
)
|
||||
# Absences (nb d'abs non just. dans ce semestre)
|
||||
nbabsnj = sco_assiduites.get_assiduites_count(etudid, sem)[0]
|
||||
nbabsnj = sco_assiduites.formsemestre_get_assiduites_count(
|
||||
etudid, formsemestre
|
||||
)[0]
|
||||
H.append(f"""<td class="rcp_abs">{nbabsnj}</td>""")
|
||||
|
||||
# UEs
|
||||
|
@ -767,26 +784,30 @@ def formsemestre_recap_parcours_table(
|
|||
H.append("<td></td>")
|
||||
if with_links:
|
||||
H.append(
|
||||
'<td><a href="%sformsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s">modifier</a></td>'
|
||||
% (a_url, sem["formsemestre_id"], etudid)
|
||||
f"""<td><a class="stdlink" href="{
|
||||
url_for("notes.formsemestre_validation_etud_form", scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id, etudid=etudid
|
||||
)}">modifier</a></td>"""
|
||||
)
|
||||
|
||||
H.append("</tr>")
|
||||
# 3eme ligne: ECTS
|
||||
if (
|
||||
sco_preferences.get_preference("bul_show_ects", sem["formsemestre_id"])
|
||||
sco_preferences.get_preference("bul_show_ects", formsemestre.id)
|
||||
or nt.parcours.ECTS_ONLY
|
||||
):
|
||||
etud_ects_infos = nt.get_etud_ects_pot(etudid) # ECTS potentiels
|
||||
H.append(
|
||||
f"""<tr class="{class_sem} rcp_l2 sem_{sem["formsemestre_id"]}">
|
||||
f"""<tr class="{class_sem} rcp_l2 sem_{formsemestre.id}">
|
||||
<td class="rcp_type_sem" style="background-color:{bgcolor};"> </td>
|
||||
<td></td>"""
|
||||
)
|
||||
# Total ECTS (affiché sous la moyenne générale)
|
||||
H.append(
|
||||
f"""<td class="sem_ects_tit"><a title="crédit acquis">ECTS:</a></td>
|
||||
<td class="sem_ects">{pv.get("sum_ects",0):2.2g} / {etud_ects_infos["ects_total"]:2.2g}</td>
|
||||
<td class="sem_ects">{
|
||||
pv.get("sum_ects",0):2.2g} / {etud_ects_infos["ects_total"]:2.2g
|
||||
}</td>
|
||||
<td class="rcp_abs"></td>
|
||||
"""
|
||||
)
|
||||
|
@ -865,7 +886,7 @@ def form_decision_manuelle(Se, formsemestre_id, etudid, desturl="", sortcol=None
|
|||
# précédent n'est pas géré dans ScoDoc (code ADC_)
|
||||
# log(str(Se.sems))
|
||||
for sem in Se.sems:
|
||||
if sem["can_compensate"]:
|
||||
if sem["formsemestre_id"] in Se.can_compensate:
|
||||
H.append(
|
||||
'<option value="%s_%s">Admis par compensation avec S%s (%s)</option>'
|
||||
% (
|
||||
|
@ -882,7 +903,7 @@ def form_decision_manuelle(Se, formsemestre_id, etudid, desturl="", sortcol=None
|
|||
H.append("</select></td></tr>")
|
||||
|
||||
# Choix code semestre precedent:
|
||||
if Se.prev:
|
||||
if Se.prev_formsemestre:
|
||||
H.append(
|
||||
'<tr><td>Code semestre précédent: </td><td><select name="new_code_prev"><option value="">Choisir une décision...</option>'
|
||||
)
|
||||
|
@ -975,7 +996,7 @@ def do_formsemestre_validation_auto(formsemestre_id):
|
|||
conflicts = [] # liste des etudiants avec decision differente déjà saisie
|
||||
with sco_cache.DeferredSemCacheManager():
|
||||
for etudid in etudids:
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
etud = Identite.get_etud(etudid)
|
||||
Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
|
||||
ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
|
||||
{"etudid": etudid, "formsemestre_id": formsemestre_id}
|
||||
|
@ -984,7 +1005,7 @@ def do_formsemestre_validation_auto(formsemestre_id):
|
|||
# Conditions pour validation automatique:
|
||||
if ins["etat"] == scu.INSCRIT and (
|
||||
(
|
||||
(not Se.prev)
|
||||
(not Se.prev_formsemestre)
|
||||
or (
|
||||
Se.prev_decision and Se.prev_decision["code"] in (ADM, ADC, ADJ)
|
||||
)
|
||||
|
@ -1055,8 +1076,8 @@ def do_formsemestre_validation_auto(formsemestre_id):
|
|||
f"""<li><a href="{
|
||||
url_for('notes.formsemestre_validation_etud_form',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id,
|
||||
etudid=etud["etudid"], check=1)
|
||||
}">{etud["nomprenom"]}</li>"""
|
||||
etudid=etud.id, check=1)
|
||||
}">{etud_d["nomprenom"]}</li>"""
|
||||
)
|
||||
H.append("</ul>")
|
||||
H.append(
|
||||
|
|
214
app/scodoc/sco_gen_cal.py
Normal file
214
app/scodoc/sco_gen_cal.py
Normal file
|
@ -0,0 +1,214 @@
|
|||
"""
|
||||
Génération d'un calendrier
|
||||
(Classe abstraite à implémenter dans les classes filles)
|
||||
"""
|
||||
|
||||
import datetime
|
||||
from flask import render_template
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import g
|
||||
|
||||
|
||||
class Jour:
|
||||
"""
|
||||
Représente un jour dans le calendrier
|
||||
Permet d'obtenir les informations sur le jour
|
||||
et générer une représentation html
|
||||
"""
|
||||
|
||||
def __init__(self, date: datetime.date):
|
||||
self.date = date
|
||||
self.class_list: list[str] = []
|
||||
|
||||
if self.is_non_work():
|
||||
self.class_list.append("non-travail")
|
||||
if self.is_current_week():
|
||||
self.class_list.append("sem-courante")
|
||||
|
||||
def get_nom(self, short=True):
|
||||
"""
|
||||
Renvoie le nom du jour
|
||||
"M19" ou "Mer 19"
|
||||
|
||||
par défaut en version courte
|
||||
"""
|
||||
str_jour: str = scu.DAY_NAMES[self.date.weekday()].capitalize()
|
||||
return (
|
||||
f"{str_jour[0] if short or self.is_non_work() else str_jour[:3]+' '}"
|
||||
+ f"{self.date.day}"
|
||||
)
|
||||
|
||||
def is_non_work(self):
|
||||
"""
|
||||
Renvoie True si le jour est un jour non travaillé
|
||||
(en fonction de la préférence du département)
|
||||
"""
|
||||
return self.date.weekday() in scu.NonWorkDays.get_all_non_work_days(
|
||||
dept_id=g.scodoc_dept_id
|
||||
)
|
||||
|
||||
def is_current_week(self):
|
||||
"""
|
||||
Renvoie True si le jour est dans la semaine courante
|
||||
"""
|
||||
return self.date.isocalendar()[0:2] == datetime.date.today().isocalendar()[0:2]
|
||||
|
||||
def get_date(self, fmt=scu.DATE_FMT) -> str:
|
||||
"""
|
||||
Renvoie la date du jour au format fmt ou "dd/mm/yyyy" par défaut
|
||||
"""
|
||||
return self.date.strftime(fmt)
|
||||
|
||||
def get_html(self):
|
||||
"""
|
||||
Renvoie le code html du jour
|
||||
à surcharger dans les classes filles
|
||||
|
||||
l'html final ressemblera à :
|
||||
|
||||
<div class="jour {{jour.get_class()}}">
|
||||
<span class="nom">{{jour.get_nom()}}</span>
|
||||
<div class="contenu">
|
||||
{{jour.get_html() | safe}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
"""
|
||||
raise NotImplementedError("Méthode à implémenter dans les classes filles")
|
||||
|
||||
def get_class(self):
|
||||
"""
|
||||
Renvoie la classe css du jour
|
||||
|
||||
utilise self.class_list
|
||||
-> fait un join de la liste
|
||||
|
||||
"""
|
||||
return " ".join(self.class_list)
|
||||
|
||||
|
||||
class Calendrier:
|
||||
"""
|
||||
Représente un calendrier
|
||||
Permet d'obtenir les informations sur les jours
|
||||
et générer une représentation html
|
||||
|
||||
highlight: str
|
||||
-> ["jour", "semaine", "mois"]
|
||||
permet de mettre en valeur lors du passage de la souris
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
date_debut: datetime.date,
|
||||
date_fin: datetime.date,
|
||||
highlight: str = None,
|
||||
):
|
||||
self.date_debut = date_debut
|
||||
self.date_fin = date_fin
|
||||
self.jours: dict[str, list[Jour]] = {}
|
||||
self.highlight: str = highlight
|
||||
|
||||
def _get_dates_between(self) -> list[datetime.date]:
|
||||
"""
|
||||
get_dates_between Renvoie la liste des dates entre date_debut et date_fin
|
||||
|
||||
Returns:
|
||||
list[datetime.date]: liste des dates entre date_debut et date_fin
|
||||
"""
|
||||
resultat = []
|
||||
date_actuelle: datetime.date = self.date_debut
|
||||
while date_actuelle <= self.date_fin:
|
||||
if isinstance(date_actuelle, datetime.datetime):
|
||||
resultat.append(date_actuelle.date())
|
||||
elif isinstance(date_actuelle, datetime.date):
|
||||
resultat.append(date_actuelle)
|
||||
date_actuelle += datetime.timedelta(days=1)
|
||||
return resultat
|
||||
|
||||
def organize_by_month(self):
|
||||
"""
|
||||
Organise les jours par mois
|
||||
Instancie un objet Jour pour chaque jour
|
||||
|
||||
met à jour self.jours
|
||||
"""
|
||||
organized = {}
|
||||
for date in self._get_dates_between():
|
||||
# Récupérer le mois en français
|
||||
month = scu.MONTH_NAMES_ABBREV[date.month - 1]
|
||||
# Ajouter le jour à la liste correspondante au mois
|
||||
if month not in organized:
|
||||
organized[month] = {} # semaine {22: []}
|
||||
|
||||
jour: Jour = self.instanciate_jour(date)
|
||||
semaine = date.strftime("%G-W%V")
|
||||
if semaine not in organized[month]:
|
||||
organized[month][semaine] = []
|
||||
organized[month][semaine].append(jour)
|
||||
|
||||
self.jours = organized
|
||||
|
||||
def instanciate_jour(self, date: datetime.date) -> Jour:
|
||||
"""
|
||||
Instancie un objet Jour pour chaque jour
|
||||
A surcharger dans les classes filles si besoin
|
||||
"""
|
||||
raise NotImplementedError("Méthode à implémenter dans les classes filles")
|
||||
|
||||
def get_html(self):
|
||||
"""
|
||||
get_html Renvoie le code html du calendrier
|
||||
"""
|
||||
self.organize_by_month()
|
||||
return render_template(
|
||||
"calendrier.j2", calendrier=self.jours, highlight=self.highlight
|
||||
)
|
||||
|
||||
|
||||
class JourChoix(Jour):
|
||||
"""
|
||||
Représente un jour dans le calendrier pour choisir une date
|
||||
"""
|
||||
|
||||
def get_html(self):
|
||||
return ""
|
||||
|
||||
|
||||
class CalendrierChoix(Calendrier):
|
||||
"""
|
||||
Représente un calendrier pour choisir une date
|
||||
"""
|
||||
|
||||
def instanciate_jour(self, date: datetime.date) -> Jour:
|
||||
return JourChoix(date)
|
||||
|
||||
|
||||
def calendrier_choix_date(
|
||||
date_debut: datetime.date,
|
||||
date_fin: datetime.date,
|
||||
url: str,
|
||||
mode: str = "jour",
|
||||
titre: str = "Choisir une date",
|
||||
):
|
||||
"""
|
||||
Permet d'afficher un calendrier pour choisir une date et renvoyer sur une url.
|
||||
|
||||
mode : str
|
||||
- "jour" -> ajoutera "&day=yyyy-mm-dd" à l'url (ex: 2024-05-30)
|
||||
- "semaine" -> ajoutera "&week=yyyy-Www" à l'url (ex : 2024-W22)
|
||||
|
||||
titre : str
|
||||
- texte à afficher au dessus du calendrier
|
||||
"""
|
||||
|
||||
calendrier: CalendrierChoix = CalendrierChoix(date_debut, date_fin, highlight=mode)
|
||||
|
||||
return render_template(
|
||||
"choix_date.j2",
|
||||
calendrier=calendrier.get_html(),
|
||||
url=url,
|
||||
titre=titre,
|
||||
mode=mode,
|
||||
)
|
|
@ -466,9 +466,9 @@ def etud_add_group_infos(
|
|||
etud['groupes'] = "TDB, Gr2, TPB1"
|
||||
etud['partitionsgroupes'] = "Groupes TD:TDB, Groupes TP:Gr2 (...)"
|
||||
"""
|
||||
etud[
|
||||
"partitions"
|
||||
] = collections.OrderedDict() # partition_id : group + partition_name
|
||||
etud["partitions"] = (
|
||||
collections.OrderedDict()
|
||||
) # partition_id : group + partition_name
|
||||
if not formsemestre_id:
|
||||
etud["groupes"] = ""
|
||||
return etud
|
||||
|
@ -1409,20 +1409,16 @@ def groups_auto_repartition(partition: Partition):
|
|||
return flask.redirect(dest_url)
|
||||
|
||||
|
||||
def _get_prev_moy(etudid, formsemestre_id):
|
||||
def _get_prev_moy(etudid: int, formsemestre_id: int) -> float | str:
|
||||
"""Donne la derniere moyenne generale calculee pour cette étudiant,
|
||||
ou 0 si on n'en trouve pas (nouvel inscrit,...).
|
||||
"""
|
||||
info = sco_etud.get_etud_info(etudid=etudid, filled=True)
|
||||
if not info:
|
||||
raise ScoValueError("etudiant invalide: etudid=%s" % etudid)
|
||||
etud = info[0]
|
||||
etud = Identite.get_etud(etudid)
|
||||
Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
|
||||
if Se.prev:
|
||||
prev_sem = db.session.get(FormSemestre, Se.prev["formsemestre_id"])
|
||||
if Se.prev_formsemestre:
|
||||
prev_sem = db.session.get(FormSemestre, Se.prev_formsemestre.id)
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(prev_sem)
|
||||
return nt.get_etud_moy_gen(etudid)
|
||||
else:
|
||||
return nt.get_etud_moy_gen(etud.id)
|
||||
return 0.0
|
||||
|
||||
|
||||
|
|
|
@ -27,10 +27,8 @@
|
|||
|
||||
"""Exports groupes
|
||||
"""
|
||||
from flask import request
|
||||
|
||||
from app.scodoc import notesdb as ndb
|
||||
from app.scodoc import sco_excel
|
||||
from app.scodoc import sco_groups_view
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
|
@ -83,14 +81,13 @@ def groups_export_annotations(group_ids, formsemestre_id=None, fmt="html"):
|
|||
"date_str": "Date",
|
||||
"comment": "Annotation",
|
||||
},
|
||||
origin="Généré par %s le " % sco_version.SCONAME
|
||||
+ scu.timedate_human_repr()
|
||||
+ "",
|
||||
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
|
||||
page_title=f"Annotations sur les étudiants de {groups_infos.groups_titles}",
|
||||
caption="Annotations",
|
||||
base_url=groups_infos.base_url,
|
||||
html_sortable=True,
|
||||
html_class="table_leftalign",
|
||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
table_id="groups_export_annotations",
|
||||
)
|
||||
return table.make_page(fmt=fmt)
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
##############################################################################
|
||||
|
||||
"""Affichage étudiants d'un ou plusieurs groupes
|
||||
sous forme: de liste html (table exportable), de trombinoscope (exportable en pdf)
|
||||
sous forme: de liste html (table exportable), de trombinoscope (exportable en pdf)
|
||||
"""
|
||||
|
||||
# Re-ecriture en 2014 (re-organisation de l'interface, modernisation du code)
|
||||
|
@ -39,7 +39,7 @@ from flask import url_for, g, request
|
|||
from flask_login import current_user
|
||||
|
||||
from app import db
|
||||
from app.models import FormSemestre
|
||||
from app.models import FormSemestre, Identite
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_excel
|
||||
|
@ -661,6 +661,7 @@ def groups_table(
|
|||
text_fields_separator=prefs["moodle_csv_separator"],
|
||||
text_with_titles=prefs["moodle_csv_with_headerline"],
|
||||
preferences=prefs,
|
||||
table_id="groups_table",
|
||||
)
|
||||
#
|
||||
if fmt == "html":
|
||||
|
@ -861,21 +862,25 @@ def groups_table(
|
|||
# et ajoute infos inscription
|
||||
for m in groups_infos.members:
|
||||
etud_info = sco_etud.get_etud_info(m["etudid"], filled=True)[0]
|
||||
# TODO utiliser Identite
|
||||
etud = Identite.get_etud(m["etudid"])
|
||||
m.update(etud_info)
|
||||
sco_etud.etud_add_lycee_infos(etud_info)
|
||||
# et ajoute le parcours
|
||||
Se = sco_cursus.get_situation_etud_cursus(
|
||||
etud_info, groups_infos.formsemestre_id
|
||||
etud, groups_infos.formsemestre_id
|
||||
)
|
||||
m["parcours"] = Se.get_cursus_descr()
|
||||
m["code_cursus"], _ = sco_report.get_code_cursus_etud(
|
||||
etud_info["etudid"], sems=etud_info["sems"]
|
||||
etud.id, formsemestres=etud.get_formsemestres()
|
||||
)
|
||||
# TODO utiliser Identite:
|
||||
rows = [[m.get(k, "") for k in keys] for m in groups_infos.members]
|
||||
title = "etudiants_%s" % groups_infos.groups_filename
|
||||
title = f"etudiants_{groups_infos.groups_filename}"
|
||||
xls = sco_excel.excel_simple_table(titles=titles, lines=rows, sheet_name=title)
|
||||
filename = title
|
||||
return scu.send_file(xls, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE)
|
||||
return scu.send_file(
|
||||
xls, filename=title, suffix=scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE
|
||||
)
|
||||
else:
|
||||
raise ScoValueError("unsupported format")
|
||||
|
||||
|
@ -977,16 +982,16 @@ def form_choix_jour_saisie_hebdo(groups_infos, moduleimpl_id=None):
|
|||
if not authuser.has_permission(Permission.AbsChange):
|
||||
return ""
|
||||
return f"""
|
||||
<button onclick="window.location='{
|
||||
<a class="stdlink" href="{
|
||||
url_for(
|
||||
"assiduites.signal_assiduites_group",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
group_ids=",".join(map(str,groups_infos.group_ids)),
|
||||
jour=datetime.date.today().isoformat(),
|
||||
day=datetime.date.today().isoformat(),
|
||||
formsemestre_id=groups_infos.formsemestre_id,
|
||||
moduleimpl_id="" if moduleimpl_id is None else moduleimpl_id
|
||||
)
|
||||
}';">Saisie du jour ({datetime.date.today().strftime('%d/%m/%Y')})</button>
|
||||
}">Saisie du jour ({datetime.date.today().strftime('%d/%m/%Y')})</a>
|
||||
"""
|
||||
|
||||
|
||||
|
@ -997,16 +1002,16 @@ def form_choix_saisie_semaine(groups_infos):
|
|||
return ""
|
||||
query_args = parse_qs(request.query_string)
|
||||
moduleimpl_id = query_args.get("moduleimpl_id", [None])[0]
|
||||
semaine = datetime.date.today().isocalendar().week
|
||||
semaine = datetime.datetime.now().strftime("%G-W%V")
|
||||
return f"""
|
||||
<button onclick="window.location='{url_for(
|
||||
"assiduites.signal_assiduites_diff",
|
||||
<a class="stdlink" href="{url_for(
|
||||
"assiduites.signal_assiduites_hebdo",
|
||||
group_ids=",".join(map(str,groups_infos.group_ids)),
|
||||
semaine=semaine,
|
||||
week=semaine,
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=groups_infos.formsemestre_id,
|
||||
moduleimpl_id=moduleimpl_id
|
||||
)}';">Saisie à la semaine</button>
|
||||
)}">Saisie à la semaine (semaine {''.join(semaine[-2:])})</a>
|
||||
"""
|
||||
|
||||
|
||||
|
@ -1028,10 +1033,9 @@ def export_groups_as_moodle_csv(formsemestre_id=None):
|
|||
moodle_sem_name = sem["session_id"]
|
||||
|
||||
columns_ids = ("email", "semestre_groupe")
|
||||
T = []
|
||||
for partition_id in partitions_etud_groups:
|
||||
rows = []
|
||||
for partition_id, members in partitions_etud_groups.items():
|
||||
partition = sco_groups.get_partition(partition_id)
|
||||
members = partitions_etud_groups[partition_id]
|
||||
for etudid in members:
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
group_name = members[etudid]["group_name"]
|
||||
|
@ -1040,16 +1044,17 @@ def export_groups_as_moodle_csv(formsemestre_id=None):
|
|||
elts.append(partition["partition_name"])
|
||||
if group_name:
|
||||
elts.append(group_name)
|
||||
T.append({"email": etud["email"], "semestre_groupe": "-".join(elts)})
|
||||
rows.append({"email": etud["email"], "semestre_groupe": "-".join(elts)})
|
||||
# Make table
|
||||
prefs = sco_preferences.SemPreferences(formsemestre_id)
|
||||
tab = GenTable(
|
||||
rows=T,
|
||||
columns_ids=("email", "semestre_groupe"),
|
||||
filename=moodle_sem_name + "-moodle",
|
||||
titles={x: x for x in columns_ids},
|
||||
preferences=prefs,
|
||||
rows=rows,
|
||||
text_fields_separator=prefs["moodle_csv_separator"],
|
||||
text_with_titles=prefs["moodle_csv_with_headerline"],
|
||||
preferences=prefs,
|
||||
table_id="export_groups_as_moodle_csv",
|
||||
titles={x: x for x in columns_ids},
|
||||
)
|
||||
return tab.make_page(fmt="csv")
|
||||
|
|
|
@ -834,11 +834,12 @@ def adm_table_description_format():
|
|||
columns_ids = ("attribute", "type", "writable", "description", "aliases_str")
|
||||
|
||||
tab = GenTable(
|
||||
titles=titles,
|
||||
columns_ids=columns_ids,
|
||||
rows=list(Fmt.values()),
|
||||
html_sortable=True,
|
||||
html_class="table_leftalign",
|
||||
html_sortable=True,
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
rows=list(Fmt.values()),
|
||||
table_id="adm_table_description_format",
|
||||
titles=titles,
|
||||
)
|
||||
return tab
|
||||
|
|
|
@ -747,10 +747,11 @@ def etuds_select_box_xls(src_cat):
|
|||
else:
|
||||
e["paiementinscription_str"] = "-"
|
||||
tab = GenTable(
|
||||
titles=titles,
|
||||
columns_ids=columns_ids,
|
||||
rows=etuds,
|
||||
caption="%(title)s. %(help)s" % src_cat["infos"],
|
||||
columns_ids=columns_ids,
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
rows=etuds,
|
||||
table_id="etuds_select_box_xls",
|
||||
titles=titles,
|
||||
)
|
||||
return tab.excel() # tab.make_page(filename=src_cat["infos"]["filename"])
|
||||
|
|
|
@ -599,20 +599,21 @@ def _make_table_notes(
|
|||
)
|
||||
# display
|
||||
tab = GenTable(
|
||||
titles=titles,
|
||||
columns_ids=columns_ids,
|
||||
rows=rows,
|
||||
html_sortable=True,
|
||||
base_url=base_url,
|
||||
filename=filename,
|
||||
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
|
||||
caption=caption,
|
||||
html_next_section=html_next_section,
|
||||
page_title="Notes de " + formsemestre.titre_mois(),
|
||||
html_title=html_title,
|
||||
pdf_title=pdf_title,
|
||||
columns_ids=columns_ids,
|
||||
filename=filename,
|
||||
html_class="notes_evaluation",
|
||||
html_next_section=html_next_section,
|
||||
html_sortable=True,
|
||||
html_title=html_title,
|
||||
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
|
||||
page_title="Notes de " + formsemestre.titre_mois(),
|
||||
pdf_title=pdf_title,
|
||||
preferences=sco_preferences.SemPreferences(formsemestre.id),
|
||||
rows=rows,
|
||||
table_id="table-liste-notes",
|
||||
titles=titles,
|
||||
# html_generate_cells=False # la derniere ligne (moyennes) est incomplete
|
||||
)
|
||||
if fmt == "bordereau":
|
||||
|
|
|
@ -180,6 +180,7 @@ def _table_etuds_lycees(etuds, group_lycees, title, preferences, no_links=False)
|
|||
html_class="table_leftalign table_listegroupe",
|
||||
bottom_titles=bottom_titles,
|
||||
preferences=preferences,
|
||||
table_id="table_etuds_lycees",
|
||||
)
|
||||
return tab, etuds_by_lycee
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ def moduleimpl_evaluation_menu(evaluation: Evaluation, nbnotes: int = 0) -> str:
|
|||
can_edit_notes_ens = modimpl.can_edit_notes(current_user)
|
||||
|
||||
if can_edit_notes and nbnotes != 0:
|
||||
sup_label = "Supprimer évaluation impossible (il y a des notes)"
|
||||
sup_label = "Suppression évaluation impossible (il y a des notes)"
|
||||
else:
|
||||
sup_label = "Supprimer évaluation"
|
||||
|
||||
|
@ -146,29 +146,48 @@ def moduleimpl_evaluation_menu(evaluation: Evaluation, nbnotes: int = 0) -> str:
|
|||
return htmlutils.make_menu("actions", menu_eval, alone=True)
|
||||
|
||||
|
||||
def _ue_coefs_html(coefs_lst) -> str:
|
||||
def _ue_coefs_html(modimpl: ModuleImpl) -> str:
|
||||
""" """
|
||||
max_coef = max([x[1] for x in coefs_lst]) if coefs_lst else 1.0
|
||||
H = """
|
||||
coefs_lst = modimpl.module.ue_coefs_list()
|
||||
max_coef = max(x[1] for x in coefs_lst) if coefs_lst else 1.0
|
||||
H = f"""
|
||||
<div id="modimpl_coefs">
|
||||
<div>Coefficients vers les UE</div>
|
||||
<div>Coefficients vers les UEs
|
||||
<span><a class="stdlink" href="{
|
||||
url_for(
|
||||
"notes.edit_modules_ue_coefs",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formation_id=modimpl.module.formation.id,
|
||||
semestre_idx=modimpl.formsemestre.semestre_id,
|
||||
)
|
||||
}">détail</a>
|
||||
</span>
|
||||
</div>
|
||||
"""
|
||||
|
||||
if coefs_lst:
|
||||
H += (
|
||||
f"""
|
||||
<div class="coefs_histo" style="--max:{max_coef}">
|
||||
"""
|
||||
+ "\n".join(
|
||||
[
|
||||
f"""<div style="--coef:{coef};
|
||||
{'background-color: ' + ue.color + ';' if ue.color else ''}
|
||||
"><div>{coef}</div>{ue.acronyme}</div>"""
|
||||
for ue, coef in coefs_lst
|
||||
if coef > 0
|
||||
]
|
||||
)
|
||||
+ "</div>"
|
||||
H += _html_hinton_map(
|
||||
colors=(uc[0].color for uc in coefs_lst),
|
||||
max_val=max_coef,
|
||||
size=36,
|
||||
title=modimpl.module.get_ue_coefs_descr(),
|
||||
values=(uc[1] for uc in coefs_lst),
|
||||
)
|
||||
# (
|
||||
# f"""
|
||||
# <div class="coefs_histo" style="--max:{max_coef}">
|
||||
# """
|
||||
# + "\n".join(
|
||||
# [
|
||||
# f"""<div style="--coef:{coef};
|
||||
# {'background-color: ' + ue.color + ';' if ue.color else ''}
|
||||
# "><div>{coef}</div>{ue.acronyme}</div>"""
|
||||
# for ue, coef in coefs_lst
|
||||
# if coef > 0
|
||||
# ]
|
||||
# )
|
||||
# + "</div>"
|
||||
# )
|
||||
else:
|
||||
H += """<div class="missing_value">non définis</div>"""
|
||||
H += "</div>"
|
||||
|
@ -195,12 +214,21 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||
Evaluation.date_debut.desc(),
|
||||
).all()
|
||||
nb_evaluations = len(evaluations)
|
||||
max_poids = max(
|
||||
# Le poids max pour chaque catégorie d'évaluation
|
||||
max_poids_by_type: dict[int, float] = {}
|
||||
for eval_type in (
|
||||
Evaluation.EVALUATION_NORMALE,
|
||||
Evaluation.EVALUATION_RATTRAPAGE,
|
||||
Evaluation.EVALUATION_SESSION2,
|
||||
Evaluation.EVALUATION_BONUS,
|
||||
):
|
||||
max_poids_by_type[eval_type] = max(
|
||||
[
|
||||
max([p.poids for p in e.ue_poids] or [0]) * (e.coefficient or 0.0)
|
||||
for e in evaluations
|
||||
if e.evaluation_type == eval_type
|
||||
]
|
||||
or [0]
|
||||
or [0.0]
|
||||
)
|
||||
#
|
||||
sem_locked = not formsemestre.etat
|
||||
|
@ -265,7 +293,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||
H.append(scu.icontag("lock32_img", title="verrouillé"))
|
||||
H.append("""</td><td class="fichetitre2">""")
|
||||
if modimpl.module.is_apc():
|
||||
H.append(_ue_coefs_html(modimpl.module.ue_coefs_list()))
|
||||
H.append(_ue_coefs_html(modimpl))
|
||||
else:
|
||||
H.append(
|
||||
f"""Coef. dans le semestre: {
|
||||
|
@ -318,12 +346,28 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||
f"""
|
||||
<span class="moduleimpl_abs_link"><a class="stdlink" href="{
|
||||
url_for("assiduites.signal_assiduites_group", scodoc_dept=g.scodoc_dept)
|
||||
}?group_ids={group_id}&jour={
|
||||
}?group_ids={group_id}&day={
|
||||
datetime.date.today().isoformat()
|
||||
}&formsemestre_id={formsemestre.id}
|
||||
&moduleimpl_id={moduleimpl_id}
|
||||
"
|
||||
>Saisie Absences journée</a></span>
|
||||
>Saisie Absences</a></span>
|
||||
"""
|
||||
)
|
||||
current_week: str = datetime.datetime.now().strftime("%G-W%V")
|
||||
H.append(
|
||||
f"""
|
||||
<span class="moduleimpl_abs_link"><a class="stdlink" href="{
|
||||
url_for("assiduites.signal_assiduites_hebdo",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
group_ids=group_id,
|
||||
week=current_week,
|
||||
moduleimpl_id=moduleimpl_id
|
||||
)
|
||||
}
|
||||
"
|
||||
>Saisie Absences (Hebdo)</a></span>
|
||||
"""
|
||||
)
|
||||
H.append(
|
||||
|
@ -335,8 +379,8 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||
group_ids=group_id,
|
||||
formsemestre_id=formsemestre.id,
|
||||
moduleimpl_id="" if moduleimpl_id is None else moduleimpl_id
|
||||
)}"
|
||||
>Saisie Absences Différée</a></span>
|
||||
)}" title="Page en cours de fusion et sera prochainement supprimée. Veuillez utiliser la page `Saisie Absences`"
|
||||
>(Saisie Absences Différée)</a></span>
|
||||
"""
|
||||
)
|
||||
|
||||
|
@ -344,9 +388,34 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||
#
|
||||
if not modimpl.check_apc_conformity(nt):
|
||||
H.append(
|
||||
"""<div class="warning conformite">Les poids des évaluations de ce module ne sont
|
||||
pas encore conformes au PN.
|
||||
Ses notes ne peuvent pas être prises en compte dans les moyennes d'UE.
|
||||
"""<div class="warning conformite">Les poids des évaluations de ce
|
||||
module ne permettent pas d'évaluer toutes les UEs (compétences)
|
||||
prévues par les coefficients du programme.
|
||||
<b>Ses notes ne peuvent pas être prises en compte dans les moyennes d'UE.</b>
|
||||
Vérifiez les poids des évaluations.
|
||||
</div>"""
|
||||
)
|
||||
if not modimpl.check_apc_conformity(
|
||||
nt, evaluation_type=Evaluation.EVALUATION_SESSION2
|
||||
):
|
||||
H.append(
|
||||
"""<div class="warning conformite">
|
||||
Il y a des évaluations de <b>deuxième session</b>
|
||||
mais leurs poids ne permettent pas d'évaluer toutes les UEs (compétences)
|
||||
prévues par les coefficients du programme.
|
||||
La deuxième session ne sera donc <b>pas prise en compte</b>.
|
||||
Vérifiez les poids de ces évaluations.
|
||||
</div>"""
|
||||
)
|
||||
if not modimpl.check_apc_conformity(
|
||||
nt, evaluation_type=Evaluation.EVALUATION_RATTRAPAGE
|
||||
):
|
||||
H.append(
|
||||
"""<div class="warning conformite">
|
||||
Il y a des évaluations de <b>rattrapage</b>
|
||||
mais leurs poids n'évaluent pas toutes les UEs (compétences)
|
||||
prévues par les coefficients du programme.
|
||||
Vérifiez les poids de ces évaluations.
|
||||
</div>"""
|
||||
)
|
||||
|
||||
|
@ -437,7 +506,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||
eval_index=eval_index,
|
||||
nb_evals=nb_evaluations,
|
||||
is_apc=nt.is_apc,
|
||||
max_poids=max_poids,
|
||||
max_poids=max_poids_by_type.get(evaluation.evaluation_type, 10000.0),
|
||||
)
|
||||
)
|
||||
eval_index -= 1
|
||||
|
@ -756,27 +825,27 @@ def _ligne_evaluation(
|
|||
#
|
||||
if etat["nb_notes"] == 0:
|
||||
H.append(f"""<tr class="{tr_class}"><td></td>""")
|
||||
if modimpl.module.is_apc():
|
||||
H.append(
|
||||
f"""<td colspan="8" class="eval_poids">{
|
||||
evaluation.get_ue_poids_str()}</td>"""
|
||||
)
|
||||
else:
|
||||
H.append('<td colspan="8"></td>')
|
||||
# if modimpl.module.is_apc():
|
||||
# H.append(
|
||||
# f"""<td colspan="8" class="eval_poids">{
|
||||
# evaluation.get_ue_poids_str()}</td>"""
|
||||
# )
|
||||
# else:
|
||||
# H.append('<td colspan="8"></td>')
|
||||
H.append("""</tr>""")
|
||||
else: # il y a deja des notes saisies
|
||||
gr_moyennes = etat["gr_moyennes"]
|
||||
first_group = True
|
||||
# first_group = True
|
||||
for gr_moyenne in gr_moyennes:
|
||||
H.append(f"""<tr class="{tr_class}"><td> </td>""")
|
||||
if first_group and modimpl.module.is_apc():
|
||||
H.append(
|
||||
f"""<td class="eval_poids" colspan="4">{
|
||||
evaluation.get_ue_poids_str()}</td>"""
|
||||
)
|
||||
else:
|
||||
# if first_group and modimpl.module.is_apc():
|
||||
# H.append(
|
||||
# f"""<td class="eval_poids" colspan="4">{
|
||||
# evaluation.get_ue_poids_str()}</td>"""
|
||||
# )
|
||||
# else:
|
||||
H.append("""<td colspan="4"></td>""")
|
||||
first_group = False
|
||||
# first_group = False
|
||||
if gr_moyenne["group_name"] is None:
|
||||
name = "Tous" # tous
|
||||
else:
|
||||
|
@ -832,26 +901,47 @@ def _evaluation_poids_html(evaluation: Evaluation, max_poids: float = 0.0) -> st
|
|||
ue_poids = evaluation.get_ue_poids_dict(sort=True) # { ue_id : poids }
|
||||
if not ue_poids:
|
||||
return ""
|
||||
if max_poids < scu.NOTES_PRECISION:
|
||||
values = [poids * (evaluation.coefficient) for poids in ue_poids.values()]
|
||||
colors = [db.session.get(UniteEns, ue_id).color for ue_id in ue_poids]
|
||||
return _html_hinton_map(
|
||||
classes=("evaluation_poids",),
|
||||
colors=colors,
|
||||
max_val=max_poids,
|
||||
title=f"Poids de l'évaluation vers les UEs: {evaluation.get_ue_poids_str()}",
|
||||
values=values,
|
||||
)
|
||||
|
||||
|
||||
def _html_hinton_map(
|
||||
classes=(),
|
||||
colors=(),
|
||||
max_val: float | None = None,
|
||||
size=12,
|
||||
title: str = "",
|
||||
values=(),
|
||||
) -> str:
|
||||
"""Représente une liste de nombres sous forme de carrés"""
|
||||
if max_val is None:
|
||||
max_val = max(values)
|
||||
if max_val < scu.NOTES_PRECISION:
|
||||
return ""
|
||||
H = (
|
||||
"""<div class="evaluation_poids">"""
|
||||
return (
|
||||
f"""<div class="hinton_map {" ".join(classes)}"
|
||||
style="--size:{size}px;"
|
||||
title="{title}"
|
||||
data-tooltip>"""
|
||||
+ "\n".join(
|
||||
[
|
||||
f"""<div title="poids vers {ue.acronyme}: {poids:g}">
|
||||
<div style="--size:{math.sqrt(poids*(evaluation.coefficient)/max_poids*144)}px;
|
||||
{'background-color: ' + ue.color + ';' if ue.color else ''}
|
||||
f"""<div>
|
||||
<div style="--boxsize:{size*math.sqrt(value/max_val)}px;
|
||||
{'background-color: ' + color + ';' if color else ''}
|
||||
"></div>
|
||||
</div>"""
|
||||
for ue, poids in (
|
||||
(db.session.get(UniteEns, ue_id), poids)
|
||||
for ue_id, poids in ue_poids.items()
|
||||
)
|
||||
for value, color in zip(values, colors)
|
||||
]
|
||||
)
|
||||
+ "</div>"
|
||||
)
|
||||
return H
|
||||
|
||||
|
||||
def _html_modimpl_etuds_attente(res: ResultatsSemestre, modimpl: ModuleImpl) -> str:
|
||||
|
|
|
@ -180,7 +180,7 @@ def fiche_etud(etudid=None):
|
|||
)
|
||||
else:
|
||||
info["etat_civil"] = ""
|
||||
info["ScoURL"] = scu.ScoURL()
|
||||
info["ScoURL"] = url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
|
||||
info["authuser"] = current_user
|
||||
if restrict_etud_data:
|
||||
info["info_naissance"] = ""
|
||||
|
@ -264,14 +264,13 @@ def fiche_etud(etudid=None):
|
|||
sem_info[formsemestre.id] = grlink
|
||||
|
||||
if inscriptions:
|
||||
Se = sco_cursus.get_situation_etud_cursus(info, info["last_formsemestre_id"])
|
||||
Se = sco_cursus.get_situation_etud_cursus(etud, info["last_formsemestre_id"])
|
||||
info["liste_inscriptions"] = formsemestre_recap_parcours_table(
|
||||
Se,
|
||||
etudid,
|
||||
with_links=False,
|
||||
sem_info=sem_info,
|
||||
with_all_columns=False,
|
||||
a_url="Notes/",
|
||||
)
|
||||
info["link_bul_pdf"] = (
|
||||
"""<span class="link_bul_pdf fontred">PDF interdits par l'admin.</span>"""
|
||||
|
|
|
@ -247,6 +247,7 @@ class ScoDocPageTemplate(PageTemplate):
|
|||
footer_template=DEFAULT_PDF_FOOTER_TEMPLATE,
|
||||
filigranne=None,
|
||||
preferences=None, # dictionnary with preferences, required
|
||||
with_page_numbers=False,
|
||||
):
|
||||
"""Initialise our page template."""
|
||||
# defered import (solve circular dependency ->sco_logo ->scodoc, ->sco_pdf
|
||||
|
@ -259,8 +260,9 @@ class ScoDocPageTemplate(PageTemplate):
|
|||
self.pdfmeta_subject = subject
|
||||
self.server_name = server_name
|
||||
self.filigranne = filigranne
|
||||
self.page_number = 1
|
||||
self.footer_template = footer_template
|
||||
self.with_page_numbers = with_page_numbers
|
||||
self.page_number = 1
|
||||
if self.preferences:
|
||||
self.with_page_background = self.preferences["bul_pdf_with_background"]
|
||||
else:
|
||||
|
@ -337,6 +339,7 @@ class ScoDocPageTemplate(PageTemplate):
|
|||
|
||||
def draw_footer(self, canv, content):
|
||||
"""Print the footer"""
|
||||
# called 1/page
|
||||
try:
|
||||
canv.setFont(
|
||||
self.preferences["SCOLAR_FONT"],
|
||||
|
@ -351,8 +354,11 @@ class ScoDocPageTemplate(PageTemplate):
|
|||
canv.drawString(
|
||||
self.preferences["pdf_footer_x"] * mm,
|
||||
self.preferences["pdf_footer_y"] * mm,
|
||||
content,
|
||||
content + " " + (self.preferences["pdf_footer_extra"] or ""),
|
||||
)
|
||||
if self.with_page_numbers:
|
||||
canv.drawString(190.0 * mm, 6 * mm, f"Page {self.page_number}")
|
||||
|
||||
canv.restoreState()
|
||||
|
||||
def footer_string(self) -> str:
|
||||
|
@ -382,18 +388,14 @@ class ScoDocPageTemplate(PageTemplate):
|
|||
filigranne = self.filigranne.get(doc.page, None)
|
||||
if filigranne:
|
||||
canv.saveState()
|
||||
canv.translate(9 * cm, 27.6 * cm)
|
||||
canv.rotate(30)
|
||||
canv.scale(4.5, 4.5)
|
||||
canv.translate(10 * cm, 21.0 * cm)
|
||||
canv.rotate(36)
|
||||
canv.scale(7, 7)
|
||||
canv.setFillColorRGB(1.0, 0.65, 0.65, alpha=0.6)
|
||||
canv.drawRightString(0, 0, SU(filigranne))
|
||||
canv.drawCentredString(0, 0, SU(filigranne))
|
||||
canv.restoreState()
|
||||
doc.filigranne = None
|
||||
|
||||
def afterPage(self):
|
||||
"""Called after all flowables have been drawn on a page.
|
||||
Increment pageNum since the page has been completed.
|
||||
"""
|
||||
# Increment page number
|
||||
self.page_number += 1
|
||||
|
||||
|
||||
|
@ -458,7 +460,12 @@ def pdf_basic_page(
|
|||
if title:
|
||||
head = Paragraph(SU(title), StyleSheet["Heading3"])
|
||||
objects = [head] + objects
|
||||
|
||||
try:
|
||||
document.build(objects)
|
||||
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
|
||||
raise ScoPDFFormatError(str(exc)) from exc
|
||||
|
||||
data = report.getvalue()
|
||||
return data
|
||||
|
||||
|
|
|
@ -96,13 +96,16 @@ def photo_portal_url(code_nip: str):
|
|||
return None
|
||||
|
||||
|
||||
def get_etud_photo_url(etudid, size="small"):
|
||||
def get_etud_photo_url(etudid, size="small", seed=None):
|
||||
"L'URL scodoc vers la photo de l'étudiant"
|
||||
kwargs = {"seed": seed} if seed else {}
|
||||
return (
|
||||
url_for(
|
||||
"scolar.get_photo_image",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=etudid,
|
||||
size=size,
|
||||
**kwargs,
|
||||
)
|
||||
if has_request_context()
|
||||
else ""
|
||||
|
@ -114,9 +117,11 @@ def etud_photo_url(etud: dict, size="small", fast=False) -> str:
|
|||
If ScoDoc doesn't have an image and a portal is configured, link to it.
|
||||
|
||||
"""
|
||||
photo_url = get_etud_photo_url(etud["etudid"], size=size)
|
||||
if fast:
|
||||
return photo_url
|
||||
return get_etud_photo_url(etud["etudid"], size=size)
|
||||
photo_url = get_etud_photo_url(
|
||||
etud["etudid"], size=size, seed=hash(etud.get("photo_filename"))
|
||||
)
|
||||
path = photo_pathname(etud["photo_filename"], size=size)
|
||||
if not path:
|
||||
# Portail ?
|
||||
|
@ -374,7 +379,15 @@ def copy_portal_photo_to_fs(etudid: int):
|
|||
portal_timeout = sco_preferences.get_preference("portal_timeout")
|
||||
error_message = None
|
||||
try:
|
||||
r = requests.get(url, timeout=portal_timeout)
|
||||
r = requests.get(
|
||||
url,
|
||||
timeout=portal_timeout,
|
||||
params={
|
||||
"nom": etud.nom or "",
|
||||
"prenom": etud.prenom or "",
|
||||
"civilite": etud.civilite,
|
||||
},
|
||||
)
|
||||
except requests.ConnectionError:
|
||||
error_message = "ConnectionError"
|
||||
except requests.Timeout:
|
||||
|
|
|
@ -378,6 +378,7 @@ class PlacementRunner:
|
|||
preferences=sco_preferences.SemPreferences(
|
||||
self.moduleimpl_data["formsemestre_id"]
|
||||
),
|
||||
table_id="placement_pdf",
|
||||
)
|
||||
return tab.make_page(fmt="pdf", with_html_headers=False)
|
||||
|
||||
|
|
|
@ -221,6 +221,7 @@ def formsemestre_poursuite_report(formsemestre_id, fmt="html"):
|
|||
html_class="table_leftalign table_listegroupe",
|
||||
pdf_link=False, # pas d'export pdf
|
||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
table_id="formsemestre_poursuite_report",
|
||||
)
|
||||
tab.filename = scu.make_filename("poursuite " + sem["titreannee"])
|
||||
|
||||
|
|
|
@ -110,6 +110,7 @@ get_base_preferences(formsemestre_id)
|
|||
Return base preferences for current scodoc_dept (instance BasePreferences)
|
||||
|
||||
"""
|
||||
|
||||
import flask
|
||||
from flask import current_app, flash, g, request, url_for
|
||||
|
||||
|
@ -611,26 +612,15 @@ class BasePreferences:
|
|||
"explanation": "toute saisie d'absence doit indiquer le module concerné",
|
||||
},
|
||||
),
|
||||
# (
|
||||
# "forcer_present",
|
||||
# {
|
||||
# "initvalue": 0,
|
||||
# "title": "Forcer l'appel des présents",
|
||||
# "input_type": "boolcheckbox",
|
||||
# "labels": ["non", "oui"],
|
||||
# "category": "assi",
|
||||
# },
|
||||
# ),
|
||||
(
|
||||
"periode_defaut",
|
||||
"non_present",
|
||||
{
|
||||
"initvalue": 2.0,
|
||||
"size": 10,
|
||||
"title": "Durée par défaut d'un créneau",
|
||||
"type": "float",
|
||||
"initvalue": 0,
|
||||
"title": "Désactiver la saisie des présences",
|
||||
"input_type": "boolcheckbox",
|
||||
"labels": ["non", "oui"],
|
||||
"category": "assi",
|
||||
"only_global": True,
|
||||
"explanation": "Durée d'un créneau en heure. Utilisé dans les pages de saisie",
|
||||
"explanation": "Désactive la saisie et l'affichage des présences",
|
||||
},
|
||||
),
|
||||
(
|
||||
|
@ -644,18 +634,18 @@ class BasePreferences:
|
|||
"category": "assi",
|
||||
},
|
||||
),
|
||||
(
|
||||
"assi_etat_defaut",
|
||||
{
|
||||
"explanation": "⚠ non fonctionnel, travaux en cours !",
|
||||
"initvalue": "aucun",
|
||||
"input_type": "menu",
|
||||
"labels": ["aucun", "present", "retard", "absent"],
|
||||
"allowed_values": ["aucun", "present", "retard", "absent"],
|
||||
"title": "Définir l'état par défaut",
|
||||
"category": "assi",
|
||||
},
|
||||
),
|
||||
# (
|
||||
# "assi_etat_defaut",
|
||||
# {
|
||||
# "explanation": "⚠ non fonctionnel, travaux en cours !",
|
||||
# "initvalue": "aucun",
|
||||
# "input_type": "menu",
|
||||
# "labels": ["aucun", "present", "retard", "absent"],
|
||||
# "allowed_values": ["aucun", "present", "retard", "absent"],
|
||||
# "title": "Définir l'état par défaut",
|
||||
# "category": "assi",
|
||||
# },
|
||||
# ),
|
||||
(
|
||||
"non_travail",
|
||||
{
|
||||
|
@ -962,6 +952,16 @@ class BasePreferences:
|
|||
"category": "pdf",
|
||||
},
|
||||
),
|
||||
(
|
||||
"pdf_footer_extra",
|
||||
{
|
||||
"initvalue": "",
|
||||
"title": "Texte à ajouter en pied de page",
|
||||
"explanation": "sur tous les documents, par exemple vos coordonnées, ...",
|
||||
"size": 78,
|
||||
"category": "pdf",
|
||||
},
|
||||
),
|
||||
(
|
||||
"pdf_footer_x",
|
||||
{
|
||||
|
@ -2260,16 +2260,17 @@ class BasePreferences:
|
|||
before_table="<details><summary>{title}</summary>",
|
||||
after_table="</details>",
|
||||
)
|
||||
dest_url = url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
|
||||
if tf[0] == 0:
|
||||
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
|
||||
elif tf[0] == -1:
|
||||
return flask.redirect(scu.ScoURL()) # cancel
|
||||
else:
|
||||
if tf[0] == -1:
|
||||
return flask.redirect(dest_url) # cancel
|
||||
#
|
||||
for pref in self.prefs_definition:
|
||||
self.prefs[None][pref[0]] = tf[2][pref[0]]
|
||||
self.save()
|
||||
flash("Préférences modifiées")
|
||||
return flask.redirect(scu.ScoURL())
|
||||
return flask.redirect(dest_url)
|
||||
|
||||
def build_tf_form(self, categories: list[str] = None, formsemestre_id: int = None):
|
||||
"""Build list of elements for TrivialFormulator.
|
||||
|
@ -2290,9 +2291,7 @@ class BasePreferences:
|
|||
if "explanation" in descr:
|
||||
del descr["explanation"]
|
||||
if formsemestre_id:
|
||||
descr[
|
||||
"explanation"
|
||||
] = f"""ou <span class="spanlink"
|
||||
descr["explanation"] = f"""ou <span class="spanlink"
|
||||
onclick="set_global_pref(this, '{pref_name}');"
|
||||
>utiliser paramètre global</span>"""
|
||||
if formsemestre_id and self.is_global(formsemestre_id, pref_name):
|
||||
|
@ -2433,10 +2432,12 @@ function set_global_pref(el, pref_name) {
|
|||
before_table="<details><summary>{title}</summary>",
|
||||
after_table="</details>",
|
||||
)
|
||||
dest_url = (
|
||||
scu.NotesURL()
|
||||
+ "/formsemestre_status?formsemestre_id=%s" % self.formsemestre_id
|
||||
dest_url = url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=self.formsemestre_id,
|
||||
)
|
||||
|
||||
if tf[0] == 0:
|
||||
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
|
||||
elif tf[0] == -1:
|
||||
|
@ -2482,7 +2483,9 @@ function set_global_pref(el, pref_name) {
|
|||
request.base_url + "?formsemestre_id=" + str(self.formsemestre_id)
|
||||
)
|
||||
elif destination == "global":
|
||||
return flask.redirect(scu.ScoURL() + "/edit_preferences")
|
||||
return flask.redirect(
|
||||
url_for("scolar.edit_preferences", scodoc_dept=g.scodoc_dept)
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
|
|
|
@ -81,14 +81,11 @@ def feuille_preparation_jury(formsemestre_id):
|
|||
nbabs = {}
|
||||
nbabsjust = {}
|
||||
for etud in etuds:
|
||||
Se = sco_cursus.get_situation_etud_cursus(
|
||||
etud.to_dict_scodoc7(), formsemestre_id
|
||||
Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
|
||||
if Se.prev_formsemestre:
|
||||
ntp: NotesTableCompat = res_sem.load_formsemestre_results(
|
||||
Se.prev_formsemestre
|
||||
)
|
||||
if Se.prev:
|
||||
formsemestre_prev = FormSemestre.query.get_or_404(
|
||||
Se.prev["formsemestre_id"]
|
||||
)
|
||||
ntp: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre_prev)
|
||||
for ue in ntp.get_ues_stat_dict(filter_sport=True):
|
||||
ue_status = ntp.get_etud_ue_status(etud.id, ue["ue_id"])
|
||||
ue_code_s = (
|
||||
|
@ -110,7 +107,7 @@ def feuille_preparation_jury(formsemestre_id):
|
|||
moy_ue[ue_code_s][etud.id] = ue_status["moy"] if ue_status else ""
|
||||
ue_acro[ue_code_s] = (ue["numero"], ue["acronyme"], ue["titre"])
|
||||
|
||||
if Se.prev:
|
||||
if Se.prev_formsemestre:
|
||||
try:
|
||||
moy_inter[etud.id] = (moy[etud.id] + prev_moy[etud.id]) / 2.0
|
||||
except (KeyError, TypeError):
|
||||
|
|
|
@ -42,7 +42,6 @@ from app.models import (
|
|||
but_validations,
|
||||
)
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_edit_ue
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_cursus
|
||||
|
@ -81,6 +80,7 @@ def dict_pvjury(
|
|||
},
|
||||
'autorisations' : [ { 'semestre_id' : { ... } } ],
|
||||
'validation_parcours' : True si parcours validé (diplome obtenu)
|
||||
'parcours' : 'S1, S2, S3, S4, A1',
|
||||
'prev_code' : code (calculé slt si with_prev),
|
||||
'mention' : mention (en fct moy gen),
|
||||
'sum_ects' : total ECTS acquis dans ce semestre (incluant les UE capitalisées)
|
||||
|
@ -107,10 +107,10 @@ def dict_pvjury(
|
|||
D = {} # même chose que decisions, mais { etudid : dec }
|
||||
for etudid in etudids:
|
||||
etud = Identite.get_etud(etudid)
|
||||
Se = sco_cursus.get_situation_etud_cursus(
|
||||
etud.to_dict_scodoc7(), formsemestre_id
|
||||
situation_etud = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
|
||||
semestre_non_terminal = (
|
||||
semestre_non_terminal or situation_etud.semestre_non_terminal
|
||||
)
|
||||
semestre_non_terminal = semestre_non_terminal or Se.semestre_non_terminal
|
||||
d = {}
|
||||
d["identite"] = nt.identdict[etudid]
|
||||
d["etat"] = nt.get_etud_etat(
|
||||
|
@ -120,9 +120,8 @@ def dict_pvjury(
|
|||
d["decisions_ue"] = nt.get_etud_decisions_ue(etudid)
|
||||
if formsemestre.formation.is_apc():
|
||||
d.update(but_validations.dict_decision_jury(etud, formsemestre))
|
||||
d["last_formsemestre_id"] = Se.get_semestres()[
|
||||
-1
|
||||
] # id du dernier semestre (chronologiquement) dans lequel il a été inscrit
|
||||
# id du dernier semestre (chronologiquement) dans lequel il a été inscrit:
|
||||
d["last_formsemestre_id"] = situation_etud.get_semestres()[-1]
|
||||
|
||||
ects_capitalises_by_ue_code = _comp_ects_capitalises_by_ue_code(nt, etudid)
|
||||
d["sum_ects_capitalises"] = sum(ects_capitalises_by_ue_code.values())
|
||||
|
@ -162,10 +161,13 @@ def dict_pvjury(
|
|||
d["autorisations"] = [a.to_dict() for a in autorisations]
|
||||
d["autorisations_descr"] = descr_autorisations(autorisations)
|
||||
|
||||
d["validation_parcours"] = Se.parcours_validated()
|
||||
d["parcours"] = Se.get_cursus_descr(filter_futur=True)
|
||||
d["validation_parcours"] = situation_etud.parcours_validated()
|
||||
d["parcours"] = situation_etud.get_cursus_descr(filter_futur=True)
|
||||
d["parcours_in_cur_formation"] = situation_etud.get_cursus_descr(
|
||||
filter_futur=True, filter_formation_code=True
|
||||
)
|
||||
if with_parcours_decisions:
|
||||
d["parcours_decisions"] = Se.get_parcours_decisions()
|
||||
d["parcours_decisions"] = situation_etud.get_parcours_decisions()
|
||||
# Observations sur les compensations:
|
||||
compensators = sco_cursus_dut.scolar_formsemestre_validation_list(
|
||||
cnx, args={"compense_formsemestre_id": formsemestre_id, "etudid": etudid}
|
||||
|
@ -206,19 +208,19 @@ def dict_pvjury(
|
|||
if not info:
|
||||
continue # should not occur
|
||||
etud = info[0]
|
||||
if Se.prev and Se.prev_decision:
|
||||
d["prev_decision_sem"] = Se.prev_decision
|
||||
d["prev_code"] = Se.prev_decision["code"]
|
||||
if situation_etud.prev_formsemestre and situation_etud.prev_decision:
|
||||
d["prev_decision_sem"] = situation_etud.prev_decision
|
||||
d["prev_code"] = situation_etud.prev_decision["code"]
|
||||
d["prev_code_descr"] = _descr_decision_sem(
|
||||
scu.INSCRIT, Se.prev_decision
|
||||
scu.INSCRIT, situation_etud.prev_decision
|
||||
)
|
||||
d["prev"] = Se.prev
|
||||
d["prev"] = situation_etud.prev_formsemestre.to_dict()
|
||||
has_prev = True
|
||||
else:
|
||||
d["prev_decision_sem"] = None
|
||||
d["prev_code"] = ""
|
||||
d["prev_code_descr"] = ""
|
||||
d["Se"] = Se
|
||||
d["Se"] = situation_etud
|
||||
|
||||
decisions.append(d)
|
||||
D[etudid] = d
|
||||
|
|
|
@ -149,7 +149,7 @@ def pvjury_table(
|
|||
etudid=e["identite"]["etudid"],
|
||||
),
|
||||
"_nomprenom_td_attrs": f"""id="{e['identite']['etudid']}" class="etudinfo" """,
|
||||
"parcours": e["parcours"],
|
||||
"parcours": e["parcours_in_cur_formation"],
|
||||
"decision": _descr_decision_sem_abbrev(e["etat"], e["decision_sem"]),
|
||||
"ue_cap": e["decisions_ue_descr"],
|
||||
"validation_parcours_code": "ADM" if e["validation_parcours"] else "",
|
||||
|
@ -252,6 +252,7 @@ def formsemestre_pvjury(formsemestre_id, fmt="html", publish=True):
|
|||
html_class="table_leftalign",
|
||||
html_sortable=True,
|
||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
table_id="formsemestre_pvjury",
|
||||
)
|
||||
if fmt != "html":
|
||||
return tab.make_page(
|
||||
|
@ -312,6 +313,7 @@ def formsemestre_pvjury(formsemestre_id, fmt="html", publish=True):
|
|||
html_sortable=True,
|
||||
html_with_td_classes=True,
|
||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
table_id="formsemestre_pvjury_counts",
|
||||
).html()
|
||||
)
|
||||
H.append(
|
||||
|
|
|
@ -50,7 +50,7 @@ from app.scodoc import sco_bulletins_pdf
|
|||
from app.scodoc import sco_pv_dict
|
||||
from app.scodoc import sco_pdf
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_exceptions import ScoPDFFormatError, ScoValueError
|
||||
from app.scodoc.sco_cursus_dut import SituationEtudCursus
|
||||
from app.scodoc.sco_pv_templates import CourrierIndividuelTemplate, jury_titres
|
||||
import sco_version
|
||||
|
@ -132,7 +132,11 @@ def pdf_lettres_individuelles(
|
|||
)
|
||||
)
|
||||
|
||||
try:
|
||||
document.build(objects)
|
||||
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
|
||||
raise ScoPDFFormatError(str(exc)) from exc
|
||||
|
||||
data = report.getvalue()
|
||||
return data
|
||||
|
||||
|
@ -241,14 +245,15 @@ def pdf_lettre_individuelle(sem, decision, etud: Identite, params, signature=Non
|
|||
titre_jury_court = "s"
|
||||
else:
|
||||
titre_jury_court = ""
|
||||
params[
|
||||
"autorisations_txt"
|
||||
] = """Vous êtes autorisé%s à continuer dans le%s semestre%s : <b>%s</b>""" % (
|
||||
params["autorisations_txt"] = (
|
||||
"""Vous êtes autorisé%s à continuer dans le%s semestre%s : <b>%s</b>"""
|
||||
% (
|
||||
etud.e,
|
||||
titre_jury_court,
|
||||
titre_jury_court,
|
||||
decision["autorisations_descr"],
|
||||
)
|
||||
)
|
||||
else:
|
||||
params["autorisations_txt"] = ""
|
||||
|
||||
|
|
|
@ -126,7 +126,11 @@ def pvjury_pdf(
|
|||
)
|
||||
)
|
||||
|
||||
try:
|
||||
document.build(objects)
|
||||
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
|
||||
raise ScoPDFFormatError(str(exc)) from exc
|
||||
|
||||
data = report.getvalue()
|
||||
return data
|
||||
|
||||
|
|
|
@ -198,9 +198,9 @@ def formsemestre_recapcomplet(
|
|||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
|
||||
}">Calcul automatique des décisions du jury</a>
|
||||
</li>
|
||||
<li><a class="stdlink" href="{url_for('notes.formsemestre_jury_but_erase',
|
||||
<li><a class="stdlink" href="{url_for('notes.formsemestre_jury_erase',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, only_one_sem=1)
|
||||
}">Effacer <em>toutes</em> les décisions de jury BUT issues de ce semestre</a>
|
||||
}">Effacer <em>toutes</em> les décisions de jury issues de ce semestre</a>
|
||||
</li>
|
||||
"""
|
||||
)
|
||||
|
|
|
@ -236,6 +236,7 @@ def _results_by_category(
|
|||
html_col_width="4em",
|
||||
html_sortable=True,
|
||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
table_id=f"results_by_category-{category_name}",
|
||||
)
|
||||
|
||||
|
||||
|
@ -350,6 +351,7 @@ def formsemestre_report_counts(
|
|||
"statut",
|
||||
"annee_admission",
|
||||
"type_admission",
|
||||
"boursier",
|
||||
"boursier_prec",
|
||||
]
|
||||
if jury_but_mode:
|
||||
|
@ -695,19 +697,18 @@ def table_suivi_cohorte(
|
|||
if statut:
|
||||
dbac += " statut: %s" % statut
|
||||
tab = GenTable(
|
||||
titles=titles,
|
||||
caption="Suivi cohorte " + pp + sem["titreannee"] + dbac,
|
||||
columns_ids=columns_ids,
|
||||
rows=L,
|
||||
filename=scu.make_filename("cohorte " + sem["titreannee"]),
|
||||
html_class="table_cohorte",
|
||||
html_col_width="4em",
|
||||
html_sortable=True,
|
||||
filename=scu.make_filename("cohorte " + sem["titreannee"]),
|
||||
origin="Généré par %s le " % sco_version.SCONAME
|
||||
+ scu.timedate_human_repr()
|
||||
+ "",
|
||||
caption="Suivi cohorte " + pp + sem["titreannee"] + dbac,
|
||||
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
|
||||
page_title="Suivi cohorte " + sem["titreannee"],
|
||||
html_class="table_cohorte",
|
||||
preferences=sco_preferences.SemPreferences(formsemestre.id),
|
||||
rows=L,
|
||||
table_id="table_suivi_cohorte",
|
||||
titles=titles,
|
||||
)
|
||||
# Explication: liste des semestres associés à chaque date
|
||||
if not P:
|
||||
|
@ -1304,6 +1305,7 @@ def table_suivi_cursus(formsemestre_id, only_primo=False, grouped_parcours=True)
|
|||
"code_cursus": len(etuds),
|
||||
},
|
||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
table_id="table_suivi_cursus",
|
||||
)
|
||||
return tab
|
||||
|
||||
|
@ -1535,6 +1537,9 @@ def graph_cursus(
|
|||
# semestre de depart en vert
|
||||
n = g.get_node("SEM" + str(formsemestre_id))[0]
|
||||
n.set_color("green")
|
||||
n.set_style("filled")
|
||||
n.set_fillcolor("lightgreen")
|
||||
n.set_penwidth(2.0)
|
||||
# demissions en rouge, octagonal
|
||||
for nid in dem_nodes.values():
|
||||
n = g.get_node(nid)[0]
|
||||
|
|
|
@ -87,15 +87,16 @@ def formsemestre_but_indicateurs(formsemestre_id: int, fmt="html"):
|
|||
bacs.append("Total")
|
||||
|
||||
tab = GenTable(
|
||||
titles={bac: bac for bac in bacs},
|
||||
columns_ids=["titre_indicateur"] + bacs,
|
||||
rows=rows,
|
||||
html_sortable=False,
|
||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
filename=scu.make_filename(f"Indicateurs_BUT_{formsemestre.titre_annee()}"),
|
||||
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
|
||||
html_caption="Indicateurs BUT annuels.",
|
||||
base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}",
|
||||
columns_ids=["titre_indicateur"] + bacs,
|
||||
filename=scu.make_filename(f"Indicateurs_BUT_{formsemestre.titre_annee()}"),
|
||||
html_caption="Indicateurs BUT annuels.",
|
||||
html_sortable=False,
|
||||
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
|
||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
rows=rows,
|
||||
titles={bac: bac for bac in bacs},
|
||||
table_id="formsemestre_but_indicateurs",
|
||||
)
|
||||
title = "Indicateurs suivi annuel BUT"
|
||||
t = tab.make_page(
|
||||
|
|
|
@ -51,7 +51,24 @@ SCO_ROLES_DEFAULTS = {
|
|||
p.UsersView,
|
||||
p.ViewEtudData,
|
||||
),
|
||||
# Rôles pour l'application relations entreprises
|
||||
# LecteurAPI peut utiliser l'API en lecture
|
||||
"LecteurAPI": (p.ScoView,),
|
||||
"Observateur": (p.Observateur,),
|
||||
# RespPE est le responsable poursuites d'études
|
||||
# il peut ajouter des tags sur les formations:
|
||||
# (doit avoir un rôle Ens en plus !)
|
||||
"RespPe": (p.EditFormationTags,),
|
||||
# Super Admin est un root: création/suppression de départements
|
||||
# _tous_ les droits
|
||||
# Afin d'avoir tous les droits, il ne doit pas être asscoié à un département
|
||||
"SuperAdmin": p.ALL_PERMISSIONS,
|
||||
}
|
||||
|
||||
# Rôles pour l'application relations entreprises
|
||||
# séparés pour pouvoir les réinitialiser lors de l'activation du module Entreprises
|
||||
# Note: Admin (chef de dept n'a par défaut aucun rôle lié à ce module)
|
||||
|
||||
SCO_ROLES_ENTREPRISES_DEFAULT = {
|
||||
# ObservateurEntreprise est un observateur de l'application entreprise
|
||||
"ObservateurEntreprise": (p.RelationsEntrepView,),
|
||||
# UtilisateurEntreprise est un utilisateur de l'application entreprise (droit de modification)
|
||||
|
@ -70,19 +87,10 @@ SCO_ROLES_DEFAULTS = {
|
|||
p.RelationsEntrepValidate,
|
||||
p.RelationsEntrepViewCorrs,
|
||||
),
|
||||
# LecteurAPI peut utiliser l'API en lecture
|
||||
"LecteurAPI": (p.ScoView,),
|
||||
"Observateur": (p.Observateur,),
|
||||
# RespPE est le responsable poursuites d'études
|
||||
# il peut ajouter des tags sur les formations:
|
||||
# (doit avoir un rôle Ens en plus !)
|
||||
"RespPe": (p.EditFormationTags,),
|
||||
# Super Admin est un root: création/suppression de départements
|
||||
# _tous_ les droits
|
||||
# Afin d'avoir tous les droits, il ne doit pas être asscoié à un département
|
||||
"SuperAdmin": p.ALL_PERMISSIONS,
|
||||
}
|
||||
|
||||
SCO_ROLES_DEFAULTS.update(SCO_ROLES_ENTREPRISES_DEFAULT)
|
||||
|
||||
# Les rôles accessibles via la page d'admin utilisateurs
|
||||
# - associés à un département:
|
||||
ROLES_ATTRIBUABLES_DEPT = ("Ens", "Secr", "Admin", "RespPe")
|
||||
|
|
|
@ -378,9 +378,8 @@ class SemSet(dict):
|
|||
|
||||
def html_diagnostic(self):
|
||||
"""Affichage de la partie Effectifs et Liste des étudiants
|
||||
(actif seulement si un portail est configuré) XXX pourquoi ??
|
||||
(actif seulement si un portail est configuré)
|
||||
"""
|
||||
if sco_portal_apogee.has_portal():
|
||||
return self.bilan.html_diagnostic()
|
||||
return ""
|
||||
|
||||
|
@ -482,10 +481,9 @@ def semset_page(fmt="html"):
|
|||
# (remplacé par n liens vers chacun des semestres)
|
||||
# s['_semtitles_str_target'] = s['_export_link_target']
|
||||
# Experimental:
|
||||
s[
|
||||
"_title_td_attrs"
|
||||
] = 'class="inplace_edit" data-url="edit_semset_set_title" id="%s"' % (
|
||||
s["semset_id"]
|
||||
s["_title_td_attrs"] = (
|
||||
'class="inplace_edit" data-url="edit_semset_set_title" id="%s"'
|
||||
% (s["semset_id"])
|
||||
)
|
||||
|
||||
tab = GenTable(
|
||||
|
@ -513,6 +511,7 @@ def semset_page(fmt="html"):
|
|||
html_class="table_leftalign",
|
||||
filename="semsets",
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
table_id="table-semsets",
|
||||
)
|
||||
if fmt != "html":
|
||||
return tab.make_page(fmt=fmt)
|
||||
|
|
|
@ -115,7 +115,8 @@ def formsemestre_synchro_etuds(
|
|||
url_for('notes.formsemestre_editwithmodules',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
|
||||
}">Modifier ce semestre</a>)
|
||||
"""
|
||||
""",
|
||||
safe=True,
|
||||
)
|
||||
footer = html_sco_header.sco_footer()
|
||||
base_url = url_for(
|
||||
|
|
|
@ -47,12 +47,11 @@ from app import db, log
|
|||
from app.models import Identite
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_exceptions import ScoPDFFormatError, ScoValueError
|
||||
from app.scodoc.sco_pdf import SU
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import htmlutils
|
||||
from app.scodoc import sco_import_etuds
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_excel
|
||||
from app.scodoc import sco_groups_view
|
||||
from app.scodoc import sco_pdf
|
||||
|
@ -388,7 +387,10 @@ def _trombino_pdf(groups_infos):
|
|||
preferences=sco_preferences.SemPreferences(sem["formsemestre_id"]),
|
||||
)
|
||||
)
|
||||
try:
|
||||
document.build(objects)
|
||||
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
|
||||
raise ScoPDFFormatError(str(exc)) from exc
|
||||
report.seek(0)
|
||||
return send_file(
|
||||
report,
|
||||
|
@ -465,7 +467,10 @@ def _listeappel_photos_pdf(groups_infos):
|
|||
preferences=sco_preferences.SemPreferences(sem["formsemestre_id"]),
|
||||
)
|
||||
)
|
||||
try:
|
||||
document.build(objects)
|
||||
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
|
||||
raise ScoPDFFormatError(str(exc)) from exc
|
||||
data = report.getvalue()
|
||||
|
||||
return scu.sendPDFFile(data, filename)
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
"""
|
||||
|
||||
import io
|
||||
|
||||
import reportlab
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.colors import black
|
||||
from reportlab.lib.pagesizes import A4, A3
|
||||
|
@ -277,10 +277,12 @@ def pdf_trombino_tours(
|
|||
preferences=sco_preferences.SemPreferences(),
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
document.build(objects)
|
||||
except (ValueError, KeyError) as exc:
|
||||
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
|
||||
raise ScoPDFFormatError(str(exc)) from exc
|
||||
|
||||
data = report.getvalue()
|
||||
|
||||
return scu.sendPDFFile(data, filename)
|
||||
|
@ -470,7 +472,10 @@ def pdf_feuille_releve_absences(
|
|||
preferences=sco_preferences.SemPreferences(),
|
||||
)
|
||||
)
|
||||
try:
|
||||
document.build(objects)
|
||||
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
|
||||
raise ScoPDFFormatError(str(exc)) from exc
|
||||
data = report.getvalue()
|
||||
|
||||
return scu.sendPDFFile(data, filename)
|
||||
|
|
|
@ -169,6 +169,7 @@ def evaluation_list_operations(evaluation_id):
|
|||
preferences=sco_preferences.SemPreferences(
|
||||
evaluation.moduleimpl.formsemestre_id
|
||||
),
|
||||
table_id="evaluation_list_operations",
|
||||
)
|
||||
return tab.make_page()
|
||||
|
||||
|
@ -241,6 +242,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, fmt="html"):
|
|||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
base_url="%s?formsemestre_id=%s" % (request.base_url, formsemestre_id),
|
||||
origin=f"Généré par {sco_version.SCONAME} le " + scu.timedate_human_repr() + "",
|
||||
table_id="formsemestre_list_saisies_notes",
|
||||
)
|
||||
return tab.make_page(fmt=fmt)
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user