Compare commits

...

35 Commits

Author SHA1 Message Date
Emmanuel Viennet 18b1f00586 version 9.6.964 2024-04-24 20:58:02 +02:00
Iziram 6b985620e9 Assiduité : signal_assiduites_diff : plage depuis args url closes #741 2024-04-24 20:55:21 +02:00
Iziram 4d234ba353 Assiduité : désactiver saisie présence closes #793 2024-04-24 20:55:21 +02:00
Iziram 5d45fcf656 Assiduité : signal_assiduites_group : bug fix suppr assi autre 2024-04-24 20:55:21 +02:00
Iziram 0a5919b788 Assiduité : pseudo-fix (catch + http error) #872 2024-04-24 20:55:21 +02:00
Iziram 09f4525e66 Assiduité : maj couleurs minitimeline + legende 2024-04-24 20:55:21 +02:00
Emmanuel Viennet 0bc57807de release build script using gitea again 2024-04-24 20:37:03 +02:00
Emmanuel Viennet 87aaf12d27 Protect against Reflected XSS on home page (and other exception-handling pages) 2024-04-23 18:28:00 +02:00
Emmanuel Viennet c8ab9b9b6c Invalidation cache lors d'une erreur sur association UE/Niveau. Peut-être cause de #874. 2024-04-15 18:06:26 +02:00
Emmanuel Viennet ad7b48e110 Calendrier évaluations: fix #875 2024-04-15 17:53:02 +02:00
Emmanuel Viennet f2ce16f161 Archive PV: gzip large files 2024-04-15 03:21:32 +02:00
Emmanuel Viennet 1ddf9b6ab8 Fix: création utilisateur si un seul département 2024-04-12 15:50:53 +02:00
Emmanuel Viennet 0a2e39cae1 Ajoute aide sur édition parcours UEs 2024-04-12 01:10:42 +02:00
Emmanuel Viennet a194b4b6e0 Edition parcours UE: si tous cochés, tronc commun 2024-04-12 01:05:02 +02:00
Emmanuel Viennet cbe85dfb7d anonymize_users: ignore admin 2024-04-12 01:04:27 +02:00
Emmanuel Viennet beba69bfe4 Améliore/met à jour tests unitaires API 2024-04-11 06:00:00 +02:00
Emmanuel Viennet 41fec29452 Bulletin BUT: ne mentionne pas les évaluations rattrapage/session2 sans notes. (c'est déjà le cas en classic) 2024-04-11 01:45:25 +02:00
Emmanuel Viennet 9bd05ea241 Modify SCO_URL in all js: no trailing slash. 2024-04-11 01:44:17 +02:00
Emmanuel Viennet 58b831513d Améliore traitement des erreurs lors de la génération des PDF 2024-04-10 15:29:30 +02:00
Emmanuel Viennet b861aba6a3 Tableaux génériques: possibilité de déclarer un colonne seulement pour excel. Assiduité: ajout etudid et NIP a visu_assi_group: closes #873. 2024-04-10 15:09:32 +02:00
Emmanuel Viennet c2443c361f Améliore page activation module entreprises. Implements #634 2024-04-09 00:36:46 +02:00
Emmanuel Viennet ab4731bd43 Suppression des anciennes fonctions ScoDoc7 donnant les URLs de base. 2024-04-08 18:57:00 +02:00
Emmanuel Viennet c17bc8b61b Fix: liste semestres avec code 2024-04-08 16:26:38 +02:00
Emmanuel Viennet e44a5ee55d Corrige templates formsemestre 2024-04-07 19:52:22 +02:00
Emmanuel Viennet a747ed22e2 Ajoute équivalences pour ref. comp. QLIO 2024-04-07 19:51:34 +02:00
Emmanuel Viennet 5d0a932634 Bulletins BUT: utilisation de l'abbréviation du titre module si présente. 2024-04-06 12:33:07 +02:00
Emmanuel Viennet 2b150cf521 Modif config Jina2. Refonte ScoData, fournit par défaut à tous les templates. 2024-04-06 12:16:53 +02:00
Emmanuel Viennet 5a5ddcacd7 Associer une formation BUT à un nouveau référentiel 'équivalent'. 2024-04-05 23:41:34 +02:00
Emmanuel Viennet 3f6e65b9da Elimine @cached_property sur Identite, pourrait provoquer incohérences temporaires en multithread 2024-04-05 11:00:01 +02:00
Emmanuel Viennet 5eba6170a5 Fix: typo bloquant affichage formations avec UEs sans semestre_idx 2024-04-05 10:11:34 +02:00
Emmanuel Viennet bd9bf87112 Enrichissement du tableau des formations (coche 'détails') 2024-04-05 00:23:29 +02:00
Emmanuel Viennet a0e2af481f Fonction expérimentale pour changer le ref. de compétences d'une formation 2024-04-05 00:22:14 +02:00
Emmanuel Viennet 42e8f97441 Fix: missing exception 2024-04-04 11:23:26 +02:00
Emmanuel Viennet 8ec0171ca0 Script préparation démos: renommage de tous les étudiants 2024-04-03 19:02:40 +02:00
Emmanuel Viennet 6dfab2d843 Fix typo affichage heures 2024-04-03 18:47:44 +02:00
100 changed files with 2713 additions and 1995 deletions

View File

@ -315,12 +315,6 @@ def create_app(config_class=DevConfig):
app.register_error_handler(503, postgresql_server_error)
app.register_error_handler(APIInvalidParams, handle_invalid_usage)
# Add some globals
# previously in Flask-Bootstrap:
app.jinja_env.globals["bootstrap_is_hidden_field"] = lambda field: isinstance(
field, HiddenField
)
from app.auth import bp as auth_bp
app.register_blueprint(auth_bp, url_prefix="/auth")
@ -338,8 +332,15 @@ def create_app(config_class=DevConfig):
from app.api import api_bp
from app.api import api_web_bp
# Jinja2 configuration
# Enable autoescaping of all templates, including .j2
app.jinja_env.autoescape = select_autoescape(default_for_string=True, default=True)
app.jinja_env.trim_blocks = True
app.jinja_env.lstrip_blocks = True
# previously in Flask-Bootstrap:
app.jinja_env.globals["bootstrap_is_hidden_field"] = lambda field: isinstance(
field, HiddenField
)
# https://scodoc.fr/ScoDoc
app.register_blueprint(scodoc_bp)

View File

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

View File

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

View File

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

View File

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

92
app/but/change_refcomp.py Normal file
View File

@ -0,0 +1,92 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Code expérimental: si deux référentiel sont presques identiques
(mêmes compétences, niveaux, parcours)
essaie de changer une formation de référentiel.
"""
from app import clear_scodoc_cache, db
from app.models import (
ApcParcours,
ApcReferentielCompetences,
ApcValidationRCUE,
Formation,
FormSemestreInscription,
UniteEns,
)
from app.scodoc.sco_exceptions import ScoValueError
def formation_change_referentiel(
formation: Formation, new_ref: ApcReferentielCompetences
):
"""Try to change ref."""
if not formation.referentiel_competence:
raise ScoValueError("formation non associée à un référentiel")
if not isinstance(new_ref, ApcReferentielCompetences):
raise ScoValueError("nouveau référentiel invalide")
r = formation.referentiel_competence.map_to_other_referentiel(new_ref)
if isinstance(r, str):
raise ScoValueError(f"référentiels incompatibles: {r}")
parcours_map, competences_map, niveaux_map = r
formation.referentiel_competence = new_ref
db.session.add(formation)
# UEs - Niveaux et UEs - parcours
for ue in formation.ues:
if ue.niveau_competence:
ue.niveau_competence_id = niveaux_map[ue.niveau_competence_id]
db.session.add(ue)
if ue.parcours:
new_list = [ApcParcours.query.get(parcours_map[p.id]) for p in ue.parcours]
ue.parcours.clear()
ue.parcours.extend(new_list)
db.session.add(ue)
# Modules / parcours et app_critiques
for module in formation.modules:
if module.parcours:
new_list = [
ApcParcours.query.get(parcours_map[p.id]) for p in module.parcours
]
module.parcours.clear()
module.parcours.extend(new_list)
db.session.add(module)
if module.app_critiques: # efface les apprentissages critiques
module.app_critiques.clear()
db.session.add(module)
# ApcValidationRCUE
for valid_rcue in ApcValidationRCUE.query.join(
UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id
).filter_by(formation_id=formation.id):
if valid_rcue.parcour:
valid_rcue.parcour_id = parcours_map[valid_rcue.parcour.id]
db.session.add(valid_rcue)
for valid_rcue in ApcValidationRCUE.query.join(
UniteEns, UniteEns.id == ApcValidationRCUE.ue2_id
).filter_by(formation_id=formation.id):
if valid_rcue.parcour:
valid_rcue.parcour_id = parcours_map[valid_rcue.parcour.id]
db.session.add(valid_rcue)
# FormSemestre / parcours_formsemestre
for formsemestre in formation.formsemestres:
new_list = [
ApcParcours.query.get(parcours_map[p.id]) for p in formsemestre.parcours
]
formsemestre.parcours.clear()
formsemestre.parcours.extend(new_list)
db.session.add(formsemestre)
# FormSemestreInscription.parcour_id
for inscr in FormSemestreInscription.query.filter_by(
formsemestre_id=formsemestre.id
).filter(FormSemestreInscription.parcour_id != None):
if inscr.parcour_id is not None:
inscr.parcour_id = parcours_map[inscr.parcour_id]
#
db.session.commit()
clear_scodoc_cache()

View File

@ -10,9 +10,11 @@
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed
from wtforms import SelectField, SubmitField
from wtforms.validators import DataRequired
class FormationRefCompForm(FlaskForm):
"Choix d'un référentiel"
referentiel_competence = SelectField(
"Choisir parmi les référentiels déjà chargés :"
)
@ -21,6 +23,7 @@ class FormationRefCompForm(FlaskForm):
class RefCompLoadForm(FlaskForm):
"Upload d'un référentiel"
referentiel_standard = SelectField(
"Choisir un référentiel de compétences officiel BUT"
)
@ -47,3 +50,12 @@ class RefCompLoadForm(FlaskForm):
)
return False
return True
class FormationChangeRefCompForm(FlaskForm):
"choix d'un nouveau ref. comp. pour une formation"
object_select = SelectField(
"Choisir le nouveau référentiel", validators=[DataRequired()]
)
submit = SubmitField("Changer le référentiel de la formation")
cancel = SubmitField("Annuler")

View File

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

View File

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

View File

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

View File

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

View File

@ -8,16 +8,19 @@
from datetime import datetime
import functools
from operator import attrgetter
import yaml
from flask import g
from flask_sqlalchemy.query import Query
from sqlalchemy.orm import class_mapper
import sqlalchemy
from app import db
from app import db, log
from app.scodoc.sco_utils import ModuleType
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
REFCOMP_EQUIVALENCE_FILENAME = "ressources/referentiels/equivalences.yaml"
# from https://stackoverflow.com/questions/2537471/method-of-iterating-over-sqlalchemy-models-defined-columns
@ -104,6 +107,11 @@ class ApcReferentielCompetences(db.Model, XMLModel):
def __repr__(self):
return f"<ApcReferentielCompetences {self.id} {self.specialite!r} {self.departement!r}>"
def get_title(self) -> str:
"Titre affichable"
# utilise type_titre (B.U.T.), spécialité, version
return f"{self.type_titre} {self.specialite} {self.get_version()}"
def get_version(self) -> str:
"La version, normalement sous forme de date iso yyy-mm-dd"
if not self.version_orebut:
@ -124,9 +132,11 @@ class ApcReferentielCompetences(db.Model, XMLModel):
"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 "",
"scodoc_date_loaded": (
self.scodoc_date_loaded.isoformat() + "Z"
if self.scodoc_date_loaded
else ""
),
"scodoc_orig_filename": self.scodoc_orig_filename,
"competences": {
x.titre: x.to_dict(with_app_critiques=with_app_critiques)
@ -234,6 +244,92 @@ class ApcReferentielCompetences(db.Model, XMLModel):
return parcours_info
def equivalents(self) -> set["ApcReferentielCompetences"]:
"""Ensemble des référentiels du même département
qui peuvent être considérés comme "équivalents", au sens
une formation de ce référentiel pourrait changer vers un équivalent,
en ignorant les apprentissages critiques.
Pour cela, il faut avoir le même type, etc et les mêmes compétences,
niveaux et parcours (voir map_to_other_referentiel).
"""
candidats = ApcReferentielCompetences.query.filter_by(
dept_id=self.dept_id
).filter(ApcReferentielCompetences.id != self.id)
return {
referentiel
for referentiel in candidats
if not isinstance(self.map_to_other_referentiel(referentiel), str)
}
def map_to_other_referentiel(
self, other: "ApcReferentielCompetences"
) -> str | tuple[dict[int, int], dict[int, int], dict[int, int]]:
"""Build mapping between this referentiel and ref2.
If successful, returns 3 dicts mapping self ids to other ids.
Else return a string, error message.
"""
if self.type_structure != other.type_structure:
return "type_structure mismatch"
if self.type_departement != other.type_departement:
return "type_departement mismatch"
# Table d'équivalences entre refs:
equiv = self._load_config_equivalences()
# mêmes parcours ?
eq_parcours = equiv.get("parcours", {})
parcours_by_code_1 = {eq_parcours.get(p.code, p.code): p for p in self.parcours}
parcours_by_code_2 = {
eq_parcours.get(p.code, p.code): p for p in other.parcours
}
if parcours_by_code_1.keys() != parcours_by_code_2.keys():
return "parcours mismatch"
parcours_map = {
parcours_by_code_1[eq_parcours.get(code, code)]
.id: parcours_by_code_2[eq_parcours.get(code, code)]
.id
for code in parcours_by_code_1
}
# mêmes compétences ?
competence_by_code_1 = {c.titre: c for c in self.competences}
competence_by_code_2 = {c.titre: c for c in other.competences}
if competence_by_code_1.keys() != competence_by_code_2.keys():
return "competences mismatch"
competences_map = {
competence_by_code_1[titre].id: competence_by_code_2[titre].id
for titre in competence_by_code_1
}
# mêmes niveaux (dans chaque compétence) ?
niveaux_map = {}
for titre in competence_by_code_1:
c1 = competence_by_code_1[titre]
c2 = competence_by_code_2[titre]
niveau_by_attr_1 = {(n.annee, n.ordre, n.libelle): n for n in c1.niveaux}
niveau_by_attr_2 = {(n.annee, n.ordre, n.libelle): n for n in c2.niveaux}
if niveau_by_attr_1.keys() != niveau_by_attr_2.keys():
return f"niveaux mismatch in comp. '{titre}'"
niveaux_map.update(
{
niveau_by_attr_1[a].id: niveau_by_attr_2[a].id
for a in niveau_by_attr_1
}
)
return parcours_map, competences_map, niveaux_map
def _load_config_equivalences(self) -> dict:
"""Load config file ressources/referentiels/equivalences.yaml
used to define equivalences between distinct referentiels
"""
try:
with open(REFCOMP_EQUIVALENCE_FILENAME, encoding="utf-8") as f:
doc = yaml.safe_load(f.read())
except FileNotFoundError:
log(f"_load_config_equivalences: {REFCOMP_EQUIVALENCE_FILENAME} not found")
return {}
except yaml.parser.ParserError as exc:
raise ScoValueError(
f"erreur dans le fichier {REFCOMP_EQUIVALENCE_FILENAME}"
) from exc
return doc.get(self.specialite, {})
class ApcCompetence(db.Model, XMLModel):
"Compétence"
@ -374,9 +470,11 @@ class ApcNiveau(db.Model, XMLModel):
"libelle": self.libelle,
"annee": self.annee,
"ordre": self.ordre,
"app_critiques": {x.code: x.to_dict() for x in self.app_critiques}
if with_app_critiques
else {},
"app_critiques": (
{x.code: x.to_dict() for x in self.app_critiques}
if with_app_critiques
else {}
),
}
def to_dict_bul(self):
@ -464,9 +562,9 @@ class ApcNiveau(db.Model, XMLModel):
return []
if competence is None:
parcour_niveaux: list[
ApcParcoursNiveauCompetence
] = annee_parcour.niveaux_competences
parcour_niveaux: list[ApcParcoursNiveauCompetence] = (
annee_parcour.niveaux_competences
)
niveaux: list[ApcNiveau] = [
pn.competence.niveaux.filter_by(ordre=pn.niveau).first()
for pn in parcour_niveaux

View File

@ -297,7 +297,7 @@ class Identite(models.ScoDocModel):
else:
return self.nom
@cached_property
@property
def nomprenom(self, reverse=False) -> str:
"""Civilité/nom/prenom pour affichages: "M. Pierre Dupont"
Si reverse, "Dupont Pierre", sans civilité.

View File

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

View File

@ -1,5 +1,7 @@
"""ScoDoc 9 models : Formations
"""
from flask import abort, g
from flask_sqlalchemy.query import Query
import app
@ -64,6 +66,21 @@ class Formation(db.Model):
"titre complet pour affichage"
return f"""Formation {self.titre} ({self.acronyme}) [version {self.version}] code {self.formation_code}"""
@classmethod
def get_formation(cls, formation_id: int | str, dept_id: int = None) -> "Formation":
"""Formation ou 404, cherche uniquement dans le département spécifié
ou le courant (g.scodoc_dept)"""
if not isinstance(formation_id, int):
try:
formation_id = int(formation_id)
except (TypeError, ValueError):
abort(404, "formation_id invalide")
if g.scodoc_dept:
dept_id = dept_id if dept_id is not None else g.scodoc_dept_id
if dept_id is not None:
return cls.query.filter_by(id=formation_id, dept_id=dept_id).first_or_404()
return cls.query.filter_by(id=formation_id).first_or_404()
def to_dict(self, with_refcomp_attrs=False, with_departement=True):
"""As a dict.
Si with_refcomp_attrs, ajoute attributs permettant de retrouver le ref. de comp.

View File

@ -945,7 +945,7 @@ class FormSemestre(models.ScoDocModel):
ins.etudid for ins in self.inscriptions if ins.etat == scu.INSCRIT
}
@cached_property
@property
def etuds_inscriptions(self) -> dict:
"""Map { etudid : inscription } (incluant DEM et DEF)"""
return {ins.etud.id: ins for ins in self.inscriptions}

View File

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

View File

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

View File

@ -30,7 +30,7 @@
import html
from flask import g, render_template
from flask import g, render_template, url_for
from flask import request
from flask_login import current_user
@ -163,7 +163,7 @@ def sco_header(
params = {
"page_title": page_title or sco_version.SCONAME,
"no_side_bar": no_side_bar,
"ScoURL": scu.ScoURL(),
"ScoURL": url_for("scolar.index_html", scodoc_dept=g.scodoc_dept),
"encoding": scu.SCO_ENCODING,
"titrebandeau_mkup": "<td>" + titrebandeau + "</td>",
"authuser": current_user.user_name,
@ -179,6 +179,7 @@ def sco_header(
H = [
"""<!DOCTYPE html><html lang="fr">
<!-- ScoDoc legacy -->
<head>
<meta charset="utf-8"/>
<title>%(page_title)s</title>
@ -219,7 +220,7 @@ def sco_header(
<script>
window.onload=function(){{enableTooltips("gtrcontent")}};
const SCO_URL="{scu.ScoURL()}";
const SCO_URL="{url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)}";
const SCO_TIMEZONE="{scu.TIME_ZONE}";
</script>"""
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -55,7 +55,6 @@ from flask import g
import app
from app import db, log
from app.scodoc import notesdb as ndb
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import ScoException
@ -174,17 +173,15 @@ class EvaluationCache(ScoDocCache):
@classmethod
def invalidate_all_sems(cls):
"delete all evaluations in current dept from cache"
from app.models.evaluations import Evaluation
from app.models.formsemestre import FormSemestre
from app.models.moduleimpls import ModuleImpl
evaluation_ids = [
x[0]
for x in ndb.SimpleQuery(
"""SELECT e.id
FROM notes_evaluation e, notes_moduleimpl mi, notes_formsemestre s
WHERE s.dept_id=%(dept_id)s
AND s.id = mi.formsemestre_id
AND mi.id = e.moduleimpl_id;
""",
{"dept_id": g.scodoc_dept_id},
)
e.id
for e in Evaluation.query.join(ModuleImpl)
.join(FormSemestre)
.filter_by(dept_id=g.scodoc_dept_id)
]
cls.delete_many(evaluation_ids)

View File

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

View File

@ -148,7 +148,7 @@ def _convert_formsemestres_to_dicts(
),
"formsemestre_id": formsemestre.id,
"groupicon": groupicon if nb_inscrits > 0 else emptygroupicon,
"lockimg": lockicon,
"lockimg": "" if formsemestre.etat else lockicon,
"modalite": formsemestre.modalite,
"mois_debut": formsemestre.mois_debut(),
"mois_fin": formsemestre.mois_fin(),
@ -192,7 +192,7 @@ def _sem_table_gt(formsemestres: Query, showcodes=False, fmt="html") -> GenTable
"elt_sem_apo",
]
if showcodes:
columns_ids = ("formsemestre_id",) + columns_ids
columns_ids.insert(0, "formsemestre_id") # prepend
html_class = "stripe cell-border compact hover order-column table_leftalign semlist"
if current_user.has_permission(Permission.EditApogee):

View File

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

View File

@ -448,7 +448,7 @@ def module_edit(
(
"titre",
{
"size": 30,
"size": 64,
"explanation": """nom du module. Exemple:
<em>Introduction à la démarche ergonomique</em>""",
},
@ -456,8 +456,8 @@ def module_edit(
(
"abbrev",
{
"size": 20,
"explanation": """nom abrégé (pour bulletins).
"size": 32,
"explanation": """(optionnel) nom abrégé pour bulletins.
Exemple: <em>Intro. à l'ergonomie</em>""",
},
),

