Compare commits

...

139 Commits
master ... api

Author SHA1 Message Date
leonard_montalbano a43f1e0e22 amélioration des asserts de departement et etudiant 2022-03-11 16:18:50 +01:00
leonard_montalbano 8e36201482 Merge branch 'dev92' of https://scodoc.org/git/ScoDoc/ScoDoc into api 2022-03-11 09:11:26 +01:00
Emmanuel Viennet f61a528d12 Bul. BUT: amélioration, inverse colonnes droites 2022-03-10 20:44:01 +01:00
Emmanuel Viennet 5a56138e55 PDF tables: fix mix tags/Platypus markup 2022-03-10 19:36:30 +01:00
Emmanuel Viennet 462c084bf4 Bul. BUT: Poids des evals avec valeurs par défaut. 2022-03-10 19:35:12 +01:00
leonard_montalbano 433b4b8f5c début de validation des routes des parties départements et etudiants par les tests 2022-03-10 17:43:12 +01:00
Emmanuel Viennet 8be3ecfeaf Bul. BUT: versions + nettoyage 2022-03-10 09:28:59 +01:00
leonard_montalbano 288cad21cc Merge branch 'dev92' of https://scodoc.org/git/ScoDoc/ScoDoc into api 2022-03-10 09:19:48 +01:00
Emmanuel Viennet 5efebb1336 Bul. BUT: SAE dans synthèse, pied de bul. sur meme page. 2022-03-10 09:18:19 +01:00
Emmanuel Viennet 9587159692 format malus 2022-03-10 01:24:37 +01:00
Emmanuel Viennet 793eca017a Bonus Tarbes (à tester) 2022-03-10 00:51:13 +01:00
Emmanuel Viennet a83d491874 Bulletins PDV BUT, v0 2022-03-10 00:50:36 +01:00
Emmanuel Viennet fe6790738f Base test: complète étudiants et groupe par defaut 2022-03-09 18:03:18 +01:00
leonard_montalbano f9817966cf création des fichiers tests et des requêtes aux routes de l'api 2022-03-09 16:52:07 +01:00
leonard_montalbano 28ec8a482a Merge branch 'dev92' of https://scodoc.org/git/ScoDoc/ScoDoc into api 2022-03-09 16:08:49 +01:00
Emmanuel Viennet 10230d20ef Commande init-test-database pour générer base de test (pour l'API) 2022-03-09 16:05:44 +01:00
Emmanuel Viennet d2fe27b67c allows Paragraph in table cells for pdf, and specific cell values for pdf 2022-03-09 13:59:40 +01:00
Emmanuel Viennet a4035411d9 logo 2022-03-08 09:16:22 +01:00
Emmanuel Viennet a09418329f Intégration bulletins html 2022-03-07 23:43:48 +01:00
Emmanuel Viennet 2220b617b8 WIP: intégration bulletins 2022-03-07 21:49:11 +01:00
leonard_montalbano c4f2f0925d Merge branch 'master' of https://scodoc.org/git/ScoDoc/ScoDoc into api 2022-03-07 15:48:22 +01:00
Emmanuel Viennet 8f911234b2 modernisation/methodes sur Identite/bul. head. 2022-03-06 22:40:20 +01:00
Emmanuel Viennet c923a5015b Infos pour bulletins BUT pdf 2022-03-05 12:47:08 +01:00
Emmanuel Viennet ec9cdfe50a Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into dev92 2022-03-04 20:59:04 +01:00
Emmanuel Viennet 3d0509de64 Bonus Saint-Brieuc (à valider: semestres à affecter?) 2022-03-04 20:51:44 +01:00
Emmanuel Viennet b4a7749e5a Mode test pour les mails. Closes #326 2022-03-04 20:02:50 +01:00
Emmanuel Viennet e04a187a01 Exception si archive introuvable 2022-03-04 18:55:45 +01:00
leonard_montalbano 47123aeb1e permissions non fonctionnel 2022-03-04 17:16:08 +01:00
leonard_montalbano 90e292341e Merge branch 'master' of https://scodoc.org/git/ScoDoc/ScoDoc into api 2022-03-04 14:58:45 +01:00
Emmanuel Viennet bcbace0120 N'affiche pas les UE sans inscriptions sur les buleltins classiques 2022-03-03 23:02:24 +01:00
Emmanuel Viennet 4a03887120 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into dev92 2022-03-03 21:53:34 +01:00
Emmanuel Viennet 3b7370f6df Bonus Brest (identique à Bordeaux) 2022-03-03 21:20:54 +01:00
Emmanuel Viennet a0d8f89b18 Clippe les moyennes finales dans [0,20] 2022-03-03 21:17:03 +01:00
leonard_montalbano b1e0def55a ajout des permissions, factorisation de liste_etudiants et tests de la partie départements 2022-03-03 16:25:29 +01:00
leonard_montalbano c1b11bd9d1 Merge branch 'master' of https://scodoc.org/git/ScoDoc/ScoDoc into api 2022-03-03 09:44:02 +01:00
Emmanuel Viennet e6be8d9ecb Bonus Calais 2022-03-03 09:42:12 +01:00
Emmanuel Viennet 09111d9455 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into dev92 2022-03-03 09:05:32 +01:00
Emmanuel Viennet 577cac00ee Export des annotations sur des groupes d'étudiants. Closes #327 2022-03-02 23:14:42 +01:00
leonard_montalbano 4d49de397c réorganisation des fichiers de travail pour l'api 2022-03-02 16:45:47 +01:00
leonard_montalbano e9656dc07f Merge branch 'master' of https://scodoc.org/git/ScoDoc/ScoDoc into api 2022-03-02 16:45:07 +01:00
Emmanuel Viennet 912ee8b4da Fix: modimpls_sorted 2022-03-02 16:31:42 +01:00
leonard_montalbano 6bab2c00ad Merge branch 'master' of https://scodoc.org/git/ScoDoc/ScoDoc into api 2022-03-02 15:04:43 +01:00
Emmanuel Viennet 750744094e version 9.1.69 2022-03-02 00:01:22 +01:00
Emmanuel Viennet a19a54e054 fix typo 2022-03-01 23:51:39 +01:00
Emmanuel Viennet 9d53e38992 cosmetic / ECTS 2022-03-01 23:25:42 +01:00
Emmanuel Viennet c0719df0c0 noms modules sur menu saisie absences 2022-03-01 19:27:03 +01:00
leonard_montalbano 1cd7a84b15 test et récupération de dept, formsemestre et etu en variable global pour les tests suivant 2022-03-01 16:00:41 +01:00
leonard_montalbano 1c271bbad4 Merge branch 'master' of https://scodoc.org/git/ScoDoc/ScoDoc into api 2022-03-01 12:58:47 +01:00
Emmanuel Viennet 523ad7ad2a Modif bonus La Rochelle 2022-03-01 10:40:38 +01:00
Emmanuel Viennet f0e731d151 Fix: bulletin classique quand coef UE None 2022-03-01 10:33:53 +01:00
Emmanuel Viennet 10c96ad683 PE: check submitted template (utf8) 2022-03-01 10:21:15 +01:00
Emmanuel Viennet 6943ccb872 typo (sel. modules BUT) 2022-03-01 10:16:34 +01:00
Emmanuel Viennet c5c0b510ec filename export formations 2022-03-01 09:48:37 +01:00
Emmanuel Viennet 7edd051183 Fix: bonus St Quentin / Ville d'Avray 2022-03-01 09:34:18 +01:00
Emmanuel Viennet 23ea53294d Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into dev92 2022-02-28 20:23:25 +01:00
Emmanuel Viennet 13b40936b8 Nouveau calcul (correct?) de la moyenne de matière en classic 2022-02-28 20:02:10 +01:00
Emmanuel Viennet b56a20643d largeur colonne codes modules 2022-02-28 20:01:24 +01:00
Emmanuel Viennet e993599b39 Restreint edition modules semestres BUT aux module du même sem. 2022-02-28 17:57:12 +01:00
Emmanuel Viennet 8b5a996571 Semestre BUT: ne propose pas indice -1 2022-02-28 16:28:08 +01:00
Emmanuel Viennet 0e7f2f4deb flash 2022-02-28 16:27:27 +01:00
Emmanuel Viennet 8330009dcf En BUT, remet S1 si semestre non spécifié 2022-02-28 16:26:13 +01:00
Emmanuel Viennet 732a4c5ce5 code cleaning 2022-02-28 16:25:18 +01:00
leonard_montalbano f0bdb5e9bd Merge branch 'master' of https://scodoc.org/git/ScoDoc/ScoDoc into api 2022-02-28 15:44:26 +01:00
Emmanuel Viennet df9ec49568 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into dev92 2022-02-28 15:22:02 +01:00
Emmanuel Viennet bee7b74f17 Fichier oublié (flask flash) 2022-02-28 15:18:21 +01:00
Emmanuel Viennet f7c90397a8 Enhance scodoc7 decorator: FileStorage arguments 2022-02-28 15:12:32 +01:00
Emmanuel Viennet 5aa896f793 Bonus Aisne St Quentin + fix bonus Ville d'Avray 2022-02-28 15:08:32 +01:00
Emmanuel Viennet 546e10c83a Finalise calcul moy. gen. indicative BUT 2022-02-28 15:07:48 +01:00
Emmanuel Viennet e1db9c542b Messages flash flask sur ancioennes pages ScoDoc + warning ECTS BUT 2022-02-28 11:47:39 +01:00
Emmanuel Viennet ef408e5d8e Gestion calcul moy gen et capit. BUT si ECTS manquants 2022-02-28 11:00:24 +01:00
Emmanuel Viennet 68680e89d3 Exception si erreur connexion vers assistance 2022-02-28 09:22:17 +01:00
Emmanuel Viennet 00fa91e598 Calcul moyenne gen. BUT avec ECTS 2022-02-27 20:32:38 +01:00
Emmanuel Viennet 29b5d54d22 Prise en compte UE capitalisées lorsque non inscrit dans le sem. courant. Affichage sur bulletins classiques. Capitalisation en BUT avec ECTS. 2022-02-27 20:12:20 +01:00
Emmanuel Viennet 6b8410e43b cosmetic: edit prog. 2022-02-27 17:49:39 +01:00
Emmanuel Viennet 091d34dd88 Améliore creation UE 2022-02-27 10:19:25 +01:00
Emmanuel Viennet 1dfccb6737 Modif bonus Roanne 2022-02-27 09:45:15 +01:00
Emmanuel Viennet 40f823ee7c Fix: edition module 2022-02-26 20:35:34 +01:00
Emmanuel Viennet c0494d8d71 exception handling (export Apo) 2022-02-26 20:22:18 +01:00
Emmanuel Viennet c1c9f22a31 exception -handling 2022-02-26 20:11:22 +01:00
Emmanuel Viennet dbab59039c Fix: recherche images fond de page (logos) 2022-02-26 11:00:08 +01:00
Emmanuel Viennet 9b27503d01 Fix: gestion logos 2022-02-26 10:15:00 +01:00
Emmanuel Viennet aa609aa0cf Améliore form. logos (validation des noms) + messages flash 2022-02-26 10:09:14 +01:00
leonard_montalbano afe43f98e3 code de sco_api.py commenté et début des tests 2022-02-25 16:03:05 +01:00
leonard_montalbano 8ea9f04ea6 Merge branch 'master' of https://scodoc.org/git/ScoDoc/ScoDoc into api 2022-02-25 12:34:54 +01:00
Emmanuel Viennet 9bc5f27b16 moduleimpl_withmodule_list (api ScoDoc 7 compat): fix 2022-02-25 10:30:57 +01:00
leonard_montalbano cd961e6e3e changement de strategie pour les routes logos 2022-02-24 16:15:41 +01:00
leonard_montalbano 35be7ebb4c Merge branch 'master' of https://scodoc.org/git/ScoDoc/ScoDoc into api 2022-02-24 14:10:53 +01:00
Emmanuel Viennet 6a07bb85a0 Message erreur si bul_intro_mail invalide 2022-02-23 20:21:13 +01:00
Emmanuel Viennet 2cac0031f6 Erreur si la reponse portail n'a pas le mail 2022-02-23 20:15:28 +01:00
leonard_montalbano d468a3f49e soucis avec routes logos 2022-02-23 16:10:10 +01:00
leonard_montalbano ba164481a6 Merge branch 'master' of https://scodoc.org/git/ScoDoc/ScoDoc into api 2022-02-23 13:44:47 +01:00
Emmanuel Viennet e9ad417f1f check matieres 2022-02-23 09:42:41 +01:00
Emmanuel Viennet bd33b288db Merge pull request 'soften error when logo not found' (#322) from jmplace/ScoDoc-Lille:soften_logos_error_message into master
Reviewed-on: ScoDoc/ScoDoc#322
2022-02-22 20:02:23 +01:00
Jean-Marie PLACE 875c12d703 soften error when logo not found 2022-02-22 19:48:22 +01:00
Emmanuel Viennet e7b980bff7 Fix: accès moyennes_matieres 2022-02-22 18:46:47 +01:00
Emmanuel Viennet 276d7977a7 Ajout UE bonus aux parcours ILEPS 2022-02-22 18:45:43 +01:00
Emmanuel Viennet 0e42df55c9 Option pour afficher coef. UE séparée de celle pour les coefs modules (et évals). 2022-02-22 18:44:53 +01:00
leonard_montalbano 976fdf5b4e partie formation fini 2022-02-22 15:50:52 +01:00
leonard_montalbano 3fab8300a1 Merge branch 'master' of https://scodoc.org/git/ScoDoc/ScoDoc into api 2022-02-22 13:33:52 +01:00
Emmanuel Viennet 6b8b0f9c24 WIP: bulletins BUT pdf 2022-02-21 19:25:38 +01:00
Emmanuel Viennet d314d47dc5 traitement erreur sur formsemestre_description 2022-02-21 18:51:45 +01:00
Emmanuel Viennet 0801919b80 Calcul moyennes matières (formations classiques). 2022-02-21 17:36:50 +01:00
leonard_montalbano 677094aaac Merge branch 'master' of https://scodoc.org/git/ScoDoc/ScoDoc into api 2022-02-21 16:14:49 +01:00
leonard_montalbano ba0062135b formsemestre et etudiant fini 2022-02-21 16:12:31 +01:00
Emmanuel Viennet ba974df04f Fix: mise à jour bonus sur oy. gen. après capitalisation UEs 2022-02-21 15:10:10 +01:00
leonard_montalbano 401a43378d Merge branch 'master' of https://scodoc.org/git/ScoDoc/ScoDoc into api 2022-02-21 14:39:08 +01:00
Emmanuel Viennet aa3a2fb3e0 Cosmetic: edition prog. classiques 2022-02-20 15:10:15 +01:00
Emmanuel Viennet f2c3841db9 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into dev92 2022-02-19 16:17:51 +01:00
Emmanuel Viennet 44123c022e Améliore édition programmes classiques 2022-02-19 16:16:52 +01:00
Emmanuel Viennet 63784e341a Correction pour Amiens, Roanne 2022-02-19 01:00:40 +01:00
Emmanuel Viennet cca72dfed2 Bonus IUT Amiens 2022-02-19 00:28:24 +01:00
Emmanuel Viennet 5c951d58e7 Fix lien chargement ref. compétences 2022-02-18 23:43:13 +01:00
Emmanuel Viennet 202ce4e73e nginx frontal: augmentation du timeout proxy 2022-02-18 23:30:45 +01:00
Emmanuel Viennet c81c4efb40 Fix: formsemestre_evaluations_cal 2022-02-18 22:59:05 +01:00
Emmanuel Viennet d877648546 empeche création de départements avec même acronyme 2022-02-18 22:19:23 +01:00
Emmanuel Viennet 58a8bcb83d Ajoute lien 'inscrire des étudiants'. Closes #319. Auteur: PB. 2022-02-18 22:14:12 +01:00
Emmanuel Viennet fae11d82ce Fix: prise en compte éval de rattrapage dans les modules BUT 2022-02-18 21:54:30 +01:00
Emmanuel Viennet 7a85ec7466 Ajout bonus Cachan1, et suppression bonus Annecy 2022-02-18 21:18:08 +01:00
Emmanuel Viennet 091a49cb0d Edition module: simplifie liste rattachement 2022-02-18 19:36:51 +01:00
Emmanuel Viennet ab212a5b2b Edition programme: avertissements 2022-02-18 19:35:57 +01:00
Emmanuel Viennet a67515d560 Ignore les poids en BD des évals des modules non APC 2022-02-18 19:34:40 +01:00
leonard_montalbano 84f43e1b36 avancement sur les routes jury 2022-02-18 16:08:50 +01:00
leonard_montalbano 79b5530813 ajout des routes jury 2022-02-18 15:53:54 +01:00
leonard_montalbano 25b0648284 Merge branch 'master' of https://scodoc.org/git/ScoDoc/ScoDoc into api 2022-02-18 15:25:17 +01:00
Emmanuel Viennet 39e31983ee Fix (pour suite tests) 2022-02-18 14:24:35 +01:00
Emmanuel Viennet 175c66c834 Affichage plus clair du bonus sur bulletins classiques 2022-02-18 14:15:29 +01:00
leonard_montalbano 6842669ffe Merge branch 'master' of https://scodoc.org/git/ScoDoc/ScoDoc into api 2022-02-18 14:07:56 +01:00
Emmanuel Viennet fffba011ea Pour tests: modif calcul bonus DUT (corrige erreur de signe) 2022-02-18 14:05:48 +01:00
Emmanuel Viennet 89db5cf858 Pour tests: modif calcul bonus DUT : UE vers moy. gen. 2022-02-18 13:59:39 +01:00
Emmanuel Viennet 9e4c19a292 Fix: coefs absents en formations classiques plantaient bonus sport nouvelle formule 2022-02-18 12:12:16 +01:00
leonard_montalbano 2e1dcce69d résolution des conflis 2022-02-18 11:26:17 +01:00
Emmanuel Viennet 716a6bf41f Fix migrations redondantes 2022-02-18 00:08:19 +01:00
Emmanuel Viennet bfd0a4b311 phrase sur bul. BUT 2022-02-17 23:15:15 +01:00
Emmanuel Viennet f8630b3cdb Modification calcul bonus sport BUT 2022-02-17 23:13:55 +01:00
Emmanuel Viennet 98e7f7a710 Corrige affichage numero version sur templates Jinja 2022-02-17 22:40:11 +01:00
Emmanuel Viennet 1dbb199d2c Ajout relations pour acces aux partitions et groupes via l'ORM 2022-02-17 18:13:04 +01:00
Emmanuel Viennet 5bd60d9c34 Merge pull request 'Rang + espacements' (#317) from lehmann/ScoDoc-Front:master into dev92
Reviewed-on: ScoDoc/ScoDoc#317
2022-02-16 23:25:25 +01:00
Sébastien Lehmann b165bc2659 Rang + amélioration espacements 2022-02-15 11:45:56 +01:00
Emmanuel Viennet 60a77b8ba7 WIP: réorganisation code bulletins 2022-02-14 23:21:42 +01:00
116 changed files with 5537 additions and 1433 deletions

View File

@ -20,10 +20,10 @@ Flask, SQLAlchemy, au lien de Python2/Zope dans les versions précédentes).
### État actuel (26 jan 22)
- 9.1 (master) reproduit l'ensemble des fonctions de ScoDoc 7 (donc pas de BUT), sauf:
- 9.1.5x (master) reproduit l'ensemble des fonctions de ScoDoc 7 (donc pas de BUT), sauf:
- ancien module "Entreprises" (obsolète) et ajoute la gestion du BUT.
- 9.2 (branche refactor_nt) est la version de développement.
- 9.2 (branche dev92) est la version de développement.
### Lignes de commandes

View File

@ -13,7 +13,7 @@ from logging.handlers import SMTPHandler, WatchedFileHandler
from flask import current_app, g, request
from flask import Flask
from flask import abort, has_request_context, jsonify
from flask import abort, flash, has_request_context, jsonify
from flask import render_template
from flask.logging import default_handler
from flask_sqlalchemy import SQLAlchemy
@ -201,7 +201,7 @@ def create_app(config_class=DevConfig):
app.register_blueprint(auth_bp, url_prefix="/auth")
from app.entreprises import bp as entreprises_bp
app.register_blueprint(entreprises_bp, url_prefix="/ScoDoc/entreprises")
from app.views import scodoc_bp
@ -295,10 +295,12 @@ def create_app(config_class=DevConfig):
from app.scodoc.sco_bulletins_legacy import BulletinGeneratorLegacy
from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
from app.but.bulletin_but_pdf import BulletinGeneratorStandardBUT
from app.scodoc.sco_bulletins_ucac import BulletinGeneratorUCAC
# l'ordre est important, le premeir sera le "défaut" pour les nouveaux départements.
# l'ordre est important, le premier sera le "défaut" pour les nouveaux départements.
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorStandard)
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorStandardBUT)
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorLegacy)
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorUCAC)
if app.testing or app.debug:
@ -457,15 +459,12 @@ from app.models import Departement
from app.scodoc import notesdb as ndb, sco_preferences
from app.scodoc import sco_cache
# admin_role = Role.query.filter_by(name="SuperAdmin").first()
# if admin_role:
# admin = (
# User.query.join(UserRole)
# .filter((UserRole.user_id == User.id) & (UserRole.role_id == admin_role.id))
# .first()
# )
# else:
# click.echo(
# "Warning: user database not initialized !\n (use: flask user-db-init)"
# )
# admin = None
def scodoc_flash_status_messages():
"""Should be called on each page: flash messages indicating specific ScoDoc status"""
email_test_mode_address = sco_preferences.get_preference("email_test_mode_address")
if email_test_mode_address:
flash(
f"Mode test: mails redirigés vers {email_test_mode_address}",
category="warning",
)

View File

@ -23,4 +23,14 @@ def requested_format(default_format="json", allowed_formats=None):
from app.api import tokens
from app.api import sco_api
from app.api import test_api
from app.api import departements
from app.api import etudiants
from app.api import formations
from app.api import formsemestres
from app.api import partitions
from app.api import evaluations
from app.api import jury
from app.api import absences
from app.api import logos

315
app/api/absences.py Normal file
View File

@ -0,0 +1,315 @@
#################################################### Absences #########################################################
from datetime import datetime
from flask import jsonify
from app import models
from app.api import bp
from app.api.auth import token_auth
from app.api.errors import error_response
from app.decorators import permission_required
from app.scodoc.sco_abs import add_absence, add_justif, annule_absence, annule_justif, list_abs_date
from app.scodoc.sco_groups import get_group_members
from app.scodoc.sco_permissions import Permission
@bp.route("/absences/etudid/<int:etudid>", methods=["GET"])
@bp.route("/absences/nip/<int:nip>", methods=["GET"])
@bp.route("/absences/ine/<int:ine>", methods=["GET"])
@permission_required(Permission.APIView)
def absences(etudid: int = None, nip: int = None, ine: int = None):
"""
Retourne la liste des absences d'un étudiant donné
etudid : l'etudid d'un étudiant
nip: le code nip d'un étudiant
ine : le code ine d'un étudiant
"""
abs = None
if etudid is not None: # Si route etudid
# Récupération des absences de l'étudiant
abs = models.Absence.query.filter_by(etudid=etudid).all()
else:
if nip is not None: # Si route nip
# Récupération de l'étudiant
etu = models.Identite.query.filter_by(code_nip=nip).first()
# Récupération des absences de l'étudiant
abs = models.Absence.query.filter_by(etudid=etu.etudid).all()
if ine is not None: # Si route ine
# Récupération de l'étudiant
etu = models.Identite.query.filter_by(code_ine=ine).first()
# Récupération des absences de l'étudiant
abs = models.Absence.query.filter_by(etudid=etu.etudid).all()
if abs is not None: # Si des absences ont bien été trouvé
# Mise en forme des données
data = [d.to_dict() for d in abs]
return jsonify(data)
return error_response(501, message="Not implemented")
@bp.route("/absences/etudid/<int:etudid>/abs_just_only", methods=["GET"])
@bp.route("/absences/nip/<int:nip>/abs_just_only", methods=["GET"])
@bp.route("/absences/ine/<int:ine>/abs_just_only", methods=["GET"])
@permission_required(Permission.APIView)
def absences_justify(etudid: int = None, nip: int = None, ine: int = None):
"""
Retourne la liste des absences justifiées d'un étudiant donné
etudid : l'etudid d'un étudiant
nip: le code nip d'un étudiant
ine : le code ine d'un étudiant
"""
abs = None
if etudid is not None: # Si route etudid
# Récupération des absences justifiées de l'étudiant
abs = models.Absence.query.filter_by(etudid=etudid, estjust=True).all()
else:
if nip is not None: # Si route nip
# Récupération de l'étudiant
etu = models.Identite.query.filter_by(code_nip=nip).first()
# Récupération des absences justifiées de l'étudiant
abs = models.Absence.query.filter_by(etudid=etu.etudid, estjust=True).all()
if ine is not None: # Si route ine
# Récupération de l'étudiant
etu = models.Identite.query.filter_by(code_ine=ine).first()
# Récupération des absences justifiées de l'étudiant
abs = models.Absence.query.filter_by(etudid=etu.etudid, estjust=True).all()
if abs is not None: # Si des absences ont bien été trouvé
# Mise en forme des données
data = [d.to_dict() for d in abs]
return jsonify(data)
return error_response(501, message="Not implemented")
@bp.route("/absences/abs_signale?etudid=<int:etudid>&date=<string:date>&matin=<string:matin>&justif=<string:justif>"
"&description=<string:description>", methods=["POST"])
@bp.route("/absences/abs_signale?nip=<int:nip>&date=<string:date>&matin=<string:matin>&justif=<string:justif>"
"&description=<string:description>", methods=["POST"])
@bp.route("/absences/abs_signale?ine=<int:ine>&date=<string:date>&matin=<string:matin>&justif=<string:justif>"
"&description=<string:description>", methods=["POST"])
@bp.route("/absences/abs_signale?ine=<int:ine>&date=<string:date>&matin=<string:matin>&justif=<string:justif>"
"&description=<string:description>&moduleimpl_id=<int:moduleimpl_id>", methods=["POST"])
@token_auth.login_required
@permission_required(Permission.APIAbsChange)
def abs_signale(date: datetime, matin: bool, justif: bool, etudid: int = None, nip: int = None, ine: int = None,
description: str = None, moduleimpl_id: int = None):
"""
Permet d'ajouter une absence en base
date : la date de l'absence
matin : True ou False
justif : True ou False
etudid : l'etudid d'un étudiant
nip: le code nip d'un étudiant
ine : le code ine d'un étudiant
description : description possible à ajouter sur l'absence
moduleimpl_id : l'id d'un moduleimpl
"""
# Fonctions utilisées : app.scodoc.sco_abs.add_absence() et app.scodoc.sco_abs.add_justif()
if description is not None: # Si la description a été renseignée
if moduleimpl_id is not None: # Si le moduleimpl a été renseigné
if etudid is not None: # Si route etudid
try:
# Utilisation de la fonction add_absence
add_absence(etudid, date, matin, justif, description, moduleimpl_id)
# Utilisation de la fonction add_justif
add_justif(etudid, date, matin, description)
except ValueError:
return error_response(409, message="La requête ne peut être traitée en létat actuel")
if nip is not None: # Si route nip
# Récupération de l'étudiant
etu = models.Identite.query.filter_by(code_nip=nip).first()
try:
# Utilisation de la fonction add_absence
add_absence(etu.etudid, date, matin, justif, description, moduleimpl_id)
# Utilisation de la fonction add_justif
add_justif(etu.etudid, date, matin, description)
except ValueError:
return error_response(409, message="La requête ne peut être traitée en létat actuel")
if ine is not None: # Si route ine
# Récupération de l'étudiant
etu = models.Identite.query.filter_by(code_ine=ine).first()
try:
# Utilisation de la fonction add_absence
add_absence(etu.etudid, date, matin, justif, description, moduleimpl_id)
# Utilisation de la fonction add_justif
add_justif(etu.etudid, date, matin, description)
except ValueError:
return error_response(409, message="La requête ne peut être traitée en létat actuel")
return error_response(409, message="La requête ne peut être traitée en létat actuel")
else: # Si le moduleimpl n'a pas été renseigné
if etudid is not None: # Si route etudid
try:
# Utilisation de la fonction add_absence
add_absence(etudid, date, matin, justif, description)
# Utilisation de la fonction add_justif
add_justif(etudid, date, matin, description)
except ValueError:
return error_response(409, message="La requête ne peut être traitée en létat actuel")
if nip is not None: # Si route nip
# Récupération de l'étudiant
etu = models.Identite.query.filter_by(code_nip=nip).first()
try:
# Utilisation de la fonction add_absence
add_absence(etu.etudid, date, matin, justif, description)
# Utilisation de la fonction add_justif
add_justif(etu.etudid, date, matin, description)
except ValueError:
return error_response(409, message="La requête ne peut être traitée en létat actuel")
if ine is not None: # Si route ine
# Récupération de l'étudiant
etu = models.Identite.query.filter_by(code_ine=ine).first()
try:
# Utilisation de la fonction add_absence
add_absence(etu.etudid, date, matin, justif, description)
# Utilisation de la fonction add_justif
add_justif(etu.etudid, date, matin, description)
except ValueError:
return error_response(409, message="La requête ne peut être traitée en létat actuel")
return error_response(409, message="La requête ne peut être traitée en létat actuel")
else:
if etudid is not None: # Si route etudid
try:
# Utilisation de la fonction add_absence
add_absence(etudid, date, matin, justif)
# Utilisation de la fonction add_justif
add_justif(etudid, date, matin)
except ValueError:
return error_response(409, message="La requête ne peut être traitée en létat actuel")
if nip is not None: # Si route nip
# Récupération de l'étudiant
etu = models.Identite.query.filter_by(code_nip=nip).first()
try:
# Utilisation de la fonction add_absence
add_absence(etu.etudid, date, matin, justif)
# Utilisation de la fonction add_justif
add_justif(etu.etudid, date, matin)
except ValueError:
return error_response(409, message="La requête ne peut être traitée en létat actuel")
if ine is not None: # Si route ine
# Récupération de l'étudiant
etu = models.Identite.query.filter_by(code_ine=ine).first()
try:
# Utilisation de la fonction add_absence
add_absence(etu.etudid, date, matin, justif)
# Utilisation de la fonction add_justif
add_justif(etu.etudid, date, matin)
except ValueError:
return error_response(409, message="La requête ne peut être traitée en létat actuel")
return error_response(200, message="OK")
@bp.route("/absences/abs_annule?etudid=<int:etudid>&jour=<string:jour>&matin=<string:matin>", methods=["POST"])
@bp.route("/absences/abs_annule?nip=<int:nip>&jour=<string:jour>&matin=<string:matin>", methods=["POST"])
@bp.route("/absences/abs_annule?ine=<int:ine>&jour=<string:jour>&matin=<string:matin>", methods=["POST"])
@token_auth.login_required
@permission_required(Permission.APIAbsChange)
def abs_annule(jour: datetime, matin: str, etudid: int = None, nip: int = None, ine: int = None):
"""
Retourne un html
jour : la date de l'absence a annulé
matin : True ou False
etudid : l'etudid d'un étudiant
nip: le code nip d'un étudiant
ine : le code ine d'un étudiant
"""
# Fonction utilisée : app.scodoc.sco_abs.annule_absence()
if etudid is None:
if nip is not None: # Si route nip
# Récupération de l'étudiant
etu = models.Identite.query.filter_by(code_nip=nip).first()
# Récupération de l'etudid de l'étudiant
etudid = etu.etudid
if ine is not None: # Si route ine
# Récupération de l'étudiant
etu = models.Identite.query.filter_by(code_ine=ine).first()
# Récupération de l'etudid de l'étudiant
etudid = etu.etudid
try:
# Utilisation de la fonction annule_absence
annule_absence(etudid, jour, matin)
except ValueError:
return error_response(409, message="La requête ne peut être traitée en létat actuel")
return error_response(200, message="OK")
@bp.route("/absences/abs_annule_justif?etudid=<int:etudid>&jour=<string:jour>&matin=<string:matin>", methods=["POST"])
@bp.route("/absences/abs_annule_justif?nip=<int:nip>&jour=<string:jour>&matin=<string:matin>", methods=["POST"])
@bp.route("/absences/abs_annule_justif?ine=<int:ine>&jour=<string:jour>&matin=<string:matin>", methods=["POST"])
@token_auth.login_required
@permission_required(Permission.APIAbsChange)
def abs_annule_justif(jour: datetime, matin: str, etudid: int = None, nip: int = None, ine: int = None):
"""
Retourne un html
jour : la date de l'absence a annulé
matin : True ou False
etudid : l'etudid d'un étudiant
nip: le code nip d'un étudiant
ine : le code ine d'un étudiant
"""
# Fonction utilisée : app.scodoc.sco_abs.annule_justif()
if etudid is None:
if nip is not None: # Si route nip
# Récupération de l'étudiant
etu = models.Identite.query.filter_by(code_nip=nip).first()
# Récupération de l'etudid de l'étudiant
etudid = etu.etudid
if ine is not None: # Si route ine
# Récupération de l'étudiant
etu = models.Identite.query.filter_by(code_ine=ine).first()
# Récupération de l'etudid de l'étudiant
etudid = etu.etudid
try:
# Utilisation de la fonction annule_justif
annule_justif(etudid, jour, matin)
except ValueError:
return error_response(409, message="La requête ne peut être traitée en létat actuel")
return error_response(200, message="OK")
@bp.route("/absences/abs_group_etat/?group_id=<int:group_id>&date_debut=date_debut&date_fin=date_fin", methods=["GET"])
@permission_required(Permission.APIView)
def abs_groupe_etat(group_id: int, date_debut, date_fin, with_boursier=True, format="html"):
"""
Retoune la liste des absences d'un ou plusieurs groupes entre deux dates
"""
# Fonction utilisée : app.scodoc.sco_groups.get_group_members() et app.scodoc.sco_abs.list_abs_date()
try:
# Utilisation de la fonction get_group_members
members = get_group_members(group_id)
except ValueError:
return error_response(409, message="La requête ne peut être traitée en létat actuel")
data = []
# Filtre entre les deux dates renseignées
for member in members:
abs = list_abs_date(member.id, date_debut, date_fin)
data.append(abs)
# return jsonify(data) # XXX TODO faire en sorte de pouvoir renvoyer sa (ex to_dict() dans absences)
return error_response(501, message="Not implemented")

191
app/api/departements.py Normal file
View File

@ -0,0 +1,191 @@
############################################### Departements ##########################################################
from flask import jsonify
from app import models
from app.api import bp
from app.api.auth import token_auth
from app.api.errors import error_response
from app.decorators import permission_required
from app.models import ApcReferentielCompetences
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_prepajury import feuille_preparation_jury
from app.scodoc.sco_pvjury import formsemestre_pvjury
from app.scodoc.sco_recapcomplet import formsemestre_recapcomplet
from app.scodoc.sco_saisie_notes import notes_add
@bp.route("/departements", methods=["GET"])
@token_auth.login_required # Commenté le temps des tests
# @permission_required(Permission.ScoView)
def departements():
"""
Retourne la liste des ids de départements visibles
Exemple de résultat : [2, 5, 8, 1, 4, 18]
"""
# Récupération de tous les départements
depts = models.Departement.query.filter_by(visible=True).all()
# Mise en place de la liste avec tous les ids de départements
depts_ids = [d.id for d in depts]
return jsonify(depts_ids)
@bp.route("/departements/<string:dept>/etudiants/liste", methods=["GET"])
@bp.route("/departements/<string:dept>/etudiants/liste/<int:formsemestre_id>", methods=["GET"])
@token_auth.login_required
# @permission_required(Permission.APIView)
def liste_etudiants(dept: str, formsemestre_id=None):
"""
Retourne la liste des étudiants d'un département
dept: l'acronym d'un département
formsemestre_id: l'id d'un formesemestre
Exemple de résultat :
{
"civilite": "X",
"code_ine": null,
"code_nip": null,
"date_naissance": null,
"email": null,
"emailperso": null,
"etudid": 18,
"nom": "MOREL",
"prenom": "JACQUES"
},
{
"civilite": "X",
"code_ine": null,
"code_nip": null,
"date_naissance": null,
"email": null,
"emailperso": null,
"etudid": 19,
"nom": "FOURNIER",
"prenom": "ANNE"
},
...
"""
# Si le formsemestre_id a été renseigné
if formsemestre_id is not None:
# Récupération du formsemestre
formsemestre = models.FormSemestre.query.filter_by(id=formsemestre_id).first()
# Récupération du département
departement = formsemestre.departement
# Récupération des étudiants
etudiants = departement.etudiants.all()
# Mise en forme des données
list_etu = [etu.to_dict_bul(include_urls=False) for etu in etudiants]
# Si le formsemestre_id n'a pas été renseigné
else:
# Récupération du formsemestre
departement = models.Departement.query.filter_by(acronym=dept).first()
# Récupération des étudiants
etudiants = departement.etudiants.all()
# Mise en forme des données
list_etu = [etu.to_dict_bul(include_urls=False) for etu in etudiants]
return jsonify(list_etu)
@bp.route("/departements/<string:dept>/semestres_courants", methods=["GET"])
# @token_auth.login_required # Commenté le temps des tests
# @permission_required(Permission.APIView)
def liste_semestres_courant(dept: str):
"""
Liste des semestres actifs d'un départements donné
dept: l'acronym d'un département
Exemple de résultat :
[
{
"titre": "master machine info",
"gestion_semestrielle": false,
"scodoc7_id": null,
"date_debut": "01/09/2021",
"bul_bgcolor": null,
"date_fin": "15/12/2022",
"resp_can_edit": false,
"dept_id": 1,
"etat": true,
"resp_can_change_ens": false,
"id": 1,
"modalite": "FI",
"ens_can_edit_eval": false,
"formation_id": 1,
"gestion_compensation": false,
"elt_sem_apo": null,
"semestre_id": 1,
"bul_hide_xml": false,
"elt_annee_apo": null,
"block_moyennes": false,
"formsemestre_id": 1,
"titre_num": "master machine info semestre 1",
"date_debut_iso": "2021-09-01",
"date_fin_iso": "2022-12-15",
"responsables": [
3,
2
]
},
...
]
"""
# Récupération des départements comportant l'acronym mit en paramètre
depts = models.Departement.query.filter_by(acronym=dept).all()
# Récupération de l'id
id_dept = depts[0].id
# Récupération des semestres suivant id_dept
semestres = models.FormSemestre.query.filter_by(dept_id=id_dept, etat=True).all()
# Mise en forme des données
data = [d.to_dict() for d in semestres]
return jsonify(data)
@bp.route("/departements/<string:dept>/formations/<int:formation_id>/referentiel_competences", methods=["GET"])
# @permission_required(Permission.APIView)
def referenciel_competences(dept: str, formation_id: int):
"""
Retourne le référentiel de compétences
dept : l'acronym d'un département
formation_id : l'id d'une formation
"""
depts = models.Departement.query.filter_by(acronym=dept).all()
id_dept = depts[0].id
formations = models.Formation.query.filter_by(id=formation_id, dept_id=id_dept).all()
ref_comp = formations[0].referentiel_competence_id
if ref_comp is None:
return error_response(204, message="Pas de référenciel de compétences pour cette formation")
else:
return jsonify(ref_comp)
# ref = ApcReferentielCompetences.query.get_or_404(formation_id)
#
# return jsonify(ref.to_dict())
@bp.route("/departements/<string:dept>/formsemestre/<string:formsemestre_id>/programme", methods=["GET"])
# @permission_required(Permission.APIView)
def semestre_index(dept: str, formsemestre_id: int):
"""
Retourne la liste des Ues, ressources et SAE d'un semestre
"""
return error_response(501, message="not implemented")

