diff --git a/app/models/formations.py b/app/models/formations.py index d2970fce98..224b8475a4 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 450482bc9e..01579dc147 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 65307d7e96..7a20a1a6cb 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 d7ea5e9764..752bdef68a 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 e41accd474..06d9c8c142 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 17d39a4c26..2537509972 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 a20ca3a765..679ddc138e 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)