From 6090672089f8317d9179f9cf131ec4d133d45104 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 27 Aug 2021 17:03:47 +0200 Subject: [PATCH] Modified install/upgrade scripts to use Flask-Migrate (Alembic) --- README.md | 19 + app/__init__.py | 22 +- app/models/formations.py | 3 +- app/models/raw_sql_init.py | 8 +- migrations/README | 1 + migrations/alembic.ini | 50 + migrations/env.py | 91 ++ migrations/script.py.mako | 24 + ...7947e5_scodoc_9_0_4_code_module_en_text.py | 34 + .../versions/993ce4a01d57_scodoc_9_0_3.py | 1199 +++++++++++++++++ scodoc.py | 2 +- tests/conftest.py | 2 +- tools/configure-scodoc9.sh | 2 +- tools/debian/postinst | 19 +- 14 files changed, 1457 insertions(+), 19 deletions(-) create mode 100755 migrations/README create mode 100644 migrations/alembic.ini create mode 100755 migrations/env.py create mode 100755 migrations/script.py.mako create mode 100644 migrations/versions/6b071b7947e5_scodoc_9_0_4_code_module_en_text.py create mode 100644 migrations/versions/993ce4a01d57_scodoc_9_0_3.py diff --git a/README.md b/README.md index 45743c41..2a77b25e 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,25 @@ un utilisateur: **Attention:** les tests unitaires **effacent** complètement le contenu de la base de données (tous les départements, et les utilisateurs) avant de commencer ! +#### Modification du schéma de la base + +On utilise SQLAlchemy avec Alembic et Flask-Migrate. + + flask db migrate -m "ScoDoc 9.0.4" # ajuster le message ! + flask db upgrade + +Ne pas oublier de commiter les migrations (`git add migrations` ...). + +Mémo pour développeurs: séquence re-création d'une base: + + dropdb SCODOC_DEV + tools/create_database.sh SCODOC_DEV # créé base SQL + flask db upgrade # créé les tables à partir des migrations + flask sco-db-init # ajoute au besoin les constantes (todo: mettre en migration 0) + + # puis imports: + flask import-scodoc7-users + flask import-scodoc7-dept STID SCOSTID # Paquet debian 11 diff --git a/app/__init__.py b/app/__init__.py index 001bbc24..fa9bb0c0 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -25,7 +25,7 @@ from config import DevConfig import sco_version db = SQLAlchemy() -migrate = Migrate() +migrate = Migrate(compare_type=True) login = LoginManager() login.login_view = "auth.login" login.login_message = "Please log in to access this page." @@ -140,7 +140,9 @@ def set_sco_dept(scodoc_dept: str): def user_db_init(): - """Initialize the users database.""" + """Initialize the users database. + Check that basic roles and admin user exist. + """ from app.auth.models import User, Role current_app.logger.info("Init User's db") @@ -167,8 +169,8 @@ def user_db_init(): ) -def sco_db_init(): - """Initialize Sco database""" +def sco_db_insert_constants(): + """Initialize Sco database: insert some constants (modalités, ...).""" from app import models current_app.logger.info("Init Sco db") @@ -176,25 +178,25 @@ def sco_db_init(): models.NotesFormModalite.insert_modalites() -def initialize_scodoc_database(erase=False): - """Initialize the database. +def initialize_scodoc_database(erase=False, create_all=False): + """Initialize the database for unit tests Starts from an existing database and create all necessary SQL tables and functions. If erase is True, _erase_ all database content. """ from app import models - # - our specific functions and sequences, not generated by SQLAlchemy - models.create_database_functions() # - ERASE (the truncation sql function has been defined above) if erase: truncate_database() # - Create all tables - db.create_all() + if create_all: + # managed by migrations, except for TESTS + db.create_all() # - Insert initial roles and create super-admin user user_db_init() # - Insert some constant values (modalites, ...) - sco_db_init() + sco_db_insert_constants() # - Flush cache clear_scodoc_cache() diff --git a/app/models/formations.py b/app/models/formations.py index 81828b82..756879d5 100644 --- a/app/models/formations.py +++ b/app/models/formations.py @@ -87,7 +87,8 @@ class NotesModule(db.Model): module_id = db.synonym("id") titre = db.Column(db.Text()) abbrev = db.Column(db.Text()) # nom court - code = db.Column(db.String(SHORT_STR_LEN), nullable=False) + # certains départements ont des codes infiniment longs: donc Text ! + code = db.Column(db.Text(), nullable=False) heures_cours = db.Column(db.Float) heures_td = db.Column(db.Float) heures_tp = db.Column(db.Float) diff --git a/app/models/raw_sql_init.py b/app/models/raw_sql_init.py index e10f8d14..6cfde9d9 100644 --- a/app/models/raw_sql_init.py +++ b/app/models/raw_sql_init.py @@ -8,8 +8,12 @@ using raw SQL from app import db -def create_database_functions(): - """Create specific SQL functions and sequences""" +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( diff --git a/migrations/README b/migrations/README new file mode 100755 index 00000000..0e048441 --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 00000000..ec9d45c2 --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py new file mode 100755 index 00000000..68feded2 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,91 @@ +from __future__ import with_statement + +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option( + 'sqlalchemy.url', + str(current_app.extensions['migrate'].db.get_engine().url).replace( + '%', '%%')) +target_metadata = current_app.extensions['migrate'].db.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=target_metadata, literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + connectable = current_app.extensions['migrate'].db.get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=target_metadata, + process_revision_directives=process_revision_directives, + **current_app.extensions['migrate'].configure_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100755 index 00000000..2c015630 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/6b071b7947e5_scodoc_9_0_4_code_module_en_text.py b/migrations/versions/6b071b7947e5_scodoc_9_0_4_code_module_en_text.py new file mode 100644 index 00000000..c16e8582 --- /dev/null +++ b/migrations/versions/6b071b7947e5_scodoc_9_0_4_code_module_en_text.py @@ -0,0 +1,34 @@ +"""ScoDoc 9.0.4: code module en Text + +Revision ID: 6b071b7947e5 +Revises: 993ce4a01d57 +Create Date: 2021-08-27 16:00:27.322153 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '6b071b7947e5' +down_revision = '993ce4a01d57' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('notes_modules', 'code', + existing_type=sa.VARCHAR(length=32), + type_=sa.Text(), + existing_nullable=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('notes_modules', 'code', + existing_type=sa.Text(), + type_=sa.VARCHAR(length=32), + existing_nullable=False) + # ### end Alembic commands ### diff --git a/migrations/versions/993ce4a01d57_scodoc_9_0_3.py b/migrations/versions/993ce4a01d57_scodoc_9_0_3.py new file mode 100644 index 00000000..c1e87c93 --- /dev/null +++ b/migrations/versions/993ce4a01d57_scodoc_9_0_3.py @@ -0,0 +1,1199 @@ +"""ScoDoc 9.0.3 + +Revision ID: 993ce4a01d57 +Revises: +Create Date: 2021-08-27 11:17:55.205910 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "993ce4a01d57" +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # Added by Emmanuel: ScoDoc SQl functions creation + op.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'; +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) ); + $$; + """ + ) + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "departement", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("acronym", sa.String(length=32), nullable=False), + sa.Column("description", sa.Text(), nullable=True), + sa.Column( + "date_creation", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=True, + ), + sa.Column("visible", sa.Boolean(), server_default="true", nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_departement_acronym"), "departement", ["acronym"], unique=False + ) + op.create_table( + "etud_annotations", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column( + "date", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=True, + ), + sa.Column("etudid", sa.Integer(), nullable=True), + sa.Column("author", sa.Text(), nullable=True), + sa.Column("comment", sa.Text(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "itemsuivi_tags", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("title", sa.Text(), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("title"), + ) + op.create_table( + "notes_form_modalites", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("modalite", sa.String(length=32), server_default="FI", nullable=True), + sa.Column("titre", sa.Text(), nullable=True), + sa.Column("numero", sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_notes_form_modalites_modalite"), + "notes_form_modalites", + ["modalite"], + unique=True, + ) + op.create_table( + "role", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(length=64), nullable=True), + sa.Column("default", sa.Boolean(), nullable=True), + sa.Column("permissions", sa.BigInteger(), nullable=True), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("name"), + ) + op.create_index(op.f("ix_role_default"), "role", ["default"], unique=False) + op.create_table( + "scolog", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column( + "date", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=True, + ), + sa.Column("method", sa.Text(), nullable=True), + sa.Column("msg", sa.Text(), nullable=True), + sa.Column("etudid", sa.Integer(), nullable=True), + sa.Column("authenticated_user", sa.Text(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "user", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("user_name", sa.String(length=64), nullable=True), + sa.Column("email", sa.String(length=120), nullable=True), + sa.Column("nom", sa.String(length=64), nullable=True), + sa.Column("prenom", sa.String(length=64), nullable=True), + sa.Column("dept", sa.String(length=32), nullable=True), + sa.Column("active", sa.Boolean(), nullable=True), + sa.Column("password_hash", sa.String(length=128), nullable=True), + sa.Column("password_scodoc7", sa.String(length=42), nullable=True), + sa.Column("last_seen", sa.DateTime(), nullable=True), + sa.Column("date_modif_passwd", sa.DateTime(), nullable=True), + sa.Column("date_created", sa.DateTime(), nullable=True), + sa.Column("date_expiration", sa.DateTime(), nullable=True), + sa.Column("passwd_temp", sa.Boolean(), nullable=True), + sa.Column("token", sa.String(length=32), nullable=True), + sa.Column("token_expiration", sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_user_active"), "user", ["active"], unique=False) + op.create_index(op.f("ix_user_dept"), "user", ["dept"], unique=False) + op.create_index(op.f("ix_user_token"), "user", ["token"], unique=True) + op.create_index(op.f("ix_user_user_name"), "user", ["user_name"], unique=True) + op.create_table( + "entreprises", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("dept_id", sa.Integer(), nullable=True), + sa.Column("nom", sa.Text(), nullable=True), + sa.Column("adresse", sa.Text(), nullable=True), + sa.Column("ville", sa.Text(), nullable=True), + sa.Column("codepostal", sa.Text(), nullable=True), + sa.Column("pays", sa.Text(), nullable=True), + sa.Column("contact_origine", sa.Text(), nullable=True), + sa.Column("secteur", sa.Text(), nullable=True), + sa.Column("note", sa.Text(), nullable=True), + sa.Column("privee", sa.Text(), nullable=True), + sa.Column("localisation", sa.Text(), nullable=True), + sa.Column("qualite_relation", sa.Integer(), nullable=True), + sa.Column("plus10salaries", sa.Boolean(), nullable=True), + sa.Column( + "date_creation", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=True, + ), + sa.ForeignKeyConstraint( + ["dept_id"], + ["departement.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_entreprises_dept_id"), "entreprises", ["dept_id"], unique=False + ) + op.create_table( + "identite", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("dept_id", sa.Integer(), nullable=True), + sa.Column("nom", sa.Text(), nullable=True), + sa.Column("prenom", sa.Text(), nullable=True), + sa.Column("nom_usuel", sa.Text(), nullable=True), + sa.Column("civilite", sa.String(length=1), nullable=False), + sa.Column("date_naissance", sa.Date(), nullable=True), + sa.Column("lieu_naissance", sa.Text(), nullable=True), + sa.Column("dept_naissance", sa.Text(), nullable=True), + sa.Column("nationalite", sa.Text(), nullable=True), + sa.Column("statut", sa.Text(), nullable=True), + sa.Column("boursier", sa.Boolean(), nullable=True), + sa.Column("photo_filename", sa.Text(), nullable=True), + sa.Column("code_nip", sa.Text(), nullable=True), + sa.Column("code_ine", sa.Text(), nullable=True), + sa.CheckConstraint("civilite IN ('M', 'F', 'X')"), + sa.ForeignKeyConstraint( + ["dept_id"], + ["departement.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_identite_dept_id"), "identite", ["dept_id"], unique=False) + op.create_table( + "notes_formations", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("dept_id", sa.Integer(), nullable=True), + sa.Column("acronyme", sa.Text(), nullable=False), + sa.Column("titre", sa.Text(), nullable=False), + sa.Column("titre_officiel", sa.Text(), nullable=False), + sa.Column("version", sa.Integer(), server_default="1", nullable=True), + sa.Column( + "formation_code", + sa.String(length=32), + server_default=sa.text("notes_newid_fcod()"), + nullable=False, + ), + sa.Column("type_parcours", sa.Integer(), server_default="0", nullable=True), + sa.Column("code_specialite", sa.String(length=32), nullable=True), + sa.ForeignKeyConstraint( + ["dept_id"], + ["departement.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("acronyme", "titre", "version"), + ) + op.create_index( + op.f("ix_notes_formations_dept_id"), + "notes_formations", + ["dept_id"], + unique=False, + ) + op.create_table( + "notes_semset", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("dept_id", sa.Integer(), nullable=True), + sa.Column("title", sa.Text(), nullable=True), + sa.Column("annee_scolaire", sa.Integer(), nullable=True), + sa.Column("sem_id", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["dept_id"], + ["departement.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "notes_tags", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("dept_id", sa.Integer(), nullable=True), + sa.Column("title", sa.Text(), nullable=False), + sa.ForeignKeyConstraint( + ["dept_id"], + ["departement.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("title", "dept_id"), + ) + op.create_index( + op.f("ix_notes_tags_dept_id"), "notes_tags", ["dept_id"], unique=False + ) + op.create_table( + "scolar_news", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("dept_id", sa.Integer(), nullable=True), + sa.Column( + "date", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=True, + ), + sa.Column("authenticated_user", sa.Text(), nullable=True), + sa.Column("type", sa.String(length=32), nullable=True), + sa.Column("object", sa.Integer(), nullable=True), + sa.Column("text", sa.Text(), nullable=True), + sa.Column("url", sa.Text(), nullable=True), + sa.ForeignKeyConstraint( + ["dept_id"], + ["departement.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_scolar_news_dept_id"), "scolar_news", ["dept_id"], unique=False + ) + op.create_table( + "user_role", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=True), + sa.Column("role_id", sa.Integer(), nullable=True), + sa.Column("dept", sa.String(length=64), nullable=True), + sa.ForeignKeyConstraint( + ["role_id"], + ["role.id"], + ), + sa.ForeignKeyConstraint( + ["user_id"], + ["user.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "admissions", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("etudid", sa.Integer(), nullable=True), + sa.Column("annee", sa.Integer(), nullable=True), + sa.Column("bac", sa.Text(), nullable=True), + sa.Column("specialite", sa.Text(), nullable=True), + sa.Column("annee_bac", sa.Integer(), nullable=True), + sa.Column("math", sa.Text(), nullable=True), + sa.Column("physique", sa.Float(), nullable=True), + sa.Column("anglais", sa.Float(), nullable=True), + sa.Column("francais", sa.Float(), nullable=True), + sa.Column("rang", sa.Integer(), nullable=True), + sa.Column("qualite", sa.Float(), nullable=True), + sa.Column("rapporteur", sa.Text(), nullable=True), + sa.Column("decision", sa.Text(), nullable=True), + sa.Column("score", sa.Float(), nullable=True), + sa.Column("commentaire", sa.Text(), nullable=True), + sa.Column("nomlycee", sa.Text(), nullable=True), + sa.Column("villelycee", sa.Text(), nullable=True), + sa.Column("codepostallycee", sa.Text(), nullable=True), + sa.Column("codelycee", sa.Text(), nullable=True), + sa.Column("type_admission", sa.Text(), nullable=True), + sa.Column("boursier_prec", sa.Boolean(), nullable=True), + sa.Column("classement", sa.Integer(), nullable=True), + sa.Column("apb_groupe", sa.Text(), nullable=True), + sa.Column("apb_classement_gr", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["etudid"], + ["identite.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "adresse", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("etudid", sa.Integer(), nullable=True), + sa.Column("email", sa.Text(), nullable=True), + sa.Column("emailperso", sa.Text(), nullable=True), + sa.Column("domicile", sa.Text(), nullable=True), + sa.Column("codepostaldomicile", sa.Text(), nullable=True), + sa.Column("villedomicile", sa.Text(), nullable=True), + sa.Column("paysdomicile", sa.Text(), nullable=True), + sa.Column("telephone", sa.Text(), nullable=True), + sa.Column("telephonemobile", sa.Text(), nullable=True), + sa.Column("fax", sa.Text(), nullable=True), + sa.Column("typeadresse", sa.Text(), server_default="domicile", nullable=False), + sa.Column("description", sa.Text(), nullable=True), + sa.ForeignKeyConstraint( + ["etudid"], + ["identite.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "billet_absence", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("etudid", sa.Integer(), nullable=True), + sa.Column("abs_begin", sa.DateTime(timezone=True), nullable=True), + sa.Column("abs_end", sa.DateTime(timezone=True), nullable=True), + sa.Column("description", sa.Text(), nullable=True), + sa.Column("etat", sa.Boolean(), server_default="false", nullable=True), + sa.Column( + "entry_date", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=True, + ), + sa.Column("justified", sa.Boolean(), server_default="false", nullable=True), + sa.ForeignKeyConstraint( + ["etudid"], + ["identite.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_billet_absence_etudid"), "billet_absence", ["etudid"], unique=False + ) + op.create_table( + "entreprise_correspondant", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("entreprise_id", sa.Integer(), nullable=True), + sa.Column("nom", sa.Text(), nullable=True), + sa.Column("prenom", sa.Text(), nullable=True), + sa.Column("civilite", sa.Text(), nullable=True), + sa.Column("fonction", sa.Text(), nullable=True), + sa.Column("phone1", sa.Text(), nullable=True), + sa.Column("phone2", sa.Text(), nullable=True), + sa.Column("mobile", sa.Text(), nullable=True), + sa.Column("mail1", sa.Text(), nullable=True), + sa.Column("mail2", sa.Text(), nullable=True), + sa.Column("fax", sa.Text(), nullable=True), + sa.Column("note", sa.Text(), nullable=True), + sa.ForeignKeyConstraint( + ["entreprise_id"], + ["entreprises.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "itemsuivi", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("etudid", sa.Integer(), nullable=True), + sa.Column( + "item_date", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=True, + ), + sa.Column("situation", sa.Text(), nullable=True), + sa.ForeignKeyConstraint( + ["etudid"], + ["identite.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "notes_formsemestre", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("dept_id", sa.Integer(), nullable=True), + sa.Column("formation_id", sa.Integer(), nullable=True), + sa.Column("semestre_id", sa.Integer(), server_default="1", nullable=False), + sa.Column("titre", sa.Text(), nullable=True), + sa.Column("date_debut", sa.Date(), nullable=True), + sa.Column("date_fin", sa.Date(), nullable=True), + sa.Column("etat", sa.Boolean(), server_default="true", nullable=False), + sa.Column("modalite", sa.String(length=32), nullable=True), + sa.Column( + "gestion_compensation", sa.Boolean(), server_default="false", nullable=False + ), + sa.Column("bul_hide_xml", sa.Boolean(), server_default="false", nullable=False), + sa.Column( + "gestion_semestrielle", sa.Boolean(), server_default="false", nullable=False + ), + sa.Column( + "bul_bgcolor", sa.String(length=32), server_default="white", nullable=True + ), + sa.Column( + "resp_can_edit", sa.Boolean(), server_default="false", nullable=False + ), + sa.Column( + "resp_can_change_ens", sa.Boolean(), server_default="true", nullable=False + ), + sa.Column( + "ens_can_edit_eval", sa.Boolean(), server_default="False", nullable=False + ), + sa.Column("elt_sem_apo", sa.Text(), nullable=True), + sa.Column("elt_annee_apo", sa.Text(), nullable=True), + sa.ForeignKeyConstraint( + ["dept_id"], + ["departement.id"], + ), + sa.ForeignKeyConstraint( + ["formation_id"], + ["notes_formations.id"], + ), + sa.ForeignKeyConstraint( + ["modalite"], + ["notes_form_modalites.modalite"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_notes_formsemestre_dept_id"), + "notes_formsemestre", + ["dept_id"], + unique=False, + ) + op.create_table( + "notes_notes_log", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("etudid", sa.Integer(), nullable=True), + sa.Column("evaluation_id", sa.Integer(), nullable=True), + sa.Column("value", sa.Float(), nullable=True), + sa.Column("comment", sa.Text(), nullable=True), + sa.Column( + "date", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=True, + ), + sa.Column("uid", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["etudid"], + ["identite.id"], + ), + sa.ForeignKeyConstraint( + ["uid"], + ["user.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_notes_notes_log_evaluation_id"), + "notes_notes_log", + ["evaluation_id"], + unique=False, + ) + op.create_table( + "notes_ue", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("formation_id", sa.Integer(), nullable=True), + sa.Column("acronyme", sa.Text(), nullable=False), + sa.Column("numero", sa.Integer(), nullable=True), + sa.Column("titre", sa.Text(), nullable=True), + sa.Column("type", sa.Integer(), server_default="0", nullable=True), + sa.Column( + "ue_code", + sa.String(length=32), + server_default=sa.text("notes_newid_ucod()"), + nullable=False, + ), + sa.Column("ects", sa.Float(), nullable=True), + sa.Column("is_external", sa.Boolean(), server_default="false", nullable=True), + sa.Column("code_apogee", sa.String(length=16), nullable=True), + sa.Column("coefficient", sa.Float(), nullable=True), + sa.ForeignKeyConstraint( + ["formation_id"], + ["notes_formations.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "absences_notifications", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("etudid", sa.Integer(), nullable=True), + sa.Column( + "notification_date", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=True, + ), + sa.Column("email", sa.Text(), nullable=True), + sa.Column("nbabs", sa.Integer(), nullable=True), + sa.Column("nbabsjust", sa.Integer(), nullable=True), + sa.Column("formsemestre_id", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["etudid"], + ["identite.id"], + ), + sa.ForeignKeyConstraint( + ["formsemestre_id"], + ["notes_formsemestre.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "entreprise_contact", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("date", sa.DateTime(timezone=True), nullable=True), + sa.Column("type_contact", sa.Text(), nullable=True), + sa.Column("entreprise_id", sa.Integer(), nullable=True), + sa.Column("entreprise_corresp_id", sa.Integer(), nullable=True), + sa.Column("etudid", sa.Integer(), nullable=True), + sa.Column("description", sa.Text(), nullable=True), + sa.Column("enseignant", sa.Text(), nullable=True), + sa.ForeignKeyConstraint( + ["entreprise_corresp_id"], + ["entreprise_correspondant.id"], + ), + sa.ForeignKeyConstraint( + ["entreprise_id"], + ["entreprises.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "itemsuivi_tags_assoc", + sa.Column("tag_id", sa.Integer(), nullable=True), + sa.Column("itemsuivi_id", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["itemsuivi_id"], + ["itemsuivi.id"], + ), + sa.ForeignKeyConstraint( + ["tag_id"], + ["itemsuivi_tags.id"], + ), + ) + op.create_table( + "notes_appreciations", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column( + "date", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=True, + ), + sa.Column("etudid", sa.Integer(), nullable=True), + sa.Column("formsemestre_id", sa.Integer(), nullable=True), + sa.Column("author", sa.Text(), nullable=True), + sa.Column("comment", sa.Text(), nullable=True), + sa.ForeignKeyConstraint( + ["etudid"], + ["identite.id"], + ), + sa.ForeignKeyConstraint( + ["formsemestre_id"], + ["notes_formsemestre.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_notes_appreciations_etudid"), + "notes_appreciations", + ["etudid"], + unique=False, + ) + op.create_table( + "notes_formsemestre_custommenu", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("formsemestre_id", sa.Integer(), nullable=True), + sa.Column("title", sa.Text(), nullable=True), + sa.Column("url", sa.Text(), nullable=True), + sa.Column("idx", sa.Integer(), server_default="0", nullable=True), + sa.ForeignKeyConstraint( + ["formsemestre_id"], + ["notes_formsemestre.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "notes_formsemestre_etapes", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("formsemestre_id", sa.Integer(), nullable=True), + sa.Column("etape_apo", sa.String(length=16), nullable=True), + sa.ForeignKeyConstraint( + ["formsemestre_id"], + ["notes_formsemestre.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "notes_formsemestre_inscription", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("etudid", sa.Integer(), nullable=True), + sa.Column("formsemestre_id", sa.Integer(), nullable=True), + sa.Column("etat", sa.String(length=16), nullable=True), + sa.Column("etape", sa.String(length=16), nullable=True), + sa.ForeignKeyConstraint( + ["etudid"], + ["identite.id"], + ), + sa.ForeignKeyConstraint( + ["formsemestre_id"], + ["notes_formsemestre.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("formsemestre_id", "etudid"), + ) + op.create_table( + "notes_formsemestre_responsables", + sa.Column("formsemestre_id", sa.Integer(), nullable=True), + sa.Column("responsable_id", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["formsemestre_id"], + ["notes_formsemestre.id"], + ), + sa.ForeignKeyConstraint( + ["responsable_id"], + ["user.id"], + ), + ) + op.create_table( + "notes_formsemestre_ue_computation_expr", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("formsemestre_id", sa.Integer(), nullable=True), + sa.Column("ue_id", sa.Integer(), nullable=True), + sa.Column("computation_expr", sa.Text(), nullable=True), + sa.ForeignKeyConstraint( + ["formsemestre_id"], + ["notes_formsemestre.id"], + ), + sa.ForeignKeyConstraint( + ["ue_id"], + ["notes_ue.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("formsemestre_id", "ue_id"), + ) + op.create_table( + "notes_formsemestre_uecoef", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("formsemestre_id", sa.Integer(), nullable=True), + sa.Column("ue_id", sa.Integer(), nullable=True), + sa.Column("coefficient", sa.Float(), nullable=False), + sa.ForeignKeyConstraint( + ["formsemestre_id"], + ["notes_formsemestre.id"], + ), + sa.ForeignKeyConstraint( + ["ue_id"], + ["notes_ue.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("formsemestre_id", "ue_id"), + ) + op.create_table( + "notes_matieres", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("ue_id", sa.Integer(), nullable=True), + sa.Column("titre", sa.Text(), nullable=True), + sa.Column("numero", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["ue_id"], + ["notes_ue.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("ue_id", "titre"), + ) + op.create_table( + "notes_semset_formsemestre", + sa.Column("formsemestre_id", sa.Integer(), nullable=True), + sa.Column("semset_id", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["formsemestre_id"], + ["notes_formsemestre.id"], + ), + sa.ForeignKeyConstraint( + ["semset_id"], + ["notes_semset.id"], + ), + sa.UniqueConstraint("formsemestre_id", "semset_id"), + ) + op.create_table( + "partition", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("formsemestre_id", sa.Integer(), nullable=True), + sa.Column("partition_name", sa.String(length=32), nullable=True), + sa.Column("numero", sa.Integer(), nullable=True), + sa.Column( + "bul_show_rank", sa.Boolean(), server_default="false", nullable=False + ), + sa.Column("show_in_lists", sa.Boolean(), server_default="true", nullable=False), + sa.ForeignKeyConstraint( + ["formsemestre_id"], + ["notes_formsemestre.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("formsemestre_id", "partition_name"), + ) + op.create_index( + op.f("ix_partition_formsemestre_id"), + "partition", + ["formsemestre_id"], + unique=False, + ) + op.create_table( + "sco_prefs", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("dept_id", sa.Integer(), nullable=True), + sa.Column("name", sa.String(length=128), nullable=False), + sa.Column("value", sa.Text(), nullable=True), + sa.Column("formsemestre_id", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["dept_id"], + ["departement.id"], + ), + sa.ForeignKeyConstraint( + ["formsemestre_id"], + ["notes_formsemestre.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_sco_prefs_name"), "sco_prefs", ["name"], unique=False) + op.create_table( + "scolar_autorisation_inscription", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("etudid", sa.Integer(), nullable=True), + sa.Column("formation_code", sa.String(length=32), nullable=False), + sa.Column("semestre_id", sa.Integer(), nullable=True), + sa.Column( + "date", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=True, + ), + sa.Column("origin_formsemestre_id", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["etudid"], + ["identite.id"], + ), + sa.ForeignKeyConstraint( + ["origin_formsemestre_id"], + ["notes_formsemestre.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "scolar_events", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("etudid", sa.Integer(), nullable=True), + sa.Column( + "event_date", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=True, + ), + sa.Column("formsemestre_id", sa.Integer(), nullable=True), + sa.Column("ue_id", sa.Integer(), nullable=True), + sa.Column("event_type", sa.String(length=32), nullable=True), + sa.Column("comp_formsemestre_id", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["comp_formsemestre_id"], + ["notes_formsemestre.id"], + ), + sa.ForeignKeyConstraint( + ["etudid"], + ["identite.id"], + ), + sa.ForeignKeyConstraint( + ["formsemestre_id"], + ["notes_formsemestre.id"], + ), + sa.ForeignKeyConstraint( + ["ue_id"], + ["notes_ue.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "scolar_formsemestre_validation", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("etudid", sa.Integer(), nullable=True), + sa.Column("formsemestre_id", sa.Integer(), nullable=True), + sa.Column("ue_id", sa.Integer(), nullable=True), + sa.Column("code", sa.String(length=16), nullable=False), + sa.Column("assidu", sa.Boolean(), nullable=True), + sa.Column( + "event_date", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=True, + ), + sa.Column("compense_formsemestre_id", sa.Integer(), nullable=True), + sa.Column("moy_ue", sa.Float(), nullable=True), + sa.Column("semestre_id", sa.Integer(), nullable=True), + sa.Column("is_external", sa.Boolean(), server_default="false", nullable=True), + sa.ForeignKeyConstraint( + ["compense_formsemestre_id"], + ["notes_formsemestre.id"], + ), + sa.ForeignKeyConstraint( + ["etudid"], + ["identite.id"], + ), + sa.ForeignKeyConstraint( + ["formsemestre_id"], + ["notes_formsemestre.id"], + ), + sa.ForeignKeyConstraint( + ["ue_id"], + ["notes_ue.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("etudid", "formsemestre_id", "ue_id"), + ) + op.create_table( + "group_descr", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("partition_id", sa.Integer(), nullable=True), + sa.Column("group_name", sa.String(length=64), nullable=True), + sa.ForeignKeyConstraint( + ["partition_id"], + ["partition.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("partition_id", "group_name"), + ) + op.create_table( + "notes_modules", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("titre", sa.Text(), nullable=True), + sa.Column("abbrev", sa.Text(), nullable=True), + sa.Column("code", sa.String(length=32), nullable=False), + sa.Column("heures_cours", sa.Float(), nullable=True), + sa.Column("heures_td", sa.Float(), nullable=True), + sa.Column("heures_tp", sa.Float(), nullable=True), + sa.Column("coefficient", sa.Float(), nullable=True), + sa.Column("ects", sa.Float(), nullable=True), + sa.Column("ue_id", sa.Integer(), nullable=True), + sa.Column("formation_id", sa.Integer(), nullable=True), + sa.Column("matiere_id", sa.Integer(), nullable=True), + sa.Column("semestre_id", sa.Integer(), server_default="1", nullable=False), + sa.Column("numero", sa.Integer(), nullable=True), + sa.Column("code_apogee", sa.String(length=16), nullable=True), + sa.Column("module_type", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["formation_id"], + ["notes_formations.id"], + ), + sa.ForeignKeyConstraint( + ["matiere_id"], + ["notes_matieres.id"], + ), + sa.ForeignKeyConstraint( + ["ue_id"], + ["notes_ue.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_notes_modules_ue_id"), "notes_modules", ["ue_id"], unique=False + ) + op.create_table( + "group_membership", + sa.Column("etudid", sa.Integer(), nullable=True), + sa.Column("group_id", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["etudid"], + ["identite.id"], + ), + sa.ForeignKeyConstraint( + ["group_id"], + ["group_descr.id"], + ), + sa.UniqueConstraint("etudid", "group_id"), + ) + op.create_table( + "notes_moduleimpl", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("module_id", sa.Integer(), nullable=True), + sa.Column("formsemestre_id", sa.Integer(), nullable=True), + sa.Column("responsable_id", sa.Integer(), nullable=True), + sa.Column("computation_expr", sa.Text(), nullable=True), + sa.ForeignKeyConstraint( + ["formsemestre_id"], + ["notes_formsemestre.id"], + ), + sa.ForeignKeyConstraint( + ["module_id"], + ["notes_modules.id"], + ), + sa.ForeignKeyConstraint( + ["responsable_id"], + ["user.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("formsemestre_id", "module_id"), + ) + op.create_index( + op.f("ix_notes_moduleimpl_formsemestre_id"), + "notes_moduleimpl", + ["formsemestre_id"], + unique=False, + ) + op.create_table( + "notes_modules_tags", + sa.Column("tag_id", sa.Integer(), nullable=True), + sa.Column("module_id", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["module_id"], + ["notes_modules.id"], + ), + sa.ForeignKeyConstraint( + ["tag_id"], + ["notes_tags.id"], + ), + ) + op.create_table( + "absences", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("etudid", sa.Integer(), nullable=True), + sa.Column("jour", sa.Date(), nullable=True), + sa.Column("estabs", sa.Boolean(), nullable=True), + sa.Column("estjust", sa.Boolean(), nullable=True), + sa.Column("matin", sa.Boolean(), nullable=True), + sa.Column("description", sa.Text(), nullable=True), + sa.Column( + "entry_date", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=True, + ), + sa.Column("moduleimpl_id", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["etudid"], + ["identite.id"], + ), + sa.ForeignKeyConstraint( + ["moduleimpl_id"], + ["notes_moduleimpl.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_absences_etudid"), "absences", ["etudid"], unique=False) + op.create_table( + "notes_evaluation", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("moduleimpl_id", sa.Integer(), nullable=True), + sa.Column("jour", sa.Date(), nullable=True), + sa.Column("heure_debut", sa.Time(), nullable=True), + sa.Column("heure_fin", sa.Time(), nullable=True), + sa.Column("description", sa.Text(), nullable=True), + sa.Column("note_max", sa.Float(), nullable=True), + sa.Column("coefficient", sa.Float(), nullable=True), + sa.Column("visibulletin", sa.Boolean(), server_default="true", nullable=False), + sa.Column( + "publish_incomplete", sa.Boolean(), server_default="false", nullable=False + ), + sa.Column("evaluation_type", sa.Integer(), server_default="0", nullable=False), + sa.Column("numero", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["moduleimpl_id"], + ["notes_moduleimpl.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_notes_evaluation_moduleimpl_id"), + "notes_evaluation", + ["moduleimpl_id"], + unique=False, + ) + op.create_table( + "notes_moduleimpl_inscription", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("moduleimpl_id", sa.Integer(), nullable=True), + sa.Column("etudid", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["etudid"], + ["identite.id"], + ), + sa.ForeignKeyConstraint( + ["moduleimpl_id"], + ["notes_moduleimpl.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_notes_moduleimpl_inscription_etudid"), + "notes_moduleimpl_inscription", + ["etudid"], + unique=False, + ) + op.create_index( + op.f("ix_notes_moduleimpl_inscription_moduleimpl_id"), + "notes_moduleimpl_inscription", + ["moduleimpl_id"], + unique=False, + ) + op.create_table( + "notes_modules_enseignants", + sa.Column("moduleimpl_id", sa.Integer(), nullable=True), + sa.Column("ens_id", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["ens_id"], + ["user.id"], + ), + sa.ForeignKeyConstraint( + ["moduleimpl_id"], + ["notes_moduleimpl.id"], + ), + ) + op.create_table( + "notes_notes", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("etudid", sa.Integer(), nullable=True), + sa.Column("evaluation_id", sa.Integer(), nullable=True), + sa.Column("value", sa.Float(), nullable=True), + sa.Column("comment", sa.Text(), nullable=True), + sa.Column( + "date", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=True, + ), + sa.Column("uid", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["etudid"], + ["identite.id"], + ), + sa.ForeignKeyConstraint( + ["evaluation_id"], + ["notes_evaluation.id"], + ), + sa.ForeignKeyConstraint( + ["uid"], + ["user.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("etudid", "evaluation_id"), + ) + op.create_index( + op.f("ix_notes_notes_evaluation_id"), + "notes_notes", + ["evaluation_id"], + unique=False, + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f("ix_notes_notes_evaluation_id"), table_name="notes_notes") + op.drop_table("notes_notes") + op.drop_table("notes_modules_enseignants") + op.drop_index( + op.f("ix_notes_moduleimpl_inscription_moduleimpl_id"), + table_name="notes_moduleimpl_inscription", + ) + op.drop_index( + op.f("ix_notes_moduleimpl_inscription_etudid"), + table_name="notes_moduleimpl_inscription", + ) + op.drop_table("notes_moduleimpl_inscription") + op.drop_index( + op.f("ix_notes_evaluation_moduleimpl_id"), table_name="notes_evaluation" + ) + op.drop_table("notes_evaluation") + op.drop_index(op.f("ix_absences_etudid"), table_name="absences") + op.drop_table("absences") + op.drop_table("notes_modules_tags") + op.drop_index( + op.f("ix_notes_moduleimpl_formsemestre_id"), table_name="notes_moduleimpl" + ) + op.drop_table("notes_moduleimpl") + op.drop_table("group_membership") + op.drop_index(op.f("ix_notes_modules_ue_id"), table_name="notes_modules") + op.drop_table("notes_modules") + op.drop_table("group_descr") + op.drop_table("scolar_formsemestre_validation") + op.drop_table("scolar_events") + op.drop_table("scolar_autorisation_inscription") + op.drop_index(op.f("ix_sco_prefs_name"), table_name="sco_prefs") + op.drop_table("sco_prefs") + op.drop_index(op.f("ix_partition_formsemestre_id"), table_name="partition") + op.drop_table("partition") + op.drop_table("notes_semset_formsemestre") + op.drop_table("notes_matieres") + op.drop_table("notes_formsemestre_uecoef") + op.drop_table("notes_formsemestre_ue_computation_expr") + op.drop_table("notes_formsemestre_responsables") + op.drop_table("notes_formsemestre_inscription") + op.drop_table("notes_formsemestre_etapes") + op.drop_table("notes_formsemestre_custommenu") + op.drop_index( + op.f("ix_notes_appreciations_etudid"), table_name="notes_appreciations" + ) + op.drop_table("notes_appreciations") + op.drop_table("itemsuivi_tags_assoc") + op.drop_table("entreprise_contact") + op.drop_table("absences_notifications") + op.drop_table("notes_ue") + op.drop_index( + op.f("ix_notes_notes_log_evaluation_id"), table_name="notes_notes_log" + ) + op.drop_table("notes_notes_log") + op.drop_index( + op.f("ix_notes_formsemestre_dept_id"), table_name="notes_formsemestre" + ) + op.drop_table("notes_formsemestre") + op.drop_table("itemsuivi") + op.drop_table("entreprise_correspondant") + op.drop_index(op.f("ix_billet_absence_etudid"), table_name="billet_absence") + op.drop_table("billet_absence") + op.drop_table("adresse") + op.drop_table("admissions") + op.drop_table("user_role") + op.drop_index(op.f("ix_scolar_news_dept_id"), table_name="scolar_news") + op.drop_table("scolar_news") + op.drop_index(op.f("ix_notes_tags_dept_id"), table_name="notes_tags") + op.drop_table("notes_tags") + op.drop_table("notes_semset") + op.drop_index(op.f("ix_notes_formations_dept_id"), table_name="notes_formations") + op.drop_table("notes_formations") + op.drop_index(op.f("ix_identite_dept_id"), table_name="identite") + op.drop_table("identite") + op.drop_index(op.f("ix_entreprises_dept_id"), table_name="entreprises") + op.drop_table("entreprises") + op.drop_index(op.f("ix_user_user_name"), table_name="user") + op.drop_index(op.f("ix_user_token"), table_name="user") + op.drop_index(op.f("ix_user_dept"), table_name="user") + op.drop_index(op.f("ix_user_active"), table_name="user") + op.drop_table("user") + op.drop_table("scolog") + op.drop_index(op.f("ix_role_default"), table_name="role") + op.drop_table("role") + op.drop_index( + op.f("ix_notes_form_modalites_modalite"), table_name="notes_form_modalites" + ) + op.drop_table("notes_form_modalites") + op.drop_table("itemsuivi_tags") + op.drop_table("etud_annotations") + op.drop_index(op.f("ix_departement_acronym"), table_name="departement") + op.drop_table("departement") + # ### end Alembic commands ### diff --git a/scodoc.py b/scodoc.py index 45779e4c..a79bcafd 100755 --- a/scodoc.py +++ b/scodoc.py @@ -66,7 +66,7 @@ def make_shell_context(): @app.cli.command() -def db_init(): # db-init +def sco_db_init(): # sco-db-init """Initialize the database. Starts from an existing database and create all the necessary SQL tables and functions. diff --git a/tests/conftest.py b/tests/conftest.py index a3f1e9c7..ea654c70 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,7 +23,7 @@ def test_client(): with apptest.app_context(): with apptest.test_request_context(): # erase and reset database: - initialize_scodoc_database(erase=True) + initialize_scodoc_database(erase=True, create_all=True) # Loge l'utilisateur super-admin admin_user = get_super_admin() login_user(admin_user) diff --git a/tools/configure-scodoc9.sh b/tools/configure-scodoc9.sh index f058378d..1c914830 100755 --- a/tools/configure-scodoc9.sh +++ b/tools/configure-scodoc9.sh @@ -121,7 +121,7 @@ then echo echo "Création des tables et du compte admin" echo - su -c "(cd /opt/scodoc; source venv/bin/activate; flask db-init; flask user-password admin)" "$SCODOC_USER" || die "Erreur: db-init" + su -c "(cd /opt/scodoc; source venv/bin/activate; flask db upgrade; flask sco-db-init; flask user-password admin)" "$SCODOC_USER" || die "Erreur: sco-db-init" echo echo "base initialisée et admin créé." echo diff --git a/tools/debian/postinst b/tools/debian/postinst index 53db633c..835425d6 100644 --- a/tools/debian/postinst +++ b/tools/debian/postinst @@ -15,7 +15,7 @@ check_create_scodoc_user # -- Répertoires /opt/scodoc donné à scodoc change_scodoc_file_ownership -# --- Création au bseoin de /opt/scodoc-data +# --- Création au besoin de /opt/scodoc-data set_scodoc_var_dir # ------------ LOCALES (pour compat bases ScoDoc 7 et plus anciennes) @@ -71,11 +71,11 @@ fi # ------------ CREATION DU VIRTUALENV # donc re-créé sur le client à chaque install ou upgrade #echo "Creating python3 virtualenv..." -(cd $SCODOC_DIR && python3 -m venv venv) || die "Error creating Python 3 virtualenv" +su -c "(cd $SCODOC_DIR && python3 -m venv venv)" "$SCODOC_USER" || die "Error creating Python 3 virtualenv" # ------------ INSTALL DES PAQUETS PYTHON (3.9) # pip in our env, as user "scodoc" -(cd $SCODOC_DIR && source venv/bin/activate && pip install wheel && pip install -r requirements-3.9.txt) || die "Error installing python packages" +su -c "(cd $SCODOC_DIR && source venv/bin/activate && pip install wheel && pip install -r requirements-3.9.txt)" "$SCODOC_USER" || die "Error installing python packages" # --- NGINX if [ ! -L /etc/nginx/sites-enabled/scodoc9.nginx ] @@ -89,6 +89,19 @@ fi # --- Ensure postgres user "scodoc" ($POSTGRES_USER) exists init_postgres_user +# ------------ BASE DE DONNEES +# gérées avec Flask-Migrate (Alembic/SQLAlchemy) +# Si la base SCODOC existe, tente de la mettre à jour +# (Ne gère pas les bases DEV et TEST) +n=$(su -c "psql -l | grep -c -E '^[[:blank:]]*SCODOC[[:blank:]]*\|'" "$SCODOC_USER") +if [ "$n" == 1 ] +then + echo "Upgrading existing SCODOC database..." + # utilise les scripts dans migrations/version/ + # pour mettre à jour notre base (en tant qu'utilisateur scodoc) + export SQLALCHEMY_DATABASE_URI="postgresql:///SCODOC" + su -c "(cd $SCODOC_DIR && source venv/bin/activate && flask db upgrade)" "$SCODOC_USER" +fi # ------------ CONFIG SERVICE SCODOC echo