diff --git a/app/__init__.py b/app/__init__.py index 41875f33..0943a91f 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -357,7 +357,7 @@ def sco_db_insert_constants(): current_app.logger.info("Init Sco db") # Modalités: - models.NotesFormModalite.insert_modalites() + models.FormationModalite.insert_modalites() def initialize_scodoc_database(erase=False, create_all=False): diff --git a/app/comp/moy_ue.py b/app/comp/moy_ue.py new file mode 100644 index 00000000..b2eb29e0 --- /dev/null +++ b/app/comp/moy_ue.py @@ -0,0 +1,54 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# Gestion scolarite IUT +# +# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Emmanuel Viennet emmanuel.viennet@viennet.net +# +############################################################################## + +"""Fonctions de calcul des moyennes d'UE +""" +import numpy as np +import pandas as pd + +from app import db +from app import models + + +def df_load_ue_coefs(formation_id): + """Load coefs of all modules in formation and returns a DataFrame + rows = modules, columns = UE, value = coef. + Unspecified coefs (not defined in db) are set to zero. + """ + ues = models.UniteEns.query.filter_by(formation_id=formation_id).all() + modules = models.Module.query.filter_by(formation_id=formation_id).all() + ue_ids = [ue.id for ue in ues] + module_ids = [module.id for module in modules] + df = pd.DataFrame(columns=ue_ids, index=module_ids, dtype=float) + for mod_coef in ( + db.session.query(models.ModuleUECoef) + .filter(models.UniteEns.formation_id == formation_id) + .filter(models.ModuleUECoef.ue_id == models.UniteEns.id) + ): + df[mod_coef.ue_id][mod_coef.module_id] = mod_coef.coef + df.fillna(value=0, inplace=True) + return df diff --git a/app/models/__init__.py b/app/models/__init__.py index 3d164fe4..100b0a93 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -31,26 +31,27 @@ from app.models.etudiants import ( ) from app.models.events import Scolog, ScolarNews from app.models.formations import ( - NotesFormation, - NotesUE, - NotesMatiere, - NotesModule, + Formation, + UniteEns, + Matiere, + Module, + ModuleUECoef, NotesTag, notes_modules_tags, ) from app.models.formsemestre import ( FormSemestre, - NotesFormsemestreEtape, - NotesFormModalite, - NotesFormsemestreUECoef, - NotesFormsemestreUEComputationExpr, - NotesFormsemestreCustomMenu, - NotesFormsemestreInscription, + FormsemestreEtape, + FormationModalite, + FormsemestreUECoef, + FormsemestreUEComputationExpr, + FormsemestreCustomMenu, + FormsemestreInscription, notes_formsemestre_responsables, - NotesModuleImpl, + ModuleImpl, notes_modules_enseignants, - NotesModuleImplInscription, - NotesEvaluation, + ModuleImplInscription, + Evaluation, EvaluationUEPoids, NotesSemSet, notes_semset_formsemestre, @@ -61,7 +62,7 @@ from app.models.notes import ( ScolarEvent, ScolarFormsemestreValidation, ScolarAutorisationInscription, - NotesAppreciations, + BulAppreciations, NotesNotes, NotesNotesLog, ) diff --git a/app/models/but_pn.py b/app/models/but_pn.py index 75f83a35..1e142d23 100644 --- a/app/models/but_pn.py +++ b/app/models/but_pn.py @@ -22,7 +22,7 @@ class AppCrit(db.Model): titre = db.Column(db.Text(), info={"label": "Titre"}) modules = db.relationship( - "NotesModule", secondary=Modules_ACs, lazy="dynamic", backref="acs" + "Module", secondary=Modules_ACs, lazy="dynamic", backref="acs" ) def to_dict(self): diff --git a/app/models/departements.py b/app/models/departements.py index 36aa8d4c..aa9c1006 100644 --- a/app/models/departements.py +++ b/app/models/departements.py @@ -21,9 +21,7 @@ class Departement(db.Model): entreprises = db.relationship("Entreprise", lazy="dynamic", backref="departement") etudiants = db.relationship("Identite", lazy="dynamic", backref="departement") - formations = db.relationship( - "NotesFormation", lazy="dynamic", backref="departement" - ) + formations = db.relationship("Formation", lazy="dynamic", backref="departement") formsemestres = db.relationship( "FormSemestre", lazy="dynamic", backref="departement" ) diff --git a/app/models/formations.py b/app/models/formations.py index 046a3e73..fd1f0189 100644 --- a/app/models/formations.py +++ b/app/models/formations.py @@ -5,10 +5,11 @@ from typing import Any from app import db from app.models import APO_CODE_STR_LEN from app.models import SHORT_STR_LEN +from app.scodoc import sco_utils as scu from app.scodoc.sco_utils import ModuleType -class NotesFormation(db.Model): +class Formation(db.Model): """Programme pédagogique d'une formation""" __tablename__ = "notes_formations" @@ -31,16 +32,16 @@ class NotesFormation(db.Model): type_parcours = db.Column(db.Integer, default=0, server_default="0") code_specialite = db.Column(db.String(SHORT_STR_LEN)) - ues = db.relationship("NotesUE", backref="formation", lazy="dynamic") + ues = db.relationship("UniteEns", backref="formation", lazy="dynamic") formsemestres = db.relationship("FormSemestre", lazy="dynamic", backref="formation") - ues = db.relationship("NotesUE", lazy="dynamic", backref="formation") + ues = db.relationship("UniteEns", lazy="dynamic", backref="formation") def __repr__(self): return f"<{self.__class__.__name__}(id={self.id}, dept_id={self.dept_id}, acronyme='{self.acronyme}')>" -class NotesUE(db.Model): - """Unité d'Enseignement""" +class UniteEns(db.Model): + """Unité d'Enseignement (UE)""" __tablename__ = "notes_ue" @@ -68,14 +69,14 @@ class NotesUE(db.Model): coefficient = db.Column(db.Float) # relations - matieres = db.relationship("NotesMatiere", lazy="dynamic", backref="ue") - modules = db.relationship("NotesModule", lazy="dynamic", backref="ue") + matieres = db.relationship("Matiere", lazy="dynamic", backref="ue") + modules = db.relationship("Module", lazy="dynamic", backref="ue") def __repr__(self): return f"<{self.__class__.__name__}(id={self.id}, formation_id={self.formation_id}, acronyme='{self.acronyme}')>" -class NotesMatiere(db.Model): +class Matiere(db.Model): """Matières: regroupe les modules d'une UE La matière a peu d'utilité en dehors de la présentation des modules d'une UE. @@ -90,10 +91,10 @@ class NotesMatiere(db.Model): titre = db.Column(db.Text()) numero = db.Column(db.Integer) # ordre de présentation - modules = db.relationship("NotesModule", lazy="dynamic", backref="matiere") + modules = db.relationship("Module", lazy="dynamic", backref="matiere") -class NotesModule(db.Model): +class Module(db.Model): """Module""" __tablename__ = "notes_modules" @@ -107,7 +108,7 @@ class NotesModule(db.Model): heures_cours = db.Column(db.Float) heures_td = db.Column(db.Float) heures_tp = db.Column(db.Float) - coefficient = db.Column(db.Float) # coef PPN + coefficient = db.Column(db.Float) # coef PPN (sauf en APC) ects = db.Column(db.Float) # Crédits ECTS ue_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), index=True) formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id")) @@ -120,13 +121,75 @@ class NotesModule(db.Model): # Type: ModuleType: DEFAULT, MALUS, RESSOURCE, MODULE_SAE (enum) module_type = db.Column(db.Integer) # Relations: - modimpls = db.relationship("NotesModuleImpl", backref="module", lazy="dynamic") + modimpls = db.relationship("ModuleImpl", backref="module", lazy="dynamic") + ues_apc = db.relationship("UniteEns", secondary="module_ue_coef", viewonly=True) + + def __init__(self, **kwargs): + self.ue_coefs = [] + super(Module, self).__init__(**kwargs) def __repr__(self): return ( f"" ) + def type_name(self): + return scu.MODULE_TYPE_NAMES[self.module_type] + + def set_ue_coef(self, ue, coef: float) -> None: + """Set coef module vers cette UE""" + self.update_ue_coef_dict({ue.id: coef}) + + def set_ue_coef_dict(self, ue_coef_dict: dict) -> None: + """set coefs vers les UE (remplace existants) + ue_coef_dict = { ue_id : coef } + """ + ue_coefs = [] + for ue_id, coef in ue_coef_dict.items(): + ue = UniteEns.query.get(ue_id) + ue_coefs.append(ModuleUECoef(module=self, ue=ue, coef=coef)) + self.ue_coefs = ue_coefs + + def update_ue_coef_dict(self, ue_coef_dict: dict): + """update coefs vers UE (ajoute aux existants)""" + current = self.get_ue_coef_dict() + current.update(ue_coef_dict) + self.set_ue_coef_dict(current) + + def get_ue_coef_dict(self): + """returns { ue_id : coef }""" + return {p.ue.id: p.coef for p in self.ue_coefs} + + def delete_ue_coef(self, ue): + """delete coef""" + ue_coef = ModuleUECoef.query.get((self.id, ue.id)) + db.session.delete(ue_coef) + + +class ModuleUECoef(db.Model): + """Coefficients des modules vers les UE (APC, BUT) + En mode APC, ces coefs remplacent le coefficient "PPN" du module. + """ + + __tablename__ = "module_ue_coef" + + module_id = db.Column( + db.Integer, db.ForeignKey("notes_modules.id"), primary_key=True + ) + ue_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), primary_key=True) + coef = db.Column( + db.Float, + nullable=False, + ) + module = db.relationship( + Module, + backref=db.backref("ue_coefs", cascade="all, delete-orphan"), + ) + ue = db.relationship( + UniteEns, + backref=db.backref("module_ue_coefs", cascade="all, delete-orphan"), + ) + class NotesTag(db.Model): """Tag sur un module""" diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index b92e2241..f4eb8da3 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -8,7 +8,10 @@ from app import db from app.models import APO_CODE_STR_LEN from app.models import SHORT_STR_LEN from app.models import CODE_STR_LEN -from app.models import NotesUE +from app.models import UniteEns + +import app.scodoc.notesdb as ndb +from app.scodoc import sco_evaluation_db class FormSemestre(db.Model): @@ -73,11 +76,9 @@ class FormSemestre(db.Model): # Relations: etapes = db.relationship( - "NotesFormsemestreEtape", cascade="all,delete", backref="formsemestre" - ) - formsemestres = db.relationship( - "NotesModuleImpl", backref="formsemestre", lazy="dynamic" + "FormsemestreEtape", cascade="all,delete", backref="formsemestre" ) + modimpls = db.relationship("ModuleImpl", backref="formsemestre", lazy="dynamic") # Ancien id ScoDoc7 pour les migrations de bases anciennes # ne pas utiliser après migrate_scodoc7_dept_archives @@ -86,7 +87,7 @@ class FormSemestre(db.Model): def __init__(self, **kwargs): super(FormSemestre, self).__init__(**kwargs) if self.modalite is None: - self.modalite = NotesFormModalite.DEFAULT_MODALITE + self.modalite = FormationModalite.DEFAULT_MODALITE # Association id des utilisateurs responsables (aka directeurs des etudes) du semestre @@ -101,7 +102,7 @@ notes_formsemestre_responsables = db.Table( ) -class NotesFormsemestreEtape(db.Model): +class FormsemestreEtape(db.Model): """Étape Apogée associées au semestre""" __tablename__ = "notes_formsemestre_etapes" @@ -113,7 +114,7 @@ class NotesFormsemestreEtape(db.Model): etape_apo = db.Column(db.String(APO_CODE_STR_LEN)) -class NotesFormModalite(db.Model): +class FormationModalite(db.Model): """Modalités de formation, utilisées pour la présentation (grouper les semestres, générer des codes, etc.) """ @@ -140,7 +141,7 @@ class NotesFormModalite(db.Model): numero = 0 try: for (code, titre) in ( - (NotesFormModalite.DEFAULT_MODALITE, "Formation Initiale"), + (FormationModalite.DEFAULT_MODALITE, "Formation Initiale"), ("FAP", "Apprentissage"), ("FC", "Formation Continue"), ("DEC", "Formation Décalées"), @@ -151,9 +152,9 @@ class NotesFormModalite(db.Model): ("EXT", "Extérieur"), ("OTHER", "Autres formations"), ): - modalite = NotesFormModalite.query.filter_by(modalite=code).first() + modalite = FormationModalite.query.filter_by(modalite=code).first() if modalite is None: - modalite = NotesFormModalite( + modalite = FormationModalite( modalite=code, titre=titre, numero=numero ) db.session.add(modalite) @@ -164,7 +165,7 @@ class NotesFormModalite(db.Model): raise -class NotesFormsemestreUECoef(db.Model): +class FormsemestreUECoef(db.Model): """Coef des UE capitalisees arrivant dans ce semestre""" __tablename__ = "notes_formsemestre_uecoef" @@ -183,7 +184,7 @@ class NotesFormsemestreUECoef(db.Model): coefficient = db.Column(db.Float, nullable=False) -class NotesFormsemestreUEComputationExpr(db.Model): +class FormsemestreUEComputationExpr(db.Model): """Formules utilisateurs pour calcul moyenne UE""" __tablename__ = "notes_formsemestre_ue_computation_expr" @@ -203,7 +204,7 @@ class NotesFormsemestreUEComputationExpr(db.Model): computation_expr = db.Column(db.Text()) -class NotesFormsemestreCustomMenu(db.Model): +class FormsemestreCustomMenu(db.Model): """Menu custom associe au semestre""" __tablename__ = "notes_formsemestre_custommenu" @@ -219,7 +220,7 @@ class NotesFormsemestreCustomMenu(db.Model): idx = db.Column(db.Integer, default=0, server_default="0") # rang dans le menu -class NotesFormsemestreInscription(db.Model): +class FormsemestreInscription(db.Model): """Inscription à un semestre de formation""" __tablename__ = "notes_formsemestre_inscription" @@ -239,7 +240,7 @@ class NotesFormsemestreInscription(db.Model): etape = db.Column(db.String(APO_CODE_STR_LEN)) -class NotesModuleImpl(db.Model): +class ModuleImpl(db.Model): """Mise en oeuvre d'un module pour une annee/semestre""" __tablename__ = "notes_moduleimpl" @@ -275,7 +276,7 @@ notes_modules_enseignants = db.Table( # XXX il manque probablement une relation pour gérer cela -class NotesModuleImplInscription(db.Model): +class ModuleImplInscription(db.Model): """Inscription à un module (etudiants,moduleimpl)""" __tablename__ = "notes_moduleimpl_inscription" @@ -291,7 +292,7 @@ class NotesModuleImplInscription(db.Model): etudid = db.Column(db.Integer, db.ForeignKey("identite.id"), index=True) -class NotesEvaluation(db.Model): +class Evaluation(db.Model): """Evaluation (contrôle, examen, ...)""" __tablename__ = "notes_evaluation" @@ -306,7 +307,7 @@ class NotesEvaluation(db.Model): heure_fin = db.Column(db.Time) description = db.Column(db.Text) note_max = db.Column(db.Float) - coefficient = db.Column(db.Float) # non BUT + coefficient = db.Column(db.Float) visibulletin = db.Column( db.Boolean, nullable=False, default=True, server_default="true" ) @@ -320,7 +321,34 @@ class NotesEvaluation(db.Model): # ordre de presentation (par défaut, le plus petit numero # est la plus ancienne eval): numero = db.Column(db.Integer) - ues = db.relationship("NotesUE", secondary="evaluation_ue_poids", viewonly=True) + ues = db.relationship("UniteEns", secondary="evaluation_ue_poids", viewonly=True) + + def to_dict(self): + e = dict(self.__dict__) + e.pop("_sa_instance_state", None) + # ScoDoc7 output_formators + e["jour"] = ndb.DateISOtoDMY(e["jour"]) + e["numero"] = ndb.int_null_is_zero(e["numero"]) + return sco_evaluation_db.evaluation_enrich_dict(e) + + # def from_dict(self, data): + # """Set evaluation attributes from given dict values.""" + # sco_evaluation_db._check_evaluation_args(data) + # for field in [ + # "moduleimpl_id", + # "jour", + # "heure_debut", + # "heure_fin", + # "description", + # "note_max", + # "coefficient", + # "visibulletin", + # "publish_incomplete", + # "evaluation_type", + # "numero", + # ]: + # if field in data: + # setattr(self, field, data[field] or None) def set_ue_poids(self, ue, poids: float): """Set poids évaluation vers cette UE""" @@ -332,7 +360,7 @@ class NotesEvaluation(db.Model): """ L = [] for ue_id, poids in ue_poids_dict.items(): - ue = NotesUE.query.get(ue_id) + ue = UniteEns.query.get(ue_id) L.append(EvaluationUEPoids(evaluation=self, ue=ue, poids=poids)) self.ue_poids = L @@ -361,11 +389,12 @@ class EvaluationUEPoids(db.Model): nullable=False, ) evaluation = db.relationship( - NotesEvaluation, + Evaluation, backref=db.backref("ue_poids", cascade="all, delete-orphan"), ) ue = db.relationship( - NotesUE, backref=db.backref("evaluation_ue_poids", cascade="all, delete-orphan") + UniteEns, + backref=db.backref("evaluation_ue_poids", cascade="all, delete-orphan"), ) def __repr__(self): diff --git a/app/models/notes.py b/app/models/notes.py index 0d0be637..98fe5f5a 100644 --- a/app/models/notes.py +++ b/app/models/notes.py @@ -100,9 +100,10 @@ class ScolarAutorisationInscription(db.Model): ) -class NotesAppreciations(db.Model): +class BulAppreciations(db.Model): """Appréciations sur bulletins""" + __tablename__ = "notes_appreciations" id = db.Column(db.Integer, primary_key=True) date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) etudid = db.Column( diff --git a/app/scodoc/notes_table.py b/app/scodoc/notes_table.py index 9f9314e2..fbfae8bc 100644 --- a/app/scodoc/notes_table.py +++ b/app/scodoc/notes_table.py @@ -34,6 +34,7 @@ from flask import g, url_for from app.models import ScoDocSiteConfig import app.scodoc.sco_utils as scu +from app.scodoc.sco_utils import ModuleType import app.scodoc.notesdb as ndb from app import log from app.scodoc.sco_formulas import NoteVector @@ -607,7 +608,7 @@ class NotesTable(object): # si 'NI', etudiant non inscrit a ce module if val != "NI": est_inscrit = True - if modimpl["module"]["module_type"] == scu.MODULE_STANDARD: + if modimpl["module"]["module_type"] == ModuleType.STANDARD: coef = modimpl["module"]["coefficient"] if modimpl["ue"]["type"] != UE_SPORT: notes.append(val, name=modimpl["module"]["code"]) @@ -644,11 +645,17 @@ class NotesTable(object): except: # log('comp_etud_moy_ue: exception: val=%s coef=%s' % (val,coef)) pass - elif modimpl["module"]["module_type"] == scu.MODULE_MALUS: + elif modimpl["module"]["module_type"] == ModuleType.MALUS: try: ue_malus += val except: pass # si non inscrit ou manquant, ignore + elif modimpl["module"]["module_type"] in ( + ModuleType.RESSOURCE, + ModuleType.SAE, + ): + # XXX temporaire pour ne pas bloquer durant le dev + pass else: raise ValueError( "invalid module type (%s)" % modimpl["module"]["module_type"] @@ -672,7 +679,7 @@ class NotesTable(object): # Recalcule la moyenne en utilisant une formule utilisateur expr_diag = {} - formula = sco_compute_moy.get_ue_expression(self.formsemestre_id, ue_id, cnx) + formula = sco_compute_moy.get_ue_expression(self.formsemestre_id, ue_id) if formula: moy = sco_compute_moy.compute_user_formula( self.sem, diff --git a/app/scodoc/notesdb.py b/app/scodoc/notesdb.py index 9be12f39..5418c3ce 100644 --- a/app/scodoc/notesdb.py +++ b/app/scodoc/notesdb.py @@ -53,7 +53,7 @@ def close_db_connection(): del g.db_conn -def GetDBConnexion(autocommit=True): # on n'utilise plus autocommit +def GetDBConnexion(): return g.db_conn diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index 98f3a917..91c912c4 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -45,6 +45,7 @@ from flask_login import current_user from flask_mail import Message import app.scodoc.sco_utils as scu +from app.scodoc.sco_utils import ModuleType import app.scodoc.notesdb as ndb from app import log from app.scodoc.sco_permissions import Permission @@ -59,7 +60,7 @@ from app.scodoc import sco_bulletins_xml from app.scodoc import sco_codes_parcours from app.scodoc import sco_cache from app.scodoc import sco_etud -from app.scodoc import sco_evaluations +from app.scodoc import sco_evaluation_db from app.scodoc import sco_formations from app.scodoc import sco_formsemestre from app.scodoc import sco_groups @@ -428,7 +429,7 @@ def _ue_mod_bulletin(etudid, formsemestre_id, ue_id, modimpls, nt, version): mod_moy = nt.get_etud_mod_moy( modimpl["moduleimpl_id"], etudid ) # peut etre 'NI' - is_malus = mod["module"]["module_type"] == scu.MODULE_MALUS + is_malus = mod["module"]["module_type"] == ModuleType.MALUS if bul_show_abs_modules: nbabs, nbabsjust = sco_abs.get_abs_count(etudid, sem) mod_abs = [nbabs, nbabsjust] @@ -558,7 +559,7 @@ def _ue_mod_bulletin(etudid, formsemestre_id, ue_id, modimpls, nt, version): mod["evaluations_incompletes"] = [] if sco_preferences.get_preference("bul_show_all_evals", formsemestre_id): complete_eval_ids = set([e["evaluation_id"] for e in evals]) - all_evals = sco_evaluations.do_evaluation_list( + all_evals = sco_evaluation_db.do_evaluation_list( args={"moduleimpl_id": modimpl["moduleimpl_id"]} ) all_evals.reverse() # plus ancienne d'abord diff --git a/app/scodoc/sco_bulletins_json.py b/app/scodoc/sco_bulletins_json.py index 4bcf839e..7ad1653b 100644 --- a/app/scodoc/sco_bulletins_json.py +++ b/app/scodoc/sco_bulletins_json.py @@ -36,7 +36,7 @@ import app.scodoc.notesdb as ndb from app.scodoc import sco_abs from app.scodoc import sco_cache from app.scodoc import sco_edit_ue -from app.scodoc import sco_evaluations +from app.scodoc import sco_evaluation_db from app.scodoc import sco_formsemestre from app.scodoc import sco_groups from app.scodoc import sco_photos @@ -277,7 +277,7 @@ def formsemestre_bulletinetud_published_dict( if sco_preferences.get_preference( "bul_show_all_evals", formsemestre_id ): - all_evals = sco_evaluations.do_evaluation_list( + all_evals = sco_evaluation_db.do_evaluation_list( args={"moduleimpl_id": modimpl["moduleimpl_id"]} ) all_evals.reverse() # plus ancienne d'abord diff --git a/app/scodoc/sco_bulletins_xml.py b/app/scodoc/sco_bulletins_xml.py index efdbe8c0..a0aff430 100644 --- a/app/scodoc/sco_bulletins_xml.py +++ b/app/scodoc/sco_bulletins_xml.py @@ -51,7 +51,7 @@ from app.scodoc import sco_abs from app.scodoc import sco_codes_parcours from app.scodoc import sco_cache from app.scodoc import sco_edit_ue -from app.scodoc import sco_evaluations +from app.scodoc import sco_evaluation_db from app.scodoc import sco_formsemestre from app.scodoc import sco_groups from app.scodoc import sco_photos @@ -289,7 +289,7 @@ def make_xml_formsemestre_bulletinetud( if sco_preferences.get_preference( "bul_show_all_evals", formsemestre_id ): - all_evals = sco_evaluations.do_evaluation_list( + all_evals = sco_evaluation_db.do_evaluation_list( args={"moduleimpl_id": modimpl["moduleimpl_id"]} ) all_evals.reverse() # plus ancienne d'abord diff --git a/app/scodoc/sco_compute_moy.py b/app/scodoc/sco_compute_moy.py index 3e505901..898fd9ad 100644 --- a/app/scodoc/sco_compute_moy.py +++ b/app/scodoc/sco_compute_moy.py @@ -34,6 +34,7 @@ from flask import url_for, g import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app.scodoc.sco_utils import ( + ModuleType, NOTES_ATTENTE, NOTES_NEUTRALISE, EVALUATION_NORMALE, @@ -44,7 +45,7 @@ from app.scodoc.sco_exceptions import ScoValueError from app import log from app.scodoc import sco_abs 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_formsemestre from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formulas @@ -103,8 +104,9 @@ formsemestre_ue_computation_expr_list = _formsemestre_ue_computation_exprEditor. formsemestre_ue_computation_expr_edit = _formsemestre_ue_computation_exprEditor.edit -def get_ue_expression(formsemestre_id, ue_id, cnx, html_quote=False): +def get_ue_expression(formsemestre_id, ue_id, html_quote=False): """Returns UE expression (formula), or None if no expression has been defined""" + cnx = ndb.GetDBConnexion() el = formsemestre_ue_computation_expr_list( cnx, {"formsemestre_id": formsemestre_id, "ue_id": ue_id} ) @@ -203,7 +205,7 @@ def compute_moduleimpl_moyennes(nt, modimpl): """ diag_info = {} # message d'erreur formule moduleimpl_id = modimpl["moduleimpl_id"] - is_malus = modimpl["module"]["module_type"] == scu.MODULE_MALUS + is_malus = modimpl["module"]["module_type"] == ModuleType.MALUS sem = sco_formsemestre.get_formsemestre(modimpl["formsemestre_id"]) etudids = sco_moduleimpl.moduleimpl_listeetuds( moduleimpl_id @@ -230,7 +232,7 @@ def compute_moduleimpl_moyennes(nt, modimpl): eval_rattr = None for e in evals: e["nb_inscrits"] = e["etat"]["nb_inscrits"] - NotesDB = sco_evaluations.do_evaluation_get_all_notes( + NotesDB = sco_evaluation_db.do_evaluation_get_all_notes( e["evaluation_id"] ) # toutes, y compris demissions # restreint aux étudiants encore inscrits à ce module diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index ae19b8b7..9fcb75a4 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -29,11 +29,13 @@ (portage from DTML) """ import flask -from flask import url_for, g, request +from flask import url_for, render_template +from flask import g, request from flask_login import current_user import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu +from app.scodoc.sco_utils import ModuleType from app import log from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.sco_permissions import Permission @@ -44,22 +46,6 @@ from app.scodoc import sco_edit_matiere from app.scodoc import sco_moduleimpl from app.scodoc import sco_news -_MODULE_HELP = """

