From 0175b3e4d45e8c0f397a826a727aa361544a5f84 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 13 Jul 2022 15:21:31 +0200 Subject: [PATCH 01/16] comment --- app/scodoc/sco_apogee_csv.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/scodoc/sco_apogee_csv.py b/app/scodoc/sco_apogee_csv.py index eef95a092..e6b40842b 100644 --- a/app/scodoc/sco_apogee_csv.py +++ b/app/scodoc/sco_apogee_csv.py @@ -433,8 +433,6 @@ class ApoEtud(dict): return VOID_APO_RES # Elements UE - # if etudid == 3661 and nt.formsemestre.semestre_id == 2: # XXX XXX XXX - # breakpoint() decisions_ue = nt.get_etud_decision_ues(etudid) for ue in nt.get_ues_stat_dict(): if ue["code_apogee"] and code in { From 48e31b5f3900f39693db9697cf9f5876be71b6e1 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 13 Jul 2022 18:52:07 +0200 Subject: [PATCH 02/16] Modernisation suppressions UE et formations --- app/models/formations.py | 6 +- app/models/ues.py | 5 +- app/scodoc/sco_cache.py | 2 +- app/scodoc/sco_edit_formation.py | 40 +++++++++----- app/scodoc/sco_edit_ue.py | 95 ++++++++++++++++++-------------- app/scodoc/sco_exceptions.py | 2 +- tests/unit/test_formations.py | 2 +- 7 files changed, 88 insertions(+), 64 deletions(-) diff --git a/app/models/formations.py b/app/models/formations.py index d2970fce9..224b8475a 100644 --- a/app/models/formations.py +++ b/app/models/formations.py @@ -105,9 +105,11 @@ class Formation(db.Model): return len(self.formsemestres.filter_by(etat=False).all()) > 0 def invalidate_module_coefs(self, semestre_idx: int = None): - """Invalide les coefficients de modules cachés. - Si semestre_idx est None, invalide tous les semestres, + """Invalide le cache des coefficients de modules. + Si semestre_idx est None, invalide les coefs de tous les semestres, sinon invalide le semestre indiqué et le cache de la formation. + + Dans tous les cas, invalide tous les formsemestres. """ if semestre_idx is None: keys = {f"{self.id}.{m.semestre_id}" for m in self.modules} diff --git a/app/models/ues.py b/app/models/ues.py index 450482bc9..01579dc14 100644 --- a/app/models/ues.py +++ b/app/models/ues.py @@ -83,10 +83,9 @@ class UniteEns(db.Model): return sco_edit_ue.ue_is_locked(self.id) def can_be_deleted(self) -> bool: - """True si l'UE n'est pas utilisée dans des formsemestre - et n'a pas de module rattachés + """True si l'UE n'a pas de moduleimpl rattachés + (pas un seul module de cette UE n'a de modimpl) """ - # "pas un seul module de cette UE n'a de modimpl..."" return (self.modules.count() == 0) or not any( m.modimpls.all() for m in self.modules ) diff --git a/app/scodoc/sco_cache.py b/app/scodoc/sco_cache.py index 65307d7e9..7a20a1a6c 100644 --- a/app/scodoc/sco_cache.py +++ b/app/scodoc/sco_cache.py @@ -228,7 +228,7 @@ class TableRecapWithEvalsCache(ScoDocCache): def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=False) formsemestre_id=None, pdfonly=False ): - """expire cache pour un semestre (ou tous si formsemestre_id non spécifié). + """expire cache pour un semestre (ou tous ceux du département si formsemestre_id non spécifié). Si pdfonly, n'expire que les bulletins pdf cachés. """ from app.models.formsemestre import FormSemestre diff --git a/app/scodoc/sco_edit_formation.py b/app/scodoc/sco_edit_formation.py index d7ea5e976..752bdef68 100644 --- a/app/scodoc/sco_edit_formation.py +++ b/app/scodoc/sco_edit_formation.py @@ -42,7 +42,7 @@ from app.models import ScolarNews import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message -from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError +from app.scodoc.sco_exceptions import ScoValueError, ScoNonEmptyFormationObject from app.scodoc import html_sco_header from app.scodoc import sco_codes_parcours @@ -100,26 +100,38 @@ def formation_delete(formation_id=None, dialog_confirmed=False): return "\n".join(H) -def do_formation_delete(oid): +def do_formation_delete(formation_id): """delete a formation (and all its UE, matieres, modules) - XXX delete all ues, will break if there are validations ! USE WITH CARE ! + Warning: delete all ues, will ask if there are validations ! """ - F = sco_formations.formation_list(args={"formation_id": oid})[0] - if sco_formations.formation_has_locked_sems(oid): - raise ScoLockedFormError() - cnx = ndb.GetDBConnexion() - # delete all UE in this formation - ues = sco_edit_ue.ue_list({"formation_id": oid}) - for ue in ues: - sco_edit_ue.do_ue_delete(ue["ue_id"], force=True) + formation: Formation = Formation.query.get(formation_id) + if formation is None: + return + acronyme = formation.acronyme + if formation.formsemestres.count(): + raise ScoNonEmptyFormationObject( + type_objet="formation", + msg=formation.titre, + dest_url=url_for( + "notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation.id + ), + ) - sco_formations._formationEditor.delete(cnx, oid) + # Suppression des modules + for module in formation.modules: + db.session.delete(module) + db.session.flush() + # Suppression des UEs + for ue in formation.ues: + sco_edit_ue.do_ue_delete(ue, force=True) + + db.session.delete(formation) # news ScolarNews.add( typ=ScolarNews.NEWS_FORM, - obj=oid, - text=f"Suppression de la formation {F['acronyme']}", + obj=formation_id, + text=f"Suppression de la formation {acronyme}", ) diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index e41accd47..06d9c8c14 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -37,7 +37,14 @@ from app import db from app import log from app.but import apc_edit_ue from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN -from app.models import Formation, UniteEns, ModuleImpl, Module +from app.models import ( + Formation, + FormSemestreUEComputationExpr, + FormSemestreUECoef, + Matiere, + UniteEns, +) +from app.models import ApcValidationRCUE, ScolarFormSemestreValidation, ScolarEvent from app.models import ScolarNews from app.models.formations import Matiere import app.scodoc.notesdb as ndb @@ -138,12 +145,11 @@ def do_ue_create(args): return ue_id -def do_ue_delete(ue_id, delete_validations=False, force=False): - "delete UE and attached matieres (but not modules)" - from app.scodoc import sco_cursus_dut - - ue = UniteEns.query.get_or_404(ue_id) - formation = ue.formation +def do_ue_delete(ue: UniteEns, delete_validations=False, force=False): + """delete UE and attached matieres (but not modules). + Si force, pas de confirmation dialog et pas de redirect + """ + formation: Formation = ue.formation semestre_idx = ue.semestre_idx if not ue.can_be_deleted(): raise ScoNonEmptyFormationObject( @@ -157,20 +163,22 @@ def do_ue_delete(ue_id, delete_validations=False, force=False): ), ) - cnx = ndb.GetDBConnexion() - log("do_ue_delete: ue_id=%s, delete_validations=%s" % (ue.id, delete_validations)) - # check - # if ue_is_locked(ue.id): - # raise ScoLockedFormError() + log(f"do_ue_delete: ue_id={ue.id}, delete_validations={delete_validations}") + # Il y a-t-il des etudiants ayant validé cette UE ? # si oui, propose de supprimer les validations - validations = sco_cursus_dut.scolar_formsemestre_validation_list( - cnx, args={"ue_id": ue.id} - ) - if validations and not delete_validations and not force: + validations_ue = ScolarFormSemestreValidation.query.filter_by(ue_id=ue.id).all() + validations_rcue = ApcValidationRCUE.query.filter( + (ApcValidationRCUE.ue1_id == ue.id) | (ApcValidationRCUE.ue2_id == ue.id) + ).all() + if ( + (len(validations_ue) > 0 or len(validations_rcue) > 0) + and not delete_validations + and not force + ): return scu.confirm_dialog( - "