View File

@ -837,8 +837,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
<a href="{url_for('notes.refcomp_show',
scodoc_dept=g.scodoc_dept, refcomp_id=formation.referentiel_competence.id)}"
class="stdlink">
{formation.referentiel_competence.type_titre}
{formation.referentiel_competence.specialite_long}
{formation.referentiel_competence.get_title()}
</a>&nbsp;"""
msg_refcomp = "changer"
H.append(f"""<ul><li>{descr_refcomp}""")

View File

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

View File

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

View File

@ -489,9 +489,10 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
return formation.id, modules_old2new, ues_old2new
def formation_list_table() -> GenTable:
def formation_list_table(detail: bool) -> GenTable:
"""List formation, grouped by titre and sorted by versions
and listing associated semestres
and listing associated semestres.
If detail, add column with more details.
returns a table
"""
formations: list[Formation] = Formation.query.filter_by(dept_id=g.scodoc_dept_id)
@ -534,6 +535,15 @@ def formation_list_table() -> GenTable:
if formation.referentiel_competence
else ""
),
"_referentiel_target": (
url_for(
"notes.refcomp_show",
scodoc_dept=g.scodoc_dept,
refcomp_id=formation.referentiel_competence.id,
)
if formation.referentiel_competence
else ""
),
}
# Ajoute les semestres associés à chaque formation:
row["formsemestres"] = formation.formsemestres.order_by(
@ -561,10 +571,15 @@ def formation_list_table() -> GenTable:
else []
)
)
# Répartition des UEs dans les semestres
# utilise pour voir si la formation couvre tous les semestres
row["semestres_ues"] = ", ".join(
"S" + str(x if (x is not None and x > 0) else "-")
for x in sorted({(ue.semestre_idx or 0) for ue in formation.ues})
)
# Date surtout utilisées pour le tri:
if row["formsemestres"]:
row["date_fin_dernier_sem"] = (
row["formsemestres"][-1].date_fin.isoformat(),
)
row["date_fin_dernier_sem"] = row["formsemestres"][-1].date_fin.isoformat()
row["annee_dernier_sem"] = row["formsemestres"][-1].date_fin.year
else:
row["date_fin_dernier_sem"] = ""
@ -617,6 +632,8 @@ def formation_list_table() -> GenTable:
"commentaire",
"sems_list_txt",
)
if detail:
columns_ids += ("annee_dernier_sem", "semestres_ues")
titles = {
"buttons": "",
"commentaire": "Commentaire",
@ -627,6 +644,9 @@ def formation_list_table() -> GenTable:
"formation_code": "Code",
"sems_list_txt": "Semestres",
"referentiel": "Réf.",
"date_fin_dernier_sem": "Fin dernier sem.",
"annee_dernier_sem": "Année dernier sem.",
"semestres_ues": "Semestres avec UEs",
}
return GenTable(
columns_ids=columns_ids,
@ -639,7 +659,7 @@ def formation_list_table() -> GenTable:
html_class="formation_list_table table_leftalign",
html_with_td_classes=True,
html_sortable=True,
base_url=f"{request.base_url}",
base_url=f"{request.base_url}" + ("?detail=on" if detail else ""),
page_title=title,
pdf_title=title,
preferences=sco_preferences.SemPreferences(),

View File

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

View File

@ -143,8 +143,10 @@ def _build_menu_stats(formsemestre: FormSemestre):
]
def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
def formsemestre_status_menubar(formsemestre: FormSemestre | None) -> str:
"""HTML to render menubar"""
if formsemestre is None:
return ""
formsemestre_id = formsemestre.id
if formsemestre.etat:
change_lock_msg = "Verrouiller"
@ -632,7 +634,7 @@ def formsemestre_description_table(
"UE": modimpl.module.ue.acronyme,
"_UE_td_attrs": ue_info.get("_UE_td_attrs", ""),
"Code": modimpl.module.code or "",
"Module": modimpl.module.abbrev or modimpl.module.titre,
"Module": modimpl.module.abbrev or modimpl.module.titre or "",
"_Module_class": "scotext",
"Inscrits": mod_nb_inscrits,
"Responsable": sco_users.user_info(modimpl.responsable_id)["nomprenom"],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1535,6 +1535,9 @@ def graph_cursus(
# semestre de depart en vert
n = g.get_node("SEM" + str(formsemestre_id))[0]
n.set_color("green")
n.set_style("filled")
n.set_fillcolor("lightgreen")
n.set_penwidth(2.0)
# demissions en rouge, octagonal
for nid in dem_nodes.values():
n = g.get_node(nid)[0]

View File

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

View File

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

View File

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

View File

@ -552,8 +552,8 @@ DAY_NAMES = ("lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "diman
TIME_FMT = "%H:%M" # affichage des heures
DATE_FMT = "%d/%m/%Y" # affichage des dates
DATEATIME_FMT = DATE_FMT + " à " + DATE_FMT
DATETIME_FMT = DATE_FMT + " " + DATE_FMT
DATEATIME_FMT = DATE_FMT + " à " + TIME_FMT
DATETIME_FMT = DATE_FMT + " " + TIME_FMT
def fmt_note(val, note_max=None, keep_numeric=False):
@ -785,51 +785,6 @@ BULLETINS_VERSIONS_BUT = BULLETINS_VERSIONS | {
"butcourt": "Version courte spéciale BUT"
}
# ----- Support for ScoDoc7 compatibility
def ScoURL():
"""base URL for this sco instance.
e.g. https://scodoc.xxx.fr/ScoDoc/DEPT/Scolarite
= page accueil département
"""
return url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)[
: -len("/index_html")
]
def NotesURL():
"""URL of Notes
e.g. https://scodoc.xxx.fr/ScoDoc/DEPT/Scolarite/Notes
= url de base des méthodes de notes
(page accueil programmes).
"""
return url_for("notes.index_html", scodoc_dept=g.scodoc_dept)[: -len("/index_html")]
def AbsencesURL():
"""URL of Absences"""
return url_for("absences.index_html", scodoc_dept=g.scodoc_dept)[
: -len("/index_html")
]
def AssiduitesURL():
"""URL of Assiduités"""
return url_for("assiduites.bilan_dept", scodoc_dept=g.scodoc_dept)[
: -len("/BilanDept")
]
def UsersURL():
"""URL of Users
e.g. https://scodoc.xxx.fr/ScoDoc/DEPT/Scolarite/Users
= url de base des requêtes ZScoUsers
et page accueil users
"""
return url_for("users.index_html", scodoc_dept=g.scodoc_dept)[: -len("/index_html")]
# ---- Simple python utilities

View File

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

View File

@ -2411,10 +2411,10 @@ li.notes_formation_list {
padding-top: 10px;
}
table.formation_list_table {
width: 100%;
table.dataTable.formation_list_table.gt_table {
border-collapse: collapse;
background-color: rgb(0%, 90%, 90%);
margin-right: 12px;
margin-left: 12px;
}
table#formation_list_table tr.gt_hl {
@ -2455,8 +2455,8 @@ table.formation_list_table td.buttons span.but_placeholder {
text-align: center;
}
.formation_list_table td.titre {
width: 45%;
.formation_list_table td.sems_list_txt {
width: 15%;
}
.formation_list_table td.commentaire {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,16 +4,17 @@
# See LICENSE
##############################################################################
"""Liste simple d'étudiants
"""
"""Liste simple d'étudiants"""
import datetime
from flask import g, url_for
from app import log
from app.models import FormSemestre, Identite, Justificatif
from app.tables import table_builder as tb
import app.scodoc.sco_assiduites as scass
from app.scodoc import sco_preferences
from app.scodoc import sco_utils as scu
import app.scodoc.sco_assiduites as scass
from app.scodoc.sco_exceptions import ScoValueError
class TableAssi(tb.Table):
@ -39,7 +40,13 @@ class TableAssi(tb.Table):
):
self.rows: list["RowAssi"] = [] # juste pour que VSCode nous aide sur .rows
classes = ["gt_table"]
self.dates = [str(dates[0]) + "T00:00", str(dates[1]) + "T23:59"]
try:
self.dates = [
datetime.datetime.fromisoformat(str(dates[0]) + "T00:00"),
datetime.datetime.fromisoformat(str(dates[1]) + "T00:00"),
]
except ValueError as exc:
raise ScoValueError("invalid dates") from exc
self.formsemestre = formsemestre
self.formsemestre_modimpls = formsemestre_modimpls
if convert_values:
@ -97,6 +104,20 @@ class RowAssi(tb.Row):
bilan_etud = url_for(
"assiduites.bilan_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id
)
self.add_cell(
"etudid",
"etudid",
etud.etudid,
"etudinfo",
only_excel=True,
)
self.add_cell(
"code_nip",
"code_nip",
etud.code_nip,
"etudinfo",
only_excel=True,
)
self.add_cell(
"nom_disp",
"Nom",
@ -119,6 +140,13 @@ class RowAssi(tb.Row):
)
stats = self._get_etud_stats(etud)
for key, value in stats.items():
if key == "present" and sco_preferences.get_preference(
"non_present",
dept_id=g.scodoc_dept_id,
formsemestre_id=self.table.formsemestre.id,
):
continue
self.add_cell(key, value[0], fmt_num(value[1] - value[2]), "assi_stats")
if key != "present":
self.add_cell(

View File

@ -0,0 +1,51 @@
{% extends "base.j2" %}
{% import 'wtf.j2' as wtf %}
{% block app_content %}
<h1>{{title}}</h1>
<div class="help">
<p>
</p>
<p>
</p>
</div>
<div class="row">
<div class="col-md-8">
<form class="form form-horizontal spacediv" method="post" enctype="multipart/form-data" role="form">
{{ form.hidden_tag() }}
{{ wtf.form_errors(form, hiddens="only") }}
{% if is_enabled %}
<p>Le module <em>relations entreprises</em> est actuellement activé.</p>
<p>Il peut être activé ou désactivé à tout moment sans aucune perte de
données (la désactivation le fait simplement disparaitre des pages
utilisateurs).
<p>
{% else %}
<p>Le module <em>relations entreprises</em> est actuellement désactivé.
</p>
<p>Il peut être activé ou désactivé à tout moment sans aucune perte de
données (la désactivation le fait simplement disparaitre des pages
utilisateurs).
<p>
<p>
Lors de son activation, vous pouvez (re)positionner les rôles qu'il utilise
à leurs valeurs par défaut en cochant la case ci-dessous.
</p>
{{ wtf.form_field(form.set_default_roles_permission) }}
{% endif %}
<div class="form-group spacediv">
{{ wtf.form_field(form.submit) }}
{{ wtf.form_field(form.cancel) }}
</div>
</div>
</div>
<style>
.spacediv {
margin-top: 16px;
}
</style>
{% endblock %}

View File

@ -51,8 +51,6 @@ Calendrier de l'assiduité
<div class="dayline">
<div class="dayline-title">
<span>Assiduité du</span>
<br>
<span>{{jour.get_date()}}</span>
{{jour.generate_minitimeline() | safe}}
</div>
@ -77,36 +75,7 @@ Calendrier de l'assiduité
<div class="help">
<h3>Calendrier</h3>
<p>Code couleur</p>
<ul class="couleurs">
<li><span title="Vert" class="present demo"></span> &rightarrow; présence de l'étudiant lors de la
période
</li>
<li><span title="Bleu clair" class="nonwork demo"></span> &rightarrow; la période n'est pas travaillée
</li>
<li><span title="Rouge" class="absent demo"></span> &rightarrow; absence de l'étudiant lors de la
période
</li>
<li><span title="Rose" class="demo color absent est_just"></span> &rightarrow; absence justifiée
</li>
<li><span title="Orange" class="retard demo"></span> &rightarrow; retard de l'étudiant lors de la
période
</li>
<li><span title="Jaune clair" class="demo color retard est_just"></span> &rightarrow; retard justifié
</li>
<li><span title="Quart Bleu" class="est_just demo"></span> &rightarrow; la période est couverte par un
justificatif valide</li>
<li><span title="Justif. non valide" class="invalide demo"></span> &rightarrow; la période est
couverte par un justificatif non valide
</li>
<li><span title="Justif. en attente" class="attente demo"></span> &rightarrow; la période
a un justificatif en attente de validation
</li>
</ul>
<p>Vous pouvez passer le curseur sur les jours colorés afin de voir les informations supplémentaires</p>
{% include "assiduites/widgets/legende_couleur.j2" %}
</div>
<ul class="couleurs print">
<li><span title="Vert" class="present demo"></span> présence
@ -149,7 +118,7 @@ Calendrier de l'assiduité
list-style-type: none;
}
.pageContent {
margin-top: 1vh;
@ -158,7 +127,7 @@ Calendrier de l'assiduité
.calendrier {
display: flex;
justify-content: start;
justify-content: center;
overflow-x: scroll;
border: 1px solid #444;
border-radius: 12px;
@ -182,21 +151,8 @@ Calendrier de l'assiduité
justify-content: start;
}
.demo.invalide {
background-color: var(--color-justi-invalide) !important;
}
.demo.attente {
background: repeating-linear-gradient(to bottom,
var(--color-justi-attente-stripe) 0px,
var(--color-justi-attente-stripe) 4px,
var(--color-justi-attente) 4px,
var(--color-justi-attente) 7px) !important;
}
.demo.est_just {
background-color: var(--color-justi) !important;
}
.demi .day.nonwork>span {
@ -335,7 +291,7 @@ Calendrier de l'assiduité
document.querySelectorAll('[assi_id]').forEach((el, i) => {
el.addEventListener('click', () => {
const assi_id = el.getAttribute('assi_id');
window.open(`${SCO_URL}/Assiduites/tableau_assiduite_actions?type=assiduite&action=details&obj_id=${assi_id}`);
window.open(`${SCO_URL}Assiduites/tableau_assiduite_actions?type=assiduite&action=details&obj_id=${assi_id}`);
})
});

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,4 @@
{# Base de toutes les pages ScoDoc #}
{% block doc -%}
<!DOCTYPE html>
{%- block doc -%}<!DOCTYPE html>{# Base de toutes les pages ScoDoc #}
<html{% block html_attribs %}{% endblock html_attribs %}>
{%- block html %}
<head>

View File

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

View File

@ -11,9 +11,9 @@
)}}">{{etud.nomprenom}}</a></h2>
{% endif %}
<form name="f" method="GET" action="{{request.base_url}}">
<input type="hidden" name="formsemestre_id" value="{{formsemestre.id}}"></input>
<input type="hidden" name="etudid" value="{{etud.id}}"></input>
<input type="hidden" name="fmt" value="{{fmt}}"></input>
<input type="hidden" name="formsemestre_id" value="{{formsemestre.id}}">
<input type="hidden" name="etudid" value="{{etud.id}}">
<input type="hidden" name="fmt" value="{{fmt}}">
<div class="bull_titre_semestre">
Bulletin
<span class="bull_liensemestre">
@ -36,7 +36,7 @@
formsemestre_id=formsemestre.id,
etudid=etud.id,
)
}}&version='+this.value;"" class="noprint">
}}&version='+this.value;" class="noprint">
{% if formsemestre.formation.is_apc() %}
{% set menu_items = scu.BULLETINS_VERSIONS_BUT.items() %}
{% else %}

