From 20d19a190df01402a3a0110653b4b43e0a3a428e Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 25 Oct 2023 23:07:34 +0200 Subject: [PATCH] Corrige cascades sur Identite. --- app/models/but_refcomp.py | 1 + app/models/but_validations.py | 8 +- app/models/etudiants.py | 10 ++- app/scodoc/sco_dept.py | 71 +++++++++++------- app/scodoc/sco_import_etuds.py | 7 +- app/scodoc/sco_synchro_etuds.py | 3 - app/views/scolar.py | 36 ++++----- .../versions/fd805feb7ba8_cascade_identite.py | 73 +++++++++++++++++++ 8 files changed, 152 insertions(+), 57 deletions(-) create mode 100644 migrations/versions/fd805feb7ba8_cascade_identite.py diff --git a/app/models/but_refcomp.py b/app/models/but_refcomp.py index 73ddf9f7..a47bac1b 100644 --- a/app/models/but_refcomp.py +++ b/app/models/but_refcomp.py @@ -97,6 +97,7 @@ class ApcReferentielCompetences(db.Model, XMLModel): validations_annee = db.relationship( "ApcValidationAnnee", backref="referentiel_competence", + cascade="all, delete-orphan", # cascade at ORM level lazy="dynamic", ) diff --git a/app/models/but_validations.py b/app/models/but_validations.py index 539c1239..7efd3069 100644 --- a/app/models/but_validations.py +++ b/app/models/but_validations.py @@ -38,8 +38,12 @@ class ApcValidationRCUE(db.Model): ) "formsemestre origine du RCUE (celui d'où a été émis la validation)" # Les deux UE associées à ce niveau: - ue1_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False) - ue2_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False) + ue1_id = db.Column( + db.Integer, db.ForeignKey("notes_ue.id", ondelete="CASCADE"), nullable=False + ) + ue2_id = db.Column( + db.Integer, db.ForeignKey("notes_ue.id", ondelete="CASCADE"), nullable=False + ) # optionnel, le parcours dans lequel se trouve la compétence: parcours_id = db.Column( db.Integer, db.ForeignKey("apc_parcours.id", ondelete="set null"), nullable=True diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 17cd213d..e20a7b6e 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -30,15 +30,17 @@ class Identite(db.Model, models.ScoDocModel): id = db.Column(db.Integer, primary_key=True) etudid = db.synonym("id") - admission_id = db.Column(db.Integer, db.ForeignKey("admissions.id"), nullable=True) + # ForeignKey ondelete set the cascade at the database level + admission_id = db.Column( + db.Integer, db.ForeignKey("admissions.id", ondelete="CASCADE"), nullable=True + ) admission = db.relationship( "Admission", back_populates="etud", uselist=False, - cascade="all,delete", + cascade="all,delete", # cascade also defined at ORM level single_parent=True, ) - dept_id = db.Column( db.Integer, db.ForeignKey("departement.id"), index=True, nullable=False ) @@ -147,7 +149,7 @@ class Identite(db.Model, models.ScoDocModel): > 0 ): raise ScoValueError( - """clonage étudiant: un étudiant de même code existe déjà + """clonage étudiant: un étudiant de même code existe déjà dans le département destination""" ) d = dict(self.__dict__) diff --git a/app/scodoc/sco_dept.py b/app/scodoc/sco_dept.py index 8e7fc1ef..189a8715 100644 --- a/app/scodoc/sco_dept.py +++ b/app/scodoc/sco_dept.py @@ -28,11 +28,12 @@ """Page accueil département (liste des semestres, etc) """ -from flask import g, request +from flask import g from flask import url_for from flask_login import current_user import app +from app import log from app.models import ScolarNews import app.scodoc.sco_utils as scu from app.scodoc.gen_tables import GenTable @@ -99,7 +100,7 @@ def index_html(showcodes=0, showsemtable=0): """

Aucun utilisateur défini !

Pour définir des utilisateurs passez par la page Utilisateurs.
- Définissez au moins un utilisateur avec le rôle AdminXXX + Définissez au moins un utilisateur avec le rôle AdminXXX (le responsable du département XXX).

""" @@ -132,9 +133,9 @@ def index_html(showcodes=0, showsemtable=0): if not showsemtable: H.append( f"""
-

Voir table des semestres (dont {len(othersems)} + }">Voir table des semestres (dont {len(othersems)} verrouillé{'s' if len(othersems) else ''})

""" ) @@ -196,7 +197,7 @@ def index_html(showcodes=0, showsemtable=0): def _sem_table(sems): """Affiche liste des semestres, utilisée pour semestres en cours""" tmpl = """%(tmpcode)s - %(lockimg)s %(groupicon)s + %(lockimg)s %(groupicon)s %(mois_debut)s - %(mois_fin)s %(titre_num)s (%(responsable_name)s) @@ -269,9 +270,9 @@ def _sem_table_gt(sems, showcodes=False): html_class=html_class, html_sortable=True, html_table_attrs=f""" - data-apo_save_url="{url_for('notes.formsemestre_set_apo_etapes', scodoc_dept=g.scodoc_dept)}" - data-elt_annee_apo_save_url="{url_for('notes.formsemestre_set_elt_annee_apo', scodoc_dept=g.scodoc_dept)}" - data-elt_sem_apo_save_url="{url_for('notes.formsemestre_set_elt_sem_apo', scodoc_dept=g.scodoc_dept)}" + data-apo_save_url="{url_for('notes.formsemestre_set_apo_etapes', scodoc_dept=g.scodoc_dept)}" + data-elt_annee_apo_save_url="{url_for('notes.formsemestre_set_elt_annee_apo', scodoc_dept=g.scodoc_dept)}" + data-elt_sem_apo_save_url="{url_for('notes.formsemestre_set_elt_sem_apo', scodoc_dept=g.scodoc_dept)}" """, html_with_td_classes=True, preferences=sco_preferences.SemPreferences(), @@ -329,12 +330,16 @@ def delete_dept(dept_id: int) -> str: # 1- Create temp tables to store ids reqs = [ "create temp table etudids_temp as select id from identite where dept_id = %(dept_id)s", - "create temp table formsemestres_temp as select id from notes_formsemestre where dept_id = %(dept_id)s", - "create temp table moduleimpls_temp as select id from notes_moduleimpl where formsemestre_id in (select id from formsemestres_temp)", - "create temp table formations_temp as select id from notes_formations where dept_id = %(dept_id)s", + """create temp table formsemestres_temp as select id + from notes_formsemestre where dept_id = %(dept_id)s""", + """create temp table moduleimpls_temp as select id from notes_moduleimpl + where formsemestre_id in (select id from formsemestres_temp)""", + """create temp table formations_temp as + select id from notes_formations where dept_id = %(dept_id)s""", "create temp table tags_temp as select id from notes_tags where dept_id = %(dept_id)s", ] for r in reqs: + log(f"delete_dept: {r}") cursor.execute(r, {"dept_id": dept_id}) # 2- Delete student-related informations @@ -342,7 +347,6 @@ def delete_dept(dept_id: int) -> str: etud_tables = [ "notes_notes", "group_membership", - "admissions", "billet_absence", "adresse", "absences", @@ -357,29 +361,45 @@ def delete_dept(dept_id: int) -> str: "scolar_events", ] for table in etud_tables: + log(f"delete from {table}") cursor.execute( f"delete from {table} where etudid in (select id from etudids_temp)" ) reqs = [ + """delete from apc_validation_annee where referentiel_competence_id + in (select id from apc_referentiel_competences where dept_id = %(dept_id)s)""", "delete from apc_referentiel_competences where dept_id = %(dept_id)s", - "delete from identite where dept_id = %(dept_id)s", "delete from sco_prefs where dept_id = %(dept_id)s", - "delete from notes_semset_formsemestre where formsemestre_id in (select id from formsemestres_temp)", - "delete from notes_evaluation where moduleimpl_id in (select id from moduleimpls_temp)", - "delete from notes_modules_enseignants where moduleimpl_id in (select id from moduleimpls_temp)", - "delete from notes_formsemestre_uecoef where formsemestre_id in (select id from formsemestres_temp)", - "delete from notes_formsemestre_ue_computation_expr where formsemestre_id in (select id from formsemestres_temp)", - "delete from notes_formsemestre_responsables where formsemestre_id in (select id from formsemestres_temp)", - "delete from notes_moduleimpl where formsemestre_id in (select id from formsemestres_temp)", - "delete from notes_modules_tags where tag_id in (select id from tags_temp)", + """delete from notes_semset_formsemestre + where formsemestre_id in (select id from formsemestres_temp)""", + """delete from notes_evaluation + where moduleimpl_id in (select id from moduleimpls_temp)""", + """delete from notes_modules_enseignants + where moduleimpl_id in (select id from moduleimpls_temp)""", + """delete from notes_formsemestre_uecoef + where formsemestre_id in (select id from formsemestres_temp)""", + """delete from notes_formsemestre_ue_computation_expr + where formsemestre_id in (select id from formsemestres_temp)""", + """delete from notes_formsemestre_responsables + where formsemestre_id in (select id from formsemestres_temp)""", + """delete from notes_moduleimpl + where formsemestre_id in (select id from formsemestres_temp)""", + """delete from notes_modules_tags + where tag_id in (select id from tags_temp)""", "delete from notes_tags where dept_id = %(dept_id)s", "delete from notes_modules where formation_id in (select id from formations_temp)", - "delete from notes_matieres where ue_id in (select id from notes_ue where formation_id in (select id from formations_temp))", - "delete from notes_formsemestre_etapes where formsemestre_id in (select id from formsemestres_temp)", - "delete from group_descr where partition_id in (select id from partition where formsemestre_id in (select id from formsemestres_temp))", + """delete from notes_matieres + where ue_id in (select id from notes_ue + where formation_id in (select id from formations_temp))""", + """delete from notes_formsemestre_etapes + where formsemestre_id in (select id from formsemestres_temp)""", + """delete from group_descr where partition_id in + (select id from partition + where formsemestre_id in (select id from formsemestres_temp))""", "delete from partition where formsemestre_id in (select id from formsemestres_temp)", - "delete from notes_formsemestre_custommenu where formsemestre_id in (select id from formsemestres_temp)", + """delete from notes_formsemestre_custommenu + where formsemestre_id in (select id from formsemestres_temp)""", "delete from notes_ue where formation_id in (select id from formations_temp)", "delete from notes_formsemestre where dept_id = %(dept_id)s", "delete from scolar_news where dept_id = %(dept_id)s", @@ -393,6 +413,7 @@ def delete_dept(dept_id: int) -> str: "drop table formsemestres_temp", ] for r in reqs: + log(f"delete_dept: {r}") cursor.execute(r, {"dept_id": dept_id}) except Exception as e: cnx.rollback() diff --git a/app/scodoc/sco_import_etuds.py b/app/scodoc/sco_import_etuds.py index 05e58ecc..4cd7cca5 100644 --- a/app/scodoc/sco_import_etuds.py +++ b/app/scodoc/sco_import_etuds.py @@ -367,7 +367,7 @@ def scolars_import_excel_file( val = input_civilite(val) except ScoValueError as exc: raise ScoValueError( - f"""valeur invalide pour 'civilite' + f"""valeur invalide pour 'civilite' (doit etre 'M', 'F', ou 'MME', 'H', 'X' mais pas '{ val}') ligne {linenum}, colonne {titleslist[i]}""" ) from exc @@ -376,7 +376,7 @@ def scolars_import_excel_file( val = input_civilite_etat_civil(val) except ScoValueError as exc: raise ScoValueError( - f"""valeur invalide pour 'civilite' + f"""valeur invalide pour 'civilite' (doit etre 'M', 'F', vide ou 'MME', 'H', 'X' mais pas '{ val}') ligne {linenum}, colonne {titleslist[i]}""" ) from exc @@ -464,9 +464,6 @@ def scolars_import_excel_file( cursor.execute( "delete from adresse where etudid=%(etudid)s", {"etudid": etudid} ) - cursor.execute( - "delete from admissions where etudid=%(etudid)s", {"etudid": etudid} - ) cursor.execute( "delete from group_membership where etudid=%(etudid)s", {"etudid": etudid}, diff --git a/app/scodoc/sco_synchro_etuds.py b/app/scodoc/sco_synchro_etuds.py index 3f3a691e..e220b843 100644 --- a/app/scodoc/sco_synchro_etuds.py +++ b/app/scodoc/sco_synchro_etuds.py @@ -696,9 +696,6 @@ def do_import_etuds_from_portal(sem, a_importer, etudsapo_ident): cursor.execute( "delete from adresse where etudid=%(etudid)s", {"etudid": etudid} ) - cursor.execute( - "delete from admissions where etudid=%(etudid)s", {"etudid": etudid} - ) cursor.execute( "delete from group_membership where etudid=%(etudid)s", {"etudid": etudid}, diff --git a/app/views/scolar.py b/app/views/scolar.py index 7ea49912..02cd2984 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -1149,7 +1149,7 @@ def _form_dem_of_def( return f""" {header}

{operation_name} de {etud.nomprenom} ({formsemestre.titre_mois()})

- +
Date de la {operation_name.lower()} (J/M/AAAA):  @@ -1162,9 +1162,9 @@ def _form_dem_of_def(
{'

Attention: il y a des décisions de jury déjà prises !

' if validations_descr else ""} {validations_descr} - {('

modifier ces décisions

') if validations_descr else ""}
{html_sco_header.sco_footer()} @@ -1870,14 +1870,14 @@ def etudident_delete(etudid, dialog_confirmed=False): return scu.confirm_dialog( """

Confirmer la suppression de l'étudiant {e[nomprenom]} ?

-

Prenez le temps de vérifier +

Prenez le temps de vérifier que vous devez vraiment supprimer cet étudiant !

Cette opération irréversible - efface toute trace de l'étudiant: inscriptions, notes, absences... + efface toute trace de l'étudiant: inscriptions, notes, absences... dans tous les semestres qu'il a fréquenté.

-

Dans la plupart des cas, vous avez seulement besoin de le

    désinscrire
+

Dans la plupart des cas, vous avez seulement besoin de le

    désinscrire
d'un semestre ? (dans ce cas passez par sa fiche, menu associé au semestre)

Vérifier la fiche de {e[nomprenom]} @@ -1896,6 +1896,7 @@ def etudident_delete(etudid, dialog_confirmed=False): ) log("etudident_delete: etudid=%(etudid)s nomprenom=%(nomprenom)s" % etud) # delete in all tables ! + # c'est l'ancienne façon de gérer les cascades dans notre pseudo-ORM :) tables = [ "notes_appreciations", "scolar_autorisation_inscription", @@ -1908,7 +1909,6 @@ def etudident_delete(etudid, dialog_confirmed=False): "group_membership", "etud_annotations", "scolog", - "admissions", "adresse", "absences", "absences_notifications", @@ -2288,24 +2288,24 @@ def form_students_import_infos_admissions(formsemestre_id=None):

A utiliser pour renseigner les informations sur l'origine des étudiants (lycées, bac, etc). Ces informations sont facultatives mais souvent utiles pour mieux connaitre les étudiants et aussi pour effectuer des statistiques (résultats suivant le type de bac...). Les données sont affichées sur les fiches individuelles des étudiants.

- Importer ici la feuille excel utilisée pour envoyer le classement Parcoursup. - Seuls les étudiants actuellement inscrits dans ce semestre ScoDoc seront affectés, - les autres lignes de la feuille seront ignorées. - Et seules les colonnes intéressant ScoDoc + Importer ici la feuille excel utilisée pour envoyer le classement Parcoursup. + Seuls les étudiants actuellement inscrits dans ce semestre ScoDoc seront affectés, + les autres lignes de la feuille seront ignorées. + Et seules les colonnes intéressant ScoDoc seront importées: il est inutile d'éliminer les autres.
- Seules les données "admission" seront modifiées + Seules les données "admission" seront modifiées (et pas l'identité de l'étudiant).
Les colonnes "nom" et "prenom" sont requises, ou bien une colonne "etudid".

- Avant d'importer vos données, il est recommandé d'enregistrer + Avant d'importer vos données, il est recommandé d'enregistrer les informations actuelles: exporter les données actuelles de ScoDoc + }">exporter les données actuelles de ScoDoc (ce fichier peut être ré-importé après d'éventuelles modifications)

""", @@ -2334,8 +2334,8 @@ def form_students_import_infos_admissions(formsemestre_id=None): ) help_text = ( - """

Les colonnes importables par cette fonction sont indiquées - dans la table ci-dessous. + """

Les colonnes importables par cette fonction sont indiquées + dans la table ci-dessous. Seule la première feuille du classeur sera utilisée.

""" diff --git a/migrations/versions/fd805feb7ba8_cascade_identite.py b/migrations/versions/fd805feb7ba8_cascade_identite.py new file mode 100644 index 00000000..90d3f25b --- /dev/null +++ b/migrations/versions/fd805feb7ba8_cascade_identite.py @@ -0,0 +1,73 @@ +"""Ajoute quelques cascades oubliées + +Revision ID: fd805feb7ba8 +Revises: 497ba81343f7 +Create Date: 2023-10-25 18:27:13.222354 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "fd805feb7ba8" +down_revision = "497ba81343f7" +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table("admissions", schema=None) as batch_op: + batch_op.drop_column("etudid") + + with op.batch_alter_table("identite", schema=None) as batch_op: + batch_op.drop_constraint("admissions_etudid_fkey", type_="foreignkey") + batch_op.create_foreign_key( + "admissions_etudid_fkey", + "admissions", + ["admission_id"], + ["id"], + ondelete="CASCADE", + ) + + with op.batch_alter_table("apc_validation_rcue", schema=None) as batch_op: + batch_op.drop_constraint("apc_validation_rcue_ue1_id_fkey", type_="foreignkey") + batch_op.drop_constraint("apc_validation_rcue_ue2_id_fkey", type_="foreignkey") + batch_op.create_foreign_key( + "apc_validation_rcue_ue1_id_fkey", + "notes_ue", + ["ue1_id"], + ["id"], + ondelete="CASCADE", + ) + batch_op.create_foreign_key( + "apc_validation_rcue_ue2_id_fkey", + "notes_ue", + ["ue2_id"], + ["id"], + ondelete="CASCADE", + ) + + +def downgrade(): + with op.batch_alter_table("identite", schema=None) as batch_op: + batch_op.drop_constraint("identite_dept_id_fkey", type_="foreignkey") + batch_op.drop_constraint("admissions_etudid_fkey", type_="foreignkey") + batch_op.create_foreign_key( + "admissions_etudid_fkey", "admissions", ["admission_id"], ["id"] + ) + + with op.batch_alter_table("admissions", schema=None) as batch_op: + batch_op.add_column( + sa.Column("etudid", sa.INTEGER(), autoincrement=False, nullable=True) + ) + + with op.batch_alter_table("apc_validation_rcue", schema=None) as batch_op: + batch_op.drop_constraint("apc_validation_rcue_ue1_id_fkey", type_="foreignkey") + batch_op.drop_constraint("apc_validation_rcue_ue2_id_fkey", type_="foreignkey") + batch_op.create_foreign_key( + "apc_validation_rcue_ue2_id_fkey", "notes_ue", ["ue2_id"], ["id"] + ) + batch_op.create_foreign_key( + "apc_validation_rcue_ue1_id_fkey", "notes_ue", ["ue1_id"], ["id"] + )