324
app/api/etudiants.py Normal file
View File

@ -0,0 +1,324 @@
#################################################### Etudiants ########################################################
from flask import jsonify
from app import models
from app.api import bp
from app.api.errors import error_response
from app.decorators import permission_required
from app.scodoc.sco_bulletins_json import make_json_formsemestre_bulletinetud
from app.scodoc.sco_groups import get_etud_groups
from app.scodoc.sco_permissions import Permission
@bp.route("/etudiants", methods=["GET"])
#@permission_required(Permission.APIView)
def etudiants():
"""
Retourne la liste de tous les étudiants
Exemple de résultat :
{
"civilite": "X",
"code_ine": null,
"code_nip": null,
"date_naissance": null,
"email": null,
"emailperso": null,
"etudid": 18,
"nom": "MOREL",
"prenom": "JACQUES"
},
{
"civilite": "X",
"code_ine": null,
"code_nip": null,
"date_naissance": null,
"email": null,
"emailperso": null,
"etudid": 19,
"nom": "FOURNIER",
"prenom": "ANNE"
},
...
"""
# Récupération de tous les étudiants
etu = models.Identite.query.all()
# Mise en forme des données
data = [d.to_dict_bul(include_urls=False) for d in etu]
return jsonify(data)
# return error_response(501, message="Not implemented")
@bp.route("/etudiants/courant", methods=["GET"])
#@permission_required(Permission.APIView)
def etudiants_courant():
"""
Retourne la liste des étudiants courant
Exemple de résultat :
{
"civilite": "X",
"code_ine": null,
"code_nip": null,
"date_naissance": null,
"email": null,
"emailperso": null,
"etudid": 18,
"nom": "MOREL",
"prenom": "JACQUES"
},
{
"civilite": "X",
"code_ine": null,
"code_nip": null,
"date_naissance": null,
"email": null,
"emailperso": null,
"etudid": 19,
"nom": "FOURNIER",
"prenom": "ANNE"
},
...
"""
# Récupération de tous les étudiants
etus = models.Identite.query.all()
data = []
# Récupère uniquement les étudiants courant
for etu in etus:
if etu.inscription_courante() is not None:
data.append(etu.to_dict_bul(include_urls=False))
return jsonify(data)
# return error_response(501, message="Not implemented")
@bp.route("/etudiant/etudid/<int:etudid>", methods=["GET"])
@bp.route("/etudiant/nip/<int:nip>", methods=["GET"])
@bp.route("/etudiant/ine/<int:ine>", methods=["GET"])
#@permission_required(Permission.APIView)
def etudiant(etudid: int = None, nip: int = None, ine: int = None):
"""
Retourne les informations de l'étudiant correspondant à l'id passé en paramètres.
etudid : l'etudid d'un étudiant
nip : le code nip d'un étudiant
ine : le code ine d'un étudiant
Exemple de résultat :
{
"civilite": "X",
"code_ine": null,
"code_nip": null,
"date_naissance": null,
"email": null,
"emailperso": null,
"etudid": 18,
"nom": "MOREL",
"prenom": "JACQUES"
}
"""
etu = []
if etudid is not None: # Si route etudid
# Récupération de l'étudiant
etu = models.Identite.query.filter_by(id=etudid).first()
if nip is not None: # Si route nip
# Récupération de l'étudiant
etu = models.Identite.query.filter_by(code_nip=nip).first()
if ine is not None: # Si route ine
# Récupération de l'étudiant
etu = models.Identite.query.filter_by(code_ine=ine).first()
# Mise en forme des données
data = etu.to_dict_bul(include_urls=False)
return jsonify(data)
@bp.route("/etudiant/etudid/<int:etudid>/formsemestres")
@bp.route("/etudiant/nip/<int:nip>/formsemestres")
@bp.route("/etudiant/ine/<int:ine>/formsemestres")
#@permission_required(Permission.APIView)
def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None):
"""
Retourne la liste des semestres qu'un étudiant a suivis
etudid : l'etudid d'un étudiant
nip : le code nip d'un étudiant
ine : le code ine d'un étudiant
Exemple de résultat :
[
{
"titre": "master machine info",
"gestion_semestrielle": false,
"scodoc7_id": null,
"date_debut": "01/09/2021",
"bul_bgcolor": null,
"date_fin": "15/12/2022",
"resp_can_edit": false,
"dept_id": 1,
"etat": true,
"resp_can_change_ens": false,
"id": 1,
"modalite": "FI",
"ens_can_edit_eval": false,
"formation_id": 1,
"gestion_compensation": false,
"elt_sem_apo": null,
"semestre_id": 1,
"bul_hide_xml": false,
"elt_annee_apo": null,
"block_moyennes": false,
"formsemestre_id": 1,
"titre_num": "master machine info semestre 1",
"date_debut_iso": "2021-09-01",
"date_fin_iso": "2022-12-15",
"responsables": [
3,
2
]
},
...
]
"""
# Récupération de toutes les inscriptions
inscriptions = models.FormSemestreInscription.query.all()
sems = []
# Filtre les inscriptions contenant l'étudiant
for sem in inscriptions:
if etudid is not None: # Si route etudid
if sem.etudid == etudid:
sems.append(sem)
if nip is not None: # Si route nip
# Récupération de l'étudiant
etu = models.Identite.query.filter_by(code_nip=nip).first()
if sem.etudid == etu.etudid:
sems.append(sem)
if ine is not None: # Si route ine
# Récupération de l'étudiant
etu = models.Identite.query.filter_by(code_ine=ine).firt()
if sem.etudid == etu.etudid:
sems.append(sem)
# Mise en forme des données
# data_inscriptions = [d.to_dict() for d in sems]
formsemestres = []
# Filtre les formsemestre contenant les inscriptions de l'étudiant
for sem in sems:#data_inscriptions:
res = models.FormSemestre.query.filter_by(id=sem.formsemestre_id).first()
formsemestres.append(res)
data = []
# Mise en forme des données
for formsem in formsemestres:
data.append(formsem.to_dict())
return jsonify(data)
@bp.route("/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin", methods=["GET"])
@bp.route("/etudiant/nip/<int:nip>/formsemestre/<int:formsemestre_id>/bulletin", methods=["GET"])
@bp.route("/etudiant/ine/<int:ine>/formsemestre/<int:formsemestre_id>/bulletin", methods=["GET"])
#@permission_required(Permission.APIView)
def etudiant_bulletin_semestre(formsemestre_id, etudid: int = None, nip: int = None, ine: int = None):
"""
Retourne le bulletin d'un étudiant en fonction de son id et d'un semestre donné
formsemestre_id : l'id d'un formsemestre
etudid : l'etudid d'un étudiant
nip : le code nip d'un étudiant
ine : le code ine d'un étudiant
"""
# Fonction utilisée : app.scodoc.sco_bulletins_json.make_json_formsemestre_bulletinetud()
etu = None
if etudid is not None: # Si route etudid
return make_json_formsemestre_bulletinetud(formsemestre_id, etudid)
else:
if nip is not None: # Si route nip
etu = models.Identite.query.filter_by(code_nip=nip).first()
if ine is not None: # Si route ine
etu = models.Identite.query.filter_by(code_nip=ine).first()
if etu is not None: # Si route nip ou ine
return make_json_formsemestre_bulletinetud(formsemestre_id, etu.etudid)
# return error_response(501, message="Not implemented")
@bp.route("/etudiant/etudid/<int:etudid>/semestre/<int:formsemestre_id>/groups", methods=["GET"])
@bp.route("/etudiant/nip/<int:nip>/semestre/<int:formsemestre_id>/groups", methods=["GET"])
@bp.route("/etudiant/ine/<int:ine>/semestre/<int:formsemestre_id>/groups", methods=["GET"])
#@permission_required(Permission.APIView)
def etudiant_groups(formsemestre_id: int, etudid: int = None, nip: int = None, ine: int = None):
"""
Retourne la liste des groupes auxquels appartient l'étudiant dans le semestre indiqué
formsemestre_id : l'id d'un formsemestre
etudid : l'etudid d'un étudiant
nip : le code nip d'un étudiant
ine : le code ine d'un étudiant
Exemple de résultat :
[
{
"partition_id": 1,
"id": 1,
"formsemestre_id": 1,
"partition_name": null,
"numero": 0,
"bul_show_rank": false,
"show_in_lists": true,
"group_id": 1,
"group_name": null
},
{
"partition_id": 2,
"id": 2,
"formsemestre_id": 1,
"partition_name": "TD",
"numero": 1,
"bul_show_rank": false,
"show_in_lists": true,
"group_id": 2,
"group_name": "A"
}
]
"""
# Fonction utilisée : app.scodoc.sco_groups.get_etud_groups()
if etudid is None:
if nip is not None: # Si route nip
# Récupération de l'étudiant
etu = models.Identite.query.filter_by(code_nip=nip).first()
# Récupération de sont etudid
etudid = etu.etudid
if ine is not None: # Si route ine
# Récupération de l'étudiant
etu = models.Identite.query.filter_by(code_ine=ine).first()
# Récupération de sont etudid
etudid = etu.etudid
# Récupération du formsemestre
sem = models.FormSemestre.query.filter_by(id=formsemestre_id).first()
try:
# Utilisation de la fonction get_etud_groups
data = get_etud_groups(etudid, sem.to_dict())
except ValueError:
return error_response(409, message="La requête ne peut être traitée en létat actuel")
return jsonify(data)

69
app/api/evaluations.py Normal file
View File

@ -0,0 +1,69 @@
############################################### Evaluations ###########################################################
from flask import jsonify
from app import models
from app.api import bp
from app.api.auth import token_auth
from app.api.errors import error_response
from app.decorators import permission_required
from app.scodoc.sco_evaluation_db import do_evaluation_get_all_notes
from app.scodoc.sco_permissions import Permission
@bp.route("/evaluations/<int:moduleimpl_id>", methods=["GET"])
@permission_required(Permission.APIView)
def evaluations(moduleimpl_id: int):
"""
Retourne la liste des évaluations à partir de l'id d'un moduleimpl
moduleimpl_id : l'id d'un moduleimpl
"""
# Récupération de toutes les évaluations
evals = models.Evaluation.query.filter_by(id=moduleimpl_id).all()
# Mise en forme des données
data = [d.to_dict() for d in evals]
return jsonify(data)
# return error_response(501, message="Not implemented")
@bp.route("/evaluations/eval_notes/<int:evaluation_id>", methods=["GET"])
@permission_required(Permission.APIView)
def evaluation_notes(evaluation_id: int):
"""
Retourne la liste des notes à partir de l'id d'une évaluation donnée
evaluation_id : l'id d'une évaluation
"""
# Fonction utilisée : app.scodoc.sco_evaluation_db.do_evaluation_get_all_notes()
try:
# Utilisation de la fonction do_evaluation_get_all_notes
data = do_evaluation_get_all_notes(evaluation_id)
except ValueError:
return error_response(409, message="La requête ne peut être traitée en létat actuel")
return jsonify(data)
@bp.route("/evaluations/eval_set_notes?eval_id=<int:eval_id>&etudid=<int:etudid>&note=<float:note>", methods=["POST"])
@bp.route("/evaluations/eval_set_notes?eval_id=<int:eval_id>&nip=<int:nip>&note=<float:note>", methods=["POST"])
@bp.route("/evaluations/eval_set_notes?eval_id=<int:eval_id>&ine=<int:ine>&note=<float:note>", methods=["POST"])
@token_auth.login_required
@permission_required(Permission.APIEditAllNotes)
def evaluation_set_notes(eval_id: int, note: float, etudid: int = None, nip: int = None, ine: int = None):
"""
Set les notes d'une évaluation pour un étudiant donnée
eval_id : l'id d'une évaluation
note : la note à attribuer
etudid : l'etudid d'un étudiant
nip : le code nip d'un étudiant
ine : le code ine d'un étudiant
"""
# Fonction utilisée : app.scodoc.sco_saisie_notes.notes_add()
# Qu'est ce qu'un user ???
# notes_add()
return error_response(501, message="Not implemented")

117
app/api/formations.py Normal file
View File

@ -0,0 +1,117 @@
##############################################" Formations ############################################################
from flask import jsonify
from app import models
from app.api import bp
from app.api.errors import error_response
from app.decorators import permission_required
from app.scodoc.sco_formations import formation_export
from app.scodoc.sco_moduleimpl import moduleimpl_list
from app.scodoc.sco_permissions import Permission
@bp.route("/formations", methods=["GET"])
@permission_required(Permission.APIView)
def formations():
"""
Retourne la liste des formations
"""
# Récupération de toutes les formations
list_formations = models.Formation.query.all()
# Mise en forme des données
data = [d.to_dict() for d in list_formations]
return jsonify(data)
@bp.route("/formations/<int:formation_id>", methods=["GET"])
@permission_required(Permission.APIView)
def formations_by_id(formation_id: int):
"""
Retourne une formation en fonction d'un id donné
formation_id : l'id d'une formation
"""
# Récupération de la formation
forma = models.Formation.query.filter_by(id=formation_id).first()
# Mise en forme des données
data = [d.to_dict() for d in forma]
return jsonify(data)
@bp.route("/formations/formation_export/<int:formation_id>", methods=["GET"])
@permission_required(Permission.APIView)
def formation_export_by_formation_id(formation_id: int, export_ids=False):
"""
Retourne la formation, avec UE, matières, modules
"""
# Fonction utilité : app.scodoc.sco_formations.formation_export()
try:
# Utilisation de la fonction formation_export
data = formation_export(formation_id)
except ValueError:
return error_response(409, message="La requête ne peut être traitée en létat actuel")
return jsonify(data)
@bp.route("/formations/apo/<string:etape_apo>", methods=["GET"])
@permission_required(Permission.APIView)
def formsemestre_apo(etape_apo: int):
"""
Retourne les informations sur les formsemestres
etape_apo : l'id d'une étape apogée
"""
# Récupération des formsemestres
apos = models.FormSemestreEtape.query.filter_by(etape_apo=etape_apo).all()
data = []
# Filtre les formsemestres correspondant + mise en forme des données
for apo in apos:
formsem = models.FormSemestre.query.filter_by(id=apo["formsemestre_id"]).first()
data.append(formsem.to_dict())
return jsonify(data)
# return error_response(501, message="Not implemented")
@bp.route("/formations/moduleimpl/<int:moduleimpl_id>", methods=["GET"])
@permission_required(Permission.APIView)
def moduleimpls(moduleimpl_id: int):
"""
Retourne la liste des moduleimpl
moduleimpl_id : l'id d'un moduleimpl
"""
# Récupération des tous les moduleimpl
list_moduleimpls = models.ModuleImpl.query.filter_by(id=moduleimpl_id).all()
# Mise en forme des données
data = list_moduleimpls[0].to_dict()
return jsonify(data)
@bp.route("/formations/moduleimpl/<int:moduleimpl_id>/formsemestre/<int:formsemestre_id>", methods=["GET"])
@permission_required(Permission.APIView)
def moduleimpls_sem(moduleimpl_id: int, formsemestre_id: int):
"""
Retourne la liste des moduleimpl d'un semestre
moduleimpl_id : l'id d'un moduleimpl
formsemestre_id : l'id d'un formsemestre
"""
# Fonction utilisée : app.scodoc.sco_moduleimpl.moduleimpl_list()
try:
# Utilisation de la fonction moduleimpl_list
data = moduleimpl_list(moduleimpl_id, formsemestre_id)
except ValueError:
return error_response(409, message="La requête ne peut être traitée en létat actuel")
return jsonify(data)

104
app/api/formsemestres.py Normal file
View File

@ -0,0 +1,104 @@
########################################## Formsemestres ##############################################################
from flask import jsonify
from app import models
from app.api import bp
from app.api.errors import error_response
from app.decorators import permission_required
from app.scodoc.sco_bulletins import formsemestre_bulletinetud_dict
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_pvjury import formsemestre_pvjury
from app.scodoc.sco_recapcomplet import formsemestre_recapcomplet
@bp.route("/formations/formsemestre/<int:formsemestre_id>", methods=["GET"])
@permission_required(Permission.APIView)
def formsemestre(formsemestre_id: int):
"""
Retourne l'information sur le formsemestre correspondant au formsemestre_id
formsemestre_id : l'id d'un formsemestre
"""
# Récupération de tous les formsemestres
list_formsemetre = models.FormSemestre.query.filter_by(id=formsemestre_id)
# Mise en forme des données
data = list_formsemetre[0].to_dict()
return jsonify(data)
@bp.route(
"/formsemestre/<int:formsemestre_id>/departements/<string:dept>/etudiant/etudid/<int:etudid>/bulletin",
methods=["GET"],
)
@bp.route(
"/formsemestre/<int:formsemestre_id>/departements/<string:dept>/etudiant/nip/<int:nip>/bulletin",
methods=["GET"],
)
@bp.route(
"/formsemestre/<int:formsemestre_id>/departements/<string:dept>/etudiant/ine/<int:ine>/bulletin",
methods=["GET"],
)
@permission_required(Permission.APIView)
def etudiant_bulletin(formsemestre_id, dept, etudid, format="json", *args, size):
"""
Retourne le bulletin de note d'un étudiant
formsemestre_id : l'id d'un formsemestre
etudid : l'etudid d'un étudiant
nip : le code nip d'un étudiant
ine : le code ine d'un étudiant
"""
# Fonction utilisée : app.scodoc.sco_bulletins.formsemestre_billetinetud_dict()
data = []
if args[0] == "short":
data = formsemestre_bulletinetud_dict(formsemestre_id, etudid, version=args[0])
elif args[0] == "selectevals":
data = formsemestre_bulletinetud_dict(formsemestre_id, etudid, version=args[0])
elif args[0] == "long":
data = formsemestre_bulletinetud_dict(formsemestre_id, etudid)
else:
return error_response(409, message="La requête ne peut être traitée en létat actuel")
return jsonify(data)
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins", methods=["GET"])
@permission_required(Permission.APIView)
def bulletins(formsemestre_id: int):
"""
Retourne les bulletins d'un formsemestre donné
formsemestre_id : l'id d'un formesemestre
"""
# Fonction utilisée : app.scodoc.sco_recapcomplet.formsemestre_recapcomplet()
try:
# Utilisation de la fonction formsemestre_recapcomplet
data = formsemestre_recapcomplet(formsemestre_id)
except ValueError:
return error_response(409, message="La requête ne peut être traitée en létat actuel")
return jsonify(data)
@bp.route("/formsemestre/<int:formsemestre_id>/jury", methods=["GET"])
@permission_required(Permission.APIView)
def jury(formsemestre_id: int):
"""
Retourne le récapitulatif des décisions jury
formsemestre_id : l'id d'un formsemestre
"""
# Fonction utilisée : app.scodoc.sco_pvjury.formsemestre_pvjury()
try:
# Utilisation de la fonction formsemestre_pvjury
data = formsemestre_pvjury(formsemestre_id)
except ValueError:
return error_response(409, message="La requête ne peut être traitée en létat actuel")
return jsonify(data)

75
app/api/jury.py Normal file
View File

@ -0,0 +1,75 @@
#################################################### Jury #############################################################
from flask import jsonify
from app import models
from app.api import bp
from app.api.errors import error_response
from app.scodoc.sco_prepajury import feuille_preparation_jury
from app.scodoc.sco_pvjury import formsemestre_pvjury
@bp.route("/jury/formsemestre/<int:formsemestre_id>/preparation_jury", methods=["GET"])
def jury_preparation(formsemestre_id: int): # XXX TODO check à quoi resemble le retour de la fonction
"""
Retourne la feuille de préparation du jury
formsemestre_id : l'id d'un formsemestre
"""
# Fonction utilisée : app.scodoc.sco_prepajury.feuille_preparation_jury()
# Utilisation de la fonction feuille_preparation_jury
prepa_jury = feuille_preparation_jury(formsemestre_id)
return error_response(501, message="Not implemented")
@bp.route("/jury/formsemestre/<int:formsemestre_id>/decisions_jury", methods=["GET"])
def jury_decisions(formsemestre_id: int): # XXX TODO check à quoi resemble le retour de la fonction
"""
Retourne les décisions du jury suivant un formsemestre donné
formsemestre_id : l'id d'un formsemestre
"""
# Fonction utilisée : app.scodoc.sco_pvjury.formsemestre_pvjury()
# Utilisation de la fonction formsemestre_pvjury
decision_jury = formsemestre_pvjury(formsemestre_id)
return error_response(501, message="Not implemented")
@bp.route("/jury/set_decision/etudid?etudid=<int:etudid>&formsemestre_id=<int:formesemestre_id>"
"&jury=<string:decision_jury>&devenir=<string:devenir_jury>&assiduite=<bool>", methods=["POST"])
@bp.route("/jury/set_decision/nip?etudid=<int:etudid>&formsemestre_id=<int:formesemestre_id>"
"&jury=<string:decision_jury>&devenir=<string:devenir_jury>&assiduite=<bool>", methods=["POST"])
@bp.route("/jury/set_decision/ine?etudid=<int:etudid>&formsemestre_id=<int:formesemestre_id>"
"&jury=<string:decision_jury>&devenir=<string:devenir_jury>&assiduite=<bool>", methods=["POST"])
def set_decision_jury(formsemestre_id: int, decision_jury: str, devenir_jury: str, assiduite: bool,
etudid: int = None, nip: int = None, ine: int = None):
"""
Attribuer la décision du jury et le devenir à un etudiant
formsemestre_id : l'id d'un formsemestre
decision_jury : la décision du jury
devenir_jury : le devenir du jury
assiduite : True ou False
etudid : l'etudid d'un étudiant
nip: le code nip d'un étudiant
ine : le code ine d'un étudiant
"""
return error_response(501, message="Not implemented")
@bp.route("/jury/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/annule_decision", methods=["DELETE"])
@bp.route("/jury/nip/<int:nip>/formsemestre/<int:formsemestre_id>/annule_decision", methods=["DELETE"])
@bp.route("/jury/ine/<int:ine>/formsemestre/<int:formsemestre_id>/annule_decision", methods=["DELETE"])
def annule_decision_jury(formsemestre_id: int, etudid: int = None, nip: int = None, ine: int = None):
"""
Supprime la déciosion du jury pour un étudiant donné
formsemestre_id : l'id d'un formsemestre
etudid : l'etudid d'un étudiant
nip: le code nip d'un étudiant
ine : le code ine d'un étudiant
"""
return error_response(501, message="Not implemented")

View File

@ -36,6 +36,7 @@ from app.api import bp
from app.api import requested_format
from app.api.auth import token_auth
from app.api.errors import error_response
from app.decorators import permission_required
from app.models import Departement
from app.scodoc.sco_logos import list_logos, find_logo
from app.scodoc.sco_permissions import Permission
@ -43,6 +44,7 @@ from app.scodoc.sco_permissions import Permission
@bp.route("/logos", methods=["GET"])
@token_auth.login_required
@permission_required(Permission.APIView)
def api_get_glob_logos():
if not g.current_user.has_permission(Permission.ScoSuperAdmin, None):
return error_response(401, message="accès interdit")
@ -55,6 +57,7 @@ def api_get_glob_logos():
@bp.route("/logos/<string:logoname>", methods=["GET"])
@token_auth.login_required
@permission_required(Permission.APIView)
def api_get_glob_logo(logoname):
if not g.current_user.has_permission(Permission.ScoSuperAdmin, None):
return error_response(401, message="accès interdit")
@ -71,6 +74,7 @@ def api_get_glob_logo(logoname):
@bp.route("/departements/<string:departement>/logos", methods=["GET"])
@token_auth.login_required
@permission_required(Permission.APIView)
def api_get_local_logos(departement):
dept_id = Departement.from_acronym(departement).id
if not g.current_user.has_permission(Permission.ScoChangePreferences, departement):
@ -81,6 +85,7 @@ def api_get_local_logos(departement):
@bp.route("/departements/<string:departement>/logos/<string:logoname>", methods=["GET"])
@token_auth.login_required
@permission_required(Permission.APIView)
def api_get_local_logo(departement, logoname):
# format = requested_format("jpg", ['png', 'jpg']) XXX ?
dept_id = Departement.from_acronym(departement).id

84
app/api/partitions.py Normal file
View File

@ -0,0 +1,84 @@
############################################### Partitions ############################################################
from flask import jsonify
from app import models
from app.api import bp
from app.api.auth import token_auth
from app.api.errors import error_response
from app.decorators import permission_required
from app.scodoc.sco_groups import get_group_members, setGroups
from app.scodoc.sco_permissions import Permission
@bp.route("/partitions/<int:formsemestre_id>", methods=["GET"])
@permission_required(Permission.APIView)
def partition(formsemestre_id: int):
"""
Retourne la liste de toutes les partitions d'un formsemestre
formsemestre_id : l'id d'un formsemestre
"""
# Récupération de toutes les partitions
partitions = models.Partition.query.filter_by(id=formsemestre_id).all()
# Mise en forme des données
data = [d.to_dict() for d in partitions]
return jsonify(data)
# return error_response(501, message="Not implemented")
# @bp.route(
# "/partitions/formsemestre/<int:formsemestre_id>/groups/group_ids?with_codes=&all_groups=&etat=",
# methods=["GET"],
# )
@bp.route("/partitions/groups/<int:group_id>", methods=["GET"])
@bp.route("/partitions/groups/<int:group_id>/etat/<string:etat>", methods=["GET"])
@permission_required(Permission.APIView)
def etud_in_group(group_id: int, etat=None):
"""
Retourne la liste des étudiants dans un groupe
group_id : l'id d'un groupe
etat :
"""
# Fonction utilisée : app.scodoc.sco_groups.get_group_members()
if etat is None: # Si l'état n'est pas renseigné
try:
# Utilisation de la fonction get_group_members
data = get_group_members(group_id)
except ValueError:
return error_response(409, message="La requête ne peut être traitée en létat actuel")
else: # Si l'état est renseigné
try:
# Utilisation de la fonction get_group_members
data = get_group_members(group_id, etat)
except ValueError:
return error_response(409, message="La requête ne peut être traitée en létat actuel")
return jsonify(data)
@bp.route(
"/partitions/set_groups?partition_id=<int:partition_id>&groups_lists=<int:groups_lists>&"
"groups_to_create=<int:groups_to_create>&groups_to_delete=<int:groups_to_delete>", methods=["POST"],
)
@token_auth.login_required
@permission_required(Permission.APIEtudChangeGroups)
def set_groups(partition_id: int, groups_lists: int, groups_to_delete: int, groups_to_create: int):
"""
Set les groups
partition_id : l'id d'une partition
groups_lists :
groups_ti_delete : les groupes à supprimer
groups_to_create : les groupes à créer
"""
# Fonction utilisée : app.scodoc.sco_groups.setGroups()
try:
# Utilisation de la fonction setGroups
setGroups(partition_id, groups_lists, groups_to_create, groups_to_delete)
return error_response(200, message="Groups set")
except ValueError:
return error_response(409, message="La requête ne peut être traitée en létat actuel")

View File

@ -50,442 +50,98 @@ from app.api.errors import error_response
from app import models
from app.models import FormSemestre, FormSemestreInscription, Identite
from app.models import ApcReferentielCompetences
from app.scodoc.sco_abs import annule_absence, annule_justif, add_absence, add_justif, list_abs_date
from app.scodoc.sco_bulletins import formsemestre_bulletinetud_dict
from app.scodoc.sco_bulletins_json import make_json_formsemestre_bulletinetud
from app.scodoc.sco_evaluation_db import do_evaluation_get_all_notes
from app.scodoc.sco_formations import formation_export
from app.scodoc.sco_formsemestre_inscriptions import do_formsemestre_inscription_listinscrits
from app.scodoc.sco_groups import setGroups, get_etud_groups, get_group_members
from app.scodoc.sco_logos import list_logos, find_logo, _list_dept_logos
from app.scodoc.sco_moduleimpl import moduleimpl_list
from app.scodoc.sco_permissions import Permission
@bp.route("/list_depts", methods=["GET"])
@token_auth.login_required
def list_depts():
depts = models.Departement.query.filter_by(visible=True).all()
data = [d.to_dict() for d in depts]
return jsonify(data)
# ###################################################### Logos ##########################################################
#
# # XXX TODO voir get_logo déjà existant dans app/views/scodoc.py
#
# @bp.route("/logos", methods=["GET"])
# def liste_logos(format="json"):
# """
# Liste des logos définis pour le site scodoc.
# """
# # fonction to use : list_logos()
# # try:
# # res = list_logos()
# # except ValueError:
# # return error_response(409, message="La requête ne peut être traitée en létat actuel")
# #
# # if res is None:
# # return error_response(200, message="Aucun logo trouvé correspondant aux informations renseignés")
# #
# # return res
#
#
#
# @bp.route("/logos/<string:logo_name>", methods=["GET"])
# def recup_logo_global(logo_name: str):
# """
# Retourne l'image au format png ou jpg
#
# logo_name : le nom du logo rechercher
# """
# # fonction to use find_logo
# # try:
# # res = find_logo(logo_name)
# # except ValueError:
# # return error_response(409, message="La requête ne peut être traitée en létat actuel")
# #
# # if res is None:
# # return error_response(200, message="Aucun logo trouvé correspondant aux informations renseignés")
# #
# # return res
#
#
# @bp.route("/departements/<string:dept>/logos", methods=["GET"])
# def logo_dept(dept: str):
# """
# Liste des logos définis pour le département visé.
#
# dept : l'id d'un département
# """
# # fonction to use: _list_dept_logos
# # dept_id = models.Departement.query.filter_by(acronym=dept).first()
# # try:
# # res = _list_dept_logos(dept_id.id)
# # except ValueError:
# # return error_response(409, message="La requête ne peut être traitée en létat actuel")
# #
# # if res is None:
# # return error_response(200, message="Aucun logo trouvé correspondant aux informations renseignés")
# #
# # return res
#
#
# @bp.route("/departement/<string:dept>/logos/<string:logo_name>", methods=["GET"])
# def recup_logo_dept_global(dept: str, logo_name: str):
# """
# L'image format png ou jpg
#
# dept : l'id d'un département
# logo_name : le nom du logo rechercher
# """
# # fonction to use find_logo
# # dept_id = models.Departement.query.filter_by(acronym=dept).first()
# # try:
# # res = find_logo(logo_name, dept_id.id)
# # except ValueError:
# # return error_response(409, message="La requête ne peut être traitée en létat actuel")
# #
# # if res is None:
# # return error_response(200, message="Aucun logo trouvé correspondant aux informations renseignés")
# #
# # return res
@bp.route("/etudiants/courant", methods=["GET"])
@token_auth.login_required
def etudiants():
"""Liste de tous les étudiants actuellement inscrits à un semestre
en cours.
"""
# Vérification de l'accès: permission Observateur sur tous les départements
# (c'est un exemple à compléter)
if not g.current_user.has_permission(Permission.ScoObservateur, None):
return error_response(401, message="accès interdit")
query = db.session.query(Identite).filter(
FormSemestreInscription.formsemestre_id == FormSemestre.id,
FormSemestreInscription.etudid == Identite.id,
FormSemestre.date_debut <= func.now(),
FormSemestre.date_fin >= func.now(),
)
return jsonify([e.to_dict_bul(include_urls=False) for e in query])
######################## Departements ##################################
@bp.route("/departements", methods=["GET"])
@token_auth.login_required
def departements():
"""
Liste des ids de départements
"""
depts = models.Departement.query.filter_by(visible=True).all()
data = [d.id for d in depts]
return jsonify(data)
@bp.route("/departements/<string:dept>/etudiants/liste/<int:sem_id>", methods=["GET"])
@token_auth.login_required
def liste_etudiants(dept, *args, sem_id): # XXX TODO A REVOIR
"""
Liste des étudiants d'un département
"""
# Test si le sem_id à été renseigné ou non
if sem_id is not None:
# Récupération du/des depts
list_depts = models.Departement.query.filter(
models.Departement.acronym == dept,
models.FormSemestre.semestre_id == sem_id,
)
list_etuds = []
for dept in list_depts:
# Récupération des étudiants d'un département
x = models.Identite.query.filter(models.Identite.dept_id == dept.getId())
for y in x:
# Ajout des étudiants dans la liste global
list_etuds.append(y)
else:
list_depts = models.Departement.query.filter(
models.Departement.acronym == dept,
models.FormSemestre.semestre_id == models.Departement.formsemestres,
)
list_etuds = []
for dept in list_depts:
x = models.Identite.query.filter(models.Identite.dept_id == dept.getId())
for y in x:
list_etuds.append(y)
data = [d.to_dict() for d in list_etuds]
# return jsonify(data)
return error_response(501, message="Not implemented")
@bp.route("/departements/<string:dept>/semestres_actifs", methods=["GET"])
@token_auth.login_required
def liste_semestres_actifs(dept): # TODO : changer nom
"""
Liste des semestres actifs d'un départements donné
"""
# Récupération de l'id du dept
dept_id = models.Departement.query.filter(models.Departement.acronym == dept)
# Puis ici récupération du FormSemestre correspondant
depts_actifs = models.FormSemestre.query.filter_by(
etat=True,
dept_id=dept_id,
)
data = [da.to_dict() for da in depts_actifs]
# return jsonify(data)
return error_response(501, message="Not implemented")
@bp.route("/referentiel_competences/<int:referentiel_competence_id>")
@token_auth.login_required
def referentiel_competences(referentiel_competence_id):
"""
Le référentiel de compétences
"""
ref = ApcReferentielCompetences.query.get_or_404(referentiel_competence_id)
return jsonify(ref.to_dict())
####################### Etudiants ##################################
@bp.route("/etudiant/<int:etudid>", methods=["GET"])
@token_auth.login_required
def etudiant(etudid):
"""
Un dictionnaire avec les informations de l'étudiant correspondant à l'id passé en paramètres.
"""
etud: Identite = Identite.query.get_or_404(etudid)
return jsonify(etud.to_dict_bul())
@bp.route("/etudiant/<int:etudid>/semestre/<int:sem_id>/bulletin", methods=["GET"])
@token_auth.login_required
def etudiant_bulletin_semestre(etudid, sem_id):
"""
Le bulletin d'un étudiant en fonction de son id et d'un semestre donné
"""
# return jsonify(models.BulAppreciations.query.filter_by(etudid=etudid, formsemestre_id=sem_id))
return error_response(501, message="Not implemented")
@bp.route(
"/formsemestre/<int:formsemestre_id>/departements/<string:dept>/etudiant/nip/<int:NIP>/releve",
methods=["GET"],
)
@bp.route(
"/formsemestre/<int:formsemestre_id>/departements/<string:dept>/etudiant/id/<int:etudid>/releve",
methods=["GET"],
)
@bp.route(
"/formsemestre/<int:formsemestre_id>/departements/<string:dept>/etudiant/ine/<int:numScodoc>/releve",
methods=["GET"],
)
@token_auth.login_required
def etudiant_bulletin(formsemestre_id, dept, etudid, format="json", *args, size):
"""
Un bulletin de note
"""
formsemestres = models.FormSemestre.query.filter_by(id=formsemestre_id)
depts = models.Departement.query.filter_by(acronym=dept)
etud = ""
data = []
if args[0] == "short":
pass
elif args[0] == "selectevals":
pass
elif args[0] == "long":
pass
else:
return "erreur"
# return jsonify(data)
return error_response(501, message="Not implemented")
@bp.route(
"/etudiant/<int:etudid>/semestre/<int:formsemestre_id>/groups", methods=["GET"]
)
@token_auth.login_required
def etudiant_groups(etudid: int, formsemestre_id: int):
"""
Liste des groupes auxquels appartient l'étudiant dans le semestre indiqué
"""
semestre = models.FormSemestre.query.filter_by(id=formsemestre_id)
etudiant = models.Identite.query.filter_by(id=etudid)
groups = models.Partition.query.filter(
models.Partition.formsemestre_id == semestre,
models.GroupDescr.etudiants == etudiant,
)
data = [d.to_dict() for d in groups]
# return jsonify(data)
return error_response(501, message="Not implemented")
#######################" Programmes de formations #########################
@bp.route("/formations", methods=["GET"])
@bp.route("/formations/<int:formation_id>", methods=["GET"])
@token_auth.login_required
def formations(formation_id: int):
"""
Liste des formations
"""
formations = models.Formation.query.filter_by(id=formation_id)
data = [d.to_dict() for d in formations]
# return jsonify(data)
return error_response(501, message="Not implemented")
@bp.route("/formations/formation_export/<int:formation_id>", methods=["GET"])
@token_auth.login_required
def formation_export(formation_id: int, export_ids=False):
"""
La formation, avec UE, matières, modules
"""
return error_response(501, message="Not implemented")
###################### UE #######################################
@bp.route(
"/departements/<string:dept>/formations/programme/<string:sem_id>", methods=["GET"]
)
@token_auth.login_required
def eus(dept: str, sem_id: int):
"""
Liste des UES, ressources et SAE d'un semestre
"""
return error_response(501, message="Not implemented")
######## Semestres de formation ###############
@bp.route("/formations/formsemestre/<int:formsemestre_id>", methods=["GET"])
@bp.route("/formations/apo/<int:etape_apo>", methods=["GET"])
@token_auth.login_required
def formsemestre(
id: int,
):
"""
Information sur les formsemestres
"""
return error_response(501, message="Not implemented")
############ Modules de formation ##############
@bp.route("/formations/moduleimpl/<int:moduleimpl_id>", methods=["GET"])
@bp.route(
"/formations/moduleimpl/<int:moduleimpl_id>/formsemestre/<int:formsemestre_id>",
methods=["GET"],
)
@token_auth.login_required
def moduleimpl(id: int):
"""
Liste de moduleimpl
"""
return error_response(501, message="Not implemented")
########### Groupes et partitions ###############
@bp.route("/partitions/<int:formsemestre_id>", methods=["GET"])
@token_auth.login_required
def partition(formsemestre_id: int):
"""
La liste de toutes les partitions d'un formsemestre
"""
partitions = models.Partition.query.filter_by(id=formsemestre_id)
data = [d.to_dict() for d in partitions]
# return jsonify(data)
return error_response(501, message="Not implemented")
@bp.route(
"/partitions/formsemestre/<int:formsemestre_id>/groups/group_ids?with_codes=&all_groups=&etat=",
methods=["GET"],
)
@token_auth.login_required
def groups(formsemestre_id: int, group_ids: int):
"""
Liste des étudiants dans un groupe
"""
return error_response(501, message="Not implemented")
@bp.route(
"/partitions/set_groups?partition_id=<int:partition_id>&groups=<int:groups>&groups_to_delete=<int:groups_to_delete>&groups_to_create=<int:groups_to_create>",
methods=["POST"],
)
@token_auth.login_required
def set_groups(
partition_id: int, groups: int, groups_to_delete: int, groups_to_create: int
):
"""
Set les groups
"""
return error_response(501, message="Not implemented")
####### Bulletins de notes ###########
@bp.route("/evaluations/<int:moduleimpl_id>", methods=["GET"])
@token_auth.login_required
def evaluations(moduleimpl_id: int):
"""
Liste des évaluations à partir de l'id d'un moduleimpl
"""
evals = models.Evaluation.query.filter_by(id=moduleimpl_id)
data = [d.to_dict() for d in evals]
# return jsonify(data)
return error_response(501, message="Not implemented")
@bp.route("/evaluations/eval_notes/<int:evaluation_id>", methods=["GET"])
@token_auth.login_required
def evaluation_notes(evaluation_id: int):
"""
Liste des notes à partir de l'id d'une évaluation donnée
"""
evals = models.Evaluation.query.filter_by(id=evaluation_id)
notes = evals.get_notes()
data = [d.to_dict() for d in notes]
# return jsonify(data)
return error_response(501, message="Not implemented")
@bp.route(
"/evaluations/eval_set_notes?eval_id=<int:eval_id>&etudid=<int:etudid>&note=<int:note>",
methods=["POST"],
)
@token_auth.login_required
def evaluation_set_notes(eval_id: int, etudid: int, note: float):
"""
Set les notes d'une évaluation pour un étudiant donnée
"""
return error_response(501, message="Not implemented")
############## Absences #############
@bp.route("/absences/<int:etudid>", methods=["GET"])
@bp.route("/absences/<int:etudid>/abs_just_only", methods=["GET"])
def absences(etudid: int):
"""
Liste des absences d'un étudiant donnée
"""
abs = models.Absence.query.filter_by(id=etudid)
data = [d.to_dict() for d in abs]
# return jsonify(data)
return error_response(501, message="Not implemented")
@bp.route("/absences/abs_signale", methods=["POST"])
@token_auth.login_required
def abs_signale():
"""
Retourne un html
"""
return error_response(501, message="Not implemented")
@bp.route("/absences/abs_annule", methods=["POST"])
@token_auth.login_required
def abs_annule():
"""
Retourne un html
"""
return error_response(501, message="Not implemented")
@bp.route("/absences/abs_annule_justif", methods=["POST"])
@token_auth.login_required
def abs_annule_justif():
"""
Retourne un html
"""
return error_response(501, message="Not implemented")
@bp.route(
"/absences/abs_group_etat/?group_ids=<int:group_ids>&date_debut=date_debut&date_fin=date_fin",
methods=["GET"],
)
@token_auth.login_required
def abs_groupe_etat(
group_ids: int, date_debut, date_fin, with_boursier=True, format="html"
):
"""
Liste des absences d'un ou plusieurs groupes entre deux dates
"""
return error_response(501, message="Not implemented")
################ Logos ################
@bp.route("/logos", methods=["GET"])
@token_auth.login_required
def liste_logos(format="json"):
"""
Liste des logos définis pour le site scodoc.
"""
return error_response(501, message="Not implemented")
@bp.route("/logos/<string:nom>", methods=["GET"])
@token_auth.login_required
def recup_logo_global(nom: str):
"""
Retourne l'image au format png ou jpg
"""
return error_response(501, message="Not implemented")
@bp.route("/departements/<string:dept>/logos", methods=["GET"])
@token_auth.login_required
def logo_dept(dept: str):
"""
Liste des logos définis pour le département visé.
"""
return error_response(501, message="Not implemented")
@bp.route("/departement/<string:dept>/logos/<string:nom>", methods=["GET"])
@token_auth.login_required
def recup_logo_dept_global(dept: str, nom: str):
"""
L'image format png ou jpg
"""
return error_response(501, message="Not implemented")