View File

@ -0,0 +1,58 @@
{# -*- mode: jinja-html -*- #}
{% extends "sco_page.j2" %}
{% import 'wtf.j2' as wtf %}
{% block app_content %}
<h1>Changer le référentiel de compétences de la formation
{{formation.get_titre_version()}}
</h1>
<div class="help">
<p> Normalement, il n'est pas possible de changer une
formation de référentiel de compétences. En effet, de nombreux éléments sont
déduis du référentiel: les parcours, les compétences, les apprentissages
critiques, et donc les validations de jury des étudiants qui ont suivi la
formation.
</p>
<p> Cependant, dans certains cas, le ministère a publié une nouvelle version
d'un référentiel de spécialité, mais les changements ne sont que très légers:
détails des noms de parcours ou des compétences. Dans ces cas là, la structure
étant la même, il est autorisé de changer le référentiel.
</p>
<p>Seuls les référentiels déjà chargés et compatibles (ayant la même structure)
sont proposés dans le menu ci-dessous.
</p>
<p class="fontred">
Attention: tout changement de référentiel entraine la perte des associations
entre les modules et les apprentissages critiques.
</p>
<p class="fontred">
Attention: <b>fonction expérimentale</b>. Vérifiez vos sauvegardes.
</p>
</div>
<div>La formation est actuellement associée au référentiel
<a class="stdlink" href="{{
url_for('notes.refcomp_show',
scodoc_dept=g.scodoc_dept,
refcomp_id=formation.referentiel_competence.id)
}}">{{formation.referentiel_competence.get_title()}}</a>
</div>
<form method="POST" style="margin-top: 24px;">
{{ form.hidden_tag() }}
<div>
{{ form.object_select.label }}<br>
{{ form.object_select }}
</div>
<div style="margin-top: 24px;">{{ form.submit() }} {{ form.cancel() }}</div>
</form>
{% endblock %}

