diff --git a/README.md b/README.md
index 45743c4149fb428ee661c6924287f7fe065c1d8c..2a77b25e219e35c681850f40873111152cd92286 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 001bbc241b021be7f747d4f6b5648f4d2caba4ce..fa9bb0c03743e64d496179ec72faeb9ba963e473 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 81828b824b1235437354284c09b495a4c109331b..756879d5939c202a7edaf8eb9034d0d2b5a29bd6 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 e10f8d14254855f2b39c9ef3d9ffbdf8d1787cd0..6cfde9d9a7749d7040a4e1e0d9e3c68c68715646 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 0000000000000000000000000000000000000000..0e048441597444a7e2850d6d7c4ce15550f79bda
--- /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 0000000000000000000000000000000000000000..ec9d45c26a6bb54e833fd4e6ce2de29343894f4b
--- /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 0000000000000000000000000000000000000000..68feded2a040005310d770ac7136b2e4ff8a6312
--- /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 0000000000000000000000000000000000000000..2c0156303a8df3ffdc9de87765bf801bf6bea4a5
--- /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 0000000000000000000000000000000000000000..c16e858206ea4c477123189320d7c577f9abcb55
--- /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 0000000000000000000000000000000000000000..c1e87c93aaa5716dd4d793708cb2b46e273dc66c
--- /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 45779e4ceb299582604f34e651c8e32fc6bd4e85..a79bcafd86887171a9a4932bddf1d82ada3718ec 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 a3f1e9c79a666c8ee87521eabaa6d0f9fdbe769f..ea654c70388405600c2a17ddb962012497a8e17b 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 f058378d5e09cbd242a1917e9448b5e6c3a1f8f6..1c914830ac6457a07b89cb0edf5f25f60805ec23 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 53db633c5c19dac2e38e234dbb4c5b28e3e80b8d..835425d6e3134d3163d61313ce4dc6166d6211cc 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