493
app/api/test_api.py Normal file
View File

@ -0,0 +1,493 @@
################################################## Tests ##############################################################
import requests
import os
from app import models
from app.api import bp, requested_format
from app.api.auth import token_auth
from app.api.errors import error_response
SCODOC_USER = "test"
SCODOC_PASSWORD = "test"
SCODOC_URL = "http://192.168.1.12:5000"
CHECK_CERTIFICATE = bool(int(os.environ.get("CHECK_CERTIFICATE", False)))
HEADERS = None
def get_token():
"""
Permet de set le token dans le header
"""
global HEADERS
global SCODOC_USER
global SCODOC_PASSWORD
r0 = requests.post(
SCODOC_URL + "/ScoDoc/api/tokens", auth=(SCODOC_USER, SCODOC_PASSWORD)
)
token = r0.json()["token"]
HEADERS = {"Authorization": f"Bearer {token}"}
DEPT = None
FORMSEMESTRE = None
ETU = None
@bp.route("/test_dept", methods=["GET"])
def get_departement():
"""
Permet de tester departements() mais également de set un département dans DEPT pour la suite des tests
"""
get_token()
global HEADERS
global CHECK_CERTIFICATE
global SCODOC_USER
global SCODOC_PASSWORD
# print(HEADERS)
# departements
r = requests.get(
SCODOC_URL + "/ScoDoc/api/departements",
headers=HEADERS, verify=CHECK_CERTIFICATE,
)
if r.status_code == 200:
dept_id = r.json()[0]
# print(dept_id)
dept = models.Departement.query.filter_by(id=dept_id).first()
dept = dept.to_dict()
fields = ["id", "acronym", "description", "visible", "date_creation"]
for field in dept:
if field not in fields:
return error_response(501, field + " field missing")
global DEPT
DEPT = dept
return error_response(200, "OK")
return error_response(409, "La requête ne peut être traitée en létat actuel")
@bp.route("/test_formsemestre", methods=["GET"])
def get_formsemestre():
"""
Permet de tester liste_semestres_courant() mais également de set un formsemestre dans FORMSEMESTRE
pour la suite des tests
"""
get_departement()
global DEPT
dept_acronym = DEPT["acronym"]
# liste_semestres_courant
r = requests.get(
SCODOC_URL + "/ScoDoc/api/departements/" + dept_acronym + "/semestres_courants",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
if r.status_code == 200:
formsemestre = r.json()[0]
print(r.json()[0])
fields = ["gestion_semestrielle", "titre", "scodoc7_id", "date_debut", "bul_bgcolor", "date_fin",
"resp_can_edit", "dept_id", "etat", "resp_can_change_ens", "id", "modalite", "ens_can_edit_eval",
"formation_id", "gestion_compensation", "elt_sem_apo", "semestre_id", "bul_hide_xml", "elt_annee_apo",
"block_moyennes", "formsemestre_id", "titre_num", "date_debut_iso", "date_fin_iso", "responsables"]
for field in formsemestre:
if field not in fields:
return error_response(501, field + " field missing")
global FORMSEMESTRE
FORMSEMESTRE = formsemestre
return error_response(200, "OK")
return error_response(409, "La requête ne peut être traitée en létat actuel")
@bp.route("/test_etu", methods=["GET"])
def get_etudiant():
"""
Permet de tester etudiants() mais également de set un etudiant dans ETU pour la suite des tests
"""
# etudiants
r = requests.get(
SCODOC_URL + "/ScoDoc/api/etudiants/courant",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
if r.status_code == 200:
etu = r.json()[0]
fields = ["civilite", "code_ine", "code_nip", "date_naissance", "email", "emailperso", "etudid", "nom",
"prenom"]
for field in etu:
if field not in fields:
return error_response(501, field + " field missing")
global ETU
ETU = etu
print(etu)
return error_response(200, "OK")
return error_response(409, "La requête ne peut être traitée en létat actuel")
############################################### Departements ##########################################################
@bp.route("/test_liste_etudiants")
def test_departements_liste_etudiants():
"""
Test la route liste_etudiants
"""
# Set un département et un formsemestre pour les tests
get_departement()
get_formsemestre()
global DEPT
global FORMSEMESTRE
# Set les fields à vérifier
fields = ["civilite", "code_ine", "code_nip", "date_naissance", "email", "emailperso", "etudid", "nom", "prenom"]
# liste_etudiants (sans formsemestre)
r1 = requests.get(
SCODOC_URL + "/ScoDoc/api/departements/" + DEPT["acronym"] + "/etudiants/liste",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
if r1.status_code == 200: # Si la requête est "OK"
# On récupère la liste des étudiants
etudiants = r1.json()
# Vérification que tous les étudiants ont bien tous les bons champs
for etu in etudiants:
for field in etu:
if field not in fields:
return error_response(501, field + " field missing")
# liste_etudiants (avec formsemestre)
r2 = requests.get(
SCODOC_URL + "/ScoDoc/api/departements/" + DEPT["acronym"] + "/etudiants/liste/" +
str(FORMSEMESTRE["formsemestre_id"]),
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
if r2.status_code == 200: # Si la requête est "OK"
# On récupère la liste des étudiants
etudiants = r2.json()
# Vérification que tous les étudiants ont bien tous les bons champs
for etu in etudiants:
for field in etu:
if field not in fields:
return error_response(501, field + " field missing")
return error_response(200, "OK")
return error_response(409, "La requête ne peut être traitée en létat actuel")
@bp.route("/test_referenciel_competences")
def test_departements_referenciel_competences():
"""
Test la route referenciel_competences
"""
get_departement()
get_formsemestre()
global DEPT
global FORMSEMESTRE
# referenciel_competences
r = requests.post(
SCODOC_URL + "/ScoDoc/api/departements/" + DEPT["acronym"] + "/formations/" +
FORMSEMESTRE["formation_id"] + "/referentiel_competences",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
@bp.route("/test_liste_semestre_index")
def test_departements_semestre_index():
"""
Test la route semestre_index
"""
# semestre_index
r5 = requests.post(
SCODOC_URL + "/ScoDoc/api/departements/" + DEPT["acronym"] + "/formsemestre/" +
FORMSEMESTRE["formation_id"] + "/programme",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
#################################################### Etudiants ########################################################
def test_routes_etudiants():
"""
Test les routes de la partie Etudiants
"""
# etudiants
r1 = requests.get(
SCODOC_URL + "/ScoDoc/api/etudiants",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
# etudiants_courant
r2 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
# etudiant
r3 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
# etudiant_formsemestres
r4 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
# etudiant_bulletin_semestre
r5 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
# etudiant_groups
r6 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
def test_routes_formation():
"""
Test les routes de la partie Formation
"""
# formations
r1 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
# formations_by_id
r2 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
# formation_export_by_formation_id
r3 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
# formsemestre_apo
r4 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
# moduleimpls
r5 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
# moduleimpls_sem
r6 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
def test_routes_formsemestres():
"""
Test les routes de la partie Formsemestres
"""
# formsemestre
r1 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
# etudiant_bulletin
r2 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
# bulletins
r3 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
# jury
r4 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
def test_routes_partitions():
"""
Test les routes de la partie Partitions
"""
# partition
r1 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
# etud_in_group
r2 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
# set_groups
r3 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
def test_routes_evaluations():
"""
Test les routes de la partie Evaluations
"""
# evaluations
r1 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
# evaluation_notes
r2 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
# evaluation_set_notes
r3 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
def test_routes_jury():
"""
Test les routes de la partie Jury
"""
# jury_preparation
r1 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
# jury_decisions
r2 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
# set_decision_jury
r3 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
# annule_decision_jury
r4 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
def test_routes_absences():
"""
Test les routes de la partie Absences
"""
# absences
r1 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
# absences_justify
r2 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
# abs_signale
r3 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
# abs_annule
r4 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
# abs_annule_justif
r5 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
# abs_groupe_etat
r6 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
def test_routes_logos():
"""
Test les routes de la partie Logos
"""
# liste_logos
r1 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
# recup_logo_global
r2 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
# logo_dept
r3 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
# recup_logo_dept_global
r4 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)

View File

@ -9,14 +9,15 @@
import datetime
from flask import url_for, g
from app.models.formsemestre import FormSemestre
from app.scodoc import sco_utils as scu
from app.scodoc import sco_bulletins_json
from app.scodoc import sco_preferences
from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc.sco_utils import fmt_note
from app.comp.res_but import ResultatsSemestreBUT
from app.models import FormSemestre, Identite
from app.scodoc import sco_bulletins, sco_utils as scu
from app.scodoc import sco_bulletins_json
from app.scodoc import sco_bulletins_pdf
from app.scodoc import sco_preferences
from app.scodoc.sco_codes_parcours import UE_SPORT, DEF
from app.scodoc.sco_utils import fmt_note
class BulletinBUT:
@ -28,6 +29,7 @@ class BulletinBUT:
def __init__(self, formsemestre: FormSemestre):
""" """
self.res = ResultatsSemestreBUT(formsemestre)
self.prefs = sco_preferences.SemPreferences(formsemestre.id)
def etud_ue_mod_results(self, etud, ue, modimpls) -> dict:
"dict synthèse résultats dans l'UE pour les modules indiqués"
@ -78,13 +80,13 @@ class BulletinBUT:
"bonus": fmt_note(res.bonus_ues[ue.id][etud.id])
if res.bonus_ues is not None and ue.id in res.bonus_ues
else fmt_note(0.0),
"malus": res.malus[ue.id][etud.id],
"malus": fmt_note(res.malus[ue.id][etud.id]),
"capitalise": None, # "AAAA-MM-JJ" TODO #sco92
"ressources": self.etud_ue_mod_results(etud, ue, res.ressources),
"saes": self.etud_ue_mod_results(etud, ue, res.saes),
}
if ue.type != UE_SPORT:
if sco_preferences.get_preference("bul_show_ue_rangs", res.formsemestre.id):
if self.prefs["bul_show_ue_rangs"]:
rangs, effectif = res.ue_rangs[ue.id]
rang = rangs[etud.id]
else:
@ -109,9 +111,10 @@ class BulletinBUT:
d["modules"] = self.etud_mods_results(etud, modimpls_spo)
return d
def etud_mods_results(self, etud, modimpls) -> dict:
def etud_mods_results(self, etud, modimpls, version="long") -> dict:
"""dict synthèse résultats des modules indiqués,
avec évaluations de chacun."""
avec évaluations de chacun (sauf si version == "short")
"""
res = self.res
d = {}
# etud_idx = self.etud_index[etud.id]
@ -152,14 +155,14 @@ class BulletinBUT:
"evaluations": [
self.etud_eval_results(etud, e)
for e in modimpl.evaluations
if e.visibulletin
if (e.visibulletin or version == "long")
and (
modimpl_results.evaluations_etat[e.id].is_complete
or sco_preferences.get_preference(
"bul_show_all_evals", res.formsemestre.id
)
or self.prefs["bul_show_all_evals"]
)
],
]
if version != "short"
else [],
}
return d
@ -168,14 +171,18 @@ class BulletinBUT:
# 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()
poids = {
ue.acronyme: self.res.modimpls_evals_poids[e.moduleimpl_id][ue.id][e.id]
for ue in self.res.ues
}
d = {
"id": e.id,
"description": e.description,
"date": e.jour.isoformat() if e.jour else None,
"heure_debut": e.heure_debut.strftime("%H:%M") if e.heure_debut else None,
"heure_fin": e.heure_fin.strftime("%H:%M") if e.heure_debut else None,
"coef": e.coefficient,
"poids": {p.ue.acronyme: p.poids for p in e.ue_poids},
"coef": fmt_note(e.coefficient),
"poids": poids,
"note": {
"value": fmt_note(
eval_notes[etud.id],
@ -216,13 +223,23 @@ class BulletinBUT:
else:
return f"Bonus de {fmt_note(bonus_vect.iloc[0])}"
def bulletin_etud(self, etud, formsemestre, force_publishing=False) -> dict:
"""Le bulletin de l'étudiant dans ce semestre.
Si force_publishing, rempli le bulletin même si bul_hide_xml est vrai
def bulletin_etud(
self,
etud: Identite,
formsemestre: FormSemestre,
force_publishing=False,
version="long",
) -> dict:
"""Le bulletin de l'étudiant dans ce semestre: dict pour la version JSON / HTML.
- version:
"long", "selectedevals": toutes les infos (notes des évaluations)
"short" : ne descend pas plus bas que les modules.
- Si force_publishing, rempli le bulletin même si bul_hide_xml est vrai
(bulletins non publiés).
"""
res = self.res
etat_inscription = etud.etat_inscription(formsemestre.id)
etat_inscription = etud.inscription_etat(formsemestre.id)
nb_inscrits = self.res.get_inscriptions_counts()[scu.INSCRIT]
published = (not formsemestre.bul_hide_xml) or force_publishing
d = {
@ -239,7 +256,9 @@ class BulletinBUT:
},
"formsemestre_id": formsemestre.id,
"etat_inscription": etat_inscription,
"options": sco_preferences.bulletin_option_affichage(formsemestre.id),
"options": sco_preferences.bulletin_option_affichage(
formsemestre.id, self.prefs
),
}
if not published:
return d
@ -278,8 +297,10 @@ class BulletinBUT:
)
d.update(
{
"ressources": self.etud_mods_results(etud, res.ressources),
"saes": self.etud_mods_results(etud, res.saes),
"ressources": self.etud_mods_results(
etud, res.ressources, version=version
),
"saes": self.etud_mods_results(etud, res.saes, version=version),
"ues": {
ue.acronyme: self.etud_ue_results(etud, ue)
for ue in res.ues
@ -312,3 +333,56 @@ class BulletinBUT:
)
return d
def bulletin_etud_complet(self, etud: Identite, version="long") -> dict:
"""Bulletin dict complet avec toutes les infos pour les bulletins BUT pdf
Résultat compatible avec celui de sco_bulletins.formsemestre_bulletinetud_dict
"""
d = self.bulletin_etud(
etud, self.res.formsemestre, version=version, force_publishing=True
)
d["etudid"] = etud.id
d["etud"] = d["etudiant"]
d["etud"]["nomprenom"] = etud.nomprenom
d.update(self.res.sem)
etud_etat = self.res.get_etud_etat(etud.id)
d["filigranne"] = sco_bulletins_pdf.get_filigranne(
etud_etat,
self.prefs,
decision_sem=d["semestre"].get("decision_sem"),
)
if etud_etat == scu.DEMISSION:
d["demission"] = "(Démission)"
elif etud_etat == DEF:
d["demission"] = "(Défaillant)"
else:
d["demission"] = ""
# --- Absences
d["nbabs"], d["nbabsjust"] = self.res.formsemestre.get_abs_count(etud.id)
# --- Decision Jury
infos, dpv = sco_bulletins.etud_descr_situation_semestre(
etud.id,
self.res.formsemestre.id,
format="html",
show_date_inscr=self.prefs["bul_show_date_inscr"],
show_decisions=self.prefs["bul_show_decision"],
show_uevalid=self.prefs["bul_show_uevalid"],
show_mention=self.prefs["bul_show_mention"],
)
d.update(infos)
# --- Rangs
d[
"rang_nt"
] = f"{d['semestre']['rang']['value']} / {d['semestre']['rang']['total']}"
d["rang_txt"] = "Rang " + d["rang_nt"]
# --- Appréciations
d.update(
sco_bulletins.get_appreciations_list(self.res.formsemestre.id, etud.id)
)
d.update(sco_bulletins.make_context_dict(self.res.formsemestre, d["etud"]))
return d

309
app/but/bulletin_but_pdf.py Normal file
View File

@ -0,0 +1,309 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Génération bulletin BUT au format PDF standard
"""
from reportlab.platypus import Paragraph, Spacer
from app.scodoc.sco_pdf import blue, cm, mm
from app.scodoc import gen_tables
from app.scodoc.sco_utils import fmt_note
from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
"""Génération du bulletin de BUT au format PDF.
self.infos est le dict issu de BulletinBUT.bulletin_etud_complet()
"""
list_in_menu = False # spécialisation du BulletinGeneratorStandard, ne pas présenter à l'utilisateur
scale_table_in_page = False
def bul_table(self, format="html"):
"""Génère la table centrale du bulletin de notes
Renvoie:
- en HTML: une chaine
- en PDF: une liste d'objets PLATYPUS (eg instance de Table).
"""
tables_infos = [
# ---- TABLE SYNTHESE UES
self.but_table_synthese_ues(),
]
if self.version != "short":
tables_infos += [
# ---- TABLE RESSOURCES
self.but_table_ressources(),
# ---- TABLE SAE
self.but_table_saes(),
]
objects = []
for i, (col_keys, rows, pdf_style, col_widths) in enumerate(tables_infos):
table = gen_tables.GenTable(
rows=rows,
columns_ids=col_keys,
pdf_table_style=pdf_style,
pdf_col_widths=[col_widths[k] for k in col_keys],
preferences=self.preferences,
html_class="notes_bulletin",
html_class_ignore_default=True,
html_with_td_classes=True,
)
table_objects = table.gen(format=format)
objects += table_objects
# objects += [KeepInFrame(0, 0, table_objects, mode="shrink")]
if i != 2:
objects.append(Spacer(1, 6 * mm))
return objects
def but_table_synthese_ues(self, title_bg=(182, 235, 255)):
"""La table de synthèse; pour chaque UE, liste des ressources et SAÉs avec leurs notes
et leurs coefs.
Renvoie: colkeys, P, pdf_style, colWidths
- colkeys: nom des colonnes de la table (clés)
- P : table (liste de dicts de chaines de caracteres)
- pdf_style : commandes table Platypus
- largeurs de colonnes pour PDF
"""
col_widths = {
"titre": None,
"moyenne": 2 * cm,
"coef": 2 * cm,
}
title_bg = tuple(x / 255.0 for x in title_bg)
# elems pour générer table avec gen_table (liste de dicts)
rows = [
# Ligne de titres
{
"titre": "Unités d'enseignement",
"moyenne": "Note/20",
"coef": "Coef.",
"_coef_pdf": Paragraph("<para align=right><b><i>Coef.</i></b></para>"),
"_css_row_class": "note_bold",
"_pdf_row_markup": ["b"],
"_pdf_style": [
("BACKGROUND", (0, 0), (-1, 0), title_bg),
("BOTTOMPADDING", (0, 0), (-1, 0), 7),
(
"LINEBELOW",
(0, 0),
(-1, 0),
self.PDF_LINEWIDTH,
blue,
),
],
}
]
col_keys = ["titre", "coef", "moyenne"] # noms des colonnes à afficher
for ue_acronym, ue in self.infos["ues"].items():
# 1er ligne titre UE
moy_ue = ue.get("moyenne")
t = {
"titre": f"{ue_acronym} - {ue['titre']}",
"moyenne": moy_ue.get("value", "-") if moy_ue is not None else "-",
"_css_row_class": "note_bold",
"_pdf_row_markup": ["b"],
"_pdf_style": [
(
"LINEABOVE",
(0, 0),
(-1, 0),
self.PDF_LINEWIDTH,
self.PDF_LINECOLOR,
),
("BACKGROUND", (0, 0), (-1, 0), title_bg),
("BOTTOMPADDING", (0, 0), (-1, 0), 7),
],
}
rows.append(t)
# 2eme ligne titre UE (bonus/malus/ects)
ects_txt = f'ECTS: {ue["ECTS"]["acquis"]:.3g} / {ue["ECTS"]["total"]:.3g}'
t = {
"titre": f"""Bonus: {ue['bonus']} - Malus: {
ue["malus"]}""",
"coef": ects_txt,
"_coef_pdf": Paragraph(f"""<para align=right>{ects_txt}</para>"""),
"_coef_colspan": 2,
# "_css_row_class": "",
# "_pdf_row_markup": [""],
"_pdf_style": [
("BACKGROUND", (0, 0), (-1, 0), title_bg),
(
"LINEBELOW",
(0, 0),
(-1, 0),
self.PDF_LINEWIDTH,
self.PDF_LINECOLOR,
),
# cadre autour du bonus/malus
(
"BOX",
(0, 0),
(0, 0),
self.PDF_LINEWIDTH,
(0.7, 0.7, 0.7), # gris clair
),
],
}
rows.append(t)
# Liste chaque ressource puis SAE
for mod_type in ("ressources", "saes"):
for mod_code, mod in ue[mod_type].items():
t = {
"titre": f"{mod_code} {self.infos[mod_type][mod_code]['titre']}",
"moyenne": mod["moyenne"],
"coef": mod["coef"],
"_coef_pdf": Paragraph(
f"<para align=right><i>{mod['coef']}</i></para>"
),
"_pdf_style": [
(
"LINEBELOW",
(0, 0),
(-1, 0),
self.PDF_LINEWIDTH,
(0.7, 0.7, 0.7), # gris clair
)
],
}
rows.append(t)
# Global pdf style commands:
pdf_style = [
("VALIGN", (0, 0), (-1, -1), "TOP"),
("BOX", (0, 0), (-1, -1), 0.4, blue), # ajoute cadre extérieur bleu:
]
return col_keys, rows, pdf_style, col_widths
def but_table_ressources(self):
"""La table de synthèse; pour chaque ressources, note et liste d'évaluations
Renvoie: colkeys, P, pdf_style, colWidths
"""
return self.bul_table_modules(
mod_type="ressources", title="Ressources", title_bg=(248, 200, 68)
)
def but_table_saes(self):
"table des SAEs"
return self.bul_table_modules(
mod_type="saes",
title="Situations d'apprentissage et d'évaluation",
title_bg=(198, 255, 171),
)
def bul_table_modules(self, mod_type=None, title="", title_bg=(248, 200, 68)):
"""Table ressources ou SAEs
- colkeys: nom des colonnes de la table (clés)
- P : table (liste de dicts de chaines de caracteres)
- pdf_style : commandes table Platypus
- largeurs de colonnes pour PDF
"""
poids_fontsize = "8"
# UE à utiliser pour les poids (# colonne/UE)
ue_acros = list(self.infos["ues"].keys()) # ['RT1.1', 'RT2.1', 'RT3.1']
# Colonnes à afficher:
col_keys = ["titre"] + ue_acros + ["coef", "moyenne"]
# Largeurs des colonnes:
col_widths = {
"titre": None,
# "poids": None,
"moyenne": 2 * cm,
"coef": 2 * cm,
}
for ue_acro in ue_acros:
col_widths[ue_acro] = 12 * mm # largeur col. poids
title_bg = tuple(x / 255.0 for x in title_bg)
# elems pour générer table avec gen_table (liste de dicts)
# Ligne de titres
t = {
"titre": title,
# "_titre_colspan": 1 + len(ue_acros),
"moyenne": "Note/20",
"coef": "Coef.",
"_coef_pdf": Paragraph("<para align=right><i>Coef.</i></para>"),
"_css_row_class": "note_bold",
"_pdf_row_markup": ["b"],
"_pdf_style": [
("BACKGROUND", (0, 0), (-1, 0), title_bg),
("BOTTOMPADDING", (0, 0), (-1, 0), 7),
(
"LINEBELOW",
(0, 0),
(-1, 0),
self.PDF_LINEWIDTH,
blue,
),
],
}
for ue_acro in ue_acros:
t[ue_acro] = Paragraph(
f"<para align=right fontSize={poids_fontsize}><i>{ue_acro}</i></para>"
)
rows = [t]
for mod_code, mod in self.infos[mod_type].items():
# 1er ligne titre module
t = {
"titre": f"{mod_code} - {mod['titre']}",
"_titre_colspan": 2 + len(ue_acros),
"_css_row_class": "note_bold",
"_pdf_row_markup": ["b"],
"_pdf_style": [
(
"LINEABOVE",
(0, 0),
(-1, 0),
self.PDF_LINEWIDTH,
self.PDF_LINECOLOR,
),
("BACKGROUND", (0, 0), (-1, 0), title_bg),
("BOTTOMPADDING", (0, 0), (-1, 0), 7),
],
}
rows.append(t)
# Evaluations:
for e in mod["evaluations"]:
t = {
"titre": f"{e['description']}",
"moyenne": e["note"]["value"],
"coef": e["coef"],
"_coef_pdf": Paragraph(
f"<para align=right fontSize={poids_fontsize}><i>{e['coef']}</i></para>"
),
"_pdf_style": [
(
"LINEBELOW",
(0, 0),
(-1, 0),
self.PDF_LINEWIDTH,
(0.7, 0.7, 0.7), # gris clair
)
],
}
col_idx = 1 # 1ere col. poids
for ue_acro in ue_acros:
t[ue_acro] = Paragraph(
f"""<para align=right fontSize={poids_fontsize}><i>{e["poids"].get(ue_acro, "")}</i></para>"""
)
t["_pdf_style"].append(
(
"BOX",
(col_idx, 0),
(col_idx, 0),
self.PDF_LINEWIDTH,
(0.7, 0.7, 0.7), # gris clair
),
)
col_idx += 1
rows.append(t)
# Global pdf style commands:
pdf_style = [
("VALIGN", (0, 0), (-1, -1), "TOP"),
("BOX", (0, 0), (-1, -1), 0.4, blue), # ajoute cadre extérieur bleu:
]
return col_keys, rows, pdf_style, col_widths

View File

@ -72,7 +72,7 @@ def bulletin_but_xml_compat(
etud: Identite = Identite.query.get_or_404(etudid)
results = bulletin_but.ResultatsSemestreBUT(formsemestre)
nb_inscrits = results.get_inscriptions_counts()[scu.INSCRIT]
# etat_inscription = etud.etat_inscription(formsemestre.id)
# etat_inscription = etud.inscription_etat(formsemestre.id)
etat_inscription = results.formsemestre.etuds_inscriptions[etudid].etat
if (not formsemestre.bul_hide_xml) or force_publishing:
published = 1

View File

@ -21,6 +21,7 @@ from flask import g
from app.models.formsemestre import FormSemestre
from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc.sco_codes_parcours import ParcoursDUT, ParcoursDUTMono
from app.scodoc.sco_utils import ModuleType
@ -53,7 +54,7 @@ class BonusSport:
etud_moy_gen et etud_moy_ue ne sont PAS modifiés (mais utilisés par certains bonus non additifs).
"""
# En classique, active un bonus sur les UEs: (dans ce cas bonus_moy_gen reste None)
# En classique, active un bonus sur les UEs: (dans ce cas bonus_moy_gen est ajusté pour le prendre en compte)
classic_use_bonus_ues = False
# Attributs virtuels:
@ -198,23 +199,29 @@ class BonusSportAdditif(BonusSport):
à la moyenne générale du semestre déjà obtenue par l'étudiant.
"""
seuil_moy_gen = 10.0 # seuls les points au dessus du seuil sont comptés
seuil_moy_gen = 10.0 # seuls les bonus au dessus du seuil sont pris en compte
seuil_comptage = (
None # les points au dessus du seuil sont comptés (defaut: seuil_moy_gen)
)
proportion_point = 0.05 # multiplie les points au dessus du seuil
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus
sem_modimpl_moys_inscrits: les notes de sport
En APC: ndarray (nb_etuds, nb_mod_sport, nb_ues_non_bonus)
modimpl_coefs_etuds_no_nan:
En classic: ndarray (nb_etuds, nb_mod_sport)
modimpl_coefs_etuds_no_nan: même shape, les coefs.
"""
if 0 in sem_modimpl_moys_inscrits.shape:
# pas d'étudiants ou pas d'UE ou pas de module...
return
seuil_comptage = (
self.seuil_moy_gen if self.seuil_comptage is None else self.seuil_comptage
)
bonus_moy_arr = np.sum(
np.where(
sem_modimpl_moys_inscrits > self.seuil_moy_gen,
(sem_modimpl_moys_inscrits - self.seuil_moy_gen)
* self.proportion_point,
(sem_modimpl_moys_inscrits - seuil_comptage) * self.proportion_point,
0.0,
),
axis=1,
@ -227,13 +234,27 @@ class BonusSportAdditif(BonusSport):
else: # necessaire pour éviter bonus négatifs !
bonus_moy_arr = np.clip(bonus_moy_arr, 0.0, 20.0, out=bonus_moy_arr)
self.bonus_additif(bonus_moy_arr)
def bonus_additif(self, bonus_moy_arr: np.array):
"Set bonus_ues et bonus_moy_gen"
# en APC, bonus_moy_arr est (nb_etuds, nb_ues_non_bonus)
if self.formsemestre.formation.is_apc() or self.classic_use_bonus_ues:
if self.formsemestre.formation.is_apc():
# Bonus sur les UE et None sur moyenne générale
ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)]
self.bonus_ues = pd.DataFrame(
bonus_moy_arr, index=self.etuds_idx, columns=ues_idx, dtype=float
)
elif self.classic_use_bonus_ues:
# Formations classiques apppliquant le bonus sur les UEs
# ici bonus_moy_arr = ndarray 1d nb_etuds
ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)]
self.bonus_ues = pd.DataFrame(
np.stack([bonus_moy_arr] * len(ues_idx)).T,
index=self.etuds_idx,
columns=ues_idx,
dtype=float,
)
else:
# Bonus sur la moyenne générale seulement
self.bonus_moy_gen = pd.Series(
@ -284,6 +305,7 @@ class BonusSportMultiplicatif(BonusSport):
class BonusDirect(BonusSportAdditif):
"""Bonus direct: les points sont directement ajoutés à la moyenne générale.
Les coefficients sont ignorés: tous les points de bonus sont sommés.
(rappel: la note est ramenée sur 20 avant application).
"""
@ -294,47 +316,108 @@ class BonusDirect(BonusSportAdditif):
proportion_point = 1.0
class BonusAnnecy(BonusSport):
"""Calcul bonus modules optionnels (sport), règle IUT d'Annecy.
Il peut y avoir plusieurs modules de bonus.
Prend pour chaque étudiant la meilleure de ses notes bonus et
ajoute à chaque UE :
0.05 point si >=10,
0.1 point si >=12,
0.15 point si >=14,
0.2 point si >=16,
0.25 point si >=18.
class BonusAisneStQuentin(BonusSportAdditif):
"""Calcul bonus modules optionels (sport, culture), règle IUT Aisne St Quentin
<p>Les étudiants de l'IUT peuvent suivre des enseignements optionnels
de l'Université de St Quentin non rattachés à une unité d'enseignement.
</p>
<ul>
<li>Si la note est >= 10 et < 12.1, bonus de 0.1 point</li>
<li>Si la note est >= 12.1 et < 14.1, bonus de 0.2 point</li>
<li>Si la note est >= 14.1 et < 16.1, bonus de 0.3 point</li>
<li>Si la note est >= 16.1 et < 18.1, bonus de 0.4 point</li>
<li>Si la note est >= 18.1, bonus de 0.5 point</li>
</ul>
<p>
Ce bonus s'ajoute à la moyenne générale du semestre déjà obtenue par
l'étudiant (en BUT, s'ajoute à la moyenne de chaque UE).
</p>
"""
name = "bonus_iut_annecy"
displayed_name = "IUT d'Annecy"
name = "bonus_iutstq"
displayed_name = "IUT de Saint-Quentin"
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus"""
# if math.prod(sem_modimpl_moys_inscrits.shape) == 0:
# return # no etuds or no mod sport
# Prend la note de chaque modimpl, sans considération d'UE
if len(sem_modimpl_moys_inscrits.shape) > 2: # apc
sem_modimpl_moys_inscrits = sem_modimpl_moys_inscrits[:, :, 0]
# ici sem_modimpl_moys_inscrits est nb_etuds x nb_mods_bonus, en APC et en classic
note_bonus_max = np.max(sem_modimpl_moys_inscrits, axis=1) # 1d, nb_etuds
bonus = np.zeros(note_bonus_max.shape)
bonus[note_bonus_max >= 18.0] = 0.25
bonus[note_bonus_max >= 16.0] = 0.20
bonus[note_bonus_max >= 14.0] = 0.15
bonus[note_bonus_max >= 12.0] = 0.10
bonus[note_bonus_max >= 10.0] = 0.05
if 0 in sem_modimpl_moys_inscrits.shape:
# pas d'étudiants ou pas d'UE ou pas de module...
return
# Calcule moyenne pondérée des notes de sport:
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
bonus_moy_arr = np.sum(
sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
np.nan_to_num(bonus_moy_arr, nan=0.0, copy=False)
# Bonus moyenne générale et sur les UE
self.bonus_moy_gen = pd.Series(bonus, index=self.etuds_idx, dtype=float)
ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)]
nb_ues_no_bonus = len(ues_idx)
self.bonus_ues = pd.DataFrame(
np.stack([bonus] * nb_ues_no_bonus, axis=1),
columns=ues_idx,
index=self.etuds_idx,
dtype=float,
)
bonus_moy_arr[bonus_moy_arr < 10.0] = 0.0
bonus_moy_arr[bonus_moy_arr >= 18.1] = 0.5
bonus_moy_arr[bonus_moy_arr >= 16.1] = 0.4
bonus_moy_arr[bonus_moy_arr >= 14.1] = 0.3
bonus_moy_arr[bonus_moy_arr >= 12.1] = 0.2
bonus_moy_arr[bonus_moy_arr >= 10] = 0.1
self.bonus_additif(bonus_moy_arr)
class BonusAmiens(BonusSportAdditif):
"""Bonus IUT Amiens pour les modules optionnels (sport, culture, ...).
Toute note non nulle, peu importe sa valeur, entraine un bonus de 0,1 point
sur toutes les moyennes d'UE.
"""
name = "bonus_amiens"
displayed_name = "IUT d'Amiens"
seuil_moy_gen = 0.0 # tous les points sont comptés
proportion_point = 1e10
bonus_max = 0.1
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
# Finalement ils n'en veulent pas.
# class BonusAnnecy(BonusSport):
# """Calcul bonus modules optionnels (sport), règle IUT d'Annecy.
# Il peut y avoir plusieurs modules de bonus.
# Prend pour chaque étudiant la meilleure de ses notes bonus et
# ajoute à chaque UE :<br>
# 0.05 point si >=10,<br>
# 0.1 point si >=12,<br>
# 0.15 point si >=14,<br>
# 0.2 point si >=16,<br>
# 0.25 point si >=18.
# """
# name = "bonus_iut_annecy"
# displayed_name = "IUT d'Annecy"
# def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
# """calcul du bonus"""
# # if math.prod(sem_modimpl_moys_inscrits.shape) == 0:
# # return # no etuds or no mod sport
# # Prend la note de chaque modimpl, sans considération d'UE
# if len(sem_modimpl_moys_inscrits.shape) > 2: # apc
# sem_modimpl_moys_inscrits = sem_modimpl_moys_inscrits[:, :, 0]
# # ici sem_modimpl_moys_inscrits est nb_etuds x nb_mods_bonus, en APC et en classic
# note_bonus_max = np.max(sem_modimpl_moys_inscrits, axis=1) # 1d, nb_etuds
# bonus = np.zeros(note_bonus_max.shape)
# bonus[note_bonus_max >= 10.0] = 0.05
# bonus[note_bonus_max >= 12.0] = 0.10
# bonus[note_bonus_max >= 14.0] = 0.15
# bonus[note_bonus_max >= 16.0] = 0.20
# bonus[note_bonus_max >= 18.0] = 0.25
# # Bonus moyenne générale et sur les UE
# self.bonus_moy_gen = pd.Series(bonus, index=self.etuds_idx, dtype=float)
# ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)]
# nb_ues_no_bonus = len(ues_idx)
# self.bonus_ues = pd.DataFrame(
# np.stack([bonus] * nb_ues_no_bonus, axis=1),
# columns=ues_idx,
# index=self.etuds_idx,
# dtype=float,
# )
class BonusBethune(BonusSportMultiplicatif):
@ -373,26 +456,150 @@ class BonusBezier(BonusSportAdditif):
class BonusBordeaux1(BonusSportMultiplicatif):
"""Calcul bonus modules optionnels (sport, culture), règle IUT Bordeaux 1, sur moyenne générale
et UE.
"""Calcul bonus modules optionnels (sport, culture), règle IUT Bordeaux 1,
sur moyenne générale et UEs.
<p>
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
de l'Université Bordeaux 1 (sport, théâtre) non rattachés à une unité d'enseignement.
</p><p>
Chaque point au-dessus de 10 sur 20 obtenus dans cet enseignement correspond à un %
qui augmente la moyenne de chaque UE et la moyenne générale.
Formule : le % = points>moyenne / 2
qui augmente la moyenne de chaque UE et la moyenne générale.<br>
Formule : pourcentage = (points au dessus de 10) / 2
</p><p>
Par exemple : sport 13/20 : chaque UE sera multipliée par 1+0,015, ainsi que la moyenne générale.
</p>
"""
name = "bonus_iutBordeaux1"
displayed_name = "IUT de Bordeaux 1"
displayed_name = "IUT de Bordeaux"
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
seuil_moy_gen = 10.0
amplitude = 0.005
# Exactement le même que Bordeaux:
class BonusBrest(BonusSportMultiplicatif):
"""Calcul bonus modules optionnels (sport, culture), règle IUT de Brest,
sur moyenne générale et UEs.
<p>
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
de l'Université (sport, théâtre) non rattachés à une unité d'enseignement.
</p><p>
Chaque point au-dessus de 10 sur 20 obtenus dans cet enseignement correspond à un %
qui augmente la moyenne de chaque UE et la moyenne générale.<br>
Formule : pourcentage = (points au dessus de 10) / 2
</p><p>
Par exemple : sport 13/20 : chaque UE sera multipliée par 1+0,015, ainsi que la moyenne générale.
</p>
"""
name = "bonus_iut_brest"
displayed_name = "IUT de Brest"
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
seuil_moy_gen = 10.0
amplitude = 0.005
class BonusCachan1(BonusSportAdditif):
"""Calcul bonus optionnels (sport, culture), règle IUT de Cachan 1.
<ul>
<li> DUT/LP : la meilleure note d'option, si elle est supérieure à 10,
bonifie les moyennes d'UE (<b>sauf l'UE41 dont le code est UE41_E</b>) à raison
de <em>bonus = (option - 10)/10</em>.
</li>
<li> BUT : la meilleure note d'option, si elle est supérieure à 10, bonifie
les moyennes d'UE à raison de <em>bonus = (option - 10)*5%</em>.</li>
</ul>
"""
name = "bonus_cachan1"
displayed_name = "IUT de Cachan 1"
seuil_moy_gen = 10.0 # tous les points sont comptés
proportion_point = 0.05
classic_use_bonus_ues = True
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus, avec réglage différent suivant le type de formation"""
# Prend la note de chaque modimpl, sans considération d'UE
if len(sem_modimpl_moys_inscrits.shape) > 2: # apc
sem_modimpl_moys_inscrits = sem_modimpl_moys_inscrits[:, :, 0]
# ici sem_modimpl_moys_inscrits est nb_etuds x nb_mods_bonus, en APC et en classic
note_bonus_max = np.max(sem_modimpl_moys_inscrits, axis=1) # 1d, nb_etuds
ues = self.formsemestre.query_ues(with_sport=False).all()
ues_idx = [ue.id for ue in ues]
if self.formsemestre.formation.is_apc(): # --- BUT
bonus_moy_arr = np.where(
note_bonus_max > self.seuil_moy_gen,
(note_bonus_max - self.seuil_moy_gen) * self.proportion_point,
0.0,
)
self.bonus_ues = pd.DataFrame(
np.stack([bonus_moy_arr] * len(ues)).T,
index=self.etuds_idx,
columns=ues_idx,
dtype=float,
)
else: # --- DUT
# pareil mais proportion différente et exclusion d'une UE
proportion_point = 0.1
bonus_moy_arr = np.where(
note_bonus_max > self.seuil_moy_gen,
(note_bonus_max - self.seuil_moy_gen) * proportion_point,
0.0,
)
self.bonus_ues = pd.DataFrame(
np.stack([bonus_moy_arr] * len(ues)).T,
index=self.etuds_idx,
columns=ues_idx,
dtype=float,
)
# Pas de bonus sur la ou les ue de code "UE41_E"
ue_exclues = [ue for ue in ues if ue.ue_code == "UE41_E"]
for ue in ue_exclues:
self.bonus_ues[ue.id] = 0.0
class BonusCalais(BonusSportAdditif):
"""Calcul bonus modules optionnels (sport, culture), règle IUT LCO.
Les étudiants de l'IUT LCO peuvent suivre des enseignements optionnels non
rattachés à une unité d'enseignement. Les points au-dessus de 10
sur 20 obtenus dans chacune des matières optionnelles sont cumulés
dans la limite de 10 points. 6% de ces points cumulés s'ajoutent :
<ul>
<li><b>en DUT</b> à la moyenne générale du semestre déjà obtenue par l'étudiant.
<li><b>en BUT et LP</b> à la moyenne des UE dont l'acronyme fini par <b>BS</b> (ex : UE2.1BS, UE32BS)
</ul>
"""
name = "bonus_calais"
displayed_name = "IUT du Littoral"
bonus_max = 0.6
seuil_moy_gen = 10.0 # au dessus de 10
proportion_point = 0.06 # 6%
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
parcours = self.formsemestre.formation.get_parcours()
# Variantes de DUT ?
if (
isinstance(parcours, ParcoursDUT)
or parcours.TYPE_PARCOURS == ParcoursDUTMono.TYPE_PARCOURS
): # DUT
super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
else:
self.classic_use_bonus_ues = True # pour les LP
super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
ues = self.formsemestre.query_ues(with_sport=False).all()
ues_sans_bs = [
ue for ue in ues if ue.acronyme[-2:].upper() != "BS"
] # les 2 derniers cars forcés en majus
for ue in ues_sans_bs:
self.bonus_ues[ue.id] = 0.0
class BonusColmar(BonusSportAdditif):
"""Calcul bonus modules optionnels (sport, culture), règle IUT Colmar.
@ -417,19 +624,21 @@ class BonusColmar(BonusSportAdditif):
class BonusGrenobleIUT1(BonusSportMultiplicatif):
"""Bonus IUT1 de Grenoble
<p>
À compter de sept. 2021:
La note de sport est sur 20, et on calcule une bonification (en %)
qui va s'appliquer à la moyenne de chaque UE du semestre en appliquant
la formule : bonification (en %) = (note-10)*0,5.
Bonification qui ne s'applique que si la note est >10.
(Une note de 10 donne donc 0% de bonif ; note de 20 : 5% de bonif)
</p><p>
<em>La bonification ne s'applique que si la note est supérieure à 10.</em>
</p><p>
(Une note de 10 donne donc 0% de bonif, et une note de 20 : 5% de bonif)
</p><p>
Avant sept. 2021, la note de sport allait de 0 à 5 points (sur 20).
Chaque point correspondait à 0.25% d'augmentation de la moyenne
générale.
Par exemple : note de sport 2/5 : la moyenne générale était augmentée de 0.5%.
</p>
"""
name = "bonus_iut1grenoble_2017"
@ -456,15 +665,18 @@ class BonusGrenobleIUT1(BonusSportMultiplicatif):
class BonusLaRochelle(BonusSportAdditif):
"""Calcul bonus modules optionnels (sport, culture), règle IUT de La Rochelle.
Si la note de sport est comprise entre 0 et 10 : pas d'ajout de point.
Si la note de sport est comprise entre 10 et 20 : ajout de 1% de cette
note sur la moyenne générale du semestre (ou sur les UE en BUT).
<ul>
<li>Si la note de sport est comprise entre 0 et 10 : pas d'ajout de point.</li>
<li>Si la note de sport est comprise entre 10 et 20 : ajout de 1% de cette
note sur la moyenne générale du semestre (ou sur les UE en BUT).</li>
</ul>
"""
name = "bonus_iutlr"
displayed_name = "IUT de La Rochelle"
seuil_moy_gen = 10.0 # tous les points sont comptés
proportion_point = 0.01
seuil_moy_gen = 10.0 # si bonus > 10,
seuil_comptage = 0.0 # tous les points sont comptés
proportion_point = 0.01 # 1%
class BonusLeHavre(BonusSportMultiplicatif):
@ -483,16 +695,17 @@ class BonusLeHavre(BonusSportMultiplicatif):
class BonusLeMans(BonusSportAdditif):
"""Calcul bonus modules optionnels (sport, culture), règle IUT Le Mans.
Les points au-dessus de 10 sur 20 obtenus dans chacune des matières
<p>Les points au-dessus de 10 sur 20 obtenus dans chacune des matières
optionnelles sont cumulés.
</p>
<ul>
<li>En BUT: la moyenne de chacune des UE du semestre est augmentée de
2% du cumul des points de bonus;</li>
En BUT: la moyenne de chacune des UE du semestre est augmentée de
2% du cumul des points de bonus,
En DUT/LP: la moyenne générale est augmentée de 5% du cumul des points bonus.
Dans tous les cas, le bonus est dans la limite de 0,5 point.
<li>En DUT/LP: la moyenne générale est augmentée de 5% du cumul des points bonus.
</li>
</ul>
<p>Dans tous les cas, le bonus est dans la limite de 0,5 point.</p>
"""
name = "bonus_iutlemans"
@ -516,12 +729,13 @@ class BonusLeMans(BonusSportAdditif):
class BonusLille(BonusSportAdditif):
"""Calcul bonus modules optionnels (sport, culture), règle IUT Villeneuve d'Ascq
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
<p>Les étudiants de l'IUT peuvent suivre des enseignements optionnels
de l'Université Lille (sports, etc) non rattachés à une unité d'enseignement.
</p><p>
Les points au-dessus de 10 sur 20 obtenus dans chacune des matières
optionnelles sont cumulés et 4% (2% avant août 2010) de ces points cumulés
s'ajoutent à la moyenne générale du semestre déjà obtenue par l'étudiant.
</p>
"""
name = "bonus_lille"
@ -573,17 +787,19 @@ class BonusMulhouse(BonusSportAdditif):
class BonusNantes(BonusSportAdditif):
"""IUT de Nantes (Septembre 2018)
Nous avons différents types de bonification
<p>Nous avons différents types de bonification
(sport, culture, engagement citoyen).
</p><p>
Nous ajoutons aux moyennes une bonification de 0,2 pour chaque item
la bonification totale ne doit pas excéder les 0,5 point.
Sur le bulletin nous ne mettons pas une note sur 20 mais directement les bonifications.
Dans ScoDoc: on a déclarera une UE "sport&culture" dans laquelle on aura des modules
pour chaque activité (Sport, Associations, ...)
avec à chaque fois une note (ScoDoc l'affichera comme une note sur 20, mais en fait ce sera la
valeur de la bonification: entrer 0,1/20 signifiera un bonus de 0,1 point la moyenne générale)
</p><p>
Dans ScoDoc: on a déclarera une UE "sport&culture" dans laquelle on aura
des modules pour chaque activité (Sport, Associations, ...)
avec à chaque fois une note (ScoDoc l'affichera comme une note sur 20,
mais en fait ce sera la valeur de la bonification: entrer 0,1/20 signifiera
un bonus de 0,1 point la moyenne générale).
</p>
"""
name = "bonus_nantes"
@ -604,7 +820,29 @@ class BonusRoanne(BonusSportAdditif):
displayed_name = "IUT de Roanne"
seuil_moy_gen = 0.0
bonus_max = 0.6 # plafonnement à 0.6 points
apply_bonus_mg_to_ues = True # sur les UE, même en DUT et LP
classic_use_bonus_ues = True # sur les UE, même en DUT et LP
proportion_point = 1
class BonusStBrieuc(BonusSportAdditif):
"""IUT de Saint Brieuc
Ne s'applique qu'aux semestres pairs (S2, S4, S6), et bonifie les moyennes d'UE:
<ul>
<li>Bonus = (S - 10)/20</li>
</ul>
<div class="warning">(XXX vérifier si S6 est éligible au bonus, et le S2 du DUT XXX)</div>
"""
name = "bonus_iut_stbrieuc"
displayed_name = "IUT de Saint-Brieuc"
proportion_point = 1 / 20.0
classic_use_bonus_ues = True
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus"""
if self.formsemestre.semestre_id % 2 == 0:
super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
class BonusStDenis(BonusSportAdditif):
@ -624,16 +862,62 @@ class BonusStDenis(BonusSportAdditif):
bonus_max = 0.5
class BonusTarbes(BonusSportAdditif):
"""Calcul bonus optionnels (sport, culture), règle IUT de Tarbes.
<ul>
<li>Les étudiants opeuvent suivre un ou plusieurs activités optionnelles notées.
La meilleure des notes obtenue est prise en compte, si elle est supérieure à 10/20.
</li>
<li>Le trentième des points au dessus de 10 est ajouté à la moyenne des UE.
</li>
<li> Exemple: un étudiant ayant 16/20 bénéficiera d'un bonus de (16-10)/30 = 0,2 points
sur chaque UE.
</li>
</ul>
"""
name = "bonus_tarbes"
displayed_name = "IUT de Tazrbes"
seuil_moy_gen = 10.0
proportion_point = 1 / 30.0
classic_use_bonus_ues = True
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus"""
# Prend la note de chaque modimpl, sans considération d'UE
if len(sem_modimpl_moys_inscrits.shape) > 2: # apc
sem_modimpl_moys_inscrits = sem_modimpl_moys_inscrits[:, :, 0]
# ici sem_modimpl_moys_inscrits est nb_etuds x nb_mods_bonus, en APC et en classic
note_bonus_max = np.max(sem_modimpl_moys_inscrits, axis=1) # 1d, nb_etuds
ues = self.formsemestre.query_ues(with_sport=False).all()
ues_idx = [ue.id for ue in ues]
if self.formsemestre.formation.is_apc(): # --- BUT
bonus_moy_arr = np.where(
note_bonus_max > self.seuil_moy_gen,
(note_bonus_max - self.seuil_moy_gen) * self.proportion_point,
0.0,
)
self.bonus_ues = pd.DataFrame(
np.stack([bonus_moy_arr] * len(ues)).T,
index=self.etuds_idx,
columns=ues_idx,
dtype=float,
)
class BonusTours(BonusDirect):
"""Calcul bonus sport & culture IUT Tours.
Les notes des UE bonus (ramenées sur 20) sont sommées
<p>Les notes des UE bonus (ramenées sur 20) sont sommées
et 1/40 (2,5%) est ajouté aux moyennes: soit à la moyenne générale,
soit pour le BUT à chaque moyenne d'UE.
Attention: en GEII, facteur 1/40, ailleurs facteur 1.
</p><p>
<em>Attention: en GEII, facteur 1/40, ailleurs facteur 1.</em>
</p><p>
Le bonus total est limité à 1 point.
</p>
"""
name = "bonus_tours"
@ -658,11 +942,13 @@ class BonusVilleAvray(BonusSport):
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
de l'Université Paris 10 (C2I) non rattachés à une unité d'enseignement.
Si la note est >= 10 et < 12, bonus de 0.1 point
Si la note est >= 12 et < 16, bonus de 0.2 point
Si la note est >= 16, bonus de 0.3 point
Ce bonus s'ajoute à la moyenne générale du semestre déjà obtenue par
l'étudiant.
<ul>
<li>Si la note est >= 10 et < 12, bonus de 0.1 point</li>
<li>Si la note est >= 12 et < 16, bonus de 0.2 point</li>
<li>Si la note est >= 16, bonus de 0.3 point</li>
</ul>
<p>Ce bonus s'ajoute à la moyenne générale du semestre déjà obtenue par
l'étudiant.</p>
"""
name = "bonus_iutva"
@ -670,21 +956,21 @@ class BonusVilleAvray(BonusSport):
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus"""
if 0 in sem_modimpl_moys_inscrits.shape:
# pas d'étudiants ou pas d'UE ou pas de module...
return
# Calcule moyenne pondérée des notes de sport:
bonus_moy_arr = np.sum(
sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
bonus_moy_arr[bonus_moy_arr >= 10.0] = 0.1
bonus_moy_arr[bonus_moy_arr >= 12.0] = 0.2
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
bonus_moy_arr = np.sum(
sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
np.nan_to_num(bonus_moy_arr, nan=0.0, copy=False)
bonus_moy_arr[bonus_moy_arr < 10.0] = 0.0
bonus_moy_arr[bonus_moy_arr >= 16.0] = 0.3
bonus_moy_arr[bonus_moy_arr >= 12.0] = 0.2
bonus_moy_arr[bonus_moy_arr >= 10.0] = 0.1
# Bonus moyenne générale, et 0 sur les UE
self.bonus_moy_gen = pd.Series(bonus_moy_arr, index=self.etuds_idx, dtype=float)
if self.bonus_max is not None:
# Seuil: bonus (sur moy. gen.) limité à bonus_max points
self.bonus_moy_gen = self.bonus_moy_gen.clip(upper=self.bonus_max)
# Laisse bonus_ues à None, en APC le bonus moy. gen. sera réparti sur les UEs.
self.bonus_additif(bonus_moy_arr)
class BonusIUTV(BonusSportAdditif):
@ -700,7 +986,7 @@ class BonusIUTV(BonusSportAdditif):
name = "bonus_iutv"
displayed_name = "IUT de Villetaneuse"
pass # oui, c'ets le bonus par défaut
pass # oui, c'est le bonus par défaut
def get_bonus_class_dict(start=BonusSport, d=None):

50
app/comp/moy_mat.py Normal file
View File

@ -0,0 +1,50 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Calcul des moyennes de matières
"""
# C'est un recalcul (optionnel) effectué _après_ le calcul standard.
import numpy as np
import pandas as pd
from app.comp import moy_ue
from app.models.formsemestre import FormSemestre
from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc.sco_utils import ModuleType
def compute_mat_moys_classic(
formsemestre: FormSemestre,
sem_matrix: np.array,
ues: list,
modimpl_inscr_df: pd.DataFrame,
modimpl_coefs: np.array,
) -> dict:
"""Calcul des moyennes par matières.
Result: dict, { matiere_id : Series, index etudid }
"""
modimpls_std = [
m
for m in formsemestre.modimpls_sorted
if (m.module.module_type == ModuleType.STANDARD)
and (m.module.ue.type != UE_SPORT)
]
matiere_ids = {m.module.matiere.id for m in modimpls_std}
matiere_moy = {} # { matiere_id : moy pd.Series, index etudid }
for matiere_id in matiere_ids:
modimpl_mask = np.array(
[m.module.matiere.id == matiere_id for m in formsemestre.modimpls_sorted]
)
etud_moy_mat = moy_ue.compute_mat_moys_classic(
sem_matrix=sem_matrix,
modimpl_inscr_df=modimpl_inscr_df,
modimpl_coefs=modimpl_coefs,
modimpl_mask=modimpl_mask,
)
matiere_moy[matiere_id] = etud_moy_mat
return matiere_moy