View File

@ -27,9 +27,18 @@
<li>Formations se référant à ce référentiel:
<ul>
{% for formation in ref.formations %}
<li><a class="stdlink" href="{{
url_for('notes.ue_table', scodoc_dept=g.scodoc_dept, formation_id=formation.id )
}}">{{ formation.get_titre_version() }}</a></li>
<li>
<a class="stdlink" href="{{
url_for('notes.ue_table', scodoc_dept=g.scodoc_dept, formation_id=formation.id )
}}">{{ formation.get_titre_version() }}</a>
{% if referentiels_equivalents %}
<a style="margin-left: 8px;" class="stdlink" href="
{{ url_for('notes.formation_change_refcomp',
scodoc_dept=g.scodoc_dept, formation_id=formation.id )
}}
">(changer)</a>
{% endif %}
</li>
{% else %}
<li><em>aucune</em></li>
{% endfor %}

View File

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

View File

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

View File

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

View File

@ -3,39 +3,46 @@
<!-- formsemestre_header -->
<div class="formsemestre_page_title noprint">
<div class="infos">
<span class="semtitle"><a class="stdlink" title="{{sco.sem.session_id()}}" href="{{
<span class="semtitle"><a class="stdlink" title="{{sco.formsemestre.session_id()}}" href="{{
url_for('notes.formsemestre_status',
scodoc_dept=g.scodoc_dept, formsemestre_id=sco.sem.id)
}}">{{sco.sem.titre}}</a>
<a title="{{sco.sem.etapes_apo_str()}}">
{% if sco.sem.semestre_id != -1 %}, {{sco.sem.formation.get_cursus().SESSION_NAME}}
{{sco.sem.semestre_id}}
scodoc_dept=g.scodoc_dept, formsemestre_id=sco.formsemestre.id)
}}">{{sco.formsemestre.titre}}</a>
<a title="{{sco.formsemestre.etapes_apo_str()}}">
{% if sco.formsemestre.semestre_id != -1 %}, {{sco.formsemestre.formation.get_cursus().SESSION_NAME}}
{{sco.formsemestre.semestre_id}}
{% endif %}</a>
{% if sco.sem.modalite %} en {{sco.sem.modalite}}{% endif %}</span>
{% if sco.formsemestre.modalite %} en {{sco.formsemestre.modalite}}{% endif %}
</span>
<span class="dates">
<a title="du {{sco.sem.date_debut.strftime('%d/%m/%Y')}}
au {{sco.sem.date_fin.strftime('%d/%m/%Y')}} ">{{scu.MONTH_NAMES_ABBREV[ sco.sem.date_debut.month - 1]}}
{{sco.sem.date_debut.year}} - {{scu.MONTH_NAMES_ABBREV[sco.sem.date_fin.month - 1]}}
{{sco.sem.date_fin.year}}</a></span>
<a title="du {{sco.formsemestre.date_debut.strftime('%d/%m/%Y')}}
au {{sco.formsemestre.date_fin.strftime('%d/%m/%Y')}} ">{{scu.MONTH_NAMES_ABBREV[ sco.formsemestre.date_debut.month - 1]}}
{{sco.formsemestre.date_debut.year}} - {{scu.MONTH_NAMES_ABBREV[sco.formsemestre.date_fin.month - 1]}}
{{sco.formsemestre.date_fin.year}}</a>
</span>
<span class="resp"><a
title="{{sco.sem.responsables_str(abbrev_prenom=False)}}">{{sco.sem.responsables_str()}}</a></span>
title="{{sco.formsemestre.responsables_str(abbrev_prenom=False)}}">{{sco.formsemestre.responsables_str()}}</a></span>
<span class="nbinscrits"><a class="discretelink" href="{{url_for('scolar.groups_view', scodoc_dept=g.scodoc_dept,
formsemestre_id=sco.sem.id)}}">{{sco.sem.inscriptions|length}} inscrits</a></span><span class="lock">{% if
not sco.sem.etat %}<a href="{{url_for('notes.formsemestre_flip_lock', scodoc_dept=g.scodoc_dept,
formsemestre_id=sco.sem.id)}}">{{scu.icontag("lock_img", border="0", title="Semestre
verrouillé")|safe}}</a>{% endif %}</span><span class="eye">
formsemestre_id=sco.formsemestre.id)}}">{{sco.formsemestre.inscriptions|length}} inscrits</a></span>
<span class="lock">
{% if not sco.formsemestre.etat %}<a href="{{url_for('notes.formsemestre_flip_lock', scodoc_dept=g.scodoc_dept,
formsemestre_id=sco.formsemestre.id)}}">{{scu.icontag("lock_img", border="0", title="Semestre
verrouillé")|safe}}</a>
{% endif %}
</span>
<span class="eye">
{% if not scu.is_passerelle_disabled() %}
<a href="{{url_for('notes.formsemestre_change_publication_bul', scodoc_dept=g.scodoc_dept,
formsemestre_id=sco.sem.id)}}">
{% if sco.sem.bul_hide_xml %}
formsemestre_id=sco.formsemestre.id)}}">
{% if sco.formsemestre.bul_hide_xml %}
{{ scu.ICON_HIDDEN|safe}}
{% else %}
{{ scu.ICON_PUBLISHED|safe }}
{% endif %}
</a>
{% endif %}
</span>
</div>
{{ sco.sem_menu_bar|safe }}
{{ sco.formsemestre_status_menu_bar()|safe }}
</div>
<!-- end of formsemestre_header -->

