Compare commits

...

95 Commits

Author SHA1 Message Date
Arthur ZHU d637ffe70c renomme tables application relations entreprises 2022-01-24 11:16:46 +01:00
Emmanuel Viennet 2b1a3ee95e Améliore affichage ref. comp. 2022-01-17 00:41:57 +01:00
Emmanuel Viennet 54b1ce7bfb Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into refactor_nt 2022-01-17 00:30:39 +01:00
Emmanuel Viennet 831d14cf7d 9.0.25 2022-01-17 00:18:08 +01:00
Emmanuel Viennet b8b3185901 Traite #276 2022-01-17 00:06:21 +01:00
Emmanuel Viennet 02989e6c88 WIP: reorganisation des calculs 2022-01-16 23:47:52 +01:00
Emmanuel Viennet a7324ac634 début de modernisation des tests 2022-01-15 21:45:46 +01:00
Emmanuel Viennet 3639d5db94 titre lien trombino 2022-01-15 15:15:38 +01:00
Emmanuel Viennet 7da5e0a5c3 update to 44d674e de Scodoc_Notes 2022-01-15 15:14:59 +01:00
Emmanuel Viennet 4334a83c17 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into refactor_nt 2022-01-15 15:00:44 +01:00
Emmanuel Viennet da5445baa8 Améliore message erreur import étudiants via excel 2022-01-15 14:59:59 +01:00
Emmanuel Viennet 69d494c02c Fix: evaluation_listenotes si aucun inscrit 2022-01-14 11:18:31 +01:00
Emmanuel Viennet 0b87714ac0 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into refactor_nt 2022-01-13 23:40:30 +01:00
Emmanuel Viennet fa99cbf3d0 Fix #269. Utilisateurs avec logins numériques. 2022-01-13 23:30:25 +01:00
Emmanuel Viennet b6cedbd6b6 introducing ScoPDFFormatError for friendlier PDF errors 2022-01-13 22:36:40 +01:00
Emmanuel Viennet 55bd15a67b Fix: script migration si tables entreprises ScoDoc 7 incohérentes 2022-01-13 21:36:56 +01:00
Emmanuel Viennet ec108a4454 geb_tables: meilleur msg erreur si template pdf invalide 2022-01-13 21:13:09 +01:00
Emmanuel Viennet 158ac7b1fc Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into refactor_nt 2022-01-11 23:40:30 +01:00
Emmanuel Viennet a5c0619102 Bulletin BUT: reprise du f5d0074 de la passerelle Scodoc_Notes 2022-01-11 23:16:59 +01:00
Emmanuel Viennet 6f0b03242d modif exc. handling 2022-01-11 22:44:03 +01:00
Emmanuel Viennet af12191cc4 change exc sur can_change_groups 2022-01-10 23:43:12 +01:00
Emmanuel Viennet 126f719f7a Index INE et NIP 2022-01-10 15:15:26 +01:00
Emmanuel Viennet b2893a3371 Améliore validation des dates et des ids 2022-01-10 12:00:02 +01:00
Emmanuel Viennet 00b6d19c0c hotfix: enlève décision jury bul. BUT 2022-01-10 00:02:34 +01:00
Emmanuel Viennet 4477a25147 Affichage référentiel de compétences (contrib Séb. L.) 2022-01-09 23:17:48 +01:00
Emmanuel Viennet 53630f08de WIP bonus sport 2022-01-09 22:34:49 +01:00
Emmanuel Viennet 782e291725 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into refactor_nt
WIP
2022-01-09 22:33:08 +01:00
Emmanuel Viennet 58a6d16d12 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc 2022-01-09 21:49:21 +01:00
Emmanuel Viennet 0a1264051c Bulletin BUT: n'affiche que les modules auxquels l'étudiant est inscrit 2022-01-09 21:48:58 +01:00
Emmanuel Viennet 4e1811e609 WIP: misc bonus sport 2022-01-09 21:02:07 +01:00
Emmanuel Viennet f7dbff782f Merge pull request 'Relevé : décision jury + flèches' (#268) from lehmann/ScoDoc-Front:master into master
Reviewed-on: ScoDoc/ScoDoc#268
2022-01-09 12:03:47 +01:00
Sébastien Lehmann acdd037483 Relevé : décision jury + flèches 2022-01-09 11:36:15 +01:00
Emmanuel Viennet 4daa9e8945 logo comet 2022-01-09 11:27:24 +01:00
Emmanuel Viennet 68dec8e1f8 Fix: anciens bulletins XML du BUT des démissionnaires 2022-01-09 10:11:50 +01:00
Emmanuel Viennet 9172282451 backport fix formations 2022-01-08 20:07:13 +01:00
Emmanuel Viennet 9e3ad8efbc Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into refactor_nt 2022-01-08 19:58:57 +01:00
Emmanuel Viennet a467ef27db WIP: recap but avec UEs 2022-01-08 19:56:09 +01:00
Emmanuel Viennet 24bfb8a13d 9.1.20 avec des cerises 2022-01-08 19:53:17 +01:00
Emmanuel Viennet 0e930d5fe4 filtre modules non BUT dans edit_modules_ue_coefs 2022-01-08 19:30:30 +01:00
Emmanuel Viennet 795ca343de Calcul sans notes table: table recap ok. 2022-01-08 18:06:00 +01:00
Emmanuel Viennet 4c325b70de sanitize formations after upgrades + renumber modules 2022-01-08 17:38:38 +01:00
Emmanuel Viennet 72b8e04064 ajout id orebut dans les niveaux de comp. 2022-01-08 17:22:51 +01:00
Emmanuel Viennet c362ccef0e Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into refactor_nt 2022-01-08 15:28:06 +01:00
Emmanuel Viennet fbae5d268f Fix: exports bul. xml quand non publiés 2022-01-08 15:27:40 +01:00
Emmanuel Viennet 2161d7bddc etudiant, ref. comp. 2022-01-08 14:35:02 +01:00
Emmanuel Viennet f2e9fbb8cd Fix: edition de formations migrées (non affichages de certains modules) 2022-01-08 14:01:16 +01:00
Emmanuel Viennet 4bb6f93b32 Merge pull request 'première phase implémentation api scodoc9' (#264) from leonard.montalbano/ScoDoc:master into refactor_nt
Reviewed-on: ScoDoc/ScoDoc#264
2022-01-07 19:02:18 +01:00
Emmanuel Viennet fbb4b0841b Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into refactor_nt 2022-01-07 18:55:14 +01:00
Emmanuel Viennet 0c57aa83ca version 9.1.19 2022-01-07 18:44:42 +01:00
Emmanuel Viennet 8b2edca257 Fix: passage d'un semestre à l'autre 2022-01-07 18:38:32 +01:00
Emmanuel Viennet 0ceb1c8046 Fix: flag publication bulletins XML BUT compat 2022-01-07 18:09:45 +01:00
Emmanuel Viennet b1ab7a4df9 Fix: notes des évaluations BUT en XML compt sur 20 2022-01-07 18:05:27 +01:00
Emmanuel Viennet d68f81b4ea Fix exports Apogée (pb liés passage à Python 3) 2022-01-07 17:58:34 +01:00
Emmanuel Viennet 4140f11b7b Fonction bonus_sport lr (La Rochelle) 2022-01-07 15:21:30 +01:00
Emmanuel Viennet d1ff47727b Fix: bug si user_name ne contient que des chiffres 2022-01-07 15:19:34 +01:00
Emmanuel Viennet 3eb038b491 formsemestre header sans notes_table 2022-01-07 15:11:24 +01:00
Emmanuel Viennet 5e144d8745 chargement resultats semestres 2022-01-07 15:10:52 +01:00
Emmanuel Viennet da1a2ccf43 suite réorganisation calculs 2022-01-07 15:08:45 +01:00
Emmanuel Viennet af48eb8fb8 recapcomplet sans NotesTable 2022-01-07 10:37:48 +01:00
leonard_montalbano 858b922b5a première phase implémentation api scodoc9 2022-01-07 08:49:16 +01:00
Emmanuel Viennet 6b1ccfe400 about: demande login 2022-01-06 23:37:57 +01:00
Emmanuel Viennet 54b714fdbc Améliore affichage coefs sur tableau bord module 2022-01-06 22:42:26 +01:00
Emmanuel Viennet 1b98e5f8dd Numeros ue et modules: invalide cache 2022-01-06 21:23:29 +01:00
Emmanuel Viennet 0098df4a8f Référentiels de compétences BUT Orébut 2022-01-05 23:58:32 +01:00
Emmanuel Viennet 9b147b85ae répare html template (erreur html mode sur jinja) 2022-01-05 23:57:33 +01:00
Emmanuel Viennet b1a3a15a94 nouveau format refcomp Orebut + supprime migration entreprises 2022-01-05 23:42:11 +01:00
Emmanuel Viennet 20c8f22c7b cosmétique accueil 2022-01-05 21:44:22 +01:00
Emmanuel Viennet d6c6a08828 Fix: 2 bugs sur formulaire création/modif utilisateurs 2022-01-05 21:11:57 +01:00
Emmanuel Viennet 96130f1a75 Merge branch 'refactor_nt' of https://scodoc.org/git/viennet/ScoDoc into orebut 2022-01-05 16:53:47 +01:00
Emmanuel Viennet 235556f825 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into refactor_nt 2022-01-05 16:52:53 +01:00
Emmanuel Viennet d622b313b0 Merge branch 'jmplace-gestion_scodoc-data.old' 2022-01-05 16:46:09 +01:00
Emmanuel Viennet db9acb67dd détails bash 2022-01-05 16:45:55 +01:00
Emmanuel Viennet 8a415984c6 Merge branch 'gestion_scodoc-data.old' of https://scodoc.org/git/jmplace/ScoDoc-Lille into jmplace-gestion_scodoc-data.old 2022-01-05 16:14:59 +01:00
Emmanuel Viennet 6157e54a5f affichage nom complet du dept. sur la page accueil 2022-01-05 16:06:56 +01:00
Jean-Marie PLACE efe997fe55 check scodoc-data.old ; correction bug --keep-env 2022-01-05 06:10:25 +01:00
Emmanuel Viennet ff948cb98d new users default dept in form 2022-01-05 01:04:07 +01:00
Emmanuel Viennet 4b63fe81e4 fix: het sort 2022-01-05 01:03:25 +01:00
Emmanuel Viennet 88e15367b0 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into refactor_nt 2022-01-04 23:55:20 +01:00
Emmanuel Viennet 5895e5c33c edition formations / tests unitaires ok 2022-01-04 23:05:37 +01:00
Emmanuel Viennet 9cf6cf838e Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into refactor_nt 2022-01-04 20:06:25 +01:00
Emmanuel Viennet 82e3de02f6 update 2022-01-04 20:03:38 +01:00
Emmanuel Viennet e3535aa4da check_moduleimpl_conformity: modif exc incohérence 2022-01-04 19:46:35 +01:00
Emmanuel Viennet e1adf93bf0 Fix: edition des users sans dept 2022-01-04 19:32:58 +01:00
Emmanuel Viennet be2227f8a3 L'admin peut cacher/montrer des départements 2022-01-04 18:10:14 +01:00
Emmanuel Viennet 9b9b2f270b Fix: flag publication bulletins 2022-01-04 17:49:13 +01:00
Emmanuel Viennet 6fe77988a0 Fix: liste des indices de semestres 2022-01-04 17:44:56 +01:00
Emmanuel Viennet a1bb957eaf meilleure gestion des suppressions d'objets dans l'édition des formations 2022-01-04 17:33:02 +01:00
Emmanuel Viennet f2e21e0cc2 default module type name 2022-01-04 15:03:38 +01:00
Emmanuel Viennet a65e9f9e08 cube vide 2022-01-04 14:55:32 +01:00
Emmanuel Viennet e838321e0c 9.1.17 2022-01-04 12:26:08 +01:00
Emmanuel Viennet 75b13ed9f7 Merge branch 'arthur-pr' into refactor_nt 2022-01-04 11:43:41 +01:00
Emmanuel Viennet 51d75acb68 Merge branch 'entreprises' of https://scodoc.org/git/arthur.zhu/ScoDoc into arthur-pr 2022-01-04 11:41:15 +01:00
Emmanuel Viennet 46c64ba78b comments 2022-01-03 12:33:27 +01:00
Emmanuel Viennet b6a3bd2388 cosmetic 2022-01-03 12:31:20 +01:00
Emmanuel Viennet 407c3ef472 WIP: nouveau format XML Orebut ref. comp. 2022-01-03 12:25:42 +01:00
140 changed files with 13537 additions and 2135 deletions

2
.gitignore vendored
View File

@ -169,5 +169,7 @@ Thumbs.db
.vscode/
*.code-workspace
# PyCharm
.idea/
copy

View File

@ -190,6 +190,7 @@ def create_app(config_class=DevConfig):
app.register_error_handler(ScoGenError, handle_sco_value_error)
app.register_error_handler(ScoValueError, handle_sco_value_error)
app.register_error_handler(AccessDenied, handle_access_denied)
app.register_error_handler(500, internal_server_error)
app.register_error_handler(503, postgresql_server_error)

View File

@ -49,10 +49,11 @@ from app.api.auth import token_auth
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_permissions import Permission
@bp.route("list_depts", methods=["GET"])
@bp.route("/list_depts", methods=["GET"])
@token_auth.login_required
def list_depts():
depts = models.Departement.query.filter_by(visible=True).all()
@ -66,7 +67,7 @@ def etudiants():
"""Liste de tous les étudiants actuellement inscrits à un semestre
en cours.
"""
# Vérification de l'accès: permission Observateir sur tous les départements
# 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")
@ -78,3 +79,413 @@ def etudiants():
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")

View File

@ -11,7 +11,7 @@ from time import time
from typing import Optional
import cracklib # pylint: disable=import-error
from flask import current_app, url_for, g
from flask import current_app, g
from flask_login import UserMixin, AnonymousUserMixin
from werkzeug.security import generate_password_hash, check_password_hash
@ -136,6 +136,7 @@ class User(UserMixin, db.Model):
return check_password_hash(self.password_hash, password)
def get_reset_password_token(self, expires_in=600):
"Un token pour réinitialiser son mot de passe"
return jwt.encode(
{"reset_password": self.id, "exp": time() + expires_in},
current_app.config["SECRET_KEY"],
@ -144,15 +145,17 @@ class User(UserMixin, db.Model):
@staticmethod
def verify_reset_password_token(token):
"Vérification du token de reéinitialisation du mot de passe"
try:
id = jwt.decode(
user_id = jwt.decode(
token, current_app.config["SECRET_KEY"], algorithms=["HS256"]
)["reset_password"]
except:
return
return User.query.get(id)
return User.query.get(user_id)
def to_dict(self, include_email=True):
"""l'utilisateur comme un dict, avec des champs supplémentaires"""
data = {
"date_expiration": self.date_expiration.isoformat() + "Z"
if self.date_expiration
@ -472,5 +475,5 @@ def get_super_admin():
@login.user_loader
def load_user(id):
return User.query.get(int(id))
def load_user(uid):
return User.query.get(int(uid))

View File

@ -30,17 +30,18 @@ class BulletinBUT(ResultatsSemestreBUT):
ue_idx = self.modimpl_coefs_df.index.get_loc(ue.id)
etud_moy_module = self.sem_cube[etud_idx] # module x UE
for modimpl in modimpls:
coef = self.modimpl_coefs_df[modimpl.id][ue.id]
if coef > 0:
d[modimpl.module.code] = {
"id": modimpl.id,
"coef": coef,
"moyenne": fmt_note(
etud_moy_module[
self.modimpl_coefs_df.columns.get_loc(modimpl.id)
][ue_idx]
),
}
if self.modimpl_inscr_df[str(modimpl.id)][etud.id]: # si inscrit
coef = self.modimpl_coefs_df[modimpl.id][ue.id]
if coef > 0:
d[modimpl.module.code] = {
"id": modimpl.id,
"coef": coef,
"moyenne": fmt_note(
etud_moy_module[
self.modimpl_coefs_df.columns.get_loc(modimpl.id)
][ue_idx]
),
}
return d
def etud_ue_results(self, etud, ue):
@ -87,29 +88,30 @@ class BulletinBUT(ResultatsSemestreBUT):
# except RuntimeWarning: # all nans in np.nanmean
# pass
modimpl_results = self.modimpls_results[modimpl.id]
d[modimpl.module.code] = {
"id": modimpl.id,
"titre": modimpl.module.titre,
"code_apogee": modimpl.module.code_apogee,
"url": url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=modimpl.id,
),
"moyenne": {
# # moyenne indicative de module: moyenne des UE, ignorant celles sans notes (nan)
# "value": fmt_note(moy_indicative_mod),
# "min": fmt_note(moyennes_etuds.min()),
# "max": fmt_note(moyennes_etuds.max()),
# "moy": fmt_note(moyennes_etuds.mean()),
},
"evaluations": [
self.etud_eval_results(etud, e)
for e in modimpl.evaluations
if e.visibulletin
and modimpl_results.evaluations_etat[e.id].is_complete
],
}
if self.modimpl_inscr_df[str(modimpl.id)][etud.id]: # si inscrit
d[modimpl.module.code] = {
"id": modimpl.id,
"titre": modimpl.module.titre,
"code_apogee": modimpl.module.code_apogee,
"url": url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=modimpl.id,
),
"moyenne": {
# # moyenne indicative de module: moyenne des UE, ignorant celles sans notes (nan)
# "value": fmt_note(moy_indicative_mod),
# "min": fmt_note(moyennes_etuds.min()),
# "max": fmt_note(moyennes_etuds.max()),
# "moy": fmt_note(moyennes_etuds.mean()),
},
"evaluations": [
self.etud_eval_results(etud, e)
for e in modimpl.evaluations
if e.visibulletin
and modimpl_results.evaluations_etat[e.id].is_complete
],
}
return d
def etud_eval_results(self, etud, e) -> dict:
@ -145,6 +147,7 @@ class BulletinBUT(ResultatsSemestreBUT):
def bulletin_etud(self, etud, formsemestre) -> dict:
"""Le bulletin de l'étudiant dans ce semestre"""
etat_inscription = etud.etat_inscription(formsemestre.id)
nb_inscrits = self.get_inscriptions_counts()[scu.INSCRIT]
d = {
"version": "0",
"type": "BUT",
@ -187,7 +190,7 @@ class BulletinBUT(ResultatsSemestreBUT):
},
"rang": { # classement wrt moyenne général, indicatif
"value": self.etud_moy_gen_ranks[etud.id],
"total": len(self.etuds),
"total": nb_inscrits,
},
},
)
@ -210,7 +213,7 @@ class BulletinBUT(ResultatsSemestreBUT):
"moy": "",
"max": "",
},
"rang": {"value": "DEM", "total": len(self.etuds)},
"rang": {"value": "DEM", "total": nb_inscrits},
}
)
d.update(

View File

@ -68,14 +68,16 @@ def bulletin_but_xml_compat(
"bulletin_but_xml_compat( formsemestre_id=%s, etudid=%s )"
% (formsemestre_id, etudid)
)
sem = FormSemestre.query.get_or_404(formsemestre_id)
etud = Identite.query.get_or_404(etudid)
results = bulletin_but.ResultatsSemestreBUT(sem)
nb_inscrits = len(results.etuds)
if sem.bul_hide_xml or force_publishing:
published = "1"
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
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 = results.formsemestre.etuds_inscriptions[etudid].etat
if (not formsemestre.bul_hide_xml) or force_publishing:
published = 1
else:
published = "0"
published = 0
if xml_nodate:
docdate = ""
else:
@ -84,12 +86,12 @@ def bulletin_but_xml_compat(
"etudid": str(etudid),
"formsemestre_id": str(formsemestre_id),
"date": docdate,
"publie": published,
"publie": str(published),
}
if sem.etapes:
el["etape_apo"] = sem.etapes[0].etape_apo or ""
if formsemestre.etapes:
el["etape_apo"] = formsemestre.etapes[0].etape_apo or ""
n = 2
for et in sem.etapes[1:]:
for et in formsemestre.etapes[1:]:
el["etape_apo" + str(n)] = et.etape_apo or ""
n += 1
x = Element("bulletinetud", **el)
@ -117,79 +119,75 @@ def bulletin_but_xml_compat(
)
# Disponible pour publication ?
if not published:
return doc # stop !
# Moyenne générale:
doc.append(
Element(
"note",
value=scu.fmt_note(results.etud_moy_gen[etud.id]),
min=scu.fmt_note(results.etud_moy_gen.min()),
max=scu.fmt_note(results.etud_moy_gen.max()),
moy=scu.fmt_note(results.etud_moy_gen.mean()), # moyenne des moy. gen.
)
)
rang = 0 # XXX TODO rang de l'étduiant selon la moy gen indicative
bonus = 0 # XXX TODO valeur du bonus sport
doc.append(Element("rang", value=str(rang), ninscrits=str(nb_inscrits)))
# XXX TODO: ajouter "rang_group" : rangs dans les partitions
doc.append(Element("note_max", value="20")) # notes toujours sur 20
doc.append(Element("bonus_sport_culture", value=str(bonus)))
# Liste les UE / modules /evals
for ue in results.ues:
rang_ue = 0 # XXX TODO rang de l'étudiant dans cette UE
nb_inscrits_ue = (
nb_inscrits # approx: compliqué de définir le "nb d'inscrit à une UE"
)
x_ue = Element(
"ue",
id=str(ue.id),
numero=scu.quote_xml_attr(ue.numero),
acronyme=scu.quote_xml_attr(ue.acronyme or ""),
titre=scu.quote_xml_attr(ue.titre or ""),
code_apogee=scu.quote_xml_attr(ue.code_apogee or ""),
)
doc.append(x_ue)
if ue.type != sco_codes_parcours.UE_SPORT:
v = results.etud_moy_ue[ue.id][etud.id]
else:
v = 0 # XXX TODO valeur bonus sport pour cet étudiant
x_ue.append(
return sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(
scu.SCO_ENCODING
) # stop !
if etat_inscription == scu.INSCRIT:
# Moyenne générale:
doc.append(
Element(
"note",
value=scu.fmt_note(v),
min=scu.fmt_note(results.etud_moy_ue[ue.id].min()),
max=scu.fmt_note(results.etud_moy_ue[ue.id].max()),
value=scu.fmt_note(results.etud_moy_gen[etud.id]),
min=scu.fmt_note(results.etud_moy_gen.min()),
max=scu.fmt_note(results.etud_moy_gen.max()),
moy=scu.fmt_note(results.etud_moy_gen.mean()), # moyenne des moy. gen.
)
)
x_ue.append(Element("ects", value=str(ue.ects if ue.ects else 0)))
x_ue.append(Element("rang", value=str(rang_ue)))
x_ue.append(Element("effectif", value=str(nb_inscrits_ue)))
# Liste les modules rattachés à cette UE
for modimpl in results.modimpls:
# Liste ici uniquement les modules rattachés à cette UE
if modimpl.module.ue.id == ue.id:
mod_moy = scu.fmt_note(results.etud_moy_ue[ue.id][etud.id])
coef = results.modimpl_coefs_df[modimpl.id][ue.id]
x_mod = Element(
"module",
id=str(modimpl.id),
code=str(modimpl.module.code or ""),
coefficient=str(coef),
numero=str(modimpl.module.numero or 0),
titre=scu.quote_xml_attr(modimpl.module.titre or ""),
abbrev=scu.quote_xml_attr(modimpl.module.abbrev or ""),
code_apogee=scu.quote_xml_attr(modimpl.module.code_apogee or ""),
rang = 0 # XXX TODO rang de l'étduiant selon la moy gen indicative
bonus = 0 # XXX TODO valeur du bonus sport
doc.append(Element("rang", value=str(rang), ninscrits=str(nb_inscrits)))
# XXX TODO: ajouter "rang_group" : rangs dans les partitions
doc.append(Element("note_max", value="20")) # notes toujours sur 20
doc.append(Element("bonus_sport_culture", value=str(bonus)))
# Liste les UE / modules /evals
for ue in results.ues:
rang_ue = 0 # XXX TODO rang de l'étudiant dans cette UE
nb_inscrits_ue = (
nb_inscrits # approx: compliqué de définir le "nb d'inscrit à une UE"
)
x_ue = Element(
"ue",
id=str(ue.id),
numero=scu.quote_xml_attr(ue.numero),
acronyme=scu.quote_xml_attr(ue.acronyme or ""),
titre=scu.quote_xml_attr(ue.titre or ""),
code_apogee=scu.quote_xml_attr(ue.code_apogee or ""),
)
doc.append(x_ue)
if ue.type != sco_codes_parcours.UE_SPORT:
v = results.etud_moy_ue[ue.id][etud.id]
else:
v = 0 # XXX TODO valeur bonus sport pour cet étudiant
x_ue.append(
Element(
"note",
value=scu.fmt_note(v),
min=scu.fmt_note(results.etud_moy_ue[ue.id].min()),
max=scu.fmt_note(results.etud_moy_ue[ue.id].max()),
)
x_ue.append(x_mod)
x_mod.append(
Element(
"note",
value=mod_moy,
min=scu.fmt_note(results.etud_moy_ue[ue.id].min()),
max=scu.fmt_note(results.etud_moy_ue[ue.id].max()),
moy=scu.fmt_note(results.etud_moy_ue[ue.id].mean()),
)
x_ue.append(Element("ects", value=str(ue.ects if ue.ects else 0)))
x_ue.append(Element("rang", value=str(rang_ue)))
x_ue.append(Element("effectif", value=str(nb_inscrits_ue)))
# Liste les modules rattachés à cette UE
for modimpl in results.modimpls:
# Liste ici uniquement les modules rattachés à cette UE
if modimpl.module.ue.id == ue.id:
# mod_moy = scu.fmt_note(results.etud_moy_ue[ue.id][etud.id])
coef = results.modimpl_coefs_df[modimpl.id][ue.id]
x_mod = Element(
"module",
id=str(modimpl.id),
code=str(modimpl.module.code or ""),
coefficient=str(coef),
numero=str(modimpl.module.numero or 0),
titre=scu.quote_xml_attr(modimpl.module.titre or ""),
abbrev=scu.quote_xml_attr(modimpl.module.abbrev or ""),
code_apogee=scu.quote_xml_attr(
modimpl.module.code_apogee or ""
),
)
)
# XXX TODO rangs et effectifs
# --- notes de chaque eval:
if version != "short":
@ -218,16 +216,17 @@ def bulletin_but_xml_compat(
value=scu.fmt_note(
results.modimpls_results[
e.moduleimpl_id
].evals_notes[e.id][etud.id]
].evals_notes[e.id][etud.id],
note_max=e.note_max,
),
)
)
# XXX TODO: Evaluations incomplètes ou futures: XXX
# XXX TODO UE capitalisee (listee seulement si meilleure que l'UE courante)
# XXX TODO UE capitalisee (listee seulement si meilleure que l'UE courante)
# --- Absences
if sco_preferences.get_preference("bul_show_abs", formsemestre_id):
nbabs, nbabsjust = sem.get_abs_count(etud.id)
nbabs, nbabsjust = formsemestre.get_abs_count(etud.id)
doc.append(Element("absences", nbabs=str(nbabs), nbabsjust=str(nbabsjust)))
# -------- LA SUITE EST COPIEE SANS MODIF DE sco_bulletins_xml.py ---------

View File

@ -29,8 +29,8 @@ def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None):
"""
try:
root = ElementTree.XML(xml_data)
except ElementTree.ParseError:
raise ScoFormatError("fichier XML Orébut invalide")
except ElementTree.ParseError as exc:
raise ScoFormatError(f"fichier XML Orébut invalide (2): {exc.args}")
if root.tag != "referentiel_competence":
raise ScoFormatError("élément racine 'referentiel_competence' manquant")
args = ApcReferentielCompetences.attr_from_xml(root.attrib)
@ -54,8 +54,8 @@ def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None):
composantes = competence.find("composantes_essentielles")
for composante in composantes:
libelle = "".join(composante.itertext()).strip()
ce = ApcComposanteEssentielle(libelle=libelle)
c.composantes_essentielles.append(ce)
compo_ess = ApcComposanteEssentielle(libelle=libelle)
c.composantes_essentielles.append(compo_ess)
# --- NIVEAUX (années)
niveaux = competence.find("niveaux")
for niveau in niveaux:
@ -77,16 +77,14 @@ def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None):
a = ApcAnneeParcours(**ApcAnneeParcours.attr_from_xml(annee.attrib))
parc.annees.append(a)
for competence in annee.findall("competence"):
nom = competence.attrib["nom"]
comp_id_orebut = competence.attrib["id"]
niveau = int(competence.attrib["niveau"])
# Retrouve la competence
comp = ref.competences.filter_by(titre=nom).all()
if len(comp) == 0:
raise ScoFormatError(f"competence {nom} référencée mais on définie")
elif len(comp) > 1:
raise ScoFormatError(f"competence {nom} ambigüe")
comp = ref.competences.filter_by(id_orebut=comp_id_orebut).first()
if comp is None:
raise ScoFormatError(f"competence {comp_id_orebut} non définie")
ass = ApcParcoursNiveauCompetence(
niveau=niveau, annee_parcours=a, competence=comp[0]
niveau=niveau, annee_parcours=a, competence=comp
)
db.session.add(ass)

View File

@ -4,11 +4,11 @@
# See LICENSE
##############################################################################
import numpy as np
"""Quelques classes auxiliaires pour les calculs des notes
"""
import numpy as np
class StatsMoyenne:
"""Une moyenne d'un ensemble étudiants sur quelque chose

View File

@ -16,13 +16,13 @@ from app import models
#
def df_load_modimpl_inscr(formsemestre) -> pd.DataFrame:
"""Charge la matrice des inscriptions aux modules du semestre
rows: etudid
rows: etudid (inscrits au semestre, avec DEM et DEF)
columns: moduleimpl_id (en chaîne)
value: bool (0/1 inscrit ou pas)
"""
# méthode la moins lente: une requete par module, merge les dataframes
moduleimpl_ids = [m.id for m in formsemestre.modimpls]
etudids = [i.etudid for i in formsemestre.get_inscrits(include_dem=False)]
etudids = [inscr.etudid for inscr in formsemestre.inscriptions]
df = pd.DataFrame(index=etudids, dtype=int)
for moduleimpl_id in moduleimpl_ids:
ins_df = pd.read_sql_query(

View File

@ -69,11 +69,13 @@ class ModuleImplResults:
"nombre d'inscrits (non DEM) au module"
self.evaluations_completes = []
"séquence de booléens, indiquant les évals à prendre en compte."
self.evaluations_completes_dict = {}
"{ evaluation.id : bool } indique si à prendre en compte ou non."
self.evaluations_etat = {}
"{ evaluation_id: EvaluationEtat }"
#
self.evals_notes = None
"""DataFrame, colonnes: EVALS, Lignes: etudid
"""DataFrame, colonnes: EVALS, Lignes: etudid (inscrits au SEMESTRE)
valeur: notes brutes, float ou NOTES_ATTENTE, NOTES_NEUTRALISE,
NOTES_ABSENCE.
Les NaN désignent les notes manquantes (non saisies).
@ -103,7 +105,7 @@ class ModuleImplResults:
Évaluation "complete" (prise en compte dans les calculs) si:
- soit tous les étudiants inscrits au module ont des notes
- soit elle a été déclarée "à prise ne compte immédiate" (publish_incomplete)
- soit elle a été déclarée "à prise en compte immédiate" (publish_incomplete)
Évaluation "attente" (prise en compte dans les calculs, mais il y
manque des notes) ssi il y a des étudiants inscrits au semestre et au module
@ -122,6 +124,7 @@ class ModuleImplResults:
# dataFrame vide, index = tous les inscrits au SEMESTRE
evals_notes = pd.DataFrame(index=self.etudids, dtype=float)
self.evaluations_completes = []
self.evaluations_completes_dict = {}
for evaluation in moduleimpl.evaluations:
eval_df = self._load_evaluation_notes(evaluation)
# is_complete ssi tous les inscrits (non dem) au semestre ont une note
@ -131,6 +134,7 @@ class ModuleImplResults:
== self.nb_inscrits_module
) or evaluation.publish_incomplete # immédiate
self.evaluations_completes.append(is_complete)
self.evaluations_completes_dict[evaluation.id] = is_complete
# NULL en base => ABS (= -999)
eval_df.fillna(scu.NOTES_ABSENCE, inplace=True)
@ -174,14 +178,12 @@ class ModuleImplResults:
return eval_df
def _etudids(self):
"""L'index du dataframe est la liste des étudiants inscrits au semestre,
sans les démissionnaires.
"""
"""L'index du dataframe est la liste de tous les étudiants inscrits au semestre"""
return [
e.etudid
for e in ModuleImpl.query.get(self.moduleimpl_id).formsemestre.get_inscrits(
include_dem=False
)
inscr.etudid
for inscr in ModuleImpl.query.get(
self.moduleimpl_id
).formsemestre.inscriptions
]
def get_evaluations_coefs(self, moduleimpl: ModuleImpl) -> np.array:
@ -270,7 +272,7 @@ def load_evaluations_poids(
remplies par default_poids.
Résultat: (evals_poids, liste de UE du semestre)
"""
modimpl = ModuleImpl.query.get(moduleimpl_id)
modimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id)
evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all()
ues = modimpl.formsemestre.query_ues(with_sport=False).all()
ue_ids = [ue.id for ue in ues]

View File

@ -31,8 +31,10 @@ import numpy as np
import pandas as pd
def compute_sem_moys_apc(etud_moy_ue_df, modimpl_coefs_df):
"""Calcule la moyenne générale indicative
def compute_sem_moys_apc(
etud_moy_ue_df: pd.DataFrame, modimpl_coefs_df: pd.DataFrame
) -> pd.Series:
"""Calcule les moyennes générales indicatives de tous les étudiants
= moyenne des moyennes d'UE, pondérée par la somme de leurs coefs
etud_moy_ue_df: DataFrame, colonnes ue_id, lignes etudid
@ -46,10 +48,11 @@ def compute_sem_moys_apc(etud_moy_ue_df, modimpl_coefs_df):
return moy_gen
def comp_ranks_series(notes: pd.Series):
"""Calcul rangs à partir d'une séries ("vecteur") de notes (index etudid, valeur numérique)
en tenant compte des ex-aequos
Le resultat est: { etudid : rang } rang est une chaine decrivant le rang
def comp_ranks_series(notes: pd.Series) -> dict[int, str]:
"""Calcul rangs à partir d'une séries ("vecteur") de notes (index etudid, valeur
numérique) en tenant compte des ex-aequos.
Result: { etudid : rang:str } rang est une chaine decrivant le rang.
"""
notes = notes.sort_values(ascending=False) # Serie, tri par ordre décroissant
rangs = pd.Series(index=notes.index, dtype=str) # le rang est une chaîne

View File

@ -36,6 +36,7 @@ from app.models import UniteEns, Module, ModuleImpl, ModuleUECoef
from app.comp import moy_mod
from app.models.formsemestre import FormSemestre
from app.scodoc import sco_codes_parcours
from app.scodoc.sco_utils import ModuleType
def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.DataFrame:
@ -56,8 +57,15 @@ def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.Data
.filter(UniteEns.type != sco_codes_parcours.UE_SPORT)
.order_by(UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme)
)
modules = Module.query.filter_by(formation_id=formation_id).order_by(
Module.semestre_id, Module.module_type.desc(), Module.numero, Module.code
modules = (
Module.query.filter_by(formation_id=formation_id)
.filter(
(Module.module_type == ModuleType.RESSOURCE)
| (Module.module_type == ModuleType.SAE)
)
.order_by(
Module.semestre_id, Module.module_type.desc(), Module.numero, Module.code
)
)
if semestre_idx is not None:
ues = ues.filter_by(semestre_idx=semestre_idx)
@ -76,7 +84,9 @@ def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.Data
query = query.filter(UniteEns.semestre_idx == semestre_idx)
for mod_coef in query:
module_coefs_df[mod_coef.module_id][mod_coef.ue_id] = mod_coef.coef
if mod_coef.module_id in module_coefs_df:
module_coefs_df[mod_coef.module_id][mod_coef.ue_id] = mod_coef.coef
# silently ignore coefs associated to other modules (ie when module_type is changed)
module_coefs_df.fillna(value=0, inplace=True)
@ -121,6 +131,7 @@ def notes_sem_assemble_cube(modimpls_notes: list[pd.DataFrame]) -> np.ndarray:
(DataFrames rendus par compute_module_moy, (etud x UE))
Resultat: ndarray (etud x module x UE)
"""
assert len(modimpls_notes)
modimpls_notes_arr = [df.values for df in modimpls_notes]
modimpls_notes = np.stack(modimpls_notes_arr)
# passe de (mod x etud x ue) à (etud x mod x UE)
@ -128,9 +139,14 @@ def notes_sem_assemble_cube(modimpls_notes: list[pd.DataFrame]) -> np.ndarray:
def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
"""Calcule le cube des notes du semestre
(charge toutes les notes, calcule les moyenne des modules
et assemble le cube)
"""Construit le "cube" (tenseur) des notes du semestre.
Charge toutes les notes (sql), calcule les moyennes des modules
et assemble le cube.
etuds: tous les inscrits au semestre (avec dem. et def.)
modimpls: _tous_ les modimpls de ce semestre
UEs: X?X voir quelles sont les UE considérées ici
Resultat:
sem_cube : ndarray (etuds x modimpls x UEs)
modimpls_evals_poids dict { modimpl.id : evals_poids }
@ -145,8 +161,13 @@ def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
etuds_moy_module = mod_results.compute_module_moy(evals_poids)
modimpls_results[modimpl.id] = mod_results
modimpls_notes.append(etuds_moy_module)
if len(modimpls_notes):
cube = notes_sem_assemble_cube(modimpls_notes)
else:
nb_etuds = formsemestre.etuds.count()
cube = np.zeros((nb_etuds, 0, 0), dtype=float)
return (
notes_sem_assemble_cube(modimpls_notes),
cube,
modimpls_evals_poids,
modimpls_results,
)
@ -162,14 +183,14 @@ def compute_ue_moys_apc(
) -> 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]
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]
sem_cube: notes moyennes aux modules
ndarray (etuds x modimpls x UEs)
(floats avec des NaN)
etuds : listes des étudiants (dim. 0 du cube)
etuds : liste des étudiants (dim. 0 du cube)
modimpls : liste des modules à considérer (dim. 1 du cube)
ues : liste des UE (dim. 2 du cube)
modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl)
@ -178,8 +199,12 @@ def compute_ue_moys_apc(
Resultat: DataFrame columns UE, rows etudid
"""
nb_etuds, nb_modules, nb_ues = sem_cube.shape
assert len(etuds) == nb_etuds
assert len(modimpls) == nb_modules
if nb_modules == 0 or nb_etuds == 0:
return pd.DataFrame(
index=modimpl_inscr_df.index, columns=modimpl_coefs_df.index
)
assert len(etuds) == nb_etuds
assert len(ues) == nb_ues
assert modimpl_inscr_df.shape[0] == nb_etuds
assert modimpl_inscr_df.shape[1] == nb_modules
@ -187,10 +212,6 @@ def compute_ue_moys_apc(
assert modimpl_coefs_df.shape[1] == nb_modules
modimpl_inscr = modimpl_inscr_df.values
modimpl_coefs = modimpl_coefs_df.values
if nb_etuds == 0:
return pd.DataFrame(
index=modimpl_inscr_df.index, columns=modimpl_coefs_df.index
)
# Duplique les inscriptions sur les UEs:
modimpl_inscr_stacked = np.stack([modimpl_inscr] * nb_ues, axis=2)
# Enlève les NaN du numérateur:
@ -223,12 +244,12 @@ def compute_ue_moys_classic(
ues: list,
modimpl_inscr_df: pd.DataFrame,
modimpl_coefs: np.array,
) -> pd.DataFrame:
) -> tuple[pd.Series, pd.DataFrame, pd.DataFrame]:
"""Calcul de la moyenne d'UE en mode classique.
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]
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]
sem_matrix: notes moyennes aux modules
ndarray (etuds x modimpls)
@ -241,6 +262,9 @@ def compute_ue_moys_classic(
Résultat:
- moyennes générales: pd.Series, index etudid
- moyennes d'UE: DataFrame columns UE, rows etudid
- coefficients d'UE: DataFrame, columns UE, rows etudid
les coefficients effectifs de chaque UE pour chaque étudiant
(sommes de coefs de modules pris en compte)
"""
nb_etuds, nb_modules = sem_matrix.shape
assert len(modimpl_coefs) == nb_modules
@ -281,4 +305,9 @@ def compute_ue_moys_classic(
etud_moy_ue_df = pd.DataFrame(
etud_moy_ue, index=modimpl_inscr_df.index, columns=[ue.id for ue in ues]
)
return etud_moy_gen_s, etud_moy_ue_df
etud_coef_ue_df = pd.DataFrame(
coefs.sum(axis=2).T,
index=modimpl_inscr_df.index, # etudids
columns=[ue.id for ue in ues],
)
return etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df

View File

@ -6,9 +6,10 @@
"""Résultats semestres BUT
"""
import pandas as pd
from app.comp import moy_ue, moy_sem, inscr_mod
from app.comp.res_sem import NotesTableCompat
from app.comp.res_common import NotesTableCompat
class ResultatsSemestreBUT(NotesTableCompat):
@ -49,6 +50,10 @@ class ResultatsSemestreBUT(NotesTableCompat):
self.modimpl_inscr_df,
self.modimpl_coefs_df,
)
# 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
)
self.etud_moy_gen = moy_sem.compute_sem_moys_apc(
self.etud_moy_ue, self.modimpl_coefs_df
)

View File

@ -8,8 +8,9 @@
"""
import numpy as np
import pandas as pd
from app.comp import moy_mod, moy_ue, moy_sem, inscr_mod
from app.comp.res_sem import NotesTableCompat
from app.comp.res_common import NotesTableCompat
from app.models.formsemestre import FormSemestre
@ -45,7 +46,11 @@ class ResultatsSemestreClassic(NotesTableCompat):
self.modimpl_idx = {m.id: i for i, m in enumerate(self.formsemestre.modimpls)}
"l'idx de la colonne du mod modimpl.id est modimpl_idx[modimpl.id]"
self.etud_moy_gen, self.etud_moy_ue = moy_ue.compute_ue_moys_classic(
(
self.etud_moy_gen,
self.etud_moy_ue,
self.etud_coef_ue_df,
) = moy_ue.compute_ue_moys_classic(
self.formsemestre,
self.sem_matrix,
self.ues,
@ -60,6 +65,25 @@ class ResultatsSemestreClassic(NotesTableCompat):
"""
return self.modimpls_results[moduleimpl_id].etuds_moy_module.get(etudid, "NI")
def get_mod_stats(self, moduleimpl_id: int) -> dict:
"""Stats sur les notes obtenues dans un modimpl"""
notes_series: pd.Series = self.modimpls_results[moduleimpl_id].etuds_moy_module
nb_notes = len(notes_series)
if not nb_notes:
super().get_mod_stats(moduleimpl_id)
return {
# Series: Statistical methods from ndarray have been overridden to automatically
# exclude missing data (currently represented as NaN)
"moy": notes_series.mean(), # donc sans prendre en compte les NaN
"max": notes_series.max(),
"min": notes_series.min(),
"nb_notes": nb_notes,
"nb_missing": sum(notes_series.isna()),
"nb_valid_evals": sum(
self.modimpls_results[moduleimpl_id].evaluations_completes
),
}
def notes_sem_load_matrix(formsemestre: FormSemestre) -> tuple:
"""Calcule la matrice des notes du semestre

397
app/comp/res_common.py Normal file
View File

@ -0,0 +1,397 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
from collections import defaultdict, Counter
from functools import cached_property
import numpy as np
import pandas as pd
from app.comp.aux import StatsMoyenne
from app.comp.moy_mod import ModuleImplResults
from app.models import FormSemestre, Identite, ModuleImpl
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, ATT, DEF
# Il faut bien distinguer
# - ce qui est caché de façon persistente (via redis):
# ce sont les attributs listés dans `_cached_attrs`
# le stockage et l'invalidation sont gérés dans sco_cache.py
#
# - les valeurs cachées durant le temps d'une requête
# (durée de vie de l'instance de ResultatsSemestre)
# qui sont notamment les attributs décorés par `@cached_property``
#
class ResultatsSemestre:
_cached_attrs = (
"etud_moy_gen_ranks",
"etud_moy_gen",
"etud_moy_ue",
"modimpl_inscr_df",
"modimpls_results",
"etud_coef_ue_df",
)
def __init__(self, formsemestre: FormSemestre):
self.formsemestre: FormSemestre = formsemestre
# BUT ou standard ? (apc == "approche par compétences")
self.is_apc = formsemestre.formation.is_apc()
# Attributs "virtuels", définis dans les sous-classes
# ResultatsSemestreBUT ou ResultatsSemestreClassic
self.etud_moy_ue = {}
"etud_moy_ue: DataFrame columns UE, rows etudid"
self.etud_moy_gen = {}
self.etud_moy_gen_ranks = {}
self.modimpls_results: ModuleImplResults = None
self.etud_coef_ue_df = None
"""coefs d'UE effectifs pour chaque etudiant (pour form. classiques)"""
# TODO ?
def load_cached(self) -> bool:
"Load cached dataframes, returns False si pas en cache"
data = ResultatsSemestreCache.get(self.formsemestre.id)
if not data:
return False
for attr in self._cached_attrs:
setattr(self, attr, data[attr])
return True
def store(self):
"Cache our data"
ResultatsSemestreCache.set(
self.formsemestre.id,
{attr: getattr(self, attr) for attr in self._cached_attrs},
)
def compute(self):
"Charge les notes et inscriptions et calcule toutes les moyennes"
# voir ce qui est chargé / calculé ici et dans les sous-classes
raise NotImplementedError()
def get_inscriptions_counts(self) -> Counter:
"""Nombre d'inscrits, défaillants, démissionnaires.
Exemple: res.get_inscriptions_counts()[scu.INSCRIT]
Result: a collections.Counter instance
"""
return Counter(ins.etat for ins in self.formsemestre.inscriptions)
@cached_property
def etuds(self) -> list[Identite]:
"Liste des inscrits au semestre, avec les démissionnaires et les défaillants"
# nb: si la liste des inscrits change, ResultatsSemestre devient invalide
return self.formsemestre.get_inscrits(include_demdef=True)
@cached_property
def etud_index(self) -> dict[int, int]:
"dict { etudid : indice dans les inscrits }"
return {e.id: idx for idx, e in enumerate(self.etuds)}
@cached_property
def etuds_dict(self) -> dict[int, Identite]:
"""dict { etudid : Identite } inscrits au semestre,
avec les démissionnaires et defs."""
return {etud.id: etud for etud in self.etuds}
@cached_property
def ues(self) -> list[UniteEns]:
"""Liste des UEs du semestre
(indices des DataFrames)
"""
return self.formsemestre.query_ues(with_sport=True).all()
@cached_property
def modimpls(self):
"""Liste des modimpls du semestre
- triée par numéro de module en APC
- triée par numéros d'UE/matières/modules pour les formations standard.
"""
modimpls = self.formsemestre.modimpls.all()
if self.is_apc:
modimpls.sort(key=lambda m: (m.module.numero, m.module.code))
else:
modimpls.sort(
key=lambda m: (
m.module.ue.numero,
m.module.matiere.numero,
m.module.numero,
m.module.code,
)
)
return modimpls
@cached_property
def ressources(self):
"Liste des ressources du semestre, triées par numéro de module"
return [
m for m in self.modimpls if m.module.module_type == scu.ModuleType.RESSOURCE
]
@cached_property
def saes(self):
"Liste des SAÉs du semestre, triées par numéro de module"
return [m for m in self.modimpls if m.module.module_type == scu.ModuleType.SAE]
@cached_property
def ue_validables(self) -> list:
"""Liste des UE du semestre qui doivent être validées
(toutes sauf le sport)
"""
return self.formsemestre.query_ues().filter(UniteEns.type != UE_SPORT).all()
@cached_property
def ue_au_dessus(self, seuil=10.0) -> pd.DataFrame:
"""DataFrame columns UE, rows etudid, valeurs: bool
Par exemple, pour avoir le nombre d'UE au dessus de 10 pour l'étudiant etudid
nb_ues_ok = sum(res.ue_au_dessus().loc[etudid])
"""
return self.etud_moy_ue > (seuil - scu.NOTES_TOLERANCE)
# Pour raccorder le code des anciens codes qui attendent une NoteTable
class NotesTableCompat(ResultatsSemestre):
"""Implementation partielle de NotesTable WIP TODO
Les méthodes définies dans cette classe sont
pour conserver la compatibilité abvec les codes anciens et
il n'est pas recommandé de les utiliser dans de nouveaux
développements (API malcommode et peu efficace).
"""
_cached_attrs = ResultatsSemestre._cached_attrs + ()
def __init__(self, formsemestre: FormSemestre):
super().__init__(formsemestre)
nb_etuds = len(self.etuds)
self.bonus = defaultdict(lambda: 0.0) # XXX TODO
self.ue_rangs = {u.id: (defaultdict(lambda: 0.0), nb_etuds) for u in self.ues}
self.mod_rangs = {
m.id: (defaultdict(lambda: 0), nb_etuds) for m in self.modimpls
}
self.moy_min = "NA"
self.moy_max = "NA"
self.moy_moy = "NA"
self.expr_diagnostics = ""
self.parcours = self.formsemestre.formation.get_parcours()
def get_etudids(self, sorted=False) -> list[int]:
"""Liste des etudids inscrits, incluant les démissionnaires.
Si sorted, triée par moy. générale décroissante
Sinon, triée par ordre alphabetique de NOM
"""
# Note: pour avoir les inscrits non triés,
# utiliser [ ins.etudid for ins in self.formsemestre.inscriptions ]
if sorted:
# Tri par moy. generale décroissante
return [x[-1] for x in self.T]
return [x["etudid"] for x in self.inscrlist]
@cached_property
def sem(self) -> dict:
"""le formsemestre, comme un dict (nt.sem)"""
return self.formsemestre.to_dict()
@cached_property
def inscrlist(self) -> list[dict]: # utilisé par PE seulement
"""Liste des inscrits au semestre (avec DEM et DEF),
sous forme de dict etud,
classée dans l'ordre alphabétique de noms.
"""
etuds = self.formsemestre.get_inscrits(include_demdef=True)
etuds.sort(key=lambda e: e.sort_key)
return [e.to_dict_scodoc7() for e in etuds]
@cached_property
def stats_moy_gen(self):
"""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()
"""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):
d = ue.to_dict()
d.update(StatsMoyenne(self.etud_moy_ue[ue.id]).to_dict())
ues.append(d)
return ues
def get_modimpls_dict(self, ue_id=None):
"""Liste des modules pour une UE (ou toutes si ue_id==None),
triés par numéros (selon le type de formation)
"""
if ue_id is None:
return [m.to_dict() for m in self.modimpls]
else:
return [m.to_dict() for m in self.modimpls if m.module.ue.id == ue_id]
def get_etud_decision_sem(self, etudid: int) -> dict:
"""Decision du jury prise pour cet etudiant, ou None s'il n'y en pas eu.
{ 'code' : None|ATT|..., 'assidu' : 0|1, 'event_date' : , compense_formsemestre_id }
Si état défaillant, force le code a DEF
"""
if self.get_etud_etat(etudid) == DEF:
return {
"code": DEF,
"assidu": False,
"event_date": "",
"compense_formsemestre_id": None,
}
else:
return {
"code": ATT, # XXX TODO
"assidu": True, # XXX TODO
"event_date": "",
"compense_formsemestre_id": None,
}
def get_etud_etat(self, etudid: int) -> str:
"Etat de l'etudiant: 'I', 'D', DEF ou '' (si pas connu dans ce semestre)"
ins = self.formsemestre.etuds_inscriptions.get(etudid, None)
if ins is None:
return ""
return ins.etat
def get_etud_moy_gen(self, etudid): # -> float | str
"""Moyenne générale de cet etudiant dans ce semestre.
Prend(ra) en compte les UE capitalisées. (TODO) XXX
Si apc, moyenne indicative.
Si pas de notes: 'NA'
"""
return self.etud_moy_gen[etudid]
def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float:
"""La moyenne de l'étudiant dans le moduleimpl
En APC, il s'agira d'une moyenne indicative sans valeur.
Result: valeur float (peut être naN) ou chaîne "NI" (non inscrit ou DEM)
"""
raise NotImplementedError() # virtual method
def get_etud_ue_status(self, etudid: int, ue_id: int):
coef_ue = self.etud_coef_ue_df[ue_id][etudid]
return {
"cur_moy_ue": self.etud_moy_ue[ue_id][etudid],
"moy": self.etud_moy_ue[ue_id][etudid],
"is_capitalized": False, # XXX TODO
"coef_ue": coef_ue, # XXX TODO
}
def get_etud_rang(self, etudid: int):
return self.etud_moy_gen_ranks.get(etudid, 99999) # XXX
def get_etud_rang_group(self, etudid: int, group_id: int):
return (None, 0) # XXX unimplemented TODO
def get_evals_in_mod(self, moduleimpl_id: int) -> list[dict]:
"""Liste d'informations (compat NotesTable) sur évaluations completes
de ce module.
Évaluation "complete" ssi toutes notes saisies ou en attente.
"""
modimpl = ModuleImpl.query.get(moduleimpl_id)
evals_results = []
for e in modimpl.evaluations:
if self.modimpls_results[moduleimpl_id].evaluations_completes_dict[e.id]:
d = e.to_dict()
moduleimpl_results = self.modimpls_results[e.moduleimpl_id]
d["heure_debut"] = e.heure_debut # datetime.time
d["heure_fin"] = e.heure_fin
d["jour"] = e.jour # datetime
d["notes"] = {
etud.id: {
"etudid": etud.id,
"value": moduleimpl_results.evals_notes[e.id][etud.id],
}
for etud in self.etuds
}
d["etat"] = {
"evalattente": moduleimpl_results.evaluations_etat[e.id].nb_attente,
}
evals_results.append(d)
return evals_results
def get_moduleimpls_attente(self):
return [] # XXX TODO
def get_mod_stats(self, moduleimpl_id: int) -> dict:
"""Stats sur les notes obtenues dans un modimpl
Vide en APC
"""
return {
"moy": "-",
"max": "-",
"min": "-",
"nb_notes": "-",
"nb_missing": "-",
"nb_valid_evals": "-",
}
def get_nom_short(self, etudid):
"formatte nom d'un etud (pour table recap)"
etud = self.identdict[etudid]
return (
(etud["nom_usuel"] or etud["nom"]).upper()
+ " "
+ etud["prenom"].capitalize()[:2]
+ "."
)
@cached_property
def T(self):
return self.get_table_moyennes_triees()
def get_table_moyennes_triees(self) -> list:
"""Result: liste de tuples
moy_gen, moy_ue_0, ..., moy_ue_n, moy_mod1, ..., moy_mod_n, etudid
"""
table_moyennes = []
etuds_inscriptions = self.formsemestre.etuds_inscriptions
for etudid in etuds_inscriptions:
moy_gen = self.etud_moy_gen.get(etudid, False)
if moy_gen is False:
# pas de moyenne: démissionnaire ou def
t = ["-"] + ["0.00"] * len(self.ues) + ["NI"] * len(self.modimpls)
else:
moy_ues = self.etud_moy_ue.loc[etudid]
t = [moy_gen] + list(moy_ues)
# TODO UE capitalisées: ne pas afficher moyennes modules
for modimpl in self.modimpls:
val = self.get_etud_mod_moy(modimpl.id, etudid)
t.append(val)
t.append(etudid)
table_moyennes.append(t)
# tri par moyennes décroissantes,
# en laissant les démissionnaires à la fin, par ordre alphabetique
etuds = [ins.etud for ins in etuds_inscriptions.values()]
etuds.sort(key=lambda e: e.sort_key)
self._rang_alpha = {e.id: i for i, e in enumerate(etuds)}
table_moyennes.sort(key=self._row_key)
return table_moyennes
def _row_key(self, x):
"""clé de tri par moyennes décroissantes,
en laissant les demissionnaires à la fin, par ordre alphabetique.
(moy_gen, rang_alpha)
"""
try:
moy = -float(x[0])
except (ValueError, TypeError):
moy = 1000.0
return (moy, self._rang_alpha[x[-1]])
@cached_property
def identdict(self) -> dict:
"""{ etudid : etud_dict } pour tous les inscrits au semestre"""
return {
ins.etud.id: ins.etud.to_dict_scodoc7()
for ins in self.formsemestre.inscriptions
}

