diff --git a/app/comp/res_but.py b/app/comp/res_but.py index a7af95655..7e866a566 100644 --- a/app/comp/res_but.py +++ b/app/comp/res_but.py @@ -306,12 +306,12 @@ class ResultatsSemestreBUT(NotesTableCompat): return ues_ids - def etud_has_decision(self, etudid): + def etud_has_decision(self, etudid) -> bool: """True s'il y a une décision de jury pour cet étudiant émanant de ce formsemestre. prend aussi en compte les autorisations de passage. Sous-classée en BUT pour les RCUEs et années. """ - return ( + return bool( super().etud_has_decision(etudid) or ApcValidationAnnee.query.filter_by( formsemestre_id=self.formsemestre.id, etudid=etudid diff --git a/app/comp/res_compat.py b/app/comp/res_compat.py index cbebb8356..d5e85d99d 100644 --- a/app/comp/res_compat.py +++ b/app/comp/res_compat.py @@ -283,12 +283,12 @@ class NotesTableCompat(ResultatsSemestre): ] return etudids - def etud_has_decision(self, etudid): + def etud_has_decision(self, etudid) -> bool: """True s'il y a une décision de jury pour cet étudiant émanant de ce formsemestre. prend aussi en compte les autorisations de passage. Sous-classée en BUT pour les RCUEs et années. """ - return ( + return bool( self.get_etud_decisions_ue(etudid) or self.get_etud_decision_sem(etudid) or ScolarAutorisationInscription.query.filter_by( diff --git a/app/models/evaluations.py b/app/models/evaluations.py index 3e3cd07cd..fd777b1c2 100644 --- a/app/models/evaluations.py +++ b/app/models/evaluations.py @@ -145,6 +145,18 @@ class Evaluation(db.Model): db.session.add(copy) return copy + def is_matin(self) -> bool: + "Evaluation ayant lieu le matin (faux si pas de date)" + heure_debut_dt = self.heure_debut or datetime.time(8, 00) + # 8:00 au cas ou pas d'heure (note externe?) + return bool(self.jour) and heure_debut_dt < datetime.time(12, 00) + + def is_apresmidi(self) -> bool: + "Evaluation ayant lieu l'après midi (faux si pas de date)" + heure_debut_dt = self.heure_debut or datetime.time(8, 00) + # 8:00 au cas ou pas d'heure (note externe?) + return bool(self.jour) and heure_debut_dt >= datetime.time(12, 00) + def set_default_poids(self) -> bool: """Initialize les poids bvers les UE à leurs valeurs par défaut C'est à dire à 1 si le coef. module/UE est non nul, 0 sinon. diff --git a/app/scodoc/sco_evaluation_db.py b/app/scodoc/sco_evaluation_db.py index b99af2cc3..44ec6a768 100644 --- a/app/scodoc/sco_evaluation_db.py +++ b/app/scodoc/sco_evaluation_db.py @@ -255,9 +255,8 @@ def do_evaluation_get_all_notes( """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 + # pas de cache pour (rares) appels via undo_notes ou specifiant un enseignant + do_cache = filter_suppressed and table == "notes_notes" and (by_uid is None) if do_cache: r = sco_cache.EvaluationCache.get(evaluation_id) if r is not None: diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py index 2f2c93b55..922951379 100644 --- a/app/scodoc/sco_excel.py +++ b/app/scodoc/sco_excel.py @@ -433,7 +433,7 @@ def excel_simple_table( return ws.generate() -def excel_feuille_saisie(e, titreannee, description, lines): +def excel_feuille_saisie(evaluation: "Evaluation", titreannee, description, lines): """Genere feuille excel pour saisie des notes. E: evaluation (dict) lines: liste de tuples @@ -512,18 +512,20 @@ def excel_feuille_saisie(e, titreannee, description, lines): # description evaluation ws.append_single_cell_row(scu.unescape_html(description), style_titres) ws.append_single_cell_row( - "Evaluation du %s (coef. %g)" % (e["jour"], e["coefficient"]), style + "Evaluation du %s (coef. %g)" + % (evaluation.jour or "sans date", evaluation.coefficient or 0.0), + style, ) # ligne blanche ws.append_blank_row() # code et titres colonnes ws.append_row( [ - ws.make_cell("!%s" % e["evaluation_id"], style_ro), + ws.make_cell("!%s" % evaluation.id, style_ro), ws.make_cell("Nom", style_titres), ws.make_cell("Prénom", style_titres), ws.make_cell("Groupe", style_titres), - ws.make_cell("Note sur %g" % e["note_max"], style_titres), + ws.make_cell("Note sur %g" % (evaluation.note_max or 0.0), style_titres), ws.make_cell("Remarque", style_titres), ] ) diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py index 14a103809..f9f39bf32 100644 --- a/app/scodoc/sco_saisie_notes.py +++ b/app/scodoc/sco_saisie_notes.py @@ -40,7 +40,7 @@ from app.auth.models import User from app.comp import res_sem from app.comp.res_compat import NotesTableCompat from app.models import Evaluation, FormSemestre -from app.models import ModuleImpl, ScolarNews +from app.models import ModuleImpl, NotesNotes, ScolarNews from app.models.etudiants import Identite import app.scodoc.sco_utils as scu from app.scodoc.sco_utils import ModuleType @@ -50,7 +50,8 @@ from app.scodoc.sco_exceptions import ( AccessDenied, InvalidNoteValue, NoteProcessError, - ScoGenError, + ScoBugCatcher, + ScoException, ScoInvalidParamError, ScoValueError, ) @@ -63,7 +64,6 @@ 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 from app.scodoc import sco_groups from app.scodoc import sco_groups_view @@ -526,9 +526,8 @@ def notes_add( - si la note existe deja avec valeur distincte, ajoute une entree au log (notes_notes_log) Return tuple (nb_changed, nb_suppress, existing_decisions) """ - now = psycopg2.Timestamp( - *time.localtime()[:6] - ) # datetime.datetime.now().isoformat() + now = psycopg2.Timestamp(*time.localtime()[:6]) + # Verifie inscription et valeur note inscrits = { x[0] @@ -550,11 +549,11 @@ def notes_add( cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) nb_changed = 0 nb_suppress = 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 = ( - [] - ) # etudids pour lesquels il y a une decision de jury et que la note change + evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id) + formsemestre: FormSemestre = evaluation.moduleimpl.formsemestre + res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) + # etudids pour lesquels il y a une decision de jury et que la note change: + etudids_with_existing_decision = [] try: for etudid, value in notes: changed = False @@ -571,20 +570,29 @@ def notes_add( "date": now, } ndb.quote_dict(aa) - cursor.execute( - """INSERT INTO notes_notes - (etudid, evaluation_id, value, comment, date, uid) - VALUES (%(etudid)s,%(evaluation_id)s,%(value)s,%(comment)s,%(date)s,%(uid)s) - """, - aa, - ) + try: + cursor.execute( + """INSERT INTO notes_notes + (etudid, evaluation_id, value, comment, date, uid) + VALUES (%(etudid)s,%(evaluation_id)s,%(value)s,%(comment)s,%(date)s,%(uid)s) + """, + aa, + ) + except psycopg2.errors.UniqueViolation as exc: + # XXX ne devrait pas arriver mais bug possible ici (non reproductible) + existing_note = NotesNotes.query.filter_by( + evaluation_id=evaluation_id, etudid=etudid + ).first() + raise ScoBugCatcher( + f"dup: existing={existing_note}" + ) from exc changed = True else: # il y a deja une note oldval = notes_db[etudid]["value"] if type(value) != type(oldval): changed = True - elif type(value) == float and ( + elif isinstance(value, float) and ( abs(value - oldval) > scu.NOTES_PRECISION ): changed = True @@ -646,26 +654,21 @@ def notes_add( nb_suppress += 1 if changed: nb_changed += 1 - if has_existing_decision(M, E, etudid): - existing_decisions.append(etudid) + if res.etud_has_decision(etudid): + etudids_with_existing_decision.append(etudid) except Exception as exc: log("*** exception in notes_add") if do_it: cnx.rollback() # abort # inval cache - sco_cache.invalidate_formsemestre( - formsemestre_id=M["formsemestre_id"] - ) # > modif notes (exception) + sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id) sco_cache.EvaluationCache.delete(evaluation_id) - raise # XXX - raise ScoGenError("Erreur enregistrement note: merci de ré-essayer") from exc + raise ScoException from exc if do_it: cnx.commit() - sco_cache.invalidate_formsemestre( - formsemestre_id=M["formsemestre_id"] - ) # > modif notes + sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id) sco_cache.EvaluationCache.delete(evaluation_id) - return nb_changed, nb_suppress, existing_decisions + return nb_changed, nb_suppress, etudids_with_existing_decision def saisie_notes_tableur(evaluation_id, group_ids=()): @@ -868,35 +871,35 @@ 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_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id}) - if not evals: + evaluation: Evaluation = Evaluation.query.get(evaluation_id) + if not evaluation: raise ScoValueError("invalid evaluation_id") - eval_dict = evals[0] - M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=eval_dict["moduleimpl_id"])[0] - formsemestre_id = M["formsemestre_id"] - Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] - sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"]) - mod_responsable = sco_users.user_info(M["responsable_id"]) - if eval_dict["jour"]: - indication_date = ndb.DateDMYtoISO(eval_dict["jour"]) + modimpl = evaluation.moduleimpl + formsemestre = modimpl.formsemestre + mod_responsable = sco_users.user_info(modimpl.responsable_id) + if evaluation.jour: + indication_date = evaluation.jour.isoformat() else: - indication_date = scu.sanitize_filename(eval_dict["description"])[:12] - eval_name = "%s-%s" % (Mod["code"], indication_date) + indication_date = scu.sanitize_filename(evaluation.description or "")[:12] + eval_name = "%s-%s" % (evaluation.moduleimpl.module.code, indication_date) - if eval_dict["description"]: - evaltitre = "%s du %s" % (eval_dict["description"], eval_dict["jour"]) + if evaluation.description: + evaltitre = "%s du %s" % ( + evaluation.description, + evaluation.jour.strftime("%d/%m/%Y"), + ) else: - evaltitre = "évaluation du %s" % eval_dict["jour"] + evaltitre = "évaluation du %s" % evaluation.jour.strftime("%d/%m/%Y") description = "%s en %s (%s) resp. %s" % ( evaltitre, - Mod["abbrev"] or "", - Mod["code"] or "", + evaluation.moduleimpl.module.abbrev or "", + evaluation.moduleimpl.module.code, mod_responsable["prenomnom"], ) groups_infos = sco_groups_view.DisplayedGroupsInfos( group_ids=group_ids, - formsemestre_id=formsemestre_id, + formsemestre_id=formsemestre.id, select_all_when_unspecified=True, etat=None, ) @@ -919,15 +922,15 @@ def feuille_saisie_notes(evaluation_id, group_ids=[]): # une liste de liste de chaines: lignes de la feuille de calcul L = [] - etuds = _get_sorted_etuds(eval_dict, etudids, formsemestre_id) + etuds = _get_sorted_etuds(evaluation, etudids, formsemestre.id) for e in etuds: etudid = e["etudid"] - groups = sco_groups.get_etud_groups(etudid, formsemestre_id) + groups = sco_groups.get_etud_groups(etudid, formsemestre.id) grc = sco_groups.listgroups_abbrev(groups) L.append( [ - "%s" % etudid, + str(etudid), e["nom"].upper(), e["prenom"].lower().capitalize(), e["inscr"]["etat"], @@ -939,29 +942,9 @@ def feuille_saisie_notes(evaluation_id, group_ids=[]): filename = "notes_%s_%s" % (eval_name, gr_title_filename) xls = sco_excel.excel_feuille_saisie( - eval_dict, sem["titreannee"], description, lines=L + evaluation, formsemestre.titre_annee(), description, lines=L ) return scu.send_file(xls, filename, scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE) - # return sco_excel.send_excel_file(xls, filename) - - -def has_existing_decision(M, E, etudid): - """Verifie s'il y a une validation pour cet etudiant dans ce semestre ou UE - Si oui, return True - """ - formsemestre_id = M["formsemestre_id"] - formsemestre = FormSemestre.get_formsemestre(formsemestre_id) - nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) - if nt.get_etud_decision_sem(etudid): - return True - dec_ues = nt.get_etud_decisions_ue(etudid) - if dec_ues: - mod = sco_edit_module.module_list({"module_id": M["module_id"]})[0] - ue_id = mod["ue_id"] - if ue_id in dec_ues: - return True # decision pour l'UE a laquelle appartient cette evaluation - - return False # pas de decision de jury affectee par cette note # ----------------------------- @@ -973,20 +956,18 @@ def saisie_notes(evaluation_id: int, group_ids: list = None): if not isinstance(evaluation_id, int): raise ScoInvalidParamError() group_ids = [int(group_id) for group_id in (group_ids or [])] - evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id}) - if not evals: + evaluation: Evaluation = Evaluation.query.get(evaluation_id) + if evaluation is None: raise ScoValueError("évaluation inexistante") - E = evals[0] - M = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=E["moduleimpl_id"])[0] - formsemestre_id = M["formsemestre_id"] + modimpl = evaluation.moduleimpl moduleimpl_status_url = url_for( "notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, - moduleimpl_id=E["moduleimpl_id"], + moduleimpl_id=evaluation.moduleimpl_id, ) # Check access # (admin, respformation, and responsable_id) - if not sco_permissions_check.can_edit_notes(current_user, E["moduleimpl_id"]): + if not sco_permissions_check.can_edit_notes(current_user, evaluation.moduleimpl_id): return f""" {html_sco_header.sco_header()}