# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2023 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 évaluations (ScoDoc7, code en voie de modernisation) """ import pprint import flask from flask import url_for, g from flask_login import current_user from app import db, log from app.models import Evaluation, ModuleImpl, ScolarNews from app.models.evaluations import evaluation_enrich_dict, check_evaluation_args 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_moduleimpl 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 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 ScoDoc7 en SQLAlchemy cnx = ndb.GetDBConnexion() evals = _evaluationEditor.list(cnx, args, sortkey=sortkey) # calcule duree (chaine de car.) de chaque evaluation et ajoute jour_iso, 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 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( f"Modification évaluation impossible pour {current_user.get_nomplogin()}" ) args = locals() log("do_evaluation_create: args=" + str(args)) modimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id) if modimpl is None: raise ValueError("module not found") check_evaluation_args(args) # Check numeros moduleimpl_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 = moduleimpl_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 sco_cache.invalidate_formsemestre(formsemestre_id=modimpl.formsemestre_id) url = url_for( "notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id, ) ScolarNews.add( typ=ScolarNews.NEWS_NOTE, obj=moduleimpl_id, text=f"""Création d'une évaluation dans <a href="{url}">{ modimpl.module.titre or '(module sans titre)'}</a>""", url=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" evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id) modimpl: ModuleImpl = evaluation.moduleimpl if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=modimpl.id): raise AccessDenied( f"Modification évaluation impossible pour {current_user.get_nomplogin()}" ) notes_db = do_evaluation_get_all_notes(evaluation_id) # { etudid : value } notes = [x["value"] for x in notes_db.values()] if notes: raise ScoValueError( "Impossible de supprimer cette évaluation: il reste des notes" ) db.session.delete(evaluation) db.session.commit() # inval cache pour ce semestre sco_cache.invalidate_formsemestre(formsemestre_id=modimpl.formsemestre_id) # news url = url_for( "notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id, ) ScolarNews.add( typ=ScolarNews.NEWS_NOTE, obj=modimpl.id, text=f"""Suppression d'une évaluation dans <a href="{ url }">{modimpl.module.titre}</a>""", url=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 is not 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 moduleimpl_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('moduleimpl_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 moduleimpl_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") moduleimpl_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 moduleimpl_evaluation_move(evaluation_id: int, after=0, redirect=1): """Move before/after previous one (decrement/increment numero) (published) """ evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id) moduleimpl_id = evaluation.moduleimpl_id redirect = int(redirect) # access: can change eval ? if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id): raise AccessDenied( f"Modification évaluation impossible pour {current_user.get_nomplogin()}" ) moduleimpl_evaluation_renumber(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: moduleimpl_evaluation_move: forcing renumber") moduleimpl_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"], ) )