diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index 918d14fd..b73b871a 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -104,9 +104,11 @@ class BulletinBUT: "competence": None, # XXX TODO lien avec référentiel "moyenne": None, # Le bonus sport appliqué sur cette UE - "bonus": fmt_note(res.bonus_ues[ue.id][etud.id]) - if res.bonus_ues is not None and ue.id in res.bonus_ues - else fmt_note(0.0), + "bonus": ( + fmt_note(res.bonus_ues[ue.id][etud.id]) + if res.bonus_ues is not None and ue.id in res.bonus_ues + else fmt_note(0.0) + ), "malus": fmt_note(res.malus[ue.id][etud.id]), "capitalise": None, # "AAAA-MM-JJ" TODO #sco93 "ressources": self.etud_ue_mod_results(etud, ue, res.ressources), @@ -181,14 +183,16 @@ class BulletinBUT: "is_external": ue_capitalisee.is_external, "date_capitalisation": ue_capitalisee.event_date, "formsemestre_id": ue_capitalisee.formsemestre_id, - "bul_orig_url": url_for( - "notes.formsemestre_bulletinetud", - scodoc_dept=g.scodoc_dept, - etudid=etud.id, - formsemestre_id=ue_capitalisee.formsemestre_id, - ) - if ue_capitalisee.formsemestre_id - else None, + "bul_orig_url": ( + url_for( + "notes.formsemestre_bulletinetud", + scodoc_dept=g.scodoc_dept, + etudid=etud.id, + formsemestre_id=ue_capitalisee.formsemestre_id, + ) + if ue_capitalisee.formsemestre_id + else None + ), "ressources": {}, # sans détail en BUT "saes": {}, } @@ -227,13 +231,15 @@ class BulletinBUT: "id": modimpl.id, "titre": modimpl.module.titre, "code_apogee": modimpl.module.code_apogee, - "url": url_for( - "notes.moduleimpl_status", - scodoc_dept=g.scodoc_dept, - moduleimpl_id=modimpl.id, - ) - if has_request_context() - else "na", + "url": ( + url_for( + "notes.moduleimpl_status", + scodoc_dept=g.scodoc_dept, + moduleimpl_id=modimpl.id, + ) + if has_request_context() + else "na" + ), "moyenne": { # # moyenne indicative de module: moyenne des UE, # # ignorant celles sans notes (nan) @@ -242,18 +248,20 @@ class BulletinBUT: # "max": fmt_note(moyennes_etuds.max()), # "moy": fmt_note(moyennes_etuds.mean()), }, - "evaluations": [ - self.etud_eval_results(etud, e) - for e in modimpl.evaluations - if (e.visibulletin or version == "long") - and (e.id in modimpl_results.evaluations_etat) - and ( - modimpl_results.evaluations_etat[e.id].is_complete - or self.prefs["bul_show_all_evals"] - ) - ] - if version != "short" - else [], + "evaluations": ( + [ + self.etud_eval_results(etud, e) + for e in modimpl.evaluations + if (e.visibulletin or version == "long") + and (e.id in modimpl_results.evaluations_etat) + and ( + modimpl_results.evaluations_etat[e.id].is_complete + or self.prefs["bul_show_all_evals"] + ) + ] + if version != "short" + else [] + ), } return d @@ -274,35 +282,43 @@ class BulletinBUT: poids = collections.defaultdict(lambda: 0.0) d = { "id": e.id, - "coef": fmt_note(e.coefficient) - if e.evaluation_type == scu.EVALUATION_NORMALE - else None, + "coef": ( + fmt_note(e.coefficient) + if e.evaluation_type == Evaluation.EVALUATION_NORMALE + else None + ), "date_debut": e.date_debut.isoformat() if e.date_debut else None, "date_fin": e.date_fin.isoformat() if e.date_fin else None, "description": e.description, "evaluation_type": e.evaluation_type, - "note": { - "value": fmt_note( - eval_notes[etud.id], - note_max=e.note_max, - ), - "min": fmt_note(notes_ok.min(), note_max=e.note_max), - "max": fmt_note(notes_ok.max(), note_max=e.note_max), - "moy": fmt_note(notes_ok.mean(), note_max=e.note_max), - }, + "note": ( + { + "value": fmt_note( + eval_notes[etud.id], + note_max=e.note_max, + ), + "min": fmt_note(notes_ok.min(), note_max=e.note_max), + "max": fmt_note(notes_ok.max(), note_max=e.note_max), + "moy": fmt_note(notes_ok.mean(), note_max=e.note_max), + } + if not e.is_blocked() + else {} + ), "poids": poids, - "url": url_for( - "notes.evaluation_listenotes", - scodoc_dept=g.scodoc_dept, - evaluation_id=e.id, - ) - if has_request_context() - else "na", + "url": ( + url_for( + "notes.evaluation_listenotes", + scodoc_dept=g.scodoc_dept, + evaluation_id=e.id, + ) + if has_request_context() + else "na" + ), # deprecated (supprimer avant #sco9.7) "date": e.date_debut.isoformat() if e.date_debut else None, - "heure_debut": e.date_debut.time().isoformat("minutes") - if e.date_debut - else None, + "heure_debut": ( + e.date_debut.time().isoformat("minutes") if e.date_debut else None + ), "heure_fin": e.date_fin.time().isoformat("minutes") if e.date_fin else None, } return d @@ -524,9 +540,9 @@ class BulletinBUT: d.update(infos) # --- Rangs - d[ - "rang_nt" - ] = f"{d['semestre']['rang']['value']} / {d['semestre']['rang']['total']}" + d["rang_nt"] = ( + f"{d['semestre']['rang']['value']} / {d['semestre']['rang']['total']}" + ) d["rang_txt"] = "Rang " + d["rang_nt"] d.update(sco_bulletins.make_context_dict(self.res.formsemestre, d["etud"])) diff --git a/app/but/bulletin_but_court.py b/app/but/bulletin_but_court.py index 7fb389a5..a4fc6ec1 100644 --- a/app/but/bulletin_but_court.py +++ b/app/but/bulletin_but_court.py @@ -119,6 +119,12 @@ def _build_bulletin_but_infos( refcomp = formsemestre.formation.referentiel_competence if refcomp is None: raise ScoNoReferentielCompetences(formation=formsemestre.formation) + + warn_html = cursus_but.formsemestre_warning_apc_setup( + formsemestre, bulletins_sem.res + ) + if warn_html: + raise ScoValueError("Formation mal configurée pour le BUT" + warn_html) ue_validation_by_niveau = validations_view.get_ue_validation_by_niveau( refcomp, etud ) diff --git a/app/but/bulletin_but_pdf.py b/app/but/bulletin_but_pdf.py index f56b86c0..999846f7 100644 --- a/app/but/bulletin_but_pdf.py +++ b/app/but/bulletin_but_pdf.py @@ -24,7 +24,7 @@ from reportlab.lib.colors import blue from reportlab.lib.units import cm, mm from reportlab.platypus import Paragraph, Spacer -from app.models import ScoDocSiteConfig +from app.models import Evaluation, ScoDocSiteConfig from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard from app.scodoc import gen_tables from app.scodoc.codes_cursus import UE_SPORT @@ -422,7 +422,11 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard): def evaluations_rows(self, rows, evaluations: list[dict], ue_acros=()): "lignes des évaluations" for e in evaluations: - coef = e["coef"] if e["evaluation_type"] == scu.EVALUATION_NORMALE else "*" + coef = ( + e["coef"] + if e["evaluation_type"] == Evaluation.EVALUATION_NORMALE + else "*" + ) t = { "titre": f"{e['description'] or ''}", "moyenne": e["note"]["value"], @@ -431,7 +435,10 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard): ), "coef": coef, "_coef_pdf": Paragraph( - f"{coef}" + f"""{ + coef if e["evaluation_type"] != Evaluation.EVALUATION_BONUS + else "bonus" + }""" ), "_pdf_style": [ ( diff --git a/app/but/cursus_but.py b/app/but/cursus_but.py index 1f453882..e2c45c9f 100644 --- a/app/but/cursus_but.py +++ b/app/but/cursus_but.py @@ -23,29 +23,21 @@ from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_compat import NotesTableCompat from app.models.but_refcomp import ( - ApcAnneeParcours, ApcCompetence, ApcNiveau, ApcParcours, - ApcParcoursNiveauCompetence, ApcReferentielCompetences, ) -from app.models import Scolog, ScolarAutorisationInscription -from app.models.but_validations import ( - ApcValidationAnnee, - ApcValidationRCUE, -) +from app.models.ues import UEParcours +from app.models.but_validations import ApcValidationRCUE from app.models.etudiants import Identite from app.models.formations import Formation -from app.models.formsemestre import FormSemestre, FormSemestreInscription +from app.models.formsemestre import FormSemestre from app.models.ues import UniteEns from app.models.validations import ScolarFormSemestreValidation from app.scodoc import codes_cursus as sco_codes from app.scodoc.codes_cursus import code_ue_validant, CODES_UE_VALIDES, UE_STANDARD - -from app.scodoc import sco_utils as scu from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError - from app.scodoc import sco_cursus_dut @@ -440,11 +432,16 @@ def formsemestre_warning_apc_setup( """ if not formsemestre.formation.is_apc(): return "" + url_formation = url_for( + "notes.ue_table", + scodoc_dept=g.scodoc_dept, + formation_id=formsemestre.formation.id, + semestre_idx=formsemestre.semestre_id, + ) if formsemestre.formation.referentiel_competence is None: return f"""
- La formation n'est pas associée à un référentiel de compétence. + La formation + n'est pas associée à un référentiel de compétence.
""" H = [] @@ -462,7 +459,9 @@ def formsemestre_warning_apc_setup( ) if nb_ues_sans_parcours != nb_ues_tot: H.append( - f"""Le semestre n'est associé à aucun parcours, mais les UEs de la formation ont des parcours""" + """Le semestre n'est associé à aucun parcours, + mais les UEs de la formation ont des parcours + """ ) # Vérifie les niveaux de chaque parcours for parcour in formsemestre.parcours or [None]: @@ -489,7 +488,8 @@ def formsemestre_warning_apc_setup( if not H: return "" return f"""
- Problème dans la configuration de la formation: + Problème dans la + configuration de la formation: @@ -502,6 +502,78 @@ def formsemestre_warning_apc_setup( """ +def formation_semestre_niveaux_warning(formation: Formation, semestre_idx: int) -> str: + """Vérifie que tous les niveaux de compétences de cette année de formation + ont bien des UEs. + Afin de ne pas générer trop de messages, on ne considère que les parcours + du référentiel de compétences pour lesquels au moins une UE a été associée. + + Renvoie fragment de html + """ + annee = (semestre_idx - 1) // 2 + 1 # année BUT + ref_comp: ApcReferentielCompetences = formation.referentiel_competence + if not ref_comp: + return "" # détecté ailleurs... + niveaux_sans_ue_by_parcour = {} # { parcour.code : [ ue ... ] } + parcours_ids = { + uep.parcours_id + for uep in UEParcours.query.join(UniteEns).filter_by( + formation_id=formation.id, type=UE_STANDARD + ) + } + for parcour in ref_comp.parcours: + if parcour.id not in parcours_ids: + continue # saute parcours associés à aucune UE (tous semestres) + niveaux_sans_ue = [] + niveaux = ApcNiveau.niveaux_annee_de_parcours(parcour, annee, ref_comp) + # print(f"\n# Parcours {parcour.code} : {len(niveaux)} niveaux") + for niveau in niveaux: + ues = [ue for ue in formation.ues if ue.niveau_competence_id == niveau.id] + if not ues: + niveaux_sans_ue.append(niveau) + # print( niveau.competence.titre + " " + str(niveau.ordre) + "\t" + str(ue) ) + if niveaux_sans_ue: + niveaux_sans_ue_by_parcour[parcour.code] = niveaux_sans_ue + # + H = [] + for parcour_code, niveaux in niveaux_sans_ue_by_parcour.items(): + H.append( + f"""
  • Parcours {parcour_code} : { + len(niveaux)} niveaux sans UEs + + { ', '.join( f'{niveau.competence.titre} {niveau.ordre}' + for niveau in niveaux + ) + } + +
  • + """ + ) + # Combien de compétences de tronc commun ? + _, niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(annee) + nb_niveaux_tc = len(niveaux_by_parcours["TC"]) + nb_ues_tc = len( + formation.query_ues_parcour(None) + .filter(UniteEns.semestre_idx == semestre_idx) + .all() + ) + if nb_niveaux_tc != nb_ues_tc: + H.append( + f"""
  • {nb_niveaux_tc} niveaux de compétences de tronc commun, + mais {nb_ues_tc} UEs de tronc commun !
  • """ + ) + + if H: + return f"""
    +
    Problèmes détectés à corriger :
    + +
    + """ + return "" # no problem detected + + def ue_associee_au_niveau_du_parcours( ues_possibles: list[UniteEns], niveau: ApcNiveau, sem_name: str = "S" ) -> UniteEns: diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 3f8fa9a7..fe670d70 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -77,7 +77,7 @@ from app.models.but_refcomp import ( ApcNiveau, ApcParcours, ) -from app.models import Scolog, ScolarAutorisationInscription +from app.models import Evaluation, Scolog, ScolarAutorisationInscription from app.models.but_validations import ( ApcValidationAnnee, ApcValidationRCUE, @@ -260,11 +260,11 @@ class DecisionsProposeesAnnee(DecisionsProposees): else [] ) # ---- Niveaux et RCUEs - niveaux_by_parcours = ( - formsemestre.formation.referentiel_competence.get_niveaux_by_parcours( - self.annee_but, [self.parcour] if self.parcour else None - )[1] - ) + niveaux_by_parcours = formsemestre.formation.referentiel_competence.get_niveaux_by_parcours( + self.annee_but, [self.parcour] if self.parcour else None + )[ + 1 + ] self.niveaux_competences = niveaux_by_parcours["TC"] + ( niveaux_by_parcours[self.parcour.id] if self.parcour else [] ) @@ -358,13 +358,17 @@ class DecisionsProposeesAnnee(DecisionsProposees): # self.codes = [] # pas de décision annuelle sur semestres impairs elif self.inscription_etat != scu.INSCRIT: self.codes = [ - sco_codes.DEM - if self.inscription_etat == scu.DEMISSION - else sco_codes.DEF, + ( + sco_codes.DEM + if self.inscription_etat == scu.DEMISSION + else sco_codes.DEF + ), # propose aussi d'autres codes, au cas où... - sco_codes.DEM - if self.inscription_etat != scu.DEMISSION - else sco_codes.DEF, + ( + sco_codes.DEM + if self.inscription_etat != scu.DEMISSION + else sco_codes.DEF + ), sco_codes.ABAN, sco_codes.ABL, sco_codes.EXCLU, @@ -595,11 +599,9 @@ class DecisionsProposeesAnnee(DecisionsProposees): # Ordonne par numéro d'UE niv_rcue = sorted( self.rcue_by_niveau.items(), - key=lambda x: x[1].ue_1.numero - if x[1].ue_1 - else x[1].ue_2.numero - if x[1].ue_2 - else 0, + key=lambda x: ( + x[1].ue_1.numero if x[1].ue_1 else x[1].ue_2.numero if x[1].ue_2 else 0 + ), ) return { niveau_id: DecisionsProposeesRCUE(self, rcue, self.inscription_etat) @@ -816,9 +818,15 @@ class DecisionsProposeesAnnee(DecisionsProposees): Return: True si au moins un code modifié et enregistré. """ modif = False - # Vérification notes en attente dans formsemestre origine - if only_validantes and self.has_notes_en_attente(): - return False + if only_validantes: + if self.has_notes_en_attente(): + # notes en attente dans formsemestre origine + return False + if Evaluation.get_evaluations_blocked_for_etud( + self.formsemestre, self.etud + ): + # évaluation(s) qui seront débloquées dans le futur + return False # Toujours valider dans l'ordre UE, RCUE, Année annee_scolaire = self.formsemestre.annee_scolaire() @@ -1488,9 +1496,11 @@ class DecisionsProposeesUE(DecisionsProposees): self.validation = None # cache toute validation self.explanation = "non inscrit (dem. ou déf.)" self.codes = [ - sco_codes.DEM - if res.get_etud_etat(etud.id) == scu.DEMISSION - else sco_codes.DEF + ( + sco_codes.DEM + if res.get_etud_etat(etud.id) == scu.DEMISSION + else sco_codes.DEF + ) ] return diff --git a/app/but/jury_but_view.py b/app/but/jury_but_view.py index b82007b8..915d7539 100644 --- a/app/but/jury_but_view.py +++ b/app/but/jury_but_view.py @@ -331,250 +331,6 @@ def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str: """ -def jury_but_semestriel( - formsemestre: FormSemestre, - etud: Identite, - read_only: bool, - navigation_div: str = "", -) -> str: - """Page: formulaire saisie décision d'UE d'un semestre BUT isolé (pas jury annuel).""" - res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre) - parcour, ues = jury_but.list_ue_parcour_etud(formsemestre, etud, res) - inscription_etat = etud.inscription_etat(formsemestre.id) - semestre_terminal = ( - formsemestre.semestre_id >= formsemestre.formation.get_cursus().NB_SEM - ) - autorisations_passage = ScolarAutorisationInscription.query.filter_by( - etudid=etud.id, - origin_formsemestre_id=formsemestre.id, - ).all() - # Par défaut: autorisé à passer dans le semestre suivant si sem. impair, - # ou si décision déjà enregistrée: - est_autorise_a_passer = (formsemestre.semestre_id % 2) or ( - formsemestre.semestre_id + 1 - ) in (a.semestre_id for a in autorisations_passage) - decisions_ues = { - ue.id: DecisionsProposeesUE(etud, formsemestre, ue, inscription_etat) - for ue in ues - } - for dec_ue in decisions_ues.values(): - dec_ue.compute_codes() - - if request.method == "POST": - if not read_only: - for key in request.form: - code = request.form[key] - # Codes d'UE - code_match = re.match(r"^code_ue_(\d+)$", key) - if code_match: - ue_id = int(code_match.group(1)) - dec_ue = decisions_ues.get(ue_id) - if not dec_ue: - raise ScoValueError(f"UE invalide ue_id={ue_id}") - dec_ue.record(code) - db.session.commit() - flash("codes enregistrés") - if not semestre_terminal: - if request.form.get("autorisation_passage"): - if not formsemestre.semestre_id + 1 in ( - a.semestre_id for a in autorisations_passage - ): - ScolarAutorisationInscription.delete_autorisation_etud( - etud.id, formsemestre.id - ) - ScolarAutorisationInscription.autorise_etud( - etud.id, - formsemestre.formation.formation_code, - formsemestre.id, - formsemestre.semestre_id + 1, - ) - db.session.commit() - flash( - f"""autorisation de passage en S{formsemestre.semestre_id + 1 - } enregistrée""" - ) - else: - if est_autorise_a_passer: - ScolarAutorisationInscription.delete_autorisation_etud( - etud.id, formsemestre.id - ) - db.session.commit() - flash( - f"autorisation de passage en S{formsemestre.semestre_id + 1} annulée" - ) - ScolarNews.add( - typ=ScolarNews.NEWS_JURY, - obj=formsemestre.id, - text=f"""Saisie décision jury dans {formsemestre.html_link_status()}""", - url=url_for( - "notes.formsemestre_status", - scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre.id, - ), - ) - return flask.redirect( - url_for( - "notes.formsemestre_validation_but", - scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre.id, - etudid=etud.id, - ) - ) - # GET - if formsemestre.semestre_id % 2 == 0: - warning = f"""
    - Cet étudiant de S{formsemestre.semestre_id} ne peut pas passer - en jury BUT annuel car il lui manque le semestre précédent. -
    """ - else: - warning = "" - H = [ - html_sco_header.sco_header( - page_title=f"Validation BUT S{formsemestre.semestre_id}", - formsemestre_id=formsemestre.id, - etudid=etud.id, - cssstyles=("css/jury_but.css",), - javascripts=("js/jury_but.js",), - ), - f""" -
    -
    -
    -
    -
    Jury BUT S{formsemestre.id} - - Parcours {(parcour.libelle if parcour else False) or "non spécifié"} -
    -
    {etud.nomprenom}
    -
    - -
    -

    Jury sur un semestre BUT isolé (ne concerne que les UEs)

    - {warning} -
    - -
    - """, - ] - - erase_span = "" - if not read_only: - # Requête toutes les validations (pas seulement celles du deca courant), - # au cas où: changement d'architecture, saisie en mode classique, ... - validations = ScolarFormSemestreValidation.query.filter_by( - etudid=etud.id, formsemestre_id=formsemestre.id - ).all() - if validations: - erase_span = f"""effacer les décisions enregistrées""" - else: - erase_span = ( - "Cet étudiant n'a aucune décision enregistrée pour ce semestre." - ) - - H.append( - f""" -
    -
    -
    Unités d'enseignement de S{formsemestre.semestre_id}:
    - """ - ) - if not ues: - H.append( - """
    Aucune UE ! Vérifiez votre programme de - formation, et l'association UEs / Niveaux de compétences
    """ - ) - else: - H.append( - """ -
    -
    -
    -
    -
    - """ - ) - for ue in ues: - dec_ue = decisions_ues[ue.id] - H.append("""
    """) - H.append( - _gen_but_niveau_ue( - ue, - dec_ue, - disabled=read_only, - ) - ) - H.append( - """
    -
    """ - ) - H.append("
    ") # but_annee - - div_autorisations_passage = ( - f""" -
    - Autorisé à passer en : - { ", ".join( ["S" + str(a.semestre_id or '') for a in autorisations_passage ] )} -
    - """ - if autorisations_passage - else """
    pas d'autorisations de passage enregistrées.
    """ - ) - H.append(div_autorisations_passage) - - if read_only: - H.append( - f"""
    - {"Vous n'avez pas la permission de modifier ces décisions." - if formsemestre.etat - else "Semestre verrouillé."} - Les champs entourés en vert sont enregistrés. -
    - """ - ) - else: - if formsemestre.semestre_id < formsemestre.formation.get_cursus().NB_SEM: - H.append( - f""" -
    - - autoriser à passer dans le semestre S{formsemestre.semestre_id+1} - -
    - """ - ) - else: - H.append("""
    dernier semestre de la formation.
    """) - H.append( - f""" -
    - - {erase_span} -
    - """ - ) - - H.append(navigation_div) - H.append("
    ") - H.append( - render_template( - "but/documentation_codes_jury.j2", - nom_univ=f"""Export {sco_preferences.get_preference("InstituteName") - or sco_preferences.get_preference("UnivName") - or "Apogée"}""", - codes=ScoDocSiteConfig.get_codes_apo_dict(), - ) - ) - - return "\n".join(H) - - # ------------- def infos_fiche_etud_html(etudid: int) -> str: """Section html pour fiche etudiant diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py index 94f056b2..b1af5c06 100644 --- a/app/comp/moy_mod.py +++ b/app/comp/moy_mod.py @@ -35,7 +35,6 @@ moyenne générale d'une UE. """ import dataclasses from dataclasses import dataclass - import numpy as np import pandas as pd import sqlalchemy as sa @@ -151,17 +150,18 @@ class ModuleImplResults: self.evaluations_completes_dict = {} for evaluation in moduleimpl.evaluations: eval_df = self._load_evaluation_notes(evaluation) - # is_complete ssi tous les inscrits (non dem) au semestre ont une note - # ou évaluation déclarée "à prise en compte immédiate" - # Les évaluations de rattrapage et 2eme session sont toujours complètes + # is_complete ssi + # tous les inscrits (non dem) au module ont une note + # ou évaluation déclarée "à prise en compte immédiate" + # ou rattrapage, 2eme session, bonus + # ET pas bloquée par date (is_blocked) etudids_sans_note = inscrits_module - set(eval_df.index) # sans les dem. is_complete = ( - (evaluation.evaluation_type == scu.EVALUATION_RATTRAPAGE) - or (evaluation.evaluation_type == scu.EVALUATION_SESSION2) + (evaluation.evaluation_type != Evaluation.EVALUATION_NORMALE) or (evaluation.publish_incomplete) or (not etudids_sans_note) - ) + ) and not evaluation.is_blocked() self.evaluations_completes.append(is_complete) self.evaluations_completes_dict[evaluation.id] = is_complete self.evals_etudids_sans_note[evaluation.id] = etudids_sans_note @@ -186,7 +186,7 @@ class ModuleImplResults: ].index ) if evaluation.publish_incomplete: - # et en "imédiat", tous ceux sans note + # et en "immédiat", tous ceux sans note eval_etudids_attente |= etudids_sans_note # Synthèse pour état du module: self.etudids_attente |= eval_etudids_attente @@ -240,19 +240,20 @@ class ModuleImplResults: ).formsemestre.inscriptions ] - def get_evaluations_coefs(self, moduleimpl: ModuleImpl) -> np.array: + def get_evaluations_coefs(self, modimpl: ModuleImpl) -> np.array: """Coefficients des évaluations. - Les coefs des évals incomplètes et non "normales" (session 2, rattrapage) - sont zéro. + Les coefs des évals incomplètes, rattrapage, session 2, bonus sont forcés à zéro. Résultat: 2d-array of floats, shape (nb_evals, 1) """ return ( np.array( [ - e.coefficient - if e.evaluation_type == scu.EVALUATION_NORMALE - else 0.0 - for e in moduleimpl.evaluations + ( + e.coefficient + if e.evaluation_type == Evaluation.EVALUATION_NORMALE + else 0.0 + ) + for e in modimpl.evaluations ], dtype=float, ) @@ -276,7 +277,7 @@ class ModuleImplResults: ) / [e.note_max / 20.0 for e in moduleimpl.evaluations] def get_eval_notes_dict(self, evaluation_id: int) -> dict: - """Notes d'une évaulation, brutes, sous forme d'un dict + """Notes d'une évaluation, brutes, sous forme d'un dict { etudid : valeur } avec les valeurs float, ou "ABS" ou EXC """ @@ -285,7 +286,7 @@ class ModuleImplResults: for (etudid, x) in self.evals_notes[evaluation_id].items() } - def get_evaluation_rattrapage(self, moduleimpl: ModuleImpl): + def get_evaluation_rattrapage(self, moduleimpl: ModuleImpl) -> Evaluation | None: """L'évaluation de rattrapage de ce module, ou None s'il n'en a pas. Rattrapage: la moyenne du module est la meilleure note entre moyenne des autres évals et la note eval rattrapage. @@ -293,25 +294,41 @@ class ModuleImplResults: eval_list = [ e for e in moduleimpl.evaluations - if e.evaluation_type == scu.EVALUATION_RATTRAPAGE + if e.evaluation_type == Evaluation.EVALUATION_RATTRAPAGE ] if eval_list: return eval_list[0] return None - def get_evaluation_session2(self, moduleimpl: ModuleImpl): + def get_evaluation_session2(self, moduleimpl: ModuleImpl) -> Evaluation | None: """L'évaluation de deuxième session de ce module, ou None s'il n'en a pas. Session 2: remplace la note de moyenne des autres évals. """ eval_list = [ e for e in moduleimpl.evaluations - if e.evaluation_type == scu.EVALUATION_SESSION2 + if e.evaluation_type == Evaluation.EVALUATION_SESSION2 ] if eval_list: return eval_list[0] return None + def get_evaluations_bonus(self, modimpl: ModuleImpl) -> list[Evaluation]: + """Les évaluations bonus de ce module, ou liste vide s'il n'en a pas.""" + return [ + e + for e in modimpl.evaluations + if e.evaluation_type == Evaluation.EVALUATION_BONUS + ] + + def get_evaluations_bonus_idx(self, modimpl: ModuleImpl) -> list[int]: + """Les indices des évaluations bonus""" + return [ + i + for (i, e) in enumerate(modimpl.evaluations) + if e.evaluation_type == Evaluation.EVALUATION_BONUS + ] + class ModuleImplResultsAPC(ModuleImplResults): "Calcul des moyennes de modules à la mode BUT" @@ -356,7 +373,7 @@ class ModuleImplResultsAPC(ModuleImplResults): # et dans dans evals_poids_etuds # (rappel: la comparaison est toujours false face à un NaN) # shape: (nb_etuds, nb_evals, nb_ues) - poids_stacked = np.stack([evals_poids] * nb_etuds) + poids_stacked = np.stack([evals_poids] * nb_etuds) # nb_etuds, nb_evals, nb_ues evals_poids_etuds = np.where( np.stack([self.evals_notes.values] * nb_ues, axis=2) > scu.NOTES_NEUTRALISE, poids_stacked, @@ -364,10 +381,20 @@ class ModuleImplResultsAPC(ModuleImplResults): ) # Calcule la moyenne pondérée sur les notes disponibles: evals_notes_stacked = np.stack([evals_notes_20] * nb_ues, axis=2) + # evals_notes_stacked shape: nb_etuds, nb_evals, nb_ues with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) etuds_moy_module = np.sum( evals_poids_etuds * evals_notes_stacked, axis=1 ) / np.sum(evals_poids_etuds, axis=1) + # etuds_moy_module shape: nb_etuds x nb_ues + + # Application des évaluations bonus: + etuds_moy_module = self.apply_bonus( + etuds_moy_module, + modimpl, + evals_poids_df, + evals_notes_stacked, + ) # Session2 : quand elle existe, remplace la note de module eval_session2 = self.get_evaluation_session2(modimpl) @@ -416,6 +443,30 @@ class ModuleImplResultsAPC(ModuleImplResults): ) return self.etuds_moy_module + def apply_bonus( + self, + etuds_moy_module: pd.DataFrame, + modimpl: ModuleImpl, + evals_poids_df: pd.DataFrame, + evals_notes_stacked: np.ndarray, + ): + """Ajoute les points des évaluations bonus. + Il peut y avoir un nb quelconque d'évaluations bonus. + Les points sont directement ajoutés (ils peuvent être négatifs). + """ + evals_bonus = self.get_evaluations_bonus(modimpl) + if not evals_bonus: + return etuds_moy_module + poids_stacked = np.stack([evals_poids_df.values] * len(etuds_moy_module)) + for evaluation in evals_bonus: + eval_idx = evals_poids_df.index.get_loc(evaluation.id) + etuds_moy_module += ( + evals_notes_stacked[:, eval_idx, :] * poids_stacked[:, eval_idx, :] + ) + # Clip dans [0,20] + etuds_moy_module.clip(0, 20, out=etuds_moy_module) + return etuds_moy_module + def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]: """Charge poids des évaluations d'un module et retourne un dataframe @@ -532,6 +583,13 @@ class ModuleImplResultsClassic(ModuleImplResults): evals_coefs_etuds * evals_notes_20, axis=1 ) / np.sum(evals_coefs_etuds, axis=1) + # Application des évaluations bonus: + etuds_moy_module = self.apply_bonus( + etuds_moy_module, + modimpl, + evals_notes_20, + ) + # Session2 : quand elle existe, remplace la note de module eval_session2 = self.get_evaluation_session2(modimpl) if eval_session2: @@ -571,3 +629,22 @@ class ModuleImplResultsClassic(ModuleImplResults): ) return self.etuds_moy_module + + def apply_bonus( + self, + etuds_moy_module: np.ndarray, + modimpl: ModuleImpl, + evals_notes_20: np.ndarray, + ): + """Ajoute les points des évaluations bonus. + Il peut y avoir un nb quelconque d'évaluations bonus. + Les points sont directement ajoutés (ils peuvent être négatifs). + """ + evals_bonus_idx = self.get_evaluations_bonus_idx(modimpl) + if not evals_bonus_idx: + return etuds_moy_module + for eval_idx in evals_bonus_idx: + etuds_moy_module += evals_notes_20[:, eval_idx] + # Clip dans [0,20] + etuds_moy_module.clip(0, 20, out=etuds_moy_module) + return etuds_moy_module diff --git a/app/comp/res_common.py b/app/comp/res_common.py index c4bc908f..89c0ca2a 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -205,11 +205,12 @@ class ResultatsSemestre(ResultatsCache): "coefficient" : float, # 0 si None "description" : str, # de l'évaluation, "" si None "etat" { + "blocked" : bool, # vrai si prise en compte bloquée "evalcomplete" : bool, "last_modif" : datetime.datetime | None, # saisie de note la plus récente "nb_notes" : int, # nb notes d'étudiants inscrits }, - "evaluatiuon_id" : int, + "evaluation_id" : int, "jour" : datetime.datetime, # e.date_debut or datetime.datetime(1900, 1, 1) "publish_incomplete" : bool, } @@ -230,15 +231,16 @@ class ResultatsSemestre(ResultatsCache): date_modif = cursor.one_or_none() last_modif = date_modif[0] if date_modif else None return { - "coefficient": evaluation.coefficient or 0.0, - "description": evaluation.description or "", - "evaluation_id": evaluation.id, - "jour": evaluation.date_debut or datetime.datetime(1900, 1, 1), + "coefficient": evaluation.coefficient, + "description": evaluation.description, "etat": { + "blocked": evaluation.is_blocked(), "evalcomplete": etat.is_complete, "nb_notes": etat.nb_notes, "last_modif": last_modif, }, + "evaluation_id": evaluation.id, + "jour": evaluation.date_debut or datetime.datetime(1900, 1, 1), "publish_incomplete": evaluation.publish_incomplete, } @@ -432,9 +434,24 @@ class ResultatsSemestre(ResultatsCache): ue_cap_dict["compense_formsemestre_id"] = None return ue_cap_dict - def get_etud_ue_status(self, etudid: int, ue_id: int) -> dict: + def get_etud_ue_status(self, etudid: int, ue_id: int) -> dict | None: """L'état de l'UE pour cet étudiant. Result: dict, ou None si l'UE n'est pas dans ce semestre. + { + "is_capitalized": # vrai si la version capitalisée est celle prise en compte (meilleure) + "was_capitalized":# si elle a été capitalisée (meilleure ou pas) + "is_external": # si UE externe + "coef_ue": 0.0, + "cur_moy_ue": 0.0, # moyenne de l'UE courante + "moy": 0.0, # moyenne prise en compte + "event_date": # date de la capiltalisation éventuelle (ou None) + "ue": ue_dict, # l'UE, comme un dict + "formsemestre_id": None, + "capitalized_ue_id": None, # l'id de l'UE capitalisée, ou None + "ects_pot": 0.0, # deprecated (les ECTS liés à cette UE) + "ects": 0.0, # les ECTS acquis grace à cette UE + "ects_ue": # les ECTS liés à cette UE + } """ ue: UniteEns = db.session.get(UniteEns, ue_id) ue_dict = ue.to_dict() @@ -455,7 +472,7 @@ class ResultatsSemestre(ResultatsCache): "ects": 0.0, "ects_ue": ue.ects, } - if not ue_id in self.etud_moy_ue: + if not ue_id in self.etud_moy_ue or not etudid in self.etud_moy_ue[ue_id]: return None if not self.validations: self.validations = res_sem.load_formsemestre_validations(self.formsemestre) @@ -512,11 +529,13 @@ class ResultatsSemestre(ResultatsCache): "is_external": ue_cap["is_external"] if is_capitalized else ue.is_external, "coef_ue": coef_ue, "ects_pot": ue.ects or 0.0, - "ects": self.validations.decisions_jury_ues.get(etudid, {}) - .get(ue.id, {}) - .get("ects", 0.0) - if self.validations.decisions_jury_ues - else 0.0, + "ects": ( + self.validations.decisions_jury_ues.get(etudid, {}) + .get(ue.id, {}) + .get("ects", 0.0) + if self.validations.decisions_jury_ues + else 0.0 + ), "ects_ue": ue.ects, "cur_moy_ue": cur_moy_ue, "moy": moy_ue, diff --git a/app/models/etudiants.py b/app/models/etudiants.py index bd70e03c..ef470f3e 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -125,7 +125,7 @@ class Identite(models.ScoDocModel): ) # Champs "protégés" par ViewEtudData (RGPD) - protected_attrs = {"boursier"} + protected_attrs = {"boursier", "nationalite"} def __repr__(self): return ( diff --git a/app/models/evaluations.py b/app/models/evaluations.py index 7da2d84c..8a253bf5 100644 --- a/app/models/evaluations.py +++ b/app/models/evaluations.py @@ -10,6 +10,7 @@ from flask_login import current_user import sqlalchemy as sa from app import db, log +from app import models from app.models.etudiants import Identite from app.models.events import ScolarNews from app.models.notes import NotesNotes @@ -23,10 +24,8 @@ MAX_EVALUATION_DURATION = datetime.timedelta(days=365) NOON = datetime.time(12, 00) DEFAULT_EVALUATION_TIME = datetime.time(8, 0) -VALID_EVALUATION_TYPES = {0, 1, 2} - -class Evaluation(db.Model): +class Evaluation(models.ScoDocModel): """Evaluation (contrôle, examen, ...)""" __tablename__ = "notes_evaluation" @@ -38,9 +37,9 @@ class Evaluation(db.Model): ) date_debut = db.Column(db.DateTime(timezone=True), nullable=True) date_fin = db.Column(db.DateTime(timezone=True), nullable=True) - description = db.Column(db.Text) - note_max = db.Column(db.Float) - coefficient = db.Column(db.Float) + description = db.Column(db.Text, nullable=False) + note_max = db.Column(db.Float, nullable=False) + coefficient = db.Column(db.Float, nullable=False) visibulletin = db.Column( db.Boolean, nullable=False, default=True, server_default="true" ) @@ -48,15 +47,30 @@ class Evaluation(db.Model): publish_incomplete = db.Column( db.Boolean, nullable=False, default=False, server_default="false" ) - # type d'evaluation: 0 normale, 1 rattrapage, 2 "2eme session" + "prise en compte immédiate" evaluation_type = db.Column( db.Integer, nullable=False, default=0, server_default="0" ) + "type d'evaluation: 0 normale, 1 rattrapage, 2 2eme session, 3 bonus" + blocked_until = db.Column(db.DateTime(timezone=True), nullable=True) + "date de prise en compte" + BLOCKED_FOREVER = datetime.datetime(2666, 12, 31, tzinfo=scu.TIME_ZONE) # ordre de presentation (par défaut, le plus petit numero # est la plus ancienne eval): numero = db.Column(db.Integer, nullable=False, default=0) ues = db.relationship("UniteEns", secondary="evaluation_ue_poids", viewonly=True) + EVALUATION_NORMALE = 0 # valeurs stockées en base, ne pas changer ! + EVALUATION_RATTRAPAGE = 1 + EVALUATION_SESSION2 = 2 + EVALUATION_BONUS = 3 + VALID_EVALUATION_TYPES = { + EVALUATION_NORMALE, + EVALUATION_RATTRAPAGE, + EVALUATION_SESSION2, + EVALUATION_BONUS, + } + def __repr__(self): return f""" "Evaluation": """Create an evaluation. Check permission and all arguments. Ne crée pas les poids vers les UEs. Add to session, do not commit. @@ -88,7 +103,7 @@ class Evaluation(db.Model): args = locals() del args["cls"] del args["kw"] - check_convert_evaluation_args(moduleimpl, args) + check_and_convert_evaluation_args(args, moduleimpl) # Check numeros Evaluation.moduleimpl_evaluation_renumber(moduleimpl, only_if_unumbered=True) if not "numero" in args or args["numero"] is None: @@ -199,6 +214,10 @@ class Evaluation(db.Model): def to_dict_api(self) -> dict: "Représentation dict pour API JSON" return { + "blocked": self.is_blocked(), + "blocked_until": ( + self.blocked_until.isoformat() if self.blocked_until else "" + ), "coefficient": self.coefficient, "date_debut": self.date_debut.isoformat() if self.date_debut else "", "date_fin": self.date_fin.isoformat() if self.date_fin else "", @@ -235,15 +254,6 @@ class Evaluation(db.Model): return e_dict - def from_dict(self, data): - """Set evaluation attributes from given dict values.""" - check_convert_evaluation_args(self.moduleimpl, data) - if data.get("numero") is None: - data["numero"] = Evaluation.get_max_numero(self.moduleimpl.id) + 1 - for k in self.__dict__: - 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 @@ -361,19 +371,6 @@ class Evaluation(db.Model): Chaine vide si non renseignée.""" return self.date_fin.time().isoformat("minutes") if self.date_fin else "" - def clone(self, not_copying=()): - """Clone, not copying the given attrs - Attention: la copie n'a pas d'id avant le prochain commit - """ - d = dict(self.__dict__) - d.pop("id") # get rid of id - d.pop("_sa_instance_state") # get rid of SQLAlchemy special attr - for k in not_copying: - d.pop(k) - copy = self.__class__(**d) - db.session.add(copy) - return copy - def is_matin(self) -> bool: "Evaluation commençant le matin (faux si pas de date)" if not self.date_debut: @@ -386,6 +383,14 @@ class Evaluation(db.Model): return False return self.date_debut.time() >= NOON + def is_blocked(self, now=None) -> bool: + "True si prise en compte bloquée" + if self.blocked_until is None: + return False + if now is None: + now = datetime.datetime.now(scu.TIME_ZONE) + return self.blocked_until > now + def set_default_poids(self) -> bool: """Initialize les poids vers les UE à leurs valeurs par défaut C'est à dire à 1 si le coef. module/UE est non nul, 0 sinon. @@ -474,6 +479,29 @@ class Evaluation(db.Model): """ return NotesNotes.query.filter_by(etudid=etud.id, evaluation_id=self.id).first() + @classmethod + def get_evaluations_blocked_for_etud( + cls, formsemestre, etud: Identite + ) -> list["Evaluation"]: + """Liste des évaluations de ce semestre avec note pour cet étudiant et date blocage + et date blocage < FOREVER. + Si non vide, une note apparaitra dans le futur pour cet étudiant: il faut + donc interdire la saisie du jury. + """ + now = datetime.datetime.now(scu.TIME_ZONE) + return ( + Evaluation.query.filter( + Evaluation.blocked_until != None, # pylint: disable=C0121 + Evaluation.blocked_until >= now, + ) + .join(ModuleImpl) + .filter_by(formsemestre_id=formsemestre.id) + .join(ModuleImplInscription) + .filter_by(etudid=etud.id) + .join(NotesNotes) + .all() + ) + class EvaluationUEPoids(db.Model): """Poids des évaluations (BUT) @@ -531,7 +559,7 @@ def evaluation_enrich_dict(e: Evaluation, e_dict: dict): return e_dict -def check_convert_evaluation_args(moduleimpl: "ModuleImpl", data: dict): +def check_and_convert_evaluation_args(data: dict, moduleimpl: "ModuleImpl"): """Check coefficient, dates and duration, raises exception if invalid. Convert date and time strings to date and time objects. @@ -546,7 +574,7 @@ def check_convert_evaluation_args(moduleimpl: "ModuleImpl", data: dict): # --- evaluation_type try: data["evaluation_type"] = int(data.get("evaluation_type", 0) or 0) - if not data["evaluation_type"] in VALID_EVALUATION_TYPES: + if not data["evaluation_type"] in Evaluation.VALID_EVALUATION_TYPES: raise ScoValueError("invalid evaluation_type value") except ValueError as exc: raise ScoValueError("invalid evaluation_type value") from exc @@ -571,7 +599,7 @@ def check_convert_evaluation_args(moduleimpl: "ModuleImpl", data: dict): if coef < 0: raise ScoValueError("invalid coefficient value (must be positive or null)") data["coefficient"] = coef - # --- date de l'évaluation + # --- date de l'évaluation dans le semestre ? formsemestre = moduleimpl.formsemestre date_debut = data.get("date_debut", None) if date_debut: @@ -612,6 +640,8 @@ def check_convert_evaluation_args(moduleimpl: "ModuleImpl", data: dict): "Heures de l'évaluation incohérentes !", dest_url="javascript:history.back();", ) + if "blocked_until" in data: + data["blocked_until"] = data["blocked_until"] or None def heure_to_time(heure: str) -> datetime.time: @@ -641,3 +671,6 @@ def _moduleimpl_evaluation_insert_before( db.session.add(e) db.session.commit() return n + + +from app.models.moduleimpls import ModuleImpl, ModuleImplInscription diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 9d2f5b13..09c1d305 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -93,6 +93,10 @@ class FormSemestre(db.Model): db.Boolean(), nullable=False, default=False, server_default="false" ) "Si vrai, la moyenne générale indicative BUT n'est pas calculée" + mode_calcul_moyennes = db.Column( + db.Integer, nullable=False, default=0, server_default="0" + ) + "pour usage futur" gestion_semestrielle = db.Column( db.Boolean(), nullable=False, default=False, server_default="false" ) diff --git a/app/models/modules.py b/app/models/modules.py index b0db0e68..5abb5340 100644 --- a/app/models/modules.py +++ b/app/models/modules.py @@ -6,7 +6,12 @@ from flask import current_app, g from app import db from app import models from app.models import APO_CODE_STR_LEN -from app.models.but_refcomp import ApcParcours, app_critiques_modules, parcours_modules +from app.models.but_refcomp import ( + ApcParcours, + ApcReferentielCompetences, + app_critiques_modules, + parcours_modules, +) from app.scodoc import sco_utils as scu from app.scodoc.codes_cursus import UE_SPORT from app.scodoc.sco_exceptions import ScoValueError @@ -100,6 +105,33 @@ class Module(models.ScoDocModel): return args_dict + @classmethod + def filter_model_attributes(cls, data: dict, excluded: set[str] = None) -> dict: + """Returns a copy of dict with only the keys belonging to the Model and not in excluded. + Add 'id' to excluded.""" + # on ne peut pas affecter directement parcours + return super().filter_model_attributes(data, (excluded or set()) | {"parcours"}) + + @classmethod + def create_from_dict(cls, data: dict) -> "Module": + """Create from given dict, add parcours""" + mod = super().create_from_dict(data) + for p in data.get("parcours", []) or []: + if isinstance(p, ApcParcours): + parcour: ApcParcours = p + else: + pid = int(p) + query = ApcParcours.query.filter_by(id=pid) + if g.scodoc_dept: + query = query.join(ApcReferentielCompetences).filter_by( + dept_id=g.scodoc_dept_id + ) + parcour: ApcParcours = query.first() + if parcour is None: + raise ScoValueError("Parcours invalide") + mod.parcours.append(parcour) + return mod + def clone(self): """Create a new copy of this module.""" mod = Module( diff --git a/app/models/validations.py b/app/models/validations.py index 17dd12a6..e9796876 100644 --- a/app/models/validations.py +++ b/app/models/validations.py @@ -126,7 +126,7 @@ class ScolarFormSemestreValidation(db.Model): def ects(self) -> float: "Les ECTS acquis par cette validation. (0 si ce n'est pas une validation d'UE)" return ( - self.ue.ects + self.ue.ects or 0.0 if (self.ue is not None) and (self.code in CODES_UE_VALIDES) else 0.0 ) diff --git a/app/scodoc/codes_cursus.py b/app/scodoc/codes_cursus.py index 8e595ead..5120aa05 100644 --- a/app/scodoc/codes_cursus.py +++ b/app/scodoc/codes_cursus.py @@ -85,17 +85,6 @@ UE_ELECTIVE = 4 # UE "élective" dans certains cursus (UCAC?, ISCID) UE_PROFESSIONNELLE = 5 # UE "professionnelle" (ISCID, ...) UE_OPTIONNELLE = 6 # UE non fondamentales (ILEPS, ...) - -def ue_is_fondamentale(ue_type): - return ue_type in (UE_STANDARD, UE_STAGE_LP, UE_PROFESSIONNELLE) - - -def ue_is_professionnelle(ue_type): - return ( - ue_type == UE_PROFESSIONNELLE - ) # NB: les UE_PROFESSIONNELLE sont à la fois fondamentales et pro - - UE_TYPE_NAME = { UE_STANDARD: "Standard", UE_SPORT: "Sport/Culture (points bonus)", @@ -104,8 +93,6 @@ UE_TYPE_NAME = { UE_ELECTIVE: "Elective (ISCID)", UE_PROFESSIONNELLE: "Professionnelle (ISCID)", UE_OPTIONNELLE: "Optionnelle", - # UE_FONDAMENTALE : '"Fondamentale" (eg UCAC)', - # UE_OPTIONNELLE : '"Optionnelle" (UCAC)' } # Couleurs RGB (dans [0.,1.]) des UE pour les bulletins: diff --git a/app/scodoc/html_sidebar.py b/app/scodoc/html_sidebar.py index bc84fea5..f1c8f835 100755 --- a/app/scodoc/html_sidebar.py +++ b/app/scodoc/html_sidebar.py @@ -186,7 +186,7 @@ def sidebar(etudid: int = None): formsemestre.date_fin.strftime("%d/%m/%Y") }">({ sco_preferences.get_preference("assi_metrique", None)}) -
    { nbabsjust } J., { nbabsnj } N.J.""" +
    {nbabsjust:1.0f} J., {nbabsnj:1.0f} N.J.""" ) H.append(" +

    Total: {len(etudlist)} étudiants concernés.

    + +

    Ces étudiants sont inscrits dans le semestre sélectionné et aussi + dans d'autres semestres qui se déroulent en même temps ! +

    +

    + Sauf exception, cette situation est anormale: +

    """ ) diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index c431cb91..c5fce57b 100755 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -1173,7 +1173,8 @@ def formsemestre_tableau_modules( moduleimpl_id=modimpl.id, ) mod_descr = "Module " + (mod.titre or "") - if mod.is_apc(): + is_apc = mod.is_apc() # SAE ou ressource + if is_apc: coef_descr = ", ".join( [ f"{ue.acronyme}: {co}" @@ -1193,6 +1194,7 @@ def formsemestre_tableau_modules( [u.get_nomcomplet() for u in modimpl.enseignants] ) mod_nb_inscrits = nt.modimpls_results[modimpl.id].nb_inscrits_module + mod_is_conforme = modimpl.check_apc_conformity(nt) ue = modimpl.module.ue if show_ues and (prev_ue_id != ue.id): prev_ue_id = ue.id @@ -1200,10 +1202,12 @@ def formsemestre_tableau_modules( if use_ue_coefs: titre += f""" (coef. {ue.coefficient or 0.0})""" H.append( - f""" - {ue.acronyme} - {titre} - """ + f""" + + {ue.acronyme} + {titre} + + """ ) expr = sco_compute_moy.get_ue_expression( @@ -1226,21 +1230,26 @@ def formsemestre_tableau_modules( fontorange = "" etat = sco_evaluations.do_evaluation_etat_in_mod(nt, modimpl) - # if nt.parcours.APC_SAE: - # tbd style si module non conforme if ( etat["nb_evals_completes"] > 0 and etat["nb_evals_en_cours"] == 0 and etat["nb_evals_vides"] == 0 and not etat["attente"] + and not etat["nb_evals_blocked"] > 0 ): - H.append(f'') + tr_classes = f"formsemestre_status_green{fontorange}" else: - H.append(f'') - + tr_classes = f"formsemestre_status{fontorange}" + if etat["attente"]: + tr_classes += " modimpl_attente" + if not mod_is_conforme: + tr_classes += " modimpl_non_conforme" + if etat["nb_evals_blocked"] > 0: + tr_classes += " modimpl_has_blocked" H.append( f""" - + {mod.code} ') - nb_evals = ( - etat["nb_evals_completes"] - + etat["nb_evals_en_cours"] - + etat["nb_evals_vides"] - ) + nb_evals = etat["nb_evals"] if nb_evals != 0: + if etat["nb_evals_blocked"] > 0: + blocked_txt = f"""{ + etat["nb_evals_blocked"]} bloquée{'s' + if etat["nb_evals_blocked"] > 1 else ''}""" + else: + blocked_txt = "" H.append( f"""{nb_evals} prévues, - {etat["nb_evals_completes"]} ok""" + {etat["nb_evals_completes"]} ok {blocked_txt} + """ ) if etat["nb_evals_en_cours"] > 0: H.append( @@ -1300,7 +1312,12 @@ def formsemestre_tableau_modules( if etat["attente"]: H.append( f""" [en attente]""" + title="Il y a des notes en attente">en attente""" + ) + if not mod_is_conforme: + H.append( + f""" [non conforme]""" ) elif mod.module_type == ModuleType.MALUS: nb_malus_notes = sum( diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py index 68a18d60..df4770fa 100644 --- a/app/scodoc/sco_formsemestre_validation.py +++ b/app/scodoc/sco_formsemestre_validation.py @@ -34,7 +34,7 @@ from flask import url_for, flash, g, request from flask_login import current_user import sqlalchemy as sa -from app.models.etudiants import Identite +from app.models import Identite, Evaluation import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu from app import db, log @@ -232,7 +232,9 @@ def formsemestre_validation_etud_form( H.append( tf_error_message( f"""Impossible de statuer sur cet étudiant: il a des notes en - attente dans des évaluations de ce semestre (voir tableau de bord) @@ -241,6 +243,26 @@ def formsemestre_validation_etud_form( ) return "\n".join(H + footer) + evaluations_a_debloquer = Evaluation.get_evaluations_blocked_for_etud( + formsemestre, etud + ) + if evaluations_a_debloquer: + links_evals = [ + f"""{e.description} en {e.moduleimpl.module.code}""" + for e in evaluations_a_debloquer + ] + H.append( + tf_error_message( + f"""Impossible de statuer sur cet étudiant: + il a des notes dans des évaluations qui seront débloquées plus tard: + voir {", ".join(links_evals)} + """ + ) + ) + return "\n".join(H + footer) + # Infos si pas de semestre précédent if not Se.prev: if Se.sem["semestre_id"] == 1: @@ -1399,7 +1421,7 @@ def check_formation_ues(formation: Formation) -> tuple[str, dict[int, list[Unite if ( len(semestre_ids) > 1 ): # plusieurs semestres d'indices differents dans le cursus - ue_multiples[ue["ue_id"]] = sems + ue_multiples[ue.id] = sems if not ue_multiples: return "", {} @@ -1423,12 +1445,12 @@ def check_formation_ues(formation: Formation) -> tuple[str, dict[int, list[Unite ] slist = ", ".join( [ - """%(titreannee)s (semestre %(semestre_id)s)""" - % s + f"""{s['titreannee'] + } (semestre {s['semestre_id']})""" for s in sems ] ) - H.append("
  • %s : %s
  • " % (ue.acronyme, slist)) + H.append(f"
  • {ue.acronyme} : {slist}
  • ") H.append("
    ") return "\n".join(H), ue_multiples diff --git a/app/scodoc/sco_groups_view.py b/app/scodoc/sco_groups_view.py index a404c7fa..ebfffab3 100644 --- a/app/scodoc/sco_groups_view.py +++ b/app/scodoc/sco_groups_view.py @@ -331,6 +331,7 @@ class DisplayedGroupsInfos: empty_list_select_all=True, moduleimpl_id=None, # used to find formsemestre when unspecified ): + group_ids = [] if group_ids is None else group_ids if isinstance(group_ids, int): if group_ids: group_ids = [group_ids] # cas ou un seul parametre, pas de liste @@ -466,6 +467,10 @@ class DisplayedGroupsInfos: else None ) + def get_groups_key(self) -> str: + "clé identifiant les groupes sélectionnés, utile pour cache" + return "-".join(str(x) for x in sorted(self.group_ids)) + # Ancien ZScolar.group_list renommé ici en group_table def groups_table( @@ -514,10 +519,11 @@ def groups_table( "paiementinscription_str": "Paiement", "etudarchive": "Fichiers", "annotations_str": "Annotations", - "bourse_str": "Boursier", + "bourse_str": "Boursier", # requière ViewEtudData "etape": "Etape", "semestre_groupe": "Semestre-Groupe", # pour Moodle "annee": "annee_admission", + "nationalite": "nationalite", # requière ViewEtudData } # ajoute colonnes pour groupes @@ -559,53 +565,61 @@ def groups_table( moodle_sem_name = groups_infos.formsemestre["session_id"] moodle_groupenames = set() # ajoute liens - for etud in groups_infos.members: - if etud["email"]: - etud["_email_target"] = "mailto:" + etud["email"] + for etud_info in groups_infos.members: + if etud_info["email"]: + etud_info["_email_target"] = "mailto:" + etud_info["email"] else: - etud["_email_target"] = "" - if etud["emailperso"]: - etud["_emailperso_target"] = "mailto:" + etud["emailperso"] + etud_info["_email_target"] = "" + if etud_info["emailperso"]: + etud_info["_emailperso_target"] = "mailto:" + etud_info["emailperso"] else: - etud["_emailperso_target"] = "" + etud_info["_emailperso_target"] = "" fiche_url = url_for( - "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"] + "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud_info["etudid"] ) - etud["_nom_disp_target"] = fiche_url - etud["_nom_disp_order"] = etud_sort_key(etud) - etud["_prenom_target"] = fiche_url + etud_info["_nom_disp_target"] = fiche_url + etud_info["_nom_disp_order"] = etud_sort_key(etud_info) + etud_info["_prenom_target"] = fiche_url - etud["_nom_disp_td_attrs"] = 'id="%s" class="etudinfo"' % (etud["etudid"]) - etud["bourse_str"] = "oui" if etud["boursier"] else "non" - if etud["etat"] == "D": - etud["_css_row_class"] = "etuddem" + etud_info["_nom_disp_td_attrs"] = 'id="%s" class="etudinfo"' % ( + etud_info["etudid"] + ) + etud_info["bourse_str"] = "oui" if etud_info["boursier"] else "non" + if etud_info["etat"] == "D": + etud_info["_css_row_class"] = "etuddem" # et groupes: - for partition_id in etud["partitions"]: - etud[partition_id] = etud["partitions"][partition_id]["group_name"] + for partition_id in etud_info["partitions"]: + etud_info[partition_id] = etud_info["partitions"][partition_id][ + "group_name" + ] # Ajoute colonne pour moodle: semestre_groupe, de la forme # RT-DUT-FI-S3-2021-PARTITION-GROUPE moodle_groupename = [] if groups_infos.selected_partitions: # il y a des groupes selectionnes, utilise leurs partitions for partition_id in groups_infos.selected_partitions: - if partition_id in etud["partitions"]: + if partition_id in etud_info["partitions"]: moodle_groupename.append( partitions_name[partition_id] + "-" - + etud["partitions"][partition_id]["group_name"] + + etud_info["partitions"][partition_id]["group_name"] ) else: # pas de groupes sélectionnés: prend le premier s'il y en a un moodle_groupename = ["tous"] - if etud["partitions"]: - for p in etud["partitions"].items(): # partitions is an OrderedDict + if etud_info["partitions"]: + for p in etud_info[ + "partitions" + ].items(): # partitions is an OrderedDict moodle_groupename = [ partitions_name[p[0]] + "-" + p[1]["group_name"] ] break moodle_groupenames |= set(moodle_groupename) - etud["semestre_groupe"] = moodle_sem_name + "-" + "+".join(moodle_groupename) + etud_info["semestre_groupe"] = ( + moodle_sem_name + "-" + "+".join(moodle_groupename) + ) if groups_infos.nbdem > 1: s = "s" @@ -714,9 +728,11 @@ def groups_table( }); """, - """accès aux données personnelles interdit""" - if not can_view_etud_data - else "", + ( + """accès aux données personnelles interdit""" + if not can_view_etud_data + else "" + ), ] ) H.append("") @@ -768,13 +784,7 @@ def groups_table( return "".join(H) - elif ( - fmt == "pdf" - or fmt == "xml" - or fmt == "json" - or fmt == "xls" - or fmt == "moodlecsv" - ): + elif fmt in {"pdf", "xml", "json", "xls", "moodlecsv"}: if fmt == "moodlecsv": fmt = "csv" return tab.make_page(fmt=fmt) @@ -789,7 +799,7 @@ def groups_table( with_paiement=with_paiement, server_name=request.url_root, ) - filename = "liste_%s" % groups_infos.groups_filename + filename = f"liste_{groups_infos.groups_filename}" return scu.send_file(xls, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE) elif fmt == "allxls": if not can_view_etud_data: @@ -823,6 +833,7 @@ def groups_table( "fax", "date_naissance", "lieu_naissance", + "nationalite", "bac", "specialite", "annee_bac", @@ -845,16 +856,16 @@ def groups_table( # remplis infos lycee si on a que le code lycée # et ajoute infos inscription for m in groups_infos.members: - etud = sco_etud.get_etud_info(m["etudid"], filled=True)[0] - m.update(etud) - sco_etud.etud_add_lycee_infos(etud) + etud_info = sco_etud.get_etud_info(m["etudid"], filled=True)[0] + m.update(etud_info) + sco_etud.etud_add_lycee_infos(etud_info) # et ajoute le parcours Se = sco_cursus.get_situation_etud_cursus( - etud, groups_infos.formsemestre_id + etud_info, groups_infos.formsemestre_id ) m["parcours"] = Se.get_cursus_descr() m["code_cursus"], _ = sco_report.get_code_cursus_etud( - etud["etudid"], sems=etud["sems"] + etud_info["etudid"], sems=etud_info["sems"] ) rows = [[m.get(k, "") for k in keys] for m in groups_infos.members] title = "etudiants_%s" % groups_infos.groups_filename @@ -905,9 +916,11 @@ def tab_absences_html(groups_infos, etat=None): % groups_infos.groups_query_args, """
  • Liste d'appel avec photos
  • """ % groups_infos.groups_query_args, - f"""
  • Liste des annotations
  • """ - if authuser.has_permission(Permission.ViewEtudData) - else """
  • Liste des annotations
  • """, + ( + f"""
  • Liste des annotations
  • """ + if authuser.has_permission(Permission.ViewEtudData) + else """
  • Liste des annotations
  • """ + ), "", ] ) diff --git a/app/scodoc/sco_inscr_passage.py b/app/scodoc/sco_inscr_passage.py index f97e0d44..59de8721 100644 --- a/app/scodoc/sco_inscr_passage.py +++ b/app/scodoc/sco_inscr_passage.py @@ -36,13 +36,14 @@ from flask import url_for, g, request import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu from app import db, log -from app.models import Formation, FormSemestre, GroupDescr +from app.comp import res_sem +from app.comp.res_compat import NotesTableCompat +from app.models import Formation, FormSemestre, GroupDescr, Identite from app.scodoc.gen_tables import GenTable from app.scodoc import html_sco_header from app.scodoc import sco_cache from app.scodoc import codes_cursus from app.scodoc import sco_etud -from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_groups from app.scodoc import sco_preferences @@ -50,62 +51,69 @@ from app.scodoc import sco_pv_dict from app.scodoc.sco_exceptions import ScoValueError -def list_authorized_etuds_by_sem(sem, delai=274, ignore_jury=False): +def _list_authorized_etuds_by_sem( + formsemestre: FormSemestre, ignore_jury=False +) -> tuple[dict[int, dict], list[dict], dict[int, Identite]]: """Liste des etudiants autorisés à s'inscrire dans sem. delai = nb de jours max entre la date de l'autorisation et celle de debut du semestre cible. ignore_jury: si vrai, considère tous les étudiants comme autorisés, même s'ils n'ont pas de décision de jury. """ - src_sems = list_source_sems(sem, delai=delai) - inscrits = list_inscrits(sem["formsemestre_id"]) + src_sems = _list_source_sems(formsemestre) + inscrits = list_inscrits(formsemestre.id) r = {} candidats = {} # etudid : etud (tous les etudiants candidats) nb = 0 # debug - for src in src_sems: + src_formsemestre: FormSemestre + for src_formsemestre in src_sems: if ignore_jury: # liste de tous les inscrits au semestre (sans dems) - liste = list_inscrits(src["formsemestre_id"]).values() + etud_list = list_inscrits(src_formsemestre.id).values() else: # liste des étudiants autorisés par le jury à s'inscrire ici - liste = list_etuds_from_sem(src, sem) + etud_list = _list_etuds_from_sem(src_formsemestre, formsemestre) liste_filtree = [] - for e in liste: + for e in etud_list: # Filtre ceux qui se sont déjà inscrit dans un semestre APRES le semestre src auth_used = False # autorisation deja utilisée ? - etud = sco_etud.get_etud_info(etudid=e["etudid"], filled=True)[0] - for isem in etud["sems"]: - if ndb.DateDMYtoISO(isem["date_debut"]) >= ndb.DateDMYtoISO( - src["date_fin"] - ): + etud = Identite.get_etud(e["etudid"]) + for inscription in etud.inscriptions(): + if inscription.formsemestre.date_debut >= src_formsemestre.date_fin: auth_used = True if not auth_used: candidats[e["etudid"]] = etud liste_filtree.append(e) nb += 1 - r[src["formsemestre_id"]] = { + r[src_formsemestre.id] = { "etuds": liste_filtree, "infos": { - "id": src["formsemestre_id"], - "title": src["titreannee"], - "title_target": "formsemestre_status?formsemestre_id=%s" - % src["formsemestre_id"], + "id": src_formsemestre.id, + "title": src_formsemestre.titre_annee(), + "title_target": url_for( + "notes.formsemestre_status", + scodoc_dept=g.scodoc_dept, + formsemestre_id=src_formsemestre.id, + ), "filename": "etud_autorises", }, } # ajoute attribut inscrit qui indique si l'étudiant est déjà inscrit dans le semestre dest. - for e in r[src["formsemestre_id"]]["etuds"]: + for e in r[src_formsemestre.id]["etuds"]: e["inscrit"] = e["etudid"] in inscrits # Ajoute liste des etudiants actuellement inscrits for e in inscrits.values(): e["inscrit"] = True - r[sem["formsemestre_id"]] = { + r[formsemestre.id] = { "etuds": list(inscrits.values()), "infos": { - "id": sem["formsemestre_id"], - "title": "Semestre cible: " + sem["titreannee"], - "title_target": "formsemestre_status?formsemestre_id=%s" - % sem["formsemestre_id"], + "id": formsemestre.id, + "title": "Semestre cible: " + formsemestre.titre_annee(), + "title_target": url_for( + "notes.formsemestre_status", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre.id, + ), "comment": " actuellement inscrits dans ce semestre", "help": "Ces étudiants sont actuellement inscrits dans ce semestre. Si vous les décochez, il seront désinscrits.", "filename": "etud_inscrits", @@ -115,7 +123,7 @@ def list_authorized_etuds_by_sem(sem, delai=274, ignore_jury=False): return r, inscrits, candidats -def list_inscrits(formsemestre_id, with_dems=False): +def list_inscrits(formsemestre_id: int, with_dems=False) -> list[dict]: """Étudiants déjà inscrits à ce semestre { etudid : etud } """ @@ -133,28 +141,27 @@ def list_inscrits(formsemestre_id, with_dems=False): return inscr -def list_etuds_from_sem(src, dst) -> list[dict]: - """Liste des etudiants du semestre src qui sont autorisés à passer dans le semestre dst.""" - target = dst["semestre_id"] - dpv = sco_pv_dict.dict_pvjury(src["formsemestre_id"]) +def _list_etuds_from_sem(src: FormSemestre, dst: FormSemestre) -> list[dict]: + """Liste des étudiants du semestre src qui sont autorisés à passer dans le semestre dst.""" + target_semestre_id = dst.semestre_id + dpv = sco_pv_dict.dict_pvjury(src.id) if not dpv: return [] etuds = [ x["identite"] for x in dpv["decisions"] - if target in [a["semestre_id"] for a in x["autorisations"]] + if target_semestre_id in [a["semestre_id"] for a in x["autorisations"]] ] return etuds -def list_inscrits_date(sem): - """Liste les etudiants inscrits dans n'importe quel semestre - du même département - SAUF sem à la date de début de sem. +def list_inscrits_date(formsemestre: FormSemestre): + """Liste les etudiants inscrits à la date de début de formsemestre + dans n'importe quel semestre du même département + SAUF formsemestre """ cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) - sem["date_debut_iso"] = ndb.DateDMYtoISO(sem["date_debut"]) cursor.execute( """SELECT ins.etudid FROM @@ -166,12 +173,18 @@ def list_inscrits_date(sem): AND S.date_fin >= %(date_debut_iso)s AND S.dept_id = %(dept_id)s """, - sem, + { + "formsemestre_id": formsemestre.id, + "date_debut_iso": formsemestre.date_debut.isoformat(), + "dept_id": formsemestre.dept_id, + }, ) return [x[0] for x in cursor.fetchall()] -def do_inscrit(sem, etudids, inscrit_groupes=False, inscrit_parcours=False): +def do_inscrit( + formsemestre: FormSemestre, etudids, inscrit_groupes=False, inscrit_parcours=False +): """Inscrit ces etudiants dans ce semestre (la liste doit avoir été vérifiée au préalable) En option: @@ -181,12 +194,11 @@ def do_inscrit(sem, etudids, inscrit_groupes=False, inscrit_parcours=False): (si les deux sont vrais, inscrit_parcours n'a pas d'effet) """ # TODO à ré-écrire pour utiliser les modèles, notamment GroupDescr - formsemestre: FormSemestre = db.session.get(FormSemestre, sem["formsemestre_id"]) formsemestre.setup_parcours_groups() log(f"do_inscrit (inscrit_groupes={inscrit_groupes}): {etudids}") for etudid in etudids: sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules( - sem["formsemestre_id"], + formsemestre.id, etudid, etat=scu.INSCRIT, method="formsemestre_inscr_passage", @@ -210,7 +222,7 @@ def do_inscrit(sem, etudids, inscrit_groupes=False, inscrit_parcours=False): cursem_groups_by_name = { g["group_name"]: g - for g in sco_groups.get_sem_groups(sem["formsemestre_id"]) + for g in sco_groups.get_sem_groups(formsemestre.id) if g["group_name"] } @@ -234,53 +246,46 @@ def do_inscrit(sem, etudids, inscrit_groupes=False, inscrit_parcours=False): sco_groups.change_etud_group_in_partition(etudid, group) -def do_desinscrit(sem: dict, etudids: list[int]): +def do_desinscrit( + formsemestre: FormSemestre, etudids: list[int], check_has_dec_jury=True +): "désinscrit les étudiants indiqués du formsemestre" log(f"do_desinscrit: {etudids}") for etudid in etudids: sco_formsemestre_inscriptions.do_formsemestre_desinscription( - etudid, sem["formsemestre_id"] + etudid, formsemestre.id, check_has_dec_jury=check_has_dec_jury ) -def list_source_sems(sem, delai=None) -> list[dict]: +def _list_source_sems(formsemestre: FormSemestre) -> list[FormSemestre]: """Liste des semestres sources - sem est le semestre destination + formsemestre est le semestre destination """ - # liste des semestres débutant a moins - # de delai (en jours) de la date de fin du semestre d'origine. - sems = sco_formsemestre.do_formsemestre_list() - othersems = [] - d, m, y = [int(x) for x in sem["date_debut"].split("/")] - date_debut_dst = datetime.date(y, m, d) - - delais = datetime.timedelta(delai) - for s in sems: - if s["formsemestre_id"] == sem["formsemestre_id"]: - continue # saute le semestre destination - if s["date_fin"]: - d, m, y = [int(x) for x in s["date_fin"].split("/")] - date_fin = datetime.date(y, m, d) - if date_debut_dst - date_fin > delais: - continue # semestre trop ancien - if date_fin > date_debut_dst: - continue # semestre trop récent - # Elimine les semestres de formations speciales (sans parcours) - if s["semestre_id"] == codes_cursus.NO_SEMESTRE_ID: - continue - # - formation: Formation = Formation.query.get_or_404(s["formation_id"]) - parcours = codes_cursus.get_cursus_from_code(formation.type_parcours) - if not parcours.ALLOW_SEM_SKIP: - if s["semestre_id"] < (sem["semestre_id"] - 1): - continue - othersems.append(s) - return othersems + # liste des semestres du même type de cursus terminant + # pas trop loin de la date de début du semestre destination + date_fin_min = formsemestre.date_debut - datetime.timedelta(days=275) + date_fin_max = formsemestre.date_debut + datetime.timedelta(days=45) + return ( + FormSemestre.query.filter( + FormSemestre.dept_id == formsemestre.dept_id, + # saute le semestre destination: + FormSemestre.id != formsemestre.id, + # et les semestres de formations speciales (monosemestres): + FormSemestre.semestre_id != codes_cursus.NO_SEMESTRE_ID, + # semestre pas trop dans le futur + FormSemestre.date_fin <= date_fin_max, + # ni trop loin dans le passé + FormSemestre.date_fin >= date_fin_min, + ) + .join(Formation) + .filter_by(type_parcours=formsemestre.formation.type_parcours) + ).all() +# view, GET, POST def formsemestre_inscr_passage( formsemestre_id, - etuds=[], + etuds: str | list[int] | list[str] | int | None = None, inscrit_groupes=False, inscrit_parcours=False, submitted=False, @@ -300,36 +305,42 @@ def formsemestre_inscr_passage( - Confirmation: indiquer les étudiants inscrits et ceux désinscrits, le total courant. """ + formsemestre = FormSemestre.get_formsemestre(formsemestre_id) inscrit_groupes = int(inscrit_groupes) inscrit_parcours = int(inscrit_parcours) ignore_jury = int(ignore_jury) - sem = sco_formsemestre.get_formsemestre(formsemestre_id) # -- check lock - if not sem["etat"]: + if not formsemestre.etat: raise ScoValueError("opération impossible: semestre verrouille") - header = html_sco_header.sco_header(page_title="Passage des étudiants") + H = [ + html_sco_header.sco_header( + page_title="Passage des étudiants", + init_qtip=True, + javascripts=["js/etud_info.js"], + ) + ] footer = html_sco_header.sco_footer() - H = [header] + etuds = [] if etuds is None else etuds if isinstance(etuds, str): - # list de strings, vient du form de confirmation + # string, vient du form de confirmation etuds = [int(x) for x in etuds.split(",") if x] elif isinstance(etuds, int): etuds = [etuds] elif etuds and isinstance(etuds[0], str): etuds = [int(x) for x in etuds] - auth_etuds_by_sem, inscrits, candidats = list_authorized_etuds_by_sem( - sem, ignore_jury=ignore_jury + auth_etuds_by_sem, inscrits, candidats = _list_authorized_etuds_by_sem( + formsemestre, ignore_jury=ignore_jury ) etuds_set = set(etuds) candidats_set = set(candidats) inscrits_set = set(inscrits) candidats_non_inscrits = candidats_set - inscrits_set - inscrits_ailleurs = set(list_inscrits_date(sem)) + inscrits_ailleurs = set(list_inscrits_date(formsemestre)) - def set_to_sorted_etud_list(etudset): + def set_to_sorted_etud_list(etudset) -> list[Identite]: etuds = [candidats[etudid] for etudid in etudset] - etuds.sort(key=itemgetter("nom")) + etuds.sort(key=lambda e: e.sort_key) return etuds if submitted: @@ -340,7 +351,7 @@ def formsemestre_inscr_passage( if not submitted: H += _build_page( - sem, + formsemestre, auth_etuds_by_sem, inscrits, candidats_non_inscrits, @@ -355,30 +366,31 @@ def formsemestre_inscr_passage( if a_inscrire: H.append("

    Étudiants à inscrire

      ") for etud in set_to_sorted_etud_list(a_inscrire): - H.append("
    1. %(nomprenom)s
    2. " % etud) + H.append(f"
    3. {etud.nomprenom}
    4. ") H.append("
    ") a_inscrire_en_double = inscrits_ailleurs.intersection(a_inscrire) if a_inscrire_en_double: H.append("

    dont étudiants déjà inscrits:

    ") if a_desinscrire: H.append("

    Étudiants à désinscrire

      ") - for etudid in a_desinscrire: - H.append( - '
    1. %(nomprenom)s
    2. ' - % inscrits[etudid] - ) + a_desinscrire_ident = sorted( + (Identite.query.get(eid) for eid in a_desinscrire), + key=lambda x: x.sort_key, + ) + for etud in a_desinscrire_ident: + H.append(f'
    3. {etud.nomprenom}
    4. ') H.append("
    ") todo = a_inscrire or a_desinscrire if not todo: H.append("""

    Il n'y a rien à modifier !

    """) H.append( scu.confirm_dialog( - dest_url="formsemestre_inscr_passage" - if todo - else "formsemestre_status", + dest_url=( + "formsemestre_inscr_passage" if todo else "formsemestre_status" + ), message="

    Confirmer ?

    " if todo else "", add_headers=False, cancel_url="formsemestre_inscr_passage?formsemestre_id=" @@ -395,16 +407,26 @@ def formsemestre_inscr_passage( ) ) else: + # check decisions jury ici pour éviter de recontruire le cache + # après chaque desinscription + sco_formsemestre_inscriptions.check_if_has_decision_jury( + formsemestre, a_desinscrire + ) + # check decisions jury ici pour éviter de recontruire le cache + # après chaque desinscription + sco_formsemestre_inscriptions.check_if_has_decision_jury( + formsemestre, a_desinscrire + ) with sco_cache.DeferredSemCacheManager(): # Inscription des étudiants au nouveau semestre: do_inscrit( - sem, + formsemestre, a_inscrire, inscrit_groupes=inscrit_groupes, inscrit_parcours=inscrit_parcours, ) # Désinscriptions: - do_desinscrit(sem, a_desinscrire) + do_desinscrit(formsemestre, a_desinscrire, check_has_dec_jury=False) H.append( f"""

    Opération effectuée

    @@ -441,7 +463,7 @@ def formsemestre_inscr_passage( def _build_page( - sem, + formsemestre: FormSemestre, auth_etuds_by_sem, inscrits, candidats_non_inscrits, @@ -450,7 +472,6 @@ def _build_page( inscrit_parcours=False, ignore_jury=False, ): - formsemestre: FormSemestre = db.session.get(FormSemestre, sem["formsemestre_id"]) inscrit_groupes = int(inscrit_groupes) inscrit_parcours = int(inscrit_parcours) ignore_jury = int(ignore_jury) @@ -472,7 +493,7 @@ def _build_page( ), f"""
    - +  aide @@ -491,7 +512,7 @@ def _build_page(
    {scu.EMO_WARNING} - Seuls les semestres dont la date de fin est antérieure à la date de début + Seuls les semestres dont la date de fin est proche de la date de début de ce semestre ({formsemestre.date_debut.strftime("%d/%m/%Y")}) sont pris en compte.
    @@ -499,7 +520,7 @@ def _build_page( - {formsemestre_inscr_passage_help(sem)} + {formsemestre_inscr_passage_help(formsemestre)} """, @@ -524,19 +545,20 @@ def _build_page( return H -def formsemestre_inscr_passage_help(sem: dict): +def formsemestre_inscr_passage_help(formsemestre: FormSemestre): "texte d'aide en bas de la page passage des étudiants" return f"""

    Explications

    Cette page permet d'inscrire des étudiants dans le semestre destination {sem['titreannee']}, + url_for("notes.formsemestre_status", + scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id ) + }">{formsemestre.titre_annee()}, et d'en désincrire si besoin.

    Les étudiants sont groupés par semestre d'origine. Ceux qui sont en caractères - gras sont déjà inscrits dans le semestre destination. - Ceux qui sont en gras et en rouge sont inscrits + gras sont déjà inscrits dans le semestre destination. + Ceux qui sont en gras et en rouge sont inscrits dans un autre semestre.

    Au départ, les étudiants déjà inscrits sont sélectionnés; vous pouvez ajouter @@ -555,7 +577,7 @@ def formsemestre_inscr_passage_help(sem: dict): conserve les groupes, on conserve les parcours (là aussi, pensez à les cocher dans modifier le semestre avant de faire passer les étudiants). @@ -656,25 +678,24 @@ def etuds_select_boxes( H.append("

    ") for etud in etuds: if etud.get("inscrit", False): - c = " inscrit" + c = " deja-inscrit" checked = 'checked="checked"' else: checked = "" if etud["etudid"] in inscrits_ailleurs: - c = " inscrailleurs" + c = " inscrit-ailleurs" else: c = "" sco_etud.format_etud_ident(etud) if etud["etudid"]: - elink = """%s""" % ( - c, - url_for( - "scolar.fiche_etud", + elink = f"""{etud['nomprenom']} + """ else: # ce n'est pas un etudiant ScoDoc elink = etud["nomprenom"] diff --git a/app/scodoc/sco_liste_notes.py b/app/scodoc/sco_liste_notes.py index 44058474..9e668c90 100644 --- a/app/scodoc/sco_liste_notes.py +++ b/app/scodoc/sco_liste_notes.py @@ -490,9 +490,9 @@ def _make_table_notes( rlinks = {"_table_part": "head"} for e in evaluations: rlinks[e.id] = "afficher" - rlinks[ - "_" + str(e.id) + "_help" - ] = "afficher seulement les notes de cette évaluation" + rlinks["_" + str(e.id) + "_help"] = ( + "afficher seulement les notes de cette évaluation" + ) rlinks["_" + str(e.id) + "_target"] = url_for( "notes.evaluation_listenotes", scodoc_dept=g.scodoc_dept, @@ -709,9 +709,9 @@ def _add_eval_columns( notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation.id) if evaluation.date_debut: - titles[ - evaluation.id - ] = f"{evaluation.description} ({evaluation.date_debut.strftime('%d/%m/%Y')})" + titles[evaluation.id] = ( + f"{evaluation.description} ({evaluation.date_debut.strftime('%d/%m/%Y')})" + ) else: titles[evaluation.id] = f"{evaluation.description} " @@ -820,14 +820,17 @@ def _add_eval_columns( row_moys[evaluation.id] = scu.fmt_note( sum_notes / nb_notes, keep_numeric=keep_numeric ) - row_moys[ - "_" + str(evaluation.id) + "_help" - ] = "moyenne sur %d notes (%s le %s)" % ( - nb_notes, - evaluation.description, - evaluation.date_debut.strftime("%d/%m/%Y") - if evaluation.date_debut - else "", + row_moys["_" + str(evaluation.id) + "_help"] = ( + "moyenne sur %d notes (%s le %s)" + % ( + nb_notes, + evaluation.description, + ( + evaluation.date_debut.strftime("%d/%m/%Y") + if evaluation.date_debut + else "" + ), + ) ) else: row_moys[evaluation.id] = "" @@ -884,8 +887,9 @@ def _add_moymod_column( row["_" + col_id + "_td_attrs"] = ' class="moyenne" ' if etudid in inscrits and not isinstance(val, str): notes.append(val) - nb_notes = nb_notes + 1 - sum_notes += val + if not np.isnan(val): + nb_notes = nb_notes + 1 + sum_notes += val row_coefs[col_id] = "(avec abs)" if is_apc: row_poids[col_id] = "à titre indicatif" diff --git a/app/scodoc/sco_moduleimpl.py b/app/scodoc/sco_moduleimpl.py index 67cd380c..5cd3a1c0 100644 --- a/app/scodoc/sco_moduleimpl.py +++ b/app/scodoc/sco_moduleimpl.py @@ -91,7 +91,9 @@ def do_moduleimpl_delete(oid, formsemestre_id=None): ) # > moduleimpl_delete -def moduleimpl_list(moduleimpl_id=None, formsemestre_id=None, module_id=None): +def moduleimpl_list( + moduleimpl_id=None, formsemestre_id=None, module_id=None +) -> list[dict]: "list moduleimpls" args = locals() cnx = ndb.GetDBConnexion() diff --git a/app/scodoc/sco_moduleimpl_inscriptions.py b/app/scodoc/sco_moduleimpl_inscriptions.py index c4b683eb..20467db7 100644 --- a/app/scodoc/sco_moduleimpl_inscriptions.py +++ b/app/scodoc/sco_moduleimpl_inscriptions.py @@ -617,7 +617,7 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->

    L'inscription ou désinscription aux UEs du BUT n'affecte pas les inscriptions aux modules mais permet de "dispenser" un étudiant de suivre certaines UEs de son parcours.

    -

    Il peut s'agit d'étudiants redoublants ayant déjà acquis l'UE, ou d'une UE +

    Il peut s'agir d'étudiants redoublants ayant déjà acquis l'UE, ou d'une UE présente dans le semestre mais pas dans le parcours de l'étudiant, ou bien d'autres cas particuliers.

    diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py index 9ee69aec..336d49ff 100644 --- a/app/scodoc/sco_moduleimpl_status.py +++ b/app/scodoc/sco_moduleimpl_status.py @@ -519,16 +519,22 @@ def _ligne_evaluation( partition_id=partition_id, select_first_partition=True, ) - if evaluation.evaluation_type in ( - scu.EVALUATION_RATTRAPAGE, - scu.EVALUATION_SESSION2, - ): + if evaluation.evaluation_type == Evaluation.EVALUATION_RATTRAPAGE: tr_class = "mievr mievr_rattr" + elif evaluation.evaluation_type == Evaluation.EVALUATION_SESSION2: + tr_class = "mievr mievr_session2" + elif evaluation.evaluation_type == Evaluation.EVALUATION_BONUS: + tr_class = "mievr mievr_bonus" else: tr_class = "mievr" + if not evaluation.visibulletin: tr_class += " non_visible_inter" tr_class_1 = "mievr" + if evaluation.is_blocked(): + tr_class += " evaluation_blocked" + tr_class_1 += " evaluation_blocked" + if not first_eval: H.append(""" """) tr_class_1 += " mievr_spaced" @@ -562,14 +568,18 @@ def _ligne_evaluation( scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id) }" class="mievr_evalnodate">Évaluation sans date""" ) - H.append(f"    {evaluation.description or ''}") - if evaluation.evaluation_type == scu.EVALUATION_RATTRAPAGE: + H.append(f"    {evaluation.description}") + if evaluation.evaluation_type == Evaluation.EVALUATION_RATTRAPAGE: H.append( """rattrapage""" ) - elif evaluation.evaluation_type == scu.EVALUATION_SESSION2: + elif evaluation.evaluation_type == Evaluation.EVALUATION_SESSION2: H.append( - """session 2""" + """session 2""" + ) + elif evaluation.evaluation_type == Evaluation.EVALUATION_BONUS: + H.append( + """bonus""" ) # if etat["last_modif"]: @@ -605,8 +615,15 @@ def _ligne_evaluation( else: H.append(arrow_none) - if etat["evalcomplete"]: - etat_txt = f"""(prise en compte{ + if evaluation.is_blocked(): + etat_txt = f"""évaluation bloquée { + "jusqu'au " + evaluation.blocked_until.strftime("%d/%m/%Y") + if evaluation.blocked_until < Evaluation.BLOCKED_FOREVER + else "" } + """ + etat_descr = """prise en compte bloquée""" + elif etat["evalcomplete"]: + etat_txt = f"""Moyenne (prise en compte{ "" if evaluation.visibulletin else ", cachée en intermédiaire"}) @@ -615,7 +632,7 @@ def _ligne_evaluation( ", évaluation cachée sur les bulletins en version intermédiaire et sur la passerelle" }""" elif etat["evalattente"] and not evaluation.publish_incomplete: - etat_txt = "(prise en compte, mais notes en attente)" + etat_txt = "Moyenne (prise en compte, mais notes en attente)" etat_descr = "il y a des notes en attente" elif evaluation.publish_incomplete: etat_txt = """(prise en compte immédiate)""" @@ -623,28 +640,29 @@ def _ligne_evaluation( "il manque des notes, mais la prise en compte immédiate a été demandée" ) elif etat["nb_notes"] != 0: - etat_txt = "(non prise en compte)" + etat_txt = "Moyenne (non prise en compte)" etat_descr = "il manque des notes" else: etat_txt = "" - if can_edit_evals and etat_txt: - etat_txt = f"""{etat_txt}""" + if etat_txt: + if can_edit_evals: + etat_txt = f"""{etat_txt}""" H.append( f""" - +   Durée Coef. Notes Abs N - Moyenne {etat_txt} + {etat_txt} - + """ ) if can_edit_evals: @@ -826,7 +844,7 @@ def _evaluation_poids_html(evaluation: Evaluation, max_poids: float = 0.0) -> st + "\n".join( [ f"""
    -
    """ diff --git a/app/scodoc/sco_page_etud.py b/app/scodoc/sco_page_etud.py index 5d40b9d4..c5b21b7b 100644 --- a/app/scodoc/sco_page_etud.py +++ b/app/scodoc/sco_page_etud.py @@ -36,7 +36,7 @@ import sqlalchemy as sa from app import log from app.auth.models import User -from app.but import cursus_but +from app.but import cursus_but, validations_view from app.models import Adresse, EtudAnnotation, FormSemestre, Identite, ScoDocSiteConfig from app.scodoc import ( codes_cursus, @@ -445,6 +445,14 @@ def fiche_etud(etudid=None): # Liens vers compétences BUT if last_formsemestre and last_formsemestre.formation.is_apc(): but_cursus = cursus_but.EtudCursusBUT(etud, last_formsemestre.formation) + refcomp = last_formsemestre.formation.referentiel_competence + if refcomp: + ue_validation_by_niveau = validations_view.get_ue_validation_by_niveau( + refcomp, etud + ) + ects_total = sum((v.ects() for v in ue_validation_by_niveau.values())) + else: + ects_total = "" info[ "but_cursus_mkup" ] = f""" @@ -454,15 +462,20 @@ def fiche_etud(etudid=None): cursus=but_cursus, scu=scu, )} - """ diff --git a/app/scodoc/sco_placement.py b/app/scodoc/sco_placement.py index a9e25344..ba87f84b 100644 --- a/app/scodoc/sco_placement.py +++ b/app/scodoc/sco_placement.py @@ -48,20 +48,17 @@ from wtforms import ( HiddenField, SelectMultipleField, ) -from app.models import ModuleImpl +from app.models import Evaluation, ModuleImpl import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb -from app import ScoValueError from app.scodoc import html_sco_header, sco_preferences from app.scodoc import sco_edit_module from app.scodoc import sco_evaluations -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_groups from app.scodoc import sco_moduleimpl -from app.scodoc import sco_permissions_check from app.scodoc.gen_tables import GenTable from app.scodoc import sco_etud import sco_version @@ -138,11 +135,7 @@ class PlacementForm(FlaskForm): def set_evaluation_infos(self, evaluation_id): """Initialise les données du formulaire avec les données de l'évaluation.""" - eval_data = sco_evaluation_db.get_evaluations_dict( - {"evaluation_id": evaluation_id} - ) - if not eval_data: - raise ScoValueError("invalid evaluation_id") + _ = Evaluation.get_evaluation(evaluation_id) # check exist ? self.groups_tree, self.has_groups, self.nb_groups = _get_group_info( evaluation_id ) @@ -239,14 +232,12 @@ class PlacementRunner: self.groups_ids = [ gid if gid != TOUS else form.tous_id for gid in form["groups"].data ] - self.eval_data = sco_evaluation_db.get_evaluations_dict( - {"evaluation_id": self.evaluation_id} - )[0] + self.evaluation = Evaluation.get_evaluation(self.evaluation_id) self.groups = sco_groups.listgroups(self.groups_ids) self.gr_title_filename = sco_groups.listgroups_filename(self.groups) # gr_title = sco_groups.listgroups_abbrev(d['groups']) self.current_user = current_user - self.moduleimpl_id = self.eval_data["moduleimpl_id"] + self.moduleimpl_id = self.evaluation.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( @@ -260,20 +251,25 @@ class PlacementRunner: ) self.evalname = "%s-%s" % ( self.module_data["code"] or "?", - ndb.DateDMYtoISO(self.eval_data["jour"]), + ( + self.evaluation.date_debut.strftime("%Y-%m-%d_%Hh%M") + if self.evaluation.date_debut + else "" + ), ) - if self.eval_data["description"]: - self.evaltitre = self.eval_data["description"] + if self.evaluation.description: + self.evaltitre = self.evaluation.description else: - self.evaltitre = "évaluation du %s" % self.eval_data["jour"] + self.evaltitre = f"""évaluation{ + self.evaluation.date_debut.strftime(' du %d/%m/%Y à %Hh%M') + if self.evaluation.date_debut else ''}""" self.desceval = [ # une liste de chaines: description de l'evaluation - "%s" % self.sem["titreannee"], + self.sem["titreannee"], "Module : %s - %s" % (self.module_data["code"] or "?", self.module_data["abbrev"] or ""), "Surveillants : %s" % self.surveillants, "Batiment : %(batiment)s - Salle : %(salle)s" % self.__dict__, - "Controle : %s (coef. %g)" - % (self.evaltitre, self.eval_data["coefficient"]), + "Controle : %s (coef. %g)" % (self.evaltitre, self.evaluation.coefficient), ] self.styles = None self.plan = None @@ -339,10 +335,10 @@ class PlacementRunner: def _production_pdf(self): pdf_title = "
    ".join(self.desceval) - pdf_title += ( - "\nDate : %(jour)s - Horaire : %(heure_debut)s à %(heure_fin)s" - % self.eval_data - ) + pdf_title += f"""\nDate : {self.evaluation.date_debut.strftime("%d/%m/%Y") + if self.evaluation.date_debut else '-' + } - Horaire : {self.evaluation.heure_debut()} à {self.evaluation.heure_fin() + }""" filename = "placement_%(evalname)s_%(gr_title_filename)s" % self.__dict__ titles = { "nom": "Nom", @@ -489,8 +485,10 @@ class PlacementRunner: worksheet.append_blank_row() worksheet.append_single_cell_row(desceval, self.styles["titres"]) worksheet.append_single_cell_row( - "Date : %(jour)s - Horaire : %(heure_debut)s à %(heure_fin)s" - % self.eval_data, + f"""Date : {self.evaluation.date_debut.strftime("%d/%m/%Y") + if self.evaluation.date_debut else '-' + } - Horaire : {self.evaluation.heure_debut()} à {self.evaluation.heure_fin() + }""", self.styles["titres"], ) diff --git a/app/scodoc/sco_poursuite_dut.py b/app/scodoc/sco_poursuite_dut.py index 2ada0c6b..c271628a 100644 --- a/app/scodoc/sco_poursuite_dut.py +++ b/app/scodoc/sco_poursuite_dut.py @@ -72,9 +72,7 @@ def etud_get_poursuite_info(sem: dict, etud: dict) -> dict: moy_ues.append( ( ue["acronyme"], - scu.fmt_note( - nt.get_etud_ue_status(etudid, ue["ue_id"])["moy"] - ), + scu.fmt_note(ue_status["moy"]), ) ) else: diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py index 8f84e8c6..7110d3d2 100644 --- a/app/scodoc/sco_saisie_notes.py +++ b/app/scodoc/sco_saisie_notes.py @@ -134,12 +134,12 @@ def _displayNote(val): return val -def _check_notes(notes: list[(int, float)], evaluation: Evaluation): - # XXX typehint : float or str +def _check_notes(notes: list[(int, float | str)], evaluation: Evaluation): """notes is a list of tuples (etudid, value) mod is the module (used to ckeck type, for malus) returns list of valid notes (etudid, float value) - and 4 lists of etudid: etudids_invalids, etudids_without_notes, etudids_absents, etudid_to_suppress + and 4 lists of etudid: + etudids_invalids, etudids_without_notes, etudids_absents, etudid_to_suppress """ note_max = evaluation.note_max or 0.0 module: Module = evaluation.moduleimpl.module @@ -148,7 +148,10 @@ def _check_notes(notes: list[(int, float)], evaluation: Evaluation): scu.ModuleType.RESSOURCE, scu.ModuleType.SAE, ): - note_min = scu.NOTES_MIN + if evaluation.evaluation_type == Evaluation.EVALUATION_BONUS: + note_min, note_max = -20, 20 + else: + note_min = scu.NOTES_MIN elif module.module_type == ModuleType.MALUS: note_min = -20.0 else: @@ -881,7 +884,7 @@ def feuille_saisie_notes(evaluation_id, group_ids=[]): if evaluation.date_debut: indication_date = evaluation.date_debut.date().isoformat() else: - indication_date = scu.sanitize_filename(evaluation.description or "")[:12] + indication_date = scu.sanitize_filename(evaluation.description)[:12] eval_name = f"{evaluation.moduleimpl.module.code}-{indication_date}" date_str = ( diff --git a/app/scodoc/sco_synchro_etuds.py b/app/scodoc/sco_synchro_etuds.py index 5cab9c87..3f5008e3 100644 --- a/app/scodoc/sco_synchro_etuds.py +++ b/app/scodoc/sco_synchro_etuds.py @@ -35,7 +35,7 @@ from flask import g, url_for from flask_login import current_user from app import db, log -from app.models import Admission, Adresse, Identite, ScolarNews +from app.models import Admission, Adresse, FormSemestre, Identite, ScolarNews import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb @@ -94,6 +94,7 @@ def formsemestre_synchro_etuds( que l'on va importer/inscrire """ etuds = etuds or [] + formsemestre = FormSemestre.get_formsemestre(formsemestre_id) inscrits_without_key = inscrits_without_key or [] log(f"formsemestre_synchro_etuds: formsemestre_id={formsemestre_id}") sem = sco_formsemestre.get_formsemestre(formsemestre_id) @@ -109,12 +110,13 @@ def formsemestre_synchro_etuds( raise ScoValueError("opération impossible: semestre verrouille") if not sem["etapes"]: raise ScoValueError( - """opération impossible: ce semestre n'a pas de code étape - (voir "Modifier ce semestre") + f"""opération impossible: ce semestre n'a pas de code étape + (voir Modifier ce semestre) """ - % sem ) - header = html_sco_header.sco_header(page_title="Synchronisation étudiants") footer = html_sco_header.sco_footer() base_url = url_for( "notes.formsemestre_synchro_etuds", @@ -165,7 +167,13 @@ def formsemestre_synchro_etuds( suffix=scu.XLSX_SUFFIX, ) - H = [header] + H = [ + html_sco_header.sco_header( + page_title="Synchronisation étudiants", + init_qtip=True, + javascripts=["js/etud_info.js"], + ) + ] if not submitted: H += _build_page( sem, @@ -184,7 +192,7 @@ def formsemestre_synchro_etuds( inscrits_without_key ) log("a_desinscrire_without_key=%s" % a_desinscrire_without_key) - inscrits_ailleurs = set(sco_inscr_passage.list_inscrits_date(sem)) + inscrits_ailleurs = set(sco_inscr_passage.list_inscrits_date(formsemestre)) a_inscrire = a_inscrire.intersection(etuds_set) if not dialog_confirmed: @@ -205,10 +213,12 @@ def formsemestre_synchro_etuds( a_inscrire_en_double = inscrits_ailleurs.intersection(a_inscrire) if a_inscrire_en_double: - H.append("

    dont étudiants déjà inscrits:

      ") + H.append( + "

      dont étudiants déjà inscrits dans un autre semestre:

        " + ) for key in a_inscrire_en_double: nom = f"""{etudsapo_ident[key]['nom']} {etudsapo_ident[key].get("prenom", "")}""" - H.append(f'
      1. {nom}
      2. ') + H.append(f'
      3. {nom}
      4. ') H.append("
      ") if a_desinscrire: @@ -260,16 +270,26 @@ def formsemestre_synchro_etuds( etudids_a_desinscrire = [nip2etudid(x) for x in a_desinscrire] etudids_a_desinscrire += a_desinscrire_without_key # + # check decisions jury ici pour éviter de recontruire le cache + # après chaque desinscription + sco_formsemestre_inscriptions.check_if_has_decision_jury( + formsemestre, a_desinscrire + ) with sco_cache.DeferredSemCacheManager(): - do_import_etuds_from_portal(sem, a_importer, etudsapo_ident) - sco_inscr_passage.do_inscrit(sem, etudids_a_inscrire) - sco_inscr_passage.do_desinscrit(sem, etudids_a_desinscrire) + do_import_etuds_from_portal(formsemestre, a_importer, etudsapo_ident) + sco_inscr_passage.do_inscrit(formsemestre, etudids_a_inscrire) + sco_inscr_passage.do_desinscrit( + formsemestre, etudids_a_desinscrire, check_has_dec_jury=False + ) H.append( - """

      Opération effectuée

      + f"""

      Opération effectuée

        -
      • Continuer la synchronisation
      • """ - % formsemestre_id +
      • Continuer la synchronisation +
      • """ ) # partitions = sco_groups.get_partitions_list( @@ -279,8 +299,9 @@ def formsemestre_synchro_etuds( H.append( f"""
      • Répartir les groupes de {partitions[0]["partition_name"]}
      • + scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id + )}">Répartir les groupes de {partitions[0]["partition_name"]} + """ ) @@ -618,7 +639,7 @@ def get_annee_naissance(ddmmyyyyy: str) -> int: return None -def do_import_etuds_from_portal(sem, a_importer, etudsapo_ident): +def do_import_etuds_from_portal(formsemestre: FormSemestre, a_importer, etudsapo_ident): """Inscrit les etudiants Apogee dans ce semestre.""" log(f"do_import_etuds_from_portal: a_importer={a_importer}") if not a_importer: @@ -672,7 +693,7 @@ def do_import_etuds_from_portal(sem, a_importer, etudsapo_ident): # Inscription au semestre sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules( - sem["formsemestre_id"], + formsemestre.id, etud.id, etat=scu.INSCRIT, etape=args["etape"], @@ -716,7 +737,7 @@ def do_import_etuds_from_portal(sem, a_importer, etudsapo_ident): ScolarNews.add( typ=ScolarNews.NEWS_INSCR, text=f"Import Apogée de {len(created_etudids)} étudiants en ", - obj=sem["formsemestre_id"], + obj=formsemestre.id, ) diff --git a/app/scodoc/sco_ue_external.py b/app/scodoc/sco_ue_external.py index 4d32a405..f1840386 100644 --- a/app/scodoc/sco_ue_external.py +++ b/app/scodoc/sco_ue_external.py @@ -175,7 +175,7 @@ def external_ue_inscrit_et_note( note_max=20.0, coefficient=1.0, publish_incomplete=True, - evaluation_type=scu.EVALUATION_NORMALE, + evaluation_type=Evaluation.EVALUATION_NORMALE, visibulletin=False, description="note externe", ) diff --git a/app/scodoc/sco_undo_notes.py b/app/scodoc/sco_undo_notes.py index b04ba870..480e0b2e 100644 --- a/app/scodoc/sco_undo_notes.py +++ b/app/scodoc/sco_undo_notes.py @@ -48,16 +48,15 @@ Opérations: import datetime from flask import request -from app.models import FormSemestre +from app.models import Evaluation, FormSemestre from app.scodoc.intervals import intervalmap import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app.scodoc import sco_evaluation_db -from app.scodoc import sco_moduleimpl from app.scodoc import sco_preferences from app.scodoc import sco_users -import sco_version from app.scodoc.gen_tables import GenTable +import sco_version # deux notes (de même uid) sont considérées comme de la même opération si # elles sont séparées de moins de 2*tolerance: @@ -149,10 +148,8 @@ def list_operations(evaluation_id): def evaluation_list_operations(evaluation_id): """Page listing operations on evaluation""" - E = sco_evaluation_db.get_evaluations_dict({"evaluation_id": evaluation_id})[0] - M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] - - Ops = list_operations(evaluation_id) + evaluation = Evaluation.get_evaluation(evaluation_id) + operations = list_operations(evaluation_id) columns_ids = ("datestr", "user_name", "nb_notes", "comment") titles = { @@ -164,11 +161,14 @@ def evaluation_list_operations(evaluation_id): tab = GenTable( titles=titles, columns_ids=columns_ids, - rows=Ops, + rows=operations, html_sortable=False, - html_title="

        Opérations sur l'évaluation %s du %s

        " - % (E["description"], E["jour"]), - preferences=sco_preferences.SemPreferences(M["formsemestre_id"]), + html_title=f"""

        Opérations sur l'évaluation {evaluation.description} { + evaluation.date_debut.strftime("du %d/%m/%Y") if evaluation.date_debut else "(sans date)" + }

        """, + preferences=sco_preferences.SemPreferences( + evaluation.moduleimpl.formsemestre_id + ), ) return tab.make_page() diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 6b385099..7e03f38f 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -454,10 +454,6 @@ NOTES_MENTIONS_LABS = ( "Excellent", ) -EVALUATION_NORMALE = 0 -EVALUATION_RATTRAPAGE = 1 -EVALUATION_SESSION2 = 2 - # Dates et années scolaires # Ces dates "pivot" sont paramétrables dans les préférences générales # on donne ici les valeurs par défaut. diff --git a/app/scodoc/sco_vdi.py b/app/scodoc/sco_vdi.py index 9b8e50fe..09d1a90a 100644 --- a/app/scodoc/sco_vdi.py +++ b/app/scodoc/sco_vdi.py @@ -25,12 +25,14 @@ # ############################################################################## -"""Classe stockant le VDI avec le code étape (noms de fichiers maquettes et code semestres) +"""Apogée: gestion du VDI avec le code étape (noms de fichiers maquettes et code semestres) """ from app.scodoc.sco_exceptions import ScoValueError class ApoEtapeVDI(object): + """Classe stockant le VDI avec le code étape (noms de fichiers maquettes et code semestres)""" + _ETAPE_VDI_SEP = "!" def __init__(self, etape_vdi: str = None, etape: str = "", vdi: str = ""): @@ -110,7 +112,8 @@ class ApoEtapeVDI(object): elif len(t) == 2: etape, vdi = t else: - raise ValueError("invalid code etape") + # code étape invalide + etape, vdi = "", "" return etape, vdi else: return etape_vdi, "" diff --git a/app/static/css/jury_but.css b/app/static/css/jury_but.css index 2e3f0f71..c75c1a50 100644 --- a/app/static/css/jury_but.css +++ b/app/static/css/jury_but.css @@ -35,6 +35,11 @@ min-width: var(--sco-content-min-width); max-width: var(--sco-content-max-width); } +div.jury_but_warning { + background-color: yellow; + border-color: red; + padding-bottom: 4px; +} div.jury_but_box_title { margin-bottom: 10px; } diff --git a/app/static/css/releve-but.css b/app/static/css/releve-but.css index 25a31c97..b0c0f533 100644 --- a/app/static/css/releve-but.css +++ b/app/static/css/releve-but.css @@ -273,6 +273,10 @@ section>div:nth-child(1) { min-width: 80px; display: inline-block; } +div.eval-bonus { + color: #197614; + background-color: pink; +} .ueBonus, .ueBonus h3 { @@ -280,7 +284,7 @@ section>div:nth-child(1) { color: #000 !important; } /* UE Capitalisée */ -.synthese .ue.capitalisee, +.synthese .ue.capitalisee, .ue.capitalisee>h3{ background: var(--couleurFondTitresUECapitalisee);; } diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 948554f2..84e09201 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -962,10 +962,18 @@ td.fichetitre2 .fl { div.section_but { display: flex; flex-direction: row; - align-items: center; + align-items: flex-end; justify-content: space-evenly; } - +div.fiche_but_col2 { + display: flex; + flex-direction: column; + justify-content: space-between; +} +div.fiche_total_etcs { + font-weight: bold; + margin-top: 16px; +} div.section_but > div.link_validation_rcues { align-self: center; text-align: center; @@ -1461,6 +1469,9 @@ span.eval_title { font-size: 14pt; } +#evaluation-edit-blocked td, #evaluation-edit-coef td { + padding-top: 24px; +} /* #saisie_notes span.eval_title { border-bottom: 1px solid rgb(100,100,100); } @@ -1793,11 +1804,42 @@ table.formsemestre_status { tr.formsemestre_status { background-color: rgb(90%, 90%, 90%); } - +table.formsemestre_status tr td:first-child { + padding-left: 4px; +} +table.formsemestre_status tr td:last-child { + padding-right: 8px; +} tr.formsemestre_status_green { background-color: #eff7f2; } +tr.modimpl_non_conforme td { + background-color: #ffc458; +} +tr.modimpl_non_conforme td, tr.modimpl_attente td { + padding-top: 4px; + padding-bottom: 4px; +} +tr.modimpl_has_blocked span.nb_evals_blocked, tr span.evals_attente { + background-color: yellow; + border-radius: 4px; + font-weight: bold; + margin-left: 8px; + padding-left: 4px; + padding-right: 4px; +} +tr.modimpl_has_blocked span.nb_evals_blocked { + color: red; +} +tr span.evals_attente { + background-color: orange; + color: green; +} +table.formsemestre_status a.redlink { + text-decoration: none; +} + tr.formsemestre_status_ue { background-color: rgb(90%, 90%, 90%); } @@ -2075,15 +2117,23 @@ th.moduleimpl_evaluations a:hover { text-decoration: underline; } +tr.mievr_in.evaluation_blocked th.moduleimpl_evaluation_moy span, tr.evaluation_blocked th.moduleimpl_evaluation_moy a { + font-weight: bold; + color: red; + background-color: yellow; + padding: 2px; + border-radius: 2px; +} + tr.mievr { background-color: #eeeeee; } -tr.mievr_rattr { +tr.mievr_rattr, tr.mievr_session2, tr.mievr_bonus { background-color: #dddddd; } -span.mievr_rattr { +span.mievr_rattr, span.mievr_session2, span.mievr_bonus { display: inline-block; font-weight: bold; font-size: 80%; @@ -2129,6 +2179,16 @@ tr.mievr.non_visible_inter th { ); } +tr.mievr_tit.evaluation_blocked td,tr.mievr_tit.evaluation_blocked th { + background-image: radial-gradient(#bd7777 1px, transparent 1px); + background-size: 10px 10px; +} +tr.mievr_in.evaluation_blocked td, tr.mievr_in.evaluation_blocked th { + background-color: rgb(195, 235, 255); + padding-top: 4px; +} + + tr.mievr th { background-color: white; } @@ -2139,6 +2199,7 @@ tr.mievr td.mievr { tr.mievr td.mievr_menu { width: 110px; + padding-bottom: 4px; } tr.mievr td.mievr_dur { @@ -2411,6 +2472,29 @@ div.formation_list_ues_titre { color: #eee; } +div.formation_semestre_niveaux_warning { + font-weight: bold; + color: red; + padding: 4px; + margin-top: 8px; + margin-left: 24px; + margin-right: 24px; + background-color: yellow; + border-radius: 8px; +} +div.formation_semestre_niveaux_warning div { + color: black; + font-size: 110%; +} +div.formation_semestre_niveaux_warning ul { + list-style-type: none; + padding-left: 0; +} +div.formation_semestre_niveaux_warning ul li:before { + content: '⚠️'; + margin-right: 10px; /* Adjust space between emoji and text */ +} + div.formation_list_modules, div.formation_list_ues { border-radius: 18px; @@ -2426,6 +2510,7 @@ div.formation_list_ues { } div.formation_list_ues_content { + margin-top: 4px; } div.formation_list_modules { @@ -2508,7 +2593,13 @@ div.formation_parcs > div { opacity: 0.7; border-radius: 4px; text-align: center; - padding: 4px 8px; + padding: 2px 6px; + margin-top: 8px; + margin-bottom: 2px; +} +div.formation_parcs > div.ue_tc { + color: black; + font-style: italic; } div.formation_parcs > div.focus { @@ -3316,14 +3407,24 @@ li.tf-msg { padding-bottom: 5px; } -.warning { - font-weight: bold; +.warning, .warning-bloquant { color: red; + margin-left: 16px; + margin-bottom: 8px; + min-width: var(--sco-content-min-width); + max-width: var(--sco-content-max-width); } .warning::before { - content: url(/ScoDoc/static/icons/warning_img.png); - vertical-align: -80%; + content:""; + margin-right: 8px; + height:32px; + width: 32px; + background-size: 32px 32px; + background-image: url(/ScoDoc/static/icons/warning_std.svg); + background-repeat: no-repeat; + display: inline-block; + vertical-align: -40%; } .warning-light { @@ -3336,6 +3437,19 @@ li.tf-msg { /* EMO_WARNING, "⚠️" */ } +.warning-bloquant::before { + content:""; + margin-right: 8px; + height:32px; + width: 32px; + background-size: 32px 32px; + background-image: url(/ScoDoc/static/icons/warning_bloquant.svg); + background-repeat: no-repeat; + display: inline-block; + vertical-align: -40%; +} + + p.error { font-weight: bold; color: red; @@ -3714,10 +3828,17 @@ span.sp_etape { color: black; } -.inscrailleurs { +.deja-inscrit { + font-weight: bold; + color: rgb(1, 76, 1) !important; +} +.inscrit-ailleurs { font-weight: bold; color: red !important; } +div.etuds_select_boxes { + margin-bottom: 16px; +} span.paspaye, span.paspaye a { @@ -4682,6 +4803,10 @@ table.table_recap th.col_malus { font-weight: bold; color: rgb(165, 0, 0); } +table.table_recap td.col_eval_bonus, +table.table_recap th.col_eval_bonus { + color: #90c; +} table.table_recap tr.ects td { color: rgb(160, 86, 3); diff --git a/app/static/js/releve-but.js b/app/static/js/releve-but.js index d76ec535..2523b227 100644 --- a/app/static/js/releve-but.js +++ b/app/static/js/releve-but.js @@ -491,14 +491,15 @@ class releveBUT extends HTMLElement { let output = ""; evaluations.forEach((evaluation) => { output += ` -
        +
        ${this.URL(evaluation.url, evaluation.description || "Évaluation")}
        ${evaluation.note.value} - Coef. ${evaluation.coef ?? "*"} + ${evaluation.evaluation_type == 0 ? "Coef." : evaluation.evaluation_type == 3 ? "Bonus" : "" + } ${evaluation.coef ?? ""}
        -
        Coef
        ${evaluation.coef}
        +
        ${evaluation.evaluation_type == 0 ? "Coef." : ""}
        ${evaluation.coef ?? ""}
        Max. promo.
        ${evaluation.note.max}
        Moy. promo.
        ${evaluation.note.moy}
        Min. promo.
        ${evaluation.note.min}
        diff --git a/app/tables/recap.py b/app/tables/recap.py index f4882983..01947f63 100644 --- a/app/tables/recap.py +++ b/app/tables/recap.py @@ -13,7 +13,7 @@ import numpy as np from app import db from app.auth.models import User from app.comp.res_common import ResultatsSemestre -from app.models import Identite, FormSemestre, UniteEns +from app.models import Identite, Evaluation, FormSemestre, UniteEns from app.scodoc.codes_cursus import UE_SPORT, DEF from app.scodoc import sco_evaluation_db from app.scodoc import sco_groups @@ -405,15 +405,22 @@ class TableRecap(tb.Table): val = notes_db[etudid]["value"] else: # Note manquante mais prise en compte immédiate: affiche ATT - val = scu.NOTES_ATTENTE + val = ( + scu.NOTES_ATTENTE + if e.evaluation_type != Evaluation.EVALUATION_BONUS + else "" + ) content = self.fmt_note(val) - classes = col_classes + [ - { - "ABS": "abs", - "ATT": "att", - "EXC": "exc", - }.get(content, "") - ] + if e.evaluation_type != Evaluation.EVALUATION_BONUS: + classes = col_classes + [ + { + "ABS": "abs", + "ATT": "att", + "EXC": "exc", + }.get(content, "") + ] + else: + classes = col_classes + ["col_eval_bonus"] row.add_cell( col_id, title, content, group="eval", classes=classes ) @@ -450,7 +457,7 @@ class TableRecap(tb.Table): row_descr_eval.add_cell( col_id, None, - e.description or "", + e.description, target=url_for( "notes.evaluation_listenotes", scodoc_dept=g.scodoc_dept, diff --git a/app/templates/assiduites/pages/etat_abs_date.j2 b/app/templates/assiduites/pages/etat_abs_date.j2 index d40b61e8..22ecb8e5 100644 --- a/app/templates/assiduites/pages/etat_abs_date.j2 +++ b/app/templates/assiduites/pages/etat_abs_date.j2 @@ -20,7 +20,7 @@ Assiduité lors de l'évaluation {{evaluation.description or ''}} + }}">{{evaluation.description}} {% endif %} {{scu.ICON_XLS|safe}}
        diff --git a/app/templates/babase.j2 b/app/templates/babase.j2 index 6394643c..45d9d1dd 100644 --- a/app/templates/babase.j2 +++ b/app/templates/babase.j2 @@ -14,6 +14,7 @@ {%- block styles %} + {%- endblock styles %} {%- endblock head %} @@ -26,7 +27,14 @@ {% block scripts %} + + + + + + + diff --git a/app/templates/pn/form_ues.j2 b/app/templates/pn/form_ues.j2 index 133ccb66..437d4da2 100644 --- a/app/templates/pn/form_ues.j2 +++ b/app/templates/pn/form_ues.j2 @@ -4,6 +4,7 @@
        Unités d'Enseignement semestre {{semestre_idx}}  -  {{ects_by_sem[semestre_idx] | safe}} ECTS
        + {{ html_ue_warning[semestre_idx] | safe }}
          {% for ue in ues_by_sem[semestre_idx] %} @@ -62,6 +63,8 @@
          {% for parc in ue.parcours %}
          {{ parc.code }}
          + {% else %} +
          Tronc Commun
          {% endfor %}
          {% endif %} diff --git a/app/templates/sco_page.j2 b/app/templates/sco_page.j2 index f50bd4cb..00ae2ec4 100644 --- a/app/templates/sco_page.j2 +++ b/app/templates/sco_page.j2 @@ -43,13 +43,6 @@ {{ super() }} - - - - - - -