View File

@ -4,334 +4,35 @@
# See LICENSE
##############################################################################
from collections import defaultdict
from functools import cached_property
import numpy as np
import pandas as pd
from app.comp.aux import StatsMoyenne
from app.models import FormSemestre, ModuleImpl
from app.scodoc import sco_utils as scu
from app.scodoc.sco_cache import ResultatsSemestreCache
from app.scodoc.sco_codes_parcours import UE_SPORT, ATT, DEF
"""Chargement des résultats de semestres (tous types)
"""
from flask import g
# Il faut bien distinguer
# - ce qui est caché de façon persistente (via redis):
# ce sont les attributs listés dans `_cached_attrs`
# le stockage et l'invalidation sont gérés dans sco_cache.py
#
# - les valeurs cachées durant le temps d'une requête
# (durée de vie de l'instance de ResultatsSemestre)
# qui sont notamment les attributs décorés par `@cached_property``
#
class ResultatsSemestre:
_cached_attrs = (
"etud_moy_gen_ranks",
"etud_moy_gen",
"etud_moy_ue",
"modimpl_inscr_df",
"modimpls_results",
)
def __init__(self, formsemestre: FormSemestre):
self.formsemestre = formsemestre
# BUT ou standard ? (apc == "approche par compétences")
self.is_apc = formsemestre.formation.is_apc()
# Attributs "virtuels", définis pas les sous-classes
# ResultatsSemestreBUT ou ResultatsSemestreStd
self.etud_moy_ue = {}
self.etud_moy_gen = {}
self.etud_moy_gen_ranks = {}
# TODO
def load_cached(self) -> bool:
"Load cached dataframes, returns False si pas en cache"
data = ResultatsSemestreCache.get(self.formsemestre.id)
if not data:
return False
for attr in self._cached_attrs:
setattr(self, attr, data[attr])
return True
def store(self):
"Cache our data"
ResultatsSemestreCache.set(
self.formsemestre.id,
{attr: getattr(self, attr) for attr in self._cached_attrs},
)
def compute(self):
"Charge les notes et inscriptions et calcule toutes les moyennes"
# voir ce qui est chargé / calculé ici et dans les sous-classes
raise NotImplementedError()
@cached_property
def etuds(self):
"Liste des inscrits au semestre, sans les démissionnaires"
# nb: si la liste des inscrits change, ResultatsSemestre devient invalide
return self.formsemestre.get_inscrits(include_dem=False)
@cached_property
def etud_index(self):
"dict { etudid : indice dans les inscrits }"
return {e.id: idx for idx, e in enumerate(self.etuds)}
@cached_property
def ues(self):
"Liste des UE du semestre"
return self.formsemestre.query_ues().all()
@cached_property
def modimpls(self):
"""Liste des modimpls du semestre
- triée par numéro de module en APC
- triée par numéros d'UE/matières/modules pour les formations standard.
"""
modimpls = self.formsemestre.modimpls.all()
if self.is_apc:
modimpls.sort(key=lambda m: (m.module.numero, m.module.code))
else:
modimpls.sort(
key=lambda m: (
m.module.ue.numero,
m.module.matiere.numero,
m.module.numero,
m.module.code,
)
)
return modimpls
@cached_property
def ressources(self):
"Liste des ressources du semestre, triées par numéro de module"
return [
m for m in self.modimpls if m.module.module_type == scu.ModuleType.RESSOURCE
]
@cached_property
def saes(self):
"Liste des SAÉs du semestre, triées par numéro de module"
return [m for m in self.modimpls if m.module.module_type == scu.ModuleType.SAE]
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
# Pour raccorder le code des anciens codes qui attendent une NoteTable
class NotesTableCompat(ResultatsSemestre):
"""Implementation partielle de NotesTable WIP TODO
def load_formsemestre_result(formsemestre: FormSemestre) -> ResultatsSemestre:
"""Returns ResultatsSemestre for this formsemestre.
Suivant le type de formation, retour une instance de
ResultatsSemestreClassic ou de ResultatsSemestreBUT.
Les méthodes définies dans cette classe sont
pour conserver la compatibilité abvec les codes anciens et
il n'est pas recommandé de les utiliser dans de nouveaux
développements (API malcommode et peu efficace).
Search in local cache (g.formsemestre_result_cache)
then global app cache (eg REDIS)
If not in cache, build it and cache it.
"""
# --- Try local cache (within the same request context)
if not hasattr(g, "formsemestre_result_cache"):
g.formsemestre_result_cache = {} # pylint: disable=C0237
else:
if formsemestre.id in g.formsemestre_result_cache:
return g.formsemestre_result_cache[formsemestre.id]
_cached_attrs = ResultatsSemestre._cached_attrs + ()
def __init__(self, formsemestre: FormSemestre):
super().__init__(formsemestre)
nb_etuds = len(self.etuds)
self.bonus = defaultdict(lambda: 0.0) # XXX TODO
self.ue_rangs = {u.id: (defaultdict(lambda: 0.0), nb_etuds) for u in self.ues}
self.mod_rangs = {
m.id: (defaultdict(lambda: 0), nb_etuds) for m in self.modimpls
}
self.moy_min = "NA"
self.moy_max = "NA"
def get_etudids(self, sorted=False) -> list[int]:
"""Liste des etudids inscrits, incluant les démissionnaires.
Si sorted, triée par moy. générale décroissante
Sinon, triée par ordre alphabetique de NOM
"""
# Note: pour avoir les inscrits non triés,
# utiliser [ ins.etudid for ins in self.formsemestre.inscriptions ]
if sorted:
# Tri par moy. generale décroissante
return [x[-1] for x in self.T]
return [x["etudid"] for x in self.inscrlist]
@cached_property
def inscrlist(self) -> list[dict]: # utilisé par PE seulement
"""Liste de dict etud, avec démissionnaires
classée dans l'ordre alphabétique de noms.
"""
etuds = self.formsemestre.get_inscrits(include_dem=True)
etuds.sort(key=lambda e: e.sort_key)
return [e.to_dict_scodoc7() for e in etuds]
@cached_property
def stats_moy_gen(self):
"""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()
"""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.ues:
if filter_sport and ue.type == UE_SPORT:
continue
d = ue.to_dict()
d.update(StatsMoyenne(self.etud_moy_ue[ue.id]).to_dict())
ues.append(d)
return ues
def get_modimpls_dict(self, ue_id=None):
"""Liste des modules pour une UE (ou toutes si ue_id==None),
triés par numéros (selon le type de formation)
"""
if ue_id is None:
return [m.to_dict() for m in self.modimpls]
else:
return [m.to_dict() for m in self.modimpls if m.module.ue.id == ue_id]
def get_etud_decision_sem(self, etudid: int) -> dict:
"""Decision du jury prise pour cet etudiant, ou None s'il n'y en pas eu.
{ 'code' : None|ATT|..., 'assidu' : 0|1, 'event_date' : , compense_formsemestre_id }
Si état défaillant, force le code a DEF
"""
if self.get_etud_etat(etudid) == DEF:
return {
"code": DEF,
"assidu": False,
"event_date": "",
"compense_formsemestre_id": None,
}
else:
return {
"code": ATT, # XXX TODO
"assidu": True, # XXX TODO
"event_date": "",
"compense_formsemestre_id": None,
}
def get_etud_etat(self, etudid: int) -> str:
"Etat de l'etudiant: 'I', 'D', DEF ou '' (si pas connu dans ce semestre)"
ins = self.formsemestre.etuds_inscriptions.get(etudid, None)
if ins is None:
return ""
return ins.etat
def get_etud_moy_gen(self, etudid): # -> float | str
"""Moyenne générale de cet etudiant dans ce semestre.
Prend en compte les UE capitalisées. (TODO)
Si apc, moyenne indicative.
Si pas de notes: 'NA'
"""
return self.etud_moy_gen[etudid]
def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float:
"""La moyenne de l'étudiant dans le moduleimpl
En APC, il s'agira d'une moyenne indicative sans valeur.
Result: valeur float (peut être naN) ou chaîne "NI" (non inscrit ou DEM)
"""
raise NotImplementedError() # virtual method
def get_etud_ue_status(self, etudid: int, ue_id: int):
return {
"cur_moy_ue": self.results.etud_moy_ue[ue_id][etudid],
"is_capitalized": False, # XXX TODO
}
def get_etud_rang(self, etudid: int):
return self.etud_moy_gen_ranks.get(etudid, 99999) # XXX
def get_etud_rang_group(self, etudid: int, group_id: int):
return (None, 0) # XXX unimplemented TODO
def get_evals_in_mod(self, moduleimpl_id: int) -> list[dict]:
"liste des évaluations valides dans un module"
mi = ModuleImpl.query.get(moduleimpl_id)
evals_results = []
for e in mi.evaluations:
d = e.to_dict()
d["heure_debut"] = e.heure_debut # datetime.time
d["heure_fin"] = e.heure_fin
d["jour"] = e.jour # datetime
d["notes"] = {
etud.id: {
"etudid": etud.id,
"value": self.results.modimpls_evals_notes[e.moduleimpl_id][e.id][
etud.id
],
}
for etud in self.results.etuds
}
evals_results.append(d)
return evals_results
def get_moduleimpls_attente(self):
return [] # XXX TODO
def get_mod_stats(self, moduleimpl_id):
return {
"moy": "-",
"max": "-",
"min": "-",
"nb_notes": "-",
"nb_missing": "-",
"nb_valid_evals": "-",
}
def get_nom_short(self, etudid):
"formatte nom d'un etud (pour table recap)"
etud = self.identdict[etudid]
return (
(etud["nom_usuel"] or etud["nom"]).upper()
+ " "
+ etud["prenom"].capitalize()[:2]
+ "."
)
@cached_property
def T(self):
return self.get_table_moyennes_triees()
def get_table_moyennes_triees(self) -> list:
"""Result: liste de tuples
moy_gen, moy_ue_0, ..., moy_ue_n, moy_mod1, ..., moy_mod_n, etudid
"""
table_moyennes = []
etuds_inscriptions = self.formsemestre.etuds_inscriptions
for etudid in etuds_inscriptions:
moy_gen = self.etud_moy_gen.get(etudid, False)
if moy_gen is False:
# pas de moyenne: démissionnaire ou def
t = ["-"] + ["0.00"] * len(self.ues) + ["NI"] * len(self.modimpls)
else:
moy_ues = self.etud_moy_ue.loc[etudid]
t = [moy_gen] + list(moy_ues)
# TODO UE capitalisées: ne pas afficher moyennes modules
for modimpl in self.modimpls:
val = self.get_etud_mod_moy(modimpl.id, etudid)
t.append(val)
t.append(etudid)
table_moyennes.append(t)
# tri par moyennes décroissantes,
# en laissant les démissionnaires à la fin, par ordre alphabetique
etuds = [ins.etud for ins in etuds_inscriptions.values()]
etuds.sort(key=lambda e: e.sort_key)
self._rang_alpha = {e.id: i for i, e in enumerate(etuds)}
table_moyennes.sort(key=self._row_key)
return table_moyennes
def _row_key(self, x):
"""clé de tri par moyennes décroissantes,
en laissant les demissionnaires à la fin, par ordre alphabetique.
(moy_gen, rang_alpha)
"""
try:
moy = -float(x[0])
except (ValueError, TypeError):
moy = 1000.0
return (moy, self._rang_alpha[x[-1]])
@cached_property
def identdict(self) -> dict:
"""{ etudid : etud_dict } pour tous les inscrits au semestre"""
return {
ins.etud.id: ins.etud.to_dict_scodoc7()
for ins in self.formsemestre.inscriptions
}
klass = (
ResultatsSemestreBUT
if formsemestre.formation.is_apc()
else ResultatsSemestreClassic
)
return klass(formsemestre)

View File

@ -2,7 +2,7 @@ from app import db
class Entreprise(db.Model):
__tablename__ = "entreprises"
__tablename__ = "are_entreprises"
id = db.Column(db.Integer, primary_key=True)
siret = db.Column(db.Text)
nom = db.Column(db.Text)
@ -35,10 +35,10 @@ class Entreprise(db.Model):
class EntrepriseContact(db.Model):
__tablename__ = "entreprise_contact"
__tablename__ = "are_entreprise_contact"
id = db.Column(db.Integer, primary_key=True)
entreprise_id = db.Column(
db.Integer, db.ForeignKey("entreprises.id", ondelete="cascade")
db.Integer, db.ForeignKey("are_entreprises.id", ondelete="cascade")
)
nom = db.Column(db.Text)
prenom = db.Column(db.Text)
@ -76,10 +76,10 @@ class EntrepriseContact(db.Model):
class EntrepriseOffre(db.Model):
__tablename__ = "entreprise_offre"
__tablename__ = "are_entreprise_offre"
id = db.Column(db.Integer, primary_key=True)
entreprise_id = db.Column(
db.Integer, db.ForeignKey("entreprises.id", ondelete="cascade")
db.Integer, db.ForeignKey("are_entreprises.id", ondelete="cascade")
)
date_ajout = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
intitule = db.Column(db.Text)
@ -99,7 +99,7 @@ class EntrepriseOffre(db.Model):
class EntrepriseLog(db.Model):
__tablename__ = "entreprise_log"
__tablename__ = "are_entreprise_log"
id = db.Column(db.Integer, primary_key=True)
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
authenticated_user = db.Column(db.Text)
@ -108,9 +108,9 @@ class EntrepriseLog(db.Model):
class EntrepriseEtudiant(db.Model):
__tablename__ = "entreprise_etudiant"
__tablename__ = "are_entreprise_etudiant"
id = db.Column(db.Integer, primary_key=True)
entreprise_id = db.Column(db.Integer, db.ForeignKey("entreprises.id"))
entreprise_id = db.Column(db.Integer, db.ForeignKey("are_entreprises.id"))
etudid = db.Column(db.Integer)
type_offre = db.Column(db.Text)
date_debut = db.Column(db.Date)
@ -120,18 +120,18 @@ class EntrepriseEtudiant(db.Model):
class EntrepriseEnvoiOffre(db.Model):
__tablename__ = "entreprise_envoi_offre"
__tablename__ = "are_entreprise_envoi_offre"
id = db.Column(db.Integer, primary_key=True)
sender_id = db.Column(db.Integer, db.ForeignKey("user.id"))
receiver_id = db.Column(db.Integer, db.ForeignKey("user.id"))
offre_id = db.Column(db.Integer, db.ForeignKey("entreprise_offre.id"))
offre_id = db.Column(db.Integer, db.ForeignKey("are_entreprise_offre.id"))
date_envoi = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
class EntrepriseEnvoiOffreEtudiant(db.Model):
__tablename__ = "entreprise_envoi_offre_etudiant"
__tablename__ = "are_entreprise_envoi_offre_etudiant"
id = db.Column(db.Integer, primary_key=True)
sender_id = db.Column(db.Integer, db.ForeignKey("user.id"))
receiver_id = db.Column(db.Integer, db.ForeignKey("identite.id"))
offre_id = db.Column(db.Integer, db.ForeignKey("entreprise_offre.id"))
offre_id = db.Column(db.Integer, db.ForeignKey("are_entreprise_offre.id"))
date_envoi = db.Column(db.DateTime(timezone=True), server_default=db.func.now())

View File

@ -245,7 +245,7 @@ class DeptForm(FlaskForm):
def _make_dept_id_name():
"""Cette section assute que tous les départements sont traités (y compris ceux qu'ont pas de logo au départ)
"""Cette section assure que tous les départements sont traités (y compris ceux qu'ont pas de logo au départ)
et détermine l'ordre d'affichage des DeptForm (GLOBAL d'abord, puis par ordre alpha de nom de département)
-> [ (None, None), (dept_id, dept_name)... ]"""
depts = [(None, GLOBAL)]

View File

@ -31,16 +31,10 @@ Formulaires création département
from flask import flash, url_for, redirect, render_template
from flask_wtf import FlaskForm
from wtforms import SelectField, SubmitField, FormField, validators, FieldList
from wtforms.fields.simple import StringField, HiddenField
from wtforms import SubmitField, validators
from wtforms.fields.simple import StringField, BooleanField
from app import AccessDenied
from app.models import Departement
from app.models import ScoPreference
from app.models import SHORT_STR_LEN
from app.scodoc import sco_utils as scu
from flask_login import current_user
class CreateDeptForm(FlaskForm):
@ -60,5 +54,10 @@ class CreateDeptForm(FlaskForm):
validators.DataRequired("acronyme du département requis"),
],
)
# description = StringField(label="Description")
visible = BooleanField(
"Visible sur page d'accueil",
default=True,
)
submit = SubmitField("Valider")
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})

