forked from ScoDoc/ScoDoc
Merge branch 'scodoc-master' into pe-BUT-v2
This commit is contained in:
commit
be39245e25
@ -374,115 +374,114 @@ def formsemestre_etudiants(
|
|||||||
return sorted(etuds, key=itemgetter("sort_key"))
|
return sorted(etuds, key=itemgetter("sort_key"))
|
||||||
|
|
||||||
|
|
||||||
# retrait (temporaire ? à discuter)
|
@bp.route("/formsemestre/<int:formsemestre_id>/etat_evals")
|
||||||
# @bp.route("/formsemestre/<int:formsemestre_id>/etat_evals")
|
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/etat_evals")
|
||||||
# @api_web_bp.route("/formsemestre/<int:formsemestre_id>/etat_evals")
|
@login_required
|
||||||
# @login_required
|
@scodoc
|
||||||
# @scodoc
|
@permission_required(Permission.ScoView)
|
||||||
# @permission_required(Permission.ScoView)
|
@as_json
|
||||||
# @as_json
|
def etat_evals(formsemestre_id: int):
|
||||||
# def etat_evals(formsemestre_id: int):
|
"""
|
||||||
# """
|
Informations sur l'état des évaluations d'un formsemestre.
|
||||||
# Informations sur l'état des évaluations d'un formsemestre.
|
|
||||||
|
|
||||||
# formsemestre_id : l'id d'un semestre
|
formsemestre_id : l'id d'un semestre
|
||||||
|
|
||||||
# Exemple de résultat :
|
Exemple de résultat :
|
||||||
# [
|
[
|
||||||
# {
|
{
|
||||||
# "id": 1, // moduleimpl_id
|
"id": 1, // moduleimpl_id
|
||||||
# "titre": "Initiation aux réseaux informatiques",
|
"titre": "Initiation aux réseaux informatiques",
|
||||||
# "evaluations": [
|
"evaluations": [
|
||||||
# {
|
{
|
||||||
# "id": 1,
|
"id": 1,
|
||||||
# "description": null,
|
"description": null,
|
||||||
# "datetime_epreuve": null,
|
"datetime_epreuve": null,
|
||||||
# "heure_fin": "09:00:00",
|
"heure_fin": "09:00:00",
|
||||||
# "coefficient": "02.00"
|
"coefficient": "02.00"
|
||||||
# "is_complete": true,
|
"is_complete": true,
|
||||||
# "nb_inscrits": 16,
|
"nb_inscrits": 16,
|
||||||
# "nb_manquantes": 0,
|
"nb_manquantes": 0,
|
||||||
# "ABS": 0,
|
"ABS": 0,
|
||||||
# "ATT": 0,
|
"ATT": 0,
|
||||||
# "EXC": 0,
|
"EXC": 0,
|
||||||
# "saisie_notes": {
|
"saisie_notes": {
|
||||||
# "datetime_debut": "2021-09-11T00:00:00+02:00",
|
"datetime_debut": "2021-09-11T00:00:00+02:00",
|
||||||
# "datetime_fin": "2022-08-25T00:00:00+02:00",
|
"datetime_fin": "2022-08-25T00:00:00+02:00",
|
||||||
# "datetime_mediane": "2022-03-19T00:00:00+01:00"
|
"datetime_mediane": "2022-03-19T00:00:00+01:00"
|
||||||
# }
|
}
|
||||||
# },
|
},
|
||||||
# ...
|
...
|
||||||
# ]
|
]
|
||||||
# },
|
},
|
||||||
# ]
|
]
|
||||||
# """
|
"""
|
||||||
# query = FormSemestre.query.filter_by(id=formsemestre_id)
|
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||||
# if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
# query = query.filter_by(dept_id=g.scodoc_dept_id)
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
# formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
||||||
# app.set_sco_dept(formsemestre.departement.acronym)
|
app.set_sco_dept(formsemestre.departement.acronym)
|
||||||
# nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
|
|
||||||
# result = []
|
result = []
|
||||||
# for modimpl_id in nt.modimpls_results:
|
for modimpl_id in nt.modimpls_results:
|
||||||
# modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl_id]
|
modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl_id]
|
||||||
# modimpl: ModuleImpl = ModuleImpl.query.get_or_404(modimpl_id)
|
modimpl: ModuleImpl = ModuleImpl.query.get_or_404(modimpl_id)
|
||||||
# modimpl_dict = modimpl.to_dict(convert_objects=True)
|
modimpl_dict = modimpl.to_dict(convert_objects=True, with_module=False)
|
||||||
|
|
||||||
# list_eval = []
|
list_eval = []
|
||||||
# for evaluation_id in modimpl_results.evaluations_etat:
|
for evaluation_id in modimpl_results.evaluations_etat:
|
||||||
# eval_etat = modimpl_results.evaluations_etat[evaluation_id]
|
eval_etat = modimpl_results.evaluations_etat[evaluation_id]
|
||||||
# evaluation = Evaluation.query.get_or_404(evaluation_id)
|
evaluation = Evaluation.query.get_or_404(evaluation_id)
|
||||||
# eval_dict = evaluation.to_dict_api()
|
eval_dict = evaluation.to_dict_api()
|
||||||
# eval_dict["etat"] = eval_etat.to_dict()
|
eval_dict["etat"] = eval_etat.to_dict()
|
||||||
|
|
||||||
# eval_dict["nb_inscrits"] = modimpl_results.nb_inscrits_module
|
eval_dict["nb_inscrits"] = modimpl_results.nb_inscrits_module
|
||||||
# eval_dict["nb_notes_manquantes"] = len(
|
eval_dict["nb_notes_manquantes"] = len(
|
||||||
# modimpl_results.evals_etudids_sans_note[evaluation.id]
|
modimpl_results.evals_etudids_sans_note[evaluation.id]
|
||||||
# )
|
)
|
||||||
# eval_dict["nb_notes_abs"] = sum(
|
eval_dict["nb_notes_abs"] = sum(
|
||||||
# modimpl_results.evals_notes[evaluation.id] == scu.NOTES_ABSENCE
|
modimpl_results.evals_notes[evaluation.id] == scu.NOTES_ABSENCE
|
||||||
# )
|
)
|
||||||
# eval_dict["nb_notes_att"] = eval_etat.nb_attente
|
eval_dict["nb_notes_att"] = eval_etat.nb_attente
|
||||||
# eval_dict["nb_notes_exc"] = sum(
|
eval_dict["nb_notes_exc"] = sum(
|
||||||
# modimpl_results.evals_notes[evaluation.id] == scu.NOTES_NEUTRALISE
|
modimpl_results.evals_notes[evaluation.id] == scu.NOTES_NEUTRALISE
|
||||||
# )
|
)
|
||||||
|
|
||||||
# # Récupération de toutes les notes de l'évaluation
|
# Récupération de toutes les notes de l'évaluation
|
||||||
# # eval["notes"] = modimpl_results.get_eval_notes_dict(evaluation_id)
|
# eval["notes"] = modimpl_results.get_eval_notes_dict(evaluation_id)
|
||||||
|
|
||||||
# notes = NotesNotes.query.filter_by(evaluation_id=evaluation.id).all()
|
notes = NotesNotes.query.filter_by(evaluation_id=evaluation.id).all()
|
||||||
|
|
||||||
# date_debut = None
|
date_debut = None
|
||||||
# date_fin = None
|
date_fin = None
|
||||||
# date_mediane = None
|
date_mediane = None
|
||||||
|
|
||||||
# # Si il y a plus d'une note saisie pour l'évaluation
|
# Si il y a plus d'une note saisie pour l'évaluation
|
||||||
# if len(notes) >= 1:
|
if len(notes) >= 1:
|
||||||
# # Tri des notes en fonction de leurs dates
|
# Tri des notes en fonction de leurs dates
|
||||||
# notes_sorted = sorted(notes, key=attrgetter("date"))
|
notes_sorted = sorted(notes, key=attrgetter("date"))
|
||||||
|
|
||||||
# date_debut = notes_sorted[0].date
|
date_debut = notes_sorted[0].date
|
||||||
# date_fin = notes_sorted[-1].date
|
date_fin = notes_sorted[-1].date
|
||||||
|
|
||||||
# # Note médiane
|
# Note médiane
|
||||||
# date_mediane = notes_sorted[len(notes_sorted) // 2].date
|
date_mediane = notes_sorted[len(notes_sorted) // 2].date
|
||||||
|
|
||||||
# eval_dict["saisie_notes"] = {
|
eval_dict["saisie_notes"] = {
|
||||||
# "datetime_debut": date_debut.isoformat()
|
"datetime_debut": date_debut.isoformat()
|
||||||
# if date_debut is not None
|
if date_debut is not None
|
||||||
# else None,
|
else None,
|
||||||
# "datetime_fin": date_fin.isoformat() if date_fin is not None else None,
|
"datetime_fin": date_fin.isoformat() if date_fin is not None else None,
|
||||||
# "datetime_mediane": date_mediane.isoformat()
|
"datetime_mediane": date_mediane.isoformat()
|
||||||
# if date_mediane is not None
|
if date_mediane is not None
|
||||||
# else None,
|
else None,
|
||||||
# }
|
}
|
||||||
|
|
||||||
# list_eval.append(eval_dict)
|
list_eval.append(eval_dict)
|
||||||
|
|
||||||
# modimpl_dict["evaluations"] = list_eval
|
modimpl_dict["evaluations"] = list_eval
|
||||||
# result.append(modimpl_dict)
|
result.append(modimpl_dict)
|
||||||
# return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/formsemestre/<int:formsemestre_id>/resultats")
|
@bp.route("/formsemestre/<int:formsemestre_id>/resultats")
|
||||||
|
@ -102,6 +102,8 @@ class User(UserMixin, ScoDocModel):
|
|||||||
token = db.Column(db.Text(), index=True, unique=True)
|
token = db.Column(db.Text(), index=True, unique=True)
|
||||||
token_expiration = db.Column(db.DateTime)
|
token_expiration = db.Column(db.DateTime)
|
||||||
|
|
||||||
|
# Define the back reference from User to ModuleImpl
|
||||||
|
modimpls = db.relationship("ModuleImpl", back_populates="responsable")
|
||||||
roles = db.relationship("Role", secondary="user_role", viewonly=True)
|
roles = db.relationship("Role", secondary="user_role", viewonly=True)
|
||||||
Permission = Permission
|
Permission = Permission
|
||||||
|
|
||||||
@ -245,24 +247,26 @@ class User(UserMixin, ScoDocModel):
|
|||||||
def to_dict(self, include_email=True):
|
def to_dict(self, include_email=True):
|
||||||
"""l'utilisateur comme un dict, avec des champs supplémentaires"""
|
"""l'utilisateur comme un dict, avec des champs supplémentaires"""
|
||||||
data = {
|
data = {
|
||||||
"date_expiration": self.date_expiration.isoformat() + "Z"
|
"date_expiration": (
|
||||||
if self.date_expiration
|
self.date_expiration.isoformat() + "Z" if self.date_expiration else None
|
||||||
else None,
|
),
|
||||||
"date_modif_passwd": self.date_modif_passwd.isoformat() + "Z"
|
"date_modif_passwd": (
|
||||||
if self.date_modif_passwd
|
self.date_modif_passwd.isoformat() + "Z"
|
||||||
else None,
|
if self.date_modif_passwd
|
||||||
"date_created": self.date_created.isoformat() + "Z"
|
else None
|
||||||
if self.date_created
|
),
|
||||||
else None,
|
"date_created": (
|
||||||
|
self.date_created.isoformat() + "Z" if self.date_created else None
|
||||||
|
),
|
||||||
"dept": self.dept,
|
"dept": self.dept,
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"active": self.active,
|
"active": self.active,
|
||||||
"cas_id": self.cas_id,
|
"cas_id": self.cas_id,
|
||||||
"cas_allow_login": self.cas_allow_login,
|
"cas_allow_login": self.cas_allow_login,
|
||||||
"cas_allow_scodoc_login": self.cas_allow_scodoc_login,
|
"cas_allow_scodoc_login": self.cas_allow_scodoc_login,
|
||||||
"cas_last_login": self.cas_last_login.isoformat() + "Z"
|
"cas_last_login": (
|
||||||
if self.cas_last_login
|
self.cas_last_login.isoformat() + "Z" if self.cas_last_login else None
|
||||||
else None,
|
),
|
||||||
"edt_id": self.edt_id,
|
"edt_id": self.edt_id,
|
||||||
"status_txt": "actif" if self.active else "fermé",
|
"status_txt": "actif" if self.active else "fermé",
|
||||||
"last_seen": self.last_seen.isoformat() + "Z" if self.last_seen else None,
|
"last_seen": self.last_seen.isoformat() + "Z" if self.last_seen else None,
|
||||||
@ -477,8 +481,8 @@ class User(UserMixin, ScoDocModel):
|
|||||||
return f"{nom} {scu.format_prenom(self.prenom)} ({self.user_name})"
|
return f"{nom} {scu.format_prenom(self.prenom)} ({self.user_name})"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_user_id_from_nomplogin(nomplogin: str) -> Optional[int]:
|
def get_user_from_nomplogin(nomplogin: str) -> Optional["User"]:
|
||||||
"""Returns id from the string "Dupont Pierre (dupont)"
|
"""Returns User instance from the string "Dupont Pierre (dupont)"
|
||||||
or None if user does not exist
|
or None if user does not exist
|
||||||
"""
|
"""
|
||||||
match = re.match(r".*\((.*)\)", nomplogin.strip())
|
match = re.match(r".*\((.*)\)", nomplogin.strip())
|
||||||
@ -486,7 +490,7 @@ class User(UserMixin, ScoDocModel):
|
|||||||
user_name = match.group(1)
|
user_name = match.group(1)
|
||||||
u = User.query.filter_by(user_name=user_name).first()
|
u = User.query.filter_by(user_name=user_name).first()
|
||||||
if u:
|
if u:
|
||||||
return u.id
|
return u
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_nom_fmt(self):
|
def get_nom_fmt(self):
|
||||||
|
@ -35,7 +35,6 @@ from app.decorators import (
|
|||||||
permission_required,
|
permission_required,
|
||||||
)
|
)
|
||||||
from app.models import FormSemestre, FormSemestreInscription, Identite
|
from app.models import FormSemestre, FormSemestreInscription, Identite
|
||||||
from app.scodoc import sco_bulletins_pdf
|
|
||||||
from app.scodoc.codes_cursus import UE_STANDARD
|
from app.scodoc.codes_cursus import UE_STANDARD
|
||||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
|
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
|
||||||
from app.scodoc.sco_logos import find_logo
|
from app.scodoc.sco_logos import find_logo
|
||||||
|
@ -194,7 +194,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
|||||||
"""Génère la partie "titre" du bulletin de notes.
|
"""Génère la partie "titre" du bulletin de notes.
|
||||||
Renvoie une liste d'objets platypus
|
Renvoie une liste d'objets platypus
|
||||||
"""
|
"""
|
||||||
# comme les bulletins standard, mais avec notre préférence
|
# comme les bulletins standards, mais avec notre préférence
|
||||||
return super().bul_title_pdf(preference_field=preference_field)
|
return super().bul_title_pdf(preference_field=preference_field)
|
||||||
|
|
||||||
def bul_part_below(self, fmt="pdf") -> list:
|
def bul_part_below(self, fmt="pdf") -> list:
|
||||||
@ -406,6 +406,8 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
|||||||
|
|
||||||
def boite_identite(self) -> list:
|
def boite_identite(self) -> list:
|
||||||
"Les informations sur l'identité et l'inscription de l'étudiant"
|
"Les informations sur l'identité et l'inscription de l'étudiant"
|
||||||
|
parcour = self.formsemestre.etuds_inscriptions[self.etud.id].parcour
|
||||||
|
|
||||||
return [
|
return [
|
||||||
Paragraph(
|
Paragraph(
|
||||||
SU(f"""{self.etud.nomprenom}"""),
|
SU(f"""{self.etud.nomprenom}"""),
|
||||||
@ -416,6 +418,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
|||||||
f"""
|
f"""
|
||||||
<b>{self.bul["demission"]}</b><br/>
|
<b>{self.bul["demission"]}</b><br/>
|
||||||
Formation: {self.formsemestre.titre_num()}<br/>
|
Formation: {self.formsemestre.titre_num()}<br/>
|
||||||
|
{'Parcours ' + parcour.code + '<br/>' if parcour else ''}
|
||||||
Année universitaire: {self.formsemestre.annee_scolaire_str()}<br/>
|
Année universitaire: {self.formsemestre.annee_scolaire_str()}<br/>
|
||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
|
@ -97,7 +97,7 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
|
|||||||
<span class="avertissement_redoublement">{formsemestre_2.annee_scolaire_str()
|
<span class="avertissement_redoublement">{formsemestre_2.annee_scolaire_str()
|
||||||
if formsemestre_2 else ""}</span>
|
if formsemestre_2 else ""}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="titre">RCUE</div>
|
<div class="titre" title="Décisions sur RCUEs enregistrées sur l'ensemble du cursus">RCUE</div>
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
for dec_rcue in deca.get_decisions_rcues_annee():
|
for dec_rcue in deca.get_decisions_rcues_annee():
|
||||||
|
@ -178,19 +178,25 @@ class ModuleImplResults:
|
|||||||
eval_notes_inscr = evals_notes[str(evaluation.id)][list(inscrits_module)]
|
eval_notes_inscr = evals_notes[str(evaluation.id)][list(inscrits_module)]
|
||||||
# Nombre de notes (non vides, incluant ATT etc) des inscrits:
|
# Nombre de notes (non vides, incluant ATT etc) des inscrits:
|
||||||
nb_notes = eval_notes_inscr.notna().sum()
|
nb_notes = eval_notes_inscr.notna().sum()
|
||||||
|
# Etudiants avec notes en attente:
|
||||||
|
# = ceux avec note ATT
|
||||||
eval_etudids_attente = set(
|
eval_etudids_attente = set(
|
||||||
eval_notes_inscr.iloc[
|
eval_notes_inscr.iloc[
|
||||||
(eval_notes_inscr == scu.NOTES_ATTENTE).to_numpy()
|
(eval_notes_inscr == scu.NOTES_ATTENTE).to_numpy()
|
||||||
].index
|
].index
|
||||||
)
|
)
|
||||||
|
if evaluation.publish_incomplete:
|
||||||
|
# et en "imédiat", tous ceux sans note
|
||||||
|
eval_etudids_attente |= etudids_sans_note
|
||||||
|
# Synthèse pour état du module:
|
||||||
self.etudids_attente |= eval_etudids_attente
|
self.etudids_attente |= eval_etudids_attente
|
||||||
self.evaluations_etat[evaluation.id] = EvaluationEtat(
|
self.evaluations_etat[evaluation.id] = EvaluationEtat(
|
||||||
evaluation_id=evaluation.id,
|
evaluation_id=evaluation.id,
|
||||||
nb_attente=len(eval_etudids_attente),
|
nb_attente=len(eval_etudids_attente),
|
||||||
nb_notes=nb_notes,
|
nb_notes=int(nb_notes),
|
||||||
is_complete=is_complete,
|
is_complete=is_complete,
|
||||||
)
|
)
|
||||||
# au moins une note en ATT dans ce modimpl:
|
# au moins une note en attente (ATT ou manquante en mode "immédiat") dans ce modimpl:
|
||||||
self.en_attente = bool(self.etudids_attente)
|
self.en_attente = bool(self.etudids_attente)
|
||||||
|
|
||||||
# Force columns names to integers (evaluation ids)
|
# Force columns names to integers (evaluation ids)
|
||||||
|
@ -52,7 +52,7 @@ class ScoDocModel(db.Model):
|
|||||||
def create_from_dict(cls, data: dict) -> "ScoDocModel":
|
def create_from_dict(cls, data: dict) -> "ScoDocModel":
|
||||||
"""Create a new instance of the model with attributes given in dict.
|
"""Create a new instance of the model with attributes given in dict.
|
||||||
The instance is added to the session (but not flushed nor committed).
|
The instance is added to the session (but not flushed nor committed).
|
||||||
Use only relevant arributes for the given model and ignore others.
|
Use only relevant attributes for the given model and ignore others.
|
||||||
"""
|
"""
|
||||||
if data:
|
if data:
|
||||||
args = cls.convert_dict_fields(cls.filter_model_attributes(data))
|
args = cls.convert_dict_fields(cls.filter_model_attributes(data))
|
||||||
|
@ -445,10 +445,11 @@ class Identite(models.ScoDocModel):
|
|||||||
"prenom_etat_civil": self.prenom_etat_civil,
|
"prenom_etat_civil": self.prenom_etat_civil,
|
||||||
}
|
}
|
||||||
|
|
||||||
def to_dict_scodoc7(self, restrict=False) -> dict:
|
def to_dict_scodoc7(self, restrict=False, with_inscriptions=False) -> dict:
|
||||||
"""Représentation dictionnaire,
|
"""Représentation dictionnaire,
|
||||||
compatible ScoDoc7 mais sans infos admission.
|
compatible ScoDoc7 mais sans infos admission.
|
||||||
Si restrict, cache les infos "personnelles" si pas permission ViewEtudData
|
Si restrict, cache les infos "personnelles" si pas permission ViewEtudData
|
||||||
|
Si with_inscriptions, inclut les champs "inscription"
|
||||||
"""
|
"""
|
||||||
e_dict = self.__dict__.copy() # dict(self.__dict__)
|
e_dict = self.__dict__.copy() # dict(self.__dict__)
|
||||||
e_dict.pop("_sa_instance_state", None)
|
e_dict.pop("_sa_instance_state", None)
|
||||||
@ -460,6 +461,8 @@ class Identite(models.ScoDocModel):
|
|||||||
adresse = self.adresses.first()
|
adresse = self.adresses.first()
|
||||||
if adresse:
|
if adresse:
|
||||||
e_dict.update(adresse.to_dict(restrict=restrict))
|
e_dict.update(adresse.to_dict(restrict=restrict))
|
||||||
|
if with_inscriptions:
|
||||||
|
e_dict.update(self.inscription_descr())
|
||||||
return {k: v or "" for k, v in e_dict.items()} # convert_null_outputs_to_empty
|
return {k: v or "" for k, v in e_dict.items()} # convert_null_outputs_to_empty
|
||||||
|
|
||||||
def to_dict_bul(self, include_urls=True):
|
def to_dict_bul(self, include_urls=True):
|
||||||
@ -574,7 +577,9 @@ class Identite(models.ScoDocModel):
|
|||||||
return r[0] if r else None
|
return r[0] if r else None
|
||||||
|
|
||||||
def inscription_descr(self) -> dict:
|
def inscription_descr(self) -> dict:
|
||||||
"""Description de l'état d'inscription"""
|
"""Description de l'état d'inscription
|
||||||
|
avec champs compatibles templates ScoDoc7
|
||||||
|
"""
|
||||||
inscription_courante = self.inscription_courante()
|
inscription_courante = self.inscription_courante()
|
||||||
if inscription_courante:
|
if inscription_courante:
|
||||||
titre_sem = inscription_courante.formsemestre.titre_mois()
|
titre_sem = inscription_courante.formsemestre.titre_mois()
|
||||||
@ -585,7 +590,7 @@ class Identite(models.ScoDocModel):
|
|||||||
else:
|
else:
|
||||||
inscr_txt = "Inscrit en"
|
inscr_txt = "Inscrit en"
|
||||||
|
|
||||||
return {
|
result = {
|
||||||
"etat_in_cursem": inscription_courante.etat,
|
"etat_in_cursem": inscription_courante.etat,
|
||||||
"inscription_courante": inscription_courante,
|
"inscription_courante": inscription_courante,
|
||||||
"inscription": titre_sem,
|
"inscription": titre_sem,
|
||||||
@ -608,15 +613,20 @@ class Identite(models.ScoDocModel):
|
|||||||
inscription = "ancien"
|
inscription = "ancien"
|
||||||
situation = "ancien élève"
|
situation = "ancien élève"
|
||||||
else:
|
else:
|
||||||
inscription = ("non inscrit",)
|
inscription = "non inscrit"
|
||||||
situation = inscription
|
situation = inscription
|
||||||
return {
|
result = {
|
||||||
"etat_in_cursem": "?",
|
"etat_in_cursem": "?",
|
||||||
"inscription_courante": None,
|
"inscription_courante": None,
|
||||||
"inscription": inscription,
|
"inscription": inscription,
|
||||||
"inscription_str": inscription,
|
"inscription_str": inscription,
|
||||||
"situation": situation,
|
"situation": situation,
|
||||||
}
|
}
|
||||||
|
# aliases pour compat templates ScoDoc7
|
||||||
|
result["etatincursem"] = result["etat_in_cursem"]
|
||||||
|
result["inscriptionstr"] = result["inscription_str"]
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
def inscription_etat(self, formsemestre_id: int) -> str:
|
def inscription_etat(self, formsemestre_id: int) -> str:
|
||||||
"""État de l'inscription de cet étudiant au semestre:
|
"""État de l'inscription de cet étudiant au semestre:
|
||||||
@ -749,9 +759,7 @@ def check_etud_duplicate_code(args, code_name, edit=True):
|
|||||||
).all()
|
).all()
|
||||||
duplicate = False
|
duplicate = False
|
||||||
if edit:
|
if edit:
|
||||||
duplicate = (len(etuds) > 1) or (
|
duplicate = (len(etuds) > 1) or ((len(etuds) == 1) and etuds[0].id != etudid)
|
||||||
(len(etuds) == 1) and etuds[0].id != args["etudid"]
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
duplicate = len(etuds) > 0
|
duplicate = len(etuds) > 0
|
||||||
if duplicate:
|
if duplicate:
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
from flask import g, url_for
|
from flask import abort, g, url_for
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
@ -241,6 +241,25 @@ class Evaluation(db.Model):
|
|||||||
if k != "_sa_instance_state" and k != "id" and k in data:
|
if k != "_sa_instance_state" and k != "id" and k in data:
|
||||||
setattr(self, k, data[k])
|
setattr(self, k, data[k])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_evaluation(
|
||||||
|
cls, evaluation_id: int | str, dept_id: int = None
|
||||||
|
) -> "Evaluation":
|
||||||
|
"""Evaluation ou 404, cherche uniquement dans le département spécifié ou le courant."""
|
||||||
|
from app.models import FormSemestre, ModuleImpl
|
||||||
|
|
||||||
|
if not isinstance(evaluation_id, int):
|
||||||
|
try:
|
||||||
|
evaluation_id = int(evaluation_id)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
abort(404, "evaluation_id invalide")
|
||||||
|
if g.scodoc_dept:
|
||||||
|
dept_id = dept_id if dept_id is not None else g.scodoc_dept_id
|
||||||
|
query = cls.query.filter_by(id=evaluation_id)
|
||||||
|
if dept_id is not None:
|
||||||
|
query = query.join(ModuleImpl).join(FormSemestre).filter_by(dept_id=dept_id)
|
||||||
|
return query.first_or_404()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_max_numero(cls, moduleimpl_id: int) -> int:
|
def get_max_numero(cls, moduleimpl_id: int) -> int:
|
||||||
"""Return max numero among evaluations in this
|
"""Return max numero among evaluations in this
|
||||||
@ -265,7 +284,9 @@ class Evaluation(db.Model):
|
|||||||
evaluations = moduleimpl.evaluations.order_by(
|
evaluations = moduleimpl.evaluations.order_by(
|
||||||
Evaluation.date_debut, Evaluation.numero
|
Evaluation.date_debut, Evaluation.numero
|
||||||
).all()
|
).all()
|
||||||
all_numbered = all(e.numero is not None for e in evaluations)
|
numeros_distincts = {e.numero for e in evaluations if e.numero is not None}
|
||||||
|
# pas de None, pas de dupliqués
|
||||||
|
all_numbered = len(numeros_distincts) == len(evaluations)
|
||||||
if all_numbered and only_if_unumbered:
|
if all_numbered and only_if_unumbered:
|
||||||
return # all ok
|
return # all ok
|
||||||
|
|
||||||
|
@ -3,12 +3,13 @@
|
|||||||
"""
|
"""
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from flask import abort, g
|
from flask import abort, g
|
||||||
|
from flask_login import current_user
|
||||||
from flask_sqlalchemy.query import Query
|
from flask_sqlalchemy.query import Query
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.auth.models import User
|
from app.auth.models import User
|
||||||
from app.comp import df_cache
|
from app.comp import df_cache
|
||||||
from app.models import APO_CODE_STR_LEN
|
from app.models import APO_CODE_STR_LEN, ScoDocModel
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
from app.models.evaluations import Evaluation
|
from app.models.evaluations import Evaluation
|
||||||
from app.models.modules import Module
|
from app.models.modules import Module
|
||||||
@ -17,7 +18,7 @@ from app.scodoc.sco_permissions import Permission
|
|||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
class ModuleImpl(db.Model):
|
class ModuleImpl(ScoDocModel):
|
||||||
"""Mise en oeuvre d'un module pour une annee/semestre"""
|
"""Mise en oeuvre d'un module pour une annee/semestre"""
|
||||||
|
|
||||||
__tablename__ = "notes_moduleimpl"
|
__tablename__ = "notes_moduleimpl"
|
||||||
@ -36,7 +37,10 @@ class ModuleImpl(db.Model):
|
|||||||
index=True,
|
index=True,
|
||||||
nullable=False,
|
nullable=False,
|
||||||
)
|
)
|
||||||
responsable_id = db.Column("responsable_id", db.Integer, db.ForeignKey("user.id"))
|
responsable_id = db.Column(
|
||||||
|
"responsable_id", db.Integer, db.ForeignKey("user.id", ondelete="SET NULL")
|
||||||
|
)
|
||||||
|
responsable = db.relationship("User", back_populates="modimpls")
|
||||||
# formule de calcul moyenne:
|
# formule de calcul moyenne:
|
||||||
computation_expr = db.Column(db.Text())
|
computation_expr = db.Column(db.Text())
|
||||||
|
|
||||||
@ -52,8 +56,8 @@ class ModuleImpl(db.Model):
|
|||||||
secondary="notes_modules_enseignants",
|
secondary="notes_modules_enseignants",
|
||||||
lazy="dynamic",
|
lazy="dynamic",
|
||||||
backref="moduleimpl",
|
backref="moduleimpl",
|
||||||
viewonly=True,
|
|
||||||
)
|
)
|
||||||
|
"enseignants du module (sans le responsable)"
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__name__} {self.id} module={repr(self.module)}>"
|
return f"<{self.__class__.__name__} {self.id} module={repr(self.module)}>"
|
||||||
@ -85,7 +89,7 @@ class ModuleImpl(db.Model):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_modimpl(cls, moduleimpl_id: int | str, dept_id: int = None) -> "ModuleImpl":
|
def get_modimpl(cls, moduleimpl_id: int | str, dept_id: int = None) -> "ModuleImpl":
|
||||||
"""FormSemestre ou 404, cherche uniquement dans le département spécifié ou le courant."""
|
"""ModuleImpl ou 404, cherche uniquement dans le département spécifié ou le courant."""
|
||||||
from app.models.formsemestre import FormSemestre
|
from app.models.formsemestre import FormSemestre
|
||||||
|
|
||||||
if not isinstance(moduleimpl_id, int):
|
if not isinstance(moduleimpl_id, int):
|
||||||
@ -187,7 +191,7 @@ class ModuleImpl(db.Model):
|
|||||||
return allow_ens and user.id in (ens.id for ens in self.enseignants)
|
return allow_ens and user.id in (ens.id for ens in self.enseignants)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def can_change_ens_by(self, user: User, raise_exc=False) -> bool:
|
def can_change_responsable(self, user: User, raise_exc=False) -> bool:
|
||||||
"""Check if user can modify module resp.
|
"""Check if user can modify module resp.
|
||||||
If raise_exc, raises exception (AccessDenied or ScoLockedSemError) if not.
|
If raise_exc, raises exception (AccessDenied or ScoLockedSemError) if not.
|
||||||
= Admin, et dir des etud. (si option l'y autorise)
|
= Admin, et dir des etud. (si option l'y autorise)
|
||||||
@ -208,6 +212,27 @@ class ModuleImpl(db.Model):
|
|||||||
raise AccessDenied(f"Modification impossible pour {user}")
|
raise AccessDenied(f"Modification impossible pour {user}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def can_change_ens(self, user: User | None = None, raise_exc=True) -> bool:
|
||||||
|
"""check if user can modify ens list (raise exception if not)"
|
||||||
|
if user is None, current user.
|
||||||
|
"""
|
||||||
|
user = current_user if user is None else user
|
||||||
|
if not self.formsemestre.etat:
|
||||||
|
if raise_exc:
|
||||||
|
raise ScoLockedSemError("Modification impossible: semestre verrouille")
|
||||||
|
return False
|
||||||
|
# -- check access
|
||||||
|
# admin, resp. module ou resp. semestre
|
||||||
|
if (
|
||||||
|
user.id != self.responsable_id
|
||||||
|
and not user.has_permission(Permission.EditFormSemestre)
|
||||||
|
and user.id not in (u.id for u in self.formsemestre.responsables)
|
||||||
|
):
|
||||||
|
if raise_exc:
|
||||||
|
raise AccessDenied(f"Modification impossible pour {user}")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
def est_inscrit(self, etud: Identite) -> bool:
|
def est_inscrit(self, etud: Identite) -> bool:
|
||||||
"""
|
"""
|
||||||
Vérifie si l'étudiant est bien inscrit au moduleimpl (même si DEM ou DEF au semestre).
|
Vérifie si l'étudiant est bien inscrit au moduleimpl (même si DEM ou DEF au semestre).
|
||||||
|
@ -33,7 +33,7 @@ from flask import g, request
|
|||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import Evaluation, GroupDescr, ModuleImpl, Partition
|
from app.models import Evaluation, GroupDescr, Identite, ModuleImpl, Partition
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
@ -160,27 +160,32 @@ def sidebar(etudid: int = None):
|
|||||||
etudid = request.form.get("etudid", None)
|
etudid = request.form.get("etudid", None)
|
||||||
|
|
||||||
if etudid is not None:
|
if etudid is not None:
|
||||||
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
|
etud = Identite.get_etud(etudid)
|
||||||
params.update(etud)
|
|
||||||
params["fiche_url"] = url_for(
|
|
||||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
|
||||||
)
|
|
||||||
# compte les absences du semestre en cours
|
# compte les absences du semestre en cours
|
||||||
H.append(
|
H.append(
|
||||||
"""<h2 id="insidebar-etud"><a href="%(fiche_url)s" class="sidebar">
|
f"""<h2 id="insidebar-etud"><a href="{
|
||||||
<font color="#FF0000">%(civilite_str)s %(nom_disp)s</font></a>
|
url_for(
|
||||||
</h2>
|
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
||||||
<b>Absences</b>"""
|
)
|
||||||
% params
|
}" class="sidebar">
|
||||||
|
<font color="#FF0000">{etud.civilite_str} {etud.nom_disp()}</font></a>
|
||||||
|
</h2>
|
||||||
|
<b>Absences</b>"""
|
||||||
)
|
)
|
||||||
if etud["cursem"]:
|
inscription = etud.inscription_courante()
|
||||||
cur_sem = etud["cursem"]
|
if inscription:
|
||||||
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, cur_sem)
|
formsemestre = inscription.formsemestre
|
||||||
|
nbabs, nbabsjust = sco_assiduites.formsemestre_get_assiduites_count(
|
||||||
|
etudid, formsemestre
|
||||||
|
)
|
||||||
nbabsnj = nbabs - nbabsjust
|
nbabsnj = nbabs - nbabsjust
|
||||||
H.append(
|
H.append(
|
||||||
f"""<span title="absences du { cur_sem["date_debut"] } au {
|
f"""<span title="absences du {
|
||||||
cur_sem["date_fin"] }">({
|
formsemestre.date_debut.strftime("%d/%m/%Y")
|
||||||
sco_preferences.get_preference("assi_metrique", None)})
|
} au {
|
||||||
|
formsemestre.date_fin.strftime("%d/%m/%Y")
|
||||||
|
}">({
|
||||||
|
sco_preferences.get_preference("assi_metrique", None)})
|
||||||
<br>{ nbabsjust } J., { nbabsnj } N.J.</span>"""
|
<br>{ nbabsjust } J., { nbabsnj } N.J.</span>"""
|
||||||
)
|
)
|
||||||
H.append("<ul>")
|
H.append("<ul>")
|
||||||
@ -189,21 +194,24 @@ def sidebar(etudid: int = None):
|
|||||||
cur_formsemestre_id = retreive_formsemestre_from_request()
|
cur_formsemestre_id = retreive_formsemestre_from_request()
|
||||||
H.append(
|
H.append(
|
||||||
f"""
|
f"""
|
||||||
<li><a href="{ url_for('assiduites.ajout_assiduite_etud',
|
<li><a href="{
|
||||||
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
url_for('assiduites.ajout_assiduite_etud',
|
||||||
}">Ajouter</a></li>
|
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||||
<li><a href="{ url_for('assiduites.ajout_justificatif_etud',
|
}">Ajouter</a></li>
|
||||||
scodoc_dept=g.scodoc_dept, etudid=etudid,
|
<li><a href="{
|
||||||
formsemestre_id=cur_formsemestre_id,
|
url_for('assiduites.ajout_justificatif_etud',
|
||||||
)
|
scodoc_dept=g.scodoc_dept, etudid=etudid,
|
||||||
}">Justifier</a></li>
|
formsemestre_id=cur_formsemestre_id,
|
||||||
|
)
|
||||||
|
}">Justifier</a></li>
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
if sco_preferences.get_preference("handle_billets_abs"):
|
if sco_preferences.get_preference("handle_billets_abs"):
|
||||||
H.append(
|
H.append(
|
||||||
f"""<li><a href="{ url_for('absences.billets_etud',
|
f"""<li><a href="{
|
||||||
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
url_for('absences.billets_etud',
|
||||||
}">Billets</a></li>"""
|
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||||
|
}">Billets</a></li>"""
|
||||||
)
|
)
|
||||||
H.append(
|
H.append(
|
||||||
f"""
|
f"""
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -268,10 +268,12 @@ def abs_notification_message(
|
|||||||
"""
|
"""
|
||||||
from app.scodoc import sco_bulletins
|
from app.scodoc import sco_bulletins
|
||||||
|
|
||||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
etud = Identite.get_etud(etudid)
|
||||||
|
|
||||||
# Variables accessibles dans les balises du template: %(nom_variable)s :
|
# Variables accessibles dans les balises du template: %(nom_variable)s :
|
||||||
values = sco_bulletins.make_context_dict(formsemestre, etud)
|
values = sco_bulletins.make_context_dict(
|
||||||
|
formsemestre, etud.to_dict_scodoc7(with_inscriptions=True)
|
||||||
|
)
|
||||||
|
|
||||||
values["nbabs"] = nbabs
|
values["nbabs"] = nbabs
|
||||||
values["nbabsjust"] = nbabsjust
|
values["nbabsjust"] = nbabsjust
|
||||||
@ -287,7 +289,7 @@ def abs_notification_message(
|
|||||||
log("abs_notification_message: empty template, not sending message")
|
log("abs_notification_message: empty template, not sending message")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
subject = f"""[ScoDoc] Trop d'absences pour {etud["nomprenom"]}"""
|
subject = f"""[ScoDoc] Trop d'absences pour {etud.nomprenom}"""
|
||||||
msg = Message(subject, sender=email.get_from_addr(formsemestre.departement.acronym))
|
msg = Message(subject, sender=email.get_from_addr(formsemestre.departement.acronym))
|
||||||
msg.body = txt
|
msg.body = txt
|
||||||
return msg
|
return msg
|
||||||
|
@ -138,21 +138,18 @@ def etud_upload_file_form(etudid):
|
|||||||
"""Page with a form to choose and upload a file, with a description."""
|
"""Page with a form to choose and upload a file, with a description."""
|
||||||
# check permission
|
# check permission
|
||||||
if not can_edit_etud_archive(current_user):
|
if not can_edit_etud_archive(current_user):
|
||||||
raise AccessDenied("opération non autorisée pour %s" % current_user)
|
raise AccessDenied(f"opération non autorisée pour {current_user}")
|
||||||
etuds = sco_etud.get_etud_info(filled=True)
|
etud = Identite.get_etud(etudid)
|
||||||
if not etuds:
|
|
||||||
raise ScoValueError("étudiant inexistant")
|
|
||||||
etud = etuds[0]
|
|
||||||
H = [
|
H = [
|
||||||
html_sco_header.sco_header(
|
html_sco_header.sco_header(
|
||||||
page_title="Chargement d'un document associé à %(nomprenom)s" % etud,
|
page_title=f"Chargement d'un document associé à {etud.nomprenom}",
|
||||||
),
|
),
|
||||||
"""<h2>Chargement d'un document associé à %(nomprenom)s</h2>
|
f"""<h2>Chargement d'un document associé à {etud.nomprenom}</h2>
|
||||||
"""
|
|
||||||
% etud,
|
<p>Le fichier ne doit pas dépasser {
|
||||||
"""<p>Le fichier ne doit pas dépasser %sMo.</p>
|
scu.CONFIG.ETUD_MAX_FILE_SIZE // (1024 * 1024)}Mo.</p>
|
||||||
"""
|
""",
|
||||||
% (scu.CONFIG.ETUD_MAX_FILE_SIZE // (1024 * 1024)),
|
|
||||||
]
|
]
|
||||||
tf = TrivialFormulator(
|
tf = TrivialFormulator(
|
||||||
request.base_url,
|
request.base_url,
|
||||||
@ -176,20 +173,13 @@ def etud_upload_file_form(etudid):
|
|||||||
if tf[0] == 0:
|
if tf[0] == 0:
|
||||||
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
|
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
|
||||||
elif tf[0] == -1:
|
elif tf[0] == -1:
|
||||||
return flask.redirect(
|
return flask.redirect(etud.url_fiche())
|
||||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
data = tf[2]["datafile"].read()
|
||||||
)
|
descr = tf[2]["description"]
|
||||||
else:
|
filename = tf[2]["datafile"].filename
|
||||||
data = tf[2]["datafile"].read()
|
etud_archive_id = (etudid,)
|
||||||
descr = tf[2]["description"]
|
_store_etud_file_to_new_archive(etud_archive_id, data, filename, description=descr)
|
||||||
filename = tf[2]["datafile"].filename
|
return flask.redirect(etud.url_fiche())
|
||||||
etud_archive_id = etud["etudid"]
|
|
||||||
_store_etud_file_to_new_archive(
|
|
||||||
etud_archive_id, data, filename, description=descr
|
|
||||||
)
|
|
||||||
return flask.redirect(
|
|
||||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _store_etud_file_to_new_archive(
|
def _store_etud_file_to_new_archive(
|
||||||
@ -209,23 +199,20 @@ def etud_delete_archive(etudid, archive_name, dialog_confirmed=False):
|
|||||||
# check permission
|
# check permission
|
||||||
if not can_edit_etud_archive(current_user):
|
if not can_edit_etud_archive(current_user):
|
||||||
raise AccessDenied(f"opération non autorisée pour {current_user}")
|
raise AccessDenied(f"opération non autorisée pour {current_user}")
|
||||||
etuds = sco_etud.get_etud_info(filled=True)
|
etud = Identite.get_etud(etudid)
|
||||||
if not etuds:
|
etud_archive_id = etudid
|
||||||
raise ScoValueError("étudiant inexistant")
|
|
||||||
etud = etuds[0]
|
|
||||||
etud_archive_id = etud["etudid"]
|
|
||||||
archive_id = ETUDS_ARCHIVER.get_id_from_name(
|
archive_id = ETUDS_ARCHIVER.get_id_from_name(
|
||||||
etud_archive_id, archive_name, dept_id=etud["dept_id"]
|
etud_archive_id, archive_name, dept_id=etud.dept_id
|
||||||
)
|
)
|
||||||
if not dialog_confirmed:
|
if not dialog_confirmed:
|
||||||
return scu.confirm_dialog(
|
return scu.confirm_dialog(
|
||||||
"""<h2>Confirmer la suppression des fichiers ?</h2>
|
f"""<h2>Confirmer la suppression des fichiers ?</h2>
|
||||||
<p>Fichier associé le %s à l'étudiant %s</p>
|
<p>Fichier associé le {
|
||||||
<p>La suppression sera définitive.</p>"""
|
ETUDS_ARCHIVER.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M")
|
||||||
% (
|
} à l'étudiant {etud.nomprenom}
|
||||||
ETUDS_ARCHIVER.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M"),
|
</p>
|
||||||
etud["nomprenom"],
|
<p>La suppression sera définitive.</p>
|
||||||
),
|
""",
|
||||||
dest_url="",
|
dest_url="",
|
||||||
cancel_url=url_for(
|
cancel_url=url_for(
|
||||||
"scolar.fiche_etud",
|
"scolar.fiche_etud",
|
||||||
@ -236,22 +223,17 @@ def etud_delete_archive(etudid, archive_name, dialog_confirmed=False):
|
|||||||
parameters={"etudid": etudid, "archive_name": archive_name},
|
parameters={"etudid": etudid, "archive_name": archive_name},
|
||||||
)
|
)
|
||||||
|
|
||||||
ETUDS_ARCHIVER.delete_archive(archive_id, dept_id=etud["dept_id"])
|
ETUDS_ARCHIVER.delete_archive(archive_id, dept_id=etud.dept_id)
|
||||||
flash("Archive supprimée")
|
flash("Archive supprimée")
|
||||||
return flask.redirect(
|
return flask.redirect(etud.url_fiche())
|
||||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def etud_get_archived_file(etudid, archive_name, filename):
|
def etud_get_archived_file(etudid, archive_name, filename):
|
||||||
"""Send file to client."""
|
"""Send file to client."""
|
||||||
etuds = sco_etud.get_etud_info(etudid=etudid, filled=True)
|
etud = Identite.get_etud(etudid)
|
||||||
if not etuds:
|
etud_archive_id = etud.id
|
||||||
raise ScoValueError("étudiant inexistant")
|
|
||||||
etud = etuds[0]
|
|
||||||
etud_archive_id = etud["etudid"]
|
|
||||||
return ETUDS_ARCHIVER.get_archived_file(
|
return ETUDS_ARCHIVER.get_archived_file(
|
||||||
etud_archive_id, archive_name, filename, dept_id=etud["dept_id"]
|
etud_archive_id, archive_name, filename, dept_id=etud.dept_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -735,17 +735,18 @@ def fill_etuds_info(etuds: list[dict], add_admission=True):
|
|||||||
etud["nomlycee"] = etud.get("nomlycee", "") or ""
|
etud["nomlycee"] = etud.get("nomlycee", "") or ""
|
||||||
|
|
||||||
|
|
||||||
|
# voir Identite.inscription_descr et Identite.to_dict_scodoc7(with_inscriptions=True)
|
||||||
def etud_inscriptions_infos(etudid: int, ne="") -> dict:
|
def etud_inscriptions_infos(etudid: int, ne="") -> dict:
|
||||||
"""Dict avec les informations sur les semestres passés et courant.
|
"""Dict avec les informations sur les semestres passés et courant.
|
||||||
{
|
{
|
||||||
"sems" : , # trie les semestres par date de debut, le plus recent d'abord
|
"sems" : , # trie les semestres par date de debut, le plus recent d'abord
|
||||||
"ins" : ,
|
"ins" : ,
|
||||||
"cursem" : ,
|
"cursem" : ,
|
||||||
"inscription" : ,
|
"inscription" : , # cursem["titremois"]
|
||||||
"inscriptionstr" : ,
|
"inscriptionstr" : , # "Inscrit en " + cursem["titremois"]
|
||||||
"inscription_formsemestre_id" : ,
|
"inscription_formsemestre_id" : , # cursem["formsemestre_id"]
|
||||||
"etatincursem" : ,
|
"etatincursem" : , # curi["etat"]
|
||||||
"situation" : ,
|
"situation" : , # descr_situation_etud(etudid, ne)
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
from app.scodoc import sco_formsemestre
|
from app.scodoc import sco_formsemestre
|
||||||
|
@ -162,50 +162,48 @@ def do_evaluation_get_all_notes(
|
|||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
def moduleimpl_evaluation_move(evaluation_id: int, after=0, redirect=1):
|
def moduleimpl_evaluation_move(evaluation_id: int, after=0):
|
||||||
"""Move before/after previous one (decrement/increment numero)
|
"""Move before/after previous one (decrement/increment numero)
|
||||||
(published)
|
(published)
|
||||||
"""
|
"""
|
||||||
evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id)
|
evaluation = Evaluation.get_evaluation(evaluation_id)
|
||||||
redirect = int(redirect)
|
|
||||||
# access: can change eval ?
|
# access: can change eval ?
|
||||||
if not evaluation.moduleimpl.can_edit_evaluation(current_user):
|
if not evaluation.moduleimpl.can_edit_evaluation(current_user):
|
||||||
raise AccessDenied(
|
raise AccessDenied(
|
||||||
f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
|
f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
|
||||||
)
|
)
|
||||||
Evaluation.moduleimpl_evaluation_renumber(
|
|
||||||
evaluation.moduleimpl, only_if_unumbered=True
|
|
||||||
)
|
|
||||||
e = get_evaluations_dict(args={"evaluation_id": evaluation_id})[0]
|
|
||||||
|
|
||||||
after = int(after) # 0: deplace avant, 1 deplace apres
|
after = int(after) # 0: deplace avant, 1 deplace apres
|
||||||
if after not in (0, 1):
|
if after not in (0, 1):
|
||||||
raise ValueError('invalid value for "after"')
|
raise ValueError('invalid value for "after"')
|
||||||
mod_evals = get_evaluations_dict({"moduleimpl_id": e["moduleimpl_id"]})
|
|
||||||
if len(mod_evals) > 1:
|
Evaluation.moduleimpl_evaluation_renumber(
|
||||||
idx = [p["evaluation_id"] for p in mod_evals].index(evaluation_id)
|
evaluation.moduleimpl, only_if_unumbered=True
|
||||||
|
)
|
||||||
|
mod_evaluations = evaluation.moduleimpl.evaluations.all()
|
||||||
|
if len(mod_evaluations) > 1:
|
||||||
|
idx = [e.id for e in mod_evaluations].index(evaluation.id)
|
||||||
neigh = None # object to swap with
|
neigh = None # object to swap with
|
||||||
if after == 0 and idx > 0:
|
if after == 0 and idx > 0:
|
||||||
neigh = mod_evals[idx - 1]
|
neigh = mod_evaluations[idx - 1]
|
||||||
elif after == 1 and idx < len(mod_evals) - 1:
|
elif after == 1 and idx < len(mod_evaluations) - 1:
|
||||||
neigh = mod_evals[idx + 1]
|
neigh = mod_evaluations[idx + 1]
|
||||||
if neigh: #
|
if neigh: #
|
||||||
if neigh["numero"] == e["numero"]:
|
if neigh.numero == evaluation.numero:
|
||||||
log("Warning: moduleimpl_evaluation_move: forcing renumber")
|
log("Warning: moduleimpl_evaluation_move: forcing renumber")
|
||||||
Evaluation.moduleimpl_evaluation_renumber(
|
Evaluation.moduleimpl_evaluation_renumber(
|
||||||
evaluation.moduleimpl, only_if_unumbered=False
|
evaluation.moduleimpl, only_if_unumbered=False
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# swap numero with neighbor
|
# swap numero with neighbor
|
||||||
e["numero"], neigh["numero"] = neigh["numero"], e["numero"]
|
evaluation.numero, neigh.numero = neigh.numero, evaluation.numero
|
||||||
do_evaluation_edit(e)
|
db.session.add(evaluation)
|
||||||
do_evaluation_edit(neigh)
|
db.session.add(neigh)
|
||||||
|
db.session.commit()
|
||||||
# redirect to moduleimpl page:
|
# redirect to moduleimpl page:
|
||||||
if redirect:
|
return flask.redirect(
|
||||||
return flask.redirect(
|
url_for(
|
||||||
url_for(
|
"notes.moduleimpl_status",
|
||||||
"notes.moduleimpl_status",
|
scodoc_dept=g.scodoc_dept,
|
||||||
scodoc_dept=g.scodoc_dept,
|
moduleimpl_id=evaluation.moduleimpl.id,
|
||||||
moduleimpl_id=e["moduleimpl_id"],
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
@ -37,7 +37,7 @@ from flask_login import current_user
|
|||||||
from flask import request
|
from flask import request
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import Evaluation, FormSemestre, ModuleImpl
|
from app.models import Evaluation, Module, ModuleImpl
|
||||||
from app.models.evaluations import heure_to_time
|
from app.models.evaluations import heure_to_time
|
||||||
|
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
@ -47,7 +47,6 @@ from app.scodoc.TrivialFormulator import TrivialFormulator
|
|||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
from app.scodoc import sco_evaluations
|
from app.scodoc import sco_evaluations
|
||||||
from app.scodoc import sco_moduleimpl
|
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
|
|
||||||
|
|
||||||
@ -58,27 +57,20 @@ def evaluation_create_form(
|
|||||||
page_title="Évaluation",
|
page_title="Évaluation",
|
||||||
):
|
):
|
||||||
"Formulaire création/édition d'une évaluation (pas de ses notes)"
|
"Formulaire création/édition d'une évaluation (pas de ses notes)"
|
||||||
|
evaluation: Evaluation
|
||||||
if evaluation_id is not None:
|
if evaluation_id is not None:
|
||||||
evaluation: Evaluation = db.session.get(Evaluation, evaluation_id)
|
evaluation = db.session.get(Evaluation, evaluation_id)
|
||||||
if evaluation is None:
|
if evaluation is None:
|
||||||
raise ScoValueError("Cette évaluation n'existe pas ou plus !")
|
raise ScoValueError("Cette évaluation n'existe pas ou plus !")
|
||||||
moduleimpl_id = evaluation.moduleimpl_id
|
moduleimpl_id = evaluation.moduleimpl_id
|
||||||
#
|
#
|
||||||
modimpl: ModuleImpl = (
|
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
|
||||||
ModuleImpl.query.filter_by(id=moduleimpl_id)
|
formsemestre_id = modimpl.formsemestre_id
|
||||||
.join(FormSemestre)
|
|
||||||
.filter_by(dept_id=g.scodoc_dept_id)
|
|
||||||
.first_or_404()
|
|
||||||
)
|
|
||||||
modimpl_o = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[
|
|
||||||
0
|
|
||||||
]
|
|
||||||
mod = modimpl_o["module"]
|
|
||||||
formsemestre_id = modimpl_o["formsemestre_id"]
|
|
||||||
formsemestre = modimpl.formsemestre
|
formsemestre = modimpl.formsemestre
|
||||||
|
module: Module = modimpl.module
|
||||||
sem_ues = formsemestre.get_ues(with_sport=False)
|
sem_ues = formsemestre.get_ues(with_sport=False)
|
||||||
is_malus = mod["module_type"] == ModuleType.MALUS
|
is_malus = module.module_type == ModuleType.MALUS
|
||||||
is_apc = mod["module_type"] in (ModuleType.RESSOURCE, ModuleType.SAE)
|
is_apc = module.module_type in (ModuleType.RESSOURCE, ModuleType.SAE)
|
||||||
preferences = sco_preferences.SemPreferences(formsemestre.id)
|
preferences = sco_preferences.SemPreferences(formsemestre.id)
|
||||||
can_edit_poids = not preferences["but_disable_edit_poids_evaluations"]
|
can_edit_poids = not preferences["but_disable_edit_poids_evaluations"]
|
||||||
min_note_max = scu.NOTES_PRECISION # le plus petit bareme possible
|
min_note_max = scu.NOTES_PRECISION # le plus petit bareme possible
|
||||||
@ -98,9 +90,11 @@ def evaluation_create_form(
|
|||||||
# création nouvel
|
# création nouvel
|
||||||
if moduleimpl_id is None:
|
if moduleimpl_id is None:
|
||||||
raise ValueError("missing moduleimpl_id parameter")
|
raise ValueError("missing moduleimpl_id parameter")
|
||||||
|
numeros = [(e.numero or 0) for e in modimpl.evaluations]
|
||||||
initvalues = {
|
initvalues = {
|
||||||
"note_max": 20,
|
|
||||||
"jour": time.strftime("%d/%m/%Y", time.localtime()),
|
"jour": time.strftime("%d/%m/%Y", time.localtime()),
|
||||||
|
"note_max": 20,
|
||||||
|
"numero": (max(numeros) + 1) if numeros else 0,
|
||||||
"publish_incomplete": is_malus,
|
"publish_incomplete": is_malus,
|
||||||
"visibulletin": 1,
|
"visibulletin": 1,
|
||||||
}
|
}
|
||||||
@ -128,18 +122,7 @@ def evaluation_create_form(
|
|||||||
min_note_max_str = scu.fmt_note(min_note_max)
|
min_note_max_str = scu.fmt_note(min_note_max)
|
||||||
else:
|
else:
|
||||||
min_note_max_str = "0"
|
min_note_max_str = "0"
|
||||||
#
|
|
||||||
H = [
|
|
||||||
f"""<h3>{action} en
|
|
||||||
{scu.MODULE_TYPE_NAMES[mod["module_type"]]} <a class="stdlink" href="{
|
|
||||||
url_for("notes.moduleimpl_status",
|
|
||||||
scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id)
|
|
||||||
}">{mod["code"] or "module sans code"} {mod["titre"]}</a> {link}</h3>
|
|
||||||
"""
|
|
||||||
]
|
|
||||||
|
|
||||||
heures = [f"{h:02d}h{m:02d}" for h in range(8, 19) for m in (0, 30)]
|
|
||||||
#
|
|
||||||
initvalues["coefficient"] = initvalues.get("coefficient", 1.0)
|
initvalues["coefficient"] = initvalues.get("coefficient", 1.0)
|
||||||
vals = scu.get_request_args()
|
vals = scu.get_request_args()
|
||||||
#
|
#
|
||||||
@ -164,6 +147,7 @@ def evaluation_create_form(
|
|||||||
("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}),
|
("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}),
|
||||||
("formsemestre_id", {"default": formsemestre_id, "input_type": "hidden"}),
|
("formsemestre_id", {"default": formsemestre_id, "input_type": "hidden"}),
|
||||||
("moduleimpl_id", {"default": moduleimpl_id, "input_type": "hidden"}),
|
("moduleimpl_id", {"default": moduleimpl_id, "input_type": "hidden"}),
|
||||||
|
("numero", {"default": initvalues["numero"], "input_type": "hidden"}),
|
||||||
(
|
(
|
||||||
"jour",
|
"jour",
|
||||||
{
|
{
|
||||||
@ -323,6 +307,16 @@ def evaluation_create_form(
|
|||||||
dest_url = url_for(
|
dest_url = url_for(
|
||||||
"notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id
|
"notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id
|
||||||
)
|
)
|
||||||
|
H = [
|
||||||
|
f"""<h3>{action} en
|
||||||
|
{scu.MODULE_TYPE_NAMES[module.module_type]} <a class="stdlink" href="{
|
||||||
|
url_for("notes.moduleimpl_status",
|
||||||
|
scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id)
|
||||||
|
}">{module.code or "module sans code"} {
|
||||||
|
module.titre or module.abbrev or "(sans titre)"
|
||||||
|
}</a> {link}</h3>
|
||||||
|
"""
|
||||||
|
]
|
||||||
if tf[0] == 0:
|
if tf[0] == 0:
|
||||||
head = html_sco_header.sco_header(page_title=page_title)
|
head = html_sco_header.sco_header(page_title=page_title)
|
||||||
return (
|
return (
|
||||||
|
@ -280,18 +280,21 @@ def do_evaluation_etat(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _summarize_evals_etats(evals: list[dict]) -> dict:
|
def _summarize_evals_etats(etat_evals: list[dict]) -> dict:
|
||||||
"""Synthétise les états d'une liste d'évaluations
|
"""Synthétise les états d'une liste d'évaluations
|
||||||
evals: list of mappings (etats),
|
evals: list of mappings (etats),
|
||||||
utilise e["etat"]["evalcomplete"], e["etat"]["nb_notes"], e["etat"]["last_modif"]
|
utilise e["etat"]["evalcomplete"], e["etat"]["nb_notes"], e["etat"]["last_modif"]
|
||||||
-> nb_eval_completes, nb_evals_en_cours,
|
->
|
||||||
nb_evals_vides, date derniere modif
|
nb_eval_completes (= prises en compte)
|
||||||
|
nb_evals_en_cours (= avec des notes, mais pas complete)
|
||||||
|
nb_evals_vides (= sans aucune note)
|
||||||
|
date derniere modif
|
||||||
|
|
||||||
Une eval est "complete" ssi tous les etudiants *inscrits* ont une note.
|
Une eval est "complete" ssi tous les etudiants *inscrits* ont une note.
|
||||||
"""
|
"""
|
||||||
nb_evals_completes, nb_evals_en_cours, nb_evals_vides = 0, 0, 0
|
nb_evals_completes, nb_evals_en_cours, nb_evals_vides = 0, 0, 0
|
||||||
dates = []
|
dates = []
|
||||||
for e in evals:
|
for e in etat_evals:
|
||||||
if e["etat"]["evalcomplete"]:
|
if e["etat"]["evalcomplete"]:
|
||||||
nb_evals_completes += 1
|
nb_evals_completes += 1
|
||||||
elif e["etat"]["nb_notes"] == 0:
|
elif e["etat"]["nb_notes"] == 0:
|
||||||
@ -345,8 +348,8 @@ def do_evaluation_etat_in_sem(formsemestre: FormSemestre) -> dict:
|
|||||||
|
|
||||||
def do_evaluation_etat_in_mod(nt, modimpl: ModuleImpl):
|
def do_evaluation_etat_in_mod(nt, modimpl: ModuleImpl):
|
||||||
"""état des évaluations dans ce module"""
|
"""état des évaluations dans ce module"""
|
||||||
evals = nt.get_mod_evaluation_etat_list(modimpl)
|
etat_evals = nt.get_mod_evaluation_etat_list(modimpl)
|
||||||
etat = _summarize_evals_etats(evals)
|
etat = _summarize_evals_etats(etat_evals)
|
||||||
# Il y a-t-il des notes en attente dans ce module ?
|
# Il y a-t-il des notes en attente dans ce module ?
|
||||||
etat["attente"] = nt.modimpls_results[modimpl.id].en_attente
|
etat["attente"] = nt.modimpls_results[modimpl.id].en_attente
|
||||||
return etat
|
return etat
|
||||||
|
@ -241,7 +241,7 @@ def do_formsemestre_create(args, silent=False):
|
|||||||
write_formsemestre_etapes(args)
|
write_formsemestre_etapes(args)
|
||||||
if args["responsables"]:
|
if args["responsables"]:
|
||||||
args["formsemestre_id"] = formsemestre_id
|
args["formsemestre_id"] = formsemestre_id
|
||||||
write_formsemestre_responsables(args)
|
_write_formsemestre_responsables(args)
|
||||||
|
|
||||||
# create default partition
|
# create default partition
|
||||||
partition_id = sco_groups.partition_create(
|
partition_id = sco_groups.partition_create(
|
||||||
@ -275,7 +275,7 @@ def do_formsemestre_edit(sem, cnx=None, **kw):
|
|||||||
|
|
||||||
_formsemestreEditor.edit(cnx, sem, **kw)
|
_formsemestreEditor.edit(cnx, sem, **kw)
|
||||||
write_formsemestre_etapes(sem)
|
write_formsemestre_etapes(sem)
|
||||||
write_formsemestre_responsables(sem)
|
_write_formsemestre_responsables(sem)
|
||||||
|
|
||||||
sco_cache.invalidate_formsemestre(
|
sco_cache.invalidate_formsemestre(
|
||||||
formsemestre_id=sem["formsemestre_id"]
|
formsemestre_id=sem["formsemestre_id"]
|
||||||
@ -296,8 +296,12 @@ def read_formsemestre_responsables(formsemestre_id: int) -> list[int]: # py3.9+
|
|||||||
return [x["responsable_id"] for x in r]
|
return [x["responsable_id"] for x in r]
|
||||||
|
|
||||||
|
|
||||||
def write_formsemestre_responsables(sem):
|
def _write_formsemestre_responsables(sem): # TODO old, à ré-écrire avec models
|
||||||
return _write_formsemestre_aux(sem, "responsables", "responsable_id")
|
if sem and "responsables" in sem:
|
||||||
|
sem["responsables"] = [
|
||||||
|
uid for uid in sem["responsables"] if (uid is not None) and (uid != -1)
|
||||||
|
]
|
||||||
|
_write_formsemestre_aux(sem, "responsables", "responsable_id")
|
||||||
|
|
||||||
|
|
||||||
# ---------------------- Coefs des UE
|
# ---------------------- Coefs des UE
|
||||||
@ -362,10 +366,11 @@ def read_formsemestre_etapes(formsemestre_id): # OBSOLETE
|
|||||||
return [ApoEtapeVDI(x["etape_apo"]) for x in r if x["etape_apo"]]
|
return [ApoEtapeVDI(x["etape_apo"]) for x in r if x["etape_apo"]]
|
||||||
|
|
||||||
|
|
||||||
def write_formsemestre_etapes(sem):
|
def write_formsemestre_etapes(sem): # TODO old, à ré-écrire avec models
|
||||||
return _write_formsemestre_aux(sem, "etapes", "etape_apo")
|
return _write_formsemestre_aux(sem, "etapes", "etape_apo")
|
||||||
|
|
||||||
|
|
||||||
|
# TODO old, à ré-écrire avec models
|
||||||
def _write_formsemestre_aux(sem, fieldname, valuename):
|
def _write_formsemestre_aux(sem, fieldname, valuename):
|
||||||
"""fieldname: 'etapes' ou 'responsables'
|
"""fieldname: 'etapes' ou 'responsables'
|
||||||
valuename: 'etape_apo' ou 'responsable_id'
|
valuename: 'etape_apo' ou 'responsable_id'
|
||||||
|
@ -857,17 +857,20 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
|
|||||||
tf[2]["bul_hide_xml"] = True
|
tf[2]["bul_hide_xml"] = True
|
||||||
# remap les identifiants de responsables:
|
# remap les identifiants de responsables:
|
||||||
for field in resp_fields:
|
for field in resp_fields:
|
||||||
tf[2][field] = User.get_user_id_from_nomplogin(tf[2][field])
|
resp = User.get_user_from_nomplogin(tf[2][field])
|
||||||
|
tf[2][field] = resp.id if resp else -1
|
||||||
tf[2]["responsables"] = []
|
tf[2]["responsables"] = []
|
||||||
for field in resp_fields:
|
for field in resp_fields:
|
||||||
if tf[2][field]:
|
if tf[2][field]:
|
||||||
tf[2]["responsables"].append(tf[2][field])
|
tf[2]["responsables"].append(tf[2][field])
|
||||||
for module_id in tf[2]["tf-checked"]:
|
for module_id in tf[2]["tf-checked"]:
|
||||||
mod_resp_id = User.get_user_id_from_nomplogin(tf[2][module_id])
|
mod_resp = User.get_user_from_nomplogin(tf[2][module_id])
|
||||||
if mod_resp_id is None:
|
if mod_resp is None:
|
||||||
# Si un module n'a pas de responsable (ou inconnu),
|
# Si un module n'a pas de responsable (ou inconnu),
|
||||||
# l'affecte au 1er directeur des etudes:
|
# l'affecte au 1er directeur des etudes:
|
||||||
mod_resp_id = tf[2]["responsable_id"]
|
mod_resp_id = tf[2]["responsable_id"]
|
||||||
|
else:
|
||||||
|
mod_resp_id = mod_resp.id
|
||||||
tf[2][module_id] = mod_resp_id
|
tf[2][module_id] = mod_resp_id
|
||||||
|
|
||||||
# etapes:
|
# etapes:
|
||||||
@ -1227,9 +1230,12 @@ def formsemestre_clone(formsemestre_id):
|
|||||||
"formsemestre_status?formsemestre_id=%s" % formsemestre_id
|
"formsemestre_status?formsemestre_id=%s" % formsemestre_id
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
resp = User.get_user_from_nomplogin(tf[2]["responsable_id"])
|
||||||
|
if not resp:
|
||||||
|
raise ScoValueError("id responsable invalide")
|
||||||
new_formsemestre_id = do_formsemestre_clone(
|
new_formsemestre_id = do_formsemestre_clone(
|
||||||
formsemestre_id,
|
formsemestre_id,
|
||||||
User.get_user_id_from_nomplogin(tf[2]["responsable_id"]),
|
resp.id,
|
||||||
tf[2]["date_debut"],
|
tf[2]["date_debut"],
|
||||||
tf[2]["date_fin"],
|
tf[2]["date_fin"],
|
||||||
clone_evaluations=tf[2]["clone_evaluations"],
|
clone_evaluations=tf[2]["clone_evaluations"],
|
||||||
@ -1273,29 +1279,24 @@ def do_formsemestre_clone(
|
|||||||
log(f"created formsemestre {formsemestre_id}")
|
log(f"created formsemestre {formsemestre_id}")
|
||||||
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
|
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
|
||||||
# 2- create moduleimpls
|
# 2- create moduleimpls
|
||||||
mods_orig = sco_moduleimpl.moduleimpl_list(formsemestre_id=orig_formsemestre_id)
|
modimpl_orig: ModuleImpl
|
||||||
for mod_orig in mods_orig:
|
for modimpl_orig in formsemestre_orig.modimpls:
|
||||||
args = mod_orig.copy()
|
args = modimpl_orig.to_dict(with_module=False)
|
||||||
args["formsemestre_id"] = formsemestre_id
|
args["formsemestre_id"] = formsemestre_id
|
||||||
mid = sco_moduleimpl.do_moduleimpl_create(args)
|
modimpl_new = ModuleImpl.create_from_dict(args)
|
||||||
# copy notes_modules_enseignants
|
db.session.flush()
|
||||||
ens = sco_moduleimpl.do_ens_list(
|
# copy enseignants
|
||||||
args={"moduleimpl_id": mod_orig["moduleimpl_id"]}
|
for ens in modimpl_orig.enseignants:
|
||||||
)
|
modimpl_new.enseignants.append(ens)
|
||||||
for e in ens:
|
db.session.add(modimpl_new)
|
||||||
args = e.copy()
|
|
||||||
args["moduleimpl_id"] = mid
|
|
||||||
sco_moduleimpl.do_ens_create(args)
|
|
||||||
# optionally, copy evaluations
|
# optionally, copy evaluations
|
||||||
if clone_evaluations:
|
if clone_evaluations:
|
||||||
for e in Evaluation.query.filter_by(
|
for e in Evaluation.query.filter_by(moduleimpl_id=modimpl_orig.id):
|
||||||
moduleimpl_id=mod_orig["moduleimpl_id"]
|
|
||||||
):
|
|
||||||
# copie en enlevant la date
|
# copie en enlevant la date
|
||||||
new_eval = e.clone(
|
new_eval = e.clone(
|
||||||
not_copying=("date_debut", "date_fin", "moduleimpl_id")
|
not_copying=("date_debut", "date_fin", "moduleimpl_id")
|
||||||
)
|
)
|
||||||
new_eval.moduleimpl_id = mid
|
new_eval.moduleimpl_id = modimpl_new.id
|
||||||
# Copie les poids APC de l'évaluation
|
# Copie les poids APC de l'évaluation
|
||||||
new_eval.set_ue_poids_dict(e.get_ue_poids_dict())
|
new_eval.set_ue_poids_dict(e.get_ue_poids_dict())
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -1262,6 +1262,7 @@ def formsemestre_tableau_modules(
|
|||||||
etat["nb_evals_completes"] > 0
|
etat["nb_evals_completes"] > 0
|
||||||
and etat["nb_evals_en_cours"] == 0
|
and etat["nb_evals_en_cours"] == 0
|
||||||
and etat["nb_evals_vides"] == 0
|
and etat["nb_evals_vides"] == 0
|
||||||
|
and not etat["attente"]
|
||||||
):
|
):
|
||||||
H.append(f'<tr class="formsemestre_status_green{fontorange}">')
|
H.append(f'<tr class="formsemestre_status_green{fontorange}">')
|
||||||
else:
|
else:
|
||||||
@ -1315,6 +1316,7 @@ def formsemestre_tableau_modules(
|
|||||||
if nb_evals != 0:
|
if nb_evals != 0:
|
||||||
H.append(
|
H.append(
|
||||||
f"""<a href="{moduleimpl_status_url}"
|
f"""<a href="{moduleimpl_status_url}"
|
||||||
|
title="les évaluations 'ok' sont celles prises en compte dans les calculs"
|
||||||
class="formsemestre_status_link">{nb_evals} prévues,
|
class="formsemestre_status_link">{nb_evals} prévues,
|
||||||
{etat["nb_evals_completes"]} ok</a>"""
|
{etat["nb_evals_completes"]} ok</a>"""
|
||||||
)
|
)
|
||||||
@ -1325,11 +1327,11 @@ def formsemestre_tableau_modules(
|
|||||||
etat["nb_evals_en_cours"]
|
etat["nb_evals_en_cours"]
|
||||||
} en cours</a></span>"""
|
} en cours</a></span>"""
|
||||||
)
|
)
|
||||||
if etat["attente"]:
|
if etat["attente"]:
|
||||||
H.append(
|
H.append(
|
||||||
f""" <span><a class="redlink" href="{moduleimpl_status_url}"
|
f""" <span><a class="redlink" href="{moduleimpl_status_url}"
|
||||||
title="Il y a des notes en attente">[en attente]</a></span>"""
|
title="Il y a des notes en attente">[en attente]</a></span>"""
|
||||||
)
|
)
|
||||||
elif mod.module_type == ModuleType.MALUS:
|
elif mod.module_type == ModuleType.MALUS:
|
||||||
nb_malus_notes = sum(
|
nb_malus_notes = sum(
|
||||||
e["etat"]["nb_notes"] for e in nt.get_mod_evaluation_etat_list(modimpl)
|
e["etat"]["nb_notes"] for e in nt.get_mod_evaluation_etat_list(modimpl)
|
||||||
|
@ -25,22 +25,18 @@
|
|||||||
#
|
#
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
"""Fonctions sur les moduleimpl
|
"""Fonctions sur les moduleimpl (legacy: use models.moduleimpls instead)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from flask_login import current_user
|
|
||||||
import psycopg2
|
import psycopg2
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
|
|
||||||
from app.models import Formation
|
from app.models import Formation
|
||||||
from app.scodoc import scolog
|
from app.scodoc import scolog
|
||||||
from app.scodoc import sco_formsemestre
|
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
import app.scodoc.sco_utils as scu
|
|
||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
from app.scodoc.sco_exceptions import ScoValueError, AccessDenied
|
|
||||||
|
|
||||||
# --- Gestion des "Implémentations de Modules"
|
# --- Gestion des "Implémentations de Modules"
|
||||||
# Un "moduleimpl" correspond a la mise en oeuvre d'un module
|
# Un "moduleimpl" correspond a la mise en oeuvre d'un module
|
||||||
@ -57,15 +53,6 @@ _moduleimplEditor = ndb.EditableTable(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
_modules_enseignantsEditor = ndb.EditableTable(
|
|
||||||
"notes_modules_enseignants",
|
|
||||||
None, # pas d'id dans cette Table d'association
|
|
||||||
(
|
|
||||||
"moduleimpl_id", # associe moduleimpl
|
|
||||||
"ens_id", # a l'id de l'enseignant (User.id)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def do_moduleimpl_create(args):
|
def do_moduleimpl_create(args):
|
||||||
"create a moduleimpl"
|
"create a moduleimpl"
|
||||||
@ -108,9 +95,6 @@ def moduleimpl_list(moduleimpl_id=None, formsemestre_id=None, module_id=None):
|
|||||||
args = locals()
|
args = locals()
|
||||||
cnx = ndb.GetDBConnexion()
|
cnx = ndb.GetDBConnexion()
|
||||||
modimpls = _moduleimplEditor.list(cnx, args)
|
modimpls = _moduleimplEditor.list(cnx, args)
|
||||||
# Ajoute la liste des enseignants
|
|
||||||
for mo in modimpls:
|
|
||||||
mo["ens"] = do_ens_list(args={"moduleimpl_id": mo["moduleimpl_id"]})
|
|
||||||
return modimpls
|
return modimpls
|
||||||
|
|
||||||
|
|
||||||
@ -342,65 +326,3 @@ def do_moduleimpl_inscrit_etuds(moduleimpl_id, formsemestre_id, etudids, reset=F
|
|||||||
sco_cache.invalidate_formsemestre(
|
sco_cache.invalidate_formsemestre(
|
||||||
formsemestre_id=formsemestre_id
|
formsemestre_id=formsemestre_id
|
||||||
) # > moduleimpl_inscrit_etuds
|
) # > moduleimpl_inscrit_etuds
|
||||||
|
|
||||||
|
|
||||||
def do_ens_list(*args, **kw):
|
|
||||||
"liste les enseignants d'un moduleimpl (pas le responsable)"
|
|
||||||
cnx = ndb.GetDBConnexion()
|
|
||||||
ens = _modules_enseignantsEditor.list(cnx, *args, **kw)
|
|
||||||
return ens
|
|
||||||
|
|
||||||
|
|
||||||
def do_ens_edit(*args, **kw):
|
|
||||||
"edit ens"
|
|
||||||
cnx = ndb.GetDBConnexion()
|
|
||||||
_modules_enseignantsEditor.edit(cnx, *args, **kw)
|
|
||||||
|
|
||||||
|
|
||||||
def do_ens_create(args):
|
|
||||||
"create ens"
|
|
||||||
cnx = ndb.GetDBConnexion()
|
|
||||||
r = _modules_enseignantsEditor.create(cnx, args)
|
|
||||||
return r
|
|
||||||
|
|
||||||
|
|
||||||
def can_change_module_resp(moduleimpl_id):
|
|
||||||
"""Check if current user can modify module resp. (raise exception if not).
|
|
||||||
= Admin, et dir des etud. (si option l'y autorise)
|
|
||||||
"""
|
|
||||||
M = moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[0]
|
|
||||||
# -- check lock
|
|
||||||
sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
|
|
||||||
if not sem["etat"]:
|
|
||||||
raise ScoValueError("Modification impossible: semestre verrouille")
|
|
||||||
# -- check access
|
|
||||||
# admin ou resp. semestre avec flag resp_can_change_resp
|
|
||||||
if not current_user.has_permission(Permission.EditFormSemestre) and (
|
|
||||||
(current_user.id not in sem["responsables"]) or (not sem["resp_can_change_ens"])
|
|
||||||
):
|
|
||||||
raise AccessDenied(f"Modification impossible pour {current_user}")
|
|
||||||
return M, sem
|
|
||||||
|
|
||||||
|
|
||||||
def can_change_ens(moduleimpl_id, raise_exc=True):
|
|
||||||
"check if current user can modify ens list (raise exception if not)"
|
|
||||||
M = moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[0]
|
|
||||||
# -- check lock
|
|
||||||
sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
|
|
||||||
if not sem["etat"]:
|
|
||||||
if raise_exc:
|
|
||||||
raise ScoValueError("Modification impossible: semestre verrouille")
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
# -- check access
|
|
||||||
# admin, resp. module ou resp. semestre
|
|
||||||
if (
|
|
||||||
current_user.id != M["responsable_id"]
|
|
||||||
and not current_user.has_permission(Permission.EditFormSemestre)
|
|
||||||
and (current_user.id not in sem["responsables"])
|
|
||||||
):
|
|
||||||
if raise_exc:
|
|
||||||
raise AccessDenied("Modification impossible pour %s" % current_user)
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
return M, sem
|
|
||||||
|
@ -209,15 +209,10 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||||||
#
|
#
|
||||||
sem_locked = not formsemestre.etat
|
sem_locked = not formsemestre.etat
|
||||||
can_edit_evals = (
|
can_edit_evals = (
|
||||||
sco_permissions_check.can_edit_notes(
|
modimpl.can_edit_notes(current_user, allow_ens=formsemestre.ens_can_edit_eval)
|
||||||
current_user, moduleimpl_id, allow_ens=formsemestre.ens_can_edit_eval
|
|
||||||
)
|
|
||||||
and not sem_locked
|
|
||||||
)
|
|
||||||
can_edit_notes = (
|
|
||||||
sco_permissions_check.can_edit_notes(current_user, moduleimpl_id)
|
|
||||||
and not sem_locked
|
and not sem_locked
|
||||||
)
|
)
|
||||||
|
can_edit_notes = modimpl.can_edit_notes(current_user) and not sem_locked
|
||||||
arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags()
|
arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags()
|
||||||
#
|
#
|
||||||
module_resp = db.session.get(User, modimpl.responsable_id)
|
module_resp = db.session.get(User, modimpl.responsable_id)
|
||||||
@ -244,7 +239,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||||||
<span class="blacktt">({module_resp.user_name})</span>
|
<span class="blacktt">({module_resp.user_name})</span>
|
||||||
""",
|
""",
|
||||||
]
|
]
|
||||||
if modimpl.can_change_ens_by(current_user):
|
if modimpl.can_change_responsable(current_user):
|
||||||
H.append(
|
H.append(
|
||||||
f"""<a class="stdlink" href="{url_for("notes.edit_moduleimpl_resp",
|
f"""<a class="stdlink" href="{url_for("notes.edit_moduleimpl_resp",
|
||||||
scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id)
|
scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id)
|
||||||
@ -253,14 +248,15 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||||||
H.append("""</td><td>""")
|
H.append("""</td><td>""")
|
||||||
H.append(", ".join([u.get_nomprenom() for u in modimpl.enseignants]))
|
H.append(", ".join([u.get_nomprenom() for u in modimpl.enseignants]))
|
||||||
H.append("""</td><td>""")
|
H.append("""</td><td>""")
|
||||||
try:
|
if modimpl.can_change_ens(raise_exc=False):
|
||||||
sco_moduleimpl.can_change_ens(moduleimpl_id)
|
|
||||||
H.append(
|
H.append(
|
||||||
"""<a class="stdlink" href="edit_enseignants_form?moduleimpl_id=%s">modifier les enseignants</a>"""
|
f"""<a class="stdlink" href="{
|
||||||
% moduleimpl_id
|
url_for("notes.edit_enseignants_form",
|
||||||
|
scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id
|
||||||
|
)
|
||||||
|
}">modifier les enseignants</a>"""
|
||||||
)
|
)
|
||||||
except:
|
|
||||||
pass
|
|
||||||
H.append("""</td></tr>""")
|
H.append("""</td></tr>""")
|
||||||
|
|
||||||
# 2ieme ligne: Semestre, Coef
|
# 2ieme ligne: Semestre, Coef
|
||||||
|
@ -8,50 +8,11 @@ from flask_login import current_user
|
|||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.auth.models import User
|
from app.auth.models import User
|
||||||
from app.models import FormSemestre
|
|
||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
from app.scodoc import sco_etud
|
from app.scodoc import sco_etud
|
||||||
from app.scodoc import sco_exceptions
|
from app.scodoc import sco_exceptions
|
||||||
from app.scodoc import sco_moduleimpl
|
|
||||||
|
|
||||||
|
|
||||||
def can_edit_notes(authuser, moduleimpl_id, allow_ens=True):
|
|
||||||
"""True if authuser can enter or edit notes in this module.
|
|
||||||
If allow_ens, grant access to all ens in this module
|
|
||||||
|
|
||||||
Si des décisions de jury ont déjà été saisies dans ce semestre,
|
|
||||||
seul le directeur des études peut saisir des notes (et il ne devrait pas).
|
|
||||||
"""
|
|
||||||
from app.scodoc import sco_formsemestre
|
|
||||||
from app.scodoc import sco_cursus_dut
|
|
||||||
|
|
||||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
|
||||||
sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
|
|
||||||
if not sem["etat"]:
|
|
||||||
return False # semestre verrouillé
|
|
||||||
|
|
||||||
if sco_cursus_dut.formsemestre_has_decisions(sem["formsemestre_id"]):
|
|
||||||
# il y a des décisions de jury dans ce semestre !
|
|
||||||
return (
|
|
||||||
authuser.has_permission(Permission.EditAllNotes)
|
|
||||||
or authuser.id in sem["responsables"]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if (
|
|
||||||
(not authuser.has_permission(Permission.EditAllNotes))
|
|
||||||
and authuser.id != M["responsable_id"]
|
|
||||||
and authuser.id not in sem["responsables"]
|
|
||||||
):
|
|
||||||
# enseignant (chargé de TD) ?
|
|
||||||
if allow_ens:
|
|
||||||
for ens in M["ens"]:
|
|
||||||
if ens["ens_id"] == authuser.id:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def can_suppress_annotation(annotation_id):
|
def can_suppress_annotation(annotation_id):
|
||||||
|
@ -48,6 +48,7 @@ from wtforms import (
|
|||||||
HiddenField,
|
HiddenField,
|
||||||
SelectMultipleField,
|
SelectMultipleField,
|
||||||
)
|
)
|
||||||
|
from app.models import ModuleImpl
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
from app import ScoValueError
|
from app import ScoValueError
|
||||||
@ -58,7 +59,6 @@ from app.scodoc import sco_evaluation_db
|
|||||||
from app.scodoc import sco_excel
|
from app.scodoc import sco_excel
|
||||||
from app.scodoc.sco_excel import ScoExcelBook, COLORS
|
from app.scodoc.sco_excel import ScoExcelBook, COLORS
|
||||||
from app.scodoc import sco_formsemestre
|
from app.scodoc import sco_formsemestre
|
||||||
from app.scodoc import sco_formsemestre_inscriptions
|
|
||||||
from app.scodoc import sco_groups
|
from app.scodoc import sco_groups
|
||||||
from app.scodoc import sco_moduleimpl
|
from app.scodoc import sco_moduleimpl
|
||||||
from app.scodoc import sco_permissions_check
|
from app.scodoc import sco_permissions_check
|
||||||
@ -247,6 +247,8 @@ class PlacementRunner:
|
|||||||
# gr_title = sco_groups.listgroups_abbrev(d['groups'])
|
# gr_title = sco_groups.listgroups_abbrev(d['groups'])
|
||||||
self.current_user = current_user
|
self.current_user = current_user
|
||||||
self.moduleimpl_id = self.eval_data["moduleimpl_id"]
|
self.moduleimpl_id = self.eval_data["moduleimpl_id"]
|
||||||
|
self.moduleimpl: ModuleImpl = ModuleImpl.query.get_or_404(self.moduleimpl_id)
|
||||||
|
# TODO: à revoir pour utiliser modèle ModuleImpl
|
||||||
self.moduleimpl_data = sco_moduleimpl.moduleimpl_list(
|
self.moduleimpl_data = sco_moduleimpl.moduleimpl_list(
|
||||||
moduleimpl_id=self.moduleimpl_id
|
moduleimpl_id=self.moduleimpl_id
|
||||||
)[0]
|
)[0]
|
||||||
@ -280,9 +282,7 @@ class PlacementRunner:
|
|||||||
def check_placement(self):
|
def check_placement(self):
|
||||||
"""Vérifie que l'utilisateur courant a le droit d'édition sur les notes"""
|
"""Vérifie que l'utilisateur courant a le droit d'édition sur les notes"""
|
||||||
# Check access (admin, respformation, and responsable_id)
|
# Check access (admin, respformation, and responsable_id)
|
||||||
return sco_permissions_check.can_edit_notes(
|
return self.moduleimpl.can_edit_notes(self.current_user)
|
||||||
self.current_user, self.moduleimpl_id
|
|
||||||
)
|
|
||||||
|
|
||||||
def exec_placement(self):
|
def exec_placement(self):
|
||||||
"""Excéute l'action liée au formulaire"""
|
"""Excéute l'action liée au formulaire"""
|
||||||
|
@ -198,8 +198,8 @@ def do_evaluation_upload_xls():
|
|||||||
evaluation_id = int(vals["evaluation_id"])
|
evaluation_id = int(vals["evaluation_id"])
|
||||||
comment = vals["comment"]
|
comment = vals["comment"]
|
||||||
evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id)
|
evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id)
|
||||||
# Check access (admin, respformation, and responsable_id)
|
# Check access (admin, respformation, responsable_id, ens)
|
||||||
if not sco_permissions_check.can_edit_notes(current_user, evaluation.moduleimpl_id):
|
if not evaluation.moduleimpl.can_edit_notes(current_user):
|
||||||
raise AccessDenied(f"Modification des notes impossible pour {current_user}")
|
raise AccessDenied(f"Modification des notes impossible pour {current_user}")
|
||||||
#
|
#
|
||||||
diag, lines = sco_excel.excel_file_to_list(vals["notefile"])
|
diag, lines = sco_excel.excel_file_to_list(vals["notefile"])
|
||||||
@ -315,7 +315,7 @@ def do_evaluation_set_etud_note(evaluation: Evaluation, etud: Identite, value) -
|
|||||||
"""Enregistre la note d'un seul étudiant
|
"""Enregistre la note d'un seul étudiant
|
||||||
value: valeur externe (float ou str)
|
value: valeur externe (float ou str)
|
||||||
"""
|
"""
|
||||||
if not sco_permissions_check.can_edit_notes(current_user, evaluation.moduleimpl.id):
|
if not evaluation.moduleimpl.can_edit_notes(current_user):
|
||||||
raise AccessDenied(f"Modification des notes impossible pour {current_user}")
|
raise AccessDenied(f"Modification des notes impossible pour {current_user}")
|
||||||
# Convert and check value
|
# Convert and check value
|
||||||
L, invalids, _, _, _ = _check_notes([(etud.id, value)], evaluation)
|
L, invalids, _, _, _ = _check_notes([(etud.id, value)], evaluation)
|
||||||
@ -336,7 +336,7 @@ def do_evaluation_set_missing(
|
|||||||
modimpl = evaluation.moduleimpl
|
modimpl = evaluation.moduleimpl
|
||||||
# Check access
|
# Check access
|
||||||
# (admin, respformation, and responsable_id)
|
# (admin, respformation, and responsable_id)
|
||||||
if not sco_permissions_check.can_edit_notes(current_user, modimpl.id):
|
if not modimpl.can_edit_notes(current_user):
|
||||||
raise AccessDenied(f"Modification des notes impossible pour {current_user}")
|
raise AccessDenied(f"Modification des notes impossible pour {current_user}")
|
||||||
#
|
#
|
||||||
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
|
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
|
||||||
@ -433,15 +433,11 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
|
|||||||
"suppress all notes in this eval"
|
"suppress all notes in this eval"
|
||||||
evaluation = Evaluation.query.get_or_404(evaluation_id)
|
evaluation = Evaluation.query.get_or_404(evaluation_id)
|
||||||
|
|
||||||
if sco_permissions_check.can_edit_notes(
|
if evaluation.moduleimpl.can_edit_notes(current_user, allow_ens=False):
|
||||||
current_user, evaluation.moduleimpl_id, allow_ens=False
|
|
||||||
):
|
|
||||||
# On a le droit de modifier toutes les notes
|
# On a le droit de modifier toutes les notes
|
||||||
# recupere les etuds ayant une note
|
# recupere les etuds ayant une note
|
||||||
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
|
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
|
||||||
elif sco_permissions_check.can_edit_notes(
|
elif evaluation.moduleimpl.can_edit_notes(current_user, allow_ens=True):
|
||||||
current_user, evaluation.moduleimpl_id, allow_ens=True
|
|
||||||
):
|
|
||||||
# Enseignant associé au module: ne peut supprimer que les notes qu'il a saisi
|
# Enseignant associé au module: ne peut supprimer que les notes qu'il a saisi
|
||||||
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(
|
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(
|
||||||
evaluation_id, by_uid=current_user.id
|
evaluation_id, by_uid=current_user.id
|
||||||
@ -682,7 +678,7 @@ def saisie_notes_tableur(evaluation_id, group_ids=()):
|
|||||||
evaluation = Evaluation.query.get_or_404(evaluation_id)
|
evaluation = Evaluation.query.get_or_404(evaluation_id)
|
||||||
moduleimpl_id = evaluation.moduleimpl.id
|
moduleimpl_id = evaluation.moduleimpl.id
|
||||||
formsemestre_id = evaluation.moduleimpl.formsemestre_id
|
formsemestre_id = evaluation.moduleimpl.formsemestre_id
|
||||||
if not sco_permissions_check.can_edit_notes(current_user, moduleimpl_id):
|
if not evaluation.moduleimpl.can_edit_notes(current_user):
|
||||||
return (
|
return (
|
||||||
html_sco_header.sco_header()
|
html_sco_header.sco_header()
|
||||||
+ f"""
|
+ f"""
|
||||||
@ -813,9 +809,7 @@ def saisie_notes_tableur(evaluation_id, group_ids=()):
|
|||||||
|
|
||||||
#
|
#
|
||||||
H.append("""</div><h3>Autres opérations</h3><ul>""")
|
H.append("""</div><h3>Autres opérations</h3><ul>""")
|
||||||
if sco_permissions_check.can_edit_notes(
|
if evaluation.moduleimpl.can_edit_notes(current_user, allow_ens=False):
|
||||||
current_user, moduleimpl_id, allow_ens=False
|
|
||||||
):
|
|
||||||
H.append(
|
H.append(
|
||||||
f"""
|
f"""
|
||||||
<li>
|
<li>
|
||||||
@ -967,7 +961,7 @@ def saisie_notes(evaluation_id: int, group_ids: list = None):
|
|||||||
)
|
)
|
||||||
# Check access
|
# Check access
|
||||||
# (admin, respformation, and responsable_id)
|
# (admin, respformation, and responsable_id)
|
||||||
if not sco_permissions_check.can_edit_notes(current_user, evaluation.moduleimpl_id):
|
if not evaluation.moduleimpl.can_edit_notes(current_user):
|
||||||
return f"""
|
return f"""
|
||||||
{html_sco_header.sco_header()}
|
{html_sco_header.sco_header()}
|
||||||
<h2>Modification des notes impossible pour {current_user.user_name}</h2>
|
<h2>Modification des notes impossible pour {current_user.user_name}</h2>
|
||||||
@ -1361,7 +1355,7 @@ def save_notes(
|
|||||||
_external=True,
|
_external=True,
|
||||||
)
|
)
|
||||||
# Check access: admin, respformation, or responsable_id
|
# Check access: admin, respformation, or responsable_id
|
||||||
if not sco_permissions_check.can_edit_notes(current_user, evaluation.moduleimpl_id):
|
if not evaluation.moduleimpl.can_edit_notes(current_user):
|
||||||
return json_error(403, "modification notes non autorisee pour cet utilisateur")
|
return json_error(403, "modification notes non autorisee pour cet utilisateur")
|
||||||
#
|
#
|
||||||
valid_notes, _, _, _, _ = _check_notes(notes, evaluation)
|
valid_notes, _, _, _, _ = _check_notes(notes, evaluation)
|
||||||
|
@ -1834,9 +1834,9 @@ def signale_evaluation_abs(etudid: int = None, evaluation_id: int = None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Sinon on créé l'assiduité
|
# Sinon on créé l'assiduité
|
||||||
|
assiduite_unique: Assiduite | None = None
|
||||||
try:
|
try:
|
||||||
assiduite_unique: Assiduite = Assiduite.create_assiduite(
|
assiduite_unique = Assiduite.create_assiduite(
|
||||||
etud=etud,
|
etud=etud,
|
||||||
date_debut=scu.localize_datetime(evaluation.date_debut),
|
date_debut=scu.localize_datetime(evaluation.date_debut),
|
||||||
date_fin=scu.localize_datetime(evaluation.date_fin),
|
date_fin=scu.localize_datetime(evaluation.date_fin),
|
||||||
@ -1862,9 +1862,9 @@ def signale_evaluation_abs(etudid: int = None, evaluation_id: int = None):
|
|||||||
duplication="oui",
|
duplication="oui",
|
||||||
)
|
)
|
||||||
raise ScoValueError(msg, dest) from exc
|
raise ScoValueError(msg, dest) from exc
|
||||||
|
if assiduite_unique is not None:
|
||||||
db.session.add(assiduite_unique)
|
db.session.add(assiduite_unique)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# on flash puis on revient sur la page de l'évaluation
|
# on flash puis on revient sur la page de l'évaluation
|
||||||
flash("L'absence a bien été créée")
|
flash("L'absence a bien été créée")
|
||||||
|
@ -539,9 +539,9 @@ def ue_sharing_code():
|
|||||||
return sco_edit_ue.ue_sharing_code(
|
return sco_edit_ue.ue_sharing_code(
|
||||||
ue_code=ue_code,
|
ue_code=ue_code,
|
||||||
ue_id=None if ((ue_id is None) or ue_id == "") else int(ue_id),
|
ue_id=None if ((ue_id is None) or ue_id == "") else int(ue_id),
|
||||||
hide_ue_id=None
|
hide_ue_id=(
|
||||||
if ((hide_ue_id is None) or hide_ue_id == "")
|
None if ((hide_ue_id is None) or hide_ue_id == "") else int(hide_ue_id)
|
||||||
else int(hide_ue_id),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -961,12 +961,15 @@ def formsemestre_custommenu_edit(formsemestre_id):
|
|||||||
@scodoc7func
|
@scodoc7func
|
||||||
def edit_enseignants_form(moduleimpl_id):
|
def edit_enseignants_form(moduleimpl_id):
|
||||||
"modif liste enseignants/moduleimpl"
|
"modif liste enseignants/moduleimpl"
|
||||||
M, sem = sco_moduleimpl.can_change_ens(moduleimpl_id)
|
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
|
||||||
|
modimpl.can_change_ens(raise_exc=True)
|
||||||
# --
|
# --
|
||||||
header = html_sco_header.html_sem_header(
|
header = html_sco_header.html_sem_header(
|
||||||
'Enseignants du <a href="moduleimpl_status?moduleimpl_id=%s">module %s</a>'
|
f"""Enseignants du <a href="{
|
||||||
% (moduleimpl_id, M["module"]["titre"]),
|
url_for("notes.moduleimpl_status",
|
||||||
page_title="Enseignants du module %s" % M["module"]["titre"],
|
scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
|
||||||
|
}">module {modimpl.module.titre or modimpl.module.code}</a>""",
|
||||||
|
page_title=f"Enseignants du module {modimpl.module.titre or modimpl.module.code}",
|
||||||
javascripts=["libjs/AutoSuggest.js"],
|
javascripts=["libjs/AutoSuggest.js"],
|
||||||
cssstyles=["css/autosuggest_inquisitor.css"],
|
cssstyles=["css/autosuggest_inquisitor.css"],
|
||||||
bodyOnLoad="init_tf_form('')",
|
bodyOnLoad="init_tf_form('')",
|
||||||
@ -981,21 +984,18 @@ def edit_enseignants_form(moduleimpl_id):
|
|||||||
allowed_user_names = list(uid2display.values())
|
allowed_user_names = list(uid2display.values())
|
||||||
|
|
||||||
H = [
|
H = [
|
||||||
"<ul><li><b>%s</b> (responsable)</li>"
|
f"""<ul><li><b>{
|
||||||
% uid2display.get(M["responsable_id"], M["responsable_id"])
|
uid2display.get(modimpl.responsable_id, modimpl.responsable_id)
|
||||||
|
}</b> (responsable)</li>"""
|
||||||
]
|
]
|
||||||
for ens in M["ens"]:
|
u: User
|
||||||
u = db.session.get(User, ens["ens_id"])
|
for u in modimpl.enseignants:
|
||||||
if u:
|
|
||||||
nom = u.get_nomcomplet()
|
|
||||||
else:
|
|
||||||
nom = "? (compte inconnu)"
|
|
||||||
H.append(
|
H.append(
|
||||||
f"""
|
f"""
|
||||||
<li>{nom} (<a class="stdlink" href="{
|
<li>{u.get_nomcomplet()} (<a class="stdlink" href="{
|
||||||
url_for('notes.edit_enseignants_form_delete',
|
url_for('notes.edit_enseignants_form_delete',
|
||||||
scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id,
|
scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id,
|
||||||
ens_id=ens["ens_id"])
|
ens_id=u.id)
|
||||||
}">supprimer</a>)
|
}">supprimer</a>)
|
||||||
</li>"""
|
</li>"""
|
||||||
)
|
)
|
||||||
@ -1006,7 +1006,7 @@ def edit_enseignants_form(moduleimpl_id):
|
|||||||
<p class="help">Pour changer le responsable du module, passez par la
|
<p class="help">Pour changer le responsable du module, passez par la
|
||||||
page "<a class="stdlink" href="{
|
page "<a class="stdlink" href="{
|
||||||
url_for("notes.formsemestre_editwithmodules", scodoc_dept=g.scodoc_dept,
|
url_for("notes.formsemestre_editwithmodules", scodoc_dept=g.scodoc_dept,
|
||||||
formsemestre_id=M["formsemestre_id"])
|
formsemestre_id=modimpl.formsemestre_id)
|
||||||
}">Modification du semestre</a>",
|
}">Modification du semestre</a>",
|
||||||
accessible uniquement au responsable de la formation (chef de département)
|
accessible uniquement au responsable de la formation (chef de département)
|
||||||
</p>
|
</p>
|
||||||
@ -1053,24 +1053,24 @@ def edit_enseignants_form(moduleimpl_id):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
ens_id = User.get_user_id_from_nomplogin(tf[2]["ens_id"])
|
ens = User.get_user_from_nomplogin(tf[2]["ens_id"])
|
||||||
if not ens_id:
|
if ens is None:
|
||||||
H.append(
|
H.append(
|
||||||
'<p class="help">Pour ajouter un enseignant, choisissez un nom dans le menu</p>'
|
'<p class="help">Pour ajouter un enseignant, choisissez un nom dans le menu</p>'
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# et qu'il n'est pas deja:
|
# et qu'il n'est pas deja:
|
||||||
if (
|
if (
|
||||||
ens_id in [x["ens_id"] for x in M["ens"]]
|
ens.id in (x.id for x in modimpl.enseignants)
|
||||||
or ens_id == M["responsable_id"]
|
or ens.id == modimpl.responsable_id
|
||||||
):
|
):
|
||||||
H.append(
|
H.append(
|
||||||
f"""<p class="help">Enseignant {ens_id} déjà dans la liste !</p>"""
|
f"""<p class="help">Enseignant {ens.user_name} déjà dans la liste !</p>"""
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
sco_moduleimpl.do_ens_create(
|
modimpl.enseignants.append(ens)
|
||||||
{"moduleimpl_id": moduleimpl_id, "ens_id": ens_id}
|
db.session.add(modimpl)
|
||||||
)
|
db.session.commit()
|
||||||
return flask.redirect(
|
return flask.redirect(
|
||||||
url_for(
|
url_for(
|
||||||
"notes.edit_enseignants_form",
|
"notes.edit_enseignants_form",
|
||||||
@ -1090,10 +1090,10 @@ def edit_moduleimpl_resp(moduleimpl_id: int):
|
|||||||
Accessible par Admin et dir des etud si flag resp_can_change_ens
|
Accessible par Admin et dir des etud si flag resp_can_change_ens
|
||||||
"""
|
"""
|
||||||
modimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id)
|
modimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id)
|
||||||
modimpl.can_change_ens_by(current_user, raise_exc=True) # access control
|
modimpl.can_change_responsable(current_user, raise_exc=True) # access control
|
||||||
H = [
|
H = [
|
||||||
html_sco_header.html_sem_header(
|
html_sco_header.html_sem_header(
|
||||||
f"""Modification du responsable du <a href="{
|
f"""Modification du responsable du <a class="stdlink" href="{
|
||||||
url_for("notes.moduleimpl_status",
|
url_for("notes.moduleimpl_status",
|
||||||
scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id)
|
scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id)
|
||||||
}">module {modimpl.module.titre or ""}</a>""",
|
}">module {modimpl.module.titre or ""}</a>""",
|
||||||
@ -1156,8 +1156,8 @@ def edit_moduleimpl_resp(moduleimpl_id: int):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
responsable_id = User.get_user_id_from_nomplogin(tf[2]["responsable_id"])
|
responsable = User.get_user_from_nomplogin(tf[2]["responsable_id"])
|
||||||
if not responsable_id:
|
if not responsable:
|
||||||
# presque impossible: tf verifie les valeurs (mais qui peuvent changer entre temps)
|
# presque impossible: tf verifie les valeurs (mais qui peuvent changer entre temps)
|
||||||
return flask.redirect(
|
return flask.redirect(
|
||||||
url_for(
|
url_for(
|
||||||
@ -1167,7 +1167,7 @@ def edit_moduleimpl_resp(moduleimpl_id: int):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
modimpl.responsable_id = responsable_id
|
modimpl.responsable = responsable
|
||||||
db.session.add(modimpl)
|
db.session.add(modimpl)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash("Responsable modifié")
|
flash("Responsable modifié")
|
||||||
@ -1192,8 +1192,6 @@ def view_module_abs(moduleimpl_id, fmt="html"):
|
|||||||
.filter_by(dept_id=g.scodoc_dept_id)
|
.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
).first_or_404()
|
).first_or_404()
|
||||||
|
|
||||||
debut_sem = modimpl.formsemestre.date_debut
|
|
||||||
fin_sem = modimpl.formsemestre.date_fin
|
|
||||||
inscrits: list[Identite] = sorted(
|
inscrits: list[Identite] = sorted(
|
||||||
[i.etud for i in modimpl.inscriptions], key=lambda e: e.sort_key
|
[i.etud for i in modimpl.inscriptions], key=lambda e: e.sort_key
|
||||||
)
|
)
|
||||||
@ -1286,9 +1284,8 @@ def formsemestre_enseignants_list(formsemestre_id, fmt="html"):
|
|||||||
"""
|
"""
|
||||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||||
# resp. de modules et charges de TD
|
# resp. de modules et charges de TD
|
||||||
sem_ens: dict[
|
# uid : { "mods" : liste des modimpls, ... }
|
||||||
int, list[ModuleImpl]
|
sem_ens: dict[int, list[ModuleImpl]] = {}
|
||||||
] = {} # uid : { "mods" : liste des modimpls, ... }
|
|
||||||
modimpls = formsemestre.modimpls_sorted
|
modimpls = formsemestre.modimpls_sorted
|
||||||
for modimpl in modimpls:
|
for modimpl in modimpls:
|
||||||
if not modimpl.responsable_id in sem_ens:
|
if not modimpl.responsable_id in sem_ens:
|
||||||
@ -1372,22 +1369,17 @@ def edit_enseignants_form_delete(moduleimpl_id, ens_id: int):
|
|||||||
|
|
||||||
ens_id: user.id
|
ens_id: user.id
|
||||||
"""
|
"""
|
||||||
M, _ = sco_moduleimpl.can_change_ens(moduleimpl_id)
|
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
|
||||||
|
modimpl.can_change_ens(raise_exc=True)
|
||||||
# search ens_id
|
# search ens_id
|
||||||
ok = False
|
ens: User | None = None
|
||||||
for ens in M["ens"]:
|
for ens in modimpl.enseignants:
|
||||||
if ens["ens_id"] == ens_id:
|
if ens.id == ens_id:
|
||||||
ok = True
|
|
||||||
break
|
break
|
||||||
if not ok:
|
if ens is None:
|
||||||
raise ScoValueError(f"invalid ens_id ({ens_id})")
|
raise ScoValueError(f"invalid ens_id ({ens_id})")
|
||||||
ndb.SimpleQuery(
|
modimpl.enseignants.remove(ens)
|
||||||
"""DELETE FROM notes_modules_enseignants
|
db.session.commit()
|
||||||
WHERE moduleimpl_id = %(moduleimpl_id)s
|
|
||||||
AND ens_id = %(ens_id)s
|
|
||||||
""",
|
|
||||||
{"moduleimpl_id": moduleimpl_id, "ens_id": ens_id},
|
|
||||||
)
|
|
||||||
return flask.redirect(
|
return flask.redirect(
|
||||||
url_for(
|
url_for(
|
||||||
"notes.edit_enseignants_form",
|
"notes.edit_enseignants_form",
|
||||||
@ -1399,18 +1391,6 @@ def edit_enseignants_form_delete(moduleimpl_id, ens_id: int):
|
|||||||
|
|
||||||
# --- Gestion des inscriptions aux semestres
|
# --- Gestion des inscriptions aux semestres
|
||||||
|
|
||||||
# Ancienne API, pas certain de la publier en ScoDoc8
|
|
||||||
# sco_publish(
|
|
||||||
# "/do_formsemestre_inscription_create",
|
|
||||||
# sco_formsemestre_inscriptions.do_formsemestre_inscription_create,
|
|
||||||
# Permission.EtudInscrit,
|
|
||||||
# )
|
|
||||||
# sco_publish(
|
|
||||||
# "/do_formsemestre_inscription_edit",
|
|
||||||
# sco_formsemestre_inscriptions.do_formsemestre_inscription_edit,
|
|
||||||
# Permission.EtudInscrit,
|
|
||||||
# )
|
|
||||||
|
|
||||||
sco_publish(
|
sco_publish(
|
||||||
"/do_formsemestre_inscription_list",
|
"/do_formsemestre_inscription_list",
|
||||||
sco_formsemestre_inscriptions.do_formsemestre_inscription_list,
|
sco_formsemestre_inscriptions.do_formsemestre_inscription_list,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# -*- mode: python -*-
|
# -*- mode: python -*-
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
SCOVERSION = "9.6.92"
|
SCOVERSION = "9.6.934"
|
||||||
|
|
||||||
SCONAME = "ScoDoc"
|
SCONAME = "ScoDoc"
|
||||||
|
|
||||||
|
@ -619,7 +619,6 @@ def test_formsemestre_programme(api_headers):
|
|||||||
assert verify_fields(sae, MODIMPL_FIELDS)
|
assert verify_fields(sae, MODIMPL_FIELDS)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip # XXX WIP
|
|
||||||
def test_etat_evals(api_headers): # voir si on maintient cette route ?
|
def test_etat_evals(api_headers): # voir si on maintient cette route ?
|
||||||
"""
|
"""
|
||||||
Route : /formsemestre/<int:formsemestre_id>/etat_evals
|
Route : /formsemestre/<int:formsemestre_id>/etat_evals
|
||||||
|
@ -375,7 +375,6 @@ def test_import_formation(test_client, filename="formation-exemple-1.xml"):
|
|||||||
formsemestre_id=formsemestre_ids[mod["semestre_id"] - 1],
|
formsemestre_id=formsemestre_ids[mod["semestre_id"] - 1],
|
||||||
)
|
)
|
||||||
mi = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
mi = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
||||||
assert mi["ens"] == []
|
|
||||||
assert mi["module_id"] == mod["module_id"]
|
assert mi["module_id"] == mod["module_id"]
|
||||||
|
|
||||||
# --- Export formation en XML
|
# --- Export formation en XML
|
||||||
|
Loading…
Reference in New Issue
Block a user