%d étudiants ont validé l'UE %s (%s)

Si vous supprimez cette UE, ces validations vont être supprimées !

" - % (len(validations), ue.acronyme, ue.titre), + f"""

Des étudiants ont une décision de jury sur l'UE {ue.acronyme} ({ue.titre})

+

Si vous supprimez cette UE, ces décisions vont être supprimées !

""", dest_url="", target_variable="delete_validations", cancel_url=url_for( @@ -183,31 +191,34 @@ def do_ue_delete(ue_id, delete_validations=False, force=False): ) if delete_validations: log(f"deleting all validations of UE {ue.id}") - ndb.SimpleQuery( - "DELETE FROM scolar_formsemestre_validation WHERE ue_id=%(ue_id)s", - {"ue_id": ue.id}, - ) + for v in validations_ue: + db.session.delete(v) + for v in validations_rcue: + db.session.delete(v) + # delete old formulas - ndb.SimpleQuery( - "DELETE FROM notes_formsemestre_ue_computation_expr WHERE ue_id=%(ue_id)s", - {"ue_id": ue.id}, - ) - # delete all matiere in this UE - mats = sco_edit_matiere.matiere_list({"ue_id": ue.id}) - for mat in mats: - sco_edit_matiere.do_matiere_delete(mat["matiere_id"]) - # delete uecoef and events - ndb.SimpleQuery( - "DELETE FROM notes_formsemestre_uecoef WHERE ue_id=%(ue_id)s", - {"ue_id": ue.id}, - ) - ndb.SimpleQuery("DELETE FROM scolar_events WHERE ue_id=%(ue_id)s", {"ue_id": ue.id}) - cnx = ndb.GetDBConnexion() - _ueEditor.delete(cnx, ue.id) - # > UE delete + supr. validations associées etudiants (cas compliqué, mais rarement - # utilisé: acceptable de tout invalider): + formulas = FormSemestreUEComputationExpr.query.filter_by(ue_id=ue.id).all() + for formula in formulas: + db.session.delete(formula) + + # delete all matieres in this UE + for mat in Matiere.query.filter_by(ue_id=ue.id): + db.session.delete(mat) + + # delete uecoefs + for uecoef in FormSemestreUECoef.query.filter_by(ue_id=ue.id): + db.session.delete(uecoef) + # delete events + for event in ScolarEvent.query.filter_by(ue_id=ue.id): + db.session.delete(event) + db.session.flush() + + db.session.delete(ue) + db.session.commit() + + # cas compliqué, mais rarement utilisé: acceptable de tout invalider formation.invalidate_module_coefs() - # -> invalide aussi .invalidate_formsemestre() + # -> invalide aussi les formsemestres # news ScolarNews.add( typ=ScolarNews.NEWS_FORM, @@ -601,7 +612,7 @@ def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False): ), ) - return do_ue_delete(ue.id, delete_validations=delete_validations) + return do_ue_delete(ue, delete_validations=delete_validations) def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list diff --git a/app/scodoc/sco_exceptions.py b/app/scodoc/sco_exceptions.py index 17d39a4c2..253750997 100644 --- a/app/scodoc/sco_exceptions.py +++ b/app/scodoc/sco_exceptions.py @@ -116,7 +116,7 @@ class ScoNonEmptyFormationObject(ScoValueError): """On ne peut pas supprimer un module/matiere ou UE si des formsemestre s'y réfèrent""" def __init__(self, type_objet="objet'", msg="", dest_url=None): - msg = f"""