View File

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

View File

@ -1,5 +1,5 @@
{%- extends 'babase.j2' -%}
{# -*- Base des pages ordinaires, dans départements -*- #}
{% extends 'babase.j2' %}
{% block styles %}
{{super()}}
@ -21,24 +21,25 @@
{% endblock %}
{% block content %}
<!-- sco_page -->
{% block scodoc_sidebar %}
{% include "sidebar.j2" %}
{% include "sidebar.j2" %}
{% endblock %}
<div id="gtrcontent" class="gtrcontent">
{% include "flashed_messages.j2" %}
{% if sco.sem %}
{% block formsemestre_header %}
{% include "formsemestre_header.j2" %}
{% endblock %}
{% endif %}
<div id="gtrcontent" class="gtrcontent">
{% include "flashed_messages.j2" %}
{% if sco.formsemestre %}
{% block formsemestre_header %}
{% include "formsemestre_header.j2" %}
{% endblock %}
{% endif %}
<div class="sco-app-content">
{% block app_content %}
page vide
{% endblock %}
<div class="sco-app-content">
{% block app_content %}
page vide
{% endblock %}
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
@ -50,7 +51,7 @@
<script>
window.onload = function () { enableTooltips("gtrcontent") };
const SCO_URL = "{{ url_for('scolar.index_html', scodoc_dept=g.scodoc_dept)[:-11] }}";
const SCO_URL = "{{ url_for('scolar.index_html', scodoc_dept=g.scodoc_dept) }}";
</script>
{% endblock %}

View File

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

View File

@ -237,7 +237,7 @@ span.calendarEdit {
<input class=groupe type=checkbox ${partition.show_in_lists ? "checked" : ""} data-attr=show_in_lists> Afficher sur bulletins et tableaux
</label>
<label>
<a class="stdlink" href="{{scu.ScoURL()
<a class="stdlink" href="{{url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
}}/groups_auto_repartition/${partition.id}">Répartir les étudiants</a>
</label>
</div>

