diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py index 28837aca1..4c9c40241 100644 --- a/app/api/formsemestres.py +++ b/app/api/formsemestres.py @@ -374,115 +374,114 @@ def formsemestre_etudiants( return sorted(etuds, key=itemgetter("sort_key")) -# retrait (temporaire ? à discuter) -# @bp.route("/formsemestre//etat_evals") -# @api_web_bp.route("/formsemestre//etat_evals") -# @login_required -# @scodoc -# @permission_required(Permission.ScoView) -# @as_json -# def etat_evals(formsemestre_id: int): -# """ -# Informations sur l'état des évaluations d'un formsemestre. +@bp.route("/formsemestre//etat_evals") +@api_web_bp.route("/formsemestre//etat_evals") +@login_required +@scodoc +@permission_required(Permission.ScoView) +@as_json +def etat_evals(formsemestre_id: int): + """ + 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 : -# [ -# { -# "id": 1, // moduleimpl_id -# "titre": "Initiation aux réseaux informatiques", -# "evaluations": [ -# { -# "id": 1, -# "description": null, -# "datetime_epreuve": null, -# "heure_fin": "09:00:00", -# "coefficient": "02.00" -# "is_complete": true, -# "nb_inscrits": 16, -# "nb_manquantes": 0, -# "ABS": 0, -# "ATT": 0, -# "EXC": 0, -# "saisie_notes": { -# "datetime_debut": "2021-09-11T00:00:00+02:00", -# "datetime_fin": "2022-08-25T00:00:00+02:00", -# "datetime_mediane": "2022-03-19T00:00:00+01:00" -# } -# }, -# ... -# ] -# }, -# ] -# """ -# query = FormSemestre.query.filter_by(id=formsemestre_id) -# if g.scodoc_dept: -# query = query.filter_by(dept_id=g.scodoc_dept_id) -# formsemestre: FormSemestre = query.first_or_404(formsemestre_id) -# app.set_sco_dept(formsemestre.departement.acronym) -# nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) + Exemple de résultat : + [ + { + "id": 1, // moduleimpl_id + "titre": "Initiation aux réseaux informatiques", + "evaluations": [ + { + "id": 1, + "description": null, + "datetime_epreuve": null, + "heure_fin": "09:00:00", + "coefficient": "02.00" + "is_complete": true, + "nb_inscrits": 16, + "nb_manquantes": 0, + "ABS": 0, + "ATT": 0, + "EXC": 0, + "saisie_notes": { + "datetime_debut": "2021-09-11T00:00:00+02:00", + "datetime_fin": "2022-08-25T00:00:00+02:00", + "datetime_mediane": "2022-03-19T00:00:00+01:00" + } + }, + ... + ] + }, + ] + """ + query = FormSemestre.query.filter_by(id=formsemestre_id) + if g.scodoc_dept: + query = query.filter_by(dept_id=g.scodoc_dept_id) + formsemestre: FormSemestre = query.first_or_404(formsemestre_id) + app.set_sco_dept(formsemestre.departement.acronym) + nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) -# result = [] -# for modimpl_id in nt.modimpls_results: -# modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl_id] -# modimpl: ModuleImpl = ModuleImpl.query.get_or_404(modimpl_id) -# modimpl_dict = modimpl.to_dict(convert_objects=True) + result = [] + for modimpl_id in nt.modimpls_results: + modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl_id] + modimpl: ModuleImpl = ModuleImpl.query.get_or_404(modimpl_id) + modimpl_dict = modimpl.to_dict(convert_objects=True, with_module=False) -# list_eval = [] -# for evaluation_id in modimpl_results.evaluations_etat: -# eval_etat = modimpl_results.evaluations_etat[evaluation_id] -# evaluation = Evaluation.query.get_or_404(evaluation_id) -# eval_dict = evaluation.to_dict_api() -# eval_dict["etat"] = eval_etat.to_dict() + list_eval = [] + for evaluation_id in modimpl_results.evaluations_etat: + eval_etat = modimpl_results.evaluations_etat[evaluation_id] + evaluation = Evaluation.query.get_or_404(evaluation_id) + eval_dict = evaluation.to_dict_api() + eval_dict["etat"] = eval_etat.to_dict() -# eval_dict["nb_inscrits"] = modimpl_results.nb_inscrits_module -# eval_dict["nb_notes_manquantes"] = len( -# modimpl_results.evals_etudids_sans_note[evaluation.id] -# ) -# eval_dict["nb_notes_abs"] = sum( -# modimpl_results.evals_notes[evaluation.id] == scu.NOTES_ABSENCE -# ) -# eval_dict["nb_notes_att"] = eval_etat.nb_attente -# eval_dict["nb_notes_exc"] = sum( -# modimpl_results.evals_notes[evaluation.id] == scu.NOTES_NEUTRALISE -# ) + eval_dict["nb_inscrits"] = modimpl_results.nb_inscrits_module + eval_dict["nb_notes_manquantes"] = len( + modimpl_results.evals_etudids_sans_note[evaluation.id] + ) + eval_dict["nb_notes_abs"] = sum( + modimpl_results.evals_notes[evaluation.id] == scu.NOTES_ABSENCE + ) + eval_dict["nb_notes_att"] = eval_etat.nb_attente + eval_dict["nb_notes_exc"] = sum( + modimpl_results.evals_notes[evaluation.id] == scu.NOTES_NEUTRALISE + ) -# # Récupération de toutes les notes de l'évaluation -# # eval["notes"] = modimpl_results.get_eval_notes_dict(evaluation_id) + # Récupération de toutes les notes de l'évaluation + # 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_fin = None -# date_mediane = None + date_debut = None + date_fin = None + date_mediane = None -# # Si il y a plus d'une note saisie pour l'évaluation -# if len(notes) >= 1: -# # Tri des notes en fonction de leurs dates -# notes_sorted = sorted(notes, key=attrgetter("date")) + # Si il y a plus d'une note saisie pour l'évaluation + if len(notes) >= 1: + # Tri des notes en fonction de leurs dates + notes_sorted = sorted(notes, key=attrgetter("date")) -# date_debut = notes_sorted[0].date -# date_fin = notes_sorted[-1].date + date_debut = notes_sorted[0].date + date_fin = notes_sorted[-1].date -# # Note médiane -# date_mediane = notes_sorted[len(notes_sorted) // 2].date + # Note médiane + date_mediane = notes_sorted[len(notes_sorted) // 2].date -# eval_dict["saisie_notes"] = { -# "datetime_debut": date_debut.isoformat() -# if date_debut is not None -# else None, -# "datetime_fin": date_fin.isoformat() if date_fin is not None else None, -# "datetime_mediane": date_mediane.isoformat() -# if date_mediane is not None -# else None, -# } + eval_dict["saisie_notes"] = { + "datetime_debut": date_debut.isoformat() + if date_debut is not None + else None, + "datetime_fin": date_fin.isoformat() if date_fin is not None else None, + "datetime_mediane": date_mediane.isoformat() + if date_mediane is not None + else None, + } -# list_eval.append(eval_dict) + list_eval.append(eval_dict) -# modimpl_dict["evaluations"] = list_eval -# result.append(modimpl_dict) -# return result + modimpl_dict["evaluations"] = list_eval + result.append(modimpl_dict) + return result @bp.route("/formsemestre//resultats") diff --git a/app/auth/models.py b/app/auth/models.py index c38590ead..ed29772f4 100644 --- a/app/auth/models.py +++ b/app/auth/models.py @@ -102,6 +102,8 @@ class User(UserMixin, ScoDocModel): token = db.Column(db.Text(), index=True, unique=True) 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) Permission = Permission @@ -245,24 +247,26 @@ class User(UserMixin, ScoDocModel): def to_dict(self, include_email=True): """l'utilisateur comme un dict, avec des champs supplémentaires""" data = { - "date_expiration": self.date_expiration.isoformat() + "Z" - if self.date_expiration - else None, - "date_modif_passwd": self.date_modif_passwd.isoformat() + "Z" - if self.date_modif_passwd - else None, - "date_created": self.date_created.isoformat() + "Z" - if self.date_created - else None, + "date_expiration": ( + self.date_expiration.isoformat() + "Z" if self.date_expiration else None + ), + "date_modif_passwd": ( + self.date_modif_passwd.isoformat() + "Z" + if self.date_modif_passwd + else None + ), + "date_created": ( + self.date_created.isoformat() + "Z" if self.date_created else None + ), "dept": self.dept, "id": self.id, "active": self.active, "cas_id": self.cas_id, "cas_allow_login": self.cas_allow_login, "cas_allow_scodoc_login": self.cas_allow_scodoc_login, - "cas_last_login": self.cas_last_login.isoformat() + "Z" - if self.cas_last_login - else None, + "cas_last_login": ( + self.cas_last_login.isoformat() + "Z" if self.cas_last_login else None + ), "edt_id": self.edt_id, "status_txt": "actif" if self.active else "fermé", "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})" @staticmethod - def get_user_id_from_nomplogin(nomplogin: str) -> Optional[int]: - """Returns id from the string "Dupont Pierre (dupont)" + def get_user_from_nomplogin(nomplogin: str) -> Optional["User"]: + """Returns User instance from the string "Dupont Pierre (dupont)" or None if user does not exist """ match = re.match(r".*\((.*)\)", nomplogin.strip()) @@ -486,7 +490,7 @@ class User(UserMixin, ScoDocModel): user_name = match.group(1) u = User.query.filter_by(user_name=user_name).first() if u: - return u.id + return u return None def get_nom_fmt(self): diff --git a/app/but/bulletin_but_court.py b/app/but/bulletin_but_court.py index c8d258280..7fb389a5c 100644 --- a/app/but/bulletin_but_court.py +++ b/app/but/bulletin_but_court.py @@ -35,7 +35,6 @@ from app.decorators import ( permission_required, ) 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.sco_exceptions import ScoNoReferentielCompetences, ScoValueError from app.scodoc.sco_logos import find_logo diff --git a/app/but/bulletin_but_court_pdf.py b/app/but/bulletin_but_court_pdf.py index 2021ddf6a..e8a6563df 100644 --- a/app/but/bulletin_but_court_pdf.py +++ b/app/but/bulletin_but_court_pdf.py @@ -194,7 +194,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard): """Génère la partie "titre" du bulletin de notes. 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) def bul_part_below(self, fmt="pdf") -> list: @@ -406,6 +406,8 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard): def boite_identite(self) -> list: "Les informations sur l'identité et l'inscription de l'étudiant" + parcour = self.formsemestre.etuds_inscriptions[self.etud.id].parcour + return [ Paragraph( SU(f"""{self.etud.nomprenom}"""), @@ -416,6 +418,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard): f""" {self.bul["demission"]}
Formation: {self.formsemestre.titre_num()}
+ {'Parcours ' + parcour.code + '
' if parcour else ''} Année universitaire: {self.formsemestre.annee_scolaire_str()}
""" ), diff --git a/app/but/jury_but_view.py b/app/but/jury_but_view.py index 8536ac63c..20bd249ae 100644 --- a/app/but/jury_but_view.py +++ b/app/but/jury_but_view.py @@ -97,7 +97,7 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str: {formsemestre_2.annee_scolaire_str() if formsemestre_2 else ""} -
RCUE
+
RCUE
""" ) for dec_rcue in deca.get_decisions_rcues_annee(): diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py index ec9c450a9..94f056b2f 100644 --- a/app/comp/moy_mod.py +++ b/app/comp/moy_mod.py @@ -178,19 +178,25 @@ class ModuleImplResults: eval_notes_inscr = evals_notes[str(evaluation.id)][list(inscrits_module)] # Nombre de notes (non vides, incluant ATT etc) des inscrits: nb_notes = eval_notes_inscr.notna().sum() + # Etudiants avec notes en attente: + # = ceux avec note ATT eval_etudids_attente = set( eval_notes_inscr.iloc[ (eval_notes_inscr == scu.NOTES_ATTENTE).to_numpy() ].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.evaluations_etat[evaluation.id] = EvaluationEtat( evaluation_id=evaluation.id, nb_attente=len(eval_etudids_attente), - nb_notes=nb_notes, + nb_notes=int(nb_notes), 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) # Force columns names to integers (evaluation ids) diff --git a/app/models/__init__.py b/app/models/__init__.py index d5b88d12c..242615472 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -52,7 +52,7 @@ class ScoDocModel(db.Model): def create_from_dict(cls, data: dict) -> "ScoDocModel": """Create a new instance of the model with attributes given in dict. 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: args = cls.convert_dict_fields(cls.filter_model_attributes(data)) diff --git a/app/models/etudiants.py b/app/models/etudiants.py index d7eb0312d..6a275b46b 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -445,10 +445,11 @@ class Identite(models.ScoDocModel): "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, compatible ScoDoc7 mais sans infos admission. 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.pop("_sa_instance_state", None) @@ -460,6 +461,8 @@ class Identite(models.ScoDocModel): adresse = self.adresses.first() if adresse: 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 def to_dict_bul(self, include_urls=True): @@ -574,7 +577,9 @@ class Identite(models.ScoDocModel): return r[0] if r else None 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() if inscription_courante: titre_sem = inscription_courante.formsemestre.titre_mois() @@ -585,7 +590,7 @@ class Identite(models.ScoDocModel): else: inscr_txt = "Inscrit en" - return { + result = { "etat_in_cursem": inscription_courante.etat, "inscription_courante": inscription_courante, "inscription": titre_sem, @@ -608,15 +613,20 @@ class Identite(models.ScoDocModel): inscription = "ancien" situation = "ancien élève" else: - inscription = ("non inscrit",) + inscription = "non inscrit" situation = inscription - return { + result = { "etat_in_cursem": "?", "inscription_courante": None, "inscription": inscription, "inscription_str": inscription, "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: """État de l'inscription de cet étudiant au semestre: @@ -749,9 +759,7 @@ def check_etud_duplicate_code(args, code_name, edit=True): ).all() duplicate = False if edit: - duplicate = (len(etuds) > 1) or ( - (len(etuds) == 1) and etuds[0].id != args["etudid"] - ) + duplicate = (len(etuds) > 1) or ((len(etuds) == 1) and etuds[0].id != etudid) else: duplicate = len(etuds) > 0 if duplicate: diff --git a/app/models/evaluations.py b/app/models/evaluations.py index e2c01695f..d796381d6 100644 --- a/app/models/evaluations.py +++ b/app/models/evaluations.py @@ -5,7 +5,7 @@ import datetime from operator import attrgetter -from flask import g, url_for +from flask import abort, g, url_for from flask_login import current_user import sqlalchemy as sa @@ -241,6 +241,25 @@ class Evaluation(db.Model): if k != "_sa_instance_state" and k != "id" and k in data: 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 def get_max_numero(cls, moduleimpl_id: int) -> int: """Return max numero among evaluations in this @@ -265,7 +284,9 @@ class Evaluation(db.Model): evaluations = moduleimpl.evaluations.order_by( Evaluation.date_debut, Evaluation.numero ).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: return # all ok diff --git a/app/models/moduleimpls.py b/app/models/moduleimpls.py index 9cb168eb9..97dc82e32 100644 --- a/app/models/moduleimpls.py +++ b/app/models/moduleimpls.py @@ -3,12 +3,13 @@ """ import pandas as pd from flask import abort, g +from flask_login import current_user from flask_sqlalchemy.query import Query from app import db from app.auth.models import User 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.evaluations import Evaluation 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 -class ModuleImpl(db.Model): +class ModuleImpl(ScoDocModel): """Mise en oeuvre d'un module pour une annee/semestre""" __tablename__ = "notes_moduleimpl" @@ -36,7 +37,10 @@ class ModuleImpl(db.Model): index=True, 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: computation_expr = db.Column(db.Text()) @@ -52,8 +56,8 @@ class ModuleImpl(db.Model): secondary="notes_modules_enseignants", lazy="dynamic", backref="moduleimpl", - viewonly=True, ) + "enseignants du module (sans le responsable)" def __repr__(self): return f"<{self.__class__.__name__} {self.id} module={repr(self.module)}>" @@ -85,7 +89,7 @@ class ModuleImpl(db.Model): @classmethod 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 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 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. If raise_exc, raises exception (AccessDenied or ScoLockedSemError) if not. = 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}") 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: """ Vérifie si l'étudiant est bien inscrit au moduleimpl (même si DEM ou DEF au semestre). diff --git a/app/scodoc/html_sidebar.py b/app/scodoc/html_sidebar.py index b64cefc60..af6454456 100755 --- a/app/scodoc/html_sidebar.py +++ b/app/scodoc/html_sidebar.py @@ -33,7 +33,7 @@ from flask import g, request from flask_login import current_user 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 from app.scodoc import sco_preferences from app.scodoc.sco_permissions import Permission @@ -160,27 +160,32 @@ def sidebar(etudid: int = None): etudid = request.form.get("etudid", None) if etudid is not None: - etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0] - params.update(etud) - params["fiche_url"] = url_for( - "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid - ) + etud = Identite.get_etud(etudid) # compte les absences du semestre en cours H.append( - """

- %(civilite_str)s %(nom_disp)s -

- Absences""" - % params + f"""

+ {etud.civilite_str} {etud.nom_disp()} +

+ Absences""" ) - if etud["cursem"]: - cur_sem = etud["cursem"] - nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, cur_sem) + inscription = etud.inscription_courante() + if inscription: + formsemestre = inscription.formsemestre + nbabs, nbabsjust = sco_assiduites.formsemestre_get_assiduites_count( + etudid, formsemestre + ) nbabsnj = nbabs - nbabsjust H.append( - f"""({ - sco_preferences.get_preference("assi_metrique", None)}) + f"""({ + sco_preferences.get_preference("assi_metrique", None)})
{ nbabsjust } J., { nbabsnj } N.J.
""" ) H.append("
    ") @@ -189,21 +194,24 @@ def sidebar(etudid: int = None): cur_formsemestre_id = retreive_formsemestre_from_request() H.append( f""" -
  • Ajouter
  • -
  • Justifier
  • +
  • Ajouter
  • +
  • Justifier
  • """ ) if sco_preferences.get_preference("handle_billets_abs"): H.append( - f"""
  • Billets
  • """ + f"""
  • Billets
  • """ ) H.append( f""" diff --git a/app/scodoc/sco_abs.py b/app/scodoc/sco_abs.py deleted file mode 100755 index 2f55f3c64..000000000 --- a/app/scodoc/sco_abs.py +++ /dev/null @@ -1,1109 +0,0 @@ -# -*- mode: python -*- -# -*- coding: utf-8 -*- - -############################################################################## -# -# Gestion scolarite IUT -# -# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# -# Emmanuel Viennet emmanuel.viennet@viennet.net -# -############################################################################## - -"""Fonctions sur les absences -""" - -import calendar -import datetime -import html -import time - -from app.scodoc import notesdb as ndb -from app import log -from app.scodoc.scolog import logdb -from app.scodoc.sco_exceptions import ScoValueError, ScoInvalidDateError -from app.scodoc import sco_abs_notification -from app.scodoc import sco_cache -from app.scodoc import sco_etud -from app.scodoc import sco_formsemestre_inscriptions -from app.scodoc import sco_preferences -import app.scodoc.sco_utils as scu - -# --- Misc tools.... ------------------ - - -def _isFarFutur(jour): - # check si jour est dans le futur "lointain" - # pour autoriser les saisies dans le futur mais pas a plus de 6 mois - y, m, d = [int(x) for x in jour.split("-")] - try: - j = datetime.date(y, m, d) - except ValueError: - # les dates erronées, genre année 20022, sont considéres dans le futur - return True - # 6 mois ~ 182 jours: - return j - datetime.date.today() > datetime.timedelta(182) - - -def _toboolean(x): - "convert a value to boolean" - return bool(x) - - -def is_work_saturday(): - "Vrai si le samedi est travaillé" - return int(sco_preferences.get_preference("work_saturday")) - - -def MonthNbDays(month, year): - "returns nb of days in month" - if month > 7: - month = month + 1 - if month % 2: - return 31 - elif month == 2: - if calendar.isleap(year): - return 29 - else: - return 28 - else: - return 30 - - -class ddmmyyyy(object): - """immutable dates""" - - def __init__(self, date=None, fmt="ddmmyyyy", work_saturday=False): - self.work_saturday = work_saturday - if date is None: - return - try: - if fmt == "ddmmyyyy": - self.day, self.month, self.year = date.split("/") - elif fmt == "iso": - self.year, self.month, self.day = date.split("-") - else: - raise ValueError("invalid format spec. (%s)" % fmt) - self.year = int(self.year) - self.month = int(self.month) - self.day = int(self.day) - except ValueError: - raise ScoValueError("date invalide: %s" % date) - # accept years YYYY or YY, uses 1970 as pivot - if self.year < 1970: - if self.year > 100: - raise ScoInvalidDateError("Année invalide: %s" % self.year) - if self.year < 70: - self.year = self.year + 2000 - else: - self.year = self.year + 1900 - if self.month < 1 or self.month > 12: - raise ScoInvalidDateError("Mois invalide: %s" % self.month) - - if self.day < 1 or self.day > MonthNbDays(self.month, self.year): - raise ScoInvalidDateError("Jour invalide: %s" % self.day) - - # weekday in 0-6, where 0 is monday - self.weekday = calendar.weekday(self.year, self.month, self.day) - - self.time = time.mktime((self.year, self.month, self.day, 0, 0, 0, 0, 0, 0)) - - def iswork(self): - "returns true if workable day" - if self.work_saturday: - nbdays = 6 - else: - nbdays = 5 - if ( - self.weekday >= 0 and self.weekday < nbdays - ): # monday-friday or monday-saturday - return 1 - else: - return 0 - - def __repr__(self): - return "'%02d/%02d/%04d'" % (self.day, self.month, self.year) - - def __str__(self): - return "%02d/%02d/%04d" % (self.day, self.month, self.year) - - def ISO(self): - "iso8601 representation of the date" - return "%04d-%02d-%02d" % (self.year, self.month, self.day) - - def next_day(self, days=1): - "date for the next day (nota: may be a non workable day)" - day = self.day + days - month = self.month - year = self.year - - while day > MonthNbDays(month, year): - day = day - MonthNbDays(month, year) - month = month + 1 - if month > 12: - month = 1 - year = year + 1 - return self.__class__( - "%02d/%02d/%04d" % (day, month, year), work_saturday=self.work_saturday - ) - - def prev(self, days=1): - "date for previous day" - day = self.day - days - month = self.month - year = self.year - while day <= 0: - month = month - 1 - if month == 0: - month = 12 - year = year - 1 - day = day + MonthNbDays(month, year) - - return self.__class__( - "%02d/%02d/%04d" % (day, month, year), work_saturday=self.work_saturday - ) - - def next_monday(self): - "date of next monday" - return self.next_day((7 - self.weekday) % 7) - - def prev_monday(self): - "date of last monday, but on sunday, pick next monday" - if self.weekday == 6: - return self.next_monday() - else: - return self.prev(self.weekday) - - def __cmp__(self, other): # #py3 TODO à supprimer - """return a negative integer if self < other, - zero if self == other, a positive integer if self > other""" - return int(self.time - other.time) - - def __eq__(self, other): - return self.time == other.time - - def __ne__(self, other): - return self.time != other.time - - def __lt__(self, other): - return self.time < other.time - - def __le__(self, other): - return self.time <= other.time - - def __gt__(self, other): - return self.time > other.time - - def __ge__(self, other): - return self.time >= other.time - - def __hash__(self): - "we are immutable !" - return hash(self.time) ^ hash(str(self)) - - -# d = ddmmyyyy( '21/12/99' ) -def DateRangeISO(date_beg, date_end, workable=1): - """returns list of dates in [date_beg,date_end] - workable = 1 => keeps only workable days""" - if not date_beg: - raise ScoValueError("pas de date spécifiée !") - if not date_end: - date_end = date_beg - r = [] - work_saturday = is_work_saturday() - try: - cur = ddmmyyyy(date_beg, work_saturday=work_saturday) - end = ddmmyyyy(date_end, work_saturday=work_saturday) - except (AttributeError, ValueError) as e: - raise ScoValueError("date invalide !") from e - while cur <= end: - if (not workable) or cur.iswork(): - r.append(cur) - cur = cur.next_day() - - return [x.ISO() for x in r] - - -def day_names(): - """Returns week day names. - If work_saturday property is set, include saturday - """ - if is_work_saturday(): - return scu.DAY_NAMES[:-1] - else: - return scu.DAY_NAMES[:-2] - - -def next_iso_day(date): - "return date after date" - d = ddmmyyyy(date, fmt="iso", work_saturday=is_work_saturday()) - return d.next_day().ISO() - - -def YearTable( - year, - events=[], - firstmonth=9, - lastmonth=7, - halfday=0, - dayattributes="", - pad_width=8, -): - """Generate a calendar table - events = list of tuples (date, text, color, href [,halfday]) - where date is a string in ISO format (yyyy-mm-dd) - halfday is boolean (true: morning, false: afternoon) - text = text to put in calendar (must be short, 1-5 cars) (optional) - if halfday, generate 2 cells per day (morning, afternoon) - """ - T = [ - '' - ] - T.append("") - month = firstmonth - while 1: - T.append('") - if month == lastmonth: - break - month = month + 1 - if month > 12: - month = 1 - year = year + 1 - T.append("
    ') - T.append(MonthTableHead(month)) - T.append( - MonthTableBody( - month, - year, - events, - halfday, - dayattributes, - is_work_saturday(), - pad_width=pad_width, - ) - ) - T.append(MonthTableTail()) - T.append("
    ") - return "\n".join(T) - - -def list_abs_in_range( - etudid, debut=None, fin=None, matin=None, moduleimpl_id=None, cursor=None -): - """Liste des absences entre deux dates. - - Args: - etudid: - debut: string iso date ("2020-03-12") ou None - end: string iso date ("2020-03-12") - matin: None, True, False - moduleimpl_id: restreint le comptage aux absences dans ce module - - Returns: - List of absences - """ - if matin is not None: - matin = _toboolean(matin) - ismatin = " AND A.MATIN = %(matin)s " - else: - ismatin = "" - if moduleimpl_id: - modul = " AND A.MODULEIMPL_ID = %(moduleimpl_id)s " - else: - modul = "" - if not cursor: - cnx = ndb.GetDBConnexion() - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) - cursor.execute( - """SELECT DISTINCT A.JOUR, A.MATIN - FROM ABSENCES A - WHERE A.ETUDID = %(etudid)s - AND A.ESTABS""" - + ismatin - + modul - + ( - "" - if debut is None - else """ - AND A.JOUR BETWEEN %(debut)s AND %(fin)s - """ - ), - { - "etudid": etudid, - "debut": debut, - "fin": fin, - "matin": matin, - "moduleimpl_id": moduleimpl_id, - }, - ) - res = cursor.dictfetchall() - return res - - -def count_abs(etudid, debut, fin, matin=None, moduleimpl_id=None) -> int: - """compte le nombre d'absences - - Args: - etudid: l'étudiant considéré - debut: date, chaîne iso, eg "2021-06-15" - fin: date de fin, incluse - matin: True (compte les matinées), False (les après-midi), None (les deux) - moduleimpl_id: restreint le comptage aux absences dans ce module. - - Returns: - An integer. - """ - return len( - list_abs_in_range(etudid, debut, fin, matin=matin, moduleimpl_id=moduleimpl_id) - ) - - -def count_abs_just(etudid, debut, fin, matin=None, moduleimpl_id=None) -> int: - """compte le nombre d'absences justifiées - - Args: - etudid: l'étudiant considéré - debut: date, chaîne iso, eg "2021-06-15" - fin: date de fin, incluse - matin: True (compte les matinées), False (les après-midi), None (les deux) - moduleimpl_id: restreint le comptage aux absences dans ce module. - - Returns: - An integer. - """ - if matin is not None: - matin = _toboolean(matin) - ismatin = " AND A.MATIN = %(matin)s " - else: - ismatin = "" - if moduleimpl_id: - modul = " AND A.MODULEIMPL_ID = %(moduleimpl_id)s " - else: - modul = "" - cnx = ndb.GetDBConnexion() - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) - cursor.execute( - """SELECT COUNT(*) AS NbAbsJust FROM ( -SELECT DISTINCT A.JOUR, A.MATIN -FROM ABSENCES A, ABSENCES B -WHERE A.ETUDID = %(etudid)s - AND A.ETUDID = B.ETUDID - AND A.JOUR = B.JOUR AND A.MATIN = B.MATIN - AND A.JOUR BETWEEN %(debut)s AND %(fin)s - AND A.ESTABS AND (A.ESTJUST OR B.ESTJUST)""" - + ismatin - + modul - + """ -) AS tmp - """, - vars(), - ) - res = cursor.fetchone()[0] - return res - - -def list_abs_date(etudid, beg_date=None, end_date=None): - """Liste des absences et justifs entre deux dates (inclues).""" - cnx = ndb.GetDBConnexion() - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) - - req = """SELECT jour, matin, estabs, estjust, description - FROM ABSENCES A - WHERE A.ETUDID = %(etudid)s""" + ( - "" - if beg_date is None - else """ - AND A.jour >= %(beg_date)s - AND A.jour <= %(end_date)s - """ - ) - cursor.execute( - req, - vars(), - ) - - absences = cursor.dictfetchall() - # remove duplicates - A = {} # { (jour, matin) : abs } - for a in absences: - jour, matin = a["jour"], a["matin"] - if (jour, matin) in A: - # garde toujours la description - a["description"] = a["description"] or A[(jour, matin)]["description"] - # et la justif: - a["estjust"] = a["estjust"] or A[(jour, matin)]["estjust"] - a["estabs"] = a["estabs"] or A[(jour, matin)]["estabs"] - A[(jour, matin)] = a - else: - A[(jour, matin)] = a - if A[(jour, matin)]["description"] is None: - A[(jour, matin)]["description"] = "" - # add hours: matin = 8:00 - 12:00, apresmidi = 12:00 - 18:00 - dat = "%04d-%02d-%02d" % (a["jour"].year, a["jour"].month, a["jour"].day) - if a["matin"]: - A[(jour, matin)]["begin"] = dat + " 08:00:00" - A[(jour, matin)]["end"] = dat + " 11:59:59" - else: - A[(jour, matin)]["begin"] = dat + " 12:00:00" - A[(jour, matin)]["end"] = dat + " 17:59:59" - # sort - R = list(A.values()) - R.sort(key=lambda x: (x["begin"])) - return R - - -def _get_abs_description(a, cursor=None): - "Description associee a l'absence" - from app.scodoc import sco_moduleimpl - - if not cursor: - cnx = ndb.GetDBConnexion() - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) - a = a.copy() - # a['jour'] = a['jour'].date() - if a["matin"]: # devrait etre booleen... :-( - a["matin"] = True - else: - a["matin"] = False - cursor.execute( - """SELECT * FROM absences - WHERE etudid=%(etudid)s AND jour=%(jour)s AND matin=%(matin)s - ORDER BY entry_date desc""", - a, - ) - A = cursor.dictfetchall() - desc = None - module = "" - for a in A: - if a["description"]: - desc = a["description"] - if a["moduleimpl_id"] and a["moduleimpl_id"] != "NULL": - # Trouver le nom du module - Mlist = sco_moduleimpl.moduleimpl_withmodule_list( - moduleimpl_id=a["moduleimpl_id"] - ) - if Mlist: - M = Mlist[0] - module += "%s " % (M["module"]["code"] or "(module sans code)") - - if desc: - return "(%s) %s" % (desc, module) - if module: - return module - return "" - - -def list_abs_jour(date, am=True, pm=True, is_abs=True, is_just=None) -> list[dict]: - """Liste des absences et/ou justificatifs ce jour. - is_abs: None (peu importe), True, False - is_just: idem - """ - cnx = ndb.GetDBConnexion() - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) - req = """SELECT DISTINCT etudid, jour, matin FROM ABSENCES A -WHERE A.jour = %(date)s -""" - if is_abs is not None: - req += " AND A.estabs = %(is_abs)s" - if is_just is not None: - req += " AND A.estjust = %(is_just)s" - if not am: - req += " AND NOT matin " - if not pm: - req += " AND matin" - - cursor.execute(req, {"date": date, "is_just": is_just, "is_abs": is_abs}) - A = cursor.dictfetchall() - for a in A: - a["description"] = _get_abs_description(a, cursor=cursor) - return A - - -def list_abs_non_just_jour(date, am=True, pm=True) -> list[dict]: - "Liste des absences non justifiees ce jour" - cnx = ndb.GetDBConnexion() - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) - reqa = "" - if not am: - reqa += " AND NOT matin " - if not pm: - reqa += " AND matin " - req = ( - """SELECT etudid, jour, matin FROM ABSENCES A -WHERE A.estabs -AND A.jour = %(date)s -""" - + reqa - + """EXCEPT SELECT etudid, jour, matin FROM ABSENCES B -WHERE B.estjust AND B.jour = %(date)s""" - + reqa - ) - - cursor.execute(req, {"date": date}) - A = cursor.dictfetchall() - for a in A: - a["description"] = _get_abs_description(a, cursor=cursor) - return A - - -def list_abs_non_just(etudid, datedebut): - "Liste des absences NON justifiees (par ordre chronologique)" - cnx = ndb.GetDBConnexion() - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) - cursor.execute( - """SELECT ETUDID, JOUR, MATIN FROM ABSENCES A -WHERE A.ETUDID = %(etudid)s -AND A.estabs -AND A.jour >= %(datedebut)s -EXCEPT SELECT ETUDID, JOUR, MATIN FROM ABSENCES B -WHERE B.estjust -AND B.ETUDID = %(etudid)s -ORDER BY JOUR - """, - vars(), - ) - abs_list = cursor.dictfetchall() - for a in abs_list: - a["description"] = _get_abs_description(a, cursor=cursor) - return abs_list - - -def list_abs_just(etudid, datedebut): - "Liste des absences justifiees (par ordre chronologique)" - cnx = ndb.GetDBConnexion() - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) - cursor.execute( - """SELECT DISTINCT A.ETUDID, A.JOUR, A.MATIN FROM ABSENCES A, ABSENCES B -WHERE A.ETUDID = %(etudid)s -AND A.ETUDID = B.ETUDID -AND A.JOUR = B.JOUR AND A.MATIN = B.MATIN AND A.JOUR >= %(datedebut)s -AND A.ESTABS AND (A.ESTJUST OR B.ESTJUST) -ORDER BY A.JOUR - """, - vars(), - ) - A = cursor.dictfetchall() - for a in A: - a["description"] = _get_abs_description(a, cursor=cursor) - return A - - -def list_abs_justifs(etudid, datedebut, datefin=None, only_no_abs=False): - """Liste des justificatifs (avec ou sans absence relevée) à partir d'une date, - ou, si datefin spécifié, entre deux dates. - - Args: - etudid: - datedebut: date de début, iso, eg "2002-03-15" - datefin: date de fin, incluse, eg "2002-03-15" - only_no_abs: si vrai, seulement les justificatifs correspondant - aux jours sans absences relevées. - Returns: - Liste de dict absences - {'etudid': 'EID214', 'jour': datetime.date(2021, 1, 15), - 'matin': True, 'description': '' - } - """ - cnx = ndb.GetDBConnexion() - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) - req = """SELECT DISTINCT ETUDID, JOUR, MATIN FROM ABSENCES A -WHERE A.ETUDID = %(etudid)s -AND A.ESTJUST -AND A.JOUR >= %(datedebut)s""" - if datefin: - req += """AND A.JOUR <= %(datefin)s""" - if only_no_abs: - req += """ -EXCEPT SELECT ETUDID, JOUR, MATIN FROM ABSENCES B -WHERE B.estabs -AND B.ETUDID = %(etudid)s - """ - cursor.execute(req, vars()) - A = cursor.dictfetchall() - for a in A: - a["description"] = _get_abs_description(a, cursor=cursor) - - return A - - -def add_absence( - etudid, - jour, - matin, - estjust, - description=None, - moduleimpl_id=None, -): - "Ajoute une absence dans la bd" - if _isFarFutur(jour): - raise ScoValueError("date absence erronée ou trop loin dans le futur !") - estjust = _toboolean(estjust) - matin = _toboolean(matin) - cnx = ndb.GetDBConnexion() - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) - cursor.execute( - """ - INSERT into absences - (etudid, jour, estabs, estjust, matin, description, moduleimpl_id) - VALUES - (%(etudid)s, %(jour)s, true, %(estjust)s, %(matin)s, - %(description)s, %(moduleimpl_id)s - ) - """, - vars(), - ) - logdb( - cnx, - "AddAbsence", - etudid=etudid, - msg="JOUR=%(jour)s,MATIN=%(matin)s,ESTJUST=%(estjust)s,description=%(description)s,moduleimpl_id=%(moduleimpl_id)s" - % vars(), - ) - cnx.commit() - invalidate_abs_etud_date(etudid, jour) - sco_abs_notification.abs_notify(etudid, jour) - - -def add_justif(etudid, jour, matin, description=None): - "Ajoute un justificatif dans la base" - # unpublished - if _isFarFutur(jour): - raise ScoValueError("date justificatif trop loin dans le futur !") - matin = _toboolean(matin) - cnx = ndb.GetDBConnexion() - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) - cursor.execute( - """INSERT INTO absences (etudid, jour, estabs, estjust, matin, description) - VALUES (%(etudid)s, %(jour)s, FALSE, TRUE, %(matin)s, %(description)s) - """, - vars(), - ) - logdb( - cnx, - "AddJustif", - etudid=etudid, - msg="JOUR=%(jour)s,MATIN=%(matin)s" % vars(), - ) - cnx.commit() - invalidate_abs_etud_date(etudid, jour) - - -def add_abslist(abslist, moduleimpl_id=None): - for a in abslist: - etudid, jour, ampm = a.split(":") - if ampm == "am": - matin = 1 - elif ampm == "pm": - matin = 0 - else: - raise ValueError("invalid ampm !") - # ajoute abs si pas deja absent - if count_abs(etudid, jour, jour, matin, moduleimpl_id) == 0: - add_absence(etudid, jour, matin, 0, "", moduleimpl_id) - - -def annule_absence(etudid, jour, matin, moduleimpl_id=None): - """Annule une absence dans la base. N'efface pas l'éventuel justificatif. - Args: - etudid: - jour: date, chaîne iso, eg "1999-12-31" - matin: - moduleimpl_id: si spécifié, n'annule que pour ce module. - - Returns: - None - """ - # unpublished - matin = _toboolean(matin) - cnx = ndb.GetDBConnexion() - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) - req = "delete from absences where jour=%(jour)s and matin=%(matin)s and etudid=%(etudid)s and estabs" - if moduleimpl_id: - req += " and moduleimpl_id=%(moduleimpl_id)s" - cursor.execute(req, vars()) - logdb( - cnx, - "AnnuleAbsence", - etudid=etudid, - msg="JOUR=%(jour)s,MATIN=%(matin)s,moduleimpl_id=%(moduleimpl_id)s" % vars(), - ) - cnx.commit() - invalidate_abs_etud_date(etudid, jour) - - -def annule_justif(etudid, jour, matin): - "Annule un justificatif" - # unpublished - matin = _toboolean(matin) - cnx = ndb.GetDBConnexion() - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) - cursor.execute( - "delete from absences where jour=%(jour)s and matin=%(matin)s and etudid=%(etudid)s and ESTJUST AND NOT ESTABS", - vars(), - ) - cursor.execute( - "update absences set estjust=false where jour=%(jour)s and matin=%(matin)s and etudid=%(etudid)s", - vars(), - ) - logdb( - cnx, - "AnnuleJustif", - etudid=etudid, - msg="JOUR=%(jour)s,MATIN=%(matin)s" % vars(), - ) - cnx.commit() - invalidate_abs_etud_date(etudid, jour) - - -# ------ HTML Calendar functions (see YearTable function) - -# MONTH/DAY NAMES: - -MONTHNAMES = ( - "Janvier", - "Février", - "Mars", - "Avril", - "Mai", - "Juin", - "Juillet", - "Aout", - "Septembre", - "Octobre", - "Novembre", - "Décembre", -) - -MONTHNAMES_ABREV = ( - "Jan.", - "Fév.", - "Mars", - "Avr.", - "Mai ", - "Juin", - "Juil", - "Aout", - "Sept", - "Oct.", - "Nov.", - "Déc.", -) - -DAYNAMES = ("Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche") - -DAYNAMES_ABREV = ("L", "M", "M", "J", "V", "S", "D") - -# COLORS: - -WHITE = "#FFFFFF" -GRAY1 = "#EEEEEE" -GREEN3 = "#99CC99" -WEEKDAYCOLOR = GRAY1 -WEEKENDCOLOR = GREEN3 - - -def MonthTableHead(month): - color = WHITE - return """ - \n""" % ( - color, - MONTHNAMES_ABREV[month - 1], - ) - - -def MonthTableTail(): - return "
    %s
    \n" - - -def MonthTableBody( - month, year, events=[], halfday=0, trattributes="", work_saturday=False, pad_width=8 -): - firstday, nbdays = calendar.monthrange(year, month) - localtime = time.localtime() - current_weeknum = time.strftime("%U", localtime) - current_year = localtime[0] - T = [] - # cherche date du lundi de la 1ere semaine de ce mois - monday = ddmmyyyy("1/%d/%d" % (month, year)) - while monday.weekday != 0: - monday = monday.prev() - - if work_saturday: - weekend = ("D",) - else: - weekend = ("S", "D") - - if not halfday: - for d in range(1, nbdays + 1): - weeknum = time.strftime( - "%U", time.strptime("%d/%d/%d" % (d, month, year), "%d/%m/%Y") - ) - day = DAYNAMES_ABREV[(firstday + d - 1) % 7] - if day in weekend: - bgcolor = WEEKENDCOLOR - weekclass = "wkend" - attrs = "" - else: - bgcolor = WEEKDAYCOLOR - weekclass = "wk" + str(monday).replace("/", "_") - attrs = trattributes - color = None - legend = "" - href = "" - descr = "" - # event this day ? - # each event is a tuple (date, text, color, href) - # where date is a string in ISO format (yyyy-mm-dd) - for ev in events: - ev_year = int(ev[0][:4]) - ev_month = int(ev[0][5:7]) - ev_day = int(ev[0][8:10]) - if year == ev_year and month == ev_month and ev_day == d: - if ev[1]: - legend = ev[1] - if ev[2]: - color = ev[2] - if ev[3]: - href = ev[3] - if len(ev) > 4 and ev[4]: - descr = ev[4] - # - cc = [] - if color is not None: - cc.append('' % color) - else: - cc.append('') - - if href: - href = 'href="%s"' % href - if descr: - descr = 'title="%s"' % html.escape(descr, quote=True) - if href or descr: - cc.append("" % (href, descr)) - - if legend or d == 1: - if pad_width is not None: - n = pad_width - len(legend) # pad to 8 cars - if n > 0: - legend = ( - " " * (n // 2) + legend + " " * ((n + 1) // 2) - ) - else: - legend = " " # empty cell - cc.append(legend) - if href or descr: - cc.append("") - cc.append("") - cell = "".join(cc) - if day == "D": - monday = monday.next_day(7) - if ( - weeknum == current_weeknum - and current_year == year - and weekclass != "wkend" - ): - weekclass += " currentweek" - T.append( - '%d%s%s' - % (bgcolor, weekclass, attrs, d, day, cell) - ) - else: - # Calendar with 2 cells / day - for d in range(1, nbdays + 1): - weeknum = time.strftime( - "%U", time.strptime("%d/%d/%d" % (d, month, year), "%d/%m/%Y") - ) - day = DAYNAMES_ABREV[(firstday + d - 1) % 7] - if day in weekend: - bgcolor = WEEKENDCOLOR - weekclass = "wkend" - attrs = "" - else: - bgcolor = WEEKDAYCOLOR - weekclass = "wk" + str(monday).replace("/", "_") - attrs = trattributes - if ( - weeknum == current_weeknum - and current_year == year - and weekclass != "wkend" - ): - weeknum += " currentweek" - - if day == "D": - monday = monday.next_day(7) - T.append( - '%d%s' - % (bgcolor, weekclass, attrs, d, day) - ) - cc = [] - for morning in (True, False): - color = None - legend = "" - href = "" - descr = "" - for ev in events: - ev_year = int(ev[0][:4]) - ev_month = int(ev[0][5:7]) - ev_day = int(ev[0][8:10]) - if ev[4] is not None: - ev_half = int(ev[4]) - else: - ev_half = 0 - if ( - year == ev_year - and month == ev_month - and ev_day == d - and morning == ev_half - ): - if ev[1]: - legend = ev[1] - if ev[2]: - color = ev[2] - if ev[3]: - href = ev[3] - if len(ev) > 5 and ev[5]: - descr = ev[5] - # - if color is not None: - cc.append('' % (color)) - else: - cc.append('') - if href: - href = 'href="%s"' % href - if descr: - descr = 'title="%s"' % html.escape(descr, quote=True) - if href or descr: - cc.append("" % (href, descr)) - if legend or d == 1: - n = 3 - len(legend) # pad to 3 cars - if n > 0: - legend = ( - " " * (n // 2) + legend + " " * ((n + 1) // 2) - ) - else: - legend = "   " # empty cell - cc.append(legend) - if href or descr: - cc.append("") - cc.append("\n") - T.append("".join(cc) + "") - return "\n".join(T) - - -# -------------------------------------------------------------------- -# -# Cache absences -# -# On cache (via REDIS ou autre, voir sco_cache.py) les _nombres_ d'absences -# (justifiées et non justifiées) de chaque etudiant dans un semestre donné. -# Le cache peut être invalidé soit par étudiant/semestre, soit pour tous -# les étudiant d'un semestre. -# -# On ne cache pas la liste des absences car elle est rarement utilisée (calendrier, -# absences à une date donnée). -# -# -------------------------------------------------------------------- - - -def get_abs_count(etudid, sem): - """Les comptes d'absences de cet étudiant dans ce semestre: - tuple (nb abs non justifiées, nb abs justifiées) - Utilise un cache. - """ - return get_abs_count_in_interval(etudid, sem["date_debut_iso"], sem["date_fin_iso"]) - - -def get_abs_count_in_interval(etudid, date_debut_iso, date_fin_iso): - """Les comptes d'absences de cet étudiant entre ces deux dates, incluses: - tuple (nb abs, nb abs justifiées) - Utilise un cache. - """ - key = str(etudid) + "_" + date_debut_iso + "_" + date_fin_iso - r = sco_cache.AbsSemEtudCache.get(key) - if not r: - nb_abs = count_abs( - etudid=etudid, - debut=date_debut_iso, - fin=date_fin_iso, - ) - nb_abs_just = count_abs_just( - etudid=etudid, - debut=date_debut_iso, - fin=date_fin_iso, - ) - r = (nb_abs, nb_abs_just) - ans = sco_cache.AbsSemEtudCache.set(key, r) - if not ans: - log("warning: get_abs_count failed to cache") - return r - - -def invalidate_abs_count(etudid, sem): - """Invalidate (clear) cached counts""" - date_debut = sem["date_debut_iso"] - date_fin = sem["date_fin_iso"] - key = str(etudid) + "_" + date_debut + "_" + date_fin - sco_cache.AbsSemEtudCache.delete(key) - - -def invalidate_abs_count_sem(sem): - """Invalidate (clear) cached abs counts for all the students of this semestre""" - inscriptions = ( - sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits( - sem["formsemestre_id"] - ) - ) - for ins in inscriptions: - invalidate_abs_count(ins["etudid"], sem) - - -def invalidate_abs_etud_date(etudid, date): # was invalidateAbsEtudDate - """Doit etre appelé à chaque modification des absences - pour cet étudiant et cette date. - Invalide cache absence et caches semestre - date: date au format ISO - """ - from app.scodoc import sco_compute_moy - - # Semestres a cette date: - etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] - sems = [ - sem - for sem in etud["sems"] - if sem["date_debut_iso"] <= date and sem["date_fin_iso"] >= date - ] - - # Invalide les PDF et les absences: - for sem in sems: - # Inval cache bulletin et/ou note_table - if sco_compute_moy.formsemestre_expressions_use_abscounts( - sem["formsemestre_id"] - ): - # certaines formules utilisent les absences - pdfonly = False - else: - # efface toujours le PDF car il affiche en général les absences - pdfonly = True - - sco_cache.invalidate_formsemestre( - formsemestre_id=sem["formsemestre_id"], pdfonly=pdfonly - ) - - # Inval cache compteurs absences: - invalidate_abs_count_sem(sem) diff --git a/app/scodoc/sco_abs_notification.py b/app/scodoc/sco_abs_notification.py index 50c50ae45..c29dba2d0 100644 --- a/app/scodoc/sco_abs_notification.py +++ b/app/scodoc/sco_abs_notification.py @@ -268,10 +268,12 @@ def abs_notification_message( """ 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 : - 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["nbabsjust"] = nbabsjust @@ -287,7 +289,7 @@ def abs_notification_message( log("abs_notification_message: empty template, not sending message") 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.body = txt return msg diff --git a/app/scodoc/sco_archives_etud.py b/app/scodoc/sco_archives_etud.py index 6f174f15d..19dbf70ac 100644 --- a/app/scodoc/sco_archives_etud.py +++ b/app/scodoc/sco_archives_etud.py @@ -138,21 +138,18 @@ def etud_upload_file_form(etudid): """Page with a form to choose and upload a file, with a description.""" # check permission if not can_edit_etud_archive(current_user): - raise AccessDenied("opération non autorisée pour %s" % current_user) - etuds = sco_etud.get_etud_info(filled=True) - if not etuds: - raise ScoValueError("étudiant inexistant") - etud = etuds[0] + raise AccessDenied(f"opération non autorisée pour {current_user}") + etud = Identite.get_etud(etudid) + H = [ 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}", ), - """

    Chargement d'un document associé à %(nomprenom)s

    - """ - % etud, - """

    Le fichier ne doit pas dépasser %sMo.

    - """ - % (scu.CONFIG.ETUD_MAX_FILE_SIZE // (1024 * 1024)), + f"""

    Chargement d'un document associé à {etud.nomprenom}

    + +

    Le fichier ne doit pas dépasser { + scu.CONFIG.ETUD_MAX_FILE_SIZE // (1024 * 1024)}Mo.

    + """, ] tf = TrivialFormulator( request.base_url, @@ -176,20 +173,13 @@ def etud_upload_file_form(etudid): if tf[0] == 0: return "\n".join(H) + tf[1] + html_sco_header.sco_footer() elif tf[0] == -1: - return flask.redirect( - url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid) - ) - else: - data = tf[2]["datafile"].read() - descr = tf[2]["description"] - filename = tf[2]["datafile"].filename - 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) - ) + return flask.redirect(etud.url_fiche()) + data = tf[2]["datafile"].read() + descr = tf[2]["description"] + filename = tf[2]["datafile"].filename + etud_archive_id = (etudid,) + _store_etud_file_to_new_archive(etud_archive_id, data, filename, description=descr) + return flask.redirect(etud.url_fiche()) def _store_etud_file_to_new_archive( @@ -209,23 +199,20 @@ def etud_delete_archive(etudid, archive_name, dialog_confirmed=False): # check permission if not can_edit_etud_archive(current_user): raise AccessDenied(f"opération non autorisée pour {current_user}") - etuds = sco_etud.get_etud_info(filled=True) - if not etuds: - raise ScoValueError("étudiant inexistant") - etud = etuds[0] - etud_archive_id = etud["etudid"] + etud = Identite.get_etud(etudid) + etud_archive_id = etudid 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: return scu.confirm_dialog( - """

    Confirmer la suppression des fichiers ?

    -

    Fichier associé le %s à l'étudiant %s

    -

    La suppression sera définitive.

    """ - % ( - ETUDS_ARCHIVER.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M"), - etud["nomprenom"], - ), + f"""

    Confirmer la suppression des fichiers ?

    +

    Fichier associé le { + ETUDS_ARCHIVER.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M") + } à l'étudiant {etud.nomprenom} +

    +

    La suppression sera définitive.

    + """, dest_url="", cancel_url=url_for( "scolar.fiche_etud", @@ -236,22 +223,17 @@ def etud_delete_archive(etudid, archive_name, dialog_confirmed=False): 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") - return flask.redirect( - url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid) - ) + return flask.redirect(etud.url_fiche()) def etud_get_archived_file(etudid, archive_name, filename): """Send file to client.""" - etuds = sco_etud.get_etud_info(etudid=etudid, filled=True) - if not etuds: - raise ScoValueError("étudiant inexistant") - etud = etuds[0] - etud_archive_id = etud["etudid"] + etud = Identite.get_etud(etudid) + etud_archive_id = etud.id 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 ) diff --git a/app/scodoc/sco_etud.py b/app/scodoc/sco_etud.py index 7a6a49e93..4cfaf5625 100644 --- a/app/scodoc/sco_etud.py +++ b/app/scodoc/sco_etud.py @@ -735,17 +735,18 @@ def fill_etuds_info(etuds: list[dict], add_admission=True): 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: """Dict avec les informations sur les semestres passés et courant. { "sems" : , # trie les semestres par date de debut, le plus recent d'abord "ins" : , "cursem" : , - "inscription" : , - "inscriptionstr" : , - "inscription_formsemestre_id" : , - "etatincursem" : , - "situation" : , + "inscription" : , # cursem["titremois"] + "inscriptionstr" : , # "Inscrit en " + cursem["titremois"] + "inscription_formsemestre_id" : , # cursem["formsemestre_id"] + "etatincursem" : , # curi["etat"] + "situation" : , # descr_situation_etud(etudid, ne) } """ from app.scodoc import sco_formsemestre diff --git a/app/scodoc/sco_evaluation_db.py b/app/scodoc/sco_evaluation_db.py index e0668eb85..35d90a96a 100644 --- a/app/scodoc/sco_evaluation_db.py +++ b/app/scodoc/sco_evaluation_db.py @@ -162,50 +162,48 @@ def do_evaluation_get_all_notes( 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) (published) """ - evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id) - redirect = int(redirect) + evaluation = Evaluation.get_evaluation(evaluation_id) # access: can change eval ? if not evaluation.moduleimpl.can_edit_evaluation(current_user): raise AccessDenied( 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 if after not in (0, 1): raise ValueError('invalid value for "after"') - mod_evals = get_evaluations_dict({"moduleimpl_id": e["moduleimpl_id"]}) - if len(mod_evals) > 1: - idx = [p["evaluation_id"] for p in mod_evals].index(evaluation_id) + + Evaluation.moduleimpl_evaluation_renumber( + 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 if after == 0 and idx > 0: - neigh = mod_evals[idx - 1] - elif after == 1 and idx < len(mod_evals) - 1: - neigh = mod_evals[idx + 1] + neigh = mod_evaluations[idx - 1] + elif after == 1 and idx < len(mod_evaluations) - 1: + neigh = mod_evaluations[idx + 1] if neigh: # - if neigh["numero"] == e["numero"]: + if neigh.numero == evaluation.numero: log("Warning: moduleimpl_evaluation_move: forcing renumber") Evaluation.moduleimpl_evaluation_renumber( evaluation.moduleimpl, only_if_unumbered=False ) else: # swap numero with neighbor - e["numero"], neigh["numero"] = neigh["numero"], e["numero"] - do_evaluation_edit(e) - do_evaluation_edit(neigh) + evaluation.numero, neigh.numero = neigh.numero, evaluation.numero + db.session.add(evaluation) + db.session.add(neigh) + db.session.commit() # redirect to moduleimpl page: - if redirect: - return flask.redirect( - url_for( - "notes.moduleimpl_status", - scodoc_dept=g.scodoc_dept, - moduleimpl_id=e["moduleimpl_id"], - ) + return flask.redirect( + url_for( + "notes.moduleimpl_status", + scodoc_dept=g.scodoc_dept, + moduleimpl_id=evaluation.moduleimpl.id, ) + ) diff --git a/app/scodoc/sco_evaluation_edit.py b/app/scodoc/sco_evaluation_edit.py index 9b5c716fd..07718708c 100644 --- a/app/scodoc/sco_evaluation_edit.py +++ b/app/scodoc/sco_evaluation_edit.py @@ -37,7 +37,7 @@ from flask_login import current_user from flask import request 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 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 sco_cache from app.scodoc import sco_evaluations -from app.scodoc import sco_moduleimpl from app.scodoc import sco_preferences @@ -58,27 +57,20 @@ def evaluation_create_form( page_title="Évaluation", ): "Formulaire création/édition d'une évaluation (pas de ses notes)" + evaluation: Evaluation 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: raise ScoValueError("Cette évaluation n'existe pas ou plus !") moduleimpl_id = evaluation.moduleimpl_id # - modimpl: ModuleImpl = ( - ModuleImpl.query.filter_by(id=moduleimpl_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"] + modimpl = ModuleImpl.get_modimpl(moduleimpl_id) + formsemestre_id = modimpl.formsemestre_id formsemestre = modimpl.formsemestre + module: Module = modimpl.module sem_ues = formsemestre.get_ues(with_sport=False) - is_malus = mod["module_type"] == ModuleType.MALUS - is_apc = mod["module_type"] in (ModuleType.RESSOURCE, ModuleType.SAE) + is_malus = module.module_type == ModuleType.MALUS + is_apc = module.module_type in (ModuleType.RESSOURCE, ModuleType.SAE) preferences = sco_preferences.SemPreferences(formsemestre.id) can_edit_poids = not preferences["but_disable_edit_poids_evaluations"] min_note_max = scu.NOTES_PRECISION # le plus petit bareme possible @@ -98,9 +90,11 @@ def evaluation_create_form( # création nouvel if moduleimpl_id is None: raise ValueError("missing moduleimpl_id parameter") + numeros = [(e.numero or 0) for e in modimpl.evaluations] initvalues = { - "note_max": 20, "jour": time.strftime("%d/%m/%Y", time.localtime()), + "note_max": 20, + "numero": (max(numeros) + 1) if numeros else 0, "publish_incomplete": is_malus, "visibulletin": 1, } @@ -128,18 +122,7 @@ def evaluation_create_form( min_note_max_str = scu.fmt_note(min_note_max) else: min_note_max_str = "0" - # - H = [ - f"""

    {action} en - {scu.MODULE_TYPE_NAMES[mod["module_type"]]} {mod["code"] or "module sans code"} {mod["titre"]} {link}

    - """ - ] - 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) vals = scu.get_request_args() # @@ -164,6 +147,7 @@ def evaluation_create_form( ("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}), ("formsemestre_id", {"default": formsemestre_id, "input_type": "hidden"}), ("moduleimpl_id", {"default": moduleimpl_id, "input_type": "hidden"}), + ("numero", {"default": initvalues["numero"], "input_type": "hidden"}), ( "jour", { @@ -323,6 +307,16 @@ def evaluation_create_form( dest_url = url_for( "notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id ) + H = [ + f"""

    {action} en + {scu.MODULE_TYPE_NAMES[module.module_type]} {module.code or "module sans code"} { + module.titre or module.abbrev or "(sans titre)" + } {link}

    + """ + ] if tf[0] == 0: head = html_sco_header.sco_header(page_title=page_title) return ( diff --git a/app/scodoc/sco_evaluations.py b/app/scodoc/sco_evaluations.py index 487cf9ea4..20ae23567 100644 --- a/app/scodoc/sco_evaluations.py +++ b/app/scodoc/sco_evaluations.py @@ -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 evals: list of mappings (etats), 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. """ nb_evals_completes, nb_evals_en_cours, nb_evals_vides = 0, 0, 0 dates = [] - for e in evals: + for e in etat_evals: if e["etat"]["evalcomplete"]: nb_evals_completes += 1 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): """état des évaluations dans ce module""" - evals = nt.get_mod_evaluation_etat_list(modimpl) - etat = _summarize_evals_etats(evals) + etat_evals = nt.get_mod_evaluation_etat_list(modimpl) + etat = _summarize_evals_etats(etat_evals) # Il y a-t-il des notes en attente dans ce module ? etat["attente"] = nt.modimpls_results[modimpl.id].en_attente return etat diff --git a/app/scodoc/sco_formsemestre.py b/app/scodoc/sco_formsemestre.py index 6659109d1..437969eb2 100644 --- a/app/scodoc/sco_formsemestre.py +++ b/app/scodoc/sco_formsemestre.py @@ -241,7 +241,7 @@ def do_formsemestre_create(args, silent=False): write_formsemestre_etapes(args) if args["responsables"]: args["formsemestre_id"] = formsemestre_id - write_formsemestre_responsables(args) + _write_formsemestre_responsables(args) # create default partition partition_id = sco_groups.partition_create( @@ -275,7 +275,7 @@ def do_formsemestre_edit(sem, cnx=None, **kw): _formsemestreEditor.edit(cnx, sem, **kw) write_formsemestre_etapes(sem) - write_formsemestre_responsables(sem) + _write_formsemestre_responsables(sem) sco_cache.invalidate_formsemestre( 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] -def write_formsemestre_responsables(sem): - return _write_formsemestre_aux(sem, "responsables", "responsable_id") +def _write_formsemestre_responsables(sem): # TODO old, à ré-écrire avec models + 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 @@ -362,10 +366,11 @@ def read_formsemestre_etapes(formsemestre_id): # OBSOLETE 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") +# TODO old, à ré-écrire avec models def _write_formsemestre_aux(sem, fieldname, valuename): """fieldname: 'etapes' ou 'responsables' valuename: 'etape_apo' ou 'responsable_id' diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index 57cafe6c7..73c6180ff 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -857,17 +857,20 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N tf[2]["bul_hide_xml"] = True # remap les identifiants de responsables: 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"] = [] for field in resp_fields: if tf[2][field]: tf[2]["responsables"].append(tf[2][field]) for module_id in tf[2]["tf-checked"]: - mod_resp_id = User.get_user_id_from_nomplogin(tf[2][module_id]) - if mod_resp_id is None: + mod_resp = User.get_user_from_nomplogin(tf[2][module_id]) + if mod_resp is None: # Si un module n'a pas de responsable (ou inconnu), # l'affecte au 1er directeur des etudes: mod_resp_id = tf[2]["responsable_id"] + else: + mod_resp_id = mod_resp.id tf[2][module_id] = mod_resp_id # etapes: @@ -1227,9 +1230,12 @@ def formsemestre_clone(formsemestre_id): "formsemestre_status?formsemestre_id=%s" % formsemestre_id ) 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( formsemestre_id, - User.get_user_id_from_nomplogin(tf[2]["responsable_id"]), + resp.id, tf[2]["date_debut"], tf[2]["date_fin"], clone_evaluations=tf[2]["clone_evaluations"], @@ -1273,29 +1279,24 @@ def do_formsemestre_clone( log(f"created formsemestre {formsemestre_id}") formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id) # 2- create moduleimpls - mods_orig = sco_moduleimpl.moduleimpl_list(formsemestre_id=orig_formsemestre_id) - for mod_orig in mods_orig: - args = mod_orig.copy() + modimpl_orig: ModuleImpl + for modimpl_orig in formsemestre_orig.modimpls: + args = modimpl_orig.to_dict(with_module=False) args["formsemestre_id"] = formsemestre_id - mid = sco_moduleimpl.do_moduleimpl_create(args) - # copy notes_modules_enseignants - ens = sco_moduleimpl.do_ens_list( - args={"moduleimpl_id": mod_orig["moduleimpl_id"]} - ) - for e in ens: - args = e.copy() - args["moduleimpl_id"] = mid - sco_moduleimpl.do_ens_create(args) + modimpl_new = ModuleImpl.create_from_dict(args) + db.session.flush() + # copy enseignants + for ens in modimpl_orig.enseignants: + modimpl_new.enseignants.append(ens) + db.session.add(modimpl_new) # optionally, copy evaluations if clone_evaluations: - for e in Evaluation.query.filter_by( - moduleimpl_id=mod_orig["moduleimpl_id"] - ): + for e in Evaluation.query.filter_by(moduleimpl_id=modimpl_orig.id): # copie en enlevant la date new_eval = e.clone( 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 new_eval.set_ue_poids_dict(e.get_ue_poids_dict()) db.session.commit() diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 5e42ac75f..a3941ab33 100755 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -1262,6 +1262,7 @@ def formsemestre_tableau_modules( etat["nb_evals_completes"] > 0 and etat["nb_evals_en_cours"] == 0 and etat["nb_evals_vides"] == 0 + and not etat["attente"] ): H.append(f'') else: @@ -1315,6 +1316,7 @@ def formsemestre_tableau_modules( if nb_evals != 0: H.append( f"""{nb_evals} prévues, {etat["nb_evals_completes"]} ok""" ) @@ -1325,11 +1327,11 @@ def formsemestre_tableau_modules( etat["nb_evals_en_cours"] } en cours""" ) - if etat["attente"]: - H.append( - f""" [en attente]""" - ) + if etat["attente"]: + H.append( + f""" [en attente]""" + ) elif mod.module_type == ModuleType.MALUS: nb_malus_notes = sum( e["etat"]["nb_notes"] for e in nt.get_mod_evaluation_etat_list(modimpl) diff --git a/app/scodoc/sco_moduleimpl.py b/app/scodoc/sco_moduleimpl.py index 197180f1b..9e3f48cb8 100644 --- a/app/scodoc/sco_moduleimpl.py +++ b/app/scodoc/sco_moduleimpl.py @@ -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 from app import db from app.models import Formation from app.scodoc import scolog -from app.scodoc import sco_formsemestre from app.scodoc import sco_cache -import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb -from app.scodoc.sco_permissions import Permission -from app.scodoc.sco_exceptions import ScoValueError, AccessDenied +from app.scodoc.sco_exceptions import ScoValueError # --- Gestion des "Implémentations de Modules" # 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): "create a moduleimpl" @@ -108,9 +95,6 @@ def moduleimpl_list(moduleimpl_id=None, formsemestre_id=None, module_id=None): args = locals() cnx = ndb.GetDBConnexion() 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 @@ -342,65 +326,3 @@ def do_moduleimpl_inscrit_etuds(moduleimpl_id, formsemestre_id, etudids, reset=F sco_cache.invalidate_formsemestre( formsemestre_id=formsemestre_id ) # > 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 diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py index e97716b6a..ca14145d6 100644 --- a/app/scodoc/sco_moduleimpl_status.py +++ b/app/scodoc/sco_moduleimpl_status.py @@ -209,15 +209,10 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): # sem_locked = not formsemestre.etat can_edit_evals = ( - sco_permissions_check.can_edit_notes( - 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) + modimpl.can_edit_notes(current_user, allow_ens=formsemestre.ens_can_edit_eval) 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() # module_resp = db.session.get(User, modimpl.responsable_id) @@ -244,7 +239,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): ({module_resp.user_name}) """, ] - if modimpl.can_change_ens_by(current_user): + if modimpl.can_change_responsable(current_user): H.append( f"""""") H.append(", ".join([u.get_nomprenom() for u in modimpl.enseignants])) H.append("""""") - try: - sco_moduleimpl.can_change_ens(moduleimpl_id) + if modimpl.can_change_ens(raise_exc=False): H.append( - """modifier les enseignants""" - % moduleimpl_id + f"""modifier les enseignants""" ) - except: - pass + H.append("""""") # 2ieme ligne: Semestre, Coef diff --git a/app/scodoc/sco_permissions_check.py b/app/scodoc/sco_permissions_check.py index 2d9e39c6a..e12e32ce2 100644 --- a/app/scodoc/sco_permissions_check.py +++ b/app/scodoc/sco_permissions_check.py @@ -8,50 +8,11 @@ from flask_login import current_user from app import db from app.auth.models import User -from app.models import FormSemestre import app.scodoc.notesdb as ndb from app.scodoc.sco_permissions import Permission from app.scodoc import html_sco_header from app.scodoc import sco_etud 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): diff --git a/app/scodoc/sco_placement.py b/app/scodoc/sco_placement.py index db5080981..a9e25344f 100644 --- a/app/scodoc/sco_placement.py +++ b/app/scodoc/sco_placement.py @@ -48,6 +48,7 @@ from wtforms import ( HiddenField, SelectMultipleField, ) +from app.models import ModuleImpl import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app import ScoValueError @@ -58,7 +59,6 @@ from app.scodoc import sco_evaluation_db from app.scodoc import sco_excel from app.scodoc.sco_excel import ScoExcelBook, COLORS from app.scodoc import sco_formsemestre -from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_groups from app.scodoc import sco_moduleimpl from app.scodoc import sco_permissions_check @@ -247,6 +247,8 @@ class PlacementRunner: # gr_title = sco_groups.listgroups_abbrev(d['groups']) self.current_user = current_user 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( moduleimpl_id=self.moduleimpl_id )[0] @@ -280,9 +282,7 @@ class PlacementRunner: def check_placement(self): """Vérifie que l'utilisateur courant a le droit d'édition sur les notes""" # Check access (admin, respformation, and responsable_id) - return sco_permissions_check.can_edit_notes( - self.current_user, self.moduleimpl_id - ) + return self.moduleimpl.can_edit_notes(self.current_user) def exec_placement(self): """Excéute l'action liée au formulaire""" diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py index 1cf207d54..b902a88ad 100644 --- a/app/scodoc/sco_saisie_notes.py +++ b/app/scodoc/sco_saisie_notes.py @@ -198,8 +198,8 @@ def do_evaluation_upload_xls(): evaluation_id = int(vals["evaluation_id"]) comment = vals["comment"] evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id) - # Check access (admin, respformation, and responsable_id) - if not sco_permissions_check.can_edit_notes(current_user, evaluation.moduleimpl_id): + # Check access (admin, respformation, responsable_id, ens) + if not evaluation.moduleimpl.can_edit_notes(current_user): raise AccessDenied(f"Modification des notes impossible pour {current_user}") # 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 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}") # Convert and check value L, invalids, _, _, _ = _check_notes([(etud.id, value)], evaluation) @@ -336,7 +336,7 @@ def do_evaluation_set_missing( modimpl = evaluation.moduleimpl # Check access # (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}") # 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" evaluation = Evaluation.query.get_or_404(evaluation_id) - if sco_permissions_check.can_edit_notes( - current_user, evaluation.moduleimpl_id, allow_ens=False - ): + if evaluation.moduleimpl.can_edit_notes(current_user, allow_ens=False): # On a le droit de modifier toutes les notes # recupere les etuds ayant une note notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id) - elif sco_permissions_check.can_edit_notes( - current_user, evaluation.moduleimpl_id, allow_ens=True - ): + elif evaluation.moduleimpl.can_edit_notes(current_user, allow_ens=True): # Enseignant associé au module: ne peut supprimer que les notes qu'il a saisi notes_db = sco_evaluation_db.do_evaluation_get_all_notes( 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) moduleimpl_id = evaluation.moduleimpl.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 ( html_sco_header.sco_header() + f""" @@ -813,9 +809,7 @@ def saisie_notes_tableur(evaluation_id, group_ids=()): # H.append("""

    Autres opérations

      """) - if sco_permissions_check.can_edit_notes( - current_user, moduleimpl_id, allow_ens=False - ): + if evaluation.moduleimpl.can_edit_notes(current_user, allow_ens=False): H.append( f"""
    • @@ -967,7 +961,7 @@ def saisie_notes(evaluation_id: int, group_ids: list = None): ) # Check access # (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""" {html_sco_header.sco_header()}

      Modification des notes impossible pour {current_user.user_name}

      @@ -1361,7 +1355,7 @@ def save_notes( _external=True, ) # 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") # valid_notes, _, _, _, _ = _check_notes(notes, evaluation) diff --git a/app/views/assiduites.py b/app/views/assiduites.py index 019906581..d4762a586 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -1834,9 +1834,9 @@ def signale_evaluation_abs(etudid: int = None, evaluation_id: int = None): ) # Sinon on créé l'assiduité - + assiduite_unique: Assiduite | None = None try: - assiduite_unique: Assiduite = Assiduite.create_assiduite( + assiduite_unique = Assiduite.create_assiduite( etud=etud, date_debut=scu.localize_datetime(evaluation.date_debut), 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", ) raise ScoValueError(msg, dest) from exc - - db.session.add(assiduite_unique) - db.session.commit() + if assiduite_unique is not None: + db.session.add(assiduite_unique) + db.session.commit() # on flash puis on revient sur la page de l'évaluation flash("L'absence a bien été créée") diff --git a/app/views/notes.py b/app/views/notes.py index 1201747ed..77a92e7d0 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -539,9 +539,9 @@ def ue_sharing_code(): return sco_edit_ue.ue_sharing_code( ue_code=ue_code, ue_id=None if ((ue_id is None) or ue_id == "") else int(ue_id), - hide_ue_id=None - if ((hide_ue_id is None) or hide_ue_id == "") - else int(hide_ue_id), + hide_ue_id=( + None if ((hide_ue_id is None) or hide_ue_id == "") else int(hide_ue_id) + ), ) @@ -961,12 +961,15 @@ def formsemestre_custommenu_edit(formsemestre_id): @scodoc7func def edit_enseignants_form(moduleimpl_id): "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( - 'Enseignants du module %s' - % (moduleimpl_id, M["module"]["titre"]), - page_title="Enseignants du module %s" % M["module"]["titre"], + f"""Enseignants du module {modimpl.module.titre or modimpl.module.code}""", + page_title=f"Enseignants du module {modimpl.module.titre or modimpl.module.code}", javascripts=["libjs/AutoSuggest.js"], cssstyles=["css/autosuggest_inquisitor.css"], bodyOnLoad="init_tf_form('')", @@ -981,21 +984,18 @@ def edit_enseignants_form(moduleimpl_id): allowed_user_names = list(uid2display.values()) H = [ - "
      • %s (responsable)
      • " - % uid2display.get(M["responsable_id"], M["responsable_id"]) + f"""
        • { + uid2display.get(modimpl.responsable_id, modimpl.responsable_id) + } (responsable)
        • """ ] - for ens in M["ens"]: - u = db.session.get(User, ens["ens_id"]) - if u: - nom = u.get_nomcomplet() - else: - nom = "? (compte inconnu)" + u: User + for u in modimpl.enseignants: H.append( f""" -
        • {nom} (supprimer)
        • """ ) @@ -1006,7 +1006,7 @@ def edit_enseignants_form(moduleimpl_id):

          Pour changer le responsable du module, passez par la page "Modification du semestre", accessible uniquement au responsable de la formation (chef de département)

          @@ -1053,24 +1053,24 @@ def edit_enseignants_form(moduleimpl_id): ) ) else: - ens_id = User.get_user_id_from_nomplogin(tf[2]["ens_id"]) - if not ens_id: + ens = User.get_user_from_nomplogin(tf[2]["ens_id"]) + if ens is None: H.append( '

          Pour ajouter un enseignant, choisissez un nom dans le menu

          ' ) else: # et qu'il n'est pas deja: if ( - ens_id in [x["ens_id"] for x in M["ens"]] - or ens_id == M["responsable_id"] + ens.id in (x.id for x in modimpl.enseignants) + or ens.id == modimpl.responsable_id ): H.append( - f"""

          Enseignant {ens_id} déjà dans la liste !

          """ + f"""

          Enseignant {ens.user_name} déjà dans la liste !

          """ ) else: - sco_moduleimpl.do_ens_create( - {"moduleimpl_id": moduleimpl_id, "ens_id": ens_id} - ) + modimpl.enseignants.append(ens) + db.session.add(modimpl) + db.session.commit() return flask.redirect( url_for( "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 """ 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 = [ html_sco_header.html_sem_header( - f"""Modification du responsable du module {modimpl.module.titre or ""}""", @@ -1156,8 +1156,8 @@ def edit_moduleimpl_resp(moduleimpl_id: int): ) ) else: - responsable_id = User.get_user_id_from_nomplogin(tf[2]["responsable_id"]) - if not responsable_id: + responsable = User.get_user_from_nomplogin(tf[2]["responsable_id"]) + if not responsable: # presque impossible: tf verifie les valeurs (mais qui peuvent changer entre temps) return flask.redirect( 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.commit() flash("Responsable modifié") @@ -1192,8 +1192,6 @@ def view_module_abs(moduleimpl_id, fmt="html"): .filter_by(dept_id=g.scodoc_dept_id) ).first_or_404() - debut_sem = modimpl.formsemestre.date_debut - fin_sem = modimpl.formsemestre.date_fin inscrits: list[Identite] = sorted( [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) # resp. de modules et charges de TD - sem_ens: dict[ - int, list[ModuleImpl] - ] = {} # uid : { "mods" : liste des modimpls, ... } + # uid : { "mods" : liste des modimpls, ... } + sem_ens: dict[int, list[ModuleImpl]] = {} modimpls = formsemestre.modimpls_sorted for modimpl in modimpls: 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 """ - M, _ = sco_moduleimpl.can_change_ens(moduleimpl_id) + modimpl = ModuleImpl.get_modimpl(moduleimpl_id) + modimpl.can_change_ens(raise_exc=True) # search ens_id - ok = False - for ens in M["ens"]: - if ens["ens_id"] == ens_id: - ok = True + ens: User | None = None + for ens in modimpl.enseignants: + if ens.id == ens_id: break - if not ok: + if ens is None: raise ScoValueError(f"invalid ens_id ({ens_id})") - ndb.SimpleQuery( - """DELETE FROM notes_modules_enseignants - WHERE moduleimpl_id = %(moduleimpl_id)s - AND ens_id = %(ens_id)s - """, - {"moduleimpl_id": moduleimpl_id, "ens_id": ens_id}, - ) + modimpl.enseignants.remove(ens) + db.session.commit() return flask.redirect( url_for( "notes.edit_enseignants_form", @@ -1399,18 +1391,6 @@ def edit_enseignants_form_delete(moduleimpl_id, ens_id: int): # --- 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( "/do_formsemestre_inscription_list", sco_formsemestre_inscriptions.do_formsemestre_inscription_list, diff --git a/sco_version.py b/sco_version.py index c079c96dc..8c6293055 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.92" +SCOVERSION = "9.6.934" SCONAME = "ScoDoc" diff --git a/tests/api/test_api_formsemestre.py b/tests/api/test_api_formsemestre.py index 23b0f4990..429618b81 100644 --- a/tests/api/test_api_formsemestre.py +++ b/tests/api/test_api_formsemestre.py @@ -619,7 +619,6 @@ def test_formsemestre_programme(api_headers): assert verify_fields(sae, MODIMPL_FIELDS) -@pytest.mark.skip # XXX WIP def test_etat_evals(api_headers): # voir si on maintient cette route ? """ Route : /formsemestre//etat_evals diff --git a/tests/unit/test_formations.py b/tests/unit/test_formations.py index 4e5a5e69f..ee017b538 100644 --- a/tests/unit/test_formations.py +++ b/tests/unit/test_formations.py @@ -375,7 +375,6 @@ def test_import_formation(test_client, filename="formation-exemple-1.xml"): formsemestre_id=formsemestre_ids[mod["semestre_id"] - 1], ) mi = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] - assert mi["ens"] == [] assert mi["module_id"] == mod["module_id"] # --- Export formation en XML