-Les modules sont décrits dans le programme pédagogique. Un module est pour ce -logiciel l'unité pédagogique élémentaire. On va lui associer une note -à travers des évaluations.
-Cette note (moyenne de module) sera utilisée pour calculer la moyenne -générale (et la moyenne de l'UE à laquelle appartient le module). Pour -cela, on utilisera le coefficient associé au module. -

- -

Un module possède un enseignant responsable -(typiquement celui qui dispense le cours magistral). On peut associer -au module une liste d'enseignants (typiquement les chargés de TD). -Tous ces enseignants, plus le responsable du semestre, pourront -saisir et modifier les notes de ce module. -

""" - _moduleEditor = ndb.EditableTable( "notes_modules", "module_id", @@ -120,27 +106,30 @@ def do_module_create(args) -> int: def module_create(matiere_id=None): - """Creation d'un module""" + """Création d'un module""" from app.scodoc import sco_formations from app.scodoc import sco_edit_ue if matiere_id is None: raise ScoValueError("invalid matiere !") - M = sco_edit_matiere.matiere_list(args={"matiere_id": matiere_id})[0] - UE = sco_edit_ue.ue_list(args={"ue_id": M["ue_id"]})[0] - Fo = sco_formations.formation_list(args={"formation_id": UE["formation_id"]})[0] - parcours = sco_codes_parcours.get_parcours_from_code(Fo["type_parcours"]) + matiere = sco_edit_matiere.matiere_list(args={"matiere_id": matiere_id})[0] + UE = sco_edit_ue.ue_list(args={"ue_id": matiere["ue_id"]})[0] + formation = sco_formations.formation_list( + args={"formation_id": UE["formation_id"]} + )[0] + parcours = sco_codes_parcours.get_parcours_from_code(formation["type_parcours"]) + is_apc = parcours.APC_SAE semestres_indices = list(range(1, parcours.NB_SEM + 1)) H = [ html_sco_header.sco_header(page_title="Création d'un module"), - """

Création d'un module dans la matière %(titre)s""" % M, + """

Création d'un module dans la matière %(titre)s""" % matiere, """ (UE %(acronyme)s)

""" % UE, - _MODULE_HELP, + render_template("scodoc/help/modules.html", is_apc=is_apc), ] - # cherche le numero adequat (pour placer le module en fin de liste) - Mods = module_list(args={"matiere_id": matiere_id}) - if Mods: - default_num = max([m["numero"] for m in Mods]) + 10 + # cherche le numero adéquat (pour placer le module en fin de liste) + modules = module_list(args={"matiere_id": matiere_id}) + if modules: + default_num = max([m["numero"] for m in modules]) + 10 else: default_num = 10 tf = TrivialFormulator( @@ -153,7 +142,7 @@ def module_create(matiere_id=None): "size": 10, "explanation": "code du module (doit être unique dans la formation)", "allow_null": False, - "validator": lambda val, field, formation_id=Fo[ + "validator": lambda val, field, formation_id=formation[ "formation_id" ]: check_module_code_unicity(val, field, formation_id), }, @@ -166,8 +155,8 @@ def module_create(matiere_id=None): "input_type": "menu", "title": "Type", "explanation": "", - "labels": ("Standard", "Malus"), - "allowed_values": (str(scu.MODULE_STANDARD), str(scu.MODULE_MALUS)), + "labels": [x.name.capitalize() for x in scu.ModuleType], + "allowed_values": [str(int(x)) for x in scu.ModuleType], }, ), ( @@ -201,8 +190,8 @@ def module_create(matiere_id=None): ), # ('ects', { 'size' : 4, 'type' : 'float', 'title' : 'ECTS', 'explanation' : 'nombre de crédits ECTS (inutilisés: les crédits sont associés aux UE)' }), ("formation_id", {"default": UE["formation_id"], "input_type": "hidden"}), - ("ue_id", {"default": M["ue_id"], "input_type": "hidden"}), - ("matiere_id", {"default": M["matiere_id"], "input_type": "hidden"}), + ("ue_id", {"default": matiere["ue_id"], "input_type": "hidden"}), + ("matiere_id", {"default": matiere["matiere_id"], "input_type": "hidden"}), ( "semestre_id", { @@ -350,35 +339,38 @@ def module_edit(module_id=None): if not module_id: raise ScoValueError("invalid module !") - Mod = module_list(args={"module_id": module_id}) - if not Mod: + modules = module_list(args={"module_id": module_id}) + if not modules: raise ScoValueError("invalid module !") - Mod = Mod[0] + module = modules[0] unlocked = not module_is_locked(module_id) - Fo = sco_formations.formation_list(args={"formation_id": Mod["formation_id"]})[0] - parcours = sco_codes_parcours.get_parcours_from_code(Fo["type_parcours"]) - M = ndb.SimpleDictFetch( + formation = sco_formations.formation_list( + args={"formation_id": module["formation_id"]} + )[0] + parcours = sco_codes_parcours.get_parcours_from_code(formation["type_parcours"]) + is_apc = parcours.APC_SAE + ues_matieres = ndb.SimpleDictFetch( """SELECT ue.acronyme, mat.*, mat.id AS matiere_id FROM notes_matieres mat, notes_ue ue WHERE mat.ue_id = ue.id AND ue.formation_id = %(formation_id)s ORDER BY ue.numero, mat.numero """, - {"formation_id": Mod["formation_id"]}, + {"formation_id": module["formation_id"]}, ) - Mnames = ["%s / %s" % (x["acronyme"], x["titre"]) for x in M] - Mids = ["%s!%s" % (x["ue_id"], x["matiere_id"]) for x in M] - Mod["ue_matiere_id"] = "%s!%s" % (Mod["ue_id"], Mod["matiere_id"]) + mat_names = ["%s / %s" % (x["acronyme"], x["titre"]) for x in ues_matieres] + ue_mat_ids = ["%s!%s" % (x["ue_id"], x["matiere_id"]) for x in ues_matieres] + module["ue_matiere_id"] = "%s!%s" % (module["ue_id"], module["matiere_id"]) semestres_indices = list(range(1, parcours.NB_SEM + 1)) dest_url = url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, - formation_id=str(Mod["formation_id"]), + formation_id=str(module["formation_id"]), ) H = [ html_sco_header.sco_header( - page_title="Modification du module %(titre)s" % Mod, + page_title="Modification du module %(titre)s" % module, cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css"], javascripts=[ "libjs/jQuery-tagEditor/jquery.tag-editor.min.js", @@ -386,9 +378,9 @@ def module_edit(module_id=None): "js/module_tag_editor.js", ], ), - """

Modification du module %(titre)s""" % Mod, - """ (formation %(acronyme)s, version %(version)s)

""" % Fo, - _MODULE_HELP, + """

Modification du module %(titre)s""" % module, + """ (formation %(acronyme)s, version %(version)s)

""" % formation, + render_template("scodoc/help/modules.html", is_apc=is_apc), ] if not unlocked: H.append( @@ -405,7 +397,7 @@ def module_edit(module_id=None): "size": 10, "explanation": "code du module (doit être unique dans la formation)", "allow_null": False, - "validator": lambda val, field, formation_id=Mod[ + "validator": lambda val, field, formation_id=module[ "formation_id" ]: check_module_code_unicity( val, field, formation_id, module_id=module_id @@ -465,8 +457,8 @@ def module_edit(module_id=None): "input_type": "menu", "title": "Matière", "explanation": "un module appartient à une seule matière.", - "labels": Mnames, - "allowed_values": Mids, + "labels": mat_names, + "allowed_values": ue_mat_ids, "enabled": unlocked, }, ), @@ -503,7 +495,7 @@ def module_edit(module_id=None): html_foot_markup="""
""".format( module_id, ",".join(sco_tag_module.module_tag_list(module_id)) ), - initvalues=Mod, + initvalues=module, submitlabel="Modifier ce module", ) @@ -602,7 +594,7 @@ def formation_add_malus_modules(formation_id, titre=None, redirect=True): [ mod for mod in module_list(args={"ue_id": ue["ue_id"]}) - if mod["module_type"] == scu.MODULE_MALUS + if mod["module_type"] == ModuleType.MALUS ] ) if nb_mod_malus == 0: @@ -654,7 +646,7 @@ def ue_add_malus_module(ue_id, titre=None, code=None): "matiere_id": matiere_id, "formation_id": ue["formation_id"], "semestre_id": semestre_id, - "module_type": scu.MODULE_MALUS, + "module_type": ModuleType.MALUS, }, ) diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index 0911eec8..232e3cb8 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -32,9 +32,10 @@ import flask from flask import g, url_for, request from flask_login import current_user -from app.models.formations import NotesUE +from app.models.formations import UniteEns import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu +from app.scodoc.sco_utils import ModuleType from app import log from app.scodoc.TrivialFormulator import TrivialFormulator, TF from app.scodoc.gen_tables import GenTable @@ -955,7 +956,7 @@ def _ue_table_modules( mod["module_id"] ) klass = "notes_module_list" - if mod["module_type"] == scu.MODULE_MALUS: + if mod["module_type"] == ModuleType.MALUS: klass += " module_malus" H.append('
  • ' % klass) @@ -1066,20 +1067,20 @@ def ue_sharing_code(ue_code=None, ue_id=None, hide_ue_id=None): formation_code = F["formation_code"] # UE du même code, code formation et departement: q_ues = ( - NotesUE.query.filter_by(ue_code=ue_code) - .join(NotesUE.formation, aliased=True) + UniteEns.query.filter_by(ue_code=ue_code) + .join(UniteEns.formation, aliased=True) .filter_by(dept_id=g.scodoc_dept_id, formation_code=formation_code) ) else: # Toutes les UE du departement avec ce code: q_ues = ( - NotesUE.query.filter_by(ue_code=ue_code) - .join(NotesUE.formation, aliased=True) + UniteEns.query.filter_by(ue_code=ue_code) + .join(UniteEns.formation, aliased=True) .filter_by(dept_id=g.scodoc_dept_id) ) if hide_ue_id: # enlève l'ue de depart - q_ues = q_ues.filter(NotesUE.id != hide_ue_id) + q_ues = q_ues.filter(UniteEns.id != hide_ue_id) ues = q_ues.all() if not ues: diff --git a/app/scodoc/sco_evaluation_db.py b/app/scodoc/sco_evaluation_db.py new file mode 100644 index 00000000..55f9e685 --- /dev/null +++ b/app/scodoc/sco_evaluation_db.py @@ -0,0 +1,482 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# Gestion scolarite IUT +# +# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Emmanuel Viennet emmanuel.viennet@gmail.com +# +############################################################################## + +"""Gestion evaluations (ScoDoc7, sans SQlAlchemy) +""" + +import datetime +import pprint + +import flask +from flask import url_for, g +from flask_login import current_user + +from app import log +import app.scodoc.sco_utils as scu +import app.scodoc.notesdb as ndb +from app.scodoc.sco_exceptions import AccessDenied, ScoValueError + +from app.scodoc import sco_cache +from app.scodoc import sco_edit_module +from app.scodoc import sco_formsemestre +from app.scodoc import sco_moduleimpl +from app.scodoc import sco_news +from app.scodoc import sco_permissions_check + + +_evaluationEditor = ndb.EditableTable( + "notes_evaluation", + "evaluation_id", + ( + "evaluation_id", + "moduleimpl_id", + "jour", + "heure_debut", + "heure_fin", + "description", + "note_max", + "coefficient", + "visibulletin", + "publish_incomplete", + "evaluation_type", + "numero", + ), + sortkey="numero desc, jour desc, heure_debut desc", # plus recente d'abord + output_formators={ + "jour": ndb.DateISOtoDMY, + "numero": ndb.int_null_is_zero, + }, + input_formators={ + "jour": ndb.DateDMYtoISO, + "heure_debut": ndb.TimetoISO8601, # converti par evaluation_enrich_dict + "heure_fin": ndb.TimetoISO8601, # converti par evaluation_enrich_dict + "visibulletin": bool, + "publish_incomplete": bool, + "evaluation_type": int, + }, +) + + +def evaluation_enrich_dict(e): + """add or convert some fileds in an evaluation dict""" + # For ScoDoc7 compat + heure_debut_dt = e["heure_debut"] or datetime.time( + 8, 00 + ) # au cas ou pas d'heure (note externe?) + heure_fin_dt = e["heure_fin"] or datetime.time(8, 00) + e["heure_debut"] = ndb.TimefromISO8601(e["heure_debut"]) + e["heure_fin"] = ndb.TimefromISO8601(e["heure_fin"]) + e["jouriso"] = ndb.DateDMYtoISO(e["jour"]) + heure_debut, heure_fin = e["heure_debut"], e["heure_fin"] + d = ndb.TimeDuration(heure_debut, heure_fin) + if d is not None: + m = d % 60 + e["duree"] = "%dh" % (d / 60) + if m != 0: + e["duree"] += "%02d" % m + else: + e["duree"] = "" + if heure_debut and (not heure_fin or heure_fin == heure_debut): + e["descrheure"] = " à " + heure_debut + elif heure_debut and heure_fin: + e["descrheure"] = " de %s à %s" % (heure_debut, heure_fin) + else: + e["descrheure"] = "" + # matin, apresmidi: utile pour se referer aux absences: + if heure_debut_dt < datetime.time(12, 00): + e["matin"] = 1 + else: + e["matin"] = 0 + if heure_fin_dt > datetime.time(12, 00): + e["apresmidi"] = 1 + else: + e["apresmidi"] = 0 + return e + + +def do_evaluation_list(args, sortkey=None): + """List evaluations, sorted by numero (or most recent date first). + + Ajoute les champs: + 'duree' : '2h30' + 'matin' : 1 (commence avant 12:00) ou 0 + 'apresmidi' : 1 (termine après 12:00) ou 0 + 'descrheure' : ' de 15h00 à 16h30' + """ + # Attention: transformation fonction ScoDc7 en SQLAlchemy + cnx = ndb.GetDBConnexion() + evals = _evaluationEditor.list(cnx, args, sortkey=sortkey) + # calcule duree (chaine de car.) de chaque evaluation et ajoute jouriso, matin, apresmidi + for e in evals: + evaluation_enrich_dict(e) + + return evals + + +def do_evaluation_list_in_formsemestre(formsemestre_id): + "list evaluations in this formsemestre" + mods = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id) + evals = [] + for modimpl in mods: + evals += do_evaluation_list(args={"moduleimpl_id": modimpl["moduleimpl_id"]}) + return evals + + +def _check_evaluation_args(args): + "Check coefficient, dates and duration, raises exception if invalid" + moduleimpl_id = args["moduleimpl_id"] + # check bareme + note_max = args.get("note_max", None) + if note_max is None: + raise ScoValueError("missing note_max") + try: + note_max = float(note_max) + except ValueError: + raise ScoValueError("Invalid note_max value") + if note_max < 0: + raise ScoValueError("Invalid note_max value (must be positive or null)") + # check coefficient + coef = args.get("coefficient", None) + if coef is None: + raise ScoValueError("missing coefficient") + try: + coef = float(coef) + except ValueError: + raise ScoValueError("Invalid coefficient value") + if coef < 0: + raise ScoValueError("Invalid coefficient value (must be positive or null)") + # check date + jour = args.get("jour", None) + args["jour"] = jour + if jour: + M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] + sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"]) + d, m, y = [int(x) for x in sem["date_debut"].split("/")] + date_debut = datetime.date(y, m, d) + d, m, y = [int(x) for x in sem["date_fin"].split("/")] + date_fin = datetime.date(y, m, d) + # passe par ndb.DateDMYtoISO pour avoir date pivot + y, m, d = [int(x) for x in ndb.DateDMYtoISO(jour).split("-")] + jour = datetime.date(y, m, d) + if (jour > date_fin) or (jour < date_debut): + raise ScoValueError( + "La date de l'évaluation (%s/%s/%s) n'est pas dans le semestre !" + % (d, m, y) + ) + heure_debut = args.get("heure_debut", None) + args["heure_debut"] = heure_debut + heure_fin = args.get("heure_fin", None) + args["heure_fin"] = heure_fin + if jour and ((not heure_debut) or (not heure_fin)): + raise ScoValueError("Les heures doivent être précisées") + d = ndb.TimeDuration(heure_debut, heure_fin) + if d and ((d < 0) or (d > 60 * 12)): + raise ScoValueError("Heures de l'évaluation incohérentes !") + + +def do_evaluation_create( + moduleimpl_id=None, + jour=None, + heure_debut=None, + heure_fin=None, + description=None, + note_max=None, + coefficient=None, + visibulletin=None, + publish_incomplete=None, + evaluation_type=None, + numero=None, + **kw, # ceci pour absorber les arguments excedentaires de tf #sco8 +): + """Create an evaluation""" + if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id): + raise AccessDenied( + "Modification évaluation impossible pour %s" % current_user.get_nomplogin() + ) + args = locals() + log("do_evaluation_create: args=" + str(args)) + _check_evaluation_args(args) + # Check numeros + module_evaluation_renumber(moduleimpl_id, only_if_unumbered=True) + if not "numero" in args or args["numero"] is None: + n = None + # determine le numero avec la date + # Liste des eval existantes triees par date, la plus ancienne en tete + mod_evals = do_evaluation_list( + args={"moduleimpl_id": moduleimpl_id}, + sortkey="jour asc, heure_debut asc", + ) + if args["jour"]: + next_eval = None + t = ( + ndb.DateDMYtoISO(args["jour"], null_is_empty=True), + ndb.TimetoISO8601(args["heure_debut"], null_is_empty=True), + ) + for e in mod_evals: + if ( + ndb.DateDMYtoISO(e["jour"], null_is_empty=True), + ndb.TimetoISO8601(e["heure_debut"], null_is_empty=True), + ) > t: + next_eval = e + break + if next_eval: + n = module_evaluation_insert_before(mod_evals, next_eval) + else: + n = None # a placer en fin + if n is None: # pas de date ou en fin: + if mod_evals: + log(pprint.pformat(mod_evals[-1])) + n = mod_evals[-1]["numero"] + 1 + else: + n = 0 # the only one + # log("creating with numero n=%d" % n) + args["numero"] = n + + # + cnx = ndb.GetDBConnexion() + r = _evaluationEditor.create(cnx, args) + + # news + M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] + mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] + mod["moduleimpl_id"] = M["moduleimpl_id"] + mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod + sco_news.add( + typ=sco_news.NEWS_NOTE, + object=moduleimpl_id, + text='Création d\'une évaluation dans %(titre)s' % mod, + url=mod["url"], + ) + + return r + + +def do_evaluation_edit(args): + "edit an evaluation" + evaluation_id = args["evaluation_id"] + the_evals = do_evaluation_list({"evaluation_id": evaluation_id}) + if not the_evals: + raise ValueError("evaluation inexistante !") + moduleimpl_id = the_evals[0]["moduleimpl_id"] + if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id): + raise AccessDenied( + "Modification évaluation impossible pour %s" % current_user.get_nomplogin() + ) + args["moduleimpl_id"] = moduleimpl_id + _check_evaluation_args(args) + + cnx = ndb.GetDBConnexion() + _evaluationEditor.edit(cnx, args) + # inval cache pour ce semestre + M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] + sco_cache.invalidate_formsemestre(formsemestre_id=M["formsemestre_id"]) + + +def do_evaluation_delete(evaluation_id): + "delete evaluation" + the_evals = do_evaluation_list({"evaluation_id": evaluation_id}) + if not the_evals: + raise ValueError("evaluation inexistante !") + moduleimpl_id = the_evals[0]["moduleimpl_id"] + if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id): + raise AccessDenied( + "Modification évaluation impossible pour %s" % current_user.get_nomplogin() + ) + NotesDB = do_evaluation_get_all_notes(evaluation_id) # { etudid : value } + notes = [x["value"] for x in NotesDB.values()] + if notes: + raise ScoValueError( + "Impossible de supprimer cette évaluation: il reste des notes" + ) + + cnx = ndb.GetDBConnexion() + + _evaluationEditor.delete(cnx, evaluation_id) + # inval cache pour ce semestre + M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] + sco_cache.invalidate_formsemestre(formsemestre_id=M["formsemestre_id"]) + # news + + mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] + mod["moduleimpl_id"] = M["moduleimpl_id"] + mod["url"] = ( + scu.NotesURL() + "/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod + ) + sco_news.add( + typ=sco_news.NEWS_NOTE, + object=moduleimpl_id, + text='Suppression d\'une évaluation dans %(titre)s' % mod, + url=mod["url"], + ) + + +# ancien _notes_getall +def do_evaluation_get_all_notes( + evaluation_id, table="notes_notes", filter_suppressed=True, by_uid=None +): + """Toutes les notes pour une evaluation: { etudid : { 'value' : value, 'date' : date ... }} + Attention: inclut aussi les notes des étudiants qui ne sont plus inscrits au module. + """ + do_cache = ( + filter_suppressed and table == "notes_notes" and (by_uid is None) + ) # pas de cache pour (rares) appels via undo_notes ou specifiant un enseignant + if do_cache: + r = sco_cache.EvaluationCache.get(evaluation_id) + if r != None: + return r + cnx = ndb.GetDBConnexion() + cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) + cond = " where evaluation_id=%(evaluation_id)s" + if by_uid: + cond += " and uid=%(by_uid)s" + + cursor.execute( + "select * from " + table + cond, + {"evaluation_id": evaluation_id, "by_uid": by_uid}, + ) + res = cursor.dictfetchall() + d = {} + if filter_suppressed: + for x in res: + if x["value"] != scu.NOTES_SUPPRESS: + d[x["etudid"]] = x + else: + for x in res: + d[x["etudid"]] = x + if do_cache: + status = sco_cache.EvaluationCache.set(evaluation_id, d) + if not status: + log(f"Warning: EvaluationCache.set: {evaluation_id}\t{status}") + return d + + +def module_evaluation_renumber(moduleimpl_id, only_if_unumbered=False, redirect=0): + """Renumber evaluations in this module, according to their date. (numero=0: oldest one) + Needed because previous versions of ScoDoc did not have eval numeros + Note: existing numeros are ignored + """ + redirect = int(redirect) + # log('module_evaluation_renumber( moduleimpl_id=%s )' % moduleimpl_id ) + # List sorted according to date/heure, ignoring numeros: + # (note that we place evaluations with NULL date at the end) + mod_evals = do_evaluation_list( + args={"moduleimpl_id": moduleimpl_id}, + sortkey="jour asc, heure_debut asc", + ) + + all_numbered = False not in [x["numero"] > 0 for x in mod_evals] + if all_numbered and only_if_unumbered: + return # all ok + + # Reset all numeros: + i = 1 + for e in mod_evals: + e["numero"] = i + do_evaluation_edit(e) + i += 1 + + # If requested, redirect to moduleimpl page: + if redirect: + return flask.redirect( + url_for( + "notes.moduleimpl_status", + scodoc_dept=g.scodoc_dept, + moduleimpl_id=moduleimpl_id, + ) + ) + + +def module_evaluation_insert_before(mod_evals, next_eval): + """Renumber evals such that an evaluation with can be inserted before next_eval + Returns numero suitable for the inserted evaluation + """ + if next_eval: + n = next_eval["numero"] + if not n: + log("renumbering old evals") + module_evaluation_renumber(next_eval["moduleimpl_id"]) + next_eval = do_evaluation_list( + args={"evaluation_id": next_eval["evaluation_id"]} + )[0] + n = next_eval["numero"] + else: + n = 1 + # log('inserting at position numero %s' % n ) + # all numeros >= n are incremented + for e in mod_evals: + if e["numero"] >= n: + e["numero"] += 1 + # log('incrementing %s to %s' % (e['evaluation_id'], e['numero'])) + do_evaluation_edit(e) + + return n + + +def module_evaluation_move(evaluation_id, after=0, redirect=1): + """Move before/after previous one (decrement/increment numero) + (published) + """ + e = do_evaluation_list(args={"evaluation_id": evaluation_id})[0] + redirect = int(redirect) + # access: can change eval ? + if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=e["moduleimpl_id"]): + raise AccessDenied( + "Modification évaluation impossible pour %s" % current_user.get_nomplogin() + ) + + module_evaluation_renumber(e["moduleimpl_id"], only_if_unumbered=True) + e = do_evaluation_list(args={"evaluation_id": evaluation_id})[0] + + after = int(after) # 0: deplace avant, 1 deplace apres + if after not in (0, 1): + raise ValueError('invalid value for "after"') + mod_evals = do_evaluation_list({"moduleimpl_id": e["moduleimpl_id"]}) + if len(mod_evals) > 1: + idx = [p["evaluation_id"] for p in mod_evals].index(evaluation_id) + neigh = None # object to swap with + if after == 0 and idx > 0: + neigh = mod_evals[idx - 1] + elif after == 1 and idx < len(mod_evals) - 1: + neigh = mod_evals[idx + 1] + if neigh: # + if neigh["numero"] == e["numero"]: + log("Warning: module_evaluation_move: forcing renumber") + module_evaluation_renumber(e["moduleimpl_id"], only_if_unumbered=False) + else: + # swap numero with neighbor + e["numero"], neigh["numero"] = neigh["numero"], e["numero"] + do_evaluation_edit(e) + do_evaluation_edit(neigh) + # redirect to moduleimpl page: + if redirect: + return flask.redirect( + url_for( + "notes.moduleimpl_status", + scodoc_dept=g.scodoc_dept, + moduleimpl_id=e["moduleimpl_id"], + ) + ) diff --git a/app/scodoc/sco_evaluation_edit.py b/app/scodoc/sco_evaluation_edit.py new file mode 100644 index 00000000..a310ecdd --- /dev/null +++ b/app/scodoc/sco_evaluation_edit.py @@ -0,0 +1,332 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# Gestion scolarite IUT +# +# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Emmanuel Viennet emmanuel.viennet@gmail.com +# +############################################################################## + +"""Formulaire ajout/édition d'une évaluation +""" + +import time + +import flask +from flask import url_for, render_template +from flask import g +from flask_login import current_user +from flask import request + +from app import db +from app import log +from app import models +import app.scodoc.sco_utils as scu +from app.scodoc.sco_utils import ModuleType +from app.scodoc.sco_exceptions import AccessDenied, ScoValueError +from app.scodoc.TrivialFormulator import TrivialFormulator +from app.scodoc import html_sco_header +from app.scodoc import sco_evaluations +from app.scodoc import sco_evaluation_db +from app.scodoc import sco_moduleimpl +from app.scodoc import sco_permissions_check + + +def evaluation_create_form( + moduleimpl_id=None, + evaluation_id=None, + edit=False, + page_title="Évaluation", +): + "Formulaire création/édition d'une évaluation (pas de ses notes)" + if evaluation_id is not None: + evaluation = models.Evaluation.query.get(evaluation_id) + moduleimpl_id = evaluation.moduleimpl_id + # + modimpl = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[0] + mod = modimpl["module"] + formsemestre_id = modimpl["formsemestre_id"] + sem_ues = db.session.query(models.UniteEns).filter( + models.ModuleImpl.formsemestre_id == formsemestre_id, + models.Module.id == models.ModuleImpl.module_id, + models.UniteEns.id == models.Module.ue_id, + ) + is_malus = mod["module_type"] == ModuleType.MALUS + is_apc = mod["module_type"] in (ModuleType.RESSOURCE, ModuleType.SAE) + + min_note_max = scu.NOTES_PRECISION # le plus petit bareme possible + # + if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id): + return ( + html_sco_header.sco_header() + + "

    Opération non autorisée

    " + + "Modification évaluation impossible pour %s" + % current_user.get_nomplogin() + + "

    " + + '

    Revenir

    ' + % (moduleimpl_id,) + + html_sco_header.sco_footer() + ) + if not edit: + # creation nouvel + if moduleimpl_id is None: + raise ValueError("missing moduleimpl_id parameter") + initvalues = { + "note_max": 20, + "jour": time.strftime("%d/%m/%Y", time.localtime()), + "publish_incomplete": is_malus, + } + submitlabel = "Créer cette évaluation" + action = "Création d'une évaluation" + link = "" + else: + # édition données existantes + # setup form init values + if evaluation_id is None: + raise ValueError("missing evaluation_id parameter") + initvalues = evaluation.to_dict() + moduleimpl_id = initvalues["moduleimpl_id"] + submitlabel = "Modifier les données" + action = "Modification d'une évaluation" + link = "" + # Note maximale actuelle dans cette éval ? + etat = sco_evaluations.do_evaluation_etat(evaluation_id) + if etat["maxi_num"] is not None: + min_note_max = max(scu.NOTES_PRECISION, etat["maxi_num"]) + else: + min_note_max = scu.NOTES_PRECISION + # + if min_note_max > scu.NOTES_PRECISION: + min_note_max_str = scu.fmt_note(min_note_max) + else: + min_note_max_str = "0" + # + mod_descr = '%s %s %s' % ( + moduleimpl_id, + mod["code"], + mod["titre"], + link, + ) + H = [ + f"""

    {action} en + {scu.MODULE_TYPE_NAMES[mod["module_type"]]} {mod_descr}

    + """ + ] + + heures = ["%02dh%02d" % (h, m) for h in range(8, 19) for m in (0, 30)] + # + initvalues["visibulletin"] = initvalues.get("visibulletin", True) + if initvalues["visibulletin"]: + initvalues["visibulletinlist"] = ["X"] + else: + initvalues["visibulletinlist"] = [] + vals = scu.get_request_args() + if vals.get("tf_submitted", False) and "visibulletinlist" not in vals: + vals["visibulletinlist"] = [] + # + if is_apc: # BUT: poids vers les UE + for ue in sem_ues: + if edit: + existing_poids = models.EvaluationUEPoids.query.filter_by( + ue=ue, evaluation=evaluation + ).first() + else: + existing_poids = None + if existing_poids: + poids = existing_poids.poids + else: + poids = 1.0 # par defaut au départ + initvalues[f"poids_{ue.id}"] = poids + # + form = [ + ("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}), + ("formsemestre_id", {"default": formsemestre_id, "input_type": "hidden"}), + ("moduleimpl_id", {"default": moduleimpl_id, "input_type": "hidden"}), + # ('jour', { 'title' : 'Date (j/m/a)', 'size' : 12, 'explanation' : 'date de l\'examen, devoir ou contrôle' }), + ( + "jour", + { + "input_type": "date", + "title": "Date", + "size": 12, + "explanation": "date de l'examen, devoir ou contrôle", + }, + ), + ( + "heure_debut", + { + "title": "Heure de début", + "explanation": "heure du début de l'épreuve", + "input_type": "menu", + "allowed_values": heures, + "labels": heures, + }, + ), + ( + "heure_fin", + { + "title": "Heure de fin", + "explanation": "heure de fin de l'épreuve", + "input_type": "menu", + "allowed_values": heures, + "labels": heures, + }, + ), + ] + if is_malus: # pas de coefficient + form.append(("coefficient", {"input_type": "hidden", "default": "1."})) + elif not is_apc: # modules standard hors BUT + form.append( + ( + "coefficient", + { + "size": 6, + "type": "float", + "explanation": "coef. dans le module (choisi librement par l'enseignant)", + "allow_null": False, + }, + ) + ) + form += [ + ( + "note_max", + { + "size": 4, + "type": "float", + "title": "Notes de 0 à", + "explanation": "barème (note max actuelle: %s)" % min_note_max_str, + "allow_null": False, + "max_value": scu.NOTES_MAX, + "min_value": min_note_max, + }, + ), + ( + "description", + { + "size": 36, + "type": "text", + "explanation": 'type d\'évaluation, apparait sur le bulletins longs. Exemples: "contrôle court", "examen de TP", "examen final".', + }, + ), + ( + "visibulletinlist", + { + "input_type": "checkbox", + "allowed_values": ["X"], + "labels": [""], + "title": "Visible sur bulletins", + "explanation": "(pour les bulletins en version intermédiaire)", + }, + ), + ( + "publish_incomplete", + { + "input_type": "boolcheckbox", + "title": "Prise en compte immédiate", + "explanation": "notes utilisées même si incomplètes", + }, + ), + ( + "evaluation_type", + { + "input_type": "menu", + "title": "Modalité", + "allowed_values": ( + scu.EVALUATION_NORMALE, + scu.EVALUATION_RATTRAPAGE, + scu.EVALUATION_SESSION2, + ), + "type": "int", + "labels": ( + "Normale", + "Rattrapage (remplace si meilleure note)", + "Deuxième session (remplace toujours)", + ), + }, + ), + ] + if is_apc: # ressources et SAÉs + form += [ + ( + "coefficient", + { + "size": 6, + "type": "float", + "explanation": "importance de l'évaluation (multiplie les poids ci-dessous)", + "allow_null": False, + }, + ), + ] + # Liste des UE utilisées dans des modules de ce semestre: + for ue in sem_ues: + form.append( + ( + f"poids_{ue.id}", + { + "title": f"Poids {ue.acronyme}", + "size": 2, + "type": "float", + "explanation": f"{ue.titre}", + "allow_null": False, + }, + ), + ) + tf = TrivialFormulator( + request.base_url, + vals, + form, + cancelbutton="Annuler", + submitlabel=submitlabel, + initvalues=initvalues, + readonly=False, + ) + + dest_url = "moduleimpl_status?moduleimpl_id=%s" % modimpl["moduleimpl_id"] + if tf[0] == 0: + head = html_sco_header.sco_header(page_title=page_title) + return ( + head + + "\n".join(H) + + "\n" + + tf[1] + + render_template("scodoc/help/evaluations.html", is_apc=is_apc) + + html_sco_header.sco_footer() + ) + elif tf[0] == -1: + return flask.redirect(dest_url) + else: + # form submission + if tf[2]["visibulletinlist"]: + tf[2]["visibulletin"] = True + else: + tf[2]["visibulletin"] = False + if edit: + sco_evaluation_db.do_evaluation_edit(tf[2]) + else: + # creation d'une evaluation + evaluation_id = sco_evaluation_db.do_evaluation_create(**tf[2]) + # Set poids + evaluation = models.Evaluation.query.get(evaluation_id) + for ue in sem_ues: + evaluation.set_ue_poids(ue, tf[2][f"poids_{ue.id}"]) + db.session.add(evaluation) + db.session.commit() + return flask.redirect(dest_url) diff --git a/app/scodoc/sco_evaluations.py b/app/scodoc/sco_evaluations.py index 0fba0b3c..e650d695 100644 --- a/app/scodoc/sco_evaluations.py +++ b/app/scodoc/sco_evaluations.py @@ -29,9 +29,7 @@ """ import datetime import operator -import pprint import time -import urllib import flask from flask import url_for @@ -41,12 +39,13 @@ from flask import request from app import log import app.scodoc.sco_utils as scu +from app.scodoc.sco_utils import ModuleType import app.scodoc.notesdb as ndb from app.scodoc.sco_exceptions import AccessDenied, ScoValueError import sco_version from app.scodoc.gen_tables import GenTable -from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc import html_sco_header +from app.scodoc import sco_evaluation_db from app.scodoc import sco_abs from app.scodoc import sco_cache from app.scodoc import sco_edit_module @@ -96,281 +95,6 @@ def ListMedian(L): # -------------------------------------------------------------------- -_evaluationEditor = ndb.EditableTable( - "notes_evaluation", - "evaluation_id", - ( - "evaluation_id", - "moduleimpl_id", - "jour", - "heure_debut", - "heure_fin", - "description", - "note_max", - "coefficient", - "visibulletin", - "publish_incomplete", - "evaluation_type", - "numero", - ), - sortkey="numero desc, jour desc, heure_debut desc", # plus recente d'abord - output_formators={ - "jour": ndb.DateISOtoDMY, - "numero": ndb.int_null_is_zero, - }, - input_formators={ - "jour": ndb.DateDMYtoISO, - "heure_debut": ndb.TimetoISO8601, # converti par do_evaluation_list - "heure_fin": ndb.TimetoISO8601, # converti par do_evaluation_list - "visibulletin": bool, - "publish_incomplete": bool, - "evaluation_type": int, - }, -) - - -def do_evaluation_list(args, sortkey=None): - """List evaluations, sorted by numero (or most recent date first). - - Ajoute les champs: - 'duree' : '2h30' - 'matin' : 1 (commence avant 12:00) ou 0 - 'apresmidi' : 1 (termine après 12:00) ou 0 - 'descrheure' : ' de 15h00 à 16h30' - """ - cnx = ndb.GetDBConnexion() - evals = _evaluationEditor.list(cnx, args, sortkey=sortkey) - # calcule duree (chaine de car.) de chaque evaluation et ajoute jouriso, matin, apresmidi - for e in evals: - heure_debut_dt = e["heure_debut"] or datetime.time( - 8, 00 - ) # au cas ou pas d'heure (note externe?) - heure_fin_dt = e["heure_fin"] or datetime.time(8, 00) - e["heure_debut"] = ndb.TimefromISO8601(e["heure_debut"]) - e["heure_fin"] = ndb.TimefromISO8601(e["heure_fin"]) - e["jouriso"] = ndb.DateDMYtoISO(e["jour"]) - heure_debut, heure_fin = e["heure_debut"], e["heure_fin"] - d = ndb.TimeDuration(heure_debut, heure_fin) - if d is not None: - m = d % 60 - e["duree"] = "%dh" % (d / 60) - if m != 0: - e["duree"] += "%02d" % m - else: - e["duree"] = "" - if heure_debut and (not heure_fin or heure_fin == heure_debut): - e["descrheure"] = " à " + heure_debut - elif heure_debut and heure_fin: - e["descrheure"] = " de %s à %s" % (heure_debut, heure_fin) - else: - e["descrheure"] = "" - # matin, apresmidi: utile pour se referer aux absences: - if heure_debut_dt < datetime.time(12, 00): - e["matin"] = 1 - else: - e["matin"] = 0 - if heure_fin_dt > datetime.time(12, 00): - e["apresmidi"] = 1 - else: - e["apresmidi"] = 0 - - return evals - - -def do_evaluation_list_in_formsemestre(formsemestre_id): - "list evaluations in this formsemestre" - mods = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id) - evals = [] - for mod in mods: - evals += do_evaluation_list(args={"moduleimpl_id": mod["moduleimpl_id"]}) - return evals - - -def _check_evaluation_args(args): - "Check coefficient, dates and duration, raises exception if invalid" - moduleimpl_id = args["moduleimpl_id"] - # check bareme - note_max = args.get("note_max", None) - if note_max is None: - raise ScoValueError("missing note_max") - try: - note_max = float(note_max) - except ValueError: - raise ScoValueError("Invalid note_max value") - if note_max < 0: - raise ScoValueError("Invalid note_max value (must be positive or null)") - # check coefficient - coef = args.get("coefficient", None) - if coef is None: - raise ScoValueError("missing coefficient") - try: - coef = float(coef) - except ValueError: - raise ScoValueError("Invalid coefficient value") - if coef < 0: - raise ScoValueError("Invalid coefficient value (must be positive or null)") - # check date - jour = args.get("jour", None) - args["jour"] = jour - if jour: - M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] - sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"]) - d, m, y = [int(x) for x in sem["date_debut"].split("/")] - date_debut = datetime.date(y, m, d) - d, m, y = [int(x) for x in sem["date_fin"].split("/")] - date_fin = datetime.date(y, m, d) - # passe par ndb.DateDMYtoISO pour avoir date pivot - y, m, d = [int(x) for x in ndb.DateDMYtoISO(jour).split("-")] - jour = datetime.date(y, m, d) - if (jour > date_fin) or (jour < date_debut): - raise ScoValueError( - "La date de l'évaluation (%s/%s/%s) n'est pas dans le semestre !" - % (d, m, y) - ) - heure_debut = args.get("heure_debut", None) - args["heure_debut"] = heure_debut - heure_fin = args.get("heure_fin", None) - args["heure_fin"] = heure_fin - if jour and ((not heure_debut) or (not heure_fin)): - raise ScoValueError("Les heures doivent être précisées") - d = ndb.TimeDuration(heure_debut, heure_fin) - if d and ((d < 0) or (d > 60 * 12)): - raise ScoValueError("Heures de l'évaluation incohérentes !") - - -def do_evaluation_create( - moduleimpl_id=None, - jour=None, - heure_debut=None, - heure_fin=None, - description=None, - note_max=None, - coefficient=None, - visibulletin=None, - publish_incomplete=None, - evaluation_type=None, - numero=None, - **kw, # ceci pour absorber les arguments excedentaires de tf #sco8 -): - """Create an evaluation""" - if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id): - raise AccessDenied( - "Modification évaluation impossible pour %s" % current_user.get_nomplogin() - ) - args = locals() - log("do_evaluation_create: args=" + str(args)) - _check_evaluation_args(args) - # Check numeros - module_evaluation_renumber(moduleimpl_id, only_if_unumbered=True) - if not "numero" in args or args["numero"] is None: - n = None - # determine le numero avec la date - # Liste des eval existantes triees par date, la plus ancienne en tete - ModEvals = do_evaluation_list( - args={"moduleimpl_id": moduleimpl_id}, - sortkey="jour asc, heure_debut asc", - ) - if args["jour"]: - next_eval = None - t = ( - ndb.DateDMYtoISO(args["jour"], null_is_empty=True), - ndb.TimetoISO8601(args["heure_debut"], null_is_empty=True), - ) - for e in ModEvals: - if ( - ndb.DateDMYtoISO(e["jour"], null_is_empty=True), - ndb.TimetoISO8601(e["heure_debut"], null_is_empty=True), - ) > t: - next_eval = e - break - if next_eval: - n = module_evaluation_insert_before(ModEvals, next_eval) - else: - n = None # a placer en fin - if n is None: # pas de date ou en fin: - if ModEvals: - log(pprint.pformat(ModEvals[-1])) - n = ModEvals[-1]["numero"] + 1 - else: - n = 0 # the only one - # log("creating with numero n=%d" % n) - args["numero"] = n - - # - cnx = ndb.GetDBConnexion() - r = _evaluationEditor.create(cnx, args) - - # news - M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] - mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] - mod["moduleimpl_id"] = M["moduleimpl_id"] - mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod - sco_news.add( - typ=sco_news.NEWS_NOTE, - object=moduleimpl_id, - text='Création d\'une évaluation dans %(titre)s' % mod, - url=mod["url"], - ) - - return r - - -def do_evaluation_edit(args): - "edit an evaluation" - evaluation_id = args["evaluation_id"] - the_evals = do_evaluation_list({"evaluation_id": evaluation_id}) - if not the_evals: - raise ValueError("evaluation inexistante !") - moduleimpl_id = the_evals[0]["moduleimpl_id"] - if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id): - raise AccessDenied( - "Modification évaluation impossible pour %s" % current_user.get_nomplogin() - ) - args["moduleimpl_id"] = moduleimpl_id - _check_evaluation_args(args) - - cnx = ndb.GetDBConnexion() - _evaluationEditor.edit(cnx, args) - # inval cache pour ce semestre - M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] - sco_cache.invalidate_formsemestre(formsemestre_id=M["formsemestre_id"]) - - -def do_evaluation_delete(evaluation_id): - "delete evaluation" - the_evals = do_evaluation_list({"evaluation_id": evaluation_id}) - if not the_evals: - raise ValueError("evaluation inexistante !") - moduleimpl_id = the_evals[0]["moduleimpl_id"] - if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id): - raise AccessDenied( - "Modification évaluation impossible pour %s" % current_user.get_nomplogin() - ) - NotesDB = do_evaluation_get_all_notes(evaluation_id) # { etudid : value } - notes = [x["value"] for x in NotesDB.values()] - if notes: - raise ScoValueError( - "Impossible de supprimer cette évaluation: il reste des notes" - ) - - cnx = ndb.GetDBConnexion() - - _evaluationEditor.delete(cnx, evaluation_id) - # inval cache pour ce semestre - M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] - sco_cache.invalidate_formsemestre(formsemestre_id=M["formsemestre_id"]) - # news - mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] - mod["moduleimpl_id"] = M["moduleimpl_id"] - mod["url"] = ( - scu.NotesURL() + "/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod - ) - sco_news.add( - typ=sco_news.NEWS_NOTE, - object=moduleimpl_id, - text='Suppression d\'une évaluation dans %(titre)s' % mod, - url=mod["url"], - ) def do_evaluation_etat(evaluation_id, partition_id=None, select_first_partition=False): @@ -385,7 +109,9 @@ def do_evaluation_etat(evaluation_id, partition_id=None, select_first_partition= nb_inscrits = len( sco_groups.do_evaluation_listeetuds_groups(evaluation_id, getallstudents=True) ) - NotesDB = do_evaluation_get_all_notes(evaluation_id) # { etudid : value } + NotesDB = sco_evaluation_db.do_evaluation_get_all_notes( + evaluation_id + ) # { etudid : value } notes = [x["value"] for x in NotesDB.values()] nb_abs = len([x for x in notes if x is None]) nb_neutre = len([x for x in notes if x == scu.NOTES_NEUTRALISE]) @@ -408,10 +134,10 @@ def do_evaluation_etat(evaluation_id, partition_id=None, select_first_partition= else: last_modif = None # ---- Liste des groupes complets et incomplets - E = do_evaluation_list(args={"evaluation_id": evaluation_id})[0] + E = sco_evaluation_db.do_evaluation_list(args={"evaluation_id": evaluation_id})[0] M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] - is_malus = Mod["module_type"] == scu.MODULE_MALUS # True si module de malus + is_malus = Mod["module_type"] == ModuleType.MALUS # True si module de malus formsemestre_id = M["formsemestre_id"] # Si partition_id is None, prend 'all' ou bien la premiere: if partition_id is None: @@ -611,46 +337,6 @@ def do_evaluation_list_in_sem(formsemestre_id, with_etat=True): return res -# ancien _notes_getall -def do_evaluation_get_all_notes( - evaluation_id, table="notes_notes", filter_suppressed=True, by_uid=None -): - """Toutes les notes pour une evaluation: { etudid : { 'value' : value, 'date' : date ... }} - Attention: inclut aussi les notes des étudiants qui ne sont plus inscrits au module. - """ - do_cache = ( - filter_suppressed and table == "notes_notes" and (by_uid is None) - ) # pas de cache pour (rares) appels via undo_notes ou specifiant un enseignant - if do_cache: - r = sco_cache.EvaluationCache.get(evaluation_id) - if r != None: - return r - cnx = ndb.GetDBConnexion() - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) - cond = " where evaluation_id=%(evaluation_id)s" - if by_uid: - cond += " and uid=%(by_uid)s" - - cursor.execute( - "select * from " + table + cond, - {"evaluation_id": evaluation_id, "by_uid": by_uid}, - ) - res = cursor.dictfetchall() - d = {} - if filter_suppressed: - for x in res: - if x["value"] != scu.NOTES_SUPPRESS: - d[x["etudid"]] = x - else: - for x in res: - d[x["etudid"]] = x - if do_cache: - status = sco_cache.EvaluationCache.set(evaluation_id, d) - if not status: - log(f"Warning: EvaluationCache.set: {evaluation_id}\t{status}") - return d - - def _eval_etat(evals): """evals: list of mappings (etats) -> nb_eval_completes, nb_evals_en_cours, @@ -818,10 +504,12 @@ def evaluation_date_first_completion(evaluation_id): # ins = [i for i in insem if i["etudid"] in insmodset] notes = list( - do_evaluation_get_all_notes(evaluation_id, filter_suppressed=False).values() + sco_evaluation_db.do_evaluation_get_all_notes( + evaluation_id, filter_suppressed=False + ).values() ) notes_log = list( - do_evaluation_get_all_notes( + sco_evaluation_db.do_evaluation_get_all_notes( evaluation_id, filter_suppressed=False, table="notes_notes_log" ).values() ) @@ -854,7 +542,7 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, format="html"): M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=e["moduleimpl_id"])[0] Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] if (e["evaluation_type"] != scu.EVALUATION_NORMALE) or ( - Mod["module_type"] == scu.MODULE_MALUS + Mod["module_type"] == ModuleType.MALUS ): continue e["date_first_complete"] = evaluation_date_first_completion(e["evaluation_id"]) @@ -917,112 +605,6 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, format="html"): return tab.make_page(format=format) -def module_evaluation_insert_before(ModEvals, next_eval): - """Renumber evals such that an evaluation with can be inserted before next_eval - Returns numero suitable for the inserted evaluation - """ - if next_eval: - n = next_eval["numero"] - if not n: - log("renumbering old evals") - module_evaluation_renumber(next_eval["moduleimpl_id"]) - next_eval = do_evaluation_list( - args={"evaluation_id": next_eval["evaluation_id"]} - )[0] - n = next_eval["numero"] - else: - n = 1 - # log('inserting at position numero %s' % n ) - # all numeros >= n are incremented - for e in ModEvals: - if e["numero"] >= n: - e["numero"] += 1 - # log('incrementing %s to %s' % (e['evaluation_id'], e['numero'])) - do_evaluation_edit(e) - - return n - - -def module_evaluation_move(evaluation_id, after=0, redirect=1): - """Move before/after previous one (decrement/increment numero) - (published) - """ - e = do_evaluation_list(args={"evaluation_id": evaluation_id})[0] - redirect = int(redirect) - # access: can change eval ? - if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=e["moduleimpl_id"]): - raise AccessDenied( - "Modification évaluation impossible pour %s" % current_user.get_nomplogin() - ) - - module_evaluation_renumber(e["moduleimpl_id"], only_if_unumbered=True) - e = do_evaluation_list(args={"evaluation_id": evaluation_id})[0] - - after = int(after) # 0: deplace avant, 1 deplace apres - if after not in (0, 1): - raise ValueError('invalid value for "after"') - ModEvals = do_evaluation_list({"moduleimpl_id": e["moduleimpl_id"]}) - # log('ModEvals=%s' % [ x['evaluation_id'] for x in ModEvals] ) - if len(ModEvals) > 1: - idx = [p["evaluation_id"] for p in ModEvals].index(evaluation_id) - neigh = None # object to swap with - if after == 0 and idx > 0: - neigh = ModEvals[idx - 1] - elif after == 1 and idx < len(ModEvals) - 1: - neigh = ModEvals[idx + 1] - if neigh: # - # swap numero with neighbor - e["numero"], neigh["numero"] = neigh["numero"], e["numero"] - do_evaluation_edit(e) - do_evaluation_edit(neigh) - # redirect to moduleimpl page: - if redirect: - return flask.redirect( - url_for( - "notes.moduleimpl_status", - scodoc_dept=g.scodoc_dept, - moduleimpl_id=e["moduleimpl_id"], - ) - ) - - -def module_evaluation_renumber(moduleimpl_id, only_if_unumbered=False, redirect=0): - """Renumber evaluations in this module, according to their date. (numero=0: oldest one) - Needed because previous versions of ScoDoc did not have eval numeros - Note: existing numeros are ignored - """ - redirect = int(redirect) - # log('module_evaluation_renumber( moduleimpl_id=%s )' % moduleimpl_id ) - # List sorted according to date/heure, ignoring numeros: - # (note that we place evaluations with NULL date at the end) - ModEvals = do_evaluation_list( - args={"moduleimpl_id": moduleimpl_id}, - sortkey="jour asc, heure_debut asc", - ) - - all_numbered = False not in [x["numero"] > 0 for x in ModEvals] - if all_numbered and only_if_unumbered: - return # all ok - - # log('module_evaluation_renumber') - # Reset all numeros: - i = 1 - for e in ModEvals: - e["numero"] = i - do_evaluation_edit(e) - i += 1 - - # If requested, redirect to moduleimpl page: - if redirect: - return flask.redirect( - url_for( - "notes.moduleimpl_status", - scodoc_dept=g.scodoc_dept, - moduleimpl_id=moduleimpl_id, - ) - ) - - # -------------- VIEWS def evaluation_describe(evaluation_id="", edit_in_place=True): """HTML description of evaluation, for page headers @@ -1030,7 +612,7 @@ def evaluation_describe(evaluation_id="", edit_in_place=True): """ from app.scodoc import sco_saisie_notes - E = do_evaluation_list({"evaluation_id": evaluation_id})[0] + E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0] moduleimpl_id = E["moduleimpl_id"] M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] @@ -1054,13 +636,13 @@ def evaluation_describe(evaluation_id="", edit_in_place=True): etit = E["description"] or "" if etit: etit = ' "' + etit + '"' - if Mod["module_type"] == scu.MODULE_MALUS: + if Mod["module_type"] == ModuleType.MALUS: etit += ' (points de malus)' H = [ 'Evaluation%s

    Module : %s

    ' % (etit, mod_descr) ] - if Mod["module_type"] == scu.MODULE_MALUS: + if Mod["module_type"] == ModuleType.MALUS: # Indique l'UE ue = sco_edit_ue.ue_list(args={"ue_id": Mod["ue_id"]})[0] H.append("

    UE : %(acronyme)s

    " % ue) @@ -1099,269 +681,3 @@ def evaluation_describe(evaluation_id="", edit_in_place=True): H.append("

    ") return '
    ' + "\n".join(H) + "
    " - - -def evaluation_create_form( - moduleimpl_id=None, - evaluation_id=None, - edit=False, - readonly=False, - page_title="Evaluation", -): - "formulaire creation/edition des evaluations (pas des notes)" - if evaluation_id != None: - the_eval = do_evaluation_list({"evaluation_id": evaluation_id})[0] - moduleimpl_id = the_eval["moduleimpl_id"] - # - M = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[0] - is_malus = M["module"]["module_type"] == scu.MODULE_MALUS # True si module de malus - formsemestre_id = M["formsemestre_id"] - min_note_max = scu.NOTES_PRECISION # le plus petit bareme possible - if not readonly: - if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id): - return ( - html_sco_header.sco_header() - + "

    Opération non autorisée

    " - + "Modification évaluation impossible pour %s" - % current_user.get_nomplogin() - + "

    " - + '

    Revenir

    ' - % (moduleimpl_id,) - + html_sco_header.sco_footer() - ) - if readonly: - edit = True # montre les donnees existantes - if not edit: - # creation nouvel - if moduleimpl_id is None: - raise ValueError("missing moduleimpl_id parameter") - initvalues = { - "note_max": 20, - "jour": time.strftime("%d/%m/%Y", time.localtime()), - "publish_incomplete": is_malus, - } - submitlabel = "Créer cette évaluation" - action = "Création d'une é" - link = "" - else: - # edition donnees existantes - # setup form init values - if evaluation_id is None: - raise ValueError("missing evaluation_id parameter") - initvalues = the_eval - moduleimpl_id = initvalues["moduleimpl_id"] - submitlabel = "Modifier les données" - if readonly: - action = "E" - link = ( - 'voir toutes les notes du module' - % M["moduleimpl_id"] - ) - else: - action = "Modification d'une é" - link = "" - # Note maximale actuelle dans cette eval ? - etat = do_evaluation_etat(evaluation_id) - if etat["maxi_num"] is not None: - min_note_max = max(scu.NOTES_PRECISION, etat["maxi_num"]) - else: - min_note_max = scu.NOTES_PRECISION - # - if min_note_max > scu.NOTES_PRECISION: - min_note_max_str = scu.fmt_note(min_note_max) - else: - min_note_max_str = "0" - # - Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] - # - help = """

    - Le coefficient d'une évaluation n'est utilisé que pour pondérer les évaluations au sein d'un module. - Il est fixé librement par l'enseignant pour refléter l'importance de ses différentes notes - (examens, projets, travaux pratiques...). Ce coefficient est utilisé pour calculer la note - moyenne de chaque étudiant dans ce module. -

    - Ne pas confondre ce coefficient avec le coefficient du module, qui est lui fixé par le programme - pédagogique (le PPN pour les DUT) et pondère les moyennes de chaque module pour obtenir - les moyennes d'UE et la moyenne générale. -

    - L'option Visible sur bulletins indique que la note sera reportée sur les bulletins - en version dite "intermédiaire" (dans cette version, on peut ne faire apparaitre que certaines - notes, en sus des moyennes de modules. Attention, cette option n'empêche pas la publication sur - les bulletins en version "longue" (la note est donc visible par les étudiants sur le portail). -

    - Les modalités "rattrapage" et "deuxième session" définissent des évaluations prises en compte de - façon spéciale:

    -
      -
    • les notes d'une évaluation de "rattrapage" remplaceront les moyennes du module - si elles sont meilleures que celles calculées.
    • -
    • les notes de "deuxième session" remplacent, lorsqu'elles sont saisies, la moyenne de l'étudiant - à ce module, même si la note de deuxième session est plus faible.
    • -
    -

    - Dans ces deux cas, le coefficient est ignoré, et toutes les notes n'ont - pas besoin d'être rentrées. -

    -

    - Par ailleurs, les évaluations des modules de type "malus" sont toujours spéciales: le coefficient n'est pas utilisé. - Les notes de malus sont toujours comprises entre -20 et 20. Les points sont soustraits à la moyenne - de l'UE à laquelle appartient le module malus (si la note est négative, la moyenne est donc augmentée). -

    - """ - mod_descr = '%s %s %s' % ( - moduleimpl_id, - Mod["code"], - Mod["titre"], - link, - ) - if not readonly: - H = ["

    %svaluation en %s

    " % (action, mod_descr)] - else: - return evaluation_describe(evaluation_id) - - heures = ["%02dh%02d" % (h, m) for h in range(8, 19) for m in (0, 30)] - # - initvalues["visibulletin"] = initvalues.get("visibulletin", True) - if initvalues["visibulletin"]: - initvalues["visibulletinlist"] = ["X"] - else: - initvalues["visibulletinlist"] = [] - vals = scu.get_request_args() - if vals.get("tf_submitted", False) and "visibulletinlist" not in vals: - vals["visibulletinlist"] = [] - # - form = [ - ("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}), - ("formsemestre_id", {"default": formsemestre_id, "input_type": "hidden"}), - ("moduleimpl_id", {"default": moduleimpl_id, "input_type": "hidden"}), - # ('jour', { 'title' : 'Date (j/m/a)', 'size' : 12, 'explanation' : 'date de l\'examen, devoir ou contrôle' }), - ( - "jour", - { - "input_type": "date", - "title": "Date", - "size": 12, - "explanation": "date de l'examen, devoir ou contrôle", - }, - ), - ( - "heure_debut", - { - "title": "Heure de début", - "explanation": "heure du début de l'épreuve", - "input_type": "menu", - "allowed_values": heures, - "labels": heures, - }, - ), - ( - "heure_fin", - { - "title": "Heure de fin", - "explanation": "heure de fin de l'épreuve", - "input_type": "menu", - "allowed_values": heures, - "labels": heures, - }, - ), - ] - if is_malus: # pas de coefficient - form.append(("coefficient", {"input_type": "hidden", "default": "1."})) - else: - form.append( - ( - "coefficient", - { - "size": 10, - "type": "float", - "explanation": "coef. dans le module (choisi librement par l'enseignant)", - "allow_null": False, - }, - ) - ) - form += [ - ( - "note_max", - { - "size": 4, - "type": "float", - "title": "Notes de 0 à", - "explanation": "barème (note max actuelle: %s)" % min_note_max_str, - "allow_null": False, - "max_value": scu.NOTES_MAX, - "min_value": min_note_max, - }, - ), - ( - "description", - { - "size": 36, - "type": "text", - "explanation": 'type d\'évaluation, apparait sur le bulletins longs. Exemples: "contrôle court", "examen de TP", "examen final".', - }, - ), - ( - "visibulletinlist", - { - "input_type": "checkbox", - "allowed_values": ["X"], - "labels": [""], - "title": "Visible sur bulletins", - "explanation": "(pour les bulletins en version intermédiaire)", - }, - ), - ( - "publish_incomplete", - { - "input_type": "boolcheckbox", - "title": "Prise en compte immédiate", - "explanation": "notes utilisées même si incomplètes", - }, - ), - ( - "evaluation_type", - { - "input_type": "menu", - "title": "Modalité", - "allowed_values": ( - scu.EVALUATION_NORMALE, - scu.EVALUATION_RATTRAPAGE, - scu.EVALUATION_SESSION2, - ), - "type": "int", - "labels": ( - "Normale", - "Rattrapage (remplace si meilleure note)", - "Deuxième session (remplace toujours)", - ), - }, - ), - ] - tf = TrivialFormulator( - request.base_url, - vals, - form, - cancelbutton="Annuler", - submitlabel=submitlabel, - initvalues=initvalues, - readonly=readonly, - ) - - dest_url = "moduleimpl_status?moduleimpl_id=%s" % M["moduleimpl_id"] - if tf[0] == 0: - head = html_sco_header.sco_header(page_title=page_title) - return head + "\n".join(H) + "\n" + tf[1] + help + html_sco_header.sco_footer() - elif tf[0] == -1: - return flask.redirect(dest_url) - else: - # form submission - if tf[2]["visibulletinlist"]: - tf[2]["visibulletin"] = True - else: - tf[2]["visibulletin"] = False - if not edit: - # creation d'une evaluation - evaluation_id = do_evaluation_create(**tf[2]) - return flask.redirect(dest_url) - else: - do_evaluation_edit(tf[2]) - return flask.redirect(dest_url) diff --git a/app/scodoc/sco_formsemestre.py b/app/scodoc/sco_formsemestre.py index 6d29b8cf..307d1188 100644 --- a/app/scodoc/sco_formsemestre.py +++ b/app/scodoc/sco_formsemestre.py @@ -39,7 +39,6 @@ from app.scodoc import sco_codes_parcours from app.scodoc import sco_cache from app.scodoc import sco_formations from app.scodoc import sco_preferences -from app.scodoc import sco_users from app.scodoc.gen_tables import GenTable from app import log from app.scodoc.sco_codes_parcours import NO_SEMESTRE_ID @@ -369,7 +368,7 @@ def _write_formsemestre_aux(sem, fieldname, valuename): # uniquify values = set([str(x) for x in sem[fieldname]]) - cnx = ndb.GetDBConnexion(autocommit=False) + cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) tablename = "notes_formsemestre_" + fieldname try: @@ -398,6 +397,8 @@ def _write_formsemestre_aux(sem, fieldname, valuename): def sem_set_responsable_name(sem): "ajoute champs responsable_name" + from app.scodoc import sco_users + sem["responsable_name"] = ", ".join( [ sco_users.user_info(responsable_id)["nomprenom"] diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index bd36427d..ce4278db 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -49,6 +49,7 @@ from app.scodoc import sco_edit_module from app.scodoc import sco_edit_ue from app.scodoc import sco_etud from app.scodoc import sco_evaluations +from app.scodoc import sco_evaluation_db from app.scodoc import sco_formations from app.scodoc import sco_formsemestre from app.scodoc import sco_groups_copy @@ -851,7 +852,7 @@ def formsemestre_delete_moduleimpls(formsemestre_id, module_ids_to_del): )[0]["moduleimpl_id"] mod = sco_edit_module.module_list({"module_id": module_id})[0] # Evaluations dans ce module ? - evals = sco_evaluations.do_evaluation_list({"moduleimpl_id": moduleimpl_id}) + evals = sco_evaluation_db.do_evaluation_list({"moduleimpl_id": moduleimpl_id}) if evals: msg += [ 'impossible de supprimer %s (%s) car il y a %d évaluations définies (supprimer les d\'abord)' @@ -1030,14 +1031,14 @@ def do_formsemestre_clone( sco_moduleimpl.do_ens_create(args) # optionally, copy evaluations if clone_evaluations: - evals = sco_evaluations.do_evaluation_list( + evals = sco_evaluation_db.do_evaluation_list( args={"moduleimpl_id": mod_orig["moduleimpl_id"]} ) for e in evals: args = e.copy() del args["jour"] # erase date args["moduleimpl_id"] = mid - _ = sco_evaluations.do_evaluation_create(**args) + _ = sco_evaluation_db.do_evaluation_create(**args) # 3- copy uecoefs objs = sco_formsemestre.formsemestre_uecoef_list( @@ -1229,7 +1230,7 @@ def formsemestre_delete(formsemestre_id):
    """, ] - evals = sco_evaluations.do_evaluation_list_in_formsemestre(formsemestre_id) + evals = sco_evaluation_db.do_evaluation_list_in_formsemestre(formsemestre_id) if evals: H.append( """

    Attention: il y a %d évaluations dans ce semestre (sa suppression entrainera l'effacement définif des notes) !

    """ @@ -1311,7 +1312,7 @@ def do_formsemestre_delete(formsemestre_id): mods = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id) for mod in mods: # evaluations - evals = sco_evaluations.do_evaluation_list( + evals = sco_evaluation_db.do_evaluation_list( args={"moduleimpl_id": mod["moduleimpl_id"]} ) for e in evals: diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 04f17d5b..48af5949 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -36,6 +36,7 @@ from flask_login import current_user from app import log import app.scodoc.sco_utils as scu +from app.scodoc.sco_utils import ModuleType import app.scodoc.notesdb as ndb from app.scodoc.sco_permissions import Permission from app.scodoc.sco_exceptions import ScoValueError, ScoInvalidDateError @@ -50,6 +51,7 @@ from app.scodoc import sco_compute_moy from app.scodoc import sco_cache from app.scodoc import sco_edit_ue from app.scodoc import sco_evaluations +from app.scodoc import sco_evaluation_db from app.scodoc import sco_formations from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_edit @@ -459,7 +461,9 @@ def retreive_formsemestre_from_request() -> int: modimpl = modimpl[0] formsemestre_id = modimpl["formsemestre_id"] elif "evaluation_id" in args: - E = sco_evaluations.do_evaluation_list({"evaluation_id": args["evaluation_id"]}) + E = sco_evaluation_db.do_evaluation_list( + {"evaluation_id": args["evaluation_id"]} + ) if not E: return None # evaluation suppressed ? E = E[0] @@ -979,13 +983,22 @@ def formsemestre_status(formsemestre_id=None): # porté du DTML cnx = ndb.GetDBConnexion() sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True) - Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id) - # inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( - # args={"formsemestre_id": formsemestre_id} - # ) - prev_ue_id = None + modimpls = sco_moduleimpl.moduleimpl_withmodule_list( + formsemestre_id=formsemestre_id + ) + nt = sco_cache.NotesTableCache.get(formsemestre_id) + # Construit la liste de tous les enseignants de ce semestre: + mails_enseignants = set( + [sco_users.user_info(ens_id)["email"] for ens_id in sem["responsables"]] + ) + for modimpl in modimpls: + mails_enseignants.add(sco_users.user_info(modimpl["responsable_id"])["email"]) + mails_enseignants |= set( + [sco_users.user_info(m["ens_id"])["email"] for m in modimpl["ens"]] + ) can_edit = sco_formsemestre_edit.can_edit_sem(formsemestre_id, sem=sem) + use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id) H = [ html_sco_header.sco_header(page_title="Semestre %s" % sem["titreannee"]), @@ -995,158 +1008,69 @@ def formsemestre_status(formsemestre_id=None): ), """

    Tableau de bord: cliquez sur un module pour saisir des notes

    """, ] - nt = sco_cache.NotesTableCache.get(formsemestre_id) + if nt.expr_diagnostics: H.append(html_expr_diagnostic(nt.expr_diagnostics)) - H.append( - """ -

    - - - - - - -""" - ) - mails_enseignants = set( - [sco_users.user_info(ens_id)["email"] for ens_id in sem["responsables"]] - ) # adr. mail des enseignants - for M in Mlist: - Mod = M["module"] - ModDescr = ( - "Module " - + M["module"]["titre"] - + ", coef. " - + str(M["module"]["coefficient"]) - ) - ModEns = sco_users.user_info(M["responsable_id"])["nomcomplet"] - if M["ens"]: - ModEns += " (resp.), " + ", ".join( - [sco_users.user_info(e["ens_id"])["nomcomplet"] for e in M["ens"]] - ) - ModInscrits = sco_moduleimpl.do_moduleimpl_inscription_list( - moduleimpl_id=M["moduleimpl_id"] - ) - mails_enseignants.add(sco_users.user_info(M["responsable_id"])["email"]) - mails_enseignants |= set( - [sco_users.user_info(m["ens_id"])["email"] for m in M["ens"]] - ) - ue = M["ue"] - if prev_ue_id != ue["ue_id"]: - prev_ue_id = ue["ue_id"] - acronyme = ue["acronyme"] - titre = ue["titre"] - if sco_preferences.get_preference("use_ue_coefs", formsemestre_id): - titre += " (coef. %s)" % (ue["coefficient"] or 0.0) - H.append( - """") - - if M["ue"]["type"] != sco_codes_parcours.UE_STANDARD: - fontorange = " fontorange" # style css additionnel - else: - fontorange = "" - - etat = sco_evaluations.do_evaluation_etat_in_mod(nt, M["moduleimpl_id"]) - if ( - etat["nb_evals_completes"] > 0 - and etat["nb_evals_en_cours"] == 0 - and etat["nb_evals_vides"] == 0 - ): - H.append('' % fontorange) - else: - H.append('' % fontorange) - - H.append( - '' - % (M["moduleimpl_id"], ModDescr, Mod["code"]) - ) - H.append( - '' - % (M["moduleimpl_id"], ModDescr, Mod["abbrev"] or Mod["titre"]) - ) - H.append('' % len(ModInscrits)) - H.append( - '' - % ( - M["moduleimpl_id"], - ModEns, - sco_users.user_info(M["responsable_id"])["prenomnom"], - ) - ) - - if Mod["module_type"] == scu.MODULE_STANDARD: - H.append(' + """, + formsemestre_tableau_modules( + ressources, nt, formsemestre_id, can_edit=can_edit, show_ues=False + ), + f""" + """, + formsemestre_tableau_modules( + saes, nt, formsemestre_id, can_edit=can_edit, show_ues=False + ), + ] + if autres: + H += [ + f""" + """, + formsemestre_tableau_modules( + autres, nt, formsemestre_id, can_edit=can_edit, show_ues=False + ), + ] + H += [_TABLEAU_MODULES_FOOT, ""] + else: + # formations classiques: groupe par UE + H += [ + "

    ", + _TABLEAU_MODULES_HEAD, + formsemestre_tableau_modules( + modimpls, + nt, + formsemestre_id, + can_edit=can_edit, + use_ue_coefs=use_ue_coefs, + ), + _TABLEAU_MODULES_FOOT, + "

    ", + ] - H.append("") - H.append("
    CodeModuleInscritsResponsableEvaluations
    -%s -%s -""" - % (acronyme, titre) - ) - expr = sco_compute_moy.get_ue_expression( - formsemestre_id, ue["ue_id"], cnx, html_quote=True - ) - - if can_edit: - H.append( - ' ' - % (formsemestre_id, ue["ue_id"]) - ) - H.append( - scu.icontag( - "formula", - title="Mode calcul moyenne d'UE", - style="vertical-align:middle", - ) - ) - if can_edit: - H.append("") - if expr: - H.append( - """ %s""" - % expr - ) - - H.append("
    %s%s%s%s') - nb_evals = ( - etat["nb_evals_completes"] - + etat["nb_evals_en_cours"] - + etat["nb_evals_vides"] - ) - if nb_evals != 0: - H.append( - '%s prévues, %s ok' - % (M["moduleimpl_id"], nb_evals, etat["nb_evals_completes"]) - ) - if etat["nb_evals_en_cours"] > 0: - H.append( - ', %s en cours' - % (M["moduleimpl_id"], etat["nb_evals_en_cours"]) - ) - if etat["attente"]: - H.append( - ' [en attente]' - % M["moduleimpl_id"] - ) - elif Mod["module_type"] == scu.MODULE_MALUS: - nb_malus_notes = sum( - [ - e["etat"]["nb_notes"] - for e in nt.get_mod_evaluation_etat_list(M["moduleimpl_id"]) - ] - ) - H.append( - """ - malus (%d notes) + if nt.parcours.APC_SAE: + # BUT: tableau ressources puis SAE + ressources = [ + m for m in modimpls if m["module"]["module_type"] == ModuleType.RESSOURCE + ] + saes = [m for m in modimpls if m["module"]["module_type"] == ModuleType.SAE] + autres = [ + m + for m in modimpls + if m["module"]["module_type"] not in (ModuleType.RESSOURCE, ModuleType.SAE) + ] + H += [ """ - % (M["moduleimpl_id"], nb_malus_notes) - ) - else: - raise ValueError("Invalid module_type") # a bug +
    + """, + _TABLEAU_MODULES_HEAD, + f"""
    + Ressources +
    + SAÉs +
    + Autres modules +

    ") - if sco_preferences.get_preference("use_ue_coefs", formsemestre_id): + if use_ue_coefs: H.append( """

    utilise les coefficients d'UE pour calculer la moyenne générale.

    @@ -1166,3 +1090,156 @@ def formsemestre_status(formsemestre_id=None): % (",".join(adrlist), len(adrlist)) ) return "".join(H) + html_sco_header.sco_footer() + + +_TABLEAU_MODULES_HEAD = """ + + + + + + + + +""" +_TABLEAU_MODULES_FOOT = """
    CodeModuleInscritsResponsableÉvaluations
    """ + + +def formsemestre_tableau_modules( + modimpls, nt, formsemestre_id, can_edit=True, show_ues=True, use_ue_coefs=False +) -> str: + "Lignes table HTML avec modules du semestre" + H = [] + prev_ue_id = None + for modimpl in modimpls: + mod = modimpl["module"] + mod_descr = ( + "Module " + + modimpl["module"]["titre"] + + ", coef. " + + str(modimpl["module"]["coefficient"]) + ) + mod_ens = sco_users.user_info(modimpl["responsable_id"])["nomcomplet"] + if modimpl["ens"]: + mod_ens += " (resp.), " + ", ".join( + [sco_users.user_info(e["ens_id"])["nomcomplet"] for e in modimpl["ens"]] + ) + mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list( + moduleimpl_id=modimpl["moduleimpl_id"] + ) + + ue = modimpl["ue"] + if show_ues and (prev_ue_id != ue["ue_id"]): + prev_ue_id = ue["ue_id"] + titre = ue["titre"] + if use_ue_coefs: + titre += " (coef. %s)" % (ue["coefficient"] or 0.0) + H.append( + f""" + {ue["acronyme"]} + {titre} + """ + ) + if can_edit: + H.append( + ' ' + % (formsemestre_id, ue["ue_id"]) + ) + H.append( + scu.icontag( + "formula", + title="Mode calcul moyenne d'UE", + style="vertical-align:middle", + ) + ) + if can_edit: + H.append("") + + expr = sco_compute_moy.get_ue_expression( + formsemestre_id, ue["ue_id"], html_quote=True + ) + if expr: + H.append( + """ %s""" + % expr + ) + + H.append("") + + if modimpl["ue"]["type"] != sco_codes_parcours.UE_STANDARD: + fontorange = " fontorange" # style css additionnel + else: + fontorange = "" + + etat = sco_evaluations.do_evaluation_etat_in_mod(nt, modimpl["moduleimpl_id"]) + if ( + etat["nb_evals_completes"] > 0 + and etat["nb_evals_en_cours"] == 0 + and etat["nb_evals_vides"] == 0 + ): + H.append('' % fontorange) + else: + H.append('' % fontorange) + + H.append( + '%s' + % (modimpl["moduleimpl_id"], mod_descr, mod["code"]) + ) + H.append( + '%s' + % (modimpl["moduleimpl_id"], mod_descr, mod["abbrev"] or mod["titre"]) + ) + H.append('%s' % len(mod_inscrits)) + H.append( + '%s' + % ( + modimpl["moduleimpl_id"], + mod_ens, + sco_users.user_info(modimpl["responsable_id"])["prenomnom"], + ) + ) + + if mod["module_type"] in ( + ModuleType.STANDARD, + ModuleType.RESSOURCE, + ModuleType.SAE, + ): + H.append('') + nb_evals = ( + etat["nb_evals_completes"] + + etat["nb_evals_en_cours"] + + etat["nb_evals_vides"] + ) + if nb_evals != 0: + H.append( + '%s prévues, %s ok' + % (modimpl["moduleimpl_id"], nb_evals, etat["nb_evals_completes"]) + ) + if etat["nb_evals_en_cours"] > 0: + H.append( + ', %s en cours' + % (modimpl["moduleimpl_id"], etat["nb_evals_en_cours"]) + ) + if etat["attente"]: + H.append( + ' [en attente]' + % modimpl["moduleimpl_id"] + ) + elif mod["module_type"] == ModuleType.MALUS: + nb_malus_notes = sum( + [ + e["etat"]["nb_notes"] + for e in nt.get_mod_evaluation_etat_list(modimpl["moduleimpl_id"]) + ] + ) + H.append( + """ + malus (%d notes) + """ + % (modimpl["moduleimpl_id"], nb_malus_notes) + ) + else: + raise ValueError("Invalid module_type") # a bug + + H.append("") + return "\n".join(H) diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py index 310173fd..91bbdec3 100644 --- a/app/scodoc/sco_formsemestre_validation.py +++ b/app/scodoc/sco_formsemestre_validation.py @@ -956,7 +956,7 @@ def do_formsemestre_validation_auto(formsemestre_id): def formsemestre_validation_suppress_etud(formsemestre_id, etudid): """Suppression des decisions de jury pour un etudiant.""" log("formsemestre_validation_suppress_etud( %s, %s)" % (formsemestre_id, etudid)) - cnx = ndb.GetDBConnexion(autocommit=False) + cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) args = {"formsemestre_id": formsemestre_id, "etudid": etudid} try: @@ -1123,7 +1123,7 @@ def do_formsemestre_validate_previous_ue( cette UE (utile seulement pour les semestres extérieurs). """ sem = sco_formsemestre.get_formsemestre(formsemestre_id) - cnx = ndb.GetDBConnexion(autocommit=False) + cnx = ndb.GetDBConnexion() nt = sco_cache.NotesTableCache.get(formsemestre_id) # > get_etud_ue_status if ue_coefficient != None: sco_formsemestre.do_formsemestre_uecoef_edit_or_create( diff --git a/app/scodoc/sco_import_etuds.py b/app/scodoc/sco_import_etuds.py index fe34484f..657e3acc 100644 --- a/app/scodoc/sco_import_etuds.py +++ b/app/scodoc/sco_import_etuds.py @@ -262,7 +262,7 @@ def scolars_import_excel_file( et les inscrit dans le semestre indiqué (et à TOUS ses modules) """ log("scolars_import_excel_file: formsemestre_id=%s" % formsemestre_id) - cnx = ndb.GetDBConnexion(autocommit=False) + cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) annee_courante = time.localtime()[0] always_require_ine = sco_preferences.get_preference("always_require_ine") diff --git a/app/scodoc/sco_liste_notes.py b/app/scodoc/sco_liste_notes.py index 627e9185..8df208fa 100644 --- a/app/scodoc/sco_liste_notes.py +++ b/app/scodoc/sco_liste_notes.py @@ -37,13 +37,12 @@ import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app import log from app.scodoc.TrivialFormulator import TrivialFormulator -from app.scodoc import htmlutils from app.scodoc import html_sco_header from app.scodoc import sco_abs from app.scodoc import sco_cache from app.scodoc import sco_edit_module from app.scodoc import sco_evaluations -from app.scodoc import sco_excel +from app.scodoc import sco_evaluation_db from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_groups @@ -68,11 +67,11 @@ def do_evaluation_listenotes(): if "evaluation_id" in vals: evaluation_id = int(vals["evaluation_id"]) mode = "eval" - evals = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id}) + evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id}) if "moduleimpl_id" in vals and vals["moduleimpl_id"]: moduleimpl_id = int(vals["moduleimpl_id"]) mode = "module" - evals = sco_evaluations.do_evaluation_list({"moduleimpl_id": moduleimpl_id}) + evals = sco_evaluation_db.do_evaluation_list({"moduleimpl_id": moduleimpl_id}) if not mode: raise ValueError("missing argument: evaluation or module") if not evals: @@ -545,7 +544,7 @@ def _add_eval_columns( sum_notes = 0 notes = [] # liste des notes numeriques, pour calcul histogramme uniquement evaluation_id = e["evaluation_id"] - NotesDB = sco_evaluations.do_evaluation_get_all_notes(evaluation_id) + NotesDB = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id) for row in rows: etudid = row["etudid"] if etudid in NotesDB: @@ -710,7 +709,7 @@ def evaluation_check_absences(evaluation_id): EXC et pas justifie Ramene 3 listes d'etudid """ - E = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})[0] + E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0] if not E["jour"]: return [], [], [], [], [] # evaluation sans date @@ -731,7 +730,7 @@ def evaluation_check_absences(evaluation_id): Justs = set([x["etudid"] for x in Just]) # ensemble des etudiants avec justif # Les notes: - NotesDB = sco_evaluations.do_evaluation_get_all_notes(evaluation_id) + NotesDB = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id) ValButAbs = [] # une note mais noté absent AbsNonSignalee = [] # note ABS mais pas noté absent ExcNonSignalee = [] # note EXC mais pas noté absent @@ -764,7 +763,7 @@ def evaluation_check_absences(evaluation_id): def evaluation_check_absences_html(evaluation_id, with_header=True, show_ok=True): """Affiche etat verification absences d'une evaluation""" - E = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})[0] + E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0] am, pm, demijournee = _eval_demijournee(E) ( @@ -876,7 +875,7 @@ def formsemestre_check_absences_html(formsemestre_id): # Modules, dans l'ordre Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id) for M in Mlist: - evals = sco_evaluations.do_evaluation_list( + evals = sco_evaluation_db.do_evaluation_list( {"moduleimpl_id": M["moduleimpl_id"]} ) if evals: diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py index 41dcfb68..bbaf29d1 100644 --- a/app/scodoc/sco_moduleimpl_status.py +++ b/app/scodoc/sco_moduleimpl_status.py @@ -28,7 +28,6 @@ """Tableau de bord module """ import time -import urllib from flask import g, url_for from flask_login import current_user @@ -44,6 +43,7 @@ from app.scodoc import sco_compute_moy from app.scodoc import sco_cache 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_formations from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_status @@ -57,7 +57,7 @@ from app.scodoc import sco_users # menu evaluation dans moduleimpl def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0): "Menu avec actions sur une evaluation" - E = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})[0] + E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0] modimpl = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] group_id = sco_groups.get_default_group(modimpl["formsemestre_id"]) @@ -161,13 +161,13 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] sem = sco_formsemestre.get_formsemestre(formsemestre_id) F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0] - ModInscrits = sco_moduleimpl.do_moduleimpl_inscription_list( + mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list( moduleimpl_id=M["moduleimpl_id"] ) nt = sco_cache.NotesTableCache.get(formsemestre_id) - ModEvals = sco_evaluations.do_evaluation_list({"moduleimpl_id": moduleimpl_id}) - ModEvals.sort( + mod_evals = sco_evaluation_db.do_evaluation_list({"moduleimpl_id": moduleimpl_id}) + mod_evals.sort( key=lambda x: (x["numero"], x["jour"], x["heure_debut"]), reverse=True ) # la plus RECENTE en tête @@ -179,15 +179,19 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags() # module_resp = User.query.get(M["responsable_id"]) + mod_type_name = scu.MODULE_TYPE_NAMES[Mod["module_type"]] H = [ - html_sco_header.sco_header(page_title="Module %(titre)s" % Mod), - """

    Module %(code)s %(titre)s

    """ % Mod, - """
    - - -
    Responsable: """, - module_resp.get_nomcomplet(), # sco_users.user_info(M["responsable_id"])["nomprenom"], - f"""({module_resp.user_name})""", + html_sco_header.sco_header(page_title=f"{mod_type_name} {Mod['titre']}"), + f"""

    {mod_type_name} + {Mod['code']} {Mod['titre']}

    +
    + + +
    Responsable: + {module_resp.get_nomcomplet()} + ({module_resp.user_name}) + """, ] try: sco_moduleimpl.can_change_module_resp(moduleimpl_id) @@ -231,7 +235,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): # Ligne: Inscrits H.append( """
    Inscrits: %d étudiants""" - % len(ModInscrits) + % len(mod_inscrits) ) if current_user.has_permission(Permission.ScoEtudInscrit): H.append( @@ -297,7 +301,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): """

    %d évaluations : """ - % (len(ModEvals), moduleimpl_id) + % (len(mod_evals), moduleimpl_id) ) # # Liste les noms de partitions @@ -341,16 +345,16 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): """ % M ) - if ModEvals: + if mod_evals: H.append( '" ) H.append("""""") - eval_index = len(ModEvals) - 1 + eval_index = len(mod_evals) - 1 first = True - for eval in ModEvals: + for eval in mod_evals: etat = sco_evaluations.do_evaluation_etat( eval["evaluation_id"], partition_id=partition_id, @@ -399,7 +403,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): ) # Fleches: H.append('') - if eval_index != (len(ModEvals) - 1) and caneditevals: + if eval_index != (len(mod_evals) - 1) and caneditevals: H.append( '%s' % (eval["evaluation_id"], arrow_up) diff --git a/app/scodoc/sco_news.py b/app/scodoc/sco_news.py index e063a81d..5522e5cd 100644 --- a/app/scodoc/sco_news.py +++ b/app/scodoc/sco_news.py @@ -42,7 +42,6 @@ from app import log from app.scodoc import sco_formsemestre from app.scodoc import sco_moduleimpl from app.scodoc import sco_preferences -from app.scodoc import sco_users from app import email @@ -82,6 +81,8 @@ def add(typ, object=None, text="", url=None, max_frequency=False): Si max_frequency, ne genere pas 2 nouvelles identiques à moins de max_frequency secondes d'intervalle. """ + from app.scodoc import sco_users + authuser_name = current_user.user_name cnx = ndb.GetDBConnexion() args = { @@ -112,6 +113,7 @@ def scolar_news_summary(n=5): News are "compressed", ie redondant events are joined. """ from app.scodoc import sco_etud + from app.scodoc import sco_users cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) diff --git a/app/scodoc/sco_parcours_dut.py b/app/scodoc/sco_parcours_dut.py index cb10bf4b..acd87110 100644 --- a/app/scodoc/sco_parcours_dut.py +++ b/app/scodoc/sco_parcours_dut.py @@ -539,7 +539,7 @@ class SituationEtudParcoursGeneric(object): """Enregistre la decision (instance de DecisionSem) Enregistre codes semestre et UE, et autorisations inscription. """ - cnx = ndb.GetDBConnexion(autocommit=False) + cnx = ndb.GetDBConnexion() # -- check if decision.code_etat in self.parcours.UNUSED_CODES: raise ScoValueError("code decision invalide dans ce parcours") @@ -902,7 +902,7 @@ def formsemestre_validate_ues(formsemestre_id, etudid, code_etat_sem, assiduite) Les UE des semestres NON ASSIDUS ne sont jamais validées (code AJ). """ valid_semestre = CODES_SEM_VALIDES.get(code_etat_sem, False) - cnx = ndb.GetDBConnexion(autocommit=False) + cnx = ndb.GetDBConnexion() nt = sco_cache.NotesTableCache.get(formsemestre_id) # > get_ues, get_etud_ue_status ue_ids = [x["ue_id"] for x in nt.get_ues(etudid=etudid, filter_sport=True)] for ue_id in ue_ids: diff --git a/app/scodoc/sco_placement.py b/app/scodoc/sco_placement.py index 6fa6f412..5c7128bf 100644 --- a/app/scodoc/sco_placement.py +++ b/app/scodoc/sco_placement.py @@ -54,6 +54,7 @@ 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 @@ -137,7 +138,9 @@ 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_evaluations.do_evaluation_list({"evaluation_id": evaluation_id}) + eval_data = sco_evaluation_db.do_evaluation_list( + {"evaluation_id": evaluation_id} + ) if not eval_data: raise ScoValueError("invalid evaluation_id") self.groups_tree, self.has_groups, self.nb_groups = _get_group_info( @@ -236,7 +239,7 @@ class PlacementRunner: self.groups_ids = [ gid if gid != TOUS else form.tous_id for gid in form["groups"].data ] - self.eval_data = sco_evaluations.do_evaluation_list( + self.eval_data = sco_evaluation_db.do_evaluation_list( {"evaluation_id": self.evaluation_id} )[0] self.groups = sco_groups.listgroups(self.groups_ids) diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py index b7df01bc..ce4f2569 100644 --- a/app/scodoc/sco_recapcomplet.py +++ b/app/scodoc/sco_recapcomplet.py @@ -45,11 +45,11 @@ from app.scodoc import sco_bulletins, sco_excel from app.scodoc import sco_codes_parcours from app.scodoc import sco_cache from app.scodoc import sco_evaluations +from app.scodoc import sco_evaluation_db from app.scodoc import sco_formations from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_status from app.scodoc import sco_groups -from app.scodoc import sco_permissions from app.scodoc import sco_permissions_check from app.scodoc import sco_preferences from app.scodoc import sco_etud @@ -818,9 +818,9 @@ def _list_notes_evals(evals, etudid): or e["etat"]["evalattente"] or e["publish_incomplete"] ): - NotesDB = sco_evaluations.do_evaluation_get_all_notes(e["evaluation_id"]) - if etudid in NotesDB: - val = NotesDB[etudid]["value"] + notes_db = sco_evaluation_db.do_evaluation_get_all_notes(e["evaluation_id"]) + if etudid in notes_db: + val = notes_db[etudid]["value"] else: # Note manquante mais prise en compte immédiate: affiche ATT val = scu.NOTES_ATTENTE diff --git a/app/scodoc/sco_report.py b/app/scodoc/sco_report.py index 078da669..9ab42ec0 100644 --- a/app/scodoc/sco_report.py +++ b/app/scodoc/sco_report.py @@ -40,7 +40,7 @@ from flask import url_for, g, request import pydot import app.scodoc.sco_utils as scu -from app.models import NotesFormModalite +from app.models import FormationModalite from app.scodoc import notesdb as ndb from app.scodoc import html_sco_header from app.scodoc import sco_codes_parcours @@ -1340,7 +1340,7 @@ def graph_parcours( log("n=%s" % n) log("get=%s" % g.get_node(sem_node_name(s))) log("nodes names = %s" % [x.get_name() for x in g.get_node_list()]) - if s["modalite"] and s["modalite"] != NotesFormModalite.DEFAULT_MODALITE: + if s["modalite"] and s["modalite"] != FormationModalite.DEFAULT_MODALITE: modalite = " " + s["modalite"] else: modalite = "" diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py index 6e19f49f..b6c000ab 100644 --- a/app/scodoc/sco_saisie_notes.py +++ b/app/scodoc/sco_saisie_notes.py @@ -39,6 +39,7 @@ from flask import g, url_for, request from flask_login import current_user import app.scodoc.sco_utils as scu +from app.scodoc.sco_utils import ModuleType import app.scodoc.notesdb as ndb from app import log from app.scodoc.sco_exceptions import ( @@ -56,6 +57,7 @@ from app.scodoc import sco_abs from app.scodoc import sco_cache 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 import sco_formsemestre from app.scodoc import sco_formsemestre_inscriptions @@ -133,9 +135,13 @@ def _check_notes(notes, evaluation, mod): and 4 lists of etudid: invalids, withoutnotes, absents, tosuppress, existingjury """ note_max = evaluation["note_max"] - if mod["module_type"] == scu.MODULE_STANDARD: + if mod["module_type"] in ( + scu.ModuleType.STANDARD, + scu.ModuleType.RESSOURCE, + scu.ModuleType.SAE, + ): note_min = scu.NOTES_MIN - elif mod["module_type"] == scu.MODULE_MALUS: + elif mod["module_type"] == ModuleType.MALUS: note_min = -20.0 else: raise ValueError("Invalid module type") # bug @@ -176,7 +182,7 @@ def do_evaluation_upload_xls(): vals = scu.get_request_args() evaluation_id = int(vals["evaluation_id"]) comment = vals["comment"] - E = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})[0] + E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0] M = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=E["moduleimpl_id"])[0] # Check access # (admin, respformation, and responsable_id) @@ -253,7 +259,9 @@ def do_evaluation_upload_xls(): authuser, evaluation_id, L, comment ) # news - E = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})[0] + E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[ + 0 + ] M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] mod["moduleimpl_id"] = M["moduleimpl_id"] @@ -292,7 +300,7 @@ def do_evaluation_upload_xls(): def do_evaluation_set_missing(evaluation_id, value, dialog_confirmed=False): """Initialisation des notes manquantes""" - E = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})[0] + E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0] M = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=E["moduleimpl_id"])[0] # Check access # (admin, respformation, and responsable_id) @@ -300,7 +308,7 @@ def do_evaluation_set_missing(evaluation_id, value, dialog_confirmed=False): # XXX imaginer un redirect + msg erreur raise AccessDenied("Modification des notes impossible pour %s" % current_user) # - NotesDB = sco_evaluations.do_evaluation_get_all_notes(evaluation_id) + NotesDB = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id) etudids = sco_groups.do_evaluation_listeetuds_groups( evaluation_id, getallstudents=True, include_dems=False ) @@ -378,19 +386,19 @@ def do_evaluation_set_missing(evaluation_id, value, dialog_confirmed=False): def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False): "suppress all notes in this eval" - E = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})[0] + E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0] if sco_permissions_check.can_edit_notes( current_user, E["moduleimpl_id"], allow_ens=False ): # On a le droit de modifier toutes les notes # recupere les etuds ayant une note - NotesDB = sco_evaluations.do_evaluation_get_all_notes(evaluation_id) + NotesDB = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id) elif sco_permissions_check.can_edit_notes( current_user, E["moduleimpl_id"], allow_ens=True ): # Enseignant associé au module: ne peut supprimer que les notes qu'il a saisi - NotesDB = sco_evaluations.do_evaluation_get_all_notes( + NotesDB = sco_evaluation_db.do_evaluation_get_all_notes( evaluation_id, by_uid=current_user.id ) else: @@ -471,13 +479,13 @@ def _notes_add(user, evaluation_id: int, notes: list, comment=None, do_it=True): "etudiant %s: valeur de note invalide (%s)" % (etudid, value) ) # Recherche notes existantes - NotesDB = sco_evaluations.do_evaluation_get_all_notes(evaluation_id) + NotesDB = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id) # Met a jour la base - cnx = ndb.GetDBConnexion(autocommit=False) + cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) nb_changed = 0 nb_suppress = 0 - E = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})[0] + E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0] M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] existing_decisions = ( [] @@ -596,7 +604,7 @@ def _notes_add(user, evaluation_id: int, notes: list, comment=None, do_it=True): def saisie_notes_tableur(evaluation_id, group_ids=()): """Saisie des notes via un fichier Excel""" - evals = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id}) + evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id}) if not evals: raise ScoValueError("invalid evaluation_id") E = evals[0] @@ -767,7 +775,7 @@ def saisie_notes_tableur(evaluation_id, group_ids=()): def feuille_saisie_notes(evaluation_id, group_ids=[]): """Document Excel pour saisie notes dans l'évaluation et les groupes indiqués""" - evals = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id}) + evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id}) if not evals: raise ScoValueError("invalid evaluation_id") E = evals[0] @@ -866,7 +874,7 @@ def has_existing_decision(M, E, etudid): def saisie_notes(evaluation_id, group_ids=[]): """Formulaire saisie notes d'une évaluation pour un groupe""" group_ids = [int(group_id) for group_id in group_ids] - evals = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id}) + evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id}) if not evals: raise ScoValueError("invalid evaluation_id") E = evals[0] @@ -977,7 +985,7 @@ def saisie_notes(evaluation_id, group_ids=[]): def _get_sorted_etuds(E, etudids, formsemestre_id): sem = sco_formsemestre.get_formsemestre(formsemestre_id) - NotesDB = sco_evaluations.do_evaluation_get_all_notes( + notes_db = sco_evaluation_db.do_evaluation_get_all_notes( E["evaluation_id"] ) # Notes existantes cnx = ndb.GetDBConnexion() @@ -1017,14 +1025,14 @@ def _get_sorted_etuds(E, etudids, formsemestre_id): e["absinfo"] = '' + " ".join(warn_abs_lst) + " " # Note actuelle de l'étudiant: - if etudid in NotesDB: - e["val"] = _displayNote(NotesDB[etudid]["value"]) - comment = NotesDB[etudid]["comment"] + if etudid in notes_db: + e["val"] = _displayNote(notes_db[etudid]["value"]) + comment = notes_db[etudid]["comment"] if comment is None: comment = "" e["explanation"] = "%s (%s) %s" % ( - NotesDB[etudid]["date"].strftime("%d/%m/%y %Hh%M"), - NotesDB[etudid]["uid"], + notes_db[etudid]["date"].strftime("%d/%m/%y %Hh%M"), + notes_db[etudid]["uid"], comment, ) else: @@ -1073,7 +1081,11 @@ def _form_saisie_notes(E, M, group_ids, destination=""): ("comment", {"size": 44, "title": "Commentaire", "return_focus_next": True}), ("changed", {"default": "0", "input_type": "hidden"}), # changed in JS ] - if M["module"]["module_type"] == scu.MODULE_STANDARD: + if M["module"]["module_type"] in ( + ModuleType.STANDARD, + ModuleType.RESSOURCE, + ModuleType.SAE, + ): descr.append( ( "s3", @@ -1086,7 +1098,7 @@ def _form_saisie_notes(E, M, group_ids, destination=""): }, ) ) - elif M["module"]["module_type"] == scu.MODULE_MALUS: + elif M["module"]["module_type"] == ModuleType.MALUS: descr.append( ( "s3", @@ -1229,7 +1241,7 @@ def save_note(etudid=None, evaluation_id=None, value=None, comment=""): "save_note: evaluation_id=%s etudid=%s uid=%s value=%s" % (evaluation_id, etudid, authuser, value) ) - E = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})[0] + E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0] M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] Mod["url"] = url_for( diff --git a/app/scodoc/sco_ue_external.py b/app/scodoc/sco_ue_external.py index b06e187e..748df97a 100644 --- a/app/scodoc/sco_ue_external.py +++ b/app/scodoc/sco_ue_external.py @@ -27,7 +27,7 @@ """Fonction de gestion des UE "externes" (effectuees dans un cursus exterieur) -On rapatrie (saisit) les notes (et crédits ECTS). +On rapatrie (saisie) les notes (et crédits ECTS). Cas d'usage: les étudiants d'une formation gérée par ScoDoc peuvent suivre un certain nombre d'UE à l'extérieur. L'établissement a reconnu @@ -66,6 +66,7 @@ from app.scodoc import sco_edit_matiere from app.scodoc import sco_edit_module from app.scodoc import sco_edit_ue from app.scodoc import sco_evaluations +from app.scodoc import sco_evaluation_db from app.scodoc import sco_formations from app.scodoc import sco_formsemestre from app.scodoc import sco_moduleimpl @@ -148,13 +149,15 @@ def external_ue_inscrit_et_note(moduleimpl_id, formsemestre_id, notes_etuds): ) # Création d'une évaluation si il n'y en a pas déjà: - ModEvals = sco_evaluations.do_evaluation_list(args={"moduleimpl_id": moduleimpl_id}) - if len(ModEvals): + mod_evals = sco_evaluation_db.do_evaluation_list( + args={"moduleimpl_id": moduleimpl_id} + ) + if len(mod_evals): # met la note dans le première évaluation existante: - evaluation_id = ModEvals[0]["evaluation_id"] + evaluation_id = mod_evals[0]["evaluation_id"] else: # crée une évaluation: - evaluation_id = sco_evaluations.do_evaluation_create( + evaluation_id = sco_evaluation_db.do_evaluation_create( moduleimpl_id=moduleimpl_id, note_max=20.0, coefficient=1.0, diff --git a/app/scodoc/sco_undo_notes.py b/app/scodoc/sco_undo_notes.py index 9a13d988..a8bc46d8 100644 --- a/app/scodoc/sco_undo_notes.py +++ b/app/scodoc/sco_undo_notes.py @@ -53,6 +53,7 @@ from app.scodoc.intervals import intervalmap import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app.scodoc import sco_evaluations +from app.scodoc import sco_evaluation_db from app.scodoc import sco_formsemestre from app.scodoc import sco_moduleimpl from app.scodoc import sco_preferences @@ -105,12 +106,12 @@ class NotesOperation(dict): def list_operations(evaluation_id): """returns list of NotesOperation for this evaluation""" notes = list( - sco_evaluations.do_evaluation_get_all_notes( + sco_evaluation_db.do_evaluation_get_all_notes( evaluation_id, filter_suppressed=False ).values() ) notes_log = list( - sco_evaluations.do_evaluation_get_all_notes( + sco_evaluation_db.do_evaluation_get_all_notes( evaluation_id, filter_suppressed=False, table="notes_notes_log" ).values() ) @@ -148,7 +149,7 @@ def list_operations(evaluation_id): def evaluation_list_operations(evaluation_id): """Page listing operations on evaluation""" - E = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})[0] + E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0] M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] Ops = list_operations(evaluation_id) diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index f5a97a97..8be4b2bd 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -49,7 +49,7 @@ import pydot import requests from flask import g, request -from flask import url_for, make_response +from flask import url_for, make_response, jsonify from config import Config from app import log @@ -71,20 +71,23 @@ NOTES_ATTENTE = -1002.0 # note "en attente" (se calcule comme une note neutrali # Types de modules -MODULE_STANDARD = 0 -MODULE_MALUS = 1 - - class ModuleType(IntEnum): """Code des types de module.""" - # Stockés en BD dans NotesModule.module_type: ne pas modifier ces valeurs + # Stockés en BD dans Module.module_type: ne pas modifier ces valeurs STANDARD = 0 MALUS = 1 RESSOURCE = 2 # BUT SAE = 3 # BUT +MODULE_TYPE_NAMES = { + ModuleType.STANDARD: "Module", + ModuleType.MALUS: "Malus", + ModuleType.RESSOURCE: "Ressource", + ModuleType.SAE: "SAÉ", +} + MALUS_MAX = 20.0 MALUS_MIN = -20.0 @@ -667,6 +670,17 @@ def get_request_args(): return vals +def json_error(message, success=False, status=404): + """Simple JSON response, for errors""" + response = { + "success": success, + "status": status, + "message": message, + } + log(f"Error: {response}") + return jsonify(response), status + + def get_scodoc_version(): "return a string identifying ScoDoc version" return sco_version.SCOVERSION diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 25cb3ab9..f897375d 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -1,5 +1,5 @@ /* # -*- mode: css -*- - ScoDoc, (c) Emmanuel Viennet 1998 - 2020 + ScoDoc, (c) Emmanuel Viennet 1998 - 2021 */ html,body { @@ -1192,6 +1192,7 @@ table.formsemestre_status { tr.formsemestre_status { background-color: rgb(90%,90%,90%); } tr.formsemestre_status_green { background-color: #EFF7F2; } tr.formsemestre_status_ue { background-color: rgb(90%,90%,90%); } +tr.formsemestre_status_cat td { padding-top: 2ex;} table.formsemestre_status td { border-top: 1px solid rgb(80%,80%,80%); border-bottom: 1px solid rgb(80%,80%,80%); @@ -1236,6 +1237,7 @@ td.formsemestre_status_cell { span.status_ue_acro { font-weight: bold; } span.status_ue_title { font-style: italic; padding-left: 1cm;} +span.status_module_cat { font-weight: bold; } table.formsemestre_inscr td { padding-right: 1.25em; @@ -1268,11 +1270,18 @@ ul.ue_inscr_list li.etud { border-spacing: 1px; } -/* Modules */ +/* Tableau de bord module */ div.moduleimpl_tableaubord { padding: 7px; border: 2px solid gray; } +div.moduleimpl_type_sae { + background-color:#cfeccf; +} +div.moduleimpl_type_ressource { + background-color:#f5e9d2; +} + span.moduleimpl_abs_link { padding-right: 2em; } diff --git a/app/static/css/table_editor.css b/app/static/css/table_editor.css new file mode 100644 index 00000000..04faa943 --- /dev/null +++ b/app/static/css/table_editor.css @@ -0,0 +1,93 @@ +/* table_editor, par Sébastien L. + */ +body { + font-family: Arial, Helvetica, sans-serif; +} + +/***************************/ +/* Le tableau */ +/***************************/ +.tableau { + display: grid; + grid-auto-rows: minmax(24px, auto); +} + +.entete { + background: #09c; + font-weight: bold; +} + +.tableau>div { + padding: 4px; + border: 1px solid #999; + grid-column: var(--x) / span var(--nbX); + grid-row: var(--y) / span var(--nbY); +} + +/***************************/ +/* Attente */ +/***************************/ +.wait { + position: fixed; + top: 32px; + left: 50%; + height: 4px; + width: 32px; + margin-left: -16px; + background: #424242; + animation: attente 0.4s infinite alternate; + display: none; +} + +.go { + display: block; +} + +@keyframes attente { + 100% { + transform: translateY(-16px) rotate(360deg); + } +} + +/***************************/ +/* Système de modification */ +/***************************/ +.modifOnOff { + position: relative; + display: table; + margin: 16px; + padding-right: 8px; + cursor: pointer; +} + +.modifOnOff::before { + content: ''; + width: 40px; + height: 20px; + background: #c90; + position: absolute; + top: 0; + left: 100%; + border-radius: 20px; + transition: 0.2s; +} + +.modifOnOff::after { + content: ''; + width: 16px; + height: 16px; + background: #FFF; + position: absolute; + top: 2px; + left: calc(100% + 2px); + border-radius: 100%; + transition: 0.2s; +} + +.modifOnOff.active::before { + background: #9c0; +} + +.modifOnOff.active::after { + transform: translateX(20px); +} \ No newline at end of file diff --git a/app/static/js/table_editor.js b/app/static/js/table_editor.js new file mode 100644 index 00000000..692268af --- /dev/null +++ b/app/static/js/table_editor.js @@ -0,0 +1,56 @@ +/* table_editor, par Sébastien L. + */ + +/******************************/ +/* Gestion de la modification */ +/******************************/ + +function editableOnOff() { + if (this.classList.toggle("active")) { + document.querySelectorAll("[data-editable=true]").forEach(cellule => { + cellule.contentEditable = true; + cellule.addEventListener("input", delayBeforeSave); + cellule.addEventListener("blur", save); + }) + } else { + document.querySelectorAll("[data-editable=true]").forEach(cellule => { + cellule.removeAttribute("contentEditable"); + cellule.removeEventListener("input", delayBeforeSave); + cellule.removeEventListener("blur", save); + }) + } +} + +let timeout = 0; + +function delayBeforeSave() { + clearTimeout(timeout); + document.querySelector(".wait").classList.add("go"); + timeout = setTimeout(() => { save(this) }, 2000); +} + +/*****************************/ +/* Mise en place des données */ +/*****************************/ +function build_table(data) { + let output = ""; + + data.forEach((cellule) => { + output += ` +
    + ${cellule.data} +
    `; + }) + document.querySelector(".tableau").innerHTML = output; + document.querySelector(".modifOnOff").addEventListener("click", editableOnOff); +} \ No newline at end of file diff --git a/app/templates/pn/form_modules_ue_coefs.html b/app/templates/pn/form_modules_ue_coefs.html new file mode 100644 index 00000000..7c26f021 --- /dev/null +++ b/app/templates/pn/form_modules_ue_coefs.html @@ -0,0 +1,55 @@ + + + + + + + + + + + Édition coef. formation + + + +

    Formation {{formation.titre}} ({{formation.acronyme}}) + [version {{formation.version}}] code {{formation.code}}

    + +
    +
    Modifier
    +
    + + + + + \ No newline at end of file diff --git a/app/templates/scodoc/help/evaluations.html b/app/templates/scodoc/help/evaluations.html new file mode 100644 index 00000000..ea844fd8 --- /dev/null +++ b/app/templates/scodoc/help/evaluations.html @@ -0,0 +1,53 @@ +
    +

    + Le coefficient d'une évaluation est utilisé pour pondérer les + évaluations au sein d'un module. Il est fixé librement par l'enseignant + pour refléter l'importance de ses différentes notes (examens, projets, + travaux pratiques...). Ce coefficient est utilisé pour calculer la note + moyenne de chaque étudiant dans ce module. +

    + {%if is_apc%} +

    + Dans le BUT, une évaluation peut évaluer différents apprentissages critiques... (à compléter) + Le coefficient est multiplié par les poids vers chaque UE. +

    + {%endif%} +

    + Ne pas confondre ce coefficient avec le coefficient du module, qui est + lui fixé par le programme pédagogique (le PPN pour les DUT) et pondère + les moyennes de chaque module pour obtenir les moyennes d'UE et la + moyenne générale. +

    +

    + L'option Visible sur bulletins indique que la note sera + reportée sur les bulletins en version dite "intermédiaire" (dans cette + version, on peut ne faire apparaitre que certaines notes, en sus des + moyennes de modules. Attention, cette option n'empêche pas la + publication sur les bulletins en version "longue" (la note est donc + visible par les étudiants sur le portail). +

    +

    + Les modalités "rattrapage" et "deuxième session" définissent des + évaluations prises en compte de façon spéciale: +

    +
      +
    • les notes d'une évaluation de "rattrapage" remplaceront les moyennes + du module si elles sont meilleures que celles calculées. +
    • +
    • les notes de "deuxième session" remplacent, lorsqu'elles sont + saisies, la moyenne de l'étudiant à ce module, même si la note de + deuxième session est plus faible. +
    • +
    +

    + Dans ces deux cas, le coefficient est ignoré, et toutes les notes n'ont + pas besoin d'être rentrées. +

    +

    + Par ailleurs, les évaluations des modules de type "malus" sont + toujours spéciales: le coefficient n'est pas utilisé. Les notes de malus + sont toujours comprises entre -20 et 20. Les points sont soustraits à la + moyenne de l'UE à laquelle appartient le module malus (si la note est + négative, la moyenne est donc augmentée). +

    +
    \ No newline at end of file diff --git a/app/templates/scodoc/help/modules.html b/app/templates/scodoc/help/modules.html new file mode 100644 index 00000000..d01a5d35 --- /dev/null +++ b/app/templates/scodoc/help/modules.html @@ -0,0 +1,27 @@ +
    +

    + Les modules sont décrits dans le programme pédagogique. Un module est pour ce + logiciel l'unité pédagogique élémentaire. On va lui associer une note + à travers des évaluations.
    + Cette note (moyenne de module) sera utilisée pour calculer la moyenne + générale (et la moyenne de l'UE à laquelle appartient le module). Pour + cela, on utilisera le coefficient associé au module. +

    + +

    Un module possède un enseignant responsable + (typiquement celui qui dispense le cours magistral). On peut associer + au module une liste d'enseignants (typiquement les chargés de TD). + Tous ces enseignants, et le responsable du semestre, pourront + saisir et modifier les notes de ce module. +

    + {%if is_apc%} +

    + Dans le BUT, les modules peuvent être de type "ressource" ou "Situation + d'apprentissage et d'évaluation" (SAÉ). Ne pas oublier de préciser le + type, et de saisir les coefficients pondérant l'influence de la + ressource ou SAÉ vers les Unités d'Enseignement (UE). + Voir les détails sur + la documentation. +

    + {%endif%} +
    \ No newline at end of file diff --git a/app/views/__init__.py b/app/views/__init__.py index 9ceb5c41..d902de6c 100644 --- a/app/views/__init__.py +++ b/app/views/__init__.py @@ -16,7 +16,7 @@ notes_bp = Blueprint("notes", __name__) users_bp = Blueprint("users", __name__) absences_bp = Blueprint("absences", __name__) -from app.views import scodoc, notes, scolar, absences, users +from app.views import scodoc, notes, scolar, absences, users, pn_modules # Cette fonction est bien appelée avant toutes les requêtes diff --git a/app/views/notes.py b/app/views/notes.py index deea62bf..ac67f0e1 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -83,7 +83,6 @@ from app.scodoc import sco_archives from app.scodoc import sco_bulletins from app.scodoc import sco_bulletins_pdf from app.scodoc import sco_cache -from app.scodoc import sco_codes_parcours from app.scodoc import sco_compute_moy from app.scodoc import sco_cost_formation from app.scodoc import sco_debouche @@ -94,7 +93,8 @@ from app.scodoc import sco_edit_ue from app.scodoc import sco_etape_apogee_view from app.scodoc import sco_etud from app.scodoc import sco_evaluations -from app.scodoc import sco_excel +from app.scodoc import sco_evaluation_db +from app.scodoc import sco_evaluation_edit from app.scodoc import sco_export_results from app.scodoc import sco_formations from app.scodoc import sco_formsemestre @@ -306,12 +306,12 @@ sco_publish( ) sco_publish( "/module_evaluation_renumber", - sco_evaluations.module_evaluation_renumber, + sco_evaluation_db.module_evaluation_renumber, Permission.ScoView, ) sco_publish( "/module_evaluation_move", - sco_evaluations.module_evaluation_move, + sco_evaluation_db.module_evaluation_move, Permission.ScoView, ) sco_publish( @@ -354,7 +354,7 @@ def ue_table(formation_id=None, msg=""): @scodoc7func def ue_set_internal(ue_id): """""" - ue = models.formations.NotesUE.query.get(ue_id) + ue = models.formations.UniteEns.query.get(ue_id) if not ue: raise ScoValueError("invalid ue_id") ue.is_external = False @@ -1557,7 +1557,7 @@ sco_publish( @scodoc7func def evaluation_delete(evaluation_id): """Form delete evaluation""" - El = sco_evaluations.do_evaluation_list(args={"evaluation_id": evaluation_id}) + El = sco_evaluation_db.do_evaluation_list(args={"evaluation_id": evaluation_id}) if not El: raise ValueError("Evalution inexistante ! (%s)" % evaluation_id) E = El[0] @@ -1630,7 +1630,7 @@ def evaluation_delete(evaluation_id): sco_publish( "/do_evaluation_list", - sco_evaluations.do_evaluation_list, + sco_evaluation_db.do_evaluation_list, Permission.ScoView, ) @@ -1641,7 +1641,7 @@ sco_publish( @scodoc7func def evaluation_edit(evaluation_id): "form edit evaluation" - return sco_evaluations.evaluation_create_form( + return sco_evaluation_edit.evaluation_create_form( evaluation_id=evaluation_id, edit=True ) @@ -1652,7 +1652,7 @@ def evaluation_edit(evaluation_id): @scodoc7func def evaluation_create(moduleimpl_id): "form create evaluation" - return sco_evaluations.evaluation_create_form( + return sco_evaluation_edit.evaluation_create_form( moduleimpl_id=moduleimpl_id, edit=False ) diff --git a/app/views/pn_modules.py b/app/views/pn_modules.py new file mode 100644 index 00000000..722bcd7e --- /dev/null +++ b/app/views/pn_modules.py @@ -0,0 +1,171 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# ScoDoc +# +# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Emmanuel Viennet emmanuel.viennet@viennet.net +# +############################################################################## + +""" +PN / Edition des coefs + +Emmanuel Viennet, 2021 +""" + +from flask import url_for +from flask import jsonify +from flask import current_app, g, request +from flask.templating import render_template +from flask_login import current_user +from werkzeug.utils import redirect + +from config import Config + +from app import db +from app import models +from app.auth.models import User + +from app.comp import moy_ue +from app.decorators import scodoc, permission_required +from app.scodoc import sco_edit_formation +from app.views import notes_bp as bp + +# --------------- + +from app.scodoc import sco_utils as scu +from app.scodoc import notesdb as ndb +from app import log + +from app.models.formations import Formation, UniteEns, Module +from app.scodoc.sco_exceptions import ( + ScoValueError, + ScoLockedFormError, + ScoGenError, + AccessDenied, +) +from app.scodoc import html_sco_header +from app.scodoc.sco_permissions import Permission + + +@bp.route("/table_modules_ue_coefs/") +@scodoc +@permission_required(Permission.ScoView) +def table_modules_ue_coefs(formation_id): + """Description JSON de la table des coefs modules/UE dans une formation""" + _ = models.Formation.query.get_or_404(formation_id) # check + + df = moy_ue.df_load_ue_coefs(formation_id) + ues = models.UniteEns.query.filter_by(formation_id=formation_id).all() + modules = models.Module.query.filter_by(formation_id=formation_id).all() + # Titre des modules, en ligne + col_titres_mods = [ + { + "x": 1, # 1ere colonne + "y": row, + # "nbX": 1, + # "nbY": 1, + "style": "title_mod " + scu.ModuleType(mod.module_type).name, + "data": mod.code, + "title": mod.titre, + } + for (row, mod) in enumerate(modules, start=2) + ] + row_titres_ue = [ + { + "x": col, + "y": 1, # 1ere ligne + "nbX": 1, + "nbY": 1, + "style": "title_ue", + "data": ue.acronyme, + "title": ue.titre, + } + for (col, ue) in enumerate(ues, start=2) + ] + # Les champs de saisie + cells = [] + for (row, mod) in enumerate(modules, start=2): + for (col, ue) in enumerate(ues, start=2): + cells.append( + { + "x": col, + "y": row, + "style": "champs", + "data": df[ue.id][mod.id], + "editable": True, + "module_id": mod.id, + "ue_id": ue.id, + } + ) + return jsonify(col_titres_mods + row_titres_ue + cells) + + +@bp.route("/set_module_ue_coef", methods=["POST"]) +@scodoc +@permission_required(Permission.ScoChangeFormation) +def set_module_ue_coef(): + """Set coef from module to UE""" + try: + module_id = int(request.form["module_id"]) + except ValueError: + return scu.json_error("invalid module_id", 400) + try: + ue_id = int(request.form["ue_id"]) + except ValueError: + return scu.json_error("invalid ue_id", 400) + try: + coef = float(request.form["coef"].replace(",", ".")) + except ValueError: + return scu.json_error("invalid coef", 400) + module = models.Module.query.get(module_id) + if module is None: + return scu.json_error(f"module not found ({module_id})", 404) + ue = models.UniteEns.query.get(ue_id) + if not ue: + return scu.json_error(f"UE not found ({ue_id})", 404) + module.set_ue_coef(ue, coef) + db.session.commit() + sco_edit_formation.invalidate_sems_in_formation(module.formation_id) + return scu.json_error("ok", success=True, status=201) + + +@bp.route("/edit_modules_ue_coefs/") +@scodoc +@permission_required(Permission.ScoChangeFormation) +def edit_modules_ue_coefs(formation_id): + """Formulaire édition grille coefs EU/modules""" + formation = models.Formation.query.filter_by( + formation_id=formation_id + ).first_or_404() + return render_template( + "pn/form_modules_ue_coefs.html", + formation=formation, + data_source=url_for( + "notes.table_modules_ue_coefs", + scodoc_dept=g.scodoc_dept, + formation_id=formation_id, + ), + data_save=url_for( + "notes.set_module_ue_coef", + scodoc_dept=g.scodoc_dept, + ), + ) diff --git a/app/views/scodoc.py b/app/views/scodoc.py index 1722aca2..c8ea5aad 100644 --- a/app/views/scodoc.py +++ b/app/views/scodoc.py @@ -49,7 +49,7 @@ from wtforms.validators import ValidationError, DataRequired, Email, EqualTo import app from app.models import Departement, Identite -from app.models import FormSemestre, NotesFormsemestreInscription +from app.models import FormSemestre, FormsemestreInscription from app.models import ScoDocSiteConfig import sco_version from app.scodoc import sco_logos @@ -124,9 +124,7 @@ def get_etud_dept(): last_etud = None last_date = None for etud in etuds: - inscriptions = NotesFormsemestreInscription.query.filter_by( - etudid=etud.id - ).all() + inscriptions = FormsemestreInscription.query.filter_by(etudid=etud.id).all() for ins in inscriptions: date_fin = FormSemestre.query.get(ins.formsemestre_id).date_fin if (last_date is None) or date_fin > last_date: diff --git a/migrations/versions/6cfc21a7ae1b_coefs_modules_but.py b/migrations/versions/6cfc21a7ae1b_coefs_modules_but.py new file mode 100644 index 00000000..78bc6d74 --- /dev/null +++ b/migrations/versions/6cfc21a7ae1b_coefs_modules_but.py @@ -0,0 +1,35 @@ +"""coefs modules but + +Revision ID: 6cfc21a7ae1b +Revises: ada0d1f3d84f +Create Date: 2021-11-11 21:04:05.573172 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '6cfc21a7ae1b' +down_revision = 'ada0d1f3d84f' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('module_ue_coef', + sa.Column('module_id', sa.Integer(), nullable=False), + sa.Column('ue_id', sa.Integer(), nullable=False), + sa.Column('coef', sa.Float(), nullable=False), + sa.ForeignKeyConstraint(['module_id'], ['notes_modules.id'], ), + sa.ForeignKeyConstraint(['ue_id'], ['notes_ue.id'], ), + sa.PrimaryKeyConstraint('module_id', 'ue_id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('module_ue_coef') + # ### end Alembic commands ### diff --git a/requirements-3.9.txt b/requirements-3.9.txt index 4c83a175..5a00e282 100755 --- a/requirements-3.9.txt +++ b/requirements-3.9.txt @@ -37,8 +37,10 @@ lazy-object-proxy==1.6.0 Mako==1.1.4 MarkupSafe==2.0.1 mccabe==0.6.1 +numpy==1.21.4 openpyxl==3.0.7 packaging==21.0 +pandas==1.3.4 Pillow==8.3.1 pluggy==0.13.1 psycopg2==2.9.1 @@ -46,7 +48,6 @@ py==1.10.0 pycparser==2.20 pydot==1.4.2 PyJWT==2.1.0 -pylint==2.9.6 pyOpenSSL==20.0.1 pyparsing==2.4.7 pytest==6.2.4 @@ -58,8 +59,10 @@ redis==3.5.3 reportlab==3.6.1 requests==2.26.0 rq==1.9.0 +six==1.16.0 SQLAlchemy==1.4.22 toml==0.10.2 +tornado==6.1 urllib3==1.26.6 visitor==0.1.3 Werkzeug==2.0.1 diff --git a/tests/unit/sco_fake_gen.py b/tests/unit/sco_fake_gen.py index f9936522..19d6f0ec 100644 --- a/tests/unit/sco_fake_gen.py +++ b/tests/unit/sco_fake_gen.py @@ -17,7 +17,7 @@ import typing from config import Config from app.auth.models import User -from app.models import NotesFormModalite +from app.models import FormationModalite from app.scodoc import notesdb as ndb from app.scodoc import sco_codes_parcours from app.scodoc import sco_edit_formation @@ -26,6 +26,7 @@ from app.scodoc import sco_edit_module from app.scodoc import sco_edit_ue from app.scodoc import sco_etud from app.scodoc import sco_evaluations +from app.scodoc import sco_evaluation_db from app.scodoc import sco_formations from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_inscriptions @@ -222,7 +223,7 @@ class ScoFake(object): block_moyennes=None, gestion_semestrielle=None, bul_bgcolor=None, - modalite=NotesFormModalite.DEFAULT_MODALITE, + modalite=FormationModalite.DEFAULT_MODALITE, resp_can_edit=None, resp_can_change_ens=None, ens_can_edit_eval=None, @@ -283,8 +284,8 @@ class ScoFake(object): ): args = locals() del args["self"] - oid = sco_evaluations.do_evaluation_create(**args) - oids = sco_evaluations.do_evaluation_list(args={"evaluation_id": oid}) + oid = sco_evaluation_db.do_evaluation_create(**args) + oids = sco_evaluation_db.do_evaluation_list(args={"evaluation_id": oid}) if not oids: raise ScoValueError("evaluation not created !") return oids[0] diff --git a/tests/unit/test_but_modules.py b/tests/unit/test_but_modules.py index 96f68fbf..1b8aac93 100644 --- a/tests/unit/test_but_modules.py +++ b/tests/unit/test_but_modules.py @@ -15,8 +15,7 @@ login_user(admin_user) """ -def test_evaluation_poids(test_client): - """Association de poids vers les UE""" +def setup_formation_test(): G = sco_fake_gen.ScoFake(verbose=False) _f = G.create_formation( acronyme="F3", titre="Formation 2", titre_officiel="Titre officiel 2" @@ -33,14 +32,20 @@ def test_evaluation_poids(test_client): ue_id=_ue1["ue_id"], formation_id=_f["formation_id"], ) + return G, _f["id"], _ue1["id"], _ue2["id"], _ue3["id"], _mod["id"] + + +def test_evaluation_poids(test_client): + """Association de poids vers les UE""" + G, formation_id, ue1_id, ue2_id, ue3_id, module_id = setup_formation_test() sem = G.create_formsemestre( - formation_id=_f["formation_id"], + formation_id=formation_id, semestre_id=1, date_debut="01/01/2021", date_fin="30/06/2021", ) # formsemestre_id=716 mi = G.create_moduleimpl( - module_id=_mod["module_id"], + module_id=module_id, formsemestre_id=sem["formsemestre_id"], ) moduleimpl_id = mi["id"] @@ -51,17 +56,17 @@ def test_evaluation_poids(test_client): coefficient=0, ) evaluation_id = _e1["evaluation_id"] # evaluation_id=25246 - ue1_id = _ue1["id"] # ue1_id=1684 - formation_id = _f["id"] # formation_id=199 + # ue1_id=1684 + # formation_id=199 # - e1 = models.NotesEvaluation.query.get(evaluation_id) - ue1 = models.NotesUE.query.get(ue1_id) + e1 = models.Evaluation.query.get(evaluation_id) + ue1 = models.UniteEns.query.get(ue1_id) assert e1.ue_poids == [] p1 = 3.14 e1.set_ue_poids(ue1, p1) db.session.commit() assert e1.get_ue_poids_dict()[ue1_id] == p1 - ues = models.NotesUE.query.filter_by(formation_id=formation_id).all() + ues = models.UniteEns.query.filter_by(formation_id=formation_id).all() poids = [1.0, 2.0, 3.0] for (ue, p) in zip(ues, poids): e1.set_ue_poids(ue, p) @@ -81,3 +86,22 @@ def test_evaluation_poids(test_client): db.session.delete(e1) db.session.commit() assert len(models.EvaluationUEPoids.query.all()) == 0 + + +def test_modules_coefs(test_client): + """Coefs vers les UE (BUT)""" + G, formation_id, ue1_id, ue2_id, ue3_id, module_id = setup_formation_test() + ue1 = models.UniteEns.query.get(ue1_id) + ue2 = models.UniteEns.query.get(ue2_id) + mod = models.Module.query.get(module_id) + coef = 2.5 + mod.set_ue_coef(ue1, coef) + db.session.commit() + assert mod.ue_coefs[0].coef == coef + mod.set_ue_coef(ue2, 2 * coef) + db.session.commit() + assert set(mod.get_ue_coef_dict().values()) == {coef, 2 * coef} + assert set(mod.get_ue_coef_dict().keys()) == {ue1_id, ue2_id} + mod.delete_ue_coef(ue1) + db.session.commit() + assert len(mod.ue_coefs) == 1 diff --git a/tests/unit/test_caches.py b/tests/unit/test_caches.py index aa1f2e90..dd5d9a82 100644 --- a/tests/unit/test_caches.py +++ b/tests/unit/test_caches.py @@ -15,6 +15,7 @@ import app from app import db from app.scodoc import sco_cache from app.scodoc import sco_evaluations +from app.scodoc import sco_evaluation_db from app.scodoc import sco_formsemestre from app.scodoc import notesdb as ndb from config import TestConfig @@ -66,7 +67,7 @@ def test_cache_evaluations(test_client): raise Exception("no evaluations") # evaluation_id = sem_evals[0]["evaluation_id"] - eval_notes = sco_evaluations.do_evaluation_get_all_notes(evaluation_id) + eval_notes = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id) # should have been be cached, except if empty if eval_notes: assert sco_cache.EvaluationCache.get(evaluation_id) diff --git a/tests/unit/test_sco_basic.py b/tests/unit/test_sco_basic.py index 6917ee61..4dc2c5a0 100644 --- a/tests/unit/test_sco_basic.py +++ b/tests/unit/test_sco_basic.py @@ -25,6 +25,7 @@ from app.scodoc import sco_abs_views from app.scodoc import sco_bulletins from app.scodoc import sco_codes_parcours from app.scodoc import sco_evaluations +from app.scodoc import sco_evaluation_db from app.scodoc import sco_formsemestre_validation from app.scodoc import sco_parcours_dut from app.scodoc import sco_cache @@ -134,7 +135,7 @@ def run_sco_basic(verbose=False): # Modifie l'évaluation 2 pour "prise en compte immédiate" e2["publish_incomplete"] = True - sco_evaluations.do_evaluation_edit(e2) + sco_evaluation_db.do_evaluation_edit(e2) etat = sco_evaluations.do_evaluation_etat(e2["evaluation_id"]) assert etat["evalcomplete"] == False assert etat["nb_att"] == 0 # il n'y a pas de notes (explicitement) en attente