{type_objet} "{msg}" utilisé dans des semestres: suppression impossible.

+ msg = f"""

{type_objet} "{msg}" utilisé(e) dans des semestres: suppression impossible.

Il faut d'abord supprimer le semestre (ou en retirer ce {type_objet}). Mais il est peut-être préférable de laisser ce programme intact et d'en créer une nouvelle version pour la modifier sans affecter les semestres déjà en place. diff --git a/tests/unit/test_formations.py b/tests/unit/test_formations.py index a20ca3a76..679ddc138 100644 --- a/tests/unit/test_formations.py +++ b/tests/unit/test_formations.py @@ -327,7 +327,7 @@ def test_formations(test_client): # --- Suppression d'une formation - sco_edit_formation.do_formation_delete(oid=formation_id2) + sco_edit_formation.do_formation_delete(formation_id=formation_id2) lif3 = notes.formation_list(format="json").get_data(as_text=True) assert isinstance(lif3, str) load_lif3 = json.loads(lif3) From fb9571dde2a8bc9b32c5dfc707e1d303bd85d68a Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 13 Jul 2022 19:23:55 +0200 Subject: [PATCH 03/16] Clonage formations. Close #444 --- app/scodoc/sco_edit_ue.py | 1 + app/scodoc/sco_formations.py | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index 06d9c8c14..6658a6350 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -86,6 +86,7 @@ _ueEditor = ndb.EditableTable( "coefficient", "coef_rcue", "color", + "niveau_competence_id", ), sortkey="numero", input_formators={ diff --git a/app/scodoc/sco_formations.py b/app/scodoc/sco_formations.py index 4526ae634..8128021f7 100644 --- a/app/scodoc/sco_formations.py +++ b/app/scodoc/sco_formations.py @@ -31,7 +31,7 @@ from operator import itemgetter import xml.dom.minidom import flask -from flask import g, url_for, request +from flask import flash, g, url_for, request from flask_login import current_user import app.scodoc.sco_utils as scu @@ -65,6 +65,7 @@ _formationEditor = ndb.EditableTable( "formation_code", "type_parcours", "code_specialite", + "referentiel_competence_id", ), filter_dept=True, sortkey="acronyme", @@ -104,7 +105,7 @@ def formation_export( """Get a formation, with UE, matieres, modules in desired format """ - formation = Formation.query.get_or_404(formation_id) + formation: Formation = Formation.query.get_or_404(formation_id) F = formation.to_dict() selector = {"formation_id": formation_id} if not export_external_ues: @@ -424,17 +425,18 @@ def formation_list_table(formation_id=None, args={}): def formation_create_new_version(formation_id, redirect=True): "duplicate formation, with new version number" + formation = Formation.query.get_or_404(formation_id) resp = formation_export(formation_id, export_ids=True, format="xml") xml_data = resp.get_data(as_text=True) new_id, modules_old2new, ues_old2new = formation_import_xml(xml_data) # news - F = formation_list(args={"formation_id": new_id})[0] ScolarNews.add( typ=ScolarNews.NEWS_FORM, obj=new_id, - text="Nouvelle version de la formation %(acronyme)s" % F, + text=f"Nouvelle version de la formation {formation.acronyme}", ) if redirect: + flash("Nouvelle version !") return flask.redirect( url_for( "notes.ue_table", From 2861c2ee87737a4186ddb531821e745b12a83dfa Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 13 Jul 2022 19:34:46 +0200 Subject: [PATCH 04/16] =?UTF-8?q?Jury=20BUT:=20Autorise=20ADM=20sur=20les?= =?UTF-8?q?=20RCUE=20compens=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/jury_but.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 5b5928483..60cad7986 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -784,6 +784,8 @@ class DecisionsProposeesRCUE(DecisionsProposees): self.code_valide = self.validation.code if rcue.est_compensable(): self.codes.insert(0, sco_codes.CMP) + # les interprétations varient, on autorise aussi ADM: + self.codes.insert(1, sco_codes.ADM) elif rcue.est_validable(): self.codes.insert(0, sco_codes.ADM) else: From fae9ce4e5dc3c291499e7838c0aafbe8dd0c8952 Mon Sep 17 00:00:00 2001 From: Jean-Marie PLACE Date: Thu, 14 Jul 2022 09:50:15 +0200 Subject: [PATCH 05/16] fix null resilution bug --- app/scodoc/sco_logos.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/scodoc/sco_logos.py b/app/scodoc/sco_logos.py index 343c4c462..278cccb9c 100644 --- a/app/scodoc/sco_logos.py +++ b/app/scodoc/sco_logos.py @@ -136,7 +136,9 @@ def _list_dept_logos(dept_id=None, prefix=scu.LOGO_FILE_PREFIX): if os.access(path_dir.joinpath(entry).absolute(), os.R_OK): result = filename_parser.match(entry.name) if result: - logoname = result.group(1)[:-1] # retreive logoname from filename (less final dot) + logoname = result.group(1)[ + :-1 + ] # retreive logoname from filename (less final dot) logos[logoname] = Logo(logoname=logoname, dept_id=dept_id).select() return logos if len(logos.keys()) > 0 else None @@ -235,7 +237,7 @@ class Logo: unit = img.info.get("jfif_unit", 0) # 0 = no unit ; 1 = inch ; 2 = mm if self.density is not None: x_density, y_density = self.density - if unit != 0: + if unit != 0 and x_density != 0 and y_density != 0: unit2mm = [0, 1 / 0.254, 0.1][unit] x_mm = round(x_size * unit2mm / x_density, 2) y_mm = round(y_size * unit2mm / y_density, 2) @@ -244,7 +246,6 @@ class Logo: self.mm = None else: self.mm = None - self.size = (x_size, y_size) self.aspect_ratio = round(float(x_size) / y_size, 2) From 57223fa21dd1dce8885d056ed21ca780b072ea17 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 14 Jul 2022 22:20:24 +0200 Subject: [PATCH 06/16] =?UTF-8?q?Edition=20prog.=20BUT:=20Mise=20=C3=A0=20?= =?UTF-8?q?jour=20menus=20niveaux=20comp.=20Front=20de=20#445.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/apc_edit_ue.py | 2 +- app/static/js/edit_ue.js | 26 +++++++++++++++++++++++--- app/static/js/scodoc.js | 2 +- sco_version.py | 2 +- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/app/but/apc_edit_ue.py b/app/but/apc_edit_ue.py index 3e8be20e8..b0123d0d6 100644 --- a/app/but/apc_edit_ue.py +++ b/app/but/apc_edit_ue.py @@ -68,7 +68,7 @@ def form_ue_choix_niveau(formation: Formation, ue: UniteEns) -> str: options_str = "\n".join(options) return f"""

-
+ Niveau de compétence associé: