Merge branch 'upgrading_pip' of https://scodoc.org/git/viennet/ScoDoc into table

This commit is contained in:
Emmanuel Viennet 2023-04-03 17:55:41 +02:00
commit 2225fa8da0
19 changed files with 69 additions and 115 deletions

View File

@ -14,11 +14,11 @@ from logging.handlers import SMTPHandler, WatchedFileHandler
from threading import Thread from threading import Thread
import warnings import warnings
import flask
from flask import current_app, g, request from flask import current_app, g, request
from flask import Flask from flask import Flask
from flask import abort, flash, has_request_context, jsonify from flask import abort, flash, has_request_context, jsonify
from flask import render_template from flask import render_template
from flask.json import JSONEncoder
from flask.logging import default_handler from flask.logging import default_handler
from flask_bootstrap import Bootstrap from flask_bootstrap import Bootstrap
@ -30,7 +30,7 @@ from flask_moment import Moment
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from jinja2 import select_autoescape from jinja2 import select_autoescape
import sqlalchemy import sqlalchemy as sa
from flask_cas import CAS from flask_cas import CAS
@ -141,7 +141,7 @@ def handle_invalid_usage(error):
# JSON ENCODING # JSON ENCODING
class ScoDocJSONEncoder(JSONEncoder): class ScoDocJSONEncoder(flask.json.provider.DefaultJSONProvider):
def default(self, o): def default(self, o):
if isinstance(o, (datetime.datetime, datetime.date)): if isinstance(o, (datetime.datetime, datetime.date)):
return o.isoformat() return o.isoformat()
@ -249,9 +249,13 @@ def create_app(config_class=DevConfig):
CAS(app, url_prefix="/cas", configuration_function=cas.set_cas_configuration) CAS(app, url_prefix="/cas", configuration_function=cas.set_cas_configuration)
app.wsgi_app = ReverseProxied(app.wsgi_app) app.wsgi_app = ReverseProxied(app.wsgi_app)
app.json_encoder = ScoDocJSONEncoder app.json_provider_class = ScoDocJSONEncoder
app.config.from_object(config_class) app.config.from_object(config_class)
# Pour conserver l'ordre des objets dans les JSON:
# e.g. l'ordre des UE dans les bulletins
app.json.sort_keys = False
# Evite de logguer toutes les requetes dans notre log # Evite de logguer toutes les requetes dans notre log
logging.getLogger("werkzeug").disabled = True logging.getLogger("werkzeug").disabled = True
app.logger.setLevel(app.config["LOG_LEVEL"]) app.logger.setLevel(app.config["LOG_LEVEL"])
@ -408,7 +412,7 @@ def create_app(config_class=DevConfig):
with app.app_context(): with app.app_context():
try: try:
set_cas_configuration(app) set_cas_configuration(app)
except sqlalchemy.exc.ProgrammingError: except sa.exc.ProgrammingError:
# Si la base n'a pas été upgradée (arrive durrant l'install) # Si la base n'a pas été upgradée (arrive durrant l'install)
# il se peut que la table scodoc_site_config n'existe pas encore. # il se peut que la table scodoc_site_config n'existe pas encore.
pass pass
@ -420,7 +424,7 @@ def set_sco_dept(scodoc_dept: str, open_cnx=True):
# Check that dept exists # Check that dept exists
try: try:
dept = Departement.query.filter_by(acronym=scodoc_dept).first() dept = Departement.query.filter_by(acronym=scodoc_dept).first()
except sqlalchemy.exc.OperationalError: except sa.exc.OperationalError:
abort(503) abort(503)
if not dept: if not dept:
raise ScoValueError(f"Invalid dept: {scodoc_dept}") raise ScoValueError(f"Invalid dept: {scodoc_dept}")
@ -498,14 +502,15 @@ def truncate_database():
""" """
# use a stored SQL function, see createtables.sql # use a stored SQL function, see createtables.sql
try: try:
db.session.execute("SELECT truncate_tables('scodoc');") db.session.execute(sa.text("SELECT truncate_tables('scodoc');"))
db.session.commit() db.session.commit()
except: except:
db.session.rollback() db.session.rollback()
raise raise
# Remet les compteurs (séquences sql) à zéro # Remet les compteurs (séquences sql) à zéro
db.session.execute( db.session.execute(
""" sa.text(
"""
CREATE OR REPLACE FUNCTION reset_sequences(username IN VARCHAR) RETURNS void AS $$ CREATE OR REPLACE FUNCTION reset_sequences(username IN VARCHAR) RETURNS void AS $$
DECLARE DECLARE
statements CURSOR FOR statements CURSOR FOR
@ -521,6 +526,7 @@ def truncate_database():
SELECT reset_sequences('scodoc'); SELECT reset_sequences('scodoc');
""" """
)
) )
db.session.commit() db.session.commit()

View File

@ -21,8 +21,6 @@ convention = {
metadata_obj = sqlalchemy.MetaData(naming_convention=convention) metadata_obj = sqlalchemy.MetaData(naming_convention=convention)
from app.models.raw_sql_init import create_database_functions
from app.models.absences import Absence, AbsenceNotification, BilletAbsence from app.models.absences import Absence, AbsenceNotification, BilletAbsence
from app.models.departements import Departement from app.models.departements import Departement
from app.models.etudiants import ( from app.models.etudiants import (

View File

@ -8,7 +8,7 @@
from datetime import datetime from datetime import datetime
from operator import attrgetter from operator import attrgetter
import flask_sqlalchemy from flask_sqlalchemy.query import Query
from sqlalchemy.orm import class_mapper from sqlalchemy.orm import class_mapper
import sqlalchemy import sqlalchemy
@ -306,6 +306,7 @@ class ApcSituationPro(db.Model, XMLModel):
nullable=False, nullable=False,
) )
libelle = db.Column(db.Text(), nullable=False) libelle = db.Column(db.Text(), nullable=False)
# aucun attribut (le text devient le libellé) # aucun attribut (le text devient le libellé)
def to_dict(self): def to_dict(self):
return {"libelle": self.libelle} return {"libelle": self.libelle}
@ -450,7 +451,7 @@ class ApcAppCritique(db.Model, XMLModel):
ref_comp: ApcReferentielCompetences, ref_comp: ApcReferentielCompetences,
annee: str, annee: str,
competence: ApcCompetence = None, competence: ApcCompetence = None,
) -> flask_sqlalchemy.BaseQuery: ) -> Query:
"Liste les AC de tous les parcours de ref_comp pour l'année indiquée" "Liste les AC de tous les parcours de ref_comp pour l'année indiquée"
assert annee in {"BUT1", "BUT2", "BUT3"} assert annee in {"BUT1", "BUT2", "BUT3"}
query = cls.query.filter( query = cls.query.filter(
@ -548,7 +549,7 @@ class ApcParcours(db.Model, XMLModel):
d["annees"] = {x.ordre: x.to_dict() for x in self.annees} d["annees"] = {x.ordre: x.to_dict() for x in self.annees}
return d return d
def query_competences(self) -> flask_sqlalchemy.BaseQuery: def query_competences(self) -> Query:
"Les compétences associées à ce parcours" "Les compétences associées à ce parcours"
return ( return (
ApcCompetence.query.join(ApcParcoursNiveauCompetence, ApcAnneeParcours) ApcCompetence.query.join(ApcParcoursNiveauCompetence, ApcAnneeParcours)

View File

@ -4,7 +4,7 @@
""" """
from typing import Union from typing import Union
import flask_sqlalchemy from flask_sqlalchemy.query import Query
from app import db from app import db
from app.models import CODE_STR_LEN from app.models import CODE_STR_LEN
@ -177,7 +177,7 @@ class RegroupementCoherentUE:
def query_validations( def query_validations(
self, self,
) -> flask_sqlalchemy.BaseQuery: # list[ApcValidationRCUE] ) -> Query: # list[ApcValidationRCUE]
"""Les validations de jury enregistrées pour ce RCUE""" """Les validations de jury enregistrées pour ce RCUE"""
niveau = self.ue_2.niveau_competence niveau = self.ue_2.niveau_competence

View File

@ -1,6 +1,6 @@
"""ScoDoc 9 models : Formations """ScoDoc 9 models : Formations
""" """
import flask_sqlalchemy from flask_sqlalchemy.query import Query
import app import app
from app import db from app import db
@ -214,7 +214,7 @@ class Formation(db.Model):
def query_ues_parcour( def query_ues_parcour(
self, parcour: ApcParcours, with_sport: bool = False self, parcour: ApcParcours, with_sport: bool = False
) -> flask_sqlalchemy.BaseQuery: ) -> Query:
"""Les UEs (non bonus) d'un parcours de la formation """Les UEs (non bonus) d'un parcours de la formation
(déclarée comme faisant partie du parcours ou du tronc commun, sans aucun parcours) (déclarée comme faisant partie du parcours ou du tronc commun, sans aucun parcours)
Si parcour est None, les UE sans parcours. Si parcour est None, les UE sans parcours.
@ -243,9 +243,7 @@ class Formation(db.Model):
# ApcAnneeParcours.parcours_id == parcour.id, # ApcAnneeParcours.parcours_id == parcour.id,
# ) # )
def query_competences_parcour( def query_competences_parcour(self, parcour: ApcParcours) -> Query:
self, parcour: ApcParcours
) -> flask_sqlalchemy.BaseQuery:
"""Les ApcCompetences d'un parcours de la formation. """Les ApcCompetences d'un parcours de la formation.
None si pas de référentiel de compétences. None si pas de référentiel de compétences.
""" """

View File

@ -15,7 +15,8 @@ from functools import cached_property
from operator import attrgetter from operator import attrgetter
from flask_login import current_user from flask_login import current_user
import flask_sqlalchemy from flask_sqlalchemy.query import Query
from flask import flash, g from flask import flash, g
from sqlalchemy import and_, or_ from sqlalchemy import and_, or_
from sqlalchemy.sql import text from sqlalchemy.sql import text
@ -951,7 +952,7 @@ class FormationModalite(db.Model):
"""Create default modalities""" """Create default modalities"""
numero = 0 numero = 0
try: try:
for (code, titre) in ( for code, titre in (
(FormationModalite.DEFAULT_MODALITE, "Formation Initiale"), (FormationModalite.DEFAULT_MODALITE, "Formation Initiale"),
("FAP", "Apprentissage"), ("FAP", "Apprentissage"),
("FC", "Formation Continue"), ("FC", "Formation Continue"),

View File

@ -2,7 +2,7 @@
"""ScoDoc models: moduleimpls """ScoDoc models: moduleimpls
""" """
import pandas as pd import pandas as pd
import flask_sqlalchemy from flask_sqlalchemy.query import Query
from app import db from app import db
from app.auth.models import User from app.auth.models import User
@ -163,7 +163,7 @@ class ModuleImplInscription(db.Model):
@classmethod @classmethod
def etud_modimpls_in_ue( def etud_modimpls_in_ue(
cls, formsemestre_id: int, etudid: int, ue_id: int cls, formsemestre_id: int, etudid: int, ue_id: int
) -> flask_sqlalchemy.BaseQuery: ) -> Query:
"""moduleimpls de l'UE auxquels l'étudiant est inscrit. """moduleimpls de l'UE auxquels l'étudiant est inscrit.
(Attention: inutile en APC, il faut considérer les coefficients) (Attention: inutile en APC, il faut considérer les coefficients)
""" """

View File

@ -3,6 +3,7 @@
"""Notes, décisions de jury, évènements scolaires """Notes, décisions de jury, évènements scolaires
""" """
import sqlalchemy as sa
from app import db from app import db
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -86,7 +87,8 @@ def etud_has_notes_attente(etudid, formsemestre_id):
(ne compte que les notes en attente dans des évaluations avec coef. non nul). (ne compte que les notes en attente dans des évaluations avec coef. non nul).
""" """
cursor = db.session.execute( cursor = db.session.execute(
"""SELECT COUNT(*) sa.text(
"""SELECT COUNT(*)
FROM notes_notes n, notes_evaluation e, notes_moduleimpl m, FROM notes_notes n, notes_evaluation e, notes_moduleimpl m,
notes_moduleimpl_inscription i notes_moduleimpl_inscription i
WHERE n.etudid = :etudid WHERE n.etudid = :etudid
@ -97,7 +99,8 @@ def etud_has_notes_attente(etudid, formsemestre_id):
and e.coefficient != 0 and e.coefficient != 0
and m.id = i.moduleimpl_id and m.id = i.moduleimpl_id
and i.etudid = :etudid and i.etudid = :etudid
""", """
),
{ {
"formsemestre_id": formsemestre_id, "formsemestre_id": formsemestre_id,
"etudid": etudid, "etudid": etudid,

View File

@ -1,57 +0,0 @@
# -*- coding: UTF-8 -*
"""
Create some Postgresql sequences and functions used by ScoDoc
using raw SQL
"""
from app import db
def create_database_functions(): # XXX obsolete
"""Create specific SQL functions and sequences
XXX Obsolete: cette fonction est dans la première migration 9.0.3
Flask-Migrate fait maintenant (dans les versions >= 9.0.4) ce travail.
"""
# Important: toujours utiliser IF NOT EXISTS
# car cette fonction peut être appelée plusieurs fois sur la même db
db.session.execute(
"""
CREATE SEQUENCE IF NOT EXISTS notes_idgen_fcod;
CREATE OR REPLACE FUNCTION notes_newid_fcod() RETURNS TEXT
AS $$ SELECT 'FCOD' || to_char(nextval('notes_idgen_fcod'), 'FM999999999'); $$
LANGUAGE SQL;
CREATE OR REPLACE FUNCTION notes_newid_ucod() RETURNS TEXT
AS $$ SELECT 'UCOD' || to_char(nextval('notes_idgen_fcod'), 'FM999999999'); $$
LANGUAGE SQL;
CREATE OR REPLACE FUNCTION truncate_tables(username IN VARCHAR) RETURNS void AS $$
DECLARE
statements CURSOR FOR
SELECT tablename FROM pg_tables
WHERE tableowner = username AND schemaname = 'public'
AND tablename <> 'notes_semestres'
AND tablename <> 'notes_form_modalites'
AND tablename <> 'alembic_version';
BEGIN
FOR stmt IN statements LOOP
EXECUTE 'TRUNCATE TABLE ' || quote_ident(stmt.tablename) || ' CASCADE;';
END LOOP;
END;
$$ LANGUAGE plpgsql;
-- Fonction pour anonymisation:
-- inspirée par https://www.simononsoftware.com/random-string-in-postgresql/
CREATE OR REPLACE FUNCTION random_text_md5( integer ) returns text
LANGUAGE SQL
AS $$
select upper( substring( (SELECT string_agg(md5(random()::TEXT), '')
FROM generate_series(
1,
CEIL($1 / 32.)::integer)
), 1, $1) );
$$;
"""
)
db.session.commit()

View File

@ -29,16 +29,14 @@
""" """
from flask import g, url_for from flask import g, url_for
import flask_sqlalchemy from flask_sqlalchemy.query import Query
from app.models.absences import BilletAbsence from app.models.absences import BilletAbsence
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
def query_billets_etud( def query_billets_etud(etudid: int = None, etat: bool = None) -> Query:
etudid: int = None, etat: bool = None
) -> flask_sqlalchemy.BaseQuery:
"""Billets d'absences pour un étudiant, ou tous si etudid is None. """Billets d'absences pour un étudiant, ou tous si etudid is None.
Si etat, filtre par état. Si etat, filtre par état.
Si dans un département et que la gestion des billets n'a pas été activée Si dans un département et que la gestion des billets n'a pas été activée

View File

@ -30,6 +30,7 @@
""" """
import re import re
import sqlalchemy as sa
import flask import flask
from flask import flash, render_template, url_for from flask import flash, render_template, url_for
from flask import g, request from flask import g, request
@ -127,7 +128,7 @@ def do_ue_create(args):
): ):
# évite les conflits de code # évite les conflits de code
while True: while True:
cursor = db.session.execute("select notes_newid_ucod();") cursor = db.session.execute(sa.text("select notes_newid_ucod();"))
code = cursor.fetchone()[0] code = cursor.fetchone()[0]
if UniteEns.query.filter_by(ue_code=code).count() == 0: if UniteEns.query.filter_by(ue_code=code).count() == 0:
break break

View File

@ -690,7 +690,7 @@ def sendPDFFile(data, filename): # DEPRECATED utiliser send_file
return send_file(data, filename=filename, mime=PDF_MIMETYPE, attached=True) return send_file(data, filename=filename, mime=PDF_MIMETYPE, attached=True)
class ScoDocJSONEncoder(json.JSONEncoder): class ScoDocJSONEncoder(flask.json.provider.DefaultJSONProvider):
def default(self, o): # pylint: disable=E0202 def default(self, o): # pylint: disable=E0202
if isinstance(o, (datetime.date, datetime.datetime)): if isinstance(o, (datetime.date, datetime.datetime)):
return o.isoformat() return o.isoformat()

View File

@ -38,9 +38,6 @@ class Config:
SCODOC_ERR_FILE = os.path.join(SCODOC_VAR_DIR, "log", "scodoc_exc.log") SCODOC_ERR_FILE = os.path.join(SCODOC_VAR_DIR, "log", "scodoc_exc.log")
# #
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # Flask uploads (16Mo, en ligne avec nginx) MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # Flask uploads (16Mo, en ligne avec nginx)
# Pour conserver l'ordre des objets dans les JSON:
# e.g. l'ordre des UE dans les bulletins
JSON_SORT_KEYS = False
class ProdConfig(Config): class ProdConfig(Config):

View File

@ -5,14 +5,6 @@ flask_cas.__init__
import flask import flask
from flask import current_app from flask import current_app
# Find the stack on which we want to store the database connection.
# Starting with Flask 0.9, the _app_ctx_stack is the correct one,
# before that we need to use the _request_ctx_stack.
try:
from flask import _app_ctx_stack as stack
except ImportError:
from flask import _request_ctx_stack as stack
from . import routing from . import routing
from functools import wraps from functools import wraps
@ -67,7 +59,7 @@ class CAS(object):
app.teardown_request(self.teardown) app.teardown_request(self.teardown)
def teardown(self, exception): def teardown(self, exception):
ctx = stack.top pass # ctx = stack.top
@property @property
def app(self): def app(self):

View File

@ -43,11 +43,14 @@ def upgrade():
bind = op.get_bind() bind = op.get_bind()
session = Session(bind=bind) session = Session(bind=bind)
dispenses = session.execute( dispenses = session.execute(
"""SELECT id, ue_id, etudid FROM "dispenseUE" WHERE formsemestre_id IS NULL;""" sa.text(
"""SELECT id, ue_id, etudid FROM "dispenseUE" WHERE formsemestre_id IS NULL;"""
)
).all() ).all()
for dispense_id, ue_id, etudid in dispenses: for dispense_id, ue_id, etudid in dispenses:
formsemestre_ids = session.execute( formsemestre_ids = session.execute(
""" sa.text(
"""
SELECT notes_formsemestre.id SELECT notes_formsemestre.id
FROM notes_formsemestre, notes_formations, notes_ue, notes_formsemestre_inscription FROM notes_formsemestre, notes_formations, notes_ue, notes_formsemestre_inscription
WHERE notes_formsemestre.formation_id = notes_formations.id WHERE notes_formsemestre.formation_id = notes_formations.id
@ -58,14 +61,17 @@ def upgrade():
and notes_formsemestre_inscription.etudid = :etudid and notes_formsemestre_inscription.etudid = :etudid
ORDER BY notes_formsemestre.date_debut DESC ORDER BY notes_formsemestre.date_debut DESC
LIMIT 1; LIMIT 1;
""", """
),
{"ue_id": ue_id, "etudid": etudid}, {"ue_id": ue_id, "etudid": etudid},
).all() ).all()
if formsemestre_ids: if formsemestre_ids:
formsemestre_id = formsemestre_ids[0][0] formsemestre_id = formsemestre_ids[0][0]
session.execute( session.execute(
""" sa.text(
UPDATE "dispenseUE" SET formsemestre_id=:formsemestre_id WHERE id=:dispense_id""", """
UPDATE "dispenseUE" SET formsemestre_id=:formsemestre_id WHERE id=:dispense_id"""
),
{"formsemestre_id": formsemestre_id, "dispense_id": dispense_id}, {"formsemestre_id": formsemestre_id, "dispense_id": dispense_id},
) )

View File

@ -23,7 +23,9 @@ def upgrade():
# #
bind = op.get_bind() bind = op.get_bind()
session = Session(bind=bind) session = Session(bind=bind)
session.execute("""UPDATE notes_semset SET sem_id=0 WHERE sem_id IS NULL;""") session.execute(
sa.text("""UPDATE notes_semset SET sem_id=0 WHERE sem_id IS NULL;""")
)
op.alter_column( op.alter_column(
"notes_semset", "sem_id", existing_type=sa.INTEGER(), nullable=False "notes_semset", "sem_id", existing_type=sa.INTEGER(), nullable=False
) )

View File

@ -29,21 +29,25 @@ def upgrade():
session = Session(bind=bind) session = Session(bind=bind)
# Corrige NIP # Corrige NIP
dups = session.execute( dups = session.execute(
"""SELECT dept_id, code_nip sa.text(
"""SELECT dept_id, code_nip
FROM identite FROM identite
WHERE code_nip IS NOT NULL WHERE code_nip IS NOT NULL
GROUP BY dept_id, code_nip GROUP BY dept_id, code_nip
HAVING COUNT(*) > 1;""" HAVING COUNT(*) > 1;"""
)
).all() ).all()
for dept_id, code_nip in dups: for dept_id, code_nip in dups:
etuds_dups = session.execute( etuds_dups = session.execute(
"""SELECT id, nom, prenom FROM identite sa.text(
WHERE dept_id=:dept_id AND code_nip=:code_nip""", """SELECT id, nom, prenom FROM identite
WHERE dept_id=:dept_id AND code_nip=:code_nip"""
),
{"dept_id": dept_id, "code_nip": code_nip}, {"dept_id": dept_id, "code_nip": code_nip},
).all() ).all()
for i, (etudid, nom, prenom) in enumerate(etuds_dups[1:], start=1): for i, (etudid, nom, prenom) in enumerate(etuds_dups[1:], start=1):
session.execute( session.execute(
"""UPDATE identite SET code_nip=:code_nip WHERE id=:etudid""", sa.text("""UPDATE identite SET code_nip=:code_nip WHERE id=:etudid"""),
{ {
"code_nip": f"{code_nip}-{i}", "code_nip": f"{code_nip}-{i}",
"etudid": etudid, "etudid": etudid,
@ -55,11 +59,13 @@ def upgrade():
session.commit() session.commit()
# Corrige INE # Corrige INE
dups = session.execute( dups = session.execute(
"""SELECT dept_id, code_ine sa.text(
"""SELECT dept_id, code_ine
FROM identite FROM identite
WHERE code_ine IS NOT NULL WHERE code_ine IS NOT NULL
GROUP BY dept_id, code_ine GROUP BY dept_id, code_ine
HAVING COUNT(*) > 1;""" HAVING COUNT(*) > 1;"""
)
).all() ).all()
for dept_id, code_ine in dups: for dept_id, code_ine in dups:
etuds_dups = session.execute( etuds_dups = session.execute(
@ -69,7 +75,7 @@ def upgrade():
).all() ).all()
for i, (etudid, nom, prenom) in enumerate(etuds_dups[1:], start=1): for i, (etudid, nom, prenom) in enumerate(etuds_dups[1:], start=1):
session.execute( session.execute(
"""UPDATE identite SET code_ine=:code_ine WHERE id=:etudid""", sa.text("""UPDATE identite SET code_ine=:code_ine WHERE id=:etudid"""),
{ {
"code_ine": f"{code_ine}-{i}", "code_ine": f"{code_ine}-{i}",
"etudid": etudid, "etudid": etudid,

View File

@ -23,7 +23,7 @@ def upgrade():
bind = op.get_bind() bind = op.get_bind()
session = Session(bind=bind) session = Session(bind=bind)
session.execute( session.execute(
"""UPDATE notes_modules SET module_type=0 WHERE module_type IS NULL;""" sa.text("""UPDATE notes_modules SET module_type=0 WHERE module_type IS NULL;""")
) )
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.alter_column( op.alter_column(

View File

@ -24,13 +24,15 @@ def upgrade():
bind = op.get_bind() bind = op.get_bind()
session = Session(bind=bind) session = Session(bind=bind)
session.execute( session.execute(
""" sa.text(
"""
DELETE FROM notes_moduleimpl_inscription i1 DELETE FROM notes_moduleimpl_inscription i1
USING notes_moduleimpl_inscription i2 USING notes_moduleimpl_inscription i2
WHERE i1.id < i2.id WHERE i1.id < i2.id
AND i1.moduleimpl_id = i2.moduleimpl_id AND i1.moduleimpl_id = i2.moduleimpl_id
AND i1.etudid = i2.etudid; AND i1.etudid = i2.etudid;
""" """
)
) )
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###