View File

@ -335,15 +335,17 @@ class ModuleImplResultsAPC(ModuleImplResults):
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 > etuds_moy_module
etuds_use_rattrapage = notes_rat_ues > etuds_moy_module
etuds_moy_module = np.where(
etuds_use_rattrapage[:, np.newaxis],
np.tile(notes_rat[:, np.newaxis], nb_ues),
etuds_moy_module,
etuds_use_rattrapage, notes_rat_ues, etuds_moy_module
)
# Serie indiquant que l'étudiant utilise une note de rattarage sur l'une des UE:
self.etuds_use_rattrapage = pd.Series(
etuds_use_rattrapage, index=self.evals_notes.index
etuds_use_rattrapage.any(axis=1), index=self.evals_notes.index
)
self.etuds_moy_module = pd.DataFrame(
etuds_moy_module,
@ -359,6 +361,10 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
Les valeurs manquantes (évaluations sans coef vers des UE) sont
remplies: 1 si le coef de ce module dans l'UE est non nul, zéro sinon
(sauf pour module bonus, defaut à 1)
Si le module n'est pas une ressource ou une SAE, ne charge pas de poids
et renvoie toujours les poids par défaut.
Résultat: (evals_poids, liste de UEs du semestre sauf le sport)
"""
modimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id)
@ -367,13 +373,17 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
ue_ids = [ue.id for ue in ues]
evaluation_ids = [evaluation.id for evaluation in evaluations]
evals_poids = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float)
for ue_poids in EvaluationUEPoids.query.join(
EvaluationUEPoids.evaluation
).filter_by(moduleimpl_id=moduleimpl_id):
try:
evals_poids[ue_poids.ue_id][ue_poids.evaluation_id] = ue_poids.poids
except KeyError as exc:
pass # poids vers des UE qui n'existent plus ou sont dans un autre semestre...
if (
modimpl.module.module_type == ModuleType.RESSOURCE
or modimpl.module.module_type == ModuleType.SAE
):
for ue_poids in EvaluationUEPoids.query.join(
EvaluationUEPoids.evaluation
).filter_by(moduleimpl_id=moduleimpl_id):
try:
evals_poids[ue_poids.ue_id][ue_poids.evaluation_id] = ue_poids.poids
except KeyError as exc:
pass # poids vers des UE qui n'existent plus ou sont dans un autre semestre...
# Initialise poids non enregistrés:
default_poids = (

View File

@ -30,8 +30,10 @@
import numpy as np
import pandas as pd
from flask import flash
def compute_sem_moys_apc(
def compute_sem_moys_apc_using_coefs(
etud_moy_ue_df: pd.DataFrame, modimpl_coefs_df: pd.DataFrame
) -> pd.Series:
"""Calcule les moyennes générales indicatives de tous les étudiants
@ -48,6 +50,28 @@ def compute_sem_moys_apc(
return moy_gen
def compute_sem_moys_apc_using_ects(
etud_moy_ue_df: pd.DataFrame, ects: list, formation_id=None
) -> pd.Series:
"""Calcule les moyennes générales indicatives de tous les étudiants
= moyenne des moyennes d'UE, pondérée par leurs ECTS.
etud_moy_ue_df: DataFrame, colonnes ue_id, lignes etudid
ects: liste de floats ou None, 1 par UE
Result: panda Series, index etudid, valeur float (moyenne générale)
"""
try:
moy_gen = (etud_moy_ue_df * ects).sum(axis=1) / sum(ects)
except TypeError:
if None in ects:
flash("""Calcul moyenne générale impossible: ECTS des UE manquants !""")
moy_gen = pd.Series(np.NaN, index=etud_moy_ue_df.index)
else:
raise
return moy_gen
def comp_ranks_series(notes: pd.Series) -> (pd.Series, pd.Series):
"""Calcul rangs à partir d'une séries ("vecteur") de notes (index etudid, valeur
numérique) en tenant compte des ex-aequos.

View File

@ -27,7 +27,6 @@
"""Fonctions de calcul des moyennes d'UE (classiques ou BUT)
"""
from re import X
import numpy as np
import pandas as pd
@ -198,6 +197,7 @@ def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
evals_poids, _ = moy_mod.load_evaluations_poids(modimpl.id)
etuds_moy_module = mod_results.compute_module_moy(evals_poids)
modimpls_results[modimpl.id] = mod_results
modimpls_evals_poids[modimpl.id] = evals_poids
modimpls_notes.append(etuds_moy_module)
if len(modimpls_notes):
cube = notes_sem_assemble_cube(modimpls_notes)
@ -218,21 +218,25 @@ def compute_ue_moys_apc(
ues: list,
modimpl_inscr_df: pd.DataFrame,
modimpl_coefs_df: pd.DataFrame,
modimpl_mask: np.array,
) -> pd.DataFrame:
"""Calcul de la moyenne d'UE en mode APC (BUT).
La moyenne d'UE est un nombre (note/20), ou NI ou NA ou ERR
NI non inscrit à (au moins un) module de cette UE
NA pas de notes disponibles
ERR erreur dans une formule utilisateur. [XXX pas encore gérées ici]
ERR erreur dans une formule utilisateurs (pas gérées ici).
sem_cube: notes moyennes aux modules
ndarray (etuds x modimpls x UEs)
(floats avec des NaN)
etuds : liste des étudiants (dim. 0 du cube)
modimpls : liste des modules à considérer (dim. 1 du cube)
modimpls : liste des module_impl (dim. 1 du cube)
ues : liste des UE (dim. 2 du cube)
modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl)
modimpl_coefs_df: matrice coefficients (UE x modimpl), sans UEs bonus sport
modimpl_mask: liste de booléens, indiquants le module doit être pris ou pas.
(utilisé pour éliminer les bonus, et pourra servir à cacluler
sur des sous-ensembles de modules)
Résultat: DataFrame columns UE (sans bonus), rows etudid
"""
@ -249,7 +253,8 @@ def compute_ue_moys_apc(
assert modimpl_coefs_df.shape[0] == nb_ues_no_bonus
assert modimpl_coefs_df.shape[1] == nb_modules
modimpl_inscr = modimpl_inscr_df.values
modimpl_coefs = modimpl_coefs_df.values
# Met à zéro tous les coefs des modules non sélectionnés dans le masque:
modimpl_coefs = np.where(modimpl_mask, modimpl_coefs_df.values, 0.0)
# Duplique les inscriptions sur les UEs non bonus:
modimpl_inscr_stacked = np.stack([modimpl_inscr] * nb_ues_no_bonus, axis=2)
@ -290,7 +295,8 @@ def compute_ue_moys_classic(
modimpl_coefs: np.array,
modimpl_mask: np.array,
) -> tuple[pd.Series, pd.DataFrame, pd.DataFrame]:
"""Calcul de la moyenne d'UE en mode classique.
"""Calcul de la moyenne d'UE et de la moy. générale en mode classique (DUT, LMD, ...).
La moyenne d'UE est un nombre (note/20), ou NI ou NA ou ERR
NI non inscrit à (au moins un) module de cette UE
NA pas de notes disponibles
@ -359,7 +365,7 @@ def compute_ue_moys_classic(
modimpl_coefs_etuds_no_nan_stacked = np.stack(
[modimpl_coefs_etuds_no_nan.T] * nb_ues
)
# nb_ue x nb_etuds x nb_mods : coefs prenant en compte NaN et inscriptions
# nb_ue x nb_etuds x nb_mods : coefs prenant en compte NaN et inscriptions:
coefs = (modimpl_coefs_etuds_no_nan_stacked * ue_modules).swapaxes(1, 2)
if coefs.dtype == np.object: # arrive sur des tableaux vides
coefs = coefs.astype(np.float)
@ -404,6 +410,68 @@ def compute_ue_moys_classic(
return etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df
def compute_mat_moys_classic(
sem_matrix: np.array,
modimpl_inscr_df: pd.DataFrame,
modimpl_coefs: np.array,
modimpl_mask: np.array,
) -> pd.Series:
"""Calcul de la moyenne sur un sous-enemble de modules en formation CLASSIQUE
La moyenne est un nombre (note/20 ou NaN.
Le masque modimpl_mask est un tableau de booléens (un par modimpl) qui
permet de sélectionner un sous-ensemble de modules (ceux de la matière d'intérêt).
sem_matrix: notes moyennes aux modules (tous les étuds x tous les modimpls)
ndarray (etuds x modimpls)
(floats avec des NaN)
etuds : listes des étudiants (dim. 0 de la matrice)
modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl)
modimpl_coefs: vecteur des coefficients de modules
modimpl_mask: masque des modimpls à prendre en compte
Résultat:
- moyennes: pd.Series, index etudid
"""
if (not len(modimpl_mask)) or (
sem_matrix.shape[0] == 0
): # aucun module ou aucun étudiant
# etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df
return pd.Series(
[0.0] * len(modimpl_inscr_df.index), index=modimpl_inscr_df.index
)
# Restreint aux modules sélectionnés:
sem_matrix = sem_matrix[:, modimpl_mask]
modimpl_inscr = modimpl_inscr_df.values[:, modimpl_mask]
modimpl_coefs = modimpl_coefs[modimpl_mask]
nb_etuds, nb_modules = sem_matrix.shape
assert len(modimpl_coefs) == nb_modules
# Enlève les NaN du numérateur:
sem_matrix_no_nan = np.nan_to_num(sem_matrix, nan=0.0)
# Ne prend pas en compte les notes des étudiants non inscrits au module:
# Annule les notes:
sem_matrix_inscrits = np.where(modimpl_inscr, sem_matrix_no_nan, 0.0)
# Annule les coefs des modules où l'étudiant n'est pas inscrit:
modimpl_coefs_etuds = np.where(
modimpl_inscr, np.stack([modimpl_coefs.T] * nb_etuds), 0.0
)
# Annule les coefs des modules NaN (nb_etuds x nb_mods)
modimpl_coefs_etuds_no_nan = np.where(
np.isnan(sem_matrix), 0.0, modimpl_coefs_etuds
)
if modimpl_coefs_etuds_no_nan.dtype == np.object: # arrive sur des tableaux vides
modimpl_coefs_etuds_no_nan = modimpl_coefs_etuds_no_nan.astype(np.float)
etud_moy_mat = (modimpl_coefs_etuds_no_nan * sem_matrix_inscrits).sum(
axis=1
) / modimpl_coefs_etuds_no_nan.sum(axis=1)
return pd.Series(etud_moy_mat, index=modimpl_inscr_df.index)
def compute_malus(
formsemestre: FormSemestre,
sem_modimpl_moys: np.array,

View File

@ -14,7 +14,7 @@ from app import log
from app.comp import moy_ue, moy_sem, inscr_mod
from app.comp.res_common import NotesTableCompat
from app.comp.bonus_spo import BonusSport
from app.models import ScoDocSiteConfig
from app.models import ScoDocSiteConfig, formsemestre
from app.models.ues import UniteEns
from app.scodoc.sco_codes_parcours import UE_SPORT
@ -56,14 +56,11 @@ class ResultatsSemestreBUT(NotesTableCompat):
# modimpl_coefs_df.columns.get_loc(modimpl.id)
# idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id)
# Elimine les coefs des modimpl bonus sports:
modimpls_sport = [
modimpl
# Masque de tous les modules _sauf_ les bonus (sport)
modimpls_mask = [
modimpl.module.ue.type != UE_SPORT
for modimpl in self.formsemestre.modimpls_sorted
if modimpl.module.ue.type == UE_SPORT
]
for modimpl in modimpls_sport:
self.modimpl_coefs_df[modimpl.id] = 0
self.etud_moy_ue = moy_ue.compute_ue_moys_apc(
self.sem_cube,
@ -72,10 +69,11 @@ class ResultatsSemestreBUT(NotesTableCompat):
self.ues,
self.modimpl_inscr_df,
self.modimpl_coefs_df,
modimpls_mask,
)
# Les coefficients d'UE ne sont pas utilisés en APC
self.etud_coef_ue_df = pd.DataFrame(
1.0, index=self.etud_moy_ue.index, columns=self.etud_moy_ue.columns
0.0, index=self.etud_moy_ue.index, columns=self.etud_moy_ue.columns
)
# --- Modules de MALUS sur les UEs
@ -85,7 +83,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
self.etud_moy_ue -= self.malus
# --- Bonus Sport & Culture
if len(modimpls_sport) > 0:
if not all(modimpls_mask): # au moins un module bonus
bonus_class = ScoDocSiteConfig.get_bonus_sport_class()
if bonus_class is not None:
bonus: BonusSport = bonus_class(
@ -100,13 +98,20 @@ class ResultatsSemestreBUT(NotesTableCompat):
self.bonus_ues = bonus.get_bonus_ues()
if self.bonus_ues is not None:
self.etud_moy_ue += self.bonus_ues # somme les dataframes
self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True)
# Clippe toutes les moyennes d'UE dans [0,20]
self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True)
# Moyenne générale indicative:
# (note: le bonus sport a déjà été appliqué aux moyennes d'UE, et impacte
# donc la moyenne indicative)
self.etud_moy_gen = moy_sem.compute_sem_moys_apc(
self.etud_moy_ue, self.modimpl_coefs_df
# self.etud_moy_gen = moy_sem.compute_sem_moys_apc_using_coefs(
# self.etud_moy_ue, self.modimpl_coefs_df
# )
self.etud_moy_gen = moy_sem.compute_sem_moys_apc_using_ects(
self.etud_moy_ue,
[ue.ects for ue in self.ues if ue.type != UE_SPORT],
formation_id=self.formsemestre.formation_id,
)
# --- UE capitalisées
self.apply_capitalisation()

View File

@ -15,7 +15,7 @@ from flask import g, url_for
from app import db
from app import log
from app.comp import moy_mod, moy_ue, inscr_mod
from app.comp import moy_mat, moy_mod, moy_ue, inscr_mod
from app.comp.res_common import NotesTableCompat
from app.comp.bonus_spo import BonusSport
from app.models import ScoDocSiteConfig
@ -24,6 +24,7 @@ from app.models.formsemestre import FormSemestre
from app.models.ues import UniteEns
from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_preferences
from app.scodoc.sco_utils import ModuleType
@ -60,7 +61,7 @@ class ResultatsSemestreClassic(NotesTableCompat):
)
self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre)
self.modimpl_coefs = np.array(
[m.module.coefficient for m in self.formsemestre.modimpls_sorted]
[m.module.coefficient or 0.0 for m in self.formsemestre.modimpls_sorted]
)
self.modimpl_idx = {
m.id: i for i, m in enumerate(self.formsemestre.modimpls_sorted)
@ -113,22 +114,44 @@ class ResultatsSemestreClassic(NotesTableCompat):
self.etud_moy_ue += self.bonus_ues # somme les dataframes
self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True)
bonus_mg = bonus.get_bonus_moy_gen()
if bonus_mg is not None:
if bonus_mg is None and self.bonus_ues is not None:
# pas de bonus explicite sur la moyenne générale
# on l'ajuste pour refléter les modifs d'UE, à l'aide des coefs d'UE.
bonus_mg = (self.etud_coef_ue_df * self.bonus_ues).sum(
axis=1
) / self.etud_coef_ue_df.sum(axis=1)
self.etud_moy_gen += bonus_mg
self.etud_moy_gen.clip(lower=0.0, upper=20.0, inplace=True)
# compat nt, utilisé pour l'afficher sur les bulletins:
self.bonus = bonus_mg
elif bonus_mg is not None:
# Applique le bonus moyenne générale renvoyé
self.etud_moy_gen += bonus_mg
# compat nt, utilisé pour l'afficher sur les bulletins:
self.bonus = bonus_mg
# --- UE capitalisées
self.apply_capitalisation()
# Clippe toutes les moyennes dans [0,20]
self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True)
self.etud_moy_gen.clip(lower=0.0, upper=20.0, inplace=True)
# --- Classements:
self.compute_rangs()
# --- En option, moyennes par matières
if sco_preferences.get_preference("bul_show_matieres", self.formsemestre.id):
self.compute_moyennes_matieres()
def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float:
"""La moyenne de l'étudiant dans le moduleimpl
Result: valeur float (peut être NaN) ou chaîne "NI" (non inscrit ou DEM)
"""
return self.modimpls_results[moduleimpl_id].etuds_moy_module.get(etudid, "NI")
try:
if self.modimpl_inscr_df[moduleimpl_id][etudid]:
return self.modimpls_results[moduleimpl_id].etuds_moy_module[etudid]
except KeyError:
pass
return "NI"
def get_mod_stats(self, moduleimpl_id: int) -> dict:
"""Stats sur les notes obtenues dans un modimpl"""
@ -149,6 +172,16 @@ class ResultatsSemestreClassic(NotesTableCompat):
),
}
def compute_moyennes_matieres(self):
"""Calcul les moyennes par matière. Doit être appelée au besoin, en fin de compute."""
self.moyennes_matieres = moy_mat.compute_mat_moys_classic(
self.formsemestre,
self.sem_matrix,
self.ues,
self.modimpl_inscr_df,
self.modimpl_coefs,
)
def compute_etud_ue_coef(self, etudid: int, ue: UniteEns) -> float:
"""Détermine le coefficient de l'UE pour cet étudiant.
N'est utilisé que pour l'injection des UE capitalisées dans la

View File

@ -9,18 +9,22 @@ from functools import cached_property
import numpy as np
import pandas as pd
from flask import g, flash, url_for
from app import log
from app.comp.aux_stats import StatsMoyenne
from app.comp import moy_sem
from app.comp.res_cache import ResultatsCache
from app.comp import res_sem
from app.comp.moy_mod import ModuleImplResults
from app.models import FormSemestre, Identite, ModuleImpl
from app.models import FormSemestreUECoef
from app.models import FormSemestre, FormSemestreUECoef
from app.models import Identite
from app.models import ModuleImpl, ModuleImplInscription
from app.models.ues import UniteEns
from app.scodoc import sco_utils as scu
from app.scodoc.sco_cache import ResultatsSemestreCache
from app.scodoc.sco_codes_parcours import UE_SPORT, DEF
from app.scodoc.sco_exceptions import ScoValueError
# Il faut bien distinguer
# - ce qui est caché de façon persistente (via redis):
@ -39,6 +43,7 @@ class ResultatsSemestre(ResultatsCache):
"modimpl_inscr_df",
"modimpls_results",
"etud_coef_ue_df",
"moyennes_matieres",
)
def __init__(self, formsemestre: FormSemestre):
@ -57,6 +62,8 @@ class ResultatsSemestre(ResultatsCache):
self.etud_coef_ue_df = None
"""coefs d'UE effectifs pour chaque étudiant (pour form. classiques)"""
self.validations = None
self.moyennes_matieres = {}
"""Moyennes de matières, si calculées. { matiere_id : Series, index etudid }"""
def compute(self):
"Charge les notes et inscriptions et calcule toutes les moyennes"
@ -165,7 +172,6 @@ class ResultatsSemestre(ResultatsCache):
"""
# Supposant qu'il y a peu d'UE capitalisées,
# on va soustraire la moyenne d'UE et ajouter celle de l'UE capitalisée.
# return # XXX XXX XXX
if not self.validations:
self.validations = res_sem.load_formsemestre_validations(self.formsemestre)
ue_capitalisees = self.validations.ue_capitalisees
@ -184,10 +190,12 @@ class ResultatsSemestre(ResultatsCache):
sum_coefs_ue = 0.0
for ue in self.formsemestre.query_ues():
ue_cap = self.get_etud_ue_status(etudid, ue.id)
if ue_cap and ue_cap["is_capitalized"]:
if ue_cap is None:
continue
if ue_cap["is_capitalized"]:
recompute_mg = True
coef = ue_cap["coef_ue"]
if not np.isnan(ue_cap["moy"]):
if not np.isnan(ue_cap["moy"]) and coef:
sum_notes_ue += ue_cap["moy"] * coef
sum_coefs_ue += coef
@ -195,13 +203,25 @@ class ResultatsSemestre(ResultatsCache):
# On doit prendre en compte une ou plusieurs UE capitalisées
# et donc recalculer la moyenne générale
self.etud_moy_gen[etudid] = sum_notes_ue / sum_coefs_ue
# Ajoute le bonus sport
if self.bonus is not None and self.bonus[etudid]:
self.etud_moy_gen[etudid] += self.bonus[etudid]
self.etud_moy_gen[etudid] = max(
0.0, min(self.etud_moy_gen[etudid], 20.0)
)
def _get_etud_ue_cap(self, etudid, ue):
""""""
def _get_etud_ue_cap(self, etudid: int, ue: UniteEns) -> dict:
"""Donne les informations sur la capitalisation de l'UE ue pour cet étudiant.
Résultat:
Si pas capitalisée: None
Si capitalisée: un dict, avec les colonnes de validation.
"""
capitalisations = self.validations.ue_capitalisees.loc[etudid]
if isinstance(capitalisations, pd.DataFrame):
ue_cap = capitalisations[capitalisations["ue_code"] == ue.ue_code]
if isinstance(ue_cap, pd.DataFrame) and not ue_cap.empty:
if ue_cap.empty:
return None
if isinstance(ue_cap, pd.DataFrame):
# si plusieurs fois capitalisée, prend le max
cap_idx = ue_cap["moy_ue"].values.argmax()
ue_cap = ue_cap.iloc[cap_idx]
@ -209,8 +229,9 @@ class ResultatsSemestre(ResultatsCache):
if capitalisations["ue_code"] == ue.ue_code:
ue_cap = capitalisations
else:
ue_cap = None
return ue_cap
return None
# converti la Series en dict, afin que les np.int64 reviennent en int
return ue_cap.to_dict()
def get_etud_ue_status(self, etudid: int, ue_id: int) -> dict:
"""L'état de l'UE pour cet étudiant.
@ -238,22 +259,45 @@ class ResultatsSemestre(ResultatsCache):
cur_moy_ue = self.etud_moy_ue[ue_id][etudid]
moy_ue = cur_moy_ue
is_capitalized = False # si l'UE prise en compte est une UE capitalisée
was_capitalized = (
False # s'il y a precedemment une UE capitalisée (pas forcement meilleure)
)
# s'il y a precedemment une UE capitalisée (pas forcement meilleure):
was_capitalized = False
if etudid in self.validations.ue_capitalisees.index:
ue_cap = self._get_etud_ue_cap(etudid, ue)
if (
ue_cap is not None
and not ue_cap.empty
and not np.isnan(ue_cap["moy_ue"])
):
if ue_cap and not np.isnan(ue_cap["moy_ue"]):
was_capitalized = True
if ue_cap["moy_ue"] > cur_moy_ue or np.isnan(cur_moy_ue):
moy_ue = ue_cap["moy_ue"]
is_capitalized = True
coef_ue = self.etud_coef_ue_df[ue_id][etudid]
# Coef l'UE dans le semestre courant:
if self.is_apc:
# utilise les ECTS comme coef.
coef_ue = ue.ects
else:
# formations classiques
coef_ue = self.etud_coef_ue_df[ue_id][etudid]
if (not coef_ue) and is_capitalized: # étudiant non inscrit dans l'UE courante
if self.is_apc:
# Coefs de l'UE capitalisée en formation APC: donné par ses ECTS
ue_capitalized = UniteEns.query.get(ue_cap["ue_id"])
coef_ue = ue_capitalized.ects
if coef_ue is None:
orig_sem = FormSemestre.query.get(ue_cap["formsemestre_id"])
raise ScoValueError(
f"""L'UE capitalisée {ue_capitalized.acronyme}
du semestre {orig_sem.titre_annee()}
n'a pas d'indication d'ECTS.
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>.
"""
)
else:
# Coefs de l'UE capitalisée en formation classique:
# va chercher le coef dans le semestre d'origine
coef_ue = ModuleImplInscription.sum_coefs_modimpl_ue(
ue_cap["formsemestre_id"], etudid, ue_cap["ue_id"]
)
return {
"is_capitalized": is_capitalized,
@ -375,21 +419,31 @@ class NotesTableCompat(ResultatsSemestre):
"""Stats (moy/min/max) sur la moyenne générale"""
return StatsMoyenne(self.etud_moy_gen)
def get_ues_stat_dict(self, filter_sport=False): # was get_ues()
def get_ues_stat_dict(
self, filter_sport=False, check_apc_ects=True
) -> list[dict]: # was get_ues()
"""Liste des UEs, ordonnée par numero.
Si filter_sport, retire les UE de type SPORT.
Résultat: liste de dicts { champs UE U stats moyenne UE }
"""
ues = []
for ue in self.formsemestre.query_ues(with_sport=not filter_sport):
ues = self.formsemestre.query_ues(with_sport=not filter_sport)
ues_dict = []
for ue in ues:
d = ue.to_dict()
if ue.type != UE_SPORT:
moys = self.etud_moy_ue[ue.id]
else:
moys = None
d.update(StatsMoyenne(moys).to_dict())
ues.append(d)
return ues
ues_dict.append(d)
if check_apc_ects and self.is_apc and not hasattr(g, "checked_apc_ects"):
g.checked_apc_ects = True
if None in [ue.ects for ue in ues if ue.type != UE_SPORT]:
flash(
"""Calcul moyenne générale impossible: ECTS des UE manquants !""",
category="danger",
)
return ues_dict
def get_modimpls_dict(self, ue_id=None) -> list[dict]:
"""Liste des modules pour une UE (ou toutes si ue_id==None),
@ -508,10 +562,15 @@ class NotesTableCompat(ResultatsSemestre):
return ""
return ins.etat
def get_etud_mat_moy(self, matiere_id, etudid):
def get_etud_mat_moy(self, matiere_id: int, etudid: int) -> str:
"""moyenne d'un étudiant dans une matière (ou NA si pas de notes)"""
# non supporté en 9.2
return "na"
if not self.moyennes_matieres:
return "nd"
return (
self.moyennes_matieres[matiere_id].get(etudid, "-")
if matiere_id in self.moyennes_matieres
else "-"
)
def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float:
"""La moyenne de l'étudiant dans le moduleimpl

View File

@ -8,11 +8,13 @@
"""
from flask import g
from app import db
from app.comp.jury import ValidationsSemestre
from app.comp.res_common import ResultatsSemestre
from app.comp.res_classic import ResultatsSemestreClassic
from app.comp.res_but import ResultatsSemestreBUT
from app.models.formsemestre import FormSemestre
from app.scodoc import sco_cache
def load_formsemestre_results(formsemestre: FormSemestre) -> ResultatsSemestre:
@ -23,6 +25,13 @@ def load_formsemestre_results(formsemestre: FormSemestre) -> ResultatsSemestre:
Search in local cache (g.formsemestre_result_cache)
If not in cache, build it and cache it.
"""
is_apc = formsemestre.formation.is_apc()
if is_apc and formsemestre.semestre_id == -1:
formsemestre.semestre_id = 1
db.session.add(formsemestre)
db.session.commit()
sco_cache.invalidate_formsemestre(formsemestre.id)
# --- Try local cache (within the same request context)
if not hasattr(g, "formsemestre_results_cache"):
g.formsemestre_results_cache = {}
@ -30,11 +39,7 @@ def load_formsemestre_results(formsemestre: FormSemestre) -> ResultatsSemestre:
if formsemestre.id in g.formsemestre_results_cache:
return g.formsemestre_results_cache[formsemestre.id]
klass = (
ResultatsSemestreBUT
if formsemestre.formation.is_apc()
else ResultatsSemestreClassic
)
klass = ResultatsSemestreBUT if is_apc else ResultatsSemestreClassic
g.formsemestre_results_cache[formsemestre.id] = klass(formsemestre)
return g.formsemestre_results_cache[formsemestre.id]

View File

@ -193,7 +193,7 @@ def scodoc7func(func):
# necessary for db ids and boolean values
try:
v = int(v)
except ValueError:
except (ValueError, TypeError):
pass
pos_arg_values.append(v)
# current_app.logger.info("pos_arg_values=%s" % pos_arg_values)

View File

@ -1,8 +1,17 @@
# -*- coding: UTF-8 -*
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
from threading import Thread
from flask import current_app
from flask import current_app, g
from flask_mail import Message
from app import mail
from app.scodoc import sco_preferences
def send_async_email(app, msg):
@ -11,20 +20,66 @@ def send_async_email(app, msg):
def send_email(
subject: str, sender: str, recipients: list, text_body: str, html_body=""
subject: str,
sender: str,
recipients: list,
text_body: str,
html_body="",
bcc=(),
attachments=(),
):
"""
Send an email
Send an email. _All_ ScoDoc mails SHOULD be sent using this function.
If html_body is specified, build a multipart message with HTML content,
else send a plain text email.
attachements: list of dict { 'filename', 'mimetype', 'data' }
"""
msg = Message(subject, sender=sender, recipients=recipients)
msg = Message(subject, sender=sender, recipients=recipients, bcc=bcc)
msg.body = text_body
msg.html = html_body
if attachments:
for attachment in attachments:
msg.attach(
attachment["filename"], attachment["mimetype"], attachment["data"]
)
send_message(msg)
def send_message(msg):
def send_message(msg: Message):
"""Send a message.
All ScoDoc emails MUST be sent by this function.
In mail debug mode, addresses are discarded and all mails are sent to the
specified debugging address.
"""
if hasattr(g, "scodoc_dept"):
# on est dans un département, on peut accéder aux préférences
email_test_mode_address = sco_preferences.get_preference(
"email_test_mode_address"
)
if email_test_mode_address:
# Mode spécial test: remplace les adresses de destination
orig_to = msg.recipients
orig_cc = msg.cc
orig_bcc = msg.bcc
msg.recipients = [email_test_mode_address]
msg.cc = None
msg.bcc = None
msg.subject = "[TEST SCODOC] " + msg.subject
msg.body = (
f"""--- Message ScoDoc dérouté pour tests ---
Adresses d'origine:
to : {orig_to}
cc : {orig_cc}
bcc: {orig_bcc}
---
\n\n"""
+ msg.body
)
Thread(
target=send_async_email, args=(current_app._get_current_object(), msg)
).start()

View File

@ -30,17 +30,15 @@ Formulaires configuration logos
Contrib @jmp, dec 21
"""
import re
from flask import flash, url_for, redirect, render_template
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed
from wtforms import SelectField, SubmitField, FormField, validators, FieldList
from wtforms import SubmitField, FormField, validators, FieldList
from wtforms import ValidationError
from wtforms.fields.simple import StringField, HiddenField
from app import AccessDenied
from app.models import Departement
from app.models import ScoDocSiteConfig
from app.scodoc import sco_logos, html_sco_header
from app.scodoc import sco_utils as scu
from app.scodoc.sco_config_actions import (
@ -49,10 +47,11 @@ from app.scodoc.sco_config_actions import (
LogoInsert,
)
from flask_login import current_user
from app.scodoc import sco_utils as scu
from app.scodoc.sco_logos import find_logo
JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + []
CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
@ -111,6 +110,15 @@ def dept_key_to_id(dept_key):
return dept_key
def logo_name_validator(message=None):
def validate_logo_name(form, field):
name = field.data if field.data else ""
if not scu.is_valid_filename(name):
raise ValidationError(message)
return validate_logo_name
class AddLogoForm(FlaskForm):
"""Formulaire permettant l'ajout d'un logo (dans un département)"""
@ -118,11 +126,7 @@ class AddLogoForm(FlaskForm):
name = StringField(
label="Nom",
validators=[
validators.regexp(
r"^[a-zA-Z0-9-_]*$",
re.IGNORECASE,
"Ne doit comporter que lettres, chiffres, _ ou -",
),
logo_name_validator("Nom de logo invalide (alphanumérique, _)"),
validators.Length(
max=20, message="Un nom ne doit pas dépasser 20 caractères"
),
@ -373,11 +377,11 @@ def config_logos():
if action:
action.execute()
flash(action.message)
return redirect(
url_for(
"scodoc.configure_logos",
)
)
return redirect(url_for("scodoc.configure_logos"))
else:
if not form.validate():
scu.flash_errors(form)
return render_template(
"config_logos.html",
scodoc_dept=None,

View File

@ -55,6 +55,9 @@ def create_dept(acronym: str, visible=True) -> Departement:
"Create new departement"
from app.models import ScoPreference
existing = Departement.query.filter_by(acronym=acronym).count()
if existing:
raise ValueError(f"acronyme {acronym} déjà existant")
departement = Departement(acronym=acronym, visible=visible)
p1 = ScoPreference(name="DeptName", value=acronym, departement=departement)
db.session.add(p1)

View File

@ -4,12 +4,14 @@
et données rattachées (adresses, annotations, ...)
"""
import datetime
from functools import cached_property
from flask import abort, url_for
from flask import g, request
import sqlalchemy
from sqlalchemy import desc, text
from app import db
from app import db, log
from app import models
from app.scodoc import notesdb as ndb
@ -82,6 +84,11 @@ class Identite(db.Model):
return scu.suppress_accents(s)
return s
@property
def e(self):
"terminaison en français: 'ne', '', 'ou '(e)'"
return {"M": "", "F": "e"}.get(self.civilite, "(e)")
def nom_disp(self) -> str:
"Nom à afficher"
if self.nom_usuel:
@ -123,7 +130,7 @@ class Identite(db.Model):
def get_first_email(self, field="email") -> str:
"Le mail associé à la première adrese de l'étudiant, ou None"
return self.adresses[0].email or None if self.adresses.count() > 0 else None
return getattr(self.adresses[0], field) if self.adresses.count() > 0 else None
def to_dict_scodoc7(self):
"""Représentation dictionnaire,
@ -134,7 +141,7 @@ class Identite(db.Model):
# ScoDoc7 output_formators: (backward compat)
e["etudid"] = self.id
e["date_naissance"] = ndb.DateISOtoDMY(e["date_naissance"])
e["ne"] = {"M": "", "F": "ne"}.get(self.civilite, "(e)")
e["ne"] = self.e
return {k: e[k] or "" for k in e} # convert_null_outputs_to_empty
def to_dict_bul(self, include_urls=True):
@ -153,6 +160,7 @@ class Identite(db.Model):
"etudid": self.id,
"nom": self.nom_disp(),
"prenom": self.prenom,
"nomprenom": self.nomprenom,
}
if include_urls:
d["fiche_url"] = url_for(
@ -172,6 +180,23 @@ class Identite(db.Model):
]
return r[0] if r else None
def inscriptions_courantes(self) -> list: # -> list[FormSemestreInscription]:
"""Liste des inscriptions à des semestres _courants_
(il est rare qu'il y en ai plus d'une, mais c'est possible).
Triées par date de début de semestre décroissante (le plus récent en premier).
"""
from app.models.formsemestre import FormSemestre, FormSemestreInscription
return (
FormSemestreInscription.query.join(FormSemestreInscription.formsemestre)
.filter(
FormSemestreInscription.etudid == self.id,
text("date_debut < now() and date_fin > now()"),
)
.order_by(desc(FormSemestre.date_debut))
.all()
)
def inscription_courante_date(self, date_debut, date_fin):
"""La première inscription à un formsemestre incluant la
période [date_debut, date_fin]
@ -183,8 +208,8 @@ class Identite(db.Model):
]
return r[0] if r else None
def etat_inscription(self, formsemestre_id):
"""etat de l'inscription de cet étudiant au semestre:
def inscription_etat(self, formsemestre_id):
"""État de l'inscription de cet étudiant au semestre:
False si pas inscrit, ou scu.INSCRIT, DEMISSION, DEF
"""
# voir si ce n'est pas trop lent:
@ -195,6 +220,110 @@ class Identite(db.Model):
return ins.etat
return False
def inscription_descr(self) -> dict:
"""Description de l'état d'inscription"""
inscription_courante = self.inscription_courante()
if inscription_courante:
titre_sem = inscription_courante.formsemestre.titre_mois()
return {
"etat_in_cursem": inscription_courante.etat,
"inscription_courante": inscription_courante,
"inscription": titre_sem,
"inscription_str": "Inscrit en " + titre_sem,
"situation": self.descr_situation_etud(),
}
else:
if self.formsemestre_inscriptions:
# cherche l'inscription la plus récente:
fin_dernier_sem = max(
[
inscr.formsemestre.date_debut
for inscr in self.formsemestre_inscriptions
]
)
if fin_dernier_sem > datetime.date.today():
inscription = "futur"
situation = "futur élève"
else:
inscription = "ancien"
situation = "ancien élève"
else:
inscription = ("non inscrit",)
situation = inscription
return {
"etat_in_cursem": "?",
"inscription_courante": None,
"inscription": inscription,
"inscription_str": inscription,
"situation": situation,
}
def descr_situation_etud(self) -> str:
"""Chaîne décrivant la situation _actuelle_ de l'étudiant.
Exemple:
"inscrit en BUT R&T semestre 2 FI (Jan 2022 - Jul 2022) le 16/01/2022"
ou
"non inscrit"
"""
inscriptions_courantes = self.inscriptions_courantes()
if inscriptions_courantes:
inscr = inscriptions_courantes[0]
if inscr.etat == scu.INSCRIT:
situation = f"inscrit{self.e} en {inscr.formsemestre.titre_mois()}"
# Cherche la date d'inscription dans scolar_events:
events = models.ScolarEvent.query.filter_by(
etudid=self.id,
formsemestre_id=inscr.formsemestre.id,
event_type="INSCRIPTION",
).all()
if not events:
log(
f"*** situation inconsistante pour {self} (inscrit mais pas d'event)"
)
date_ins = "???" # ???
else:
date_ins = events[0].event_date
situation += date_ins.strftime(" le %d/%m/%Y")
else:
situation = f"démission de {inscr.formsemestre.titre_mois()}"
# Cherche la date de demission dans scolar_events:
events = models.ScolarEvent.query.filter_by(
etudid=self.id,
formsemestre_id=inscr.formsemestre.id,
event_type="DEMISSION",
).all()
if not events:
log(
f"*** situation inconsistante pour {self} (demission mais pas d'event)"
)
date_dem = "???" # ???
else:
date_dem = events[0].event_date
situation += date_dem.strftime(" le %d/%m/%Y")
else:
situation = "non inscrit" + self.e
return situation
def photo_html(self, title=None, size="small") -> str:
"""HTML img tag for the photo, either in small size (h90)
or original size (size=="orig")
"""
from app.scodoc import sco_photos
# sco_photo traite des dicts:
return sco_photos.etud_photo_html(
etud=dict(
etudid=self.id,
code_nip=self.code_nip,
nomprenom=self.nomprenom,
nom_disp=self.nom_disp(),
photo_filename=self.photo_filename,
),
title=title,
size=size,
)
def make_etud_args(
etudid=None, code_nip=None, use_request=True, raise_exc=False, abort_404=True

View File

@ -12,7 +12,6 @@ from app import log
from app.models import APO_CODE_STR_LEN
from app.models import SHORT_STR_LEN
from app.models import CODE_STR_LEN
from app.models import UniteEns
import app.scodoc.sco_utils as scu
from app.models.ues import UniteEns
@ -23,6 +22,7 @@ from app.scodoc import sco_codes_parcours
from app.scodoc import sco_preferences
from app.scodoc.sco_vdi import ApoEtapeVDI
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_utils import MONTH_NAMES_ABBREV
class FormSemestre(db.Model):
@ -104,6 +104,11 @@ class FormSemestre(db.Model):
lazy=True,
backref=db.backref("formsemestres", lazy=True),
)
partitions = db.relationship(
"Partition",
backref=db.backref("formsemestre", lazy=True),
lazy="dynamic",
)
# Ancien id ScoDoc7 pour les migrations de bases anciennes
# ne pas utiliser après migrate_scodoc7_dept_archives
scodoc7_id = db.Column(db.Text(), nullable=True)
@ -117,6 +122,7 @@ class FormSemestre(db.Model):
return f"<{self.__class__.__name__} {self.id} {self.titre_num()}>"
def to_dict(self):
"dict (compatible ScoDoc7)"
d = dict(self.__dict__)
d.pop("_sa_instance_state", None)
# ScoDoc7 output_formators: (backward compat)
@ -157,8 +163,8 @@ class FormSemestre(db.Model):
d["periode"] = 2 # typiquement, début en février: S2, S4...
d["titre_num"] = self.titre_num()
d["titreannee"] = self.titre_annee()
d["mois_debut"] = f"{self.date_debut.month} {self.date_debut.year}"
d["mois_fin"] = f"{self.date_fin.month} {self.date_fin.year}"
d["mois_debut"] = self.mois_debut()
d["mois_fin"] = self.mois_fin()
d["titremois"] = "%s %s (%s - %s)" % (
d["titre_num"],
self.modalite or "",
@ -201,7 +207,11 @@ class FormSemestre(db.Model):
modimpls = self.modimpls.all()
if self.formation.is_apc():
modimpls.sort(
key=lambda m: (m.module.module_type, m.module.numero, m.module.code)
key=lambda m: (
m.module.module_type or 0,
m.module.numero or 0,
m.module.code or 0,
)
)
else:
modimpls.sort(
@ -284,6 +294,7 @@ class FormSemestre(db.Model):
"""chaîne "J. Dupond, X. Martin"
ou "Jacques Dupond, Xavier Martin"
"""
# was "nomcomplet"
if not self.responsables:
return ""
if abbrev_prenom:
@ -295,6 +306,14 @@ class FormSemestre(db.Model):
"2021 - 2022"
return scu.annee_scolaire_repr(self.date_debut.year, self.date_debut.month)
def mois_debut(self) -> str:
"Oct 2021"
return f"{MONTH_NAMES_ABBREV[self.date_debut.month - 1]} {self.date_debut.year}"
def mois_fin(self) -> str:
"Jul 2022"
return f"{MONTH_NAMES_ABBREV[self.date_fin.month - 1]} {self.date_debut.year}"
def session_id(self) -> str:
"""identifiant externe de semestre de formation
Exemple: RT-DUT-FI-S1-ANNEE
@ -355,7 +374,7 @@ class FormSemestre(db.Model):
def get_abs_count(self, etudid):
"""Les comptes d'absences de cet étudiant dans ce semestre:
tuple (nb abs non justifiées, nb abs justifiées)
tuple (nb abs, nb abs justifiées)
Utilise un cache.
"""
from app.scodoc import sco_abs

View File

@ -31,6 +31,11 @@ class Partition(db.Model):
show_in_lists = db.Column(
db.Boolean(), nullable=False, default=True, server_default="true"
)
groups = db.relationship(
"GroupDescr",
backref=db.backref("partition", lazy=True),
lazy="dynamic",
)
def __init__(self, **kwargs):
super(Partition, self).__init__(**kwargs)
@ -42,6 +47,9 @@ class Partition(db.Model):
else:
self.numero = 1
def __repr__(self):
return f"""<{self.__class__.__name__} {self.id} "{self.partition_name or '(default)'}">"""
class GroupDescr(db.Model):
"""Description d'un groupe d'une partition"""
@ -55,6 +63,17 @@ class GroupDescr(db.Model):
# "A", "C2", ... (NULL for 'all'):
group_name = db.Column(db.String(GROUPNAME_STR_LEN))
etuds = db.relationship(
"Identite",
secondary="group_membership",
lazy="dynamic",
)
def __repr__(self):
return (
f"""<{self.__class__.__name__} {self.id} "{self.group_name or '(tous)'}">"""
)
group_membership = db.Table(
"group_membership",

View File

@ -2,10 +2,12 @@
"""ScoDoc models: moduleimpls
"""
import pandas as pd
import flask_sqlalchemy
from app import db
from app.comp import df_cache
from app.models import Identite, Module
from app.models.etudiants import Identite
from app.models.modules import Module
import app.scodoc.notesdb as ndb
from app.scodoc import sco_utils as scu
@ -129,14 +131,36 @@ class ModuleImplInscription(db.Model):
)
@classmethod
def nb_inscriptions_dans_ue(
def etud_modimpls_in_ue(
cls, formsemestre_id: int, etudid: int, ue_id: int
) -> int:
"""Nombre de moduleimpls de l'UE auxquels l'étudiant est inscrit"""
) -> flask_sqlalchemy.BaseQuery:
"""moduleimpls de l'UE auxquels l'étudiant est inscrit"""
return ModuleImplInscription.query.filter(
ModuleImplInscription.etudid == etudid,
ModuleImplInscription.moduleimpl_id == ModuleImpl.id,
ModuleImpl.formsemestre_id == formsemestre_id,
ModuleImpl.module_id == Module.id,
Module.ue_id == ue_id,
).count()
)
@classmethod
def nb_inscriptions_dans_ue(
cls, formsemestre_id: int, etudid: int, ue_id: int
) -> int:
"""Nombre de moduleimpls de l'UE auxquels l'étudiant est inscrit"""
return cls.etud_modimpls_in_ue(formsemestre_id, etudid, ue_id).count()
@classmethod
def sum_coefs_modimpl_ue(
cls, formsemestre_id: int, etudid: int, ue_id: int
) -> float:
"""Somme des coefficients des modules auxquels l'étudiant est inscrit
dans l'UE du semestre indiqué.
N'utilise que les coefficients, donc inadapté aux formations APC.
"""
return sum(
[
inscr.modimpl.module.coefficient
for inscr in cls.etud_modimpls_in_ue(formsemestre_id, etudid, ue_id)
]
)

View File

@ -54,13 +54,15 @@ class UniteEns(db.Model):
'EXTERNE' if self.is_external else ''})>"""
def to_dict(self):
"""as a dict, with the same conversions as in ScoDoc7"""
"""as a dict, with the same conversions as in ScoDoc7
(except ECTS: keep None)
"""
e = dict(self.__dict__)
e.pop("_sa_instance_state", None)
# ScoDoc7 output_formators
e["ue_id"] = self.id
e["numero"] = e["numero"] if e["numero"] else 0
e["ects"] = e["ects"] if e["ects"] else 0.0
e["ects"] = e["ects"]
e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0
e["code_apogee"] = e["code_apogee"] or "" # pas de None
return e

View File

@ -36,6 +36,7 @@
"""
from flask import send_file, request
from app.scodoc.sco_exceptions import ScoValueError
import app.scodoc.sco_utils as scu
from app.scodoc import sco_formsemestre
@ -97,8 +98,12 @@ def pe_view_sem_recap(
template_latex = ""
# template fourni via le formulaire Web
if avis_tmpl_file:
template_latex = avis_tmpl_file.read().decode('utf-8')
template_latex = template_latex
try:
template_latex = avis_tmpl_file.read().decode("utf-8")
except UnicodeDecodeError as e:
raise ScoValueError(
"Données (template) invalides (caractères non UTF8 ?)"
) from e
else:
# template indiqué dans préférences ScoDoc ?
template_latex = pe_avislatex.get_code_latex_from_scodoc_preference(
@ -114,7 +119,7 @@ def pe_view_sem_recap(
footer_latex = ""
# template fourni via le formulaire Web
if footer_tmpl_file:
footer_latex = footer_tmpl_file.read().decode('utf-8')
footer_latex = footer_tmpl_file.read().decode("utf-8")
footer_latex = footer_latex
else:
footer_latex = pe_avislatex.get_code_latex_from_scodoc_preference(

View File

@ -63,12 +63,15 @@ from app.scodoc.sco_pdf import SU
from app import log
def mark_paras(L, tags):
"""Put each (string) element of L between <b>"""
def mark_paras(L, tags) -> list[str]:
"""Put each (string) element of L between <tag>...</tag>,
for each supplied tag.
Leave non string elements untouched.
"""
for tag in tags:
b = "<" + tag + ">"
c = "</" + tag.split()[0] + ">"
L = [b + (x or "") + c for x in L]
start = "<" + tag + ">"
end = "</" + tag.split()[0] + ">"
L = [(start + (x or "") + end) if isinstance(x, str) else x for x in L]
return L
@ -233,7 +236,10 @@ class GenTable(object):
colspan_count -= 1
# if colspan_count > 0:
# continue # skip cells after a span
content = row.get(cid, "") or "" # nota: None converted to ''
if pdf_mode:
content = row.get(f"_{cid}_pdf", "") or row.get(cid, "") or ""
else:
content = row.get(cid, "") or "" # nota: None converted to ''
colspan = row.get("_%s_colspan" % cid, 0)
if colspan > 1:
pdf_style_list.append(
@ -547,9 +553,16 @@ class GenTable(object):
omit_hidden_lines=True,
)
try:
Pt = [
[Paragraph(SU(str(x)), CellStyle) for x in line] for line in data_list
]
Pt = []
for line in data_list:
Pt.append(
[
Paragraph(SU(str(x)), CellStyle)
if (not isinstance(x, Paragraph))
else x
for x in line
]
)
except ValueError as exc:
raise ScoPDFFormatError(str(exc)) from exc
pdf_style_list += self.pdf_table_style

View File

@ -30,12 +30,12 @@
import html
from flask import g
from flask import render_template
from flask import request
from flask_login import current_user
import app.scodoc.sco_utils as scu
from app import log
from app import scodoc_flash_status_messages
from app.scodoc import html_sidebar
import sco_version
@ -153,13 +153,14 @@ def sco_header(
"Main HTML page header for ScoDoc"
from app.scodoc.sco_formsemestre_status import formsemestre_page_title
scodoc_flash_status_messages()
# Get head message from http request:
if not head_message:
if request.method == "POST":
head_message = request.form.get("head_message", "")
elif request.method == "GET":
head_message = request.args.get("head_message", "")
params = {
"page_title": page_title or sco_version.SCONAME,
"no_side_bar": no_side_bar,
@ -280,6 +281,9 @@ def sco_header(
if not no_side_bar:
H.append(html_sidebar.sidebar())
H.append("""<div id="gtrcontent">""")
# En attendant le replacement complet de cette fonction,
# inclusion ici des messages flask
H.append(render_template("flashed_messages.html"))
#
# Barre menu semestre:
H.append(formsemestre_page_title())

View File

@ -1037,7 +1037,7 @@ def get_abs_count(etudid, sem):
def get_abs_count_in_interval(etudid, date_debut_iso, date_fin_iso):
"""Les comptes d'absences de cet étudiant entre ces deux dates, incluses:
tuple (nb abs non justifiées, nb abs justifiées)
tuple (nb abs, nb abs justifiées)
Utilise un cache.
"""
key = str(etudid) + "_" + date_debut_iso + "_" + date_fin_iso

View File

@ -35,6 +35,7 @@ import datetime
from flask import g, url_for
from flask_mail import Message
from app.models.formsemestre import FormSemestre
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
@ -55,27 +56,30 @@ def abs_notify(etudid, date):
"""
from app.scodoc import sco_abs
sem = retreive_current_formsemestre(etudid, date)
if not sem:
formsemestre = retreive_current_formsemestre(etudid, date)
if not formsemestre:
return # non inscrit a la date, pas de notification
nbabs, nbabsjust = sco_abs.get_abs_count(etudid, sem)
do_abs_notify(sem, etudid, date, nbabs, nbabsjust)
nbabs, nbabsjust = sco_abs.get_abs_count_in_interval(
etudid, formsemestre.date_debut.isoformat(), formsemestre.date_fin.isoformat()
)
do_abs_notify(formsemestre, etudid, date, nbabs, nbabsjust)
def do_abs_notify(sem, etudid, date, nbabs, nbabsjust):
def do_abs_notify(formsemestre: FormSemestre, etudid, date, nbabs, nbabsjust):
"""Given new counts of absences, check if notifications are requested and send them."""
# prefs fallback to global pref if sem is None:
if sem:
formsemestre_id = sem["formsemestre_id"]
if formsemestre:
formsemestre_id = formsemestre.id
else:
formsemestre_id = None
prefs = sco_preferences.SemPreferences(formsemestre_id=sem["formsemestre_id"])
prefs = sco_preferences.SemPreferences(formsemestre_id=formsemestre_id)
destinations = abs_notify_get_destinations(
sem, prefs, etudid, date, nbabs, nbabsjust
formsemestre, prefs, etudid, date, nbabs, nbabsjust
)
msg = abs_notification_message(sem, prefs, etudid, nbabs, nbabsjust)
msg = abs_notification_message(formsemestre, prefs, etudid, nbabs, nbabsjust)
if not msg:
return # abort
@ -131,19 +135,19 @@ def abs_notify_send(destinations, etudid, msg, nbabs, nbabsjust, formsemestre_id
)
def abs_notify_get_destinations(sem, prefs, etudid, date, nbabs, nbabsjust):
def abs_notify_get_destinations(
formsemestre: FormSemestre, prefs, etudid, date, nbabs, nbabsjust
) -> set:
"""Returns set of destination emails to be notified"""
formsemestre_id = sem["formsemestre_id"]
destinations = [] # list of email address to notify
if abs_notify_is_above_threshold(etudid, nbabs, nbabsjust, formsemestre_id):
if sem and prefs["abs_notify_respsem"]:
if abs_notify_is_above_threshold(etudid, nbabs, nbabsjust, formsemestre.id):
if prefs["abs_notify_respsem"]:
# notifie chaque responsable du semestre
for responsable_id in sem["responsables"]:
u = sco_users.user_info(responsable_id)
if u["email"]:
destinations.append(u["email"])
for responsable in formsemestre.responsables:
if responsable.email:
destinations.append(responsable.email)
if prefs["abs_notify_chief"] and prefs["email_chefdpt"]:
destinations.append(prefs["email_chefdpt"])
if prefs["abs_notify_email"]:
@ -156,7 +160,7 @@ def abs_notify_get_destinations(sem, prefs, etudid, date, nbabs, nbabsjust):
# Notification (à chaque fois) des resp. de modules ayant des évaluations
# à cette date
# nb: on pourrait prevoir d'utiliser un autre format de message pour ce cas
if sem and prefs["abs_notify_respeval"]:
if prefs["abs_notify_respeval"]:
mods = mod_with_evals_at_date(date, etudid)
for mod in mods:
u = sco_users.user_info(mod["responsable_id"])
@ -232,7 +236,9 @@ def user_nbdays_since_last_notif(email_addr, etudid):
return None
def abs_notification_message(sem, prefs, etudid, nbabs, nbabsjust):
def abs_notification_message(
formsemestre: FormSemestre, prefs, etudid, nbabs, nbabsjust
):
"""Mime notification message based on template.
returns a Message instance
or None if sending should be canceled (empty template).
@ -242,7 +248,7 @@ def abs_notification_message(sem, prefs, etudid, nbabs, nbabsjust):
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
# Variables accessibles dans les balises du template: %(nom_variable)s :
values = sco_bulletins.make_context_dict(sem, etud)
values = sco_bulletins.make_context_dict(formsemestre, etud)
values["nbabs"] = nbabs
values["nbabsjust"] = nbabsjust
@ -264,9 +270,11 @@ def abs_notification_message(sem, prefs, etudid, nbabs, nbabsjust):
return msg
def retreive_current_formsemestre(etudid, cur_date):
def retreive_current_formsemestre(etudid: int, cur_date) -> FormSemestre:
"""Get formsemestre dans lequel etudid est (ou était) inscrit a la date indiquée
date est une chaine au format ISO (yyyy-mm-dd)
Result: FormSemestre ou None si pas inscrit à la date indiquée
"""
req = """SELECT i.formsemestre_id
FROM notes_formsemestre_inscription i, notes_formsemestre sem
@ -278,8 +286,8 @@ def retreive_current_formsemestre(etudid, cur_date):
if not r:
return None
# s'il y a plusieurs semestres, prend le premier (rarissime et non significatif):
sem = sco_formsemestre.get_formsemestre(r[0]["formsemestre_id"])
return sem
formsemestre = FormSemestre.query.get(r[0]["formsemestre_id"])
return formsemestre
def mod_with_evals_at_date(date_abs, etudid):

View File

@ -98,7 +98,7 @@ from chardet import detect as chardet_detect
from app import log
from app.comp import res_sem
from app.comp.res_common import NotesTableCompat
from app.models import FormSemestre
from app.models import FormSemestre, Identite
from app.models.config import ScoDocSiteConfig
import app.scodoc.sco_utils as scu
from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError
@ -111,7 +111,6 @@ from app.scodoc.sco_codes_parcours import (
NAR,
RAT,
)
from app.scodoc import sco_cache
from app.scodoc import sco_formsemestre
from app.scodoc import sco_parcours_dut
from app.scodoc import sco_etud
@ -454,6 +453,12 @@ class ApoEtud(dict):
def comp_elt_semestre(self, nt, decision, etudid):
"""Calcul résultat apo semestre"""
if decision is None:
etud = Identite.query.get(etudid)
nomprenom = etud.nomprenom if etud else "(inconnu)"
raise ScoValueError(
f"decision absente pour l'étudiant {nomprenom} ({etudid})"
)
# resultat du semestre
decision_apo = ScoDocSiteConfig.get_code_apo(decision["code"])
note = nt.get_etud_moy_gen(etudid)

View File

@ -70,13 +70,13 @@ from app.scodoc.sco_exceptions import (
)
from app.scodoc import html_sco_header
from app.scodoc import sco_bulletins_pdf
from app.scodoc import sco_excel
from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups
from app.scodoc import sco_groups_view
from app.scodoc import sco_permissions_check
from app.scodoc import sco_pvjury
from app.scodoc import sco_pvpdf
from app.scodoc.sco_exceptions import ScoValueError
class BaseArchiver(object):
@ -254,7 +254,7 @@ class BaseArchiver(object):
self.initialize()
if not scu.is_valid_filename(filename):
log('Archiver.get: invalid filename "%s"' % filename)
raise ValueError("invalid filename")
raise ScoValueError("archive introuvable (déjà supprimée ?)")
fname = os.path.join(archive_id, filename)
log("reading archive file %s" % fname)
with open(fname, "rb") as f:

View File

@ -28,30 +28,19 @@
"""Génération des bulletins de notes
"""
from app.models import formsemestre
import time
import pprint
import email
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email.header import Header
from reportlab.lib.colors import Color
import urllib
import time
from flask import g, request
from flask import url_for
from flask import render_template, url_for
from flask_login import current_user
from flask_mail import Message
from app.models.moduleimpls import ModuleImplInscription
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
import app.scodoc.notesdb as ndb
from app import email
from app import log
from app.but import bulletin_but
from app.comp import res_sem
from app.comp.res_common import NotesTableCompat
from app.models import FormSemestre
from app.models import FormSemestre, Identite, ModuleImplInscription
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
from app.scodoc import html_sco_header
@ -60,9 +49,9 @@ from app.scodoc import sco_abs
from app.scodoc import sco_abs_views
from app.scodoc import sco_bulletins_generator
from app.scodoc import sco_bulletins_json
from app.scodoc import sco_bulletins_pdf
from app.scodoc import sco_bulletins_xml
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_cache
from app.scodoc import sco_etud
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formations
@ -73,7 +62,9 @@ from app.scodoc import sco_photos
from app.scodoc import sco_preferences
from app.scodoc import sco_pvjury
from app.scodoc import sco_users
from app import email
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType, fmt_note
import app.scodoc.notesdb as ndb
# ----- CLASSES DE BULLETINS DE NOTES
from app.scodoc import sco_bulletins_standard
@ -85,33 +76,20 @@ from app.scodoc import sco_bulletins_legacy
from app.scodoc import sco_bulletins_ucac # format expérimental UCAC Cameroun
def make_context_dict(sem, etud):
def make_context_dict(formsemestre: FormSemestre, etud: dict) -> dict:
"""Construit dictionnaire avec valeurs pour substitution des textes
(preferences bul_pdf_*)
"""
C = sem.copy()
C["responsable"] = " ,".join(
[
sco_users.user_info(responsable_id)["prenomnom"]
for responsable_id in sem["responsables"]
]
)
annee_debut = sem["date_debut"].split("/")[2]
annee_fin = sem["date_fin"].split("/")[2]
if annee_debut != annee_fin:
annee = "%s - %s" % (annee_debut, annee_fin)
else:
annee = annee_debut
C["anneesem"] = annee
C = formsemestre.get_infos_dict()
C["responsable"] = formsemestre.responsables_str()
C["anneesem"] = C["annee"] # backward compat
C.update(etud)
# copie preferences
# XXX devrait acceder directement à un dict de preferences, à revoir
for name in sco_preferences.get_base_preferences().prefs_name:
C[name] = sco_preferences.get_preference(name, sem["formsemestre_id"])
C[name] = sco_preferences.get_preference(name, formsemestre.id)
# ajoute groupes et group_0, group_1, ...
sco_groups.etud_add_group_infos(etud, sem)
sco_groups.etud_add_group_infos(etud, formsemestre.id)
C["groupes"] = etud["groupes"]
n = 0
for partition_id in etud["partitions"]:
@ -132,7 +110,8 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
Le contenu du dictionnaire dépend des options (rangs, ...)
et de la version choisie (short, long, selectedevals).
Cette fonction est utilisée pour les bulletins HTML et PDF, mais pas ceux en XML.
Cette fonction est utilisée pour les bulletins CLASSIQUES (DUT, ...)
en HTML et PDF, mais pas ceux en XML.
"""
from app.scodoc import sco_abs
@ -190,39 +169,23 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
show_mention=prefs["bul_show_mention"],
)
if dpv:
I["decision_sem"] = dpv["decisions"][0]["decision_sem"]
else:
I["decision_sem"] = ""
I.update(infos)
I["etud_etat_html"] = _get_etud_etat_html(
formsemestre.etuds_inscriptions[etudid].etat
)
I["etud_etat"] = nt.get_etud_etat(etudid)
I["filigranne"] = ""
I["filigranne"] = sco_bulletins_pdf.get_filigranne(
I["etud_etat"], prefs, decision_sem=I["decision_sem"]
)
I["demission"] = ""
if I["etud_etat"] == "D":
if I["etud_etat"] == scu.DEMISSION:
I["demission"] = "(Démission)"
I["filigranne"] = "Démission"
elif I["etud_etat"] == sco_codes_parcours.DEF:
I["demission"] = "(Défaillant)"
I["filigranne"] = "Défaillant"
elif (prefs["bul_show_temporary"] and not I["decision_sem"]) or prefs[
"bul_show_temporary_forced"
]:
I["filigranne"] = prefs["bul_temporary_txt"]
# --- Appreciations
cnx = ndb.GetDBConnexion()
apprecs = sco_etud.appreciations_list(
cnx, args={"etudid": etudid, "formsemestre_id": formsemestre_id}
)
I["appreciations_list"] = apprecs
I["appreciations_txt"] = [x["date"] + ": " + x["comment"] for x in apprecs]
I["appreciations"] = I[
"appreciations_txt"
] # deprecated / keep it for backward compat in templates
I.update(get_appreciations_list(formsemestre_id, etudid))
# --- Notes
ues = nt.get_ues_stat_dict()
@ -291,15 +254,17 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
I["matieres_modules"] = {}
I["matieres_modules_capitalized"] = {}
for ue in ues:
u = ue.copy()
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
if (
ModuleImplInscription.nb_inscriptions_dans_ue(
formsemestre_id, etudid, ue["ue_id"]
)
== 0
):
) and not ue_status["is_capitalized"]:
# saute les UE où l'on est pas inscrit et n'avons pas de capitalisation
continue
u = ue.copy()
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
u["ue_status"] = ue_status # { 'moy', 'coef_ue', ...}
if ue["type"] != sco_codes_parcours.UE_SPORT:
u["cur_moy_ue_txt"] = scu.fmt_note(ue_status["cur_moy_ue"])
@ -314,14 +279,14 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
else:
u["cur_moy_ue_txt"] = "bonus appliqué sur les UEs"
else:
u["cur_moy_ue_txt"] = "bonus de %.3g points" % x
u["cur_moy_ue_txt"] = f"bonus de {fmt_note(x)} points"
if nt.bonus_ues is not None:
u["cur_moy_ue_txt"] += " (+ues)"
u["moy_ue_txt"] = scu.fmt_note(ue_status["moy"])
if ue_status["coef_ue"] != None:
u["coef_ue_txt"] = scu.fmt_coef(ue_status["coef_ue"])
else:
# C'est un bug:
log("u=" + pprint.pformat(u))
raise Exception("invalid None coef for ue")
u["coef_ue_txt"] = "-"
if (
dpv
@ -405,13 +370,28 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
I["matieres_modules"].update(_sort_mod_by_matiere(modules, nt, etudid))
#
C = make_context_dict(I["sem"], I["etud"])
C = make_context_dict(formsemestre, I["etud"])
C.update(I)
#
# log( 'C = \n%s\n' % pprint.pformat(C) ) # tres pratique pour voir toutes les infos dispo
return C
def get_appreciations_list(formsemestre_id: int, etudid: int) -> dict:
"""Appréciations pour cet étudiant dans ce semestre"""
cnx = ndb.GetDBConnexion()
apprecs = sco_etud.appreciations_list(
cnx, args={"etudid": etudid, "formsemestre_id": formsemestre_id}
)
d = {
"appreciations_list": apprecs,
"appreciations_txt": [x["date"] + ": " + x["comment"] for x in apprecs],
}
# deprecated / keep it for backward compat in templates:
d["appreciations"] = d["appreciations_txt"]
return d
def _get_etud_etat_html(etat: str) -> str:
"""chaine html représentant l'état (backward compat sco7)"""
if etat == scu.INSCRIT: # "I"
@ -439,7 +419,9 @@ def _sort_mod_by_matiere(modlist, nt, etudid):
return matmod
def _ue_mod_bulletin(etudid, formsemestre_id, ue_id, modimpls, nt, version):
def _ue_mod_bulletin(
etudid, formsemestre_id, ue_id, modimpls, nt: NotesTableCompat, version
):
"""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).
@ -687,6 +669,7 @@ def etud_descr_situation_semestre(
descr_defaillance : "Défaillant" ou vide si non défaillant.
decision_jury : "Validé", "Ajourné", ... (code semestre)
descr_decision_jury : "Décision jury: Validé" (une phrase)
decision_sem :
decisions_ue : noms (acronymes) des UE validées, séparées par des virgules.
descr_decisions_ue : ' UE acquises: UE1, UE2', ou vide si pas de dec. ou si pas show_uevalid
descr_mention : 'Mention Bien', ou vide si pas de mention ou si pas show_mention
@ -696,7 +679,7 @@ def etud_descr_situation_semestre(
# --- Situation et décisions jury
# demission/inscription ?
# démission/inscription ?
events = sco_etud.scolar_events_list(
cnx, args={"etudid": etudid, "formsemestre_id": formsemestre_id}
)
@ -763,11 +746,15 @@ def etud_descr_situation_semestre(
infos["situation"] += " " + infos["descr_defaillance"]
dpv = sco_pvjury.dict_pvjury(formsemestre_id, etudids=[etudid])
if dpv:
infos["decision_sem"] = dpv["decisions"][0]["decision_sem"]
else:
infos["decision_sem"] = ""
if not show_decisions:
return infos, dpv
# Decisions de jury:
# Décisions de jury:
pv = dpv["decisions"][0]
dec = ""
if pv["decision_sem_descr"]:
@ -806,24 +793,21 @@ def etud_descr_situation_semestre(
def formsemestre_bulletinetud(
etudid=None,
formsemestre_id=None,
format="html",
format=None,
version="long",
xml_with_decisions=False,
force_publishing=False, # force publication meme si semestre non publie sur "portail"
prefer_mail_perso=False,
):
"page bulletin de notes"
try:
etud = sco_etud.get_etud_info(filled=True)[0]
etudid = etud["etudid"]
except:
sco_etud.log_unknown_etud()
raise ScoValueError("étudiant inconnu")
# API, donc erreurs admises en ScoValueError
sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
format = format or "html"
etud: Identite = Identite.query.get_or_404(etudid)
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
if not formsemestre:
raise ScoValueError(f"semestre {formsemestre_id} inconnu !")
bulletin = do_formsemestre_bulletinetud(
formsemestre_id,
formsemestre,
etudid,
format=format,
version=version,
@ -832,52 +816,22 @@ def formsemestre_bulletinetud(
prefer_mail_perso=prefer_mail_perso,
)[0]
if format not in {"html", "pdfmail"}:
filename = scu.bul_filename(sem, etud, format)
filename = scu.bul_filename(formsemestre, etud, format)
return scu.send_file(bulletin, filename, mime=scu.get_mime_suffix(format)[0])
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
H = [
_formsemestre_bulletinetud_header_html(
etud, etudid, sem, formsemestre_id, format, version
),
_formsemestre_bulletinetud_header_html(etud, formsemestre, format, version),
bulletin,
render_template(
"bul_foot.html",
etud=etud,
formsemestre=formsemestre,
inscription_courante=etud.inscription_courante(),
inscription_str=etud.inscription_descr()["inscription_str"],
),
html_sco_header.sco_footer(),
]
H.append("""<p>Situation actuelle: """)
if etud["inscription_formsemestre_id"]:
H.append(
f"""<a class="stdlink" href="{url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=etud["inscription_formsemestre_id"])
}">"""
)
H.append(etud["inscriptionstr"])
if etud["inscription_formsemestre_id"]:
H.append("""</a>""")
H.append("""</p>""")
if sem["modalite"] == "EXT":
H.append(
"""<p><a
href="formsemestre_ext_edit_ue_validations?formsemestre_id=%s&etudid=%s"
class="stdlink">
Editer les validations d'UE dans ce semestre extérieur
</a></p>"""
% (formsemestre_id, etudid)
)
# Place du diagramme radar
H.append(
"""<form id="params">
<input type="hidden" name="etudid" id="etudid" value="%s"/>
<input type="hidden" name="formsemestre_id" id="formsemestre_id" value="%s"/>
</form>"""
% (etudid, formsemestre_id)
)
H.append('<div id="radar_bulletin"></div>')
# --- Pied de page
H.append(html_sco_header.sco_footer())
return "".join(H)
@ -892,23 +846,24 @@ def can_send_bulletin_by_mail(formsemestre_id):
def do_formsemestre_bulletinetud(
formsemestre_id,
etudid,
formsemestre: FormSemestre,
etudid: int,
version="long", # short, long, selectedevals
format="html",
format=None,
nohtml=False,
xml_with_decisions=False, # force decisions dans XML
force_publishing=False, # force publication meme si semestre non publie sur "portail"
prefer_mail_perso=False, # mails envoyes sur adresse perso si non vide
xml_with_decisions=False, # force décisions dans XML
force_publishing=False, # force publication meme si semestre non publié sur "portail"
prefer_mail_perso=False, # mails envoyés sur adresse perso si non vide
):
"""Génère le bulletin au format demandé.
Retourne: (bul, filigranne)
bul est str ou bytes au format demandé (html, pdf, pdfmail, pdfpart, xml, json)
et filigranne est un message à placer en "filigranne" (eg "Provisoire").
"""
format = format or "html"
if format == "xml":
bul = sco_bulletins_xml.make_xml_formsemestre_bulletinetud(
formsemestre_id,
formsemestre.id,
etudid,
xml_with_decisions=xml_with_decisions,
force_publishing=force_publishing,
@ -919,7 +874,7 @@ def do_formsemestre_bulletinetud(
elif format == "json":
bul = sco_bulletins_json.make_json_formsemestre_bulletinetud(
formsemestre_id,
formsemestre.id,
etudid,
xml_with_decisions=xml_with_decisions,
force_publishing=force_publishing,
@ -927,8 +882,13 @@ def do_formsemestre_bulletinetud(
)
return bul, ""
I = formsemestre_bulletinetud_dict(formsemestre_id, etudid)
etud = I["etud"]
if formsemestre.formation.is_apc():
etud = Identite.query.get(etudid)
r = bulletin_but.BulletinBUT(formsemestre)
I = r.bulletin_etud_complet(etud, version=version)
else:
I = formsemestre_bulletinetud_dict(formsemestre.id, etudid)
etud = I["etud"]
if format == "html":
htm, _ = sco_bulletins_generator.make_formsemestre_bulletinetud(
@ -954,7 +914,7 @@ def do_formsemestre_bulletinetud(
elif format == "pdfmail":
# format pdfmail: envoie le pdf par mail a l'etud, et affiche le html
# check permission
if not can_send_bulletin_by_mail(formsemestre_id):
if not can_send_bulletin_by_mail(formsemestre.id):
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
if nohtml:
@ -983,7 +943,7 @@ def do_formsemestre_bulletinetud(
) + htm
return h, I["filigranne"]
#
mail_bulletin(formsemestre_id, I, pdfdata, filename, recipient_addr)
mail_bulletin(formsemestre.id, I, pdfdata, filename, recipient_addr)
emaillink = '<a class="stdlink" href="mailto:%s">%s</a>' % (
recipient_addr,
recipient_addr,
@ -1011,11 +971,16 @@ def mail_bulletin(formsemestre_id, I, pdfdata, filename, recipient_addr):
intro_mail = sco_preferences.get_preference("bul_intro_mail", formsemestre_id)
if intro_mail:
hea = intro_mail % {
"nomprenom": etud["nomprenom"],
"dept": dept,
"webmaster": webmaster,
}
try:
hea = intro_mail % {
"nomprenom": etud["nomprenom"],
"dept": dept,
"webmaster": webmaster,
}
except KeyError as e:
raise ScoValueError(
"format 'Message d'accompagnement' (bul_intro_mail) invalide, revoir les réglages dans les préférences"
) from e
else:
hea = ""
@ -1031,81 +996,32 @@ def mail_bulletin(formsemestre_id, I, pdfdata, filename, recipient_addr):
bcc = copy_addr.strip()
else:
bcc = ""
msg = Message(subject, sender=sender, recipients=recipients, bcc=[bcc])
msg.body = hea
# Attach pdf
msg.attach(filename, scu.PDF_MIMETYPE, pdfdata)
log("mail bulletin a %s" % recipient_addr)
email.send_message(msg)
email.send_email(
subject,
sender,
recipients,
bcc=[bcc],
text_body=hea,
attachments=[
{"filename": filename, "mimetype": scu.PDF_MIMETYPE, "data": pdfdata}
],
)
def _formsemestre_bulletinetud_header_html(
etud,
etudid,
sem,
formsemestre_id=None,
format=None,
version=None,
):
H = [
html_sco_header.sco_header(
page_title="Bulletin de %(nomprenom)s" % etud,
javascripts=[
"js/bulletin.js",
"libjs/d3.v3.min.js",
"js/radar_bulletin.js",
],
cssstyles=["css/radar_bulletin.css"],
),
"""<table class="bull_head"><tr><td>
<h2><a class="discretelink" href="%s">%s</a></h2>
"""
% (
url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
),
etud["nomprenom"],
),
"""
<form name="f" method="GET" action="%s">"""
% request.base_url,
f"""Bulletin <span class="bull_liensemestre"><a href="{
url_for("notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=sem["formsemestre_id"])}
">{sem["titremois"]}</a></span>
<br/>"""
% sem,
"""<table><tr>""",
"""<td>établi le %s (notes sur 20)</td>""" % time.strftime("%d/%m/%Y à %Hh%M"),
"""<td><span class="rightjust">
<input type="hidden" name="formsemestre_id" value="%s"></input>"""
% formsemestre_id,
"""<input type="hidden" name="etudid" value="%s"></input>""" % etudid,
"""<input type="hidden" name="format" value="%s"></input>""" % format,
"""<select name="version" onchange="document.f.submit()" class="noprint">""",
]
for (v, e) in (
("short", "Version courte"),
("selectedevals", "Version intermédiaire"),
("long", "Version complète"),
):
if v == version:
selected = " selected"
else:
selected = ""
H.append('<option value="%s"%s>%s</option>' % (v, selected, e))
H.append("""</select></td>""")
# Menu
endpoint = "notes.formsemestre_bulletinetud"
menuBul = [
def make_menu_autres_operations(
formsemestre: FormSemestre, etud: Identite, endpoint: str, version: str
) -> str:
etud_email = etud.get_first_email() or ""
etud_perso = etud.get_first_email("emailperso") or ""
menu_items = [
{
"title": "Réglages bulletins",
"endpoint": "notes.formsemestre_edit_options",
"args": {
"formsemestre_id": formsemestre_id,
"formsemestre_id": formsemestre.id,
# "target_url": url_for(
# "notes.formsemestre_bulletinetud",
# scodoc_dept=g.scodoc_dept,
@ -1113,54 +1029,52 @@ def _formsemestre_bulletinetud_header_html(
# etudid=etudid,
# ),
},
"enabled": (current_user.id in sem["responsables"])
or current_user.has_permission(Permission.ScoImplement),
"enabled": formsemestre.can_be_edited_by(current_user),
},
{
"title": 'Version papier (pdf, format "%s")'
% sco_bulletins_generator.bulletin_get_class_name_displayed(
formsemestre_id
formsemestre.id
),
"endpoint": endpoint,
"args": {
"formsemestre_id": formsemestre_id,
"etudid": etudid,
"formsemestre_id": formsemestre.id,
"etudid": etud.id,
"version": version,
"format": "pdf",
},
},
{
"title": "Envoi par mail à %s" % etud["email"],
"title": f"Envoi par mail à {etud_email}",
"endpoint": endpoint,
"args": {
"formsemestre_id": formsemestre_id,
"etudid": etudid,
"formsemestre_id": formsemestre.id,
"etudid": etud.id,
"version": version,
"format": "pdfmail",
},
# possible slt si on a un mail...
"enabled": etud["email"] and can_send_bulletin_by_mail(formsemestre_id),
"enabled": etud_email and can_send_bulletin_by_mail(formsemestre.id),
},
{
"title": "Envoi par mail à %s (adr. personnelle)" % etud["emailperso"],
"title": f"Envoi par mail à {etud_perso} (adr. personnelle)",
"endpoint": endpoint,
"args": {
"formsemestre_id": formsemestre_id,
"etudid": etudid,
"formsemestre_id": formsemestre.id,
"etudid": etud.id,
"version": version,
"format": "pdfmail",
"prefer_mail_perso": 1,
},
# possible slt si on a un mail...
"enabled": etud["emailperso"]
and can_send_bulletin_by_mail(formsemestre_id),
"enabled": etud_perso and can_send_bulletin_by_mail(formsemestre.id),
},
{
"title": "Version json",
"endpoint": endpoint,
"args": {
"formsemestre_id": formsemestre_id,
"etudid": etudid,
"formsemestre_id": formsemestre.id,
"etudid": etud.id,
"version": version,
"format": "json",
},
@ -1169,8 +1083,8 @@ def _formsemestre_bulletinetud_header_html(
"title": "Version XML",
"endpoint": endpoint,
"args": {
"formsemestre_id": formsemestre_id,
"etudid": etudid,
"formsemestre_id": formsemestre.id,
"etudid": etud.id,
"version": version,
"format": "xml",
},
@ -1179,20 +1093,20 @@ def _formsemestre_bulletinetud_header_html(
"title": "Ajouter une appréciation",
"endpoint": "notes.appreciation_add_form",
"args": {
"formsemestre_id": formsemestre_id,
"etudid": etudid,
"formsemestre_id": formsemestre.id,
"etudid": etud.id,
},
"enabled": (
(current_user.id in sem["responsables"])
or (current_user.has_permission(Permission.ScoEtudInscrit))
formsemestre.can_be_edited_by(current_user)
or current_user.has_permission(Permission.ScoEtudInscrit)
),
},
{
"title": "Enregistrer un semestre effectué ailleurs",
"endpoint": "notes.formsemestre_ext_create_form",
"args": {
"formsemestre_id": formsemestre_id,
"etudid": etudid,
"formsemestre_id": formsemestre.id,
"etudid": etud.id,
},
"enabled": current_user.has_permission(Permission.ScoImplement),
},
@ -1200,71 +1114,72 @@ def _formsemestre_bulletinetud_header_html(
"title": "Enregistrer une validation d'UE antérieure",
"endpoint": "notes.formsemestre_validate_previous_ue",
"args": {
"formsemestre_id": formsemestre_id,
"etudid": etudid,
"formsemestre_id": formsemestre.id,
"etudid": etud.id,
},
"enabled": sco_permissions_check.can_validate_sem(formsemestre_id),
"enabled": sco_permissions_check.can_validate_sem(formsemestre.id),
},
{
"title": "Enregistrer note d'une UE externe",
"endpoint": "notes.external_ue_create_form",
"args": {
"formsemestre_id": formsemestre_id,
"etudid": etudid,
"formsemestre_id": formsemestre.id,
"etudid": etud.id,
},
"enabled": sco_permissions_check.can_validate_sem(formsemestre_id),
"enabled": sco_permissions_check.can_validate_sem(formsemestre.id),
},
{
"title": "Entrer décisions jury",
"endpoint": "notes.formsemestre_validation_etud_form",
"args": {
"formsemestre_id": formsemestre_id,
"etudid": etudid,
"formsemestre_id": formsemestre.id,
"etudid": etud.id,
},
"enabled": sco_permissions_check.can_validate_sem(formsemestre_id),
"enabled": sco_permissions_check.can_validate_sem(formsemestre.id),
},
{
"title": "Editer PV jury",
"title": "Éditer PV jury",
"endpoint": "notes.formsemestre_pvjury_pdf",
"args": {
"formsemestre_id": formsemestre_id,
"etudid": etudid,
"formsemestre_id": formsemestre.id,
"etudid": etud.id,
},
"enabled": True,
},
]
return htmlutils.make_menu("Autres opérations", menu_items, alone=True)
H.append("""<td class="bulletin_menubar"><div class="bulletin_menubar">""")
H.append(htmlutils.make_menu("Autres opérations", menuBul, alone=True))
H.append("""</div></td>""")
H.append(
'<td> <a href="%s">%s</a></td>'
% (
url_for(
"notes.formsemestre_bulletinetud",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
etudid=etudid,
format="pdf",
def _formsemestre_bulletinetud_header_html(
etud,
formsemestre: FormSemestre,
format=None,
version=None,
):
H = [
html_sco_header.sco_header(
page_title=f"Bulletin de {etud.nomprenom}",
javascripts=[
"js/bulletin.js",
"libjs/d3.v3.min.js",
"js/radar_bulletin.js",
],
cssstyles=["css/radar_bulletin.css"],
),
render_template(
"bul_head.html",
etud=etud,
format=format,
formsemestre=formsemestre,
menu_autres_operations=make_menu_autres_operations(
etud=etud,
formsemestre=formsemestre,
endpoint="notes.formsemestre_bulletinetud",
version=version,
),
scu.ICON_PDF,
)
)
H.append("""</tr></table>""")
#
H.append(
"""</form></span></td><td class="bull_photo"><a href="%s">%s</a>
"""
% (
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
sco_photos.etud_photo_html(etud, title="fiche de " + etud["nom"]),
)
)
H.append(
"""</td></tr>
</table>
"""
)
return "".join(H)
scu=scu,
time=time,
version=version,
),
]
return "\n".join(H)

View File

@ -63,48 +63,15 @@ from app.scodoc import sco_pdf
from app.scodoc.sco_pdf import PDFLOCK
import sco_version
# Liste des types des classes de générateurs de bulletins PDF:
BULLETIN_CLASSES = collections.OrderedDict()
def register_bulletin_class(klass):
BULLETIN_CLASSES[klass.__name__] = klass
def bulletin_class_descriptions():
return [x.description for x in BULLETIN_CLASSES.values()]
def bulletin_class_names():
return list(BULLETIN_CLASSES.keys())
def bulletin_default_class_name():
return bulletin_class_names()[0]
def bulletin_get_class(class_name):
return BULLETIN_CLASSES[class_name]
def bulletin_get_class_name_displayed(formsemestre_id):
"""Le nom du générateur utilisé, en clair"""
from app.scodoc import sco_preferences
bul_class_name = sco_preferences.get_preference("bul_class_name", formsemestre_id)
try:
gen_class = bulletin_get_class(bul_class_name)
return gen_class.description
except:
return "invalide ! (voir paramètres)"
class BulletinGenerator(object):
class BulletinGenerator:
"Virtual superclass for PDF bulletin generators" ""
# Here some helper methods
# see sco_bulletins_standard.BulletinGeneratorStandard subclass for real methods
supported_formats = [] # should list supported formats, eg [ 'html', 'pdf' ]
description = "superclass for bulletins" # description for user interface
list_in_menu = True # la classe doit-elle est montrée dans le menu de config ?
scale_table_in_page = True # rescale la table sur 1 page
def __init__(
self,
@ -151,7 +118,7 @@ class BulletinGenerator(object):
def get_filename(self):
"""Build a filename to be proposed to the web client"""
sem = sco_formsemestre.get_formsemestre(self.infos["formsemestre_id"])
return scu.bul_filename(sem, self.infos["etud"], "pdf")
return scu.bul_filename_old(sem, self.infos["etud"], "pdf")
def generate(self, format="", stand_alone=True):
"""Return bulletin in specified format"""
@ -197,8 +164,9 @@ class BulletinGenerator(object):
# signatures
objects += self.bul_signatures_pdf() # pylint: disable=no-member
# Réduit sur une page
objects = [KeepInFrame(0, 0, objects, mode="shrink")]
if self.scale_table_in_page:
# Réduit sur une page
objects = [KeepInFrame(0, 0, objects, mode="shrink")]
#
if not stand_alone:
objects.append(PageBreak()) # insert page break at end
@ -253,7 +221,7 @@ class BulletinGenerator(object):
# ---------------------------------------------------------------------------
def make_formsemestre_bulletinetud(
infos,
version="long", # short, long, selectedevals
version=None, # short, long, selectedevals
format="pdf", # html, pdf
stand_alone=True,
):
@ -265,14 +233,20 @@ def make_formsemestre_bulletinetud(
"""
from app.scodoc import sco_preferences
version = version or "long"
if not version in scu.BULLETINS_VERSIONS:
raise ValueError("invalid version code !")
formsemestre_id = infos["formsemestre_id"]
bul_class_name = sco_preferences.get_preference("bul_class_name", formsemestre_id)
try:
gen_class = None
if infos.get("type") == "BUT" and format.startswith("pdf"):
gen_class = bulletin_get_class(bul_class_name + "BUT")
if gen_class is None:
gen_class = bulletin_get_class(bul_class_name)
except:
if gen_class is None:
raise ValueError(
"Type de bulletin PDF invalide (paramètre: %s)" % bul_class_name
)
@ -313,3 +287,52 @@ def make_formsemestre_bulletinetud(
filename = bul_generator.get_filename()
return data, filename
####
# Liste des types des classes de générateurs de bulletins PDF:
BULLETIN_CLASSES = collections.OrderedDict()
def register_bulletin_class(klass):
BULLETIN_CLASSES[klass.__name__] = klass
def bulletin_class_descriptions():
return [
BULLETIN_CLASSES[class_name].description
for class_name in BULLETIN_CLASSES
if BULLETIN_CLASSES[class_name].list_in_menu
]
def bulletin_class_names() -> list[str]:
"Liste les noms des classes de bulletins à présenter à l'utilisateur"
return [
class_name
for class_name in BULLETIN_CLASSES
if BULLETIN_CLASSES[class_name].list_in_menu
]
def bulletin_default_class_name():
return bulletin_class_names()[0]
def bulletin_get_class(class_name: str) -> BulletinGenerator:
"""La class de génération de bulletin de ce nom,
ou None si pas trouvée
"""
return BULLETIN_CLASSES.get(class_name)
def bulletin_get_class_name_displayed(formsemestre_id):
"""Le nom du générateur utilisé, en clair"""
from app.scodoc import sco_preferences
bul_class_name = sco_preferences.get_preference("bul_class_name", formsemestre_id)
gen_class = bulletin_get_class(bul_class_name)
if gen_class is None:
return "invalide ! (voir paramètres)"
return gen_class.description

View File

@ -138,7 +138,7 @@ def formsemestre_bulletinetud_published_dict(
if not published:
return d # stop !
etat_inscription = etud.etat_inscription(formsemestre.id)
etat_inscription = etud.inscription_etat(formsemestre.id)
if etat_inscription != scu.INSCRIT:
d.update(dict_decision_jury(etudid, formsemestre_id, with_decisions=True))
return d

View File

@ -61,12 +61,10 @@ from reportlab.platypus.doctemplate import BaseDocTemplate
from flask import g, request
from app import log, ScoValueError
from app.comp import res_sem
from app.comp.res_common import NotesTableCompat
from app.models import FormSemestre
from app.scodoc import sco_cache
from app.scodoc import sco_formsemestre
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_pdf
from app.scodoc import sco_preferences
from app.scodoc import sco_etud
@ -190,7 +188,7 @@ def get_formsemestre_bulletins_pdf(formsemestre_id, version="selectedevals"):
i = 1
for etud in formsemestre.get_inscrits(include_demdef=True, order=True):
frag, filigranne = sco_bulletins.do_formsemestre_bulletinetud(
formsemestre_id,
formsemestre,
etud.id,
format="pdfpart",
version=version,
@ -239,8 +237,9 @@ def get_etud_bulletins_pdf(etudid, version="selectedevals"):
filigrannes = {}
i = 1
for sem in etud["sems"]:
formsemestre = FormSemestre.query.get(sem["formsemestre_id"])
frag, filigranne = sco_bulletins.do_formsemestre_bulletinetud(
sem["formsemestre_id"],
formsemestre,
etudid,
format="pdfpart",
version=version,
@ -275,3 +274,16 @@ def get_etud_bulletins_pdf(etudid, version="selectedevals"):
)
return pdfdoc, filename
def get_filigranne(etud_etat: str, prefs, decision_sem=None) -> str:
"""Texte à placer en "filigranne" sur le bulletin pdf"""
if etud_etat == scu.DEMISSION:
return "Démission"
elif etud_etat == sco_codes_parcours.DEF:
return "Défaillant"
elif (prefs["bul_show_temporary"] and not decision_sem) or prefs[
"bul_show_temporary_forced"
]:
return prefs["bul_temporary_txt"]
return ""

View File

@ -46,10 +46,11 @@ de la forme %(XXX)s sont remplacées par la valeur de XXX, pour XXX dans:
Balises img: actuellement interdites.
"""
from reportlab.platypus import KeepTogether, Paragraph, Spacer, Table
from reportlab.lib.units import cm, mm
from reportlab.lib.colors import Color, blue
import app.scodoc.sco_utils as scu
from app.scodoc.sco_pdf import Color, Paragraph, Spacer, Table
from app.scodoc.sco_pdf import blue, cm, mm
from app.scodoc.sco_pdf import SU
from app.scodoc import sco_preferences
from app.scodoc.sco_permissions import Permission
@ -66,7 +67,8 @@ from app.scodoc import sco_groups
from app.scodoc import sco_evaluations
from app.scodoc import gen_tables
# Important: Le nom de la classe ne doit pas changer (bien le choisir), car il sera stocké en base de données (dans les préférences)
# Important: Le nom de la classe ne doit pas changer (bien le choisir),
# car il sera stocké en base de données (dans les préférences)
class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
description = "standard ScoDoc (version 2011)" # la description doit être courte: elle apparait dans le menu de paramètrage ScoDoc
supported_formats = ["html", "pdf"]
@ -194,7 +196,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
# -----
if format == "pdf":
return Op
return [KeepTogether(Op)]
elif format == "html":
return "\n".join(H)
@ -264,11 +266,11 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
def build_bulletin_table(self):
"""Génère la table centrale du bulletin de notes
Renvoie: colkeys, P, pdf_style, colWidths
- colkeys: nom des colonnes de la table (clés)
- table (liste de dicts de chaines de caracteres)
- style (commandes table Platypus)
- largeurs de colonnes pour PDF
Renvoie: col_keys, P, pdf_style, col_widths
- col_keys: nom des colonnes de la table (clés)
- table: liste de dicts de chaines de caractères
- pdf_style: commandes table Platypus
- col_widths: largeurs de colonnes pour PDF
"""
I = self.infos
P = [] # elems pour générer table avec gen_table (liste de dicts)
@ -284,28 +286,28 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
)
with_col_moypromo = prefs["bul_show_moypromo"]
with_col_rang = prefs["bul_show_rangs"]
with_col_coef = prefs["bul_show_coef"]
with_col_coef = prefs["bul_show_coef"] or prefs["bul_show_ue_coef"]
with_col_ects = prefs["bul_show_ects"]
colkeys = ["titre", "module"] # noms des colonnes à afficher
col_keys = ["titre", "module"] # noms des colonnes à afficher
if with_col_rang:
colkeys += ["rang"]
col_keys += ["rang"]
if with_col_minmax:
colkeys += ["min"]
col_keys += ["min"]
if with_col_moypromo:
colkeys += ["moy"]
col_keys += ["moy"]
if with_col_minmax:
colkeys += ["max"]
colkeys += ["note"]
col_keys += ["max"]
col_keys += ["note"]
if with_col_coef:
colkeys += ["coef"]
col_keys += ["coef"]
if with_col_ects:
colkeys += ["ects"]
col_keys += ["ects"]
if with_col_abs:
colkeys += ["abs"]
col_keys += ["abs"]
colidx = {} # { nom_colonne : indice à partir de 0 } (pour styles platypus)
i = 0
for k in colkeys:
for k in col_keys:
colidx[k] = i
i += 1
@ -313,7 +315,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
bul_pdf_mod_colwidth = float(prefs["bul_pdf_mod_colwidth"]) * cm
else:
bul_pdf_mod_colwidth = None
colWidths = {
col_widths = {
"titre": None,
"module": bul_pdf_mod_colwidth,
"min": 1.5 * cm,
@ -409,7 +411,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
# Chaque UE:
for ue in I["ues"]:
ue_type = None
coef_ue = ue["coef_ue_txt"]
coef_ue = ue["coef_ue_txt"] if prefs["bul_show_ue_coef"] else ""
ue_descr = ue["ue_descr_txt"]
rowstyle = ""
plusminus = minuslink #
@ -541,7 +543,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
("BOX", (0, 0), (-1, -1), 0.4, blue), # ajoute cadre extérieur bleu:
]
#
return colkeys, P, pdf_style, colWidths
return col_keys, P, pdf_style, col_widths
def _list_modules(
self,
@ -592,7 +594,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
"_titre_colspan": 2,
"rang": mod["mod_rang_txt"], # vide si pas option rang
"note": mod["mod_moy_txt"],
"coef": mod["mod_coef_txt"],
"coef": mod["mod_coef_txt"] if prefs["bul_show_coef"] else "",
"abs": mod.get(
"mod_abs_txt", ""
), # absent si pas option show abs module
@ -656,7 +658,9 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
eval_style = ""
t = {
"module": '<bullet indent="2mm">&bull;</bullet>&nbsp;' + e["name"],
"coef": "<i>" + e["coef_txt"] + "</i>",
"coef": ("<i>" + e["coef_txt"] + "</i>")
if prefs["bul_show_coef"]
else "",
"_hidden": hidden,
"_module_target": e["target_html"],
# '_module_help' : ,

View File

@ -33,17 +33,12 @@
"""
# API ScoDoc8 pour les caches:
# sco_cache.NotesTableCache.get( formsemestre_id)
# => sco_cache.NotesTableCache.get(formsemestre_id)
# API pour les caches:
# sco_cache.MyCache.get( formsemestre_id)
# => sco_cache.MyCache.get(formsemestre_id)
#
# sco_core.inval_cache(formsemestre_id=None, pdfonly=False, formsemestre_id_list=None)
# => deprecated, NotesTableCache.invalidate_formsemestre(formsemestre_id=None, pdfonly=False)
#
#
# Nouvelles fonctions:
# sco_cache.NotesTableCache.delete(formsemestre_id)
# sco_cache.NotesTableCache.delete_many(formsemestre_id_list)
# sco_cache.MyCache.delete(formsemestre_id)
# sco_cache.MyCache.delete_many(formsemestre_id_list)
#
# Bulletins PDF:
# sco_cache.SemBulletinsPDFCache.get(formsemestre_id, version)
@ -203,49 +198,6 @@ class SemInscriptionsCache(ScoDocCache):
duration = 12 * 60 * 60 # ttl 12h
class NotesTableCache(ScoDocCache):
"""Cache pour les NotesTable
Clé: formsemestre_id
Valeur: NotesTable instance
"""
prefix = "NT"
@classmethod
def get(cls, formsemestre_id, compute=True):
"""Returns NotesTable for this formsemestre
Search in local cache (g.nt_cache) or global app cache (eg REDIS)
If not in cache:
If compute is True, build it and cache it
Else return None
"""
# try local cache (same request)
if not hasattr(g, "nt_cache"):
g.nt_cache = {}
else:
if formsemestre_id in g.nt_cache:
return g.nt_cache[formsemestre_id]
# try REDIS
key = cls._get_key(formsemestre_id)
nt = CACHE.get(key)
if nt:
g.nt_cache[formsemestre_id] = nt # cache locally (same request)
return nt
if not compute:
return None
# Recompute requested table:
from app.scodoc import notes_table
t0 = time.time()
nt = notes_table.NotesTable(formsemestre_id)
t1 = time.time()
_ = cls.set(formsemestre_id, nt) # cache in REDIS
t2 = time.time()
log(f"cached formsemestre_id={formsemestre_id} ({(t1-t0):g}s +{(t2-t1):g}s)")
g.nt_cache[formsemestre_id] = nt
return nt
def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=False)
formsemestre_id=None, pdfonly=False
):
@ -278,22 +230,24 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
if not pdfonly:
# Delete cached notes and evaluations
NotesTableCache.delete_many(formsemestre_ids)
if formsemestre_id:
for fid in formsemestre_ids:
EvaluationCache.invalidate_sem(fid)
if hasattr(g, "nt_cache") and fid in g.nt_cache:
del g.nt_cache[fid]
if (
hasattr(g, "formsemestre_results_cache")
and fid in g.formsemestre_results_cache
):
del g.formsemestre_results_cache[fid]
else:
# optimization when we invalidate all evaluations:
EvaluationCache.invalidate_all_sems()
if hasattr(g, "nt_cache"):
del g.nt_cache
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)
SemBulletinsPDFCache.invalidate_sems(formsemestre_ids)
ResultatsSemestreCache.delete_many(formsemestre_ids)
ValidationsSemestreCache.delete_many(formsemestre_ids)
class DefferedSemCacheManager:

View File

@ -282,7 +282,7 @@ class TypeParcours(object):
return [
ue_status
for ue_status in ues_status
if ue_status["coef_ue"] > 0
if ue_status["coef_ue"]
and isinstance(ue_status["moy"], float)
and ue_status["moy"] < self.get_barre_ue(ue_status["ue"]["type"])
]
@ -587,7 +587,7 @@ class ParcoursILEPS(TypeParcours):
# SESSION_ABBRV = 'A' # A1, A2, ...
COMPENSATION_UE = False
UNUSED_CODES = set((ADC, ATT, ATB, ATJ))
ALLOWED_UE_TYPES = [UE_STANDARD, UE_OPTIONNELLE]
ALLOWED_UE_TYPES = [UE_STANDARD, UE_OPTIONNELLE, UE_SPORT]
# Barre moy gen. pour validation semestre:
BARRE_MOY = 10.0
# Barre pour UE ILEPS: 8/20 pour UE standards ("fondamentales")

View File

@ -51,6 +51,7 @@ import fcntl
import subprocess
import requests
from flask import flash
from flask_login import current_user
import app.scodoc.notesdb as ndb
@ -124,6 +125,7 @@ def sco_dump_and_send_db():
fcntl.flock(x, fcntl.LOCK_UN)
log("sco_dump_and_send_db: done.")
flash("Données envoyées au serveur d'assistance")
return "\n".join(H) + html_sco_header.sco_footer()
@ -186,18 +188,28 @@ def _send_db(ano_db_name):
log("uploading anonymized dump...")
files = {"file": (ano_db_name + ".dump", dump)}
r = requests.post(
scu.SCO_DUMP_UP_URL,
files=files,
data={
"dept_name": sco_preferences.get_preference("DeptName"),
"serial": _get_scodoc_serial(),
"sco_user": str(current_user),
"sent_by": sco_users.user_info(str(current_user))["nomcomplet"],
"sco_version": sco_version.SCOVERSION,
"sco_fullversion": scu.get_scodoc_version(),
},
)
try:
r = requests.post(
scu.SCO_DUMP_UP_URL,
files=files,
data={
"dept_name": sco_preferences.get_preference("DeptName"),
"serial": _get_scodoc_serial(),
"sco_user": str(current_user),
"sent_by": sco_users.user_info(str(current_user))["nomcomplet"],
"sco_version": sco_version.SCOVERSION,
"sco_fullversion": scu.get_scodoc_version(),
},
)
except requests.exceptions.ConnectionError as exc:
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

View File

@ -52,6 +52,7 @@ def html_edit_formation_apc(
"""
parcours = formation.get_parcours()
assert parcours.APC_SAE
ressources = formation.modules.filter_by(module_type=ModuleType.RESSOURCE).order_by(
Module.semestre_id, Module.numero, Module.code
)
@ -68,6 +69,19 @@ def html_edit_formation_apc(
).order_by(
Module.semestre_id, Module.module_type.desc(), Module.numero, Module.code
)
ues_by_sem = {}
ects_by_sem = {}
for semestre_idx in semestre_ids:
ues_by_sem[semestre_idx] = formation.ues.filter_by(
semestre_idx=semestre_idx
).order_by(UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme)
ects = [ue.ects for ue in ues_by_sem[semestre_idx]]
if None in ects:
ects_by_sem[semestre_idx] = '<span class="missing_ue_ects">manquant</span>'
else:
ects_by_sem[semestre_idx] = sum(ects)
arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags()
icons = {
@ -93,7 +107,8 @@ def html_edit_formation_apc(
editable=editable,
tag_editable=tag_editable,
icons=icons,
UniteEns=UniteEns,
ues_by_sem=ues_by_sem,
ects_by_sem=ects_by_sem,
),
]
for semestre_idx in semestre_ids:

View File

@ -500,13 +500,20 @@ def module_edit(module_id=None):
matieres = matieres.filter(UniteEns.semestre_idx == a_module.ue.semestre_idx)
if is_apc:
# ne conserve que la 1ere matière de chaque UE,
# et celle à laquelle ce module est rattaché
matieres = [
mat
for mat in matieres
if a_module.matiere.id == mat.id or mat.id == mat.ue.matieres.first().id
]
mat_names = [
"S%s / %s" % (mat.ue.semestre_idx, mat.ue.acronyme) for mat in matieres
]
else:
mat_names = ["%s / %s" % (mat.ue.acronyme, mat.titre or "") for mat in matieres]
ue_mat_ids = ["%s!%s" % (mat.ue.id, mat.id) for mat in matieres]
ue_mat_ids = ["%s!%s" % (mat.ue.id, mat.id) for mat in matieres]
module["ue_matiere_id"] = "%s!%s" % (module["ue_id"], module["matiere_id"])
semestres_indices = list(range(1, parcours.NB_SEM + 1))
@ -544,14 +551,18 @@ def module_edit(module_id=None):
# ne propose pas SAE et Ressources, sauf si déjà de ce type...
module_types = (
set(scu.ModuleType) - {scu.ModuleType.RESSOURCE, scu.ModuleType.SAE}
) | {a_module.module_type or scu.ModuleType.STANDARD}
) | {
scu.ModuleType(a_module.module_type)
if a_module.module_type
else scu.ModuleType.STANDARD
}
descr = [
(
"code",
{
"size": 10,
"explanation": "code du module (doit être unique dans la formation)",
"explanation": "code du module (issu du programme, exemple M1203 ou R2.01. Doit être unique dans la formation)",
"allow_null": False,
"validator": lambda val, field, formation_id=formation_id: check_module_code_unicity(
val, field, formation_id, module_id=module_id
@ -690,7 +701,10 @@ def module_edit(module_id=None):
{
"title": "Code Apogée",
"size": 25,
"explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules",
"explanation": """(optionnel) code élément pédagogique Apogée ou liste de codes ELP
séparés par des virgules (ce code est propre à chaque établissement, se rapprocher
du référent Apogée).
""",
"validator": lambda val, _: len(val) < APO_CODE_STR_LEN,
},
),
@ -734,8 +748,11 @@ def module_edit(module_id=None):
else:
# l'UE de rattachement peut changer
tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!")
x, y = tf[2]["ue_matiere_id"].split("!")
tf[2]["ue_id"] = int(x)
tf[2]["matiere_id"] = int(y)
old_ue_id = a_module.ue.id
new_ue_id = int(tf[2]["ue_id"])
new_ue_id = tf[2]["ue_id"]
if (old_ue_id != new_ue_id) and in_use:
new_ue = UniteEns.query.get_or_404(new_ue_id)
if new_ue.semestre_idx != a_module.ue.semestre_idx:

View File

@ -29,7 +29,7 @@
"""
import flask
from flask import url_for, render_template
from flask import flash, render_template, url_for
from flask import g, request
from flask_login import current_user
@ -89,6 +89,7 @@ _ueEditor = ndb.EditableTable(
input_formators={
"type": ndb.int_null_is_zero,
"is_external": ndb.bool_or_str,
"ects": ndb.float_null_is_null,
},
output_formators={
"numero": ndb.int_null_is_zero,
@ -107,8 +108,6 @@ def ue_list(*args, **kw):
def do_ue_create(args):
"create an ue"
from app.scodoc import sco_formations
cnx = ndb.GetDBConnexion()
# check duplicates
ues = ue_list({"formation_id": args["formation_id"], "acronyme": args["acronyme"]})
@ -117,6 +116,14 @@ def do_ue_create(args):
f"""Acronyme d'UE "{args['acronyme']}" déjà utilisé !
(chaque UE doit avoir un acronyme unique dans la formation)"""
)
if not "ue_code" in args:
# évite les conflits de code
while True:
cursor = db.session.execute("select notes_newid_ucod();")
code = cursor.fetchone()[0]
if UniteEns.query.filter_by(ue_code=code).count() == 0:
break
args["ue_code"] = code
# create
ue_id = _ueEditor.create(cnx, args)
@ -128,6 +135,8 @@ def do_ue_create(args):
formation = Formation.query.get(args["formation_id"])
formation.invalidate_module_coefs()
# news
ue = UniteEns.query.get(ue_id)
flash(f"UE créée (code {ue.ue_code})")
formation = Formation.query.get(args["formation_id"])
sco_news.add(
typ=sco_news.NEWS_FORM,
@ -296,7 +305,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
(
"numero",
{
"size": 2,
"size": 4,
"explanation": "numéro (1,2,3,4) de l'UE pour l'ordre d'affichage",
"type": "int",
},
@ -339,6 +348,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
"type": "float",
"title": "ECTS",
"explanation": "nombre de crédits ECTS",
"allow_null": not is_apc, # ects requis en APC
},
),
(
@ -462,8 +472,10 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
"semestre_id": tf[2]["semestre_idx"],
},
)
flash("UE créée")
else:
do_ue_edit(tf[2])
flash("UE modifiée")
return flask.redirect(
url_for(
"notes.ue_table",
@ -601,7 +613,12 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
_add_ue_semestre_id(ues_externes, is_apc)
ues.sort(key=lambda u: (u["semestre_id"], u["numero"]))
ues_externes.sort(key=lambda u: (u["semestre_id"], u["numero"]))
has_duplicate_ue_codes = len(set([ue["ue_code"] for ue in ues])) != len(ues)
# Codes dupliqués (pour aider l'utilisateur)
seen = set()
duplicated_codes = {
ue["ue_code"] for ue in ues if ue["ue_code"] in seen or seen.add(ue["ue_code"])
}
ues_with_duplicated_code = [ue for ue in ues if ue["ue_code"] in duplicated_codes]
has_perm_change = current_user.has_permission(Permission.ScoChangeFormation)
# editable = (not locked) and has_perm_change
@ -664,11 +681,17 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
if msg:
H.append('<p class="msg">' + msg + "</p>")
if has_duplicate_ue_codes:
if ues_with_duplicated_code:
H.append(
"""<div class="ue_warning"><span>Attention: plusieurs UE de cette
formation ont le même code. Il faut corriger cela ci-dessous,
sinon les calculs d'ECTS seront erronés !</span></div>"""
f"""<div class="ue_warning"><span>Attention: plusieurs UE de cette
formation ont le même code : <tt>{
', '.join([
'<a class="stdlink" href="' + url_for( "notes.ue_edit", scodoc_dept=g.scodoc_dept, ue_id=ue["ue_id"] )
+ '">' + ue["acronyme"] + " (code " + ue["ue_code"] + ")</a>"
for ue in ues_with_duplicated_code ])
}</tt>.
Il faut corriger cela, sinon les capitalisations et ECTS seront
erronés !</span></div>"""
)
# Description de la formation
@ -699,16 +722,16 @@ 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)}">
{formation.referentiel_competence.type_titre} {formation.referentiel_competence.specialite_long}
</a> """
</a>&nbsp;"""
msg_refcomp = "changer"
H.append(
f"""
<ul>
<li>{descr_refcomp}&nbsp; <a class="stdlink" href="{url_for('notes.refcomp_assoc_formation',
<li>{descr_refcomp} <a class="stdlink" href="{url_for('notes.refcomp_assoc_formation',
scodoc_dept=g.scodoc_dept, formation_id=formation_id)
}">{msg_refcomp}</a>
</li>
<li><a class="stdlink" href="{
<li> <a class="stdlink" href="{
url_for('notes.edit_modules_ue_coefs', scodoc_dept=g.scodoc_dept, formation_id=formation_id, semestre_idx=semestre_idx)
}">éditer les coefficients des ressources et SAÉs</a>
</li>
@ -735,6 +758,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
)
)
else:
H.append('<div class="formation_classic_infos">')
H.append(
_ue_table_ues(
parcours,
@ -764,7 +788,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
</ul>
"""
)
H.append("</div>")
H.append("</div>") # formation_ue_list
if ues_externes:
@ -913,10 +937,10 @@ def _ue_table_ues(
cur_ue_semestre_id = None
iue = 0
for ue in ues:
if ue["ects"]:
ue["ects_str"] = ", %g ECTS" % ue["ects"]
else:
if ue["ects"] is None:
ue["ects_str"] = ""
else:
ue["ects_str"] = ", %g ECTS" % ue["ects"]
if editable:
klass = "span_apo_edit"
else:
@ -930,13 +954,13 @@ def _ue_table_ues(
if cur_ue_semestre_id != ue["semestre_id"]:
cur_ue_semestre_id = ue["semestre_id"]
if iue > 0:
H.append("</ul>")
if ue["semestre_id"] == sco_codes_parcours.UE_SEM_DEFAULT:
lab = "Pas d'indication de semestre:"
else:
lab = "Semestre %s:" % ue["semestre_id"]
H.append('<div class="ue_list_tit_sem">%s</div>' % lab)
H.append(
'<div class="ue_list_div"><div class="ue_list_tit_sem">%s</div>' % lab
)
H.append('<ul class="notes_ue_list">')
H.append('<li class="notes_ue_list">')
if iue != 0 and editable:
@ -953,7 +977,6 @@ def _ue_table_ues(
)
else:
H.append(arrow_none)
iue += 1
ue["acro_titre"] = str(ue["acronyme"])
if ue["titre"] != ue["acronyme"]:
ue["acro_titre"] += " " + str(ue["titre"])
@ -1001,6 +1024,16 @@ def _ue_table_ues(
delete_disabled_icon,
)
)
if (iue >= len(ues) - 1) or ue["semestre_id"] != ues[iue + 1]["semestre_id"]:
H.append(
f"""</ul><ul><li><a href="{url_for('notes.ue_create', scodoc_dept=g.scodoc_dept,
formation_id=ue['formation_id'], semestre_idx=ue['semestre_id'])
}">Ajouter une UE dans le semestre {ue['semestre_id'] or ''}</a></li></ul>
</div>
"""
)
iue += 1
return "\n".join(H)
@ -1268,7 +1301,6 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False):
f"""Acronyme d'UE "{args['acronyme']}" déjà utilisé !
(chaque UE doit avoir un acronyme unique dans la formation)"""
)
# On ne peut pas supprimer le code UE:
if "ue_code" in args and not args["ue_code"]:
del args["ue_code"]

View File

@ -53,7 +53,7 @@ from app.scodoc.sco_exceptions import ScoValueError
def apo_semset_maq_status(
semset_id="",
semset_id: int,
allow_missing_apo=False,
allow_missing_decisions=False,
allow_missing_csv=False,
@ -65,7 +65,7 @@ def apo_semset_maq_status(
):
"""Page statut / tableau de bord"""
if not semset_id:
raise ValueError("invalid null semset_id")
raise ScoValueError("invalid null semset_id")
semset = sco_semset.SemSet(semset_id=semset_id)
semset.fill_formsemestres()
# autorise export meme si etudiants Apo manquants:

View File

@ -33,8 +33,7 @@ import os
import time
from operator import itemgetter
from flask import url_for, g, request
from flask_mail import Message
from flask import url_for, g
from app import email
from app import log
@ -46,7 +45,6 @@ from app.scodoc.sco_exceptions import ScoGenError, ScoValueError
from app.scodoc import safehtml
from app.scodoc import sco_preferences
from app.scodoc.scolog import logdb
from app.scodoc.TrivialFormulator import TrivialFormulator
def format_etud_ident(etud):
@ -860,7 +858,7 @@ def list_scolog(etudid):
return cursor.dictfetchall()
def fill_etuds_info(etuds, add_admission=True):
def fill_etuds_info(etuds: list[dict], add_admission=True):
"""etuds est une liste d'etudiants (mappings)
Pour chaque etudiant, ajoute ou formatte les champs
-> informations pour fiche etudiant ou listes diverses
@ -977,7 +975,10 @@ def etud_inscriptions_infos(etudid: int, ne="") -> dict:
def descr_situation_etud(etudid: int, ne="") -> str:
"""chaîne décrivant la situation actuelle de l'étudiant"""
"""Chaîne décrivant la situation actuelle de l'étudiant
XXX Obsolete, utiliser Identite.descr_situation_etud() dans
les nouveaux codes
"""
from app.scodoc import sco_formsemestre
cnx = ndb.GetDBConnexion()

View File

@ -405,7 +405,6 @@ def formsemestre_evaluations_cal(formsemestre_id):
"""Page avec calendrier de toutes les evaluations de ce semestre"""
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
sem = formsemestre.to_dict()
evals = nt.get_evaluations_etats()
nb_evals = len(evals)
@ -416,8 +415,8 @@ def formsemestre_evaluations_cal(formsemestre_id):
today = time.strftime("%Y-%m-%d")
year = int(sem["annee_debut"])
if sem["mois_debut_ord"] < 8:
year = formsemestre.date_debut.year
if formsemestre.date_debut.month < 8:
year -= 1 # calendrier septembre a septembre
events = {} # (day, halfday) : event
for e in evals:
@ -537,11 +536,10 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, format="html"):
"""Experimental: un tableau indiquant pour chaque évaluation
le nombre de jours avant la publication des notes.
N'indique pas les évaluations de ratrapage ni celles des modules de bonus/malus.
N'indique pas les évaluations de rattrapage ni celles des modules de bonus/malus.
"""
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
sem = formsemestre.to_dict()
evals = nt.get_evaluations_etats()
T = []
@ -607,7 +605,7 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, format="html"):
origin="Généré par %s le " % sco_version.SCONAME
+ scu.timedate_human_repr()
+ "",
filename=scu.make_filename("evaluations_delais_" + sem["titreannee"]),
filename=scu.make_filename("evaluations_delais_" + formsemestre.titre_annee()),
)
return tab.make_page(format=format)
@ -635,16 +633,13 @@ def evaluation_describe(evaluation_id="", edit_in_place=True):
'<span class="evallink"><a class="stdlink" href="evaluation_listenotes?moduleimpl_id=%s">voir toutes les notes du module</a></span>'
% moduleimpl_id
)
mod_descr = (
'<a href="moduleimpl_status?moduleimpl_id=%s">%s %s</a> <span class="resp">(resp. <a title="%s">%s</a>)</span> %s'
% (
moduleimpl_id,
Mod["code"] or "",
Mod["titre"] or "?",
nomcomplet,
resp,
link,
)
mod_descr = '<a href="moduleimpl_status?moduleimpl_id=%s">%s %s</a> <span class="resp">(resp. <a title="%s">%s</a>)</span> %s' % (
moduleimpl_id,
Mod["code"] or "",
Mod["titre"] or "?",
nomcomplet,
resp,
link,
)
etit = E["description"] or ""

View File

@ -39,6 +39,7 @@ from app.scodoc.gen_tables import GenTable
from app.scodoc import html_sco_header
from app.scodoc import sco_etud
from app.scodoc import sco_groups
from app.scodoc.sco_exceptions import ScoException
from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_preferences
@ -179,7 +180,9 @@ def search_etud_in_dept(expnom=""):
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"])
sco_groups.etud_add_group_infos(
e, e["cursem"]["formsemestre_id"] if e["cursem"] else None
)
tab = GenTable(
columns_ids=("nomprenom", "code_nip", "inscription", "groupes"),
@ -221,7 +224,10 @@ def search_etuds_infos(expnom=None, code_nip=None):
cnx = ndb.GetDBConnexion()
if expnom and not may_be_nip:
expnom = expnom.upper() # les noms dans la BD sont en uppercase
etuds = sco_etud.etudident_list(cnx, args={"nom": expnom}, test="~")
try:
etuds = sco_etud.etudident_list(cnx, args={"nom": expnom}, test="~")
except ScoException:
etuds = []
else:
code_nip = code_nip or expnom
if code_nip:

View File

@ -151,8 +151,14 @@ def formation_export(
if mod["ects"] is None:
del mod["ects"]
filename = f"scodoc_formation_{formation.departement.acronym}_{formation.acronyme or ''}_v{formation.version}"
return scu.sendResult(
F, name="formation", format=format, force_outer_xml_tag=False, attached=True
F,
name="formation",
format=format,
force_outer_xml_tag=False,
attached=True,
filename=filename,
)

View File

@ -78,7 +78,7 @@ def formsemestre_createwithmodules():
H = [
html_sco_header.sco_header(
page_title="Création d'un semestre",
javascripts=["libjs/AutoSuggest.js"],
javascripts=["libjs/AutoSuggest.js", "js/formsemestre_edit.js"],
cssstyles=["css/autosuggest_inquisitor.css"],
bodyOnLoad="init_tf_form('')",
),
@ -99,7 +99,7 @@ def formsemestre_editwithmodules(formsemestre_id):
H = [
html_sco_header.html_sem_header(
"Modification du semestre",
javascripts=["libjs/AutoSuggest.js"],
javascripts=["libjs/AutoSuggest.js", "js/formsemestre_edit.js"],
cssstyles=["css/autosuggest_inquisitor.css"],
bodyOnLoad="init_tf_form('')",
)
@ -213,7 +213,10 @@ def do_formsemestre_createwithmodules(edit=False):
# en APC, ne permet pas de changer de semestre
semestre_id_list = [formsemestre.semestre_id]
else:
semestre_id_list = [-1] + list(range(1, NB_SEM + 1))
semestre_id_list = list(range(1, NB_SEM + 1))
if not formation.is_apc():
# propose "pas de semestre" seulement en classique
semestre_id_list.insert(0, -1)
semestre_id_labels = []
for sid in semestre_id_list:
@ -341,6 +344,9 @@ def do_formsemestre_createwithmodules(edit=False):
"explanation": "en BUT, on ne peut pas modifier le semestre après création"
if formation.is_apc()
else "",
"attributes": ['onchange="change_semestre_id();"']
if formation.is_apc()
else "",
},
),
)
@ -493,7 +499,8 @@ def do_formsemestre_createwithmodules(edit=False):
{
"input_type": "boolcheckbox",
"title": "",
"explanation": "Autoriser tous les enseignants associés à un module à y créer des évaluations",
"explanation": """Autoriser tous les enseignants associés
à un module à y créer des évaluations""",
},
),
(
@ -534,11 +541,19 @@ def do_formsemestre_createwithmodules(edit=False):
]
nbmod = 0
if edit:
templ_sep = "<tr><td>%(label)s</td><td><b>Responsable</b></td><td><b>Inscrire</b></td></tr>"
else:
templ_sep = "<tr><td>%(label)s</td><td><b>Responsable</b></td></tr>"
for semestre_id in semestre_ids:
if formation.is_apc():
# pour restreindre l'édition aux module du semestre sélectionné
tr_class = f'class="sem{semestre_id}"'
else:
tr_class = ""
if edit:
templ_sep = f"""<tr {tr_class}><td>%(label)s</td><td><b>Responsable</b></td><td><b>Inscrire</b></td></tr>"""
else:
templ_sep = (
f"""<tr {tr_class}><td>%(label)s</td><td><b>Responsable</b></td></tr>"""
)
modform.append(
(
"sep",
@ -588,12 +603,12 @@ def do_formsemestre_createwithmodules(edit=False):
)
fcg += "</select>"
itemtemplate = (
"""<tr><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s</td><td>"""
f"""<tr {tr_class}><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s</td><td>"""
+ fcg
+ "</td></tr>"
)
else:
itemtemplate = """<tr><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s</td></tr>"""
itemtemplate = f"""<tr {tr_class}><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s</td></tr>"""
modform.append(
(
"MI" + str(mod["module_id"]),

View File

@ -31,7 +31,7 @@
from flask import current_app
from flask import g
from flask import request
from flask import url_for
from flask import render_template, url_for
from flask_login import current_user
from app import log
@ -411,7 +411,7 @@ def formsemestre_status_menubar(sem):
"enabled": sco_permissions_check.can_validate_sem(formsemestre_id),
},
{
"title": "Editer les PV et archiver les résultats",
"title": "Éditer les PV et archiver les résultats",
"endpoint": "notes.formsemestre_archive",
"args": {"formsemestre_id": formsemestre_id},
"enabled": sco_permissions_check.can_edit_pv(formsemestre_id),
@ -445,6 +445,7 @@ def retreive_formsemestre_from_request() -> int:
"""Cherche si on a de quoi déduire le semestre affiché à partir des
arguments de la requête:
formsemestre_id ou moduleimpl ou evaluation ou group_id ou partition_id
Returns None si pas défini.
"""
if request.method == "GET":
args = request.args
@ -505,34 +506,17 @@ def formsemestre_page_title():
return ""
try:
formsemestre_id = int(formsemestre_id)
sem = sco_formsemestre.get_formsemestre(formsemestre_id).copy()
formsemestre = FormSemestre.query.get(formsemestre_id)
except:
log("can't find formsemestre_id %s" % formsemestre_id)
return ""
fill_formsemestre(sem)
h = f"""<div class="formsemestre_page_title">
<div class="infos">
<span class="semtitle"><a class="stdlink" title="{sem['session_id']}"
href="{url_for('notes.formsemestre_status',
scodoc_dept=g.scodoc_dept, formsemestre_id=sem['formsemestre_id'])}"
>{sem['titre']}</a><a
title="{sem['etape_apo_str']}">{sem['num_sem']}</a>{sem['modalitestr']}</span><span
class="dates"><a
title="du {sem['date_debut']} au {sem['date_fin']} "
>{sem['mois_debut']} - {sem['mois_fin']}</a></span><span
class="resp"><a title="{sem['nomcomplet']}">{sem['resp']}</a></span><span
class="nbinscrits"><a class="discretelink"
href="{url_for("scolar.groups_view",
scodoc_dept=g.scodoc_dept, formsemestre_id=sem['formsemestre_id'])}"
>{sem['nbinscrits']} inscrits</a></span><span
class="lock">{sem['locklink']}</span><span
class="eye">{sem['eyelink']}</span>
</div>
{formsemestre_status_menubar(sem)}
</div>
"""
h = render_template(
"formsemestre_page_title.html",
formsemestre=formsemestre,
scu=scu,
sem_menu_bar=formsemestre_status_menubar(formsemestre.to_dict()),
)
return h
@ -595,11 +579,12 @@ def formsemestre_description_table(formsemestre_id, with_evals=False):
"""Description du semestre sous forme de table exportable
Liste des modules et de leurs coefficients
"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id)
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
F = sco_formations.formation_list(args={"formation_id": formsemestre.formation_id})[
0
]
parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
Mlist = sco_moduleimpl.moduleimpl_withmodule_list(
formsemestre_id=formsemestre_id, sort_by_ue=True
@ -709,7 +694,7 @@ def formsemestre_description_table(formsemestre_id, with_evals=False):
titles["coefficient"] = "Coef. éval."
titles["evalcomplete_str"] = "Complète"
titles["publish_incomplete_str"] = "Toujours Utilisée"
title = "%s %s" % (parcours.SESSION_NAME.capitalize(), sem["titremois"])
title = "%s %s" % (parcours.SESSION_NAME.capitalize(), formsemestre.titre_mois())
return GenTable(
columns_ids=columns_ids,
@ -986,7 +971,6 @@ Il y a des notes en attente ! Le classement des étudiants n'a qu'une valeur ind
def formsemestre_status(formsemestre_id=None):
"""Tableau de bord semestre HTML"""
# porté du DTML
cnx = ndb.GetDBConnexion()
sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
modimpls = sco_moduleimpl.moduleimpl_withmodule_list(
formsemestre_id=formsemestre_id
@ -1077,7 +1061,7 @@ def formsemestre_status(formsemestre_id=None):
"</p>",
]
if use_ue_coefs:
if use_ue_coefs and not formsemestre.formation.is_apc():
H.append(
"""
<p class="infop">utilise les coefficients d'UE pour calculer la moyenne générale.</p>

View File

@ -585,15 +585,17 @@ def formsemestre_recap_parcours_table(
else:
H.append('<td colspan="%d"><em>en cours</em></td>')
H.append('<td class="rcp_nonass">%s</td>' % ass) # abs
# acronymes UEs auxquelles l'étudiant est inscrit:
# XXX il est probable que l'on doive ici ajouter les
# XXX UE capitalisées
# acronymes UEs auxquelles l'étudiant est inscrit (ou capitalisé)
ues = nt.get_ues_stat_dict(filter_sport=True)
cnx = ndb.GetDBConnexion()
etud_ue_status = {
ue["ue_id"]: nt.get_etud_ue_status(etudid, ue["ue_id"]) for ue in ues
}
ues = [
ue
for ue in ues
if etud_est_inscrit_ue(cnx, etudid, sem["formsemestre_id"], ue["ue_id"])
or etud_ue_status[ue["ue_id"]]["is_capitalized"]
]
for ue in ues:
@ -644,7 +646,7 @@ def formsemestre_recap_parcours_table(
code = decisions_ue[ue["ue_id"]]["code"]
else:
code = ""
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
ue_status = etud_ue_status[ue["ue_id"]]
moy_ue = ue_status["moy"] if ue_status else ""
explanation_ue = [] # list of strings
if code == ADM:
@ -1250,7 +1252,7 @@ def check_formation_ues(formation_id):
for ue in ues:
# formsemestres utilisant cette ue ?
sems = ndb.SimpleDictFetch(
"""SELECT DISTINCT sem.id AS formsemestre_id, sem.*
"""SELECT DISTINCT sem.id AS formsemestre_id, sem.*
FROM notes_formsemestre sem, notes_modules mod, notes_moduleimpl mi
WHERE sem.formation_id = %(formation_id)s
AND mod.id = mi.module_id
@ -1269,11 +1271,11 @@ def check_formation_ues(formation_id):
return "", {}
# Genere message HTML:
H = [
"""<div class="ue_warning"><span>Attention:</span> les UE suivantes de cette formation
"""<div class="ue_warning"><span>Attention:</span> les UE suivantes de cette formation
sont utilisées dans des
semestres de rangs différents (eg S1 et S3). <br/>Cela peut engendrer des problèmes pour
la capitalisation des UE. Il serait préférable d'essayer de rectifier cette situation:
soit modifier le programme de la formation (définir des UE dans chaque semestre),
semestres de rangs différents (eg S1 et S3). <br/>Cela peut engendrer des problèmes pour
la capitalisation des UE. Il serait préférable d'essayer de rectifier cette situation:
soit modifier le programme de la formation (définir des UE dans chaque semestre),
soit veiller à saisir le bon indice de semestre dans le menu lors de la validation d'une
UE extérieure.
<ul>
@ -1286,7 +1288,11 @@ def check_formation_ues(formation_id):
for x in ue_multiples[ue["ue_id"]]
]
slist = ", ".join(
["%(titreannee)s (<em>semestre %(semestre_id)s</em>)" % s for s in sems]
[
"""%(titreannee)s (<em>semestre <b class="fontred">%(semestre_id)s</b></em>)"""
% s
for s in sems
]
)
H.append("<li><b>%s</b> : %s</li>" % (ue["acronyme"], slist))
H.append("</ul></div>")

View File

@ -124,7 +124,7 @@ def get_partition(partition_id):
{"partition_id": partition_id},
)
if not r:
raise ValueError("invalid partition_id (%s)" % partition_id)
raise ScoValueError(f"Partition inconnue (déjà supprimée ?) ({partition_id})")
return r[0]
@ -321,7 +321,7 @@ def get_group_infos(group_id, etat=None): # was _getlisteetud
t["etath"] = t["etat"]
# Add membership for all partitions, 'partition_id' : group
for etud in members: # long: comment eviter ces boucles ?
etud_add_group_infos(etud, sem)
etud_add_group_infos(etud, sem["formsemestre_id"])
if group["group_name"] != None:
group_tit = "%s %s" % (group["partition_name"], group["group_name"])
@ -413,12 +413,12 @@ def formsemestre_get_etud_groupnames(formsemestre_id, attr="group_name"):
return R
def etud_add_group_infos(etud, sem, sep=" "):
def etud_add_group_infos(etud, formsemestre_id, sep=" "):
"""Add informations on partitions and group memberships to etud (a dict with an etudid)"""
etud[
"partitions"
] = collections.OrderedDict() # partition_id : group + partition_name
if not sem:
if not formsemestre_id:
etud["groupes"] = ""
return etud
@ -430,7 +430,7 @@ def etud_add_group_infos(etud, sem, sep=" "):
and p.formsemestre_id = %(formsemestre_id)s
ORDER BY p.numero
""",
{"etudid": etud["etudid"], "formsemestre_id": sem["formsemestre_id"]},
{"etudid": etud["etudid"], "formsemestre_id": formsemestre_id},
)
for info in infos:
@ -439,13 +439,13 @@ def etud_add_group_infos(etud, sem, sep=" "):
# resume textuel des groupes:
etud["groupes"] = sep.join(
[g["group_name"] for g in infos if g["group_name"] != None]
[gr["group_name"] for gr in infos if gr["group_name"] is not None]
)
etud["partitionsgroupes"] = sep.join(
[
g["partition_name"] + ":" + g["group_name"]
for g in infos
if g["group_name"] != None
gr["partition_name"] + ":" + gr["group_name"]
for gr in infos
if gr["group_name"] is not None
]
)

View File

@ -0,0 +1,96 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 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
#
##############################################################################
"""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
import app.scodoc.sco_utils as scu
import sco_version
def groups_list_annotation(group_ids: list[int]) -> list[dict]:
"""Renvoie la liste des annotations pour les groupes d"étudiants indiqués
Arg: liste des id de groupes
Clés: etudid, ine, nip, nom, prenom, date, comment
"""
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
annotations = []
for group_id in group_ids:
cursor.execute(
"""SELECT i.id AS etudid, i.code_nip, i.code_ine, i.nom, i.prenom, ea.date, ea.comment
FROM group_membership gm, identite i, etud_annotations ea
WHERE gm.group_id=%(group_ids)s
AND gm.etudid=i.id
AND i.id=ea.etudid
""",
{"group_ids": group_id},
)
annotations += cursor.dictfetchall()
return annotations
def groups_export_annotations(group_ids, formsemestre_id=None, format="html"):
"""Les annotations"""
groups_infos = sco_groups_view.DisplayedGroupsInfos(
group_ids, formsemestre_id=formsemestre_id
)
annotations = groups_list_annotation(groups_infos.group_ids)
for annotation in annotations:
annotation["date_str"] = annotation["date"].strftime("%d/%m/%Y à %Hh%M")
if format == "xls":
columns_ids = ("etudid", "nom", "prenom", "date", "comment")
else:
columns_ids = ("etudid", "nom", "prenom", "date_str", "comment")
table = GenTable(
rows=annotations,
columns_ids=columns_ids,
titles={
"etudid": "etudid",
"nom": "Nom",
"prenom": "Prénom",
"date": "Date",
"date_str": "Date",
"comment": "Annotation",
},
origin="Généré par %s le " % sco_version.SCONAME
+ 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),
)
return table.make_page(format=format)

View File

@ -826,6 +826,8 @@ def tab_absences_html(groups_infos, etat=None):
% groups_infos.groups_query_args,
"""<li><a class="stdlink" href="trombino?%s&format=pdflist">Liste d'appel avec photos</a></li>"""
% groups_infos.groups_query_args,
"""<li><a class="stdlink" href="groups_export_annotations?%s">Liste des annotations</a></li>"""
% groups_infos.groups_query_args,
"</ul>",
]
)

View File

@ -203,7 +203,7 @@ def sco_import_generate_excel_sample(
for field in titles:
if field == "groupes":
sco_groups.etud_add_group_infos(
etud, groups_infos.formsemestre, sep=";"
etud, groups_infos.formsemestre_id, sep=";"
)
l.append(etud["partitionsgroupes"])
else:

View File

@ -196,7 +196,10 @@ def do_inscrit(sem, etudids, inscrit_groupes=False):
if len(etud["sems"]) < 2:
continue
prev_formsemestre = etud["sems"][1]
sco_groups.etud_add_group_infos(etud, prev_formsemestre)
sco_groups.etud_add_group_infos(
etud,
prev_formsemestre["formsemestre_id"] if prev_formsemestre else None,
)
cursem_groups_by_name = dict(
[

View File

@ -151,6 +151,8 @@ class Logo:
Le format est renseigné au moment de la lecture (select) ou de la création (create) de l'objet
"""
self.logoname = secure_filename(logoname)
if not self.logoname:
self.logoname = "*** *** nom de logo invalide *** à changer ! *** ***"
self.scodoc_dept_id = dept_id
self.prefix = prefix or ""
if self.scodoc_dept_id:
@ -276,7 +278,7 @@ class Logo:
if self.mm is None:
return f'<logo name="{self.logoname}" width="?? mm" height="?? mm">'
else:
return f'<logo name="{self.logoname}" width="{self.mm[0]}mm"">'
return f'<logo name="{self.logoname}" width="{self.mm[0]}mm">'
def last_modified(self):
path = Path(self.filepath)

View File

@ -305,7 +305,10 @@ def moduleimpl_inscriptions_stats(formsemestre_id):
if can_change:
c_link = (
'<a class="discretelink" href="moduleimpl_inscriptions_edit?moduleimpl_id=%s">%s</a>'
% (mod["moduleimpl_id"], mod["descri"])
% (
mod["moduleimpl_id"],
mod["descri"] or "<i>(inscrire des étudiants)</i>",
)
)
else:
c_link = mod["descri"]

View File

@ -215,7 +215,9 @@ def ficheEtud(etudid=None):
info["modifadresse"] = ""
# Groupes:
sco_groups.etud_add_group_infos(info, info["cursem"])
sco_groups.etud_add_group_infos(
info, info["cursem"]["formsemestre_id"] if info["cursem"] else None
)
# Parcours de l'étudiant
if info["sems"]:

View File

@ -139,9 +139,7 @@ class SituationEtudParcoursGeneric(object):
# 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
) # True | False
self.semestre_non_terminal = self.sem["semestre_id"] != self.parcours.NB_SEM
if self.sem["semestre_id"] == NO_SEMESTRE_ID:
self.semestre_non_terminal = False
# Liste des semestres du parcours de cet étudiant:

View File

@ -220,12 +220,16 @@ class ScolarsPageTemplate(PageTemplate):
PageTemplate.__init__(self, "ScolarsPageTemplate", [content])
self.logo = None
logo = find_logo(
logoname="bul_pdf_background", dept_id=g.scodoc_dept_id, prefix=None
logoname="bul_pdf_background", dept_id=g.scodoc_dept_id
) or find_logo(
logoname="bul_pdf_background", dept_id=g.scodoc_dept_id, prefix=""
)
if logo is None:
# Also try to use PV background
logo = find_logo(
logoname="letter_background", dept_id=g.scodoc_dept_id, prefix=None
logoname="letter_background", dept_id=g.scodoc_dept_id
) or find_logo(
logoname="letter_background", dept_id=g.scodoc_dept_id, prefix=""
)
if logo is not None:
self.background_image_filename = logo.filepath

View File

@ -47,6 +47,11 @@ _SCO_PERMISSIONS = (
),
(1 << 25, "RelationsEntreprisesSend", "Envoyer des offres"),
(1 << 26, "RelationsEntreprisesValidate", "Valide les entreprises"),
# Api scodoc9
(1 << 27, "APIView", "Voir"),
(1 << 28, "APIEtudChangeGroups", "Modifier les groupes"),
(1 << 29, "APIEditAllNotes", "Modifier toutes les notes"),
(1 << 30, "APIAbsChange", "Saisir des absences"),
)

View File

@ -175,7 +175,7 @@ def etud_photo_is_local(etud: dict, size="small"):
return photo_pathname(etud["photo_filename"], size=size)
def etud_photo_html(etud=None, etudid=None, title=None, size="small"):
def etud_photo_html(etud: dict = None, etudid=None, title=None, size="small"):
"""HTML img tag for the photo, either in small size (h90)
or original size (size=="orig")
"""
@ -351,7 +351,8 @@ def copy_portal_photo_to_fs(etud):
"""Copy the photo from portal (distant website) to local fs.
Returns rel. path or None if copy failed, with a diagnostic message
"""
sco_etud.format_etud_ident(etud)
if "nomprenom" not in etud:
sco_etud.format_etud_ident(etud)
url = photo_portal_url(etud)
if not url:
return None, "%(nomprenom)s: pas de code NIP" % etud

View File

@ -245,6 +245,7 @@ PREF_CATEGORIES = (
),
("pe", {"title": "Avis de poursuites d'études"}),
("edt", {"title": "Connexion avec le logiciel d'emplois du temps"}),
("debug", {"title": "Tests / mise au point"}),
)
@ -1296,11 +1297,21 @@ class BasePreferences(object):
"labels": ["non", "oui"],
},
),
(
"bul_show_ue_coef",
{
"initvalue": 1,
"title": "Afficher coefficient des UE sur les bulletins",
"input_type": "boolcheckbox",
"category": "bul",
"labels": ["non", "oui"],
},
),
(
"bul_show_coef",
{
"initvalue": 1,
"title": "Afficher coefficient des ue/modules sur les bulletins",
"title": "Afficher coefficient des modules sur les bulletins",
"input_type": "boolcheckbox",
"category": "bul",
"labels": ["non", "oui"],
@ -1849,6 +1860,19 @@ class BasePreferences(object):
"category": "edt",
},
),
(
"email_test_mode_address",
{
"title": "Adresse de test",
"initvalue": "",
"explanation": """si cette adresse est indiquée, TOUS les mails
envoyés par ScoDoc de ce département vont aller vers elle
AU LIEU DE LEUR DESTINATION NORMALE !""",
"size": 30,
"category": "debug",
"only_global": True,
},
),
)
self.prefs_name = set([x[0] for x in self.prefs_definition])
@ -2114,7 +2138,7 @@ class BasePreferences(object):
return form
class SemPreferences(object):
class SemPreferences:
"""Preferences for a formsemestre"""
def __init__(self, formsemestre_id=None):
@ -2270,9 +2294,8 @@ def doc_preferences():
return "\n".join([" | ".join(x) for x in L])
def bulletin_option_affichage(formsemestre_id: int) -> dict:
def bulletin_option_affichage(formsemestre_id: int, prefs: SemPreferences) -> dict:
"dict avec les options d'affichages (préférences) pour ce semestre"
prefs = SemPreferences(formsemestre_id)
fields = (
"bul_show_abs",
"bul_show_abs_modules",

View File

@ -206,12 +206,18 @@ class CourrierIndividuelTemplate(PageTemplate):
background = find_logo(
logoname="pvjury_background",
dept_id=g.scodoc_dept_id,
) or find_logo(
logoname="pvjury_background",
dept_id=g.scodoc_dept_id,
prefix="",
)
else:
background = find_logo(
logoname="letter_background",
dept_id=g.scodoc_dept_id,
) or find_logo(
logoname="letter_background",
dept_id=g.scodoc_dept_id,
prefix="",
)
if not self.background_image_filename and background is not None:

View File

@ -53,6 +53,7 @@ SCO_ROLES_DEFAULTS = {
p.ScoUsersAdmin,
p.ScoUsersView,
p.ScoView,
p.APIView,
),
# RespPE est le responsable poursuites d'études
# il peut ajouter des tags sur les formations:

View File

@ -854,23 +854,27 @@ def formsemestre_import_etud_admission(
apo_emailperso = etud.get("mailperso", "")
if info["emailperso"] and not apo_emailperso:
apo_emailperso = info["emailperso"]
if (
import_email
and info["email"] != etud["mail"]
or info["emailperso"] != apo_emailperso
):
sco_etud.adresse_edit(
cnx,
args={
"etudid": etudid,
"adresse_id": info["adresse_id"],
"email": etud["mail"],
"emailperso": apo_emailperso,
},
)
# notifie seulement les changements d'adresse mail institutionnelle
if info["email"] != etud["mail"]:
changed_mails.append((info, etud["mail"]))
if import_email:
if not "mail" in etud:
raise ScoValueError(
"la réponse portail n'a pas le champs requis 'mail'"
)
if (
info["email"] != etud["mail"]
or info["emailperso"] != apo_emailperso
):
sco_etud.adresse_edit(
cnx,
args={
"etudid": etudid,
"adresse_id": info["adresse_id"],
"email": etud["mail"],
"emailperso": apo_emailperso,
},
)
# notifie seulement les changements d'adresse mail institutionnelle
if info["email"] != etud["mail"]:
changed_mails.append((info, etud["mail"]))
else:
unknowns.append(code_nip)
sco_cache.invalidate_formsemestre(formsemestre_id=sem["formsemestre_id"])

View File

@ -50,7 +50,7 @@ import pydot
import requests
from flask import g, request
from flask import url_for, make_response, jsonify
from flask import flash, url_for, make_response, jsonify
from config import Config
from app import log
@ -608,7 +608,7 @@ def is_valid_filename(filename):
return VALID_EXP.match(filename)
def bul_filename(sem, etud, format):
def bul_filename_old(sem: dict, etud: dict, format):
"""Build a filename for this bulletin"""
dt = time.strftime("%Y-%m-%d")
filename = f"bul-{sem['titre_num']}-{dt}-{etud['nom']}.{format}"
@ -616,6 +616,24 @@ def bul_filename(sem, etud, format):
return filename
def bul_filename(formsemestre, etud, format):
"""Build a filename for this bulletin"""
dt = time.strftime("%Y-%m-%d")
filename = f"bul-{formsemestre.titre_num()}-{dt}-{etud.nom}.{format}"
filename = make_filename(filename)
return filename
def flash_errors(form):
"""Flashes form errors (version sommaire)"""
for field, errors in form.errors.items():
flash(
"Erreur: voir le champs %s" % (getattr(form, field).label.text,),
"warning",
)
# see https://getbootstrap.com/docs/4.0/components/alerts/
def sendCSVFile(data, filename): # DEPRECATED utiliser send_file
"""publication fichier CSV."""
return send_file(data, filename=filename, mime=CSV_MIMETYPE, attached=True)
@ -635,21 +653,30 @@ class ScoDocJSONEncoder(json.JSONEncoder):
return json.JSONEncoder.default(self, o)
def sendJSON(data, attached=False):
def sendJSON(data, attached=False, filename=None):
js = json.dumps(data, indent=1, cls=ScoDocJSONEncoder)
return send_file(
js, filename="sco_data.json", mime=JSON_MIMETYPE, attached=attached
js, filename=filename or "sco_data.json", mime=JSON_MIMETYPE, attached=attached
)
def sendXML(data, tagname=None, force_outer_xml_tag=True, attached=False, quote=True):
def sendXML(
data,
tagname=None,
force_outer_xml_tag=True,
attached=False,
quote=True,
filename=None,
):
if type(data) != list:
data = [data] # always list-of-dicts
if force_outer_xml_tag:
data = [{tagname: data}]
tagname += "_list"
doc = sco_xml.simple_dictlist2xml(data, tagname=tagname, quote=quote)
return send_file(doc, filename="sco_data.xml", mime=XML_MIMETYPE, attached=attached)
return send_file(
doc, filename=filename or "sco_data.xml", mime=XML_MIMETYPE, attached=attached
)
def sendResult(
@ -659,6 +686,7 @@ def sendResult(
force_outer_xml_tag=True,
attached=False,
quote_xml=True,
filename=None,
):
if (format is None) or (format == "html"):
return data
@ -669,9 +697,10 @@ def sendResult(
force_outer_xml_tag=force_outer_xml_tag,
attached=attached,
quote=quote_xml,
filename=filename,
)
elif format == "json":
return sendJSON(data, attached=attached)
return sendJSON(data, attached=attached, filename=filename)
else:
raise ValueError("invalid format: %s" % format)
@ -789,7 +818,7 @@ def abbrev_prenom(prenom):
#
def timedate_human_repr():
"representation du temps courant pour utilisateur: a localiser"
"representation du temps courant pour utilisateur"
return time.strftime("%d/%m/%Y à %Hh%M")

View File

@ -14,16 +14,25 @@
}
main{
--couleurPrincipale: rgb(240,250,255);
--couleurFondTitresUE: rgb(206,255,235);
--couleurFondTitresRes: rgb(125, 170, 255);
--couleurFondTitresSAE: rgb(211, 255, 255);
--couleurFondTitresUE: #b6ebff;
--couleurFondTitresRes: #f8c844;
--couleurFondTitresSAE: #c6ffab;
--couleurSecondaire: #fec;
--couleurIntense: #c09;
--couleurSurlignage: rgba(232, 255, 132, 0.47);
--couleurIntense: rgb(4, 16, 159);;
--couleurSurlignage: rgba(255, 253, 110, 0.49);
max-width: 1000px;
margin: auto;
display: none;
}
.releve a, .releve a:visited {
color: navy;
text-decoration: none;
}
.releve a:hover {
color: red;
text-decoration: underline;
}
.ready .wait{display: none;}
.ready main{display: block;}
h2{
@ -97,7 +106,8 @@ section>div:nth-child(1){
.hide_coef .synthese em,
.hide_coef .eval>em,
.hide_date_inscr .dateInscription,
.hide_ects .ects{
.hide_ects .ects,
.hide_rangs .rang{
display: none;
}
@ -151,14 +161,19 @@ section>div:nth-child(1){
column-gap: 4px;
flex: none;
}
.infoSemestre>div:nth-child(1){
margin-right: auto;
}
.infoSemestre>div>div:nth-child(even){
text-align: right;
}
.photo {
border: none;
margin-left: auto;
}
.rang{
text-decoration: underline var(--couleurIntense);
font-weight: bold;
}
.ue .rang{
font-weight: 400;
}
.decision{
margin: 5px 0;
@ -186,6 +201,9 @@ section>div:nth-child(1){
.synthese h3{
background: var(--couleurFondTitresUE);
}
.synthese .ue>div{
text-align: right;
}
.synthese em,
.eval em{
opacity: 0.6;
@ -206,7 +224,6 @@ section>div:nth-child(1){
scroll-margin-top: 60px;
}
.module, .ue {
background: var(--couleurSecondaire);
color: #000;
padding: 4px 32px;
border-radius: 4px;
@ -218,6 +235,15 @@ section>div:nth-child(1){
cursor: pointer;
position: relative;
}
.ue {
background: var(--couleurFondTitresRes);
}
.module {
background: var(--couleurFondTitresRes);
}
.module h3 {
background: var(--couleurFondTitresRes);
}
.module::before, .ue::before {
content:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='26px' height='26px' fill='white'><path d='M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z' /></svg>");
width: 26px;
@ -308,6 +334,14 @@ h3{
margin-bottom: 8px;
}
@media screen and (max-width: 700px) {
section{
padding: 16px;
}
.syntheseModule, .eval {
margin: 0;
}
}
/*.absences{
display: grid;
grid-template-columns: auto auto;

View File

@ -138,7 +138,7 @@ div.head_message {
border-radius: 8px;
font-family : arial, verdana, sans-serif ;
font-weight: bold;
width: 40%;
width: 70%;
text-align: center;
}
@ -287,15 +287,15 @@ div.logo-insidebar {
width: 75px; /* la marge fait 130px */
}
div.logo-logo {
margin-left: -5px;
text-align: center ;
}
div.logo-logo img {
box-sizing: content-box;
margin-top: -10px;
width: 128px;
margin-top: 10px; /* -10px */
width: 135px; /* 128px */
padding-right: 5px;
margin-left: -75px;
}
div.sidebar-bottom {
margin-top: 10px;
@ -1297,7 +1297,7 @@ th.formsemestre_status_inscrits {
text-align: center;
}
td.formsemestre_status_code {
width: 2em;
/* width: 2em; */
padding-right: 1em;
}
@ -1671,7 +1671,10 @@ div.formation_list_modules ul.notes_module_list {
padding-top: 5px;
padding-bottom: 5px;
}
span.missing_ue_ects {
color: red;
font-weight: bold;
}
li.module_malus span.formation_module_tit {
color: red;
font-weight: bold;
@ -1699,14 +1702,20 @@ ul.notes_ue_list {
margin-top: 4px;
margin-right: 1em;
margin-left: 1em;
padding-top: 1em;
/* padding-top: 1em; */
padding-bottom: 1em;
font-weight: bold;
}
.formation_classic_infos ul.notes_ue_list {
padding-top: 0px;
}
li.notes_ue_list {
.formation_classic_infos li.notes_ue_list {
margin-top: 9px;
list-style-type: none;
border: 1px solid maroon;
border-radius: 10px;
padding-bottom: 5px;
}
span.ue_type_1 {
color: green;
@ -1749,6 +1758,7 @@ ul.notes_matiere_list {
background-color: rgb(220,220,220);
font-weight: normal;
font-style: italic;
border-top: 1px solid maroon;
}
ul.notes_module_list {
@ -1757,6 +1767,27 @@ ul.notes_module_list {
font-style: normal;
}
div.ue_list_div {
border: 3px solid rgb(35, 0, 160);
padding-left: 5px;
padding-top: 5px;
margin-bottom: 5px;
margin-right: 5px;
}
div.ue_list_tit_sem {
font-size: 120%;
font-weight: bold;
color: orangered;
display: list-item; /* This has to be "list-item" */
list-style-type: disc; /* See https://developer.mozilla.org/en-US/docs/Web/CSS/list-style-type */
list-style-position: inside;
}
input.sco_tag_checkbox {
margin-bottom: 10px;
}
.notes_ue_list a.stdlink {
color: #001084;
text-decoration: underline;
@ -1932,7 +1963,20 @@ table.notes_recapcomplet a:hover {
div.notes_bulletin {
margin-right: 5px;
}
div.bull_head {
display: grid;
justify-content: space-between;
grid-template-columns: auto auto;
}
div.bull_photo {
display: inline-block;
margin-right: 10px;
}
span.bulletin_menubar_but {
display: inline-block;
margin-left: 2em;
margin-right: 2em;
}
table.notes_bulletin {
border-collapse: collapse;
border: 2px solid rgb(100,100,240);
@ -2072,12 +2116,6 @@ a.bull_link:hover {
text-decoration: underline;
}
table.bull_head {
width: 100%;
}
td.bull_photo {
text-align: right;
}
div.bulletin_menubar {
padding-left: 25px;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -0,0 +1,14 @@
// Formulaire formsemestre_createwithmodules
function change_semestre_id() {
var semestre_id = $("#tf_semestre_id")[0].value;
for (var i = -1; i < 12; i++) {
$(".sem" + i).hide();
}
$(".sem" + semestre_id).show();
}
$(window).on('load', function () {
change_semestre_id();
});

View File

@ -41,7 +41,7 @@ class releveBUT extends HTMLElement {
}
set showData(data) {
this.showInformations(data);
// this.showInformations(data);
this.showSemestre(data);
this.showSynthese(data);
this.showEvaluations(data);
@ -68,13 +68,7 @@ class releveBUT extends HTMLElement {
<div>
<div class="wait"></div>
<main class="releve">
<!--------------------------->
<!-- Info. étudiant -->
<!--------------------------->
<section class=etudiant>
<img class=studentPic src="" alt="Photo de l'étudiant" width=100 height=120>
<div class=infoEtudiant></div>
</section>
<!--------------------------------------------------------------------------------------->
<!-- Zone spéciale pour que les IUT puisse ajouter des infos locales sur la passerelle -->
@ -85,13 +79,13 @@ class releveBUT extends HTMLElement {
<!-- Semestre -->
<!--------------------------->
<section>
<h2>Semestre </h2>
<div class=flex>
<h2 id="identite_etudiant"></h2>
<div>
<div class=infoSemestre></div>
<div>
<div class=decision></div>
<div class=dateInscription>Inscrit le </div>
<em>Les moyennes servent à situer l'étudiant dans la promotion et ne correspondent pas à des validations de compétences ou d'UE.</em>
<em>Les moyennes ci-dessus servent à situer l'étudiant dans la promotion et ne correspondent pas à des validations de compétences ou d'UE.</em>
</div>
</div>
@ -103,7 +97,7 @@ class releveBUT extends HTMLElement {
<section>
<div>
<div>
<h2>Synthèse</h2>
<h2>Unités d'enseignement</h2>
<em>La moyenne des ressources dans une UE dépend des poids donnés aux évaluations.</em>
</div>
<div class=CTA_Liste>
@ -132,7 +126,7 @@ class releveBUT extends HTMLElement {
<section>
<div>
<h2>S</h2>
<h2>Situations d'apprentissage et d'évaluation (S)</h2>
<div class=CTA_Liste>
Liste <svg xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 15l-6-6-6 6" />
@ -198,7 +192,8 @@ class releveBUT extends HTMLElement {
/* Information sur le semestre */
/*******************************/
showSemestre(data) {
this.shadow.querySelector("h2").innerHTML += data.semestre.numero;
this.shadow.querySelector("#identite_etudiant").innerHTML = ` ${data.etudiant.nomprenom} `;
this.shadow.querySelector(".dateInscription").innerHTML += this.ISOToDate(data.semestre.inscription);
let output = `
<div>
@ -212,7 +207,9 @@ class releveBUT extends HTMLElement {
<div class=enteteSemestre>Absences</div>
<div class=enteteSemestre>N.J. ${data.semestre.absences?.injustifie ?? "-"}</div>
<div style="grid-column: 2">Total ${data.semestre.absences?.total ?? "-"}</div>
</div>`;
</div>
<a class=photo href="${data.etudiant.fiche_url}"><img src="${data.etudiant.photo_url || "default_Student.svg"}" alt="photo de l'étudiant" title="fiche de l'étudiant" height="120" border="0"></a>
`;
/*${data.semestre.groupes.map(groupe => {
return `
<div>
@ -254,6 +251,7 @@ class releveBUT extends HTMLElement {
</h3>
<div>
<div class=moyenne>Moyenne&nbsp;:&nbsp;${dataUE.moyenne?.value || "-"}</div>
<div class=rang>Rang&nbsp;:&nbsp;${dataUE.moyenne?.rang}&nbsp;/&nbsp;${dataUE.moyenne?.total}</div>
<div class=info>
Bonus&nbsp;:&nbsp;${dataUE.bonus || 0}&nbsp;-
Malus&nbsp;:&nbsp;${dataUE.malus || 0}

View File

@ -57,12 +57,10 @@
{% block content %}
<div class="container">
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<div class="alert alert-info" role="alert">{{ message }}</div>
{% endfor %}
{% endif %}
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
<div class="alert alert-info alert-{{ category }}" role="alert">{{ message }}</div>
{% endfor %}
{% endwith %}
{# application content needs to be provided in the app_content block #}

View File

@ -0,0 +1,34 @@
{# -*- mode: jinja-html -*- #}
{# Pied des bulletins HTML #}
<p>Situation actuelle:
{% if inscription_courante %}
<a class="stdlink" href="{{url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=inscription_courante.formsemestre_id)
}}">{{inscription_str}}</a>
{% else %}
{{inscription_str}}
{% endif %}
</p>
{% if formsemestre.modalite == "EXT" %}
<p><a href="{{
url_for('notes.formsemestre_ext_edit_ue_validations',
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
etudid=etud.id)}}"
class="stdlink">
Éditer les validations d'UE dans ce semestre extérieur
</a></p>
{% endif %}
{# Place du diagramme radar #}
<form id="params">
<input type="hidden" name="etudid" id="etudid" value="{{etud.id}}"/>
<input type="hidden" name="formsemestre_id" id="formsemestre_id" value="{{formsemestre.id}}"/>
</form>
<div id="radar_bulletin"></div>

View File

@ -0,0 +1,57 @@
{# -*- mode: jinja-html -*- #}
{# L'en-tête des bulletins HTML #}
{# was _formsemestre_bulletinetud_header_html #}
<div class="bull_head">
<div class="bull_head_text">
{% if not is_apc %}
<h2><a class="discretelink" href="{{
url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid,
)}}">{{etud.nomprenom}}</a></h2>
{% endif %}
<form name="f" method="GET" action="{{request.base_url}}">
<input type="hidden" name="formsemestre_id" value="{{formsemestre.id}}"></input>
<input type="hidden" name="etudid" value="{{etud.id}}"></input>
<input type="hidden" name="format" value="{{format}}"></input>
Bulletin
<span class="bull_liensemestre"><a href="{{
url_for("notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id)}}">{{formsemestre.titre_mois()
}}</a></span>
<div>
<em>établi le {{time.strftime("%d/%m/%Y à %Hh%M")}} (notes sur 20)</em>
<span class="rightjust">
<select name="version" onchange="document.f.submit()" class="noprint">
{% for (v, e) in (
("short", "Version courte"),
("selectedevals", "Version intermédiaire"),
("long", "Version complète"),
) %}
<option value="{{v}}" {% if (v == version) %}selected{% endif %}>{{e}}</option>
{% endfor %}
</select>
</span>
<span class="bulletin_menubar">
<span class="bulletin_menubar_but">{{menu_autres_operations|safe}}</span>
<a href="{{url_for(
'notes.formsemestre_bulletinetud',
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
etudid=etud.id,
format='pdf',
version=version,
)}}">{{scu.ICON_PDF|safe}}</a>
</span>
</div>
</form>
</div>
{% if not is_apc %}
<div class="bull_photo"><a href="{{
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
}}">{{etud.photo_html(title="fiche de " + etud["nom"])|safe}}</a>
</div>
{% endif %}
</div>

View File

@ -7,8 +7,13 @@
{% block app_content %}
{% include 'bul_head.html' %}
<releve-but></releve-but>
<script src="/ScoDoc/static/js/releve-but.js"></script>
{% include 'bul_foot.html' %}
<script>
let dataSrc = "{{bul_url|safe}}";
fetch(dataSrc)

View File

@ -18,10 +18,12 @@
<a href="{{ url_for('notes.refcomp_table', scodoc_dept=g.scodoc_dept, ) }}">
Liste des référentiels de compétences chargés</a>
</li>
{% if formation is not none %}
<li>
<a href="{{ url_for('notes.refcomp_assoc_formation', scodoc_dept=g.scodoc_dept, formation_id=formation.id) }}">
Association à la formation {{ formation.acronyme }}</a>
</li>
{% endif %}
</div>
</div>

View File

@ -0,0 +1,9 @@
{# Message flask : utilisé uniquement par les anciennes pages ScoDoc #}
{# -*- mode: jinja-html -*- #}
<div class="head_message_container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
<div class="head_message alert-info alert-{{ category }}" role="alert">{{ message }}</div>
{% endfor %}
{% endwith %}
</div>

View File

@ -0,0 +1,50 @@
{# -*- mode: jinja-html -*- #}
{# Element HTML decrivant un semestre (barre de menu et infos) #}
{# was formsemestre_page_title #}
<div class="formsemestre_page_title">
<div class="infos">
<span class="semtitle"><a class="stdlink"
title="{{formsemestre.session_id}}"
href="{{url_for('notes.formsemestre_status',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)}}"
>TATO {{formsemestre.titre}}</a>
{%- if formsemestre.semestre_id != -1 -%}
<a
title="{{formsemestre.etapes_apo_str()
}}">, {{
formsemestre.formation.get_parcours().SESSION_NAME}}
{{formsemestre.semestre_id}}</a>
{%- endif -%}
{%- if formsemestre.modalite %} en {{formsemestre.modalite}}
{%- endif %}</span><span
class="dates"><a
title="du {{formsemestre.date_debut.strftime('%d/%m/%Y')}}
au {{formsemestre.date_fin.strftime('%d/%m/%Y')}} "
>{{formsemestre.mois_debut()}} - {{formsemestre.mois_fin()}}</a></span><span
class="resp"><a title="{{formsemestre.responsables_str(abbrev_prenom=False)}}">{{formsemestre.responsables_str()}}</a></span><span
class="nbinscrits"><a class="discretelink"
href="{{url_for('scolar.groups_view',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
}}"
>{{formsemestre.etuds_inscriptions|length}} inscrits</a></span><span
class="lock">
{%-if formsemestre.etat -%}
<a href="{{ url_for( 'notes.formsemestre_change_lock',
scodoc_dept=scodoc_dept, formsemestre_id=formsemestre.id )}}">{{
scu.icontag("lock_img", border="0", title="Semestre verrouillé")|safe
}}</a>
{%- endif -%}
</span><span class="eye"><a href="{{
url_for('notes.formsemestre_change_publication_bul',
scodoc_dept=scodoc_dept, formsemestre_id=formsemestre.id )
}}">{%-
if formsemestre.bul_hide_xml -%}}
{{scu.icontag("hide_img", border="0", title="Bulletins NON publiés")|safe}}
{%- else -%}
{{scu.icontag("eye_img", border="0", title="Bulletins publiés")|safe}}
{%- endif -%}
</a></span>
</div>
{{sem_menu_bar|safe}}
</div>

View File

@ -65,6 +65,13 @@
{% endfor %}
</span>
{% if mod.ue.type != 0 and mod.module_type != 0 %}
<span class="warning" title="Une UE de type spécial ne
devrait contenir que des modules standards">
type incompatible avec son UE de rattachement !
</span>
{% endif %}
<span class="sco_tag_edit"><form><textarea data-module_id="{{mod.id}}"
class="{% if tag_editable %}module_tag_editor{% else %}module_tag_editor_ro{% endif %}">{{mod.tags|join(', ', attribute='title')}}</textarea></form></span>

View File

@ -3,11 +3,9 @@
<div class="formation_list_ues">
<div class="formation_list_ues_titre">Unités d'Enseignement (UEs)</div>
{% for semestre_idx in semestre_ids %}
<div class="formation_list_ues_sem">Semestre S{{semestre_idx}}</div>
<div class="formation_list_ues_sem">Semestre S{{semestre_idx}} (ECTS: {{ects_by_sem[semestre_idx] | safe}})</div>
<ul class="apc_ue_list">
{% for ue in formation.ues.filter_by(semestre_idx=semestre_idx).order_by(
UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme
) %}
{% for ue in ues_by_sem[semestre_idx] %}
<li class="notes_ue_list">
{% if editable and not loop.first %}
<a href="{{ url_for('notes.ue_move',
@ -38,7 +36,8 @@
{% set virg = joiner(", ") %}
<span class="ue_code">(
{%- if ue.ue_code -%}{{ virg() }}code {{ue.ue_code}} {%- endif -%}
{{ virg() }}{{ue.ects or 0}} ECTS)
{{ virg() }}{{ue.ects if ue.ects is not none
else '<span class="missing_ue_ects">aucun</span>'|safe}} ECTS)
</span>
</span>
@ -48,6 +47,9 @@
}}">modifier</a>
{% endif %}
{% if ue.type == 1 and ue.modules.count() == 0 %}
<span class="warning" title="pas de module, donc pas de bonus calculé">aucun module rattaché !</span>
{% endif %}
</li>
{% endfor %}
</ul>

View File

@ -23,12 +23,10 @@
<div id="gtrcontent" class="gtrcontent">
<div class="container">
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<div class="alert alert-info" role="alert">{{ message }}</div>
{% endfor %}
{% endif %}
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
<div class="alert alert-info alert-{{ category }}" role="alert">{{ message }}</div>
{% endfor %}
{% endwith %}
</div>
{% if sco.sem %}

View File

@ -4,7 +4,7 @@
<div class="sidebar">
{# sidebar_common #}
<a class="scodoc_title" href="{{
url_for('scodoc.index', scodoc_dept=g.scodoc_dept) }}">ScoDoc 9.2a</a>
url_for('scodoc.index', scodoc_dept=g.scodoc_dept) }}">ScoDoc {{ sco.SCOVERSION }}</a>
<div id="authuser"><a id="authuserlink" href="{{
url_for('users.user_info_page', scodoc_dept=g.scodoc_dept, user_name=current_user.user_name)
}}">{{current_user.user_name}}</a>

View File

@ -16,6 +16,7 @@ from app.scodoc import sco_formsemestre_status
from app.scodoc import sco_preferences
from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_utils as scu
import sco_version
scodoc_bp = Blueprint("scodoc", __name__)
scolar_bp = Blueprint("scolar", __name__)
@ -49,26 +50,29 @@ def close_dept_db_connection(arg):
class ScoData:
"""Classe utilisée pour passer des valeurs aux vues (templates)"""
def __init__(self):
def __init__(self, etud=None, formsemestre=None):
# Champs utilisés par toutes les pages ScoDoc (sidebar, en-tête)
self.Permission = Permission
self.scu = scu
self.SCOVERSION = sco_version.SCOVERSION
# -- Informations étudiant courant, si sélectionné:
etudid = g.get("etudid", None)
if not etudid:
if request.method == "GET":
etudid = request.args.get("etudid", None)
elif request.method == "POST":
etudid = request.form.get("etudid", None)
if etudid:
if etud is None:
etudid = g.get("etudid", None)
if etudid is None:
if request.method == "GET":
etudid = request.args.get("etudid", None)
elif request.method == "POST":
etudid = request.form.get("etudid", None)
if etudid is not None:
etud = Identite.query.get_or_404(etudid)
self.etud = etud
if etud is not None:
# Infos sur l'étudiant courant
self.etud = Identite.query.get_or_404(etudid)
ins = self.etud.inscription_courante()
if ins:
self.etud_cur_sem = ins.formsemestre
self.nbabs, self.nbabsjust = sco_abs.get_abs_count_in_interval(
etudid,
etud.id,
self.etud_cur_sem.date_debut.isoformat(),
self.etud_cur_sem.date_fin.isoformat(),
)
@ -78,17 +82,22 @@ class ScoData:
else:
self.etud = None
# --- Informations sur semestre courant, si sélectionné
formsemestre_id = sco_formsemestre_status.retreive_formsemestre_from_request()
if formsemestre_id is None:
if formsemestre is None:
formsemestre_id = (
sco_formsemestre_status.retreive_formsemestre_from_request()
)
if formsemestre_id is not None:
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if formsemestre is None:
self.sem = None
self.sem_menu_bar = None
else:
self.sem = FormSemestre.query.get_or_404(formsemestre_id)
self.sem = formsemestre
self.sem_menu_bar = sco_formsemestre_status.formsemestre_status_menubar(
self.sem.to_dict()
)
# --- Préférences
self.prefs = sco_preferences.SemPreferences(formsemestre_id)
self.prefs = sco_preferences.SemPreferences(formsemestre.id)
from app.views import scodoc, notes, scolar, absences, users, pn_modules, refcomp

View File

@ -611,8 +611,7 @@ def SignaleAbsenceGrSemestre(
"""<option value="%(modimpl_id)s" %(sel)s>%(modname)s</option>\n"""
% {
"modimpl_id": modimpl["moduleimpl_id"],
"modname": modimpl["module"]["code"]
or ""
"modname": (modimpl["module"]["code"] or "")
+ " "
+ (modimpl["module"]["abbrev"] or modimpl["module"]["titre"]),
"sel": sel,
@ -624,7 +623,7 @@ def SignaleAbsenceGrSemestre(
sel = "selected" # aucun module specifie
H.append(
"""<p>
Module concerné par ces absences (%(optionel_txt)s):
Module concerné par ces absences (%(optionel_txt)s):
<select id="moduleimpl_id" name="moduleimpl_id"
onchange="document.location='%(url)s&moduleimpl_id='+document.getElementById('moduleimpl_id').value">
<option value="" %(sel)s>non spécifié</option>

View File

@ -32,10 +32,11 @@ Emmanuel Viennet, 2021
"""
from operator import itemgetter
import time
from xml.etree import ElementTree
import flask
from flask import flash, jsonify, render_template, url_for
from flask import abort, flash, jsonify, render_template, url_for
from flask import current_app, g, request
from flask_login import current_user
from werkzeug.utils import redirect
@ -68,10 +69,14 @@ from app.scodoc import sco_utils as scu
from app.scodoc import notesdb as ndb
from app import log, send_scodoc_alarm
from app.scodoc import scolog
from app.scodoc.scolog import logdb
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError, ScoInvalidIdType
from app.scodoc.sco_exceptions import (
AccessDenied,
ScoException,
ScoValueError,
ScoInvalidIdType,
)
from app.scodoc import html_sco_header
from app.pe import pe_view
from app.scodoc import sco_abs
@ -272,7 +277,7 @@ sco_publish(
def formsemestre_bulletinetud(
etudid=None,
formsemestre_id=None,
format="html",
format=None,
version="long",
xml_with_decisions=False,
force_publishing=False,
@ -280,6 +285,7 @@ def formsemestre_bulletinetud(
code_nip=None,
code_ine=None,
):
format = format or "html"
if not formsemestre_id:
flask.abort(404, "argument manquant: formsemestre_id")
if not isinstance(formsemestre_id, int):
@ -307,12 +313,16 @@ def formsemestre_bulletinetud(
if format == "json":
r = bulletin_but.BulletinBUT(formsemestre)
return jsonify(
r.bulletin_etud(etud, formsemestre, force_publishing=force_publishing)
r.bulletin_etud(
etud,
formsemestre,
force_publishing=force_publishing,
version=version,
)
)
elif format == "html":
return render_template(
"but/bulletin.html",
title=f"Bul. {etud.nom} - BUT",
bul_url=url_for(
"notes.formsemestre_bulletinetud",
scodoc_dept=g.scodoc_dept,
@ -320,8 +330,21 @@ def formsemestre_bulletinetud(
etudid=etudid,
format="json",
force_publishing=1, # pour ScoDoc lui même
version=version,
),
sco=ScoData(),
etud=etud,
formsemestre=formsemestre,
inscription_courante=etud.inscription_courante(),
inscription_str=etud.inscription_descr()["inscription_str"],
is_apc=formsemestre.formation.is_apc(),
menu_autres_operations=sco_bulletins.make_menu_autres_operations(
formsemestre, etud, "notes.formsemestre_bulletinetud", version
),
sco=ScoData(etud=etud),
scu=scu,
time=time,
title=f"Bul. {etud.nom} - BUT",
version=version,
)
if not (etudid or code_nip or code_ine):
@ -1862,7 +1885,6 @@ def formsemestre_bulletins_choice(
formsemestre_id, title="", explanation="", choose_mail=False
):
"""Choix d'une version de bulletin"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
H = [
html_sco_header.html_sem_header(title),
"""
@ -1925,7 +1947,7 @@ def formsemestre_bulletins_mailetuds(
nb_send = 0
for etudid in etudids:
h, _ = sco_bulletins.do_formsemestre_bulletinetud(
formsemestre_id,
formsemestre,
etudid,
version=version,
prefer_mail_perso=prefer_mail_perso,
@ -2672,12 +2694,15 @@ def check_integrity_all():
def moduleimpl_list(
moduleimpl_id=None, formsemestre_id=None, module_id=None, format="json"
):
data = sco_moduleimpl.moduleimpl_list(
moduleimpl_id=moduleimpl_id,
formsemestre_id=formsemestre_id,
module_id=module_id,
)
return scu.sendResult(data, format=format)
try:
data = sco_moduleimpl.moduleimpl_list(
moduleimpl_id=moduleimpl_id,
formsemestre_id=formsemestre_id,
module_id=module_id,
)
return scu.sendResult(data, format=format)
except ScoException:
abort(404)
@bp.route("/do_moduleimpl_withmodule_list") # ancien nom
@ -2686,7 +2711,7 @@ def moduleimpl_list(
@permission_required(Permission.ScoView)
@scodoc7func
def moduleimpl_withmodule_list(
moduleimpl_id=None, formsemestre_id=None, module_id=None
moduleimpl_id=None, formsemestre_id=None, module_id=None, format="json"
):
"""API ScoDoc 7"""
data = sco_moduleimpl.moduleimpl_withmodule_list(

View File

@ -304,8 +304,9 @@ def _return_logo(name="header", dept_id="", small=False, strict: bool = True):
# stockée dans /opt/scodoc-data/config/logos donc servie manuellement ici
# from app.scodoc.sco_photos import _http_jpeg_file
logo = sco_logos.find_logo(name, dept_id, strict).select()
logo = sco_logos.find_logo(name, dept_id, strict)
if logo is not None:
logo.select()
suffix = logo.suffix
if small:
with PILImage.open(logo.filepath) as im:

View File

@ -68,8 +68,6 @@ from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
import app
from app.scodoc.gen_tables import GenTable
from app.scodoc import html_sco_header
from app.scodoc import html_sidebar
from app.scodoc import imageresize
from app.scodoc import sco_import_etuds
from app.scodoc import sco_abs
from app.scodoc import sco_archives_etud
@ -87,12 +85,9 @@ from app.scodoc import sco_formsemestre_edit
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups
from app.scodoc import sco_groups_edit
from app.scodoc import sco_groups_exports
from app.scodoc import sco_groups_view
from app.scodoc import sco_logos
from app.scodoc import sco_news
from app.scodoc import sco_page_etud
from app.scodoc import sco_parcours_dut
from app.scodoc import sco_permissions
from app.scodoc import sco_permissions_check
from app.scodoc import sco_photos
from app.scodoc import sco_portal_apogee
@ -364,6 +359,12 @@ sco_publish(
methods=["GET", "POST"],
)
sco_publish(
"/groups_export_annotations",
sco_groups_exports.groups_export_annotations,
Permission.ScoView,
)
@bp.route("/groups_view")
@scodoc
@ -512,7 +513,7 @@ def etud_info(etudid=None, format="xml"):
sem = etud["cursem"]
if sem:
sco_groups.etud_add_group_infos(etud, sem)
sco_groups.etud_add_group_infos(etud, sem["formsemestre_id"] if sem else None)
d["insemestre"] = [
{
"current": "1",

View File

@ -19,31 +19,6 @@ depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("apc_competence", sa.Column("id_orebut", sa.Text(), nullable=True))
op.drop_constraint(
"apc_competence_referentiel_id_titre_key", "apc_competence", type_="unique"
)
op.create_index(
op.f("ix_apc_competence_id_orebut"),
"apc_competence",
["id_orebut"],
)
op.add_column(
"apc_referentiel_competences", sa.Column("annexe", sa.Text(), nullable=True)
)
op.add_column(
"apc_referentiel_competences",
sa.Column("type_structure", sa.Text(), nullable=True),
)
op.add_column(
"apc_referentiel_competences",
sa.Column("type_departement", sa.Text(), nullable=True),
)
op.add_column(
"apc_referentiel_competences",
sa.Column("version_orebut", sa.Text(), nullable=True),
)
op.create_index(
op.f("ix_notes_formsemestre_uecoef_formsemestre_id"),
"notes_formsemestre_uecoef",
@ -80,15 +55,10 @@ def downgrade():
table_name="notes_formsemestre_uecoef",
)
op.drop_column("apc_referentiel_competences", "version_orebut")
op.drop_column("apc_referentiel_competences", "type_departement")
op.drop_column("apc_referentiel_competences", "type_structure")
op.drop_column("apc_referentiel_competences", "annexe")
op.drop_index(op.f("ix_apc_competence_id_orebut"), table_name="apc_competence")
op.create_unique_constraint(
"apc_competence_referentiel_id_titre_key",
"apc_competence",
["referentiel_id", "titre"],
)
op.drop_column("apc_competence", "id_orebut")
# ### end Alembic commands ###

Some files were not shown because too many files have changed in this diff Show More