View File

@ -58,7 +58,7 @@
{% if sco.etud_cur_sem %}
<span title="absences du {{ sco.etud_cur_sem['date_debut'].strftime('%d/%m/%Y') }}
au {{ sco.etud_cur_sem['date_fin'].strftime('%d/%m/%Y') }}">({{sco.prefs["assi_metrique"]}})
<br />{{'%1g'|format(sco.nbabsjust)}} J., {{'%1g'|format(sco.nbabsnj)}} N.J.</span>
<br />{{'%1g'|format(sco.nb_abs_just)}} J., {{'%1g'|format(sco.nb_abs_nj)}} N.J.</span>
{% endif %}
<ul>
{% if current_user.has_permission(sco.Permission.AbsChange) %}

View File

@ -2,6 +2,7 @@
"""ScoDoc Flask views
"""
import datetime
from functools import cached_property
from flask import Blueprint
from flask import g, current_app, request
@ -57,8 +58,20 @@ class ScoData:
self.Permission = Permission
self.scu = scu
self.SCOVERSION = sco_version.SCOVERSION
# -- Informations étudiant courant, si sélectionné:
if etud is None:
self._init_etud = etud
self._init_formsemestre = formsemestre
# les comptes d'absences sont initialisés lors de l'accès à etud_cur_sem
self.nb_abs_nj = 0
self.nb_abs_just = 0
self.nb_abs = 0
# .etud, .formsemestre, etc. sont des cached_property
# car ScoData() est créé par @context_processor
# AVANT le décorateur scodoc qui initialise g.scodoc_xxx
@cached_property
def etud(self) -> Identite | None:
"Informations étudiant courant, si sélectionné"
if self._init_etud is None:
etudid = g.get("etudid", None)
if etudid is None:
if request.method == "GET":
@ -66,50 +79,65 @@ class ScoData:
elif request.method == "POST":
etudid = request.form.get("etudid", None)
if etudid is not None:
etud = Identite.get_etud(etudid)
self.etud = etud
if etud is not None:
# Infos sur l'étudiant courant
ins = self.etud.inscription_courante()
if ins:
self.etud_cur_sem = ins.formsemestre
(
self.nbabsnj,
self.nbabsjust,
self.nbabs,
) = sco_assiduites.get_assiduites_count_in_interval(
etud.id,
self.etud_cur_sem.date_debut.isoformat(),
self.etud_cur_sem.date_fin.isoformat(),
scu.translate_assiduites_metric(
sco_preferences.get_preference("assi_metrique")
),
)
else:
self.etud_cur_sem = None
else:
self.etud = None
# --- Informations sur semestre courant, si sélectionné
if formsemestre is None:
formsemestre_id = retreive_formsemestre_from_request()
if formsemestre_id is not None:
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
if formsemestre is None:
self.sem = None
self.sem_menu_bar = None
else:
self.sem = formsemestre
self.sem_menu_bar = sco_formsemestre_status.formsemestre_status_menubar(
self.sem
return Identite.get_etud(etudid)
return self._init_etud
@cached_property
def etud_cur_sem(self) -> FormSemestre | None:
"le semestre courant de l'étudiant courant"
etud = self.etud
if etud is None:
return None
ins = self.etud.inscription_courante()
cur_sem = ins.formsemestre
if ins:
(
self.nb_abs_nj,
self.nb_abs_just,
self.nb_abs,
) = sco_assiduites.get_assiduites_count_in_interval(
etud.id,
cur_sem.date_debut.isoformat(),
cur_sem.date_fin.isoformat(),
scu.translate_assiduites_metric(
sco_preferences.get_preference("assi_metrique")
),
)
self.formsemestre = formsemestre
# --- Préférences
# prefs fallback to global pref if sem is None:
if formsemestre:
formsemestre_id = formsemestre.id
else:
formsemestre_id = None
self.prefs = sco_preferences.SemPreferences(formsemestre_id)
return cur_sem
return None
@cached_property
def formsemestre(self) -> FormSemestre | None:
"Le formsemestre courant, si sélectionné"
if self._init_formsemestre is None:
formsemestre_id = retreive_formsemestre_from_request()
return (
FormSemestre.get_formsemestre(formsemestre_id)
if formsemestre_id is not None
else None
)
return self._init_formsemestre
@cached_property
def sem_menu_bar(self) -> str | None:
"Le html de la bare de menu formsemestre s'il y en a un."
return (
sco_formsemestre_status.formsemestre_status_menubar(self.formsemestre)
if self.formsemestre
else None
)
@cached_property
def prefs(self):
"Préférences"
# prefs fallback to global pref if no current formsemestre:
return sco_preferences.SemPreferences(
self.formsemestre.id if self.formsemestre else None
)
def formsemestre_status_menu_bar(self) -> str:
"Le HTML de la barre de menu semestre"
return sco_formsemestre_status.formsemestre_status_menubar(self.formsemestre)
from app.views import (

View File

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

View File

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

View File

@ -685,21 +685,29 @@ def module_clone():
#
@bp.route("/")
@bp.route("/index_html")
@bp.route("/index_html", alias=True)
@scodoc
@permission_required(Permission.ScoView)
def index_html():
"Page accueil formations"
fmt = request.args.get("fmt", "html")
detail = scu.to_bool(request.args.get("detail", False))
editable = current_user.has_permission(Permission.EditFormation)
table = sco_formations.formation_list_table()
table = sco_formations.formation_list_table(detail=detail)
if fmt != "html":
return table.make_page(fmt=fmt, filename=f"Formations-{g.scodoc_dept}")
H = [
html_sco_header.sco_header(page_title="Formations (programmes)"),
"""<h2>Formations (programmes pédagogiques)</h2>
f"""<h2>Formations (programmes pédagogiques)</h2>
<form>
<input type="checkbox" id="detailCheckbox" name="detail"
onchange="this.form.submit();"
{'checked' if detail else ''}>
<label for="detailCheckbox">Informations détaillées</label>
</form>
""",
table.html(),
]
@ -799,7 +807,7 @@ def formation_import_xml_form():
{ html_sco_header.sco_footer() }
"""
elif tf[0] == -1:
return flask.redirect(scu.NotesURL())
return flask.redirect(url_for("notes.index_html", scodoc_dept=g.scodoc_dept))
else:
formation_id, _, _ = sco_formations.formation_import_xml(
tf[2]["xmlfile"].read()

View File

@ -3,6 +3,7 @@ PN / Référentiel de compétences
Emmanuel Viennet, 2021
"""
from pathlib import Path
import re
@ -19,11 +20,20 @@ from app import db, log
from app.decorators import scodoc, permission_required
from app.models import Formation
from app.models.but_refcomp import ApcReferentielCompetences
from app.but import change_refcomp
from app.but.import_refcomp import orebut_import_refcomp
from app.but.forms.refcomp_forms import FormationRefCompForm, RefCompLoadForm
from app.but.forms.refcomp_forms import (
FormationChangeRefCompForm,
FormationRefCompForm,
RefCompLoadForm,
)
from app.scodoc.gen_tables import GenTable
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import ScoFormatError, ScoValueError
from app.scodoc.sco_exceptions import (
ScoFormatError,
ScoNoReferentielCompetences,
ScoValueError,
)
from app.scodoc.sco_permissions import Permission
from app.views import notes_bp as bp
from app.views import ScoData
@ -47,9 +57,12 @@ def refcomp(refcomp_id):
def refcomp_show(refcomp_id):
"""Affichage du référentiel de compétences."""
referentiel_competence = ApcReferentielCompetences.query.get_or_404(refcomp_id)
# Autres référentiels "équivalents" pour proposer de changer les formations:
referentiels_equivalents = referentiel_competence.equivalents()
return render_template(
"but/refcomp_show.j2",
ref=referentiel_competence,
referentiels_equivalents=referentiels_equivalents,
title="Référentiel de compétences",
data_source=url_for(
"notes.refcomp",
@ -279,3 +292,55 @@ def refcomp_load(formation_id=None):
formation=formation,
title="Chargement réf. compétences",
)
@bp.route("/formation/<int:formation_id>/change_refcomp", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.EditFormation)
def formation_change_refcomp(formation_id: int):
"""Tente de changer le ref. de comp. de la formation"""
formation = Formation.get_formation(formation_id)
ref_comp: ApcReferentielCompetences = formation.referentiel_competence
if ref_comp is None:
raise ScoNoReferentielCompetences(formation=formation)
# Autres référentiels "équivalents" pour proposer de changer les formations:
referentiels_equivalents = ref_comp.equivalents()
form = FormationChangeRefCompForm()
form.object_select.choices = [
(ref.id, ref.get_title()) for ref in referentiels_equivalents
]
if request.method == "POST" and form.cancel.data: # cancel button
return redirect(
url_for(
"notes.refcomp_show",
scodoc_dept=g.scodoc_dept,
refcomp_id=formation.referentiel_competence.id,
)
)
if form.validate_on_submit():
try:
new_ref_id = int(form.object_select.data)
except TypeError as exc:
raise ScoValueError("nouveau refcomp id invalide") from exc
new_ref = None
for ref in referentiels_equivalents:
if ref.id == new_ref_id:
new_ref = ref
break
if new_ref is None:
raise ScoValueError("nouveau refcomp invalide")
change_refcomp.formation_change_referentiel(formation, new_ref)
flash("Formation changée de référentiel")
return redirect(
url_for(
"notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation.id
)
)
return render_template(
"but/change_refcomp.j2",
form=form,
formation=formation,
referentiels_equivalents=referentiels_equivalents,
title="Changer de référentiel de compétences",
)

View File

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

View File

@ -340,8 +340,8 @@ def showEtudLog(etudid, fmt="html"):
# ---------- PAGE ACCUEIL (listes) --------------
@bp.route("/", alias=True)
@bp.route("/index_html")
@bp.route("/")
@bp.route("/index_html", alias=True)
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
@ -823,7 +823,8 @@ def form_change_coordonnees(etudid):
("telephonemobile", {"size": 13, "title": "Mobile"}),
),
initvalues=adr,
submitlabel="Valider le formulaire",
submitlabel="Enregistrer",
cancelbutton="Annuler",
)
dest_url = url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
if tf[0] == 0:
@ -1953,7 +1954,7 @@ def etudident_delete(etudid: int = -1, dialog_confirmed=False):
for formsemestre_id in formsemestre_ids_to_inval:
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id)
flash("Étudiant supprimé !")
return flask.redirect(scu.ScoURL())
return flask.redirect(url_for("scolar.index_html", scodoc_dept=g.scodoc_dept))
@bp.route("/check_group_apogee")
@ -2147,7 +2148,7 @@ def form_students_import_excel(formsemestre_id=None):
)
else:
sem = None
dest_url = scu.ScoURL()
dest_url = url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
if sem and not sem["etat"]:
raise ScoValueError("Modification impossible: semestre verrouille")
H = [
@ -2182,13 +2183,15 @@ def form_students_import_excel(formsemestre_id=None):
)
else:
H.append(
"""
f"""
<p>Pour inscrire directement les étudiants dans un semestre de
formation, il suffit d'indiquer le code de ce semestre
(qui doit avoir été créé au préalable). <a class="stdlink" href="%s?showcodes=1">Cliquez ici pour afficher les codes</a>
(qui doit avoir été créé au préalable).
<a class="stdlink" href="{
url_for("scolar.index_html", showcodes=1, scodoc_dept=g.scodoc_dept)
}">Cliquez ici pour afficher les codes</a>
</p>
"""
% (scu.ScoURL())
)
H.append("""<ol><li>""")
@ -2413,9 +2416,11 @@ def form_students_import_infos_admissions(formsemestre_id=None):
return "\n".join(H) + tf[1] + help_text + F
elif tf[0] == -1:
return flask.redirect(
scu.ScoURL()
+ "/formsemestre_status?formsemestre_id="
+ str(formsemestre_id)
url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
)
else:
return sco_import_etuds.students_import_admission(

View File

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

View File

@ -0,0 +1,18 @@
# Certains référentiels de compétences peuvent être considérés
# comme équivalents
# Si ils ont la même structure
# Voir ApcReferentielCompetences.map_to_other_referentiel
# Mappings: nouveau : ancien
QLIO: # la clé est 'specialite'
parcours: # codes de parcours
OSC: MSC
QMI: MQSE
# et un transitoire UPHF:
MPBS: MP
PCLG: MSC
QPSMI: MQSE
ATN: MTD
# competences: # titres de compétences ('nom_court' dans le XML)
SD: STID

View File

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

View File

@ -45,7 +45,7 @@ from app.models.evaluations import Evaluation
from app.scodoc import sco_dump_db
from app.scodoc.sco_logos import make_logo_local
from app.scodoc.sco_permissions import Permission
from app.views import notes, scolar
from app.views import notes, scolar, ScoData
import app.scodoc.sco_utils as scu
import tools
from tools.fakedatabase import create_test_api_database
@ -58,9 +58,9 @@ cli.register(app)
@app.context_processor
def inject_sco_utils():
"Make scu available in all Jinja templates"
"Make scu and sco available in all Jinja templates"
# if modified, put the same in conftest.py#27
return dict(scu=scu)
return {"scu": scu, "sco": ScoData()}
@app.shell_context_processor

View File

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

View File

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

View File

@ -12,6 +12,7 @@ from app.auth.models import User, Role
from app.auth.models import get_super_admin
from app.scodoc import notesdb as ndb
import app.scodoc.sco_utils as scu
from app.views import ScoData
RESOURCES_DIR = "/opt/scodoc/tests/ressources"
@ -27,7 +28,7 @@ def test_client():
@apptest.context_processor
def inject_sco_utils():
"Make scu available in all Jinja templates"
return dict(scu=scu)
return {"scu": scu, "sco": ScoData()}
with apptest.test_request_context():
# initialize scodoc "g":

View File

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

View File

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

View File

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

View File

@ -3,7 +3,8 @@
"""Outils pour environnements de démo.
Change aléatoirement les identites (nip, civilite, nom, prenom) des étudiants d'un semestre.
Change aléatoirement les identites (nip, civilite, nom, prenom) des étudiants.
Un semestre ou tous si non spécifié.
Le NIP est choisi aléatoirement (nombre entier à 8 chiffres).
Les noms et prénoms sont issus des fichiers noms.txt, prenoms-h.txt, prenoms-f.txt
@ -19,37 +20,39 @@ from gen_nomprenoms import nomprenom
def usage():
print(f"Usage: {sys.argv[0]} dbname formsemestre_id")
print(f"Usage: {sys.argv[0]} dbname [formsemestre_id]")
sys.exit(1)
if len(sys.argv) != 3:
if len(sys.argv) not in (2, 3):
usage()
dbname = sys.argv[1]
formsemestre_id = sys.argv[2]
formsemestre_id = sys.argv[2] if len(sys.argv) > 2 else None
DBCNXSTRING = f"dbname={dbname}"
# Liste des etudiants inscrits à ce semestre
cnx = psycopg2.connect(DBCNXSTRING)
cursor = cnx.cursor()
cursor.execute(
"select count(*) from notes_formsemestre where id=%(formsemestre_id)s",
{"formsemestre_id": formsemestre_id},
)
nsem = cursor.fetchone()[0]
if nsem != 1:
print(f"{nsem} formsemestre matching {formsemestre_id} in {dbname}")
sys.exit(2)
cursor.execute(
"""select i.id
from identite i, notes_formsemestre_inscription ins
where i.id=ins.etudid and ins.formsemestre_id=%(formsemestre_id)s
""",
{"formsemestre_id": formsemestre_id},
)
if formsemestre_id is None:
cursor.execute("SELECT i.id from identite i")
else:
cursor.execute(
"select count(*) from notes_formsemestre where id=%(formsemestre_id)s",
{"formsemestre_id": formsemestre_id},
)
nsem = cursor.fetchone()[0]
if nsem != 1:
print(f"{nsem} formsemestre matching {formsemestre_id} in {dbname}")
sys.exit(2)
cursor.execute(
"""select i.id
from identite i, notes_formsemestre_inscription ins
where i.id=ins.etudid and ins.formsemestre_id=%(formsemestre_id)s
""",
{"formsemestre_id": formsemestre_id},
)
wcursor = cnx.cursor()
n = 0

File diff suppressed because it is too large Load Diff