View File

@ -9,11 +9,24 @@ from datetime import datetime
from enum import unique
from typing import Any
from sqlalchemy.orm import class_mapper
import sqlalchemy
from app import db
from app.scodoc.sco_utils import ModuleType
# from https://stackoverflow.com/questions/2537471/method-of-iterating-over-sqlalchemy-models-defined-columns
def attribute_names(cls):
"liste ids (noms de colonnes) d'un modèle"
return [
prop.key
for prop in class_mapper(cls).iterate_properties
if isinstance(prop, sqlalchemy.orm.ColumnProperty)
]
class XMLModel:
_xml_attribs = {} # to be overloaded
id = "_"
@ -24,21 +37,31 @@ class XMLModel:
and renamed for our models.
The mapping is specified by the _xml_attribs
attribute in each model class.
Keep only attributes corresponding to columns in our model:
other XML attributes are simply ignored.
"""
return {cls._xml_attribs.get(k, k): v for (k, v) in args.items()}
columns = attribute_names(cls)
renamed_attributes = {cls._xml_attribs.get(k, k): v for (k, v) in args.items()}
return {k: renamed_attributes[k] for k in renamed_attributes if k in columns}
def __repr__(self):
return f'<{self.__class__.__name__} {self.id} "{self.titre if hasattr(self, "titre") else ""}">'
class ApcReferentielCompetences(db.Model, XMLModel):
"Référentiel de compétence d'une spécialité"
id = db.Column(db.Integer, primary_key=True)
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
annexe = db.Column(db.Text())
specialite = db.Column(db.Text())
specialite_long = db.Column(db.Text())
type_titre = db.Column(db.Text())
type_structure = db.Column(db.Text())
type_departement = db.Column(db.Text()) # "secondaire", "tertiaire"
version_orebut = db.Column(db.Text())
_xml_attribs = { # Orébut xml attrib : attribute
"type": "type_titre",
"version": "version_orebut",
}
# ScoDoc specific fields:
scodoc_date_loaded = db.Column(db.DateTime, default=datetime.utcnow)
@ -64,9 +87,13 @@ class ApcReferentielCompetences(db.Model, XMLModel):
"""
return {
"dept_id": self.dept_id,
"annexe": self.annexe,
"specialite": self.specialite,
"specialite_long": self.specialite_long,
"type_structure": self.type_structure,
"type_departement": self.type_departement,
"type_titre": self.type_titre,
"version_orebut": self.version_orebut,
"scodoc_date_loaded": self.scodoc_date_loaded.isoformat() + "Z"
if self.scodoc_date_loaded
else "",
@ -77,23 +104,20 @@ class ApcReferentielCompetences(db.Model, XMLModel):
class ApcCompetence(db.Model, XMLModel):
__table_args__ = (
# les compétences dans Orébut sont identifiées par leur "titre"
# unique au sein d'un référentiel:
db.UniqueConstraint(
"referentiel_id", "titre", name="apc_competence_referentiel_id_titre_key"
),
)
"Compétence"
id = db.Column(db.Integer, primary_key=True)
referentiel_id = db.Column(
db.Integer, db.ForeignKey("apc_referentiel_competences.id"), nullable=False
)
# les compétences dans Orébut sont identifiées par leur id unique
id_orebut = db.Column(db.Text(), nullable=True, index=True, unique=True)
titre = db.Column(db.Text(), nullable=False, index=True)
titre_long = db.Column(db.Text())
couleur = db.Column(db.Text())
numero = db.Column(db.Integer) # ordre de présentation
_xml_attribs = { # xml_attrib : attribute
"name": "titre",
"id": "id_orebut",
"nom_court": "titre", # was name
"libelle_long": "titre_long",
}
situations = db.relationship(
@ -117,6 +141,7 @@ class ApcCompetence(db.Model, XMLModel):
def to_dict(self):
return {
"id_orebut": self.id_orebut,
"titre": self.titre,
"titre_long": self.titre_long,
"couleur": self.couleur,
@ -246,7 +271,10 @@ class ApcAnneeParcours(db.Model, XMLModel):
return {
"ordre": self.ordre,
"competences": {
x.competence.titre: {"niveau": x.niveau}
x.competence.titre: {
"niveau": x.niveau,
"id_orebut": x.competence.id_orebut,
}
for x in self.niveaux_competences
},
}

View File

@ -12,8 +12,10 @@ class Departement(db.Model):
"""Un département ScoDoc"""
id = db.Column(db.Integer, primary_key=True)
acronym = db.Column(db.String(SHORT_STR_LEN), nullable=False, index=True)
description = db.Column(db.Text())
acronym = db.Column(
db.String(SHORT_STR_LEN), nullable=False, index=True
) # ne change jamais, voir la pref. DeptName
description = db.Column(db.Text()) # pas utilisé par ScoDoc : voir DeptFullName
date_creation = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
visible = db.Column(
db.Boolean(), nullable=False, default=True, server_default="true"
@ -49,11 +51,11 @@ class Departement(db.Model):
return dept
def create_dept(acronym: str) -> Departement:
def create_dept(acronym: str, visible=True) -> Departement:
"Create new departement"
from app.models import ScoPreference
departement = Departement(acronym=acronym)
departement = Departement(acronym=acronym, visible=visible)
p1 = ScoPreference(name="DeptName", value=acronym, departement=departement)
db.session.add(p1)
db.session.add(departement)

View File

@ -12,6 +12,7 @@ from app import db
from app import models
from app.scodoc import notesdb as ndb
from app.scodoc.sco_bac import Baccalaureat
class Identite(db.Model):
@ -42,14 +43,16 @@ class Identite(db.Model):
boursier = db.Column(db.Boolean()) # True si boursier ('O' en ScoDoc7)
photo_filename = db.Column(db.Text())
# Codes INE et NIP pas unique car le meme etud peut etre ds plusieurs dept
code_nip = db.Column(db.Text())
code_ine = db.Column(db.Text())
code_nip = db.Column(db.Text(), index=True)
code_ine = db.Column(db.Text(), index=True)
# 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)
#
adresses = db.relationship("Adresse", lazy="dynamic", backref="etud")
billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic")
# one-to-one relation:
admission = db.relationship("Admission", backref="identite", lazy="dynamic")
def __repr__(self):
return f"<Etud {self.id} {self.nom} {self.prenom}>"
@ -294,6 +297,10 @@ class Admission(db.Model):
# classement (1..Ngr) par le jury dans le groupe APB
apb_classement_gr = db.Column(db.Integer)
def get_bac(self) -> Baccalaureat:
"Le bac. utiliser bac.abbrev() pour avoir une chaine de caractères."
return Baccalaureat(self.bac, specialite=self.specialite)
# Suivi scolarité / débouchés
class ItemSuivi(db.Model):

View File

@ -4,6 +4,8 @@
from app import db
from app.comp import df_cache
from app.models import SHORT_STR_LEN
from app.models.modules import Module
from app.models.ues import UniteEns
from app.scodoc import notesdb as ndb
from app.scodoc import sco_cache
from app.scodoc import sco_codes_parcours
@ -97,19 +99,24 @@ class Formation(db.Model):
for sem in self.formsemestres:
sco_cache.invalidate_formsemestre(formsemestre_id=sem.id)
def force_semestre_modules_aux_ues(self) -> None:
def sanitize_old_formation(self) -> None:
"""
Affecte à chaque module de cette formation le semestre de son UE de rattachement,
Corrige si nécessaire certains champs issus d'anciennes versions de ScoDoc:
- affecte à chaque module de cette formation le semestre de son UE de rattachement,
si elle en a une.
- si le module_type n'est pas renseigné, le met à STANDARD.
Devrait être appelé lorsqu'on change le type de formation vers le BUT, et aussi
lorsqu'on change le semestre d'une UE BUT.
Utile pour la migration des anciennes formations vers le BUT.
Invalide les caches coefs/poids.
En cas de changement, invalide les caches coefs/poids.
"""
if not self.is_apc():
return
change = False
for mod in self.modules:
# --- Indices de semestres:
if (
mod.ue.semestre_idx is not None
and mod.ue.semestre_idx > 0
@ -118,6 +125,21 @@ class Formation(db.Model):
mod.semestre_id = mod.ue.semestre_idx
db.session.add(mod)
change = True
# --- Types de modules
if mod.module_type is None:
mod.module_type = scu.ModuleType.STANDARD
db.session.add(mod)
change = True
# --- Numéros de modules
if Module.query.filter_by(formation_id=self.id, numero=None).count() > 0:
scu.objects_renumber(db, self.modules.all())
# --- Types d'UE (avant de rendre le type non nullable)
ues_sans_type = UniteEns.query.filter_by(formation_id=self.id, type=None)
if ues_sans_type.count() > 0:
for ue in ues_sans_type:
ue.type = 0
db.session.add(ue)
db.session.commit()
if change:
self.invalidate_module_coefs()

View File

@ -8,6 +8,7 @@ from functools import cached_property
import flask_sqlalchemy
from app import db
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
@ -116,11 +117,18 @@ class FormSemestre(db.Model):
d.pop("_sa_instance_state", None)
# ScoDoc7 output_formators: (backward compat)
d["formsemestre_id"] = self.id
d["date_debut"] = (
self.date_debut.strftime("%d/%m/%Y") if self.date_debut else ""
)
d["date_fin"] = self.date_fin.strftime("%d/%m/%Y") if self.date_fin else ""
if self.date_debut:
d["date_debut"] = self.date_debut.strftime("%d/%m/%Y")
d["date_debut_iso"] = self.date_debut.isoformat()
else:
d["date_debut"] = d["date_debut_iso"] = ""
if self.date_fin:
d["date_fin"] = self.date_fin.strftime("%d/%m/%Y")
d["date_fin_iso"] = self.date_fin.isoformat()
else:
d["date_fin"] = d["date_fin_iso"] = ""
d["responsables"] = [u.id for u in self.responsables]
return d
def query_ues(self, with_sport=False) -> flask_sqlalchemy.BaseQuery:
@ -158,6 +166,24 @@ class FormSemestre(db.Model):
"""
return (self.date_debut <= date_debut) and (date_fin <= self.date_fin)
def est_sur_une_annee(self):
"""Test si sem est entièrement sur la même année scolaire.
(ce n'est pas obligatoire mais si ce n'est pas le
cas les exports Apogée risquent de mal fonctionner)
Pivot au 1er août.
"""
if self.date_debut > self.date_fin:
log(f"Warning: semestre {self.id} begins after ending !")
annee_debut = self.date_debut.year
if self.date_debut.month < 8: # août
# considere que debut sur l'anne scolaire precedente
annee_debut -= 1
annee_fin = self.date_fin.year
if self.date_fin.month < 9:
# 9 (sept) pour autoriser un début en sept et une fin en aout
annee_fin -= 1
return annee_debut == annee_fin
def est_decale(self):
"""Vrai si semestre "décalé"
c'est à dire semestres impairs commençant entre janvier et juin
@ -252,18 +278,19 @@ class FormSemestre(db.Model):
etudid, self.date_debut.isoformat(), self.date_fin.isoformat()
)
def get_inscrits(self, include_dem=False) -> list[Identite]:
def get_inscrits(self, include_demdef=False) -> list[Identite]:
"""Liste des étudiants inscrits à ce semestre
Si all, tous les étudiants, avec les démissionnaires.
Si include_demdef, tous les étudiants, avec les démissionnaires
et défaillants.
"""
if include_dem:
if include_demdef:
return [ins.etud for ins in self.inscriptions]
else:
return [ins.etud for ins in self.inscriptions if ins.etat == scu.INSCRIT]
@cached_property
def etuds_inscriptions(self) -> dict:
"""Map { etudid : inscription }"""
"""Map { etudid : inscription } (incluant DEM et DEF)"""
return {ins.etud.id: ins for ins in self.inscriptions}

View File

@ -44,6 +44,9 @@ class ModuleImpl(db.Model):
def __init__(self, **kwargs):
super(ModuleImpl, self).__init__(**kwargs)
def __repr__(self):
return f"<{self.__class__.__name__} {self.id} module={repr(self.module)}>"
def get_evaluations_poids(self) -> pd.DataFrame:
"""Les poids des évaluations vers les UE (accès via cache)"""
evaluations_poids = df_cache.EvaluationsPoidsCache.get(self.id)

View File

@ -49,9 +49,7 @@ class Module(db.Model):
super(Module, self).__init__(**kwargs)
def __repr__(self):
return (
f"<Module{ModuleType(self.module_type).name} id={self.id} code={self.code}>"
)
return f"<Module{ModuleType(self.module_type or ModuleType.STANDARD).name} id={self.id} code={self.code}>"
def to_dict(self):
e = dict(self.__dict__)
@ -131,9 +129,27 @@ class Module(db.Model):
# à redéfinir les relationships...
return sorted(self.ue_coefs, key=lambda x: x.ue.numero)
def ue_coefs_descr(self):
"""List of tuples [ (ue_acronyme, coef) ]"""
return [(c.ue.acronyme, c.coef) for c in self.get_ue_coefs_sorted()]
def ue_coefs_list(self, include_zeros=True):
"""Liste des coefs vers les UE (pour les modules APC).
Si include_zeros, liste aussi les UE sans coef (donc nul) de ce semestre.
Result: List of tuples [ (ue, coef) ]
"""
if not self.is_apc():
return []
if include_zeros:
# Toutes les UE du même semestre:
ues_semestre = (
self.formation.ues.filter_by(semestre_idx=self.ue.semestre_idx)
.order_by(UniteEns.numero)
.all()
)
coefs_dict = self.get_ue_coef_dict()
coefs_list = []
for ue in ues_semestre:
coefs_list.append((ue, coefs_dict.get(ue.id, 0.0)))
return coefs_list
# Liste seulement les coefs définis:
return [(c.ue, c.coef) for c in self.get_ue_coefs_sorted()]
class ModuleUECoef(db.Model):

View File

@ -46,7 +46,10 @@ class UniteEns(db.Model):
modules = db.relationship("Module", lazy="dynamic", backref="ue")
def __repr__(self):
return f"<{self.__class__.__name__}(id={self.id}, formation_id={self.formation_id}, acronyme='{self.acronyme}')>"
return f"""<{self.__class__.__name__}(id={self.id}, formation_id={
self.formation_id}, acronyme='{self.acronyme}', semestre_idx={
self.semestre_idx} {
'EXTERNE' if self.is_external else ''})>"""
def to_dict(self):
"""as a dict, with the same conversions as in ScoDoc7"""

View File

@ -9,6 +9,12 @@
v 1.3 (python3)
"""
import html
import re
# re validant dd/mm/yyyy
DMY_REGEXP = re.compile(
r"^(?:(?:31(\/|-|\.)(?:0?[13578]|1[02]))\1|(?:(?:29|30)(\/|-|\.)(?:0?[13-9]|1[0-2])\2))(?:(?:1[6-9]|[2-9]\d)?\d{2})$|^(?:29(\/|-|\.)0?2\3(?:(?:(?:1[6-9]|[2-9]\d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00))))$|^(?:0?[1-9]|1\d|2[0-8])(\/|-|\.)(?:(?:0?[1-9])|(?:1[0-2]))\4(?:(?:1[6-9]|[2-9]\d)?\d{2})$"
)
def TrivialFormulator(
@ -66,8 +72,8 @@ def TrivialFormulator(
HTML elements:
input_type : 'text', 'textarea', 'password',
'radio', 'menu', 'checkbox',
'hidden', 'separator', 'file', 'date', 'boolcheckbox',
'text_suggest'
'hidden', 'separator', 'file', 'date', 'datedmy' (avec validation),
'boolcheckbox', 'text_suggest'
(default text)
size : text field width
rows, cols: textarea geometry
@ -243,6 +249,8 @@ class TF(object):
"Le champ '%s' doit être renseigné" % descr.get("title", field)
)
ok = 0
elif val == "" or val == None:
continue # allowed empty field, skip
# type
typ = descr.get("type", "string")
if val != "" and val != None:
@ -300,6 +308,10 @@ class TF(object):
if not descr["validator"](val, field):
msg.append("valeur invalide (%s) pour le champ '%s'" % (val, field))
ok = 0
elif descr.get("input_type") == "datedmy":
if not DMY_REGEXP.match(val):
msg.append("valeur invalide (%s) pour la date '%s'" % (val, field))
ok = 0
# boolean checkbox
if descr.get("input_type", None) == "boolcheckbox":
if int(val):
@ -564,7 +576,9 @@ class TF(object):
'<input type="file" name="%s" size="%s" value="%s" %s>'
% (field, size, values[field], attribs)
)
elif input_type == "date": # JavaScript widget for date input
elif (
input_type == "date" or input_type == "datedmy"
): # JavaScript widget for date input
lem.append(
'<input type="text" name="%s" size="10" value="%s" class="datepicker">'
% (field, values[field])

View File

@ -28,6 +28,44 @@
from operator import mul
import pprint
"""
La fonction bonus_sport reçoit:
- notes_sport: la liste des notes des modules de sport et culture (une note par module de l'UE de type sport/culture);
- coefs: un coef (float) pondérant chaque note (la plupart des bonus les ignorent);
- infos: dictionnaire avec des données pouvant être utilisées pour les calculs.
Ces données dépendent du type de formation.
infos = {
"moy" : la moyenne générale (float). 0. en BUT.
"sem" : {
"date_debut_iso" : "2010-08-01", # date de début de semestre
}
"moy_ues": {
ue_id : { # ue_status
"is_capitalized" : True|False,
"moy" : float, # moyenne d'UE prise en compte (peut-être capitalisée)
"sum_coefs": float, # > 0 si UE avec la moyenne calculée
"cur_moy_ue": float, # moyenne de l'UE (sans capitalisation))
}
}
}
Les notes passées sont:
- pour les formations classiques, la moyenne dans le module, calculée comme d'habitude
(moyenne pondérée des notes d'évaluations);
- pour le BUT: pareil, *en ignorant* les éventuels poids des évaluations. Le coefficient
de l'évaluation est pris en compte, mais pas les poids vers les UE.
Pour modifier les moyennes d'UE:
- modifier infos["moy_ues"][ue_id][["cur_moy_ue"]
et, seulement si l'UE n'est pas capitalisée, infos["moy_ues"][ue_id][["moy"]/
La valeur retournée est:
- formations classiques: ajoutée à la moyenne générale
- BUT: valeur multipliée par la somme des coefs modules sport ajoutée à chaque UE.
"""
def bonus_iutv(notes_sport, coefs, infos=None):
"""Calcul bonus modules optionels (sport, culture), règle IUT Villetaneuse
@ -39,6 +77,7 @@ def bonus_iutv(notes_sport, coefs, infos=None):
optionnelles sont cumulés et 5% de ces points cumulés s'ajoutent à
la moyenne générale du semestre déjà obtenue par l'étudiant.
"""
# breakpoint()
bonus = sum([(x - 10) / 20.0 for x in notes_sport if x > 10])
return bonus
@ -416,6 +455,20 @@ def bonus_iutbeziers(notes_sport, coefs, infos=None):
return bonus
def bonus_iutlr(notes_sport, coefs, infos=None):
"""Calcul bonus modules optionels (sport, culture), règle IUT 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.1 et 20 : ajout de 1% de cette note sur la moyenne générale du semestre
"""
# les coefs sont ignorés
# une seule note
note_sport = notes_sport[0]
if note_sport <= 10:
return 0
bonus = note_sport * 0.01 # 1%
return bonus
def bonus_demo(notes_sport, coefs, infos=None):
"""Fausse fonction "bonus" pour afficher les informations disponibles
et aider les développeurs.

View File

@ -58,6 +58,7 @@ from app.scodoc import sco_utils as scu
from app.scodoc import sco_excel
from app.scodoc import sco_pdf
from app.scodoc import sco_xml
from app.scodoc.sco_exceptions import ScoPDFFormatError
from app.scodoc.sco_pdf import SU
from app import log
@ -539,17 +540,18 @@ class GenTable(object):
#
# titles = ["<para><b>%s</b></para>" % x for x in self.get_titles_list()]
pdf_style_list = []
Pt = [
[Paragraph(SU(str(x)), CellStyle) for x in line]
for line in (
self.get_data_list(
pdf_mode=True,
pdf_style_list=pdf_style_list,
with_titles=True,
omit_hidden_lines=True,
)
)
]
data_list = self.get_data_list(
pdf_mode=True,
pdf_style_list=pdf_style_list,
with_titles=True,
omit_hidden_lines=True,
)
try:
Pt = [
[Paragraph(SU(str(x)), CellStyle) for x in line] for line in data_list
]
except ValueError as exc:
raise ScoPDFFormatError(str(exc)) from exc
pdf_style_list += self.pdf_table_style
T = Table(Pt, repeatRows=1, colWidths=self.pdf_col_widths, style=pdf_style_list)

View File

@ -313,7 +313,7 @@ def sco_footer():
def html_sem_header(
title, sem=None, with_page_header=True, with_h2=True, page_title=None, **args
title, with_page_header=True, with_h2=True, page_title=None, **args
):
"Titre d'une page semestre avec lien vers tableau de bord"
# sem now unused and thus optional...

View File

@ -35,13 +35,14 @@ from flask_login import current_user
import app.scodoc.sco_utils as scu
from app.scodoc import sco_preferences
from app.scodoc.sco_permissions import Permission
from sco_version import SCOVERSION
def sidebar_common():
"partie commune à toutes les sidebar"
home_link = url_for("scodoc.index", scodoc_dept=g.scodoc_dept)
H = [
f"""<a class="scodoc_title" href="{home_link}">ScoDoc 9.1</a><br>
f"""<a class="scodoc_title" href="{home_link}">ScoDoc {SCOVERSION}</a><br>
<a href="{home_link}" class="sidebar">Accueil</a> <br>
<div id="authuser"><a id="authuserlink" href="{
url_for("users.user_info_page",

View File

@ -171,6 +171,7 @@ class NotesTable:
def __init__(self, formsemestre_id):
log(f"NotesTable( formsemestre_id={formsemestre_id} )")
# raise NotImplementedError() # XXX
if not formsemestre_id:
raise ValueError("invalid formsemestre_id (%s)" % formsemestre_id)
self.formsemestre_id = formsemestre_id
@ -409,7 +410,7 @@ class NotesTable:
return ""
def get_etud_etat_html(self, etudid):
etat = self.inscrdict[etudid]["etat"]
if etat == "I":
return ""
elif etat == "D":

View File

@ -108,13 +108,14 @@ def apo_compare_csv(A_file, B_file, autodetect=True):
def _load_apo_data(csvfile, autodetect=True):
"Read data from request variable and build ApoData"
data = csvfile.read()
data_b = csvfile.read()
if autodetect:
data, message = sco_apogee_csv.fix_data_encoding(data)
data_b, message = sco_apogee_csv.fix_data_encoding(data_b)
if message:
log("apo_compare_csv: %s" % message)
if not data:
if not data_b:
raise ScoValueError("apo_compare_csv: no data")
data = data_b.decode(sco_apogee_csv.APO_INPUT_ENCODING)
apo_data = sco_apogee_csv.ApoData(data, orig_filename=csvfile.filename)
return apo_data

View File

@ -173,8 +173,10 @@ def guess_data_encoding(text, threshold=0.6):
def fix_data_encoding(
text, default_source_encoding=APO_INPUT_ENCODING, dest_encoding=APO_INPUT_ENCODING
):
text: bytes,
default_source_encoding=APO_INPUT_ENCODING,
dest_encoding=APO_INPUT_ENCODING,
) -> bytes:
"""Try to ensure that text is using dest_encoding
returns converted text, and a message describing the conversion.
"""
@ -200,7 +202,7 @@ def fix_data_encoding(
class StringIOFileLineWrapper(object):
def __init__(self, data):
def __init__(self, data: str):
self.f = io.StringIO(data)
self.lineno = 0
@ -655,7 +657,7 @@ class ApoEtud(dict):
class ApoData(object):
def __init__(
self,
data,
data: str,
periode=None,
export_res_etape=True,
export_res_sem=True,
@ -693,7 +695,7 @@ class ApoData(object):
"<h3>Erreur lecture du fichier Apogée <tt>%s</tt></h3><p>" % filename
+ e.args[0]
+ "</p>"
)
) from e
self.etape_apogee = self.get_etape_apogee() # 'V1RT'
self.vdi_apogee = self.get_vdi_apogee() # '111'
self.etape = ApoEtapeVDI(etape=self.etape_apogee, vdi=self.vdi_apogee)
@ -760,7 +762,6 @@ class ApoData(object):
def read_csv(self, data: str):
if not data:
raise ScoFormatError("Fichier Apogée vide !")
f = StringIOFileLineWrapper(data) # pour traiter comme un fichier
# check that we are at the begining of Apogee CSV
line = f.readline().strip()
@ -768,7 +769,10 @@ class ApoData(object):
raise ScoFormatError("format incorrect: pas de XX-APO_TITRES-XX")
# 1-- En-tête: du début jusqu'à la balise XX-APO_VALEURS-XX
idx = data.index("XX-APO_VALEURS-XX")
try:
idx = data.index("XX-APO_VALEURS-XX")
except ValueError as exc:
raise ScoFormatError("format incorrect: pas de XX-APO_VALEURS-XX") from exc
self.header = data[:idx]
# 2-- Titres:
@ -1178,7 +1182,7 @@ def nar_etuds_table(apo_data, NAR_Etuds):
def export_csv_to_apogee(
apo_csv_data,
apo_csv_data: str,
periode=None,
dest_zip=None,
export_res_etape=True,

View File

@ -405,7 +405,6 @@ def formsemestre_archive(formsemestre_id, group_ids=[]):
H = [
html_sco_header.html_sem_header(
"Archiver les PV et résultats du semestre",
sem=sem,
javascripts=sco_groups_view.JAVASCRIPTS,
cssstyles=sco_groups_view.CSSSTYLES,
init_qtip=True,
@ -524,7 +523,7 @@ def formsemestre_list_archives(formsemestre_id):
}
L.append(a)
H = [html_sco_header.html_sem_header("Archive des PV et résultats ", sem)]
H = [html_sco_header.html_sem_header("Archive des PV et résultats ")]
if not L:
H.append("<p>aucune archive enregistrée</p>")
else:

View File

@ -130,7 +130,7 @@ BACS_SSP = {(t[0], t[1]): t[2:] for t in _BACS}
BACS_S = {t[0]: t[2:] for t in _BACS}
class Baccalaureat(object):
class Baccalaureat:
def __init__(self, bac, specialite=""):
self.bac = bac
self.specialite = specialite

View File

@ -48,6 +48,9 @@ import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
import app.scodoc.notesdb as ndb
from app import log
from app.comp import res_sem
from app.comp.res_common import NotesTableCompat
from app.models import FormSemestre
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
from app.scodoc import html_sco_header
@ -136,7 +139,9 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
raise ValueError("invalid version code !")
prefs = sco_preferences.SemPreferences(formsemestre_id)
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > toutes notes
# nt = sco_cache.NotesTableCache.get(formsemestre_id) # > toutes notes
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_result(formsemestre)
if not nt.get_etud_etat(etudid):
raise ScoValueError("Etudiant non inscrit à ce semestre")
I = scu.DictDefault(defaultvalue="")
@ -191,7 +196,9 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
I["decision_sem"] = ""
I.update(infos)
I["etud_etat_html"] = nt.get_etud_etat_html(etudid)
I["etud_etat_html"] = _get_etud_etat_html(
formsemestre.etuds_inscriptions[etudid].etat
)
I["etud_etat"] = nt.get_etud_etat(etudid)
I["filigranne"] = ""
I["demission"] = ""
@ -261,17 +268,18 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
# notes en attente dans ce semestre
rang = scu.RANG_ATTENTE_STR
rang_gr = scu.DictDefault(defaultvalue=scu.RANG_ATTENTE_STR)
inscriptions_counts = nt.get_inscriptions_counts()
I["rang"] = rang
I["rang_gr"] = rang_gr
I["gr_name"] = gr_name
I["ninscrits_gr"] = ninscrits_gr
I["nbetuds"] = len(nt.etud_moy_gen_ranks)
I["nb_demissions"] = nt.nb_demissions
I["nb_defaillants"] = nt.nb_defaillants
I["nb_demissions"] = inscriptions_counts[scu.DEMISSION]
I["nb_defaillants"] = inscriptions_counts[scu.DEF]
if prefs["bul_show_rangs"]:
I["rang_nt"] = "%s / %d" % (
rang,
I["nbetuds"] - nt.nb_demissions - nt.nb_defaillants,
inscriptions_counts[scu.INSCRIT],
)
I["rang_txt"] = "Rang " + I["rang_nt"]
else:
@ -379,7 +387,8 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
I["ues"].append(u) # ne montre pas les UE si non inscrit
# Accès par matieres
I["matieres_modules"].update(_sort_mod_by_matiere(modules, nt, etudid))
# voir si on supporte encore cela en #sco92 XXX
# I["matieres_modules"].update(_sort_mod_by_matiere(modules, nt, etudid))
#
C = make_context_dict(I["sem"], I["etud"])
@ -389,6 +398,18 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
return C
def _get_etud_etat_html(etat: str) -> str:
"""chaine html représentant l'état (backward compat sco7)"""
if etat == scu.INSCRIT: # "I"
return ""
elif etat == scu.DEMISSION: # "D"
return ' <font color="red">(DEMISSIONNAIRE)</font> '
elif etat == scu.DEF: # "DEF"
return ' <font color="red">(DEFAILLANT)</font> '
else:
return ' <font color="red">(%s)</font> ' % etat
def _sort_mod_by_matiere(modlist, nt, etudid):
matmod = {} # { matiere_id : [] }
for mod in modlist:

View File

@ -93,9 +93,9 @@ def make_xml_formsemestre_bulletinetud(
)
if (not sem["bul_hide_xml"]) or force_publishing:
published = "1"
published = 1
else:
published = "0"
published = 0
if xml_nodate:
docdate = ""
else:
@ -105,7 +105,7 @@ def make_xml_formsemestre_bulletinetud(
"etudid": str(etudid),
"formsemestre_id": str(formsemestre_id),
"date": docdate,
"publie": published,
"publie": str(published),
}
if sem["etapes"]:
el["etape_apo"] = str(sem["etapes"][0]) or ""
@ -141,7 +141,9 @@ def make_xml_formsemestre_bulletinetud(
# Disponible pour publication ?
if not published:
return doc # stop !
return sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(
scu.SCO_ENCODING
) # stop !
# Groupes:
partitions = sco_groups.get_partitions_list(formsemestre_id, with_default=False)

View File

@ -155,16 +155,6 @@ class EvaluationCache(ScoDocCache):
cls.delete_many(evaluation_ids)
class ResultatsSemestreCache(ScoDocCache):
"""Cache pour les résultats ResultatsSemestre.
Clé: formsemestre_id
Valeur: { un paquet de dataframes }
"""
prefix = "RSEM"
timeout = 60 * 60 # ttl 1 heure (en phase de mise au point)
class AbsSemEtudCache(ScoDocCache):
"""Cache pour les comptes d'absences d'un étudiant dans un semestre.
Ce cache étant indépendant des semestres, le compte peut être faux lorsqu'on
@ -224,7 +214,9 @@ class NotesTableCache(ScoDocCache):
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 and compute is True, build it and cache it.
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"):
@ -322,3 +314,14 @@ class DefferedSemCacheManager:
while g.sem_to_invalidate:
formsemestre_id = g.sem_to_invalidate.pop()
invalidate_formsemestre(formsemestre_id)
# ---- Nouvelles classes ScoDoc 9.2
class ResultatsSemestreCache(ScoDocCache):
"""Cache pour les résultats ResultatsSemestre.
Clé: formsemestre_id
Valeur: { un paquet de dataframes }
"""
prefix = "RSEM"
timeout = 60 * 60 # ttl 1 heure (en phase de mise au point)

View File

@ -191,6 +191,7 @@ def compute_user_formula(
return user_moy
# XXX OBSOLETE
def compute_moduleimpl_moyennes(nt, modimpl):
"""Retourne dict { etudid : note_moyenne } pour tous les etuds inscrits
au moduleimpl mod, la liste des evaluations "valides" (toutes notes entrées
@ -228,22 +229,23 @@ def compute_moduleimpl_moyennes(nt, modimpl):
user_expr = moduleimpl_has_expression(modimpl)
attente = False
# recupere les notes de toutes les evaluations
# récupere les notes de toutes les evaluations
eval_rattr = None
for e in evals:
e["nb_inscrits"] = e["etat"]["nb_inscrits"]
NotesDB = sco_evaluation_db.do_evaluation_get_all_notes(
# XXX OBSOLETE
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(
e["evaluation_id"]
) # toutes, y compris demissions
# restreint aux étudiants encore inscrits à ce module
notes = [
NotesDB[etudid]["value"] for etudid in NotesDB if (etudid in insmod_set)
notes_db[etudid]["value"] for etudid in notes_db if (etudid in insmod_set)
]
e["nb_notes"] = len(notes)
e["nb_abs"] = len([x for x in notes if x is None])
e["nb_neutre"] = len([x for x in notes if x == NOTES_NEUTRALISE])
e["nb_att"] = len([x for x in notes if x == NOTES_ATTENTE])
e["notes"] = NotesDB
e["notes"] = notes_db
if e["etat"]["evalattente"]:
attente = True

View File

@ -62,7 +62,8 @@ def html_edit_formation_apc(
else:
semestre_ids = [semestre_idx]
other_modules = formation.modules.filter(
Module.module_type != ModuleType.SAE, Module.module_type != ModuleType.RESSOURCE
Module.module_type.is_distinct_from(ModuleType.SAE),
Module.module_type.is_distinct_from(ModuleType.RESSOURCE),
).order_by(
Module.semestre_id, Module.module_type.desc(), Module.numero, Module.code
)
@ -78,7 +79,8 @@ def html_edit_formation_apc(
alt="supprimer",
),
"delete_disabled": scu.icontag(
"delete_small_dis_img", title="Suppression impossible (module utilisé)"
"delete_small_dis_img",
title="Suppression impossible (utilisé dans des semestres)",
),
}

View File

@ -36,6 +36,7 @@ from app import log
from app.models import SHORT_STR_LEN
from app.models.formations import Formation
from app.models.modules import Module
from app.models.ues import UniteEns
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
@ -304,9 +305,8 @@ def do_formation_edit(args):
cnx = ndb.GetDBConnexion()
sco_formations._formationEditor.edit(cnx, args)
formation = Formation.query.get(args["formation_id"])
formation: Formation = Formation.query.get(args["formation_id"])
formation.invalidate_cached_sems()
formation.force_semestre_modules_aux_ues()
def module_move(module_id, after=0, redirect=True):
@ -341,6 +341,7 @@ def module_move(module_id, after=0, redirect=True):
db.session.add(module)
db.session.add(neigh)
db.session.commit()
module.formation.invalidate_cached_sems()
# redirect to ue_list page:
if redirect:
return flask.redirect(
@ -355,16 +356,17 @@ def module_move(module_id, after=0, redirect=True):
def ue_move(ue_id, after=0, redirect=1):
"""Move UE before/after previous one (decrement/increment numero)"""
o = sco_edit_ue.ue_list({"ue_id": ue_id})[0]
# log('ue_move %s (#%s) after=%s' % (ue_id, o['numero'], after))
ue = UniteEns.query.get_or_404(ue_id)
redirect = int(redirect)
after = int(after) # 0: deplace avant, 1 deplace apres
if after not in (0, 1):
raise ValueError('invalid value for "after"')
formation_id = o["formation_id"]
others = sco_edit_ue.ue_list({"formation_id": formation_id})
others = ue.formation.ues.order_by(UniteEns.numero).all()
if len({o.numero for o in others}) != len(others):
# il y a des numeros identiques !
scu.objects_renumber(db, others)
if len(others) > 1:
idx = [p["ue_id"] for p in others].index(ue_id)
idx = [u.id for u in others].index(ue.id)
neigh = None # object to swap with
if after == 0 and idx > 0:
neigh = others[idx - 1]
@ -372,20 +374,19 @@ def ue_move(ue_id, after=0, redirect=1):
neigh = others[idx + 1]
if neigh: #
# swap numero between partition and its neighbor
# log('moving ue %s (neigh #%s)' % (ue_id, neigh['numero']))
cnx = ndb.GetDBConnexion()
o["numero"], neigh["numero"] = neigh["numero"], o["numero"]
if o["numero"] == neigh["numero"]:
neigh["numero"] -= 2 * after - 1
sco_edit_ue._ueEditor.edit(cnx, o)
sco_edit_ue._ueEditor.edit(cnx, neigh)
ue.numero, neigh.numero = neigh.numero, ue.numero
db.session.add(ue)
db.session.add(neigh)
db.session.commit()
ue.formation.invalidate_cached_sems()
# redirect to ue_list page
if redirect:
return flask.redirect(
url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=o["formation_id"],
semestre_idx=o["semestre_idx"],
formation_id=ue.formation_id,
semestre_idx=ue.semestre_idx,
)
)

View File

@ -30,13 +30,18 @@
"""
import flask
from flask import g, url_for, request
from app.models.formations import Matiere
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
from app import log
from app.models import Formation
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError
from app.scodoc.sco_exceptions import (
ScoValueError,
ScoLockedFormError,
ScoNonEmptyFormationObject,
)
from app.scodoc import html_sco_header
_matiereEditor = ndb.EditableTable(
@ -156,6 +161,16 @@ associé.
return flask.redirect(dest_url)
def can_delete_matiere(matiere: Matiere) -> tuple[bool, str]:
"True si la matiere n'est pas utilisée dans des formsemestre"
locked = matiere_is_locked(matiere.id)
if locked:
return False
if any(m.modimpls.all() for m in matiere.modules):
return False
return True
def do_matiere_delete(oid):
"delete matiere and attached modules"
from app.scodoc import sco_formations
@ -165,17 +180,16 @@ def do_matiere_delete(oid):
cnx = ndb.GetDBConnexion()
# check
mat = matiere_list({"matiere_id": oid})[0]
matiere = Matiere.query.get_or_404(oid)
mat = matiere_list({"matiere_id": oid})[0] # compat sco7
ue = sco_edit_ue.ue_list({"ue_id": mat["ue_id"]})[0]
locked = matiere_is_locked(mat["matiere_id"])
if locked:
log("do_matiere_delete: mat=%s" % mat)
log("do_matiere_delete: ue=%s" % ue)
log("do_matiere_delete: locked sems: %s" % locked)
raise ScoLockedFormError()
log("do_matiere_delete: matiere_id=%s" % oid)
if not can_delete_matiere(matiere):
# il y a au moins un modimpl dans un module de cette matière
raise ScoNonEmptyFormationObject("Matière", matiere.titre)
log("do_matiere_delete: matiere_id=%s" % matiere.id)
# delete all modules in this matiere
mods = sco_edit_module.module_list({"matiere_id": oid})
mods = sco_edit_module.module_list({"matiere_id": matiere.id})
for mod in mods:
sco_edit_module.do_module_delete(mod["module_id"])
_matiereEditor.delete(cnx, oid)
@ -194,11 +208,25 @@ def matiere_delete(matiere_id=None):
"""Delete matière"""
from app.scodoc import sco_edit_ue
M = matiere_list(args={"matiere_id": matiere_id})[0]
UE = sco_edit_ue.ue_list(args={"ue_id": M["ue_id"]})[0]
matiere = Matiere.query.get_or_404(matiere_id)
if not can_delete_matiere(matiere):
# il y a au moins un modimpl dans un module de cette matière
raise ScoNonEmptyFormationObject(
"Matière",
matiere.titre,
dest_url=url_for(
"notes.ue_table",
formation_id=matiere.ue.formation_id,
semestre_idx=matiere.ue.semestre_idx,
scodoc_dept=g.scodoc_dept,
),
)
mat = matiere_list(args={"matiere_id": matiere_id})[0]
UE = sco_edit_ue.ue_list(args={"ue_id": mat["ue_id"]})[0]
H = [
html_sco_header.sco_header(page_title="Suppression d'une matière"),
"<h2>Suppression de la matière %(titre)s" % M,
"<h2>Suppression de la matière %(titre)s" % mat,
" dans l'UE (%(acronyme)s))</h2>" % UE,
]
dest_url = url_for(
@ -210,7 +238,7 @@ def matiere_delete(matiere_id=None):
request.base_url,
scu.get_request_args(),
(("matiere_id", {"input_type": "hidden"}),),
initvalues=M,
initvalues=mat,
submitlabel="Confirmer la suppression",
cancelbutton="Annuler",
)

View File

@ -43,7 +43,12 @@ from app import models
from app.models import Formation
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError, ScoGenError
from app.scodoc.sco_exceptions import (
ScoValueError,
ScoLockedFormError,
ScoGenError,
ScoNonEmptyFormationObject,
)
from app.scodoc import html_sco_header
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_edit_matiere
@ -330,20 +335,37 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None):
)
def can_delete_module(module):
"True si le module n'est pas utilisée dans des formsemestre"
return len(module.modimpls.all()) == 0
def do_module_delete(oid):
"delete module"
from app.scodoc import sco_formations
mod = module_list({"module_id": oid})[0]
if module_is_locked(mod["module_id"]):
module = Module.query.get_or_404(oid)
mod = module_list({"module_id": oid})[0] # sco7
if module_is_locked(module.id):
raise ScoLockedFormError()
if not can_delete_module(module):
raise ScoNonEmptyFormationObject(
"Module",
msg=module.titre,
dest_url=url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=module.formation_id,
semestre_idx=module.ue.semestre_idx,
),
)
# S'il y a des moduleimpls, on ne peut pas detruire le module !
mods = sco_moduleimpl.moduleimpl_list(module_id=oid)
if mods:
err_page = f"""<h3>Destruction du module impossible car il est utilisé dans des semestres existants !</h3>
<p class="help">Il faut d'abord supprimer le semestre. Mais il est peut être préférable de
laisser ce programme intact et d'en créer une nouvelle version pour la modifier.
<p class="help">Il faut d'abord supprimer le semestre (ou en retirer ce module). Mais il est peut être préférable de
laisser ce programme intact et d'en créer une nouvelle version pour la modifier sans affecter les semestres déjà en place.
</p>
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
formation_id=mod["formation_id"])}">reprendre</a>
@ -365,12 +387,21 @@ def do_module_delete(oid):
def module_delete(module_id=None):
"""Delete a module"""
if not module_id:
raise ScoValueError("invalid module !")
modules = module_list(args={"module_id": module_id})
if not modules:
raise ScoValueError("Module inexistant !")
mod = modules[0]
module = Module.query.get_or_404(module_id)
mod = module_list(args={"module_id": module_id})[0] # sco7
if not can_delete_module(module):
raise ScoNonEmptyFormationObject(
"Module",
msg=module.titre,
dest_url=url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=module.formation_id,
semestre_idx=module.ue.semestre_idx,
),
)
H = [
html_sco_header.sco_header(page_title="Suppression d'un module"),
"""<h2>Suppression du module %(titre)s (%(code)s)</h2>""" % mod,
@ -523,9 +554,11 @@ def module_edit(module_id=None):
),
]
if is_apc:
coefs_descr = a_module.ue_coefs_descr()
if coefs_descr:
coefs_descr_txt = ", ".join(["%s: %s" % x for x in coefs_descr])
coefs_lst = a_module.ue_coefs_list()
if coefs_lst:
coefs_descr_txt = ", ".join(
[f"{ue.acronyme}: {c}" for (ue, c) in coefs_lst]
)
else:
coefs_descr_txt = """<span class="missing_value">non définis</span>"""
descr += [

View File

@ -42,7 +42,12 @@ from app import log
from app.scodoc.TrivialFormulator import TrivialFormulator, TF
from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError
from app.scodoc.sco_exceptions import (
ScoGenError,
ScoValueError,
ScoLockedFormError,
ScoNonEmptyFormationObject,
)
from app.scodoc import html_sco_header
from app.scodoc import sco_cache
@ -130,64 +135,83 @@ def do_ue_create(args):
return ue_id
def can_delete_ue(ue: UniteEns) -> bool:
"""True si l'UE n'est pas utilisée dans des formsemestre
et n'a pas de module rattachés
"""
# "pas un seul module de cette UE n'a de modimpl...""
return (not len(ue.modules.all())) and not any(m.modimpls.all() for m in ue.modules)
def do_ue_delete(ue_id, delete_validations=False, force=False):
"delete UE and attached matieres (but not modules)"
from app.scodoc import sco_formations
from app.scodoc import sco_parcours_dut
ue = UniteEns.query.get_or_404(ue_id)
if not can_delete_ue(ue):
raise ScoNonEmptyFormationObject(
"UE",
msg=ue.titre,
dest_url=url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=ue.formation_id,
semestre_idx=ue.semestre_idx,
),
)
cnx = ndb.GetDBConnexion()
log("do_ue_delete: ue_id=%s, delete_validations=%s" % (ue_id, delete_validations))
log("do_ue_delete: ue_id=%s, delete_validations=%s" % (ue.id, delete_validations))
# check
ue = ue_list({"ue_id": ue_id})
if not ue:
raise ScoValueError("UE inexistante !")
ue = ue[0]
if ue_is_locked(ue["ue_id"]):
raise ScoLockedFormError()
# if ue_is_locked(ue.id):
# raise ScoLockedFormError()
# Il y a-t-il des etudiants ayant validé cette UE ?
# si oui, propose de supprimer les validations
validations = sco_parcours_dut.scolar_formsemestre_validation_list(
cnx, args={"ue_id": ue_id}
cnx, args={"ue_id": ue.id}
)
if validations and not delete_validations and not force:
return scu.confirm_dialog(
"<p>%d étudiants ont validé l'UE %s (%s)</p><p>Si vous supprimez cette UE, ces validations vont être supprimées !</p>"
% (len(validations), ue["acronyme"], ue["titre"]),
% (len(validations), ue.acronyme, ue.titre),
dest_url="",
target_variable="delete_validations",
cancel_url=url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=str(ue["formation_id"]),
formation_id=ue.formation_id,
semestre_idx=ue.semestre_idx,
),
parameters={"ue_id": ue_id, "dialog_confirmed": 1},
parameters={"ue_id": ue.id, "dialog_confirmed": 1},
)
if delete_validations:
log("deleting all validations of UE %s" % ue_id)
log("deleting all validations of UE %s" % ue.id)
ndb.SimpleQuery(
"DELETE FROM scolar_formsemestre_validation WHERE ue_id=%(ue_id)s",
{"ue_id": ue_id},
{"ue_id": ue.id},
)
# delete all matiere in this UE
mats = sco_edit_matiere.matiere_list({"ue_id": ue_id})
mats = sco_edit_matiere.matiere_list({"ue_id": ue.id})
for mat in mats:
sco_edit_matiere.do_matiere_delete(mat["matiere_id"])
# delete uecoef and events
ndb.SimpleQuery(
"DELETE FROM notes_formsemestre_uecoef WHERE ue_id=%(ue_id)s",
{"ue_id": ue_id},
{"ue_id": ue.id},
)
ndb.SimpleQuery("DELETE FROM scolar_events WHERE ue_id=%(ue_id)s", {"ue_id": ue_id})
ndb.SimpleQuery("DELETE FROM scolar_events WHERE ue_id=%(ue_id)s", {"ue_id": ue.id})
cnx = ndb.GetDBConnexion()
_ueEditor.delete(cnx, ue_id)
# > UE delete + supr. validations associées etudiants (cas compliqué, mais rarement utilisé: acceptable de tout invalider ?):
_ueEditor.delete(cnx, ue.id)
# > UE delete + supr. validations associées etudiants (cas compliqué, mais rarement
# utilisé: acceptable de tout invalider):
sco_cache.invalidate_formsemestre()
# news
F = sco_formations.formation_list(args={"formation_id": ue["formation_id"]})[0]
F = sco_formations.formation_list(args={"formation_id": ue.formation_id})[0]
sco_news.add(
typ=sco_news.NEWS_FORM,
object=ue["formation_id"],
object=ue.formation_id,
text="Modification de la formation %(acronyme)s" % F,
max_frequency=3,
)
@ -197,11 +221,11 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=ue["formation_id"],
formation_id=ue.formation_id,
semestre_idx=ue.semestre_idx,
)
)
else:
return None
return None
def ue_create(formation_id=None):
@ -211,8 +235,6 @@ def ue_create(formation_id=None):
def ue_edit(ue_id=None, create=False, formation_id=None):
"""Modification ou création d'une UE"""
from app.scodoc import sco_formations
create = int(create)
if not create:
U = ue_list(args={"ue_id": ue_id})
@ -444,34 +466,53 @@ def next_ue_numero(formation_id, semestre_id=None):
def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False):
"""Delete an UE"""
ues = ue_list(args={"ue_id": ue_id})
if not ues:
raise ScoValueError("UE inexistante !")
ue = ues[0]
if not dialog_confirmed:
return scu.confirm_dialog(
"<h2>Suppression de l'UE %(titre)s (%(acronyme)s))</h2>" % ue,
dest_url="",
parameters={"ue_id": ue_id},
cancel_url=url_for(
ue = UniteEns.query.get_or_404(ue_id)
if ue.modules.all():
raise ScoValueError(
f"""Suppression de l'UE {ue.titre} impossible car
des modules (ou SAÉ ou ressources) lui sont rattachés.""",
dest_url=url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=str(ue["formation_id"]),
formation_id=ue.formation.id,
semestre_idx=ue.semestre_idx,
),
)
if not can_delete_ue(ue):
raise ScoNonEmptyFormationObject(
"UE",
msg=ue.titre,
dest_url=url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=ue.formation_id,
semestre_idx=ue.semestre_idx,
),
)
return do_ue_delete(ue_id, delete_validations=delete_validations)
if not dialog_confirmed:
return scu.confirm_dialog(
f"<h2>Suppression de l'UE {ue.titre} ({ue.acronyme})</h2>",
dest_url="",
parameters={"ue_id": ue.id},
cancel_url=url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=ue.formation_id,
semestre_idx=ue.semestre_idx,
),
)
return do_ue_delete(ue.id, delete_validations=delete_validations)
def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
"""Liste des matières et modules d'une formation, avec liens pour
éditer (si non verrouillée).
"""
from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre_validation
formation = Formation.query.get(formation_id)
formation: Formation = Formation.query.get(formation_id)
if not formation:
raise ScoValueError("invalid formation_id")
parcours = formation.get_parcours()
@ -594,12 +635,16 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
descr_refcomp = ""
msg_refcomp = "associer à un référentiel de compétences"
else:
descr_refcomp = f"{formation.referentiel_competence.type_titre} {formation.referentiel_competence.specialite_long}"
descr_refcomp = f"""Référentiel de compétences:
<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> """
msg_refcomp = "changer"
H.append(
f"""
<ul>
<li>{descr_refcomp} <a class="stdlink" href="{url_for('notes.refcomp_assoc_formation',
<li>{descr_refcomp}&nbsp; <a class="stdlink" href="{url_for('notes.refcomp_assoc_formation',
scodoc_dept=g.scodoc_dept, formation_id=formation_id)
}">{msg_refcomp}</a>
</li>
@ -1010,12 +1055,14 @@ def _ue_table_modules(
H.append(arrow_none)
im += 1
if mod["nb_moduleimpls"] == 0 and editable:
H.append(
'<a class="smallbutton" href="module_delete?module_id=%s">%s</a>'
% (mod["module_id"], delete_icon)
)
icon = delete_icon
else:
H.append(delete_disabled_icon)
icon = delete_disabled_icon
H.append(
'<a class="smallbutton" href="module_delete?module_id=%s">%s</a>'
% (mod["module_id"], icon)
)
H.append("</span>")
mod_editable = (
@ -1167,7 +1214,6 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False):
if not dont_invalidate_cache:
# Invalide les semestres utilisant cette formation:
formation.invalidate_cached_sems()
formation.force_semestre_modules_aux_ues()
# essai edition en ligne:

View File

@ -43,7 +43,7 @@
apo_csv_get()
API:
apo_csv_store( annee_scolaire, sem_id)
# apo_csv_store(csv_data, annee_scolaire, sem_id)
store maq file (archive)
apo_csv_get(etape_apo, annee_scolaire, sem_id, vdi_apo=None)
@ -101,7 +101,7 @@ ApoCSVArchive = ApoCSVArchiver()
def apo_csv_store(csv_data: str, annee_scolaire, sem_id):
"""
csv_data: maquette content (string)
csv_data: maquette content (str))
annee_scolaire: int (2016)
sem_id: 0 (année ?), 1 (premier semestre de l'année) ou 2 (deuxième semestre)
:return: etape_apo du fichier CSV stocké
@ -378,7 +378,7 @@ e.associate_sco( apo_data)
print apo_csv_list_stored_archives()
apo_csv_store(csv_data, annee_scolaire, sem_id)
# apo_csv_store(csv_data, annee_scolaire, sem_id)

View File

@ -48,7 +48,7 @@ from app.scodoc import sco_preferences
from app.scodoc import sco_semset
from app.scodoc import sco_etud
from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_apogee_csv import APO_PORTAL_ENCODING, APO_INPUT_ENCODING
from app.scodoc.sco_apogee_csv import APO_INPUT_ENCODING, APO_OUTPUT_ENCODING
from app.scodoc.sco_exceptions import ScoValueError
@ -585,7 +585,7 @@ def _view_etuds_page(semset_id, title="", etuds=[], keys=(), format="html"):
return "\n".join(H) + html_sco_header.sco_footer()
def view_apo_csv_store(semset_id="", csvfile=None, data="", autodetect=False):
def view_apo_csv_store(semset_id="", csvfile=None, data: bytes = "", autodetect=False):
"""Store CSV data
Le semset identifie l'annee scolaire et le semestre
Si csvfile, lit depuis FILE, sinon utilise data
@ -593,9 +593,8 @@ def view_apo_csv_store(semset_id="", csvfile=None, data="", autodetect=False):
if not semset_id:
raise ValueError("invalid null semset_id")
semset = sco_semset.SemSet(semset_id=semset_id)
if csvfile:
data = csvfile.read()
data = csvfile.read() # bytes
if autodetect:
# check encoding (although documentation states that users SHOULD upload LATIN1)
data, message = sco_apogee_csv.fix_data_encoding(data)
@ -605,19 +604,26 @@ def view_apo_csv_store(semset_id="", csvfile=None, data="", autodetect=False):
log("view_apo_csv_store: autodetection of encoding disabled by user")
if not data:
raise ScoValueError("view_apo_csv_store: no data")
# data est du bytes, encodé en APO_INPUT_ENCODING
data_str = data.decode(APO_INPUT_ENCODING)
# check si etape maquette appartient bien au semset
apo_data = sco_apogee_csv.ApoData(
data, periode=semset["sem_id"]
data_str, periode=semset["sem_id"]
) # parse le fichier -> exceptions
if apo_data.etape not in semset["etapes"]:
raise ScoValueError(
"Le code étape de ce fichier ne correspond pas à ceux de cet ensemble"
)
sco_etape_apogee.apo_csv_store(data, semset["annee_scolaire"], semset["sem_id"])
sco_etape_apogee.apo_csv_store(data_str, semset["annee_scolaire"], semset["sem_id"])
return flask.redirect("apo_semset_maq_status?semset_id=" + semset_id)
return flask.redirect(
url_for(
"notes.apo_semset_maq_status",
scodoc_dept=g.scodoc_dept,
semset_id=semset_id,
)
)
def view_apo_csv_download_and_store(etape_apo="", semset_id=""):
@ -629,9 +635,9 @@ def view_apo_csv_download_and_store(etape_apo="", semset_id=""):
data = sco_portal_apogee.get_maquette_apogee(
etape=etape_apo, annee_scolaire=semset["annee_scolaire"]
)
# here, data is utf8
# here, data is str
# but we store and generate latin1 files, to ease further import in Apogée
data = data.decode(APO_PORTAL_ENCODING).encode(APO_INPUT_ENCODING) # XXX #py3
data = data.encode(APO_OUTPUT_ENCODING)
return view_apo_csv_store(semset_id, data=data, autodetect=False)
@ -669,7 +675,7 @@ def view_apo_csv(etape_apo="", semset_id="", format="html"):
sem_id = semset["sem_id"]
csv_data = sco_etape_apogee.apo_csv_get(etape_apo, annee_scolaire, sem_id)
if format == "raw":
scu.send_file(csv_data, etape_apo, suffix=".txt", mime=scu.CSV_MIMETYPE)
return scu.send_file(csv_data, etape_apo, suffix=".txt", mime=scu.CSV_MIMETYPE)
apo_data = sco_apogee_csv.ApoData(csv_data, periode=semset["sem_id"])

View File

@ -85,7 +85,7 @@ def evaluation_check_absences(evaluation_id):
Justs = set([x["etudid"] for x in Just]) # ensemble des etudiants avec justif
# Les notes:
NotesDB = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
ValButAbs = [] # une note mais noté absent
AbsNonSignalee = [] # note ABS mais pas noté absent
ExcNonSignalee = [] # note EXC mais pas noté absent
@ -94,8 +94,8 @@ def evaluation_check_absences(evaluation_id):
for etudid, _ in sco_groups.do_evaluation_listeetuds_groups(
evaluation_id, getallstudents=True
):
if etudid in NotesDB:
val = NotesDB[etudid]["value"]
if etudid in notes_db:
val = notes_db[etudid]["value"]
if (
val != None and val != scu.NOTES_NEUTRALISE and val != scu.NOTES_ATTENTE
) and etudid in As:
@ -222,7 +222,6 @@ def formsemestre_check_absences_html(formsemestre_id):
H = [
html_sco_header.html_sem_header(
"Vérification absences aux évaluations de ce semestre",
sem,
),
"""<p class="help">Vérification de la cohérence entre les notes saisies et les absences signalées.
Sont listés tous les modules avec des évaluations.<br/>Aucune action n'est effectuée:

View File

@ -306,8 +306,8 @@ def do_evaluation_delete(evaluation_id):
raise AccessDenied(
"Modification évaluation impossible pour %s" % current_user.get_nomplogin()
)
NotesDB = do_evaluation_get_all_notes(evaluation_id) # { etudid : value }
notes = [x["value"] for x in NotesDB.values()]
notes_db = do_evaluation_get_all_notes(evaluation_id) # { etudid : value }
notes = [x["value"] for x in notes_db.values()]
if notes:
raise ScoValueError(
"Impossible de supprimer cette évaluation: il reste des notes"

View File

@ -170,7 +170,7 @@ def evaluation_create_form(
(
"jour",
{
"input_type": "date",
"input_type": "datedmy",
"title": "Date",
"size": 12,
"explanation": "date de l'examen, devoir ou contrôle",

View File

@ -467,7 +467,6 @@ def formsemestre_evaluations_cal(formsemestre_id):
H = [
html_sco_header.html_sem_header(
"Evaluations du semestre",
sem,
cssstyles=["css/calabs.css"],
),
'<div class="cal_evaluations">',

View File

@ -52,7 +52,7 @@ class InvalidNoteValue(ScoException):
# Exception qui stoque dest_url, utilisee dans Zope standard_error_message
class ScoValueError(ScoException):
def __init__(self, msg, dest_url=None):
ScoException.__init__(self, msg)
super().__init__(msg)
self.dest_url = dest_url
@ -60,6 +60,21 @@ class ScoFormatError(ScoValueError):
pass
class ScoPDFFormatError(ScoValueError):
"erreur génération PDF (templates platypus, ...)"
def __init__(self, msg, dest_url=None):
super().__init__(
f"""Erreur dans un format pdf:
<p>{msg}</p>
<p>Vérifiez les paramètres (polices de caractères, balisage)
dans les paramètres ou préférences.
</p>
""",
dest_url=dest_url,
)
class ScoInvalidDept(ScoValueError):
"""departement invalide"""
@ -72,20 +87,57 @@ class ScoConfigurationError(ScoValueError):
pass
class ScoLockedFormError(ScoException):
def __init__(self, msg=""):
class ScoLockedFormError(ScoValueError):
"Modification d'une formation verrouillée"
def __init__(self, msg="", dest_url=None):
msg = (
"Cette formation est verrouillée (car il y a un semestre verrouillé qui s'y réfère). "
+ str(msg)
)
ScoException.__init__(self, msg)
super().__init__(msg=msg, dest_url=dest_url)
class ScoNonEmptyFormationObject(ScoValueError):
"""On ne peut pas supprimer un module/matiere ou UE si des formsemestre s'y réfèrent"""
def __init__(self, type_objet="objet'", msg="", dest_url=None):
msg = f"""<h3>{type_objet} "{msg}" utilisé dans des semestres: suppression impossible.</h3>
<p class="help">Il faut d'abord supprimer le semestre (ou en retirer ce {type_objet}).
Mais il est peut-être préférable de laisser ce programme intact et d'en créer une
nouvelle version pour la modifier sans affecter les semestres déjà en place.
</p>
"""
super().__init__(msg=msg, dest_url=dest_url)
class ScoInvalidIdType(ScoValueError):
"""Pour les clients qui s'obstinnent à utiliser des bookmarks ou
historiques anciens avec des ID ScoDoc7"""
def __init__(self, msg=""):
import app.scodoc.sco_utils as scu
msg = f"""<h3>Adresse de page invalide</h3>
<p class="help">
Vous utilisez un lien invalide, qui correspond probablement
à une ancienne version du logiciel. <br>
Au besoin, mettre à jour vos marque-pages.
</p>
<p> Si le problème persiste, merci de contacter l'assistance
via la liste de diffusion <a href="{scu.SCO_USERS_LIST}">Notes</a>
ou le salon Discord.
</p>
<p>Message serveur: <tt>{msg}</tt></p>
"""
super().__init__(msg)
class ScoGenError(ScoException):
"exception avec affichage d'une page explicative ad-hoc"
def __init__(self, msg=""):
ScoException.__init__(self, msg)
super().__init__(msg)
class AccessDenied(ScoGenError):
@ -101,7 +153,7 @@ class APIInvalidParams(Exception):
status_code = 400
def __init__(self, message, status_code=None, payload=None):
Exception.__init__(self)
super().__init__()
self.message = message
if status_code is not None:
self.status_code = status_code

View File

@ -27,21 +27,22 @@
"""Operations de base sur les formsemestres
"""
from app.scodoc.sco_exceptions import ScoValueError
import time
from operator import itemgetter
import time
from flask import g, request
import app
from app import log
from app.models import Departement
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_cache
from app.scodoc import sco_formations
from app.scodoc import sco_preferences
from app.scodoc.gen_tables import GenTable
from app import log
from app.scodoc.sco_codes_parcours import NO_SEMESTRE_ID
from app.scodoc.sco_exceptions import ScoValueError, ScoInvalidIdType
from app.scodoc.sco_vdi import ApoEtapeVDI
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
@ -97,7 +98,7 @@ def get_formsemestre(formsemestre_id, raise_soft_exc=False):
if formsemestre_id in g.stored_get_formsemestre:
return g.stored_get_formsemestre[formsemestre_id]
if not isinstance(formsemestre_id, int):
raise ValueError("formsemestre_id must be an integer !")
raise ScoInvalidIdType("formsemestre_id must be an integer !")
sems = do_formsemestre_list(args={"formsemestre_id": formsemestre_id})
if not sems:
log("get_formsemestre: invalid formsemestre_id (%s)" % formsemestre_id)
@ -450,7 +451,7 @@ def sem_in_annee_scolaire(sem, year=False):
)
def sem_une_annee(sem):
def sem_une_annee(sem): # XXX deprecated: use FormSemestre.est_sur_une_annee()
"""Test si sem est entièrement sur la même année scolaire.
(ce n'est pas obligatoire mais si ce n'est pas le cas les exports Apogée ne vont pas fonctionner)
pivot au 1er août.

View File

@ -84,7 +84,7 @@ def formsemestre_custommenu_edit(formsemestre_id):
scu.NotesURL() + "/formsemestre_status?formsemestre_id=%s" % formsemestre_id
)
H = [
html_sco_header.html_sem_header("Modification du menu du semestre ", sem),
html_sco_header.html_sem_header("Modification du menu du semestre "),
"""<p class="help">Ce menu, spécifique à chaque semestre, peut être utilisé pour placer des liens vers vos applications préférées.</p>
<p class="help">Procédez en plusieurs fois si vous voulez ajouter plusieurs items.</p>""",
]

View File

@ -96,7 +96,6 @@ def formsemestre_editwithmodules(formsemestre_id):
H = [
html_sco_header.html_sem_header(
"Modification du semestre",
sem,
javascripts=["libjs/AutoSuggest.js"],
cssstyles=["css/autosuggest_inquisitor.css"],
bodyOnLoad="init_tf_form('')",
@ -198,10 +197,13 @@ def do_formsemestre_createwithmodules(edit=False):
NB_SEM = parcours.NB_SEM
else:
NB_SEM = 10 # fallback, max 10 semestres
semestre_id_list = [-1] + list(range(1, NB_SEM + 1))
if NB_SEM == 1:
semestre_id_list = [-1]
else:
semestre_id_list = [-1] + list(range(1, NB_SEM + 1))
semestre_id_labels = []
for sid in semestre_id_list:
if sid == "-1":
if sid == -1:
semestre_id_labels.append("pas de semestres")
else:
semestre_id_labels.append(f"S{sid}")
@ -251,7 +253,7 @@ def do_formsemestre_createwithmodules(edit=False):
"date_debut",
{
"title": "Date de début", # j/m/a
"input_type": "date",
"input_type": "datedmy",
"explanation": "j/m/a",
"size": 9,
"allow_null": False,
@ -261,7 +263,7 @@ def do_formsemestre_createwithmodules(edit=False):
"date_fin",
{
"title": "Date de fin", # j/m/a
"input_type": "date",
"input_type": "datedmy",
"explanation": "j/m/a",
"size": 9,
"allow_null": False,
@ -329,6 +331,8 @@ def do_formsemestre_createwithmodules(edit=False):
"labels": modalites_titles,
},
),
]
modform.append(
(
"semestre_id",
{
@ -338,7 +342,7 @@ def do_formsemestre_createwithmodules(edit=False):
"labels": semestre_id_labels,
},
),
]
)
etapes = sco_portal_apogee.get_etapes_apogee_dept()
# Propose les etapes renvoyées par le portail
# et ajoute les étapes du semestre qui ne sont pas dans la liste (soit la liste a changé, soit l'étape a été ajoutée manuellement)
@ -895,7 +899,6 @@ def formsemestre_clone(formsemestre_id):
H = [
html_sco_header.html_sem_header(
"Copie du semestre",
sem,
javascripts=["libjs/AutoSuggest.js"],
cssstyles=["css/autosuggest_inquisitor.css"],
bodyOnLoad="init_tf_form('')",
@ -909,7 +912,7 @@ def formsemestre_clone(formsemestre_id):
"date_debut",
{
"title": "Date de début", # j/m/a
"input_type": "date",
"input_type": "datedmy",
"explanation": "j/m/a",
"size": 9,
"allow_null": False,
@ -919,7 +922,7 @@ def formsemestre_clone(formsemestre_id):
"date_fin",
{
"title": "Date de fin", # j/m/a
"input_type": "date",
"input_type": "datedmy",
"explanation": "j/m/a",
"size": 9,
"allow_null": False,
@ -1235,7 +1238,7 @@ def formsemestre_delete(formsemestre_id):
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
H = [
html_sco_header.html_sem_header("Suppression du semestre", sem),
html_sco_header.html_sem_header("Suppression du semestre"),
"""<div class="ue_warning"><span>Attention !</span>
<p class="help">A n'utiliser qu'en cas d'erreur lors de la saisie d'une formation. Normalement,
<b>un semestre ne doit jamais être supprimé</b> (on perd la mémoire des notes et de tous les événements liés à ce semestre !).</p>
@ -1514,7 +1517,7 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
</p>
"""
H = [
html_sco_header.html_sem_header("Coefficients des UE du semestre", sem),
html_sco_header.html_sem_header("Coefficients des UE du semestre"),
help,
]
#
@ -1626,7 +1629,7 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
formsemestre_id=formsemestre_id
) # > modif coef UE cap (modifs notes de _certains_ etudiants)
header = html_sco_header.html_sem_header("Coefficients des UE du semestre", sem)
header = html_sco_header.html_sem_header("Coefficients des UE du semestre")
return (
header
+ "\n".join(z)

View File

@ -154,7 +154,7 @@ def formsemestre_ext_create_form(etudid, formsemestre_id):
"date_debut",
{
"title": "Date de début", # j/m/a
"input_type": "date",
"input_type": "datedmy",
"explanation": "j/m/a (peut être approximatif)",
"size": 9,
"allow_null": False,
@ -164,7 +164,7 @@ def formsemestre_ext_create_form(etudid, formsemestre_id):
"date_fin",
{
"title": "Date de fin", # j/m/a
"input_type": "date",
"input_type": "datedmy",
"explanation": "j/m/a (peut être approximatif)",
"size": 9,
"allow_null": False,

View File

@ -374,7 +374,6 @@ def formsemestre_inscription_with_modules(
H = [
html_sco_header.html_sem_header(
"Inscription de %s dans ce semestre" % etud["nomprenom"],
sem,
)
]
F = html_sco_header.sco_footer()
@ -802,7 +801,6 @@ def formsemestre_inscrits_ailleurs(formsemestre_id):
H = [
html_sco_header.html_sem_header(
"Inscriptions multiples parmi les étudiants du semestre ",
sem,
)
]
insd = list_inscrits_ailleurs(formsemestre_id)

View File

@ -35,7 +35,10 @@ from flask import url_for
from flask_login import current_user
from app import log
from app.comp import res_sem
from app.models import Module
from app.models import formsemestre
from app.models.formsemestre import FormSemestre
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
import app.scodoc.notesdb as ndb
@ -722,7 +725,7 @@ def formsemestre_description_table(formsemestre_id, with_evals=False):
% (request.base_url, formsemestre_id, with_evals),
page_title=title,
html_title=html_sco_header.html_sem_header(
"Description du semestre", sem, with_page_header=False
"Description du semestre", with_page_header=False
),
pdf_title=title,
preferences=sco_preferences.SemPreferences(formsemestre_id),
@ -915,34 +918,35 @@ def html_expr_diagnostic(diagnostics):
def formsemestre_status_head(formsemestre_id=None, page_title=None):
"""En-tête HTML des pages "semestre" """
semlist = sco_formsemestre.do_formsemestre_list(
args={"formsemestre_id": formsemestre_id}
)
if not semlist:
raise ScoValueError("Session inexistante (elle a peut être été supprimée ?)")
sem = semlist[0]
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
sem = FormSemestre.query.get(formsemestre_id)
if not sem:
raise ScoValueError("Semestre inexistant (il a peut être été supprimé ?)")
formation = sem.formation
parcours = formation.get_parcours()
page_title = page_title or "Modules de "
H = [
html_sco_header.html_sem_header(
page_title, sem, with_page_header=False, with_h2=False
page_title, with_page_header=False, with_h2=False
),
f"""<table>
<tr><td class="fichetitre2">Formation: </td><td>
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept, formation_id=F['formation_id'])}"
class="discretelink" title="Formation {F['acronyme']}, v{F['version']}">{F['titre']}</a>""",
<a href="{url_for('notes.ue_table',
scodoc_dept=g.scodoc_dept, formation_id=sem.formation.id)}"
class="discretelink" title="Formation {
formation.acronyme}, v{formation.version}">{formation.titre}</a>
""",
]
if sem["semestre_id"] >= 0:
H.append(", %s %s" % (parcours.SESSION_NAME, sem["semestre_id"]))
if sem["modalite"]:
H.append("&nbsp;en %(modalite)s" % sem)
if sem["etapes"]:
if sem.semestre_id >= 0:
H.append(", %s %s" % (parcours.SESSION_NAME, sem.semestre_id))
if sem.modalite:
H.append(f"&nbsp;en {sem.modalite}")
if sem.etapes:
H.append(
"&nbsp;&nbsp;&nbsp;(étape <b><tt>%s</tt></b>)"
% (sem["etapes_apo_str"] or "-")
f"""&nbsp;&nbsp;&nbsp;(étape <b><tt>{
sem.etapes_apo_str() or "-"
}</tt></b>)"""
)
H.append("</td></tr>")
@ -965,18 +969,16 @@ Il y a des notes en attente ! Le classement des étudiants n'a qu'une valeur ind
)
H.append("</table>")
sem_warning = ""
if sem["bul_hide_xml"]:
if sem.bul_hide_xml:
sem_warning += "Bulletins non publiés sur le portail. "
if sem["block_moyennes"]:
if sem.block_moyennes:
sem_warning += "Calcul des moyennes bloqué !"
if sem_warning:
H.append('<p class="fontorange"><em>' + sem_warning + "</em></p>")
if sem["semestre_id"] >= 0 and not sco_formsemestre.sem_une_annee(sem):
if sem.semestre_id >= 0 and not sem.est_sur_une_annee():
H.append(
'<p class="fontorange"><em>Attention: ce semestre couvre plusieurs années scolaires !</em></p>'
)
# elif sco_preferences.get_preference( 'bul_display_publication', formsemestre_id):
# H.append('<p><em>Bulletins publiés sur le portail</em></p>')
return "".join(H)
@ -990,6 +992,9 @@ def formsemestre_status(formsemestre_id=None):
formsemestre_id=formsemestre_id
)
nt = sco_cache.NotesTableCache.get(formsemestre_id)
# WIP formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
# WIP nt = res_sem.load_formsemestre_result(formsemestre)
# Construit la liste de tous les enseignants de ce semestre:
mails_enseignants = set(
[sco_users.user_info(ens_id)["email"] for ens_id in sem["responsables"]]
@ -1119,7 +1124,7 @@ def formsemestre_tableau_modules(
mod_descr = "Module " + (mod.titre or "")
if mod.is_apc():
coef_descr = ", ".join(
[f"{ue_acro}: {co}" for ue_acro, co in mod.ue_coefs_descr()]
[f"{ue.acronyme}: {co}" for ue, co in mod.ue_coefs_list()]
)
if coef_descr:
mod_descr += " Coefs: " + coef_descr

View File

@ -847,9 +847,7 @@ def formsemestre_validation_auto(formsemestre_id):
"Formulaire saisie automatisee des decisions d'un semestre"
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
H = [
html_sco_header.html_sem_header(
"Saisie automatique des décisions du semestre", sem
),
html_sco_header.html_sem_header("Saisie automatique des décisions du semestre"),
"""
<ul>
<li>Seuls les étudiants qui obtiennent le semestre seront affectés (code ADM, moyenne générale et

View File

@ -284,7 +284,9 @@ def get_group_infos(group_id, etat=None): # was _getlisteetud
cnx = ndb.GetDBConnexion()
group = get_group(group_id)
sem = sco_formsemestre.get_formsemestre(group["formsemestre_id"])
sem = sco_formsemestre.get_formsemestre(
group["formsemestre_id"], raise_soft_exc=True
)
members = get_group_members(group_id, etat=etat)
# add human readable description of state:
@ -1431,18 +1433,19 @@ def create_etapes_partition(formsemestre_id, partition_name="apo_etapes"):
def do_evaluation_listeetuds_groups(
evaluation_id, groups=None, getallstudents=False, include_dems=False
evaluation_id, groups=None, getallstudents=False, include_demdef=False
):
"""Donne la liste des etudids inscrits a cette evaluation dans les
groupes indiqués.
Si getallstudents==True, donne tous les etudiants inscrits a cette
evaluation.
Si include_dems, compte aussi les etudiants démissionnaires
Si include_demdef, compte aussi les etudiants démissionnaires et défaillants
(sinon, par défaut, seulement les 'I')
Résultat: [ (etudid, etat) ], etat='I', 'D', 'DEF'
"""
# nb: pour notes_table / do_evaluation_etat, getallstudents est vrai et include_dems faux
# nb: pour notes_table / do_evaluation_etat, getallstudents est vrai et
# include_demdef faux
fromtables = [
"notes_moduleimpl_inscription Im",
"notes_formsemestre_inscription Isem",
@ -1474,7 +1477,7 @@ def do_evaluation_listeetuds_groups(
and E.id = %(evaluation_id)s
"""
)
if not include_dems:
if not include_demdef:
req += " and Isem.etat='I'"
req += r
cnx = ndb.GetDBConnexion()

View File

@ -550,9 +550,9 @@ def _import_one_student(
formsemestre_id = values["codesemestre"]
try:
formsemestre_id = int(formsemestre_id)
except ValueError as exc:
except (ValueError, TypeError) as exc:
raise ScoValueError(
f"valeur invalide dans la colonne codesemestre, ligne {linenum+1}"
f"valeur invalide ou manquante dans la colonne codesemestre, ligne {linenum+1}"
) from exc
# recupere liste des groupes:
if formsemestre_id not in GroupIdInferers:

View File

@ -292,6 +292,8 @@ def formsemestre_inscr_passage(
etuds = [int(x) for x in etuds.split(",") if x]
elif isinstance(etuds, int):
etuds = [etuds]
elif etuds and isinstance(etuds[0], str):
etuds = [int(x) for x in etuds]
auth_etuds_by_sem, inscrits, candidats = list_authorized_etuds_by_sem(sem)
etuds_set = set(etuds)
@ -418,7 +420,7 @@ def build_page(
H = [
html_sco_header.html_sem_header(
"Passages dans le semestre", sem, with_page_header=False
"Passages dans le semestre", with_page_header=False
),
"""<form method="post" action="%s">""" % request.base_url,
"""<input type="hidden" name="formsemestre_id" value="%(formsemestre_id)s"/>

View File

@ -201,6 +201,7 @@ def do_evaluation_listenotes(
note_sur_20 = tf[2]["note_sur_20"]
hide_groups = tf[2]["hide_groups"]
with_emails = tf[2]["with_emails"]
group_ids = [x for x in tf[2]["group_ids"] if x != ""]
return (
_make_table_notes(
tf[1],
@ -208,7 +209,7 @@ def do_evaluation_listenotes(
format=format,
note_sur_20=note_sur_20,
anonymous_listing=anonymous_listing,
group_ids=tf[2]["group_ids"],
group_ids=group_ids,
hide_groups=hide_groups,
with_emails=with_emails,
mode=mode,
@ -308,7 +309,7 @@ def _make_table_notes(
anonymous_lst_key = "etudid"
etudid_etats = sco_groups.do_evaluation_listeetuds_groups(
E["evaluation_id"], groups, include_dems=True
E["evaluation_id"], groups, include_demdef=True
)
for etudid, etat in etudid_etats:
css_row_class = None
@ -652,11 +653,11 @@ def _add_eval_columns(
notes = [] # liste des notes numeriques, pour calcul histogramme uniquement
evaluation_id = e["evaluation_id"]
e_o = Evaluation.query.get(evaluation_id) # XXX en attendant ré-écriture
NotesDB = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
for row in rows:
etudid = row["etudid"]
if etudid in NotesDB:
val = NotesDB[etudid]["value"]
if etudid in notes_db:
val = notes_db[etudid]["value"]
if val is None:
nb_abs += 1
if val == scu.NOTES_ATTENTE:
@ -673,12 +674,12 @@ def _add_eval_columns(
nb_notes = nb_notes + 1
sum_notes += val
val_fmt = scu.fmt_note(val, keep_numeric=keep_numeric)
comment = NotesDB[etudid]["comment"]
comment = notes_db[etudid]["comment"]
if comment is None:
comment = ""
explanation = "%s (%s) %s" % (
NotesDB[etudid]["date"].strftime("%d/%m/%y %Hh%M"),
sco_users.user_info(NotesDB[etudid]["uid"])["nomcomplet"],
notes_db[etudid]["date"].strftime("%d/%m/%y %Hh%M"),
sco_users.user_info(notes_db[etudid]["uid"])["nomcomplet"],
comment,
)
else:

View File

@ -36,6 +36,7 @@ from app.auth.models import User
from app.models import ModuleImpl
from app.models.evaluations import Evaluation
import app.scodoc.sco_utils as scu
from app.scodoc.sco_exceptions import ScoInvalidIdType
from app.scodoc.sco_permissions import Permission
from app.scodoc import html_sco_header
@ -156,33 +157,36 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0):
return htmlutils.make_menu("actions", menuEval, alone=True)
def _ue_coefs_html(coefs_descr) -> str:
def _ue_coefs_html(coefs_lst) -> str:
""" """
max_coef = max([x[1] for x in coefs_descr]) if coefs_descr else 1.0
max_coef = max([x[1] for x in coefs_lst]) if coefs_lst else 1.0
H = """
<div id="modimpl_coefs">
<div>Coefficients vers les UE</div>
"""
if coefs_descr:
H += f"""
if coefs_lst:
H += (
f"""
<div class="coefs_histo" style="--max:{max_coef}">
""" + "\n".join(
[
f"""<div style="--coef:{coef}"><div>{coef}</div>{ue_acronyme}</div>"""
for ue_acronyme, coef in coefs_descr
]
"""
+ "\n".join(
[
f"""<div style="--coef:{coef}"><div>{coef}</div>{ue.acronyme}</div>"""
for ue, coef in coefs_lst
]
)
+ "</div>"
)
else:
H += """<div class="missing_value">non définis</span>"""
H += """
</div>
</div>
"""
H += """<div class="missing_value">non définis</div>"""
H += "</div>"
return H
def moduleimpl_status(moduleimpl_id=None, partition_id=None):
"""Tableau de bord module (liste des evaluations etc)"""
if not isinstance(moduleimpl_id, int):
raise ScoInvalidIdType("moduleimpl_id must be an integer !")
modimpl = ModuleImpl.query.get_or_404(moduleimpl_id)
M = modimpl.to_dict()
formsemestre_id = M["formsemestre_id"]
@ -256,7 +260,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
H.append(scu.icontag("lock32_img", title="verrouillé"))
H.append("""</td><td class="fichetitre2">""")
if modimpl.module.is_apc():
H.append(_ue_coefs_html(modimpl.module.ue_coefs_descr()))
H.append(_ue_coefs_html(modimpl.module.ue_coefs_list()))
else:
H.append(f"Coef. dans le semestre: {modimpl.module.coefficient}")
H.append("""</td><td></td></tr>""")
@ -323,9 +327,10 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
#
if not modimpl.check_apc_conformity():
H.append(
"""<ul class="tf-msg"><li class="tf-msg warning conformite">Les poids des évaluations de ce module ne sont pas encore conformes au PN.
"""<div class="warning conformite">Les poids des évaluations de ce module ne sont
pas encore conformes au PN.
Ses notes ne peuvent pas être prises en compte dans les moyennes d'UE.
</li></ul>"""
</div>"""
)
#
if has_expression and nt.expr_diagnostics:

View File

@ -168,7 +168,7 @@ def can_change_groups(formsemestre_id):
"Vrai si l'utilisateur peut changer les groupes dans ce semestre"
from app.scodoc import sco_formsemestre
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
if not sem["etat"]:
return False # semestre verrouillé
if current_user.has_permission(Permission.ScoEtudChangeGroups):

View File

@ -307,7 +307,7 @@ class PlacementRunner:
self.evaluation_id,
self.groups,
getallstudents=get_all_students,
include_dems=True,
include_demdef=True,
)
listetud = [] # liste de couples (nom,prenom)
for etudid, etat in etudid_etats:

View File

@ -544,7 +544,7 @@ def check_paiement_etuds(etuds):
etud["paiementinscription_str"] = "(pb cnx Apogée)"
def get_maquette_apogee(etape="", annee_scolaire=""):
def get_maquette_apogee(etape="", annee_scolaire="") -> str:
"""Maquette CSV Apogee pour une étape et une annee scolaire"""
maquette_url = get_maquette_url()
if not maquette_url:

View File

@ -279,6 +279,7 @@ class BasePreferences(object):
{
"initvalue": "Dept",
"title": "Nom abrégé du département",
"explanation": "acronyme: par exemple R&T, ORTF, HAL",
"size": 12,
"category": "general",
"only_global": True,
@ -289,7 +290,7 @@ class BasePreferences(object):
{
"initvalue": "nom du département",
"title": "Nom complet du département",
"explanation": "inutilisé par défaut",
"explanation": "apparaît sur la page d'accueil",
"size": 40,
"category": "general",
"only_global": True,
@ -761,7 +762,7 @@ class BasePreferences(object):
{
"initvalue": "Helvetica",
"title": "Police de caractère principale",
"explanation": "pour les pdf",
"explanation": "pour les pdf (Helvetica est recommandée)",
"size": 25,
"category": "pdf",
},
@ -2149,7 +2150,7 @@ class SemPreferences(object):
) # a bug !
sem = sco_formsemestre.get_formsemestre(self.formsemestre_id)
H = [
html_sco_header.html_sem_header("Préférences du semestre", sem),
html_sco_header.html_sem_header("Préférences du semestre"),
"""
<p class="help">Les paramètres définis ici ne s'appliqueront qu'à ce semestre.</p>
<p class="msg">Attention: cliquez sur "Enregistrer les modifications" en bas de page pour appliquer vos changements !</p>

View File

@ -541,7 +541,6 @@ def formsemestre_pvjury(formsemestre_id, format="html", publish=True):
H = [
html_sco_header.html_sem_header(
"Décisions du jury pour le semestre",
sem,
init_qtip=True,
javascripts=["js/etud_info.js"],
),
@ -627,7 +626,6 @@ def formsemestre_pvjury_pdf(formsemestre_id, group_ids=[], etudid=None):
H = [
html_sco_header.html_sem_header(
"Edition du PV de jury %s" % etuddescr,
sem=sem,
javascripts=sco_groups_view.JAVASCRIPTS,
cssstyles=sco_groups_view.CSSSTYLES,
init_qtip=True,
@ -804,7 +802,6 @@ def formsemestre_lettres_individuelles(formsemestre_id, group_ids=[]):
H = [
html_sco_header.html_sem_header(
"Édition des lettres individuelles",
sem=sem,
javascripts=sco_groups_view.JAVASCRIPTS,
cssstyles=sco_groups_view.CSSSTYLES,
init_qtip=True,

View File

@ -37,7 +37,8 @@ from flask import make_response
from app import log
from app.but import bulletin_but
from app.comp.res_classic import ResultatsSemestreClassic
from app.comp import res_sem
from app.comp.res_common import NotesTableCompat
from app.models import FormSemestre
from app.models.etudiants import Identite
@ -303,14 +304,16 @@ def make_formsemestre_recapcomplet(
sem = sco_formsemestre.do_formsemestre_list(
args={"formsemestre_id": formsemestre_id}
)[0]
nt = sco_cache.NotesTableCache.get(formsemestre_id)
# XXX EXPERIMENTAL
# nt = ResultatsSemestreClassic(formsemestre)
parcours = formsemestre.formation.get_parcours()
# nt = sco_cache.NotesTableCache.get(formsemestre_id) # sco91
# sco92 :
nt: NotesTableCompat = res_sem.load_formsemestre_result(formsemestre)
modimpls = nt.get_modimpls_dict()
ues = nt.get_ues_stat_dict() # incluant le(s) UE de sport
#
if formsemestre.formation.is_apc():
nt.apc_recompute_moyennes()
# if formsemestre.formation.is_apc():
# nt.apc_recompute_moyennes()
#
partitions, partitions_etud_groups = sco_groups.get_formsemestre_groups(
formsemestre_id
@ -379,8 +382,6 @@ def make_formsemestre_recapcomplet(
h += ["code_nip", "etudid"]
F.append(h)
ue_index = [] # indices des moy UE dans l (pour appliquer style css)
def fmtnum(val): # conversion en nombre pour cellules excel
if keep_numeric:
try:
@ -431,9 +432,14 @@ def make_formsemestre_recapcomplet(
else:
l = [rank, nt.get_nom_short(etudid)] # rang, nom,
e["admission"] = {}
if not hidebac:
bac = sco_bac.Baccalaureat(e["bac"], e["specialite"])
l.append(bac.abbrev())
e["admission"] = nt.etuds_dict[etudid].admission.first()
if e["admission"]:
bac = nt.etuds_dict[etudid].admission[0].get_bac()
l.append(bac.abbrev())
else:
l.append("")
if format[:3] == "xls" or format == "csv": # tous les groupes
for partition in partitions:
@ -458,6 +464,12 @@ def make_formsemestre_recapcomplet(
for partition in partitions:
l.append(rang_gr[partition["partition_id"]])
# Nombre d'UE au dessus de 10
# t[i] est une chaine :-)
# nb_ue_ok = sum(
# [t[i] > 10 for i, ue in enumerate(ues, start=1) if ue["type"] != UE_SPORT]
# )
ue_index = [] # indices des moy UE dans l (pour appliquer style css)
for i, ue in enumerate(ues, start=1):
if ue["type"] != UE_SPORT:
l.append(
@ -486,7 +498,7 @@ def make_formsemestre_recapcomplet(
j += 1
if not hidebac:
for k in admission_extra_cols:
l.append(e[k])
l.append(getattr(e["admission"], k, ""))
l.append(
nt.identdict[etudid]["code_nip"] or ""
) # avant-derniere colonne = code_nip
@ -536,7 +548,7 @@ def make_formsemestre_recapcomplet(
# n'affiche pas la moyenne d'UE dans ce cas
if not hidemodules:
l.append("")
ue_index.append(len(l) - 1)
# ue_index.append(len(l) - 1)
if not hidemodules and not ue["is_external"]:
for modimpl in modimpls:
if modimpl["module"]["ue_id"] == ue["ue_id"]:
@ -654,7 +666,9 @@ def make_formsemestre_recapcomplet(
if disable_etudlink:
etudlink = "%(name)s"
else:
etudlink = '<a href="formsemestre_bulletinetud?formsemestre_id=%(formsemestre_id)s&etudid=%(etudid)s&version=selectedevals" id="%(etudid)s" class="etudinfo">%(name)s</a>'
etudlink = """<a
href="formsemestre_bulletinetud?formsemestre_id=%(formsemestre_id)s&etudid=%(etudid)s&version=selectedevals"
id="%(etudid)s" class="etudinfo">%(name)s</a>"""
ir = 0
nblines = len(F) - 1
for l in F[1:]:
@ -701,9 +715,7 @@ def make_formsemestre_recapcomplet(
idx_col_moy = idx_col_gr + 1
cssclass = "recap_col_moy"
try:
if float(nsn[idx_col_moy]) < (
nt.parcours.BARRE_MOY - scu.NOTES_TOLERANCE
):
if float(nsn[idx_col_moy]) < (parcours.BARRE_MOY - scu.NOTES_TOLERANCE):
cssclass = "recap_col_moy_inf"
except:
pass
@ -718,11 +730,11 @@ def make_formsemestre_recapcomplet(
if (ir < (nblines - 4)) or (ir == nblines - 3):
try:
if float(nsn[i]) < nt.parcours.get_barre_ue(
if float(nsn[i]) < parcours.get_barre_ue(
ue["type"]
): # NOTES_BARRE_UE
cssclass = "recap_col_ue_inf"
elif float(nsn[i]) >= nt.parcours.NOTES_BARRE_VALID_UE:
elif float(nsn[i]) >= parcours.NOTES_BARRE_VALID_UE:
cssclass = "recap_col_ue_val"
except:
pass
@ -732,7 +744,7 @@ def make_formsemestre_recapcomplet(
ir == nblines - 3
): # si moyenne generale module < barre ue, surligne:
try:
if float(nsn[i]) < nt.parcours.get_barre_ue(ue["type"]):
if float(nsn[i]) < parcours.get_barre_ue(ue["type"]):
cssclass = "recap_col_moy_inf"
except:
pass
@ -790,6 +802,11 @@ def make_formsemestre_recapcomplet(
for cod in cods:
H.append("<tr><td>%s</td><td>%d</td></tr>" % (cod, codes_nb[cod]))
H.append("</table>")
# Avertissements
if formsemestre.formation.is_apc():
H.append(
"""<p class="help">Pour les formations par compétences (comme le BUT), la moyenne générale est purement indicative et ne devrait pas être communiquée aux étudiants.</p>"""
)
return "\n".join(H), "", "html"
elif format == "csv":
CSV = scu.CSV_LINESEP.join(

View File

@ -49,16 +49,12 @@ from app.scodoc import sco_etud
from app.scodoc import sco_excel
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_formsemestre_status
from app.scodoc import sco_parcours_dut
from app.scodoc import sco_pdf
from app.scodoc import sco_preferences
import sco_version
from app.scodoc.gen_tables import GenTable
from app import log
from app.scodoc.sco_codes_parcours import code_semestre_validant
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_pdf import SU
MAX_ETUD_IN_DESCR = 20
@ -121,9 +117,9 @@ def _categories_and_results(etuds, category, result):
categories[etud[category]] = True
results[etud[result]] = True
categories = list(categories.keys())
categories.sort()
categories.sort(key=scu.heterogeneous_sorting_key)
results = list(results.keys())
results.sort()
results.sort(key=scu.heterogeneous_sorting_key)
return categories, results
@ -166,7 +162,7 @@ def _results_by_category(
l["sumpercent"] = "%2.1f%%" % ((100.0 * l["sum"]) / tot)
#
codes = list(results.keys())
codes.sort()
codes.sort(key=scu.heterogeneous_sorting_key)
bottom_titles = []
if C: # ligne du bas avec totaux:
@ -314,7 +310,7 @@ def formsemestre_report_counts(
"type_admission",
"boursier_prec",
]
keys.sort()
keys.sort(key=scu.heterogeneous_sorting_key)
F = [
"""<form name="f" method="get" action="%s"><p>
Colonnes: <select name="result" onchange="document.f.submit()">"""
@ -497,7 +493,7 @@ def table_suivi_cohorte(
P.append(p)
# 4-- regroupe par indice de semestre S_i
indices_sems = list(set([s["semestre_id"] for s in sems]))
indices_sems = list({s["semestre_id"] for s in sems})
indices_sems.sort()
for p in P:
p.nb_etuds = 0 # nombre total d'etudiants dans la periode
@ -788,9 +784,9 @@ def _gen_form_selectetuds(
):
"""HTML form pour choix criteres selection etudiants"""
bacs = list(bacs)
bacs.sort()
bacs.sort(key=scu.heterogeneous_sorting_key)
bacspecialites = list(bacspecialites)
bacspecialites.sort()
bacspecialites.sort(key=scu.heterogeneous_sorting_key)
# on peut avoir un mix de chaines vides et d'entiers:
annee_bacs = [int(x) if x else 0 for x in annee_bacs]
annee_bacs.sort()

View File

@ -308,13 +308,13 @@ def do_evaluation_set_missing(evaluation_id, value, dialog_confirmed=False):
# XXX imaginer un redirect + msg erreur
raise AccessDenied("Modification des notes impossible pour %s" % current_user)
#
NotesDB = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
etudid_etats = sco_groups.do_evaluation_listeetuds_groups(
evaluation_id, getallstudents=True, include_dems=False
evaluation_id, getallstudents=True, include_demdef=False
)
notes = []
for etudid, _ in etudid_etats: # pour tous les inscrits
if etudid not in NotesDB: # pas de note
if etudid not in notes_db: # pas de note
notes.append((etudid, value))
# Check value
L, invalids, _, _, _ = _check_notes(notes, E, M["module"])
@ -393,18 +393,18 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
):
# On a le droit de modifier toutes les notes
# recupere les etuds ayant une note
NotesDB = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
elif sco_permissions_check.can_edit_notes(
current_user, E["moduleimpl_id"], allow_ens=True
):
# Enseignant associé au module: ne peut supprimer que les notes qu'il a saisi
NotesDB = sco_evaluation_db.do_evaluation_get_all_notes(
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(
evaluation_id, by_uid=current_user.id
)
else:
raise AccessDenied("Modification des notes impossible pour %s" % current_user)
notes = [(etudid, scu.NOTES_SUPPRESS) for etudid in NotesDB.keys()]
notes = [(etudid, scu.NOTES_SUPPRESS) for etudid in notes_db.keys()]
if not dialog_confirmed:
nb_changed, nb_suppress, existing_decisions = notes_add(
@ -482,7 +482,7 @@ def notes_add(
inscrits = {
x[0]
for x in sco_groups.do_evaluation_listeetuds_groups(
evaluation_id, getallstudents=True, include_dems=True
evaluation_id, getallstudents=True, include_demdef=True
)
}
for (etudid, value) in notes:
@ -493,7 +493,7 @@ def notes_add(
"etudiant %s: valeur de note invalide (%s)" % (etudid, value)
)
# Recherche notes existantes
NotesDB = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
# Met a jour la base
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
@ -507,7 +507,7 @@ def notes_add(
try:
for (etudid, value) in notes:
changed = False
if etudid not in NotesDB:
if etudid not in notes_db:
# nouvelle note
if value != scu.NOTES_SUPPRESS:
if do_it:
@ -530,7 +530,7 @@ def notes_add(
changed = True
else:
# il y a deja une note
oldval = NotesDB[etudid]["value"]
oldval = notes_db[etudid]["value"]
if type(value) != type(oldval):
changed = True
elif type(value) == type(1.0) and (
@ -597,7 +597,7 @@ def notes_add(
nb_changed += 1
if has_existing_decision(M, E, etudid):
existing_decisions.append(etudid)
except:
except Exception as exc:
log("*** exception in notes_add")
if do_it:
cnx.rollback() # abort
@ -606,7 +606,7 @@ def notes_add(
formsemestre_id=M["formsemestre_id"]
) # > modif notes (exception)
sco_cache.EvaluationCache.delete(evaluation_id)
raise ScoGenError("Erreur enregistrement note: merci de ré-essayer")
raise ScoGenError("Erreur enregistrement note: merci de ré-essayer") from exc
if do_it:
cnx.commit()
sco_cache.invalidate_formsemestre(
@ -833,7 +833,7 @@ def feuille_saisie_notes(evaluation_id, group_ids=[]):
etudids = [
x[0]
for x in sco_groups.do_evaluation_listeetuds_groups(
evaluation_id, groups, getallstudents=getallstudents, include_dems=True
evaluation_id, groups, getallstudents=getallstudents, include_demdef=True
)
]
@ -1079,7 +1079,7 @@ def _form_saisie_notes(E, M, group_ids, destination=""):
etudids = [
x[0]
for x in sco_groups.do_evaluation_listeetuds_groups(
evaluation_id, getallstudents=True, include_dems=True
evaluation_id, getallstudents=True, include_demdef=True
)
]
if not etudids:

View File

@ -395,6 +395,8 @@ def do_semset_add_sem(semset_id, formsemestre_id):
"""Add a sem to a semset"""
if not semset_id:
raise ScoValueError("empty semset_id")
if formsemestre_id == "":
raise ScoValueError("pas de semestre choisi !")
s = SemSet(semset_id=semset_id)
# check for valid formsemestre_id
_ = sco_formsemestre.get_formsemestre(formsemestre_id) # raise exc

View File

@ -44,6 +44,7 @@ from app.scodoc import sco_groups_view
from app.scodoc import sco_preferences
from app.scodoc import sco_trombino
from app.scodoc import sco_etud
from app.scodoc.sco_exceptions import ScoPDFFormatError
from app.scodoc.sco_pdf import *
@ -268,7 +269,10 @@ def pdf_trombino_tours(
preferences=sco_preferences.SemPreferences(),
)
)
document.build(objects)
try:
document.build(objects)
except (ValueError, KeyError) as exc:
raise ScoPDFFormatError(str(exc)) from exc
data = report.getvalue()
return scu.sendPDFFile(data, filename)

View File

@ -220,7 +220,6 @@ def external_ue_create_form(formsemestre_id, etudid):
H = [
html_sco_header.html_sem_header(
"Ajout d'une UE externe pour %(nomprenom)s" % etud,
sem,
javascripts=["js/sco_ue_external.js"],
),
"""<p class="help">Cette page permet d'indiquer que l'étudiant a suivi une UE

View File

@ -93,6 +93,7 @@ MODULE_TYPE_NAMES = {
ModuleType.MALUS: "Malus",
ModuleType.RESSOURCE: "Ressource",
ModuleType.SAE: "SAÉ",
None: "Module",
}
MALUS_MAX = 20.0
@ -897,6 +898,11 @@ def sort_dates(L, reverse=False):
raise
def heterogeneous_sorting_key(x):
"key to sort non homogeneous sequences"
return (float(x), "") if isinstance(x, (bool, float, int)) else (-1e34, str(x))
def query_portal(req, msg="Portail Apogee", timeout=3):
"""Retreives external data using HTTP request
(used to connect to Apogee portal, or ScoDoc server)

View File

@ -0,0 +1,93 @@
:host{
font-family: Verdana;
background: #222;
display: block;
padding: 12px 32px;
color: #FFF;
max-width: 1000px;
margin: auto;
}
h1{
font-weight: 100;
}
/**********************/
/* Zone parcours */
/**********************/
.parcours{
display: flex;
gap: 4px;
padding-right: 4px;
}
.parcours>div{
background: #09c;
font-size: 18px;
text-align: center;
border-radius: 4px;
padding: 8px 16px;
cursor: pointer;
flex: 1;
transition: 0.1s;
opacity: 0.7;
}
.parcours>div:hover,
.competence>div:hover{
color: #ccc;
}
.parcours>.focus{
opacity: 1;
}
/**********************/
/* Zone compétences */
/**********************/
.competences{
display: grid;
margin-top: 8px;
row-gap: 4px;
}
.competences>div{
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
width: var(--competence-size);
margin-right: 4px;
}
.comp1{background:#a44}
.comp2{background:#84a}
.comp3{background:#a84}
.comp4{background:#8a4}
.comp5{background:#4a8}
.comp6{background:#48a}
.competences>.focus{
outline: 2px solid;
}
/**********************/
/* Zone AC */
/**********************/
h2{
display: table;
padding: 8px 16px;
font-size: 20px;
border-radius: 16px 0;
}
.ACs{
padding-right: 4px;
}
.AC li{
display: grid;
grid-template-columns: auto 1fr;
align-items: start;
gap: 4px;
margin-bottom: 4px;
border-bottom: 1px solid;
}
.AC li>div:nth-child(1){
padding: 2px 4px;
border-radius: 4px;
}
.AC li>div:nth-child(2){
padding-bottom: 2px;
}

View File

@ -77,6 +77,17 @@ section>div:nth-child(1){
display: flex !important;
}
.listeOff .ue::before,
.listeOff .module::before,
.moduleOnOff .ue::before,
.moduleOnOff .module::before{
transform: rotate(0);
}
.listeOff .moduleOnOff .ue::before,
.listeOff .moduleOnOff .module::before{
transform: rotate(180deg) !important;
}
/***********************/
/* Options d'affichage */
/***********************/
@ -118,11 +129,16 @@ section>div:nth-child(1){
/************/
/* Semestre */
/************/
.flex{
display: flex;
gap: 16px;
}
.infoSemestre{
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 4px;
flex: none;
}
.infoSemestre>div{
border: 1px solid var(--couleurIntense);
@ -141,7 +157,12 @@ section>div:nth-child(1){
.rang{
text-decoration: underline var(--couleurIntense);
}
.decision{
margin: 5px 0;
font-weight: bold;
font-size: 20px;
text-decoration: underline var(--couleurIntense);
}
.enteteSemestre{
color: black;
font-weight: bold;
@ -174,8 +195,21 @@ section>div:nth-child(1){
display: flex;
gap: 16px;
margin: 4px 0 2px 0;
overflow: auto;
overflow-x: auto;
overflow-y: hidden;
cursor: pointer;
position: relative;
}
.module::before, .ue::before {
content:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='26px' height='26px' fill='black'><path d='M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z' /></svg>");
width: 26px;
height: 26px;
position: absolute;
bottom: 0;
left: 50%;
margin-left: -13px;
transform: rotate(180deg);
transition: 0.2s;
}
h3{
display: flex;

View File

@ -239,6 +239,17 @@ div.box-chercheetud {
margin-top: 12px;
}
/* Page accueil général */
span.dept_full_name {
font-style: italic;
}
span.dept_visible {
color: rgb(6, 158, 6);
}
span.dept_cache {
color: rgb(194, 5, 5);
}
div.table_etud_in_accessible_depts {
margin-left: 3em;
margin-bottom: 2em;
@ -278,11 +289,13 @@ div.logo-insidebar {
div.logo-logo {
text-align: center ;
}
div.logo-logo img {
box-sizing: content-box;
margin-top: 20px;
width: 55px; /* 100px */
padding-right: 50px;
margin-top: -10px;
width: 128px;
padding-right: 5px;
margin-left: -75px;
}
div.sidebar-bottom {
margin-top: 10px;
@ -1344,7 +1357,13 @@ div.moduleimpl_type_ressource {
div#modimpl_coefs {
position: absolute;
border: 1px solid;
padding-top: 3px;
padding-left: 3px;
padding-right: 5px;
background-color: #d3d3d378;
}
.coefs_histo{
height: 32px;
display: flex;
@ -1652,7 +1671,10 @@ li.notes_ue_list {
margin-top: 9px;
list-style-type: none;
}
span.ue_type_1 {
color: green;
font-weight: bold;
}
span.ue_code {
font-family: Courier, monospace;
font-weight: normal;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,108 @@
class ref_competences extends HTMLElement {
constructor() {
super();
this.shadow = this.attachShadow({ mode: 'open' });
/* Template de base */
this.shadow.innerHTML = `
<div class=parcours></div>
<div class=competences></div>
<div class=ACs></div>
`;
/* Style du module */
const styles = document.createElement('link');
styles.setAttribute('rel', 'stylesheet');
if (location.href.split("/")[3] == "ScoDoc") {
styles.setAttribute('href', '/ScoDoc/static/css/ref-competences.css');
} else {
styles.setAttribute('href', 'ref-competences.css');
}
this.shadow.appendChild(styles);
}
set setData(data) {
this.data = data;
this.parcours();
}
parcours() {
let parcoursDIV = this.shadow.querySelector(".parcours");
Object.entries(this.data.parcours).forEach(([cle, parcours]) => {
let div = document.createElement("div");
div.innerText = parcours.libelle;
div.addEventListener("click", (event) => { this.competences(event, cle) })
parcoursDIV.appendChild(div);
})
this.initCompetences();
}
initCompetences() {
this.competencesNumber = {};
let i = 0;
Object.keys(this.data.competences).forEach(competence => {
this.competencesNumber[competence] = 1 + i++ % 6;
})
}
competences(event, cle) {
this.shadow.querySelector(".parcours>.focus")?.classList.remove("focus");
event.currentTarget.classList.add("focus");
let divCompetences = this.shadow.querySelector(".competences");
this.shadow.querySelector(".competences").innerHTML = "";
/* Création des compétences */
let competencesBucket = [];
Object.entries(this.data.parcours[cle].annees).forEach(([annee, dataAnnee]) => {
Object.entries(dataAnnee.competences).forEach(([competence, niveauCle]) => {
let numComp = this.competencesNumber[competence];
let divCompetence = document.createElement("div");
divCompetence.innerText = `${competence} ${niveauCle.niveau}`;
divCompetence.style.gridRowStart = annee;
divCompetence.style.gridColumnStart = competence;
divCompetence.className = "comp" + numComp;
divCompetence.dataset.competence = `${competence} ${niveauCle.niveau}`;
divCompetence.addEventListener("click", (event) => { this.AC(event, competence, niveauCle.niveau, annee, numComp) })
divCompetences.appendChild(divCompetence);
competencesBucket.push(competence);
})
})
/* Affectation de la taille des éléments */
//divCompetences.style.setProperty("--competence-size", `calc(${100 / competencesBucket.length}% )`);
let gridTemplate = "";
Object.keys(this.data.competences).forEach(competence => {
if (competencesBucket.indexOf(competence) == -1) {
gridTemplate += `[${competence}] 0`;
} else {
gridTemplate += `[${competence}] 1fr`;
}
})
this.shadow.querySelector(".competences").style.gridTemplateColumns = gridTemplate;
/* Réaffectation des focus */
this.shadow.querySelectorAll(".AC").forEach(ac => {
this.shadow.querySelector(`[data-competence="${ac.dataset.competence}"]`).classList.add("focus");
});
}
AC(event, competence, niveau, annee, numComp) {
event.currentTarget.classList.toggle("focus");
if (this.shadow.querySelector(`.ACs [data-competence="${competence} ${niveau}"]`)) {
this.shadow.querySelector(`.ACs [data-competence="${competence} ${niveau}"]`).remove();
} else {
let output = `
<ul class=AC data-competence="${competence} ${niveau}">
<h2 class=comp${numComp}>${competence} ${niveau}</h2>
`;
Object.entries(this.data.competences[competence].niveaux["BUT" + annee].app_critiques).forEach(([num, contenu]) => {
output += `<li><div class=comp${numComp}>${num}</div><div>${contenu.libelle}</div></li>`;
})
this.shadow.querySelector(".ACs").innerHTML += output + "</ul>";
}
}
}
customElements.define('ref-competences', ref_competences);

View File

@ -1,42 +1,49 @@
/* Module par Seb. L. */
class releveBUT extends HTMLElement {
constructor(){
constructor() {
super();
this.shadow = this.attachShadow({mode: 'open'});
this.shadow = this.attachShadow({ mode: 'open' });
/* Config par defaut */
this.config = {
showURL: true
};
/* Template du module */
this.shadow.innerHTML = this.template();
/* Style du module */
const styles = document.createElement('link');
styles.setAttribute('rel', 'stylesheet');
styles.setAttribute('href', '/ScoDoc/static/css/releve-but.css');
this.shadow.appendChild(styles);
/* variante "ScoDoc" ou "Passerelle" (ENT) ? */
if (location.href.split("/")[3] == "ScoDoc") { /* un peu osé... */
styles.setAttribute('href', '/ScoDoc/static/css/releve-but.css');
} else {
// Passerelle
styles.setAttribute('href', '/assets/styles/releve-but.css');
}
this.shadow.appendChild(styles);
}
listeOnOff() {
this.parentElement.parentElement.classList.toggle("listeOff");
this.parentElement.parentElement.querySelectorAll(".moduleOnOff").forEach(e=>{
this.parentElement.parentElement.querySelectorAll(".moduleOnOff").forEach(e => {
e.classList.remove("moduleOnOff")
})
}
moduleOnOff(){
moduleOnOff() {
this.parentElement.classList.toggle("moduleOnOff");
}
goTo(){
goTo() {
let module = this.dataset.module;
this.parentElement.parentElement.parentElement.parentElement.querySelector("#Module_" + module).scrollIntoView();
}
set setConfig(config){
set setConfig(config) {
this.config.showURL = config.showURL ?? this.config.showURL;
}
set showData(data) {
set showData(data) {
this.showInformations(data);
this.showSemestre(data);
this.showSynthese(data);
@ -46,7 +53,7 @@ class releveBUT extends HTMLElement {
this.shadow.querySelectorAll(".CTA_Liste").forEach(e => {
e.addEventListener("click", this.listeOnOff)
})
})
this.shadow.querySelectorAll(".ue, .module").forEach(e => {
e.addEventListener("click", this.moduleOnOff)
})
@ -57,7 +64,7 @@ class releveBUT extends HTMLElement {
this.shadow.children[0].classList.add("ready");
}
template(){
template() {
return `
<div>
<div class="wait"></div>
@ -75,10 +82,15 @@ class releveBUT extends HTMLElement {
<!--------------------------->
<section>
<h2>Semestre </h2>
<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>
<div class=infoSemestre></div>
<div class=flex>
<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>
</div>
</div>
</section>
<!--------------------------->
@ -91,8 +103,7 @@ class releveBUT extends HTMLElement {
<em>La moyenne des ressources dans une UE dépend des poids donnés aux évaluations.</em>
</div>
<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">
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" />
</svg>
</div>
@ -107,8 +118,7 @@ class releveBUT extends HTMLElement {
<div>
<h2>Ressources</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">
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" />
</svg>
</div>
@ -120,8 +130,7 @@ class releveBUT extends HTMLElement {
<div>
<h2>SAÉ</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">
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" />
</svg>
</div>
@ -140,8 +149,8 @@ class releveBUT extends HTMLElement {
this.shadow.querySelector(".studentPic").src = data.etudiant.photo_url || "default_Student.svg";
let output = '';
if(this.config.showURL){
if (this.config.showURL) {
output += `<a href="${data.etudiant.fiche_url}" class=info_etudiant>`;
} else {
output += `<div class=info_etudiant>`;
@ -165,7 +174,7 @@ class releveBUT extends HTMLElement {
</div>
<div>${data.formation.titre}</div>
`;
if(this.config.showURL){
if (this.config.showURL) {
output += `</a>`;
} else {
output += `</div>`;
@ -187,21 +196,21 @@ class releveBUT extends HTMLElement {
<div>Max. promo. :</div><div>${data.semestre.notes.max}</div>
<div>Moy. promo. :</div><div>${data.semestre.notes.moy}</div>
<div>Min. promo. :</div><div>${data.semestre.notes.min}</div>
</div>
${data.semestre.groupes.map(groupe => {
</div>`;
/*${data.semestre.groupes.map(groupe => {
return `
<div>
<div class=enteteSemestre>Groupe</div><div class=enteteSemestre>${groupe.nom}</div>
<div class=rang>Rang :</div><div class=rang>${groupe.rang.value} / ${groupe.rang.total}</div>
<div>Max. groupe :</div><div>${groupe.notes.max}</div>
<div>Moy. groupe :</div><div>${groupe.notes.min}</div>
<div>Min. groupe :</div><div>${groupe.notes.min}</div>
</div>
`;
}).join("")
}
`;
<div>
<div class=enteteSemestre>Groupe</div><div class=enteteSemestre>${groupe.nom}</div>
<div class=rang>Rang :</div><div class=rang>${groupe.rang.value} / ${groupe.rang.total}</div>
<div>Max. groupe :</div><div>${groupe.notes.max}</div>
<div>Moy. groupe :</div><div>${groupe.notes.min}</div>
<div>Min. groupe :</div><div>${groupe.notes.min}</div>
</div>
`;
}).join("")
}*/
this.shadow.querySelector(".infoSemestre").innerHTML = output;
/*this.shadow.querySelector(".decision").innerHTML = data.semestre.decision.code;*/
}
/*******************************/
@ -211,6 +220,7 @@ class releveBUT extends HTMLElement {
let output = ``;
Object.entries(data.ues).forEach(([ue, dataUE]) => {
output += `
<div>
<div class=ue>
<h3>
@ -255,7 +265,7 @@ class releveBUT extends HTMLElement {
})
return output;
}
/*******************************/
/* Evaluations */
/*******************************/
@ -333,8 +343,8 @@ class releveBUT extends HTMLElement {
/********************/
/* Fonctions d'aide */
/********************/
URL(href, content){
if(this.config.showURL){
URL(href, content) {
if (this.config.showURL) {
return `<a href=${href}>${content}</a>`;
} else {
return content;

View File

@ -24,7 +24,7 @@
{% block app_content %}
<h1>Modification du compte ScoDoc <tt>{{form.user_name.data}}</tt></h1>
<div class="help">
<p>Identifiez-vous avez votre mot de passe actuel</p>
<p>Identifiez-vous avec votre mot de passe actuel</p>
</div>
<form method=post>
{{ form.user_name }}

View File

@ -1,17 +1,40 @@
{# -*- mode: jinja-html -*- #}
{% extends "sco_page.html" %}
{% block styles %}
{{super()}}
{% endblock %}
{% block app_content %}
<h2>Référentiel de compétences {{ref.type_titre}} {{ref.specialite_long}}</h2>
<div>
Chargé le {{ref.scodoc_date_loaded.strftime("%d/%m/%Y à %H:%M") if ref.scodoc_date_loaded else ""}} à partir du fichier <tt>{{ref.scodoc_orig_filename or "(inconnu)"}}</tt>.
<ref-competences></ref-competences>
<script src="/ScoDoc/static/js/ref_competences.js"></script>
<div class="help">
Référentiel chargé le {{ref.scodoc_date_loaded.strftime("%d/%m/%Y à %H:%M") if ref.scodoc_date_loaded else ""}} à partir du fichier <tt>{{ref.scodoc_orig_filename or "(inconnu)"}}</tt>.
</div>
<div class="part2">
<a class="stdlink" href="{{url_for('notes.refcomp_table', scodoc_dept=g.scodoc_dept)}}">revenir à la liste des référentiels</a>
<a class="stdlink"
href="{{url_for('notes.refcomp_table', scodoc_dept=g.scodoc_dept)}}"
>revenir à la liste des référentiels</a>
</div>
{% endblock %}
{% block scripts %}
{{super()}}
<script>
$(function () {
let data_url = "{{data_source}}";
$.getJSON(data_url, function (data) {
document.querySelector("ref-competences").setData = data;
});
});
</script>
{% endblock %}

View File

@ -24,17 +24,16 @@
{{icons.arrow_none|safe}}
{% endif %}
</span>
{% if editable and not ue.modules.count() %}
<a class="smallbutton" href="{{ url_for('notes.ue_delete',
scodoc_dept=g.scodoc_dept, ue_id=ue.id)
}}">{{icons.delete|safe}}</a>
{% else %}
{{icons.delete_disabled|safe}}
{% endif %}
}}">{% if editable and not ue.modules.count() %}{{icons.delete|safe}}{% else %}{{icons.delete_disabled|safe}}{% endif %}</a>
<span class="ue_type_{{ue.type}}">
<b>{{ue.acronyme}}</b> <a class="discretelink" href="{{
url_for('notes.ue_infos', scodoc_dept=g.scodoc_dept, ue_id=ue.id)}}"
>{{ue.titre}}</a>
</span>
{% if editable and not ue.is_locked() %}
<a class="stdlink" href="{{ url_for('notes.ue_edit',

View File

@ -49,8 +49,12 @@
{{ moment.lang(g.locale) }}
<script src="/ScoDoc/static/libjs/menu.js"></script>
<script src="/ScoDoc/static/libjs/bubble.js"></script>
<script src="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
<script src="/ScoDoc/static/jQuery/jquery.js"></script>
<script src="/ScoDoc/static/jQuery/jquery-migrate-1.2.0.min.js"></script>
<script src="/ScoDoc/static/libjs/jquery.field.min.js"></script>
<script src="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>
<script src="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
<script src="/ScoDoc/static/js/scodoc.js"></script>
<script src="/ScoDoc/static/DataTables/datatables.min.js"></script>
<script>

View File

@ -8,10 +8,9 @@
{{ exc | safe }}
<p class="footer">
<p>
{% if g.scodoc_dept %}
<a href="{{ exc.dest_url or url_for('scolar.index_html', scodoc_dept=g.scodoc_dept) }}">retour page d'accueil
departement {{ g.scodoc_dept }}</a>
<a href="{{ exc.dest_url or url_for('scolar.index_html', scodoc_dept=g.scodoc_dept) }}">continuer</a>
{% else %}
<a href="{{ exc.dest_url or url_for('scodoc.index') }}">retour page d'accueil</a>
{% endif %}

View File

@ -13,11 +13,25 @@
<ul class="main">
{% for dept in depts %}
{% if dept.visible or current_user.is_administrator() %}
<li>
<a class="stdlink {{'link_accessible' if current_user.has_permission(Permission.ScoView, dept=dept.acronym) else 'link_unauthorized'}}"
href="{{url_for('scolar.index_html', scodoc_dept=dept.acronym)}}">Département
{{dept.preferences.filter_by(name="DeptName").first().value}}</a>
{{dept.preferences.filter_by(name="DeptName").first().value}}
</a>
<span class="dept_full_name">
{{ dept.preferences.filter_by( name="DeptFullName" ).first().value or "" }}
</span>
{% if current_user.is_administrator() %}
<span {% if dept.visible %}class="dept_visible">visible{% else %}class="dept_cache">caché aux utilisateurs{% endif %}
</span>
<a href="{{ url_for('scodoc.toggle_dept_vis', dept_id=dept.id) }}">
{% if dept.visible %}cacher{% else %}rendre visible{% endif %}
</a>
</span>
{% endif %}
</li>
{% endif %}
{% else %}
<li>
<b>Aucun département défini !</b>
@ -44,8 +58,8 @@
</div> -->
<div style="margin-top: 1cm;">
Service réservé aux personnels et enseignants, basé sur <a href="{{url_for('scodoc.about')}}">le logiciel libre
ScoDoc.</a>
Service <b>réservé aux personnels et enseignants</b>, basé sur
<a href="{{scu.SCO_WEBSITE}}">le logiciel libre ScoDoc.</a>
</div>
{% endblock %}

View File

@ -4,36 +4,36 @@
<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 9.2a</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)
url_for('users.user_info_page', scodoc_dept=g.scodoc_dept, user_name=current_user.user_name)
}}">{{current_user.user_name}}</a>
<br /><a id="deconnectlink" href="{{url_for(" auth.logout")}}">déconnexion</a>
<br /><a id="deconnectlink" href="{{url_for('auth.logout')}}">déconnexion</a>
</div>
{% block sidebar_dept %}
<h2 class="insidebar">Dépt. {{ sco.prefs["DeptName"] }}</h2>
<a href="{{ url_for('scolar.index_html', scodoc_dept=g.scodoc_dept) }}" class="sidebar">Accueil</a> <br />
{% if sco.prefs["DeptIntranetURL"] %}
<a href="{{ sco.prefs[" DeptIntranetURL"] }}" class="sidebar">
<a href="{{ sco.prefs["DeptIntranetURL"] }}" class="sidebar">
{{ sco.prefs["DeptIntranetTitle"] }}</a>
{% endif %}
<br>
{% endblock %}
<h2 class="insidebar">Scolarité</h2>
<a href="{{url_for(" scolar.index_html", scodoc_dept=g.scodoc_dept)}}" class="sidebar">Semestres</a> <br>
<a href="{{url_for(" notes.index_html", scodoc_dept=g.scodoc_dept)}}" class="sidebar">Programmes</a> <br>
<a href="{{url_for(" absences.index_html", scodoc_dept=g.scodoc_dept)}}" class="sidebar">Absences</a> <br>
<a href="{{url_for('scolar.index_html', scodoc_dept=g.scodoc_dept)}}" class="sidebar">Semestres</a> <br>
<a href="{{url_for('notes.index_html', scodoc_dept=g.scodoc_dept)}}" class="sidebar">Programmes</a> <br>
<a href="{{url_for('absences.index_html', scodoc_dept=g.scodoc_dept)}}" class="sidebar">Absences</a> <br>
{% if current_user.has_permission(sco.Permission.ScoUsersAdmin)
or current_user.has_permission(sco.Permission.ScoUsersView)
%}
<a href="{{url_for(" users.index_html", scodoc_dept=g.scodoc_dept)}}" class="sidebar">Utilisateurs</a> <br />
<a href="{{url_for('users.index_html', scodoc_dept=g.scodoc_dept)}}" class="sidebar">Utilisateurs</a> <br />
{% endif %}
{% if current_user.has_permission(sco.Permission.ScoChangePreferences) %}
<a href="{{url_for(" scolar.edit_preferences", scodoc_dept=g.scodoc_dept)}}" class="sidebar">Paramétrage</a> <br>
<a href="{{url_for('scolar.edit_preferences', scodoc_dept=g.scodoc_dept)}}" class="sidebar">Paramétrage</a> <br>
{% endif %}
{# /sidebar_common #}
@ -49,7 +49,7 @@
<div class="etud-insidebar">
{% if sco.etud %}
<h2 id="insidebar-etud"><a href="{{url_for(
" scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=sco.etud.id )}}" class="sidebar">
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=sco.etud.id )}}" class="sidebar">
<span class="fontred">{{sco.etud.nomprenom}}</span></a>
</h2>
<b>Absences</b>

View File

@ -940,9 +940,7 @@ def EtatAbsencesGr(
init_qtip=True,
javascripts=["js/etud_info.js"],
),
html_title=html_sco_header.html_sem_header(
"%s" % title, sem, with_page_header=False
)
html_title=html_sco_header.html_sem_header("%s" % title, with_page_header=False)
+ "<p>Période du %s au %s (nombre de <b>demi-journées</b>)<br/>" % (debut, fin),
base_url="%s&formsemestre_id=%s&debut=%s&fin=%s"
% (groups_infos.base_url, formsemestre_id, debut, fin),
@ -1133,8 +1131,8 @@ def AddBilletAbsenceForm(etudid):
scu.get_request_args(),
(
("etudid", {"input_type": "hidden"}),
("begin", {"input_type": "date"}),
("end", {"input_type": "date"}),
("begin", {"input_type": "datedmy"}),
("end", {"input_type": "datedmy"}),
(
"justified",
{"input_type": "boolcheckbox", "default": 0, "title": "Justifiée"},

View File

@ -72,12 +72,7 @@ from app import log, send_scodoc_alarm
from app.scodoc import scolog
from app.scodoc.scolog import logdb
from app.scodoc.sco_exceptions import (
ScoValueError,
ScoLockedFormError,
ScoGenError,
AccessDenied,
)
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError, ScoInvalidIdType
from app.scodoc import html_sco_header
from app.pe import pe_view
from app.scodoc import sco_abs
@ -284,9 +279,12 @@ def formsemestre_bulletinetud(
force_publishing=False,
prefer_mail_perso=False,
code_nip=None,
code_ine=None,
):
if not formsemestre_id:
flask.abort(404, "argument manquant: formsemestre_id")
if not isinstance(formsemestre_id, int):
raise ScoInvalidIdType("formsemestre_id must be an integer !")
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if formsemestre.formation.is_apc() and format != "oldjson":
if etudid:
@ -295,6 +293,14 @@ def formsemestre_bulletinetud(
etud = models.Identite.query.filter_by(
code_nip=str(code_nip)
).first_or_404()
elif code_ine:
etud = models.Identite.query.filter_by(
code_ine=str(code_ine)
).first_or_404()
else:
raise ScoValueError(
"Paramètre manquant: spécifier code_nip ou etudid ou code_ine"
)
if format == "json":
r = bulletin_but.BulletinBUT(formsemestre)
return jsonify(r.bulletin_etud(etud, formsemestre))
@ -312,8 +318,10 @@ def formsemestre_bulletinetud(
sco=ScoData(),
)
if not (etudid or code_nip):
raise ScoValueError("Paramètre manquant: spécifier code_nip ou etudid")
if not (etudid or code_nip or code_ine):
raise ScoValueError(
"Paramètre manquant: spécifier code_nip ou etudid ou code_ine"
)
if format == "oldjson":
format = "json"
return sco_bulletins.formsemestre_bulletinetud(
@ -744,6 +752,10 @@ def XMLgetFormsemestres(etape_apo=None, formsemestre_id=None):
DEPRECATED: use formsemestre_list()
"""
current_app.logger.debug("Warning: calling deprecated XMLgetFormsemestres")
if not formsemestre_id:
return flask.abort(404, "argument manquant: formsemestre_id")
if not isinstance(formsemestre_id, int):
return flask.abort(404, "formsemestre_id must be an integer !")
args = {}
if etape_apo:
args["etape_apo"] = etape_apo
@ -969,7 +981,6 @@ def edit_moduleimpl_resp(moduleimpl_id):
html_sco_header.html_sem_header(
'Modification du responsable du <a href="moduleimpl_status?moduleimpl_id=%s">module %s</a>'
% (moduleimpl_id, M["module"]["titre"]),
sem,
javascripts=["libjs/AutoSuggest.js"],
cssstyles=["css/autosuggest_inquisitor.css"],
bodyOnLoad="init_tf_form('')",
@ -1090,7 +1101,6 @@ def edit_moduleimpl_expr(moduleimpl_id):
html_sco_header.html_sem_header(
'Modification règle de calcul du <a href="moduleimpl_status?moduleimpl_id=%s">module %s</a>'
% (moduleimpl_id, M["module"]["titre"]),
sem,
),
_EXPR_HELP
% {
@ -1197,7 +1207,6 @@ def view_module_abs(moduleimpl_id, format="html"):
'Absences du <a href="moduleimpl_status?moduleimpl_id=%s">module %s</a>'
% (moduleimpl_id, M["module"]["titre"]),
page_title="Absences du module %s" % (M["module"]["titre"]),
sem=sem,
)
]
if not T and format == "html":
@ -1246,7 +1255,6 @@ def edit_ue_expr(formsemestre_id, ue_id):
html_sco_header.html_sem_header(
"Modification règle de calcul de l'UE %s (%s)"
% (ue["acronyme"], ue["titre"]),
sem,
),
_EXPR_HELP % {"target": "de l'UE", "objs": "modules", "ordre": ""},
]
@ -1384,7 +1392,7 @@ def formsemestre_enseignants_list(formsemestre_id, format="html"):
html_class="table_leftalign",
filename=scu.make_filename("Enseignants-" + sem["titreannee"]),
html_title=html_sco_header.html_sem_header(
"Enseignants du semestre", sem, with_page_header=False
"Enseignants du semestre", with_page_header=False
),
base_url="%s?formsemestre_id=%s" % (request.base_url, formsemestre_id),
caption="Tous les enseignants (responsables ou associés aux modules de ce semestre) apparaissent. Le nombre de saisies d'absences est le nombre d'opérations d'ajout effectuées sur ce semestre, sans tenir compte des annulations ou double saisies.",
@ -1897,7 +1905,7 @@ def formsemestre_bulletins_choice(
"""Choix d'une version de bulletin"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
H = [
html_sco_header.html_sem_header(title, sem),
html_sco_header.html_sem_header(title),
"""
<form name="f" method="GET" action="%s">
<input type="hidden" name="formsemestre_id" value="%s"></input>

View File

@ -49,6 +49,11 @@ def refcomp_show(refcomp_id):
ref=ref,
title="Référentiel de compétences",
sco=ScoData(),
data_source=url_for(
"notes.refcomp",
scodoc_dept=g.scodoc_dept,
refcomp_id=refcomp_id,
),
)
@ -172,11 +177,13 @@ def refcomp_load(formation_id=None):
filename = secure_filename(f.filename)
try:
xml_data = f.read()
ref = orebut_import_refcomp(
_ = orebut_import_refcomp(
xml_data, dept_id=g.scodoc_dept_id, orig_filename=filename
)
except TypeError as exc:
raise ScoFormatError("fichier XML Orébut invalide") from exc
raise ScoFormatError(
f"fichier XML Orébut invalide (1): {exc.args}"
) from exc
except ScoFormatError:
raise

View File

@ -53,6 +53,7 @@ from wtforms.fields.simple import BooleanField, StringField, TextAreaField, Hidd
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
import app
from app import db
from app.forms.main import config_forms
from app.forms.main.create_dept import CreateDeptForm
from app.models import Departement, Identite
@ -82,15 +83,14 @@ from PIL import Image as PILImage
@bp.route("/ScoDoc/index")
def index():
"Page d'accueil: liste des départements"
depts = (
Departement.query.filter_by(visible=True).order_by(Departement.acronym).all()
)
depts = Departement.query.filter_by().order_by(Departement.acronym).all()
return render_template(
"scodoc.html",
title=sco_version.SCONAME,
current_app=flask.current_app,
depts=depts,
Permission=Permission,
scu=scu,
)
@ -108,7 +108,11 @@ def create_dept():
if request.method == "POST" and form.cancel.data: # cancel button
return redirect(url_for("scodoc.index"))
if form.validate_on_submit():
departements.create_dept(form.acronym.data)
departements.create_dept(
form.acronym.data,
visible=form.visible.data,
# description=form.description.data,
)
flash(f"Département {form.acronym.data} créé.")
return redirect(url_for("scodoc.index"))
return render_template(
@ -118,6 +122,17 @@ def create_dept():
)
@bp.route("/ScoDoc/toggle_dept_vis/<dept_id>", methods=["GET", "POST"])
@admin_required
def toggle_dept_vis(dept_id):
"""Cache ou rend visible un dept"""
dept = Departement.query.get_or_404(dept_id)
dept.visible = not dept.visible
db.session.add(dept)
db.session.commit()
return redirect(url_for("scodoc.index"))
@bp.route("/ScoDoc/table_etud_in_accessible_depts", methods=["POST"])
@login_required
def table_etud_in_accessible_depts():
@ -190,6 +205,7 @@ def search_inscr_etud_by_nip(code_nip, format="json", __ac_name="", __ac_passwor
@bp.route("/ScoDoc/about")
@bp.route("/ScoDoc/Scolarite/<scodoc_dept>/about")
@login_required
def about(scodoc_dept=None):
"version info"
return render_template(

View File

@ -1701,8 +1701,7 @@ def check_group_apogee(group_id, etat=None, fix=False, fixmail=False):
cnx = ndb.GetDBConnexion()
H = [
html_sco_header.html_sem_header(
"Etudiants du %s" % (group["group_name"] or "semestre"),
sem,
"Etudiants du %s" % (group["group_name"] or "semestre")
),
'<table class="sortable" id="listegroupe">',
"<tr><th>Nom</th><th>Nom usuel</th><th>Prénom</th><th>Mail</th><th>NIP (ScoDoc)</th><th>Apogée</th></tr>",

View File

@ -151,8 +151,9 @@ def user_info(user_name, format="json"):
@scodoc7func
def create_user_form(user_name=None, edit=0, all_roles=1):
"form. création ou edition utilisateur"
if user_name is not None: # scodoc7func converti en int !
user_name = str(user_name)
auth_dept = current_user.dept
auth_username = current_user.user_name
from_mail = current_user.email
initvalues = {}
edit = int(edit)
@ -204,7 +205,7 @@ def create_user_form(user_name=None, edit=0, all_roles=1):
administrable_dept_acronyms = sorted(
set(
[
x.dept
x.dept or ""
for x in UserRole.query.filter_by(user=current_user)
if x.role.has_permission(Permission.ScoUsersAdmin) and x.dept
]
@ -249,7 +250,7 @@ def create_user_form(user_name=None, edit=0, all_roles=1):
r.name + "_" + (dept or "") for (r, dept) in displayed_roles
]
displayed_roles_labels = [f"{dept}: {r.name}" for (r, dept) in displayed_roles]
disabled_roles = {} # pour desactiver les roles que l'on ne peut pas editer
disabled_roles = {} # pour désactiver les roles que l'on ne peut pas éditer
for i in range(len(displayed_roles_strings)):
if displayed_roles_strings[i] not in editable_roles_strings:
disabled_roles[i] = True
@ -375,7 +376,7 @@ def create_user_form(user_name=None, edit=0, all_roles=1):
can_choose_dept = True
else:
selectable_dept_acronyms = set(administrable_dept_acronyms)
if edit: # ajoute dept actuel de l'utilisateur
if edit and the_user.dept is not None: # ajoute dept actuel de l'utilisateur
selectable_dept_acronyms |= {the_user.dept}
if len(selectable_dept_acronyms) > 1:
can_choose_dept = True
@ -389,6 +390,9 @@ def create_user_form(user_name=None, edit=0, all_roles=1):
"explanation": """département de rattachement de l'utilisateur""",
"labels": selectable_dept_acronyms,
"allowed_values": selectable_dept_acronyms,
"default": g.scodoc_dept
if g.scodoc_dept in selectable_dept_acronyms
else "",
},
)
)
@ -422,7 +426,7 @@ def create_user_form(user_name=None, edit=0, all_roles=1):
"date_expiration",
{
"title": "Date d'expiration", # j/m/a
"input_type": "date",
"input_type": "datedmy",
"explanation": "j/m/a, laisser vide si pas de limite",
"size": 9,
"allow_null": True,
@ -541,10 +545,10 @@ def create_user_form(user_name=None, edit=0, all_roles=1):
vals["active"] = vals["status"] == ""
# Département:
if auth_dept: # pas super-admin
if vals["dept"] not in selectable_dept_acronyms:
if ("dept" in vals) and (vals["dept"] not in selectable_dept_acronyms):
del vals["dept"] # ne change pas de dept
# traitement des roles: ne doit pas affecter les roles
# que l'on en controle pas:
# Traitement des roles: ne doit pas affecter les rôles
# que l'on en contrôle pas:
for role in orig_roles_strings: # { "Ens_RT", "Secr_CJ", ... }
if role and not role in editable_roles_strings:
roles.add(role)
@ -573,7 +577,7 @@ def create_user_form(user_name=None, edit=0, all_roles=1):
# A: envoi de welcome + procedure de reset
# B: envoi de welcome seulement (mot de passe saisie dans le formulaire)
# C: Aucun envoi (mot de passe saisi dans le formulaire)
if vals["welcome:list"] == "1":
if vals["welcome"] == "1":
if vals["reset_password:list"] == "1":
mode = Mode.WELCOME_AND_CHANGE_PASSWORD
else:
@ -743,6 +747,8 @@ def user_info_page(user_name=None):
"""
from app.scodoc.sco_permissions_check import can_handle_passwd
if user_name is not None: # scodoc7func converti en int !
user_name = str(user_name)
# peut on divulguer ces infos ?
if not can_handle_passwd(current_user, allow_admindepts=True):
raise AccessDenied("Vous n'avez pas la permission de voir cette page")
@ -800,6 +806,8 @@ def form_change_password(user_name=None):
"""Formulaire de changement mot de passe de l'utilisateur user_name.
Un utilisateur peut toujours changer son propre mot de passe.
"""
if user_name is not None: # scodoc7func converti en int !
user_name = str(user_name)
if not user_name:
user = current_user
else:
@ -848,6 +856,8 @@ def form_change_password(user_name=None):
@scodoc7func
def change_password(user_name, password, password2):
"Change the password for user given by user_name"
if user_name is not None: # scodoc7func converti en int !
user_name = str(user_name)
u = User.query.filter_by(user_name=user_name).first()
# Check access permission
if not can_handle_passwd(u):
@ -907,6 +917,8 @@ def change_password(user_name, password, password2):
@permission_required(Permission.ScoUsersAdmin)
def toggle_active_user(user_name: str = None):
"""Change active status of a user account"""
if user_name is not None: # scodoc7func converti en int !
user_name = str(user_name)
u = User.query.filter_by(user_name=user_name).first()
if not u:
raise ScoValueError("invalid user_name")

View File

@ -0,0 +1,62 @@
"""Evolution ref. Orebut
Revision ID: 197c658cefbb
Revises: 91be8a06d423
Create Date: 2022-01-05 22:25:12.384647
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "197c658cefbb"
down_revision = "91be8a06d423"
branch_labels = None
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"],
unique=True,
)
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),
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
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 ###

View File

@ -1,283 +0,0 @@
"""creation tables relations entreprises
Revision ID: f3b62d64efa3
Revises: 91be8a06d423
Create Date: 2021-12-24 10:36:27.150085
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = "f3b62d64efa3"
down_revision = "91be8a06d423"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"entreprise_log",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column(
"date",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=True,
),
sa.Column("authenticated_user", sa.Text(), nullable=True),
sa.Column("object", sa.Integer(), nullable=True),
sa.Column("text", sa.Text(), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"entreprise_etudiant",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("entreprise_id", sa.Integer(), nullable=True),
sa.Column("etudid", sa.Integer(), nullable=True),
sa.Column("type_offre", sa.Text(), nullable=True),
sa.Column("date_debut", sa.Date(), nullable=True),
sa.Column("date_fin", sa.Date(), nullable=True),
sa.Column("formation_text", sa.Text(), nullable=True),
sa.Column("formation_scodoc", sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(
["entreprise_id"], ["entreprises.id"], ondelete="cascade"
),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"entreprise_offre",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("entreprise_id", sa.Integer(), nullable=True),
sa.Column(
"date_ajout",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=True,
),
sa.Column("intitule", sa.Text(), nullable=True),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("type_offre", sa.Text(), nullable=True),
sa.Column("missions", sa.Text(), nullable=True),
sa.Column("duree", sa.Text(), nullable=True),
sa.ForeignKeyConstraint(
["entreprise_id"], ["entreprises.id"], ondelete="cascade"
),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"entreprise_envoi_offre",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("sender_id", sa.Integer(), nullable=True),
sa.Column("receiver_id", sa.Integer(), nullable=True),
sa.Column("offre_id", sa.Integer(), nullable=True),
sa.Column(
"date_envoi",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=True,
),
sa.ForeignKeyConstraint(
["offre_id"],
["entreprise_offre.id"],
),
sa.ForeignKeyConstraint(
["sender_id"],
["user.id"],
),
sa.ForeignKeyConstraint(
["receiver_id"],
["user.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"entreprise_envoi_offre_etudiant",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("sender_id", sa.Integer(), nullable=True),
sa.Column("receiver_id", sa.Integer(), nullable=True),
sa.Column("offre_id", sa.Integer(), nullable=True),
sa.Column(
"date_envoi",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=True,
),
sa.ForeignKeyConstraint(
["offre_id"],
["entreprise_offre.id"],
),
sa.ForeignKeyConstraint(
["sender_id"],
["user.id"],
),
sa.ForeignKeyConstraint(
["receiver_id"],
["identite.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.drop_constraint(
"entreprise_contact_entreprise_corresp_id_fkey",
"entreprise_contact",
type_="foreignkey",
)
op.drop_table("entreprise_correspondant")
op.add_column("entreprise_contact", sa.Column("nom", sa.Text(), nullable=True))
op.add_column("entreprise_contact", sa.Column("prenom", sa.Text(), nullable=True))
op.add_column(
"entreprise_contact", sa.Column("telephone", sa.Text(), nullable=True)
)
op.add_column("entreprise_contact", sa.Column("mail", sa.Text(), nullable=True))
op.add_column("entreprise_contact", sa.Column("poste", sa.Text(), nullable=True))
op.add_column("entreprise_contact", sa.Column("service", sa.Text(), nullable=True))
op.drop_column("entreprise_contact", "description")
op.drop_column("entreprise_contact", "enseignant")
op.drop_column("entreprise_contact", "date")
op.drop_column("entreprise_contact", "type_contact")
op.drop_column("entreprise_contact", "etudid")
op.drop_column("entreprise_contact", "entreprise_corresp_id")
op.add_column("entreprises", sa.Column("siret", sa.Text(), nullable=True))
op.drop_index("ix_entreprises_dept_id", table_name="entreprises")
op.drop_constraint("entreprises_dept_id_fkey", "entreprises", type_="foreignkey")
op.drop_column("entreprises", "qualite_relation")
op.drop_column("entreprises", "note")
op.drop_column("entreprises", "contact_origine")
op.drop_column("entreprises", "plus10salaries")
op.drop_column("entreprises", "privee")
op.drop_column("entreprises", "secteur")
op.drop_column("entreprises", "date_creation")
op.drop_column("entreprises", "dept_id")
op.drop_column("entreprises", "localisation")
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"entreprises",
sa.Column("localisation", sa.TEXT(), autoincrement=False, nullable=True),
)
op.add_column(
"entreprises",
sa.Column("dept_id", sa.INTEGER(), autoincrement=False, nullable=True),
)
op.add_column(
"entreprises",
sa.Column(
"date_creation",
postgresql.TIMESTAMP(timezone=True),
server_default=sa.text("now()"),
autoincrement=False,
nullable=True,
),
)
op.add_column(
"entreprises",
sa.Column("secteur", sa.TEXT(), autoincrement=False, nullable=True),
)
op.add_column(
"entreprises",
sa.Column("privee", sa.TEXT(), autoincrement=False, nullable=True),
)
op.add_column(
"entreprises",
sa.Column("plus10salaries", sa.BOOLEAN(), autoincrement=False, nullable=True),
)
op.add_column(
"entreprises",
sa.Column("contact_origine", sa.TEXT(), autoincrement=False, nullable=True),
)
op.add_column(
"entreprises", sa.Column("note", sa.TEXT(), autoincrement=False, nullable=True)
)
op.add_column(
"entreprises",
sa.Column("qualite_relation", sa.INTEGER(), autoincrement=False, nullable=True),
)
op.create_foreign_key(
"entreprises_dept_id_fkey", "entreprises", "departement", ["dept_id"], ["id"]
)
op.create_index("ix_entreprises_dept_id", "entreprises", ["dept_id"], unique=False)
op.drop_column("entreprises", "siret")
op.add_column(
"entreprise_contact",
sa.Column(
"entreprise_corresp_id", sa.INTEGER(), autoincrement=False, nullable=True
),
)
op.add_column(
"entreprise_contact",
sa.Column("etudid", sa.INTEGER(), autoincrement=False, nullable=True),
)
op.add_column(
"entreprise_contact",
sa.Column("type_contact", sa.TEXT(), autoincrement=False, nullable=True),
)
op.add_column(
"entreprise_contact",
sa.Column(
"date",
postgresql.TIMESTAMP(timezone=True),
autoincrement=False,
nullable=True,
),
)
op.add_column(
"entreprise_contact",
sa.Column("enseignant", sa.TEXT(), autoincrement=False, nullable=True),
)
op.add_column(
"entreprise_contact",
sa.Column("description", sa.TEXT(), autoincrement=False, nullable=True),
)
op.create_table(
"entreprise_correspondant",
sa.Column("id", sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column("entreprise_id", sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column("nom", sa.TEXT(), autoincrement=False, nullable=True),
sa.Column("prenom", sa.TEXT(), autoincrement=False, nullable=True),
sa.Column("civilite", sa.TEXT(), autoincrement=False, nullable=True),
sa.Column("fonction", sa.TEXT(), autoincrement=False, nullable=True),
sa.Column("phone1", sa.TEXT(), autoincrement=False, nullable=True),
sa.Column("phone2", sa.TEXT(), autoincrement=False, nullable=True),
sa.Column("mobile", sa.TEXT(), autoincrement=False, nullable=True),
sa.Column("mail1", sa.TEXT(), autoincrement=False, nullable=True),
sa.Column("mail2", sa.TEXT(), autoincrement=False, nullable=True),
sa.Column("fax", sa.TEXT(), autoincrement=False, nullable=True),
sa.Column("note", sa.TEXT(), autoincrement=False, nullable=True),
sa.ForeignKeyConstraint(
["entreprise_id"],
["entreprises.id"],
name="entreprise_correspondant_entreprise_id_fkey",
),
sa.PrimaryKeyConstraint("id", name="entreprise_correspondant_pkey"),
)
op.create_foreign_key(
"entreprise_contact_entreprise_corresp_id_fkey",
"entreprise_contact",
"entreprise_correspondant",
["entreprise_corresp_id"],
["id"],
)
op.drop_column("entreprise_contact", "service")
op.drop_column("entreprise_contact", "poste")
op.drop_column("entreprise_contact", "mail")
op.drop_column("entreprise_contact", "telephone")
op.drop_column("entreprise_contact", "prenom")
op.drop_column("entreprise_contact", "nom")
op.drop_table("entreprise_envoi_offre")
op.drop_table("entreprise_envoi_offre_etudiant")
op.drop_table("entreprise_offre")
op.drop_table("entreprise_etudiant")
op.drop_table("entreprise_log")
# ### end Alembic commands ###

View File

@ -0,0 +1,165 @@
"""tables application relations entreprises
Revision ID: ee3f2eab6f08
Revises: f40fbaf5831c
Create Date: 2022-01-24 10:44:09.706261
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'ee3f2eab6f08'
down_revision = 'f40fbaf5831c'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('are_entreprise_log',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('date', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.Column('authenticated_user', sa.Text(), nullable=True),
sa.Column('object', sa.Integer(), nullable=True),
sa.Column('text', sa.Text(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('are_entreprises',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('siret', sa.Text(), nullable=True),
sa.Column('nom', sa.Text(), nullable=True),
sa.Column('adresse', sa.Text(), nullable=True),
sa.Column('codepostal', sa.Text(), nullable=True),
sa.Column('ville', sa.Text(), nullable=True),
sa.Column('pays', sa.Text(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('are_entreprise_contact',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('entreprise_id', sa.Integer(), nullable=True),
sa.Column('nom', sa.Text(), nullable=True),
sa.Column('prenom', sa.Text(), nullable=True),
sa.Column('telephone', sa.Text(), nullable=True),
sa.Column('mail', sa.Text(), nullable=True),
sa.Column('poste', sa.Text(), nullable=True),
sa.Column('service', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['entreprise_id'], ['are_entreprises.id'], ondelete='cascade'),
sa.PrimaryKeyConstraint('id')
)
op.create_table('are_entreprise_etudiant',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('entreprise_id', sa.Integer(), nullable=True),
sa.Column('etudid', sa.Integer(), nullable=True),
sa.Column('type_offre', sa.Text(), nullable=True),
sa.Column('date_debut', sa.Date(), nullable=True),
sa.Column('date_fin', sa.Date(), nullable=True),
sa.Column('formation_text', sa.Text(), nullable=True),
sa.Column('formation_scodoc', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['entreprise_id'], ['are_entreprises.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('are_entreprise_offre',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('entreprise_id', sa.Integer(), nullable=True),
sa.Column('date_ajout', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.Column('intitule', sa.Text(), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('type_offre', sa.Text(), nullable=True),
sa.Column('missions', sa.Text(), nullable=True),
sa.Column('duree', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['entreprise_id'], ['are_entreprises.id'], ondelete='cascade'),
sa.PrimaryKeyConstraint('id')
)
op.create_table('are_entreprise_envoi_offre',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('sender_id', sa.Integer(), nullable=True),
sa.Column('receiver_id', sa.Integer(), nullable=True),
sa.Column('offre_id', sa.Integer(), nullable=True),
sa.Column('date_envoi', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.ForeignKeyConstraint(['offre_id'], ['are_entreprise_offre.id'], ),
sa.ForeignKeyConstraint(['receiver_id'], ['user.id'], ),
sa.ForeignKeyConstraint(['sender_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('are_entreprise_envoi_offre_etudiant',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('sender_id', sa.Integer(), nullable=True),
sa.Column('receiver_id', sa.Integer(), nullable=True),
sa.Column('offre_id', sa.Integer(), nullable=True),
sa.Column('date_envoi', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.ForeignKeyConstraint(['offre_id'], ['are_entreprise_offre.id'], ),
sa.ForeignKeyConstraint(['receiver_id'], ['identite.id'], ),
sa.ForeignKeyConstraint(['sender_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.drop_table('entreprise_contact')
op.drop_table('entreprise_correspondant')
op.drop_index('ix_entreprises_dept_id', table_name='entreprises')
op.drop_table('entreprises')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('entreprises',
sa.Column('id', sa.INTEGER(), server_default=sa.text("nextval('entreprises_id_seq'::regclass)"), autoincrement=True, nullable=False),
sa.Column('nom', sa.TEXT(), autoincrement=False, nullable=True),
sa.Column('adresse', sa.TEXT(), autoincrement=False, nullable=True),
sa.Column('ville', sa.TEXT(), autoincrement=False, nullable=True),
sa.Column('codepostal', sa.TEXT(), autoincrement=False, nullable=True),
sa.Column('pays', sa.TEXT(), autoincrement=False, nullable=True),
sa.Column('localisation', sa.TEXT(), autoincrement=False, nullable=True),
sa.Column('dept_id', sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column('date_creation', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True),
sa.Column('secteur', sa.TEXT(), autoincrement=False, nullable=True),
sa.Column('privee', sa.TEXT(), autoincrement=False, nullable=True),
sa.Column('plus10salaries', sa.BOOLEAN(), autoincrement=False, nullable=True),
sa.Column('contact_origine', sa.TEXT(), autoincrement=False, nullable=True),
sa.Column('note', sa.TEXT(), autoincrement=False, nullable=True),
sa.Column('qualite_relation', sa.INTEGER(), autoincrement=False, nullable=True),
sa.ForeignKeyConstraint(['dept_id'], ['departement.id'], name='entreprises_dept_id_fkey'),
sa.PrimaryKeyConstraint('id', name='entreprises_pkey'),
postgresql_ignore_search_path=False
)
op.create_index('ix_entreprises_dept_id', 'entreprises', ['dept_id'], unique=False)
op.create_table('entreprise_correspondant',
sa.Column('id', sa.INTEGER(), server_default=sa.text("nextval('entreprise_correspondant_id_seq'::regclass)"), autoincrement=True, nullable=False),
sa.Column('entreprise_id', sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column('nom', sa.TEXT(), autoincrement=False, nullable=True),
sa.Column('prenom', sa.TEXT(), autoincrement=False, nullable=True),
sa.Column('civilite', sa.TEXT(), autoincrement=False, nullable=True),
sa.Column('fonction', sa.TEXT(), autoincrement=False, nullable=True),
sa.Column('phone1', sa.TEXT(), autoincrement=False, nullable=True),
sa.Column('phone2', sa.TEXT(), autoincrement=False, nullable=True),
sa.Column('mobile', sa.TEXT(), autoincrement=False, nullable=True),
sa.Column('mail1', sa.TEXT(), autoincrement=False, nullable=True),
sa.Column('mail2', sa.TEXT(), autoincrement=False, nullable=True),
sa.Column('fax', sa.TEXT(), autoincrement=False, nullable=True),
sa.Column('note', sa.TEXT(), autoincrement=False, nullable=True),
sa.ForeignKeyConstraint(['entreprise_id'], ['entreprises.id'], name='entreprise_correspondant_entreprise_id_fkey'),
sa.PrimaryKeyConstraint('id', name='entreprise_correspondant_pkey'),
postgresql_ignore_search_path=False
)
op.create_table('entreprise_contact',
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column('entreprise_id', sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column('entreprise_corresp_id', sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column('etudid', sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column('type_contact', sa.TEXT(), autoincrement=False, nullable=True),
sa.Column('date', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True),
sa.Column('enseignant', sa.TEXT(), autoincrement=False, nullable=True),
sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True),
sa.ForeignKeyConstraint(['entreprise_corresp_id'], ['entreprise_correspondant.id'], name='entreprise_contact_entreprise_corresp_id_fkey'),
sa.ForeignKeyConstraint(['entreprise_id'], ['entreprises.id'], name='entreprise_contact_entreprise_id_fkey'),
sa.PrimaryKeyConstraint('id', name='entreprise_contact_pkey')
)
op.drop_table('are_entreprise_envoi_offre_etudiant')
op.drop_table('are_entreprise_envoi_offre')
op.drop_table('are_entreprise_offre')
op.drop_table('are_entreprise_etudiant')
op.drop_table('are_entreprise_contact')
op.drop_table('are_entreprises')
op.drop_table('are_entreprise_log')
# ### end Alembic commands ###

View File

@ -0,0 +1,34 @@
"""index ine et nip
Revision ID: f40fbaf5831c
Revises: 91be8a06d423
Create Date: 2022-01-10 15:13:06.867903
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = "f40fbaf5831c"
down_revision = "197c658cefbb"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_index(
op.f("ix_identite_code_ine"), "identite", ["code_ine"], unique=False
)
op.create_index(
op.f("ix_identite_code_nip"), "identite", ["code_nip"], unique=False
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f("ix_identite_code_nip"), table_name="identite")
op.drop_index(op.f("ix_identite_code_ine"), table_name="identite")
# ### end Alembic commands ###

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