Merge branch 'scodoc-master' into pe-BUT-v2

This commit is contained in:
Cléo Baras 2024-02-05 19:47:36 +01:00
commit be39245e25
31 changed files with 440 additions and 1637 deletions

View File

@ -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")

View File

@ -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):

View File

@ -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

View File

@ -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/>
""" """
), ),

View File

@ -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():

View File

@ -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)

View File

@ -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))

View File

@ -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:

View File

@ -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

View File

@ -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).

View File

@ -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

View File

@ -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

View File

@ -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
) )

View File

@ -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

View File

@ -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"],
)
) )
)

View File

@ -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 (

View File

@ -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

View File

@ -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'

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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"""

View File

@ -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)

View File

@ -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")

View File

@ -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,

View File

@ -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"

View File

@ -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

View File

@ -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