diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 570b8e702f..0c153b8a41 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -152,10 +152,13 @@ class ItemSuiviTag(db.Model): # Association tag <-> module itemsuivi_tags_assoc = db.Table( "itemsuivi_tags_assoc", - db.Column("tag_id", db.Integer, db.ForeignKey("itemsuivi_tags.id")), - db.Column("itemsuivi_id", db.Integer, db.ForeignKey("itemsuivi.id")), + db.Column( + "tag_id", db.Integer, db.ForeignKey("itemsuivi_tags.id", ondelete="CASCADE") + ), + db.Column( + "itemsuivi_id", db.Integer, db.ForeignKey("itemsuivi.id", ondelete="CASCADE") + ), ) -# ON DELETE CASCADE ? class EtudAnnotation(db.Model): diff --git a/app/models/formations.py b/app/models/formations.py index 3c00bb73a3..7994a40fad 100644 --- a/app/models/formations.py +++ b/app/models/formations.py @@ -114,10 +114,11 @@ class UniteEns(db.Model): ou à défaut le met à 1. """ if self.semestre_idx is None: - if self.modules: - self.semestre_idx = self.modules[0].semestre_id - else: + module = self.modules.first() + if module is None: self.semestre_idx = 1 + else: + self.semestre_idx = module.semestre_id db.session.add(self) db.session.commit() diff --git a/app/models/preferences.py b/app/models/preferences.py index b04ad0da29..59c82ec805 100644 --- a/app/models/preferences.py +++ b/app/models/preferences.py @@ -4,6 +4,7 @@ """ from app import db, log from app.scodoc import bonus_sport +from app.scodoc.sco_exceptions import ScoValueError class ScoPreference(db.Model): @@ -94,7 +95,7 @@ class ScoDocSiteConfig(db.Model): """returns bonus func with specified name. If name not specified, return the configured function. None if no bonus function configured. - Raises NameError if func_name not found in module bonus_sport. + Raises ScoValueError if func_name not found in module bonus_sport. """ if func_name is None: c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first() @@ -103,7 +104,13 @@ class ScoDocSiteConfig(db.Model): func_name = c.value if func_name == "": # pas de bonus défini return None - return getattr(bonus_sport, func_name) + try: + return getattr(bonus_sport, func_name) + except AttributeError: + raise ScoValueError( + f"""Fonction de calcul maison inexistante: {func_name}. + (contacter votre administrateur local).""" + ) @classmethod def get_bonus_sport_func_names(cls): diff --git a/app/scodoc/gen_tables.py b/app/scodoc/gen_tables.py index a986494edd..78a0f6a28b 100644 --- a/app/scodoc/gen_tables.py +++ b/app/scodoc/gen_tables.py @@ -498,7 +498,7 @@ class GenTable(object): headline = [] return "\n".join( [ - self.text_fields_separator.join([x for x in line]) + self.text_fields_separator.join([str(x) for x in line]) for line in headline + self.get_data_list() ] ) diff --git a/app/scodoc/sco_inscr_passage.py b/app/scodoc/sco_inscr_passage.py index cc91728485..2f952cb8f1 100644 --- a/app/scodoc/sco_inscr_passage.py +++ b/app/scodoc/sco_inscr_passage.py @@ -288,10 +288,11 @@ def formsemestre_inscr_passage( footer = html_sco_header.sco_footer() H = [header] if isinstance(etuds, str): - etuds = etuds.split(",") # vient du form de confirmation + # list de strings, vient du form de confirmation + etuds = [int(x) for x in etuds.split(",") if x] elif isinstance(etuds, int): etuds = [etuds] - etuds = [int(x) for x in etuds] + auth_etuds_by_sem, inscrits, candidats = list_authorized_etuds_by_sem(sem) etuds_set = set(etuds) candidats_set = set(candidats) diff --git a/app/scodoc/sco_page_etud.py b/app/scodoc/sco_page_etud.py index 424e835cf2..573d794554 100644 --- a/app/scodoc/sco_page_etud.py +++ b/app/scodoc/sco_page_etud.py @@ -149,6 +149,10 @@ def ficheEtud(etudid=None): authuser = current_user cnx = ndb.GetDBConnexion() if etudid: + try: # pour les bookmarks avec d'anciens ids... + etudid = int(etudid) + except ValueError: + raise ScoValueError("id invalide !") # la sidebar est differente s'il y a ou pas un etudid # voir html_sidebar.sidebar() g.etudid = etudid diff --git a/app/scodoc/sco_trombino.py b/app/scodoc/sco_trombino.py index d93de39518..84af342639 100644 --- a/app/scodoc/sco_trombino.py +++ b/app/scodoc/sco_trombino.py @@ -493,6 +493,8 @@ def photos_generate_excel_sample(group_ids=[]): def photos_import_files_form(group_ids=[]): """Formulaire pour importation photos""" + if not group_ids: + raise ScoValueError("paramètre manquant !") groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids) back_url = "groups_view?%s&curtab=tab-photos" % groups_infos.groups_query_args diff --git a/app/scodoc/sco_undo_notes.py b/app/scodoc/sco_undo_notes.py index aa21d8283e..870f68d244 100644 --- a/app/scodoc/sco_undo_notes.py +++ b/app/scodoc/sco_undo_notes.py @@ -177,7 +177,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"): """Table listant toutes les opérations de saisies de notes, dans toutes les évaluations du semestre. """ - sem = sco_formsemestre.get_formsemestre(formsemestre_id) + sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True) r = ndb.SimpleDictFetch( """SELECT i.nom, code_nip, n.*, mod.titre, e.description, e.jour FROM notes_notes n, notes_evaluation e, notes_moduleimpl mi, diff --git a/app/views/notes.py b/app/views/notes.py index 7431e95133..4d7164b3d5 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -45,6 +45,7 @@ from werkzeug.utils import redirect from config import Config +from app import api from app import db from app import models from app.auth.models import User @@ -661,6 +662,11 @@ def formsemestre_list( kw can specify some conditions: examples: formsemestre_list( format='json', formation_id='F777') """ + try: + formsemestre_id = int(formsemestre_id) if formsemestre_id is not None else None + formation_id = int(formation_id) if formation_id is not None else None + except ValueError: + return api.errors.error_response(404, "invalid id") # XAPI: new json api args = {} L = locals() @@ -1563,7 +1569,7 @@ def evaluation_delete(evaluation_id): """Form delete evaluation""" El = sco_evaluation_db.do_evaluation_list(args={"evaluation_id": evaluation_id}) if not El: - raise ValueError("Evalution inexistante ! (%s)" % evaluation_id) + raise ScoValueError("Evalution inexistante ! (%s)" % evaluation_id) E = El[0] M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] diff --git a/migrations/versions/39818df276aa_cascades_sur_itemsuivi.py b/migrations/versions/39818df276aa_cascades_sur_itemsuivi.py new file mode 100644 index 0000000000..a64002be72 --- /dev/null +++ b/migrations/versions/39818df276aa_cascades_sur_itemsuivi.py @@ -0,0 +1,34 @@ +"""cascades sur itemsuivi + +Revision ID: 39818df276aa +Revises: 1efe07413835 +Create Date: 2021-11-27 19:03:09.329065 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '39818df276aa' +down_revision = '1efe07413835' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint('itemsuivi_tags_assoc_tag_id_fkey', 'itemsuivi_tags_assoc', type_='foreignkey') + op.drop_constraint('itemsuivi_tags_assoc_itemsuivi_id_fkey', 'itemsuivi_tags_assoc', type_='foreignkey') + op.create_foreign_key(None, 'itemsuivi_tags_assoc', 'itemsuivi', ['itemsuivi_id'], ['id'], ondelete='CASCADE') + op.create_foreign_key(None, 'itemsuivi_tags_assoc', 'itemsuivi_tags', ['tag_id'], ['id'], ondelete='CASCADE') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'itemsuivi_tags_assoc', type_='foreignkey') + op.drop_constraint(None, 'itemsuivi_tags_assoc', type_='foreignkey') + op.create_foreign_key('itemsuivi_tags_assoc_itemsuivi_id_fkey', 'itemsuivi_tags_assoc', 'itemsuivi', ['itemsuivi_id'], ['id']) + op.create_foreign_key('itemsuivi_tags_assoc_tag_id_fkey', 'itemsuivi_tags_assoc', 'itemsuivi_tags', ['tag_id'], ['id']) + # ### end Alembic commands ### diff --git a/migrations/versions/ada0d1f3d84f_but_poids_evaluations_ac.py b/migrations/versions/ada0d1f3d84f_but_poids_evaluations_ac.py index a0ba7eecb3..8d15471740 100644 --- a/migrations/versions/ada0d1f3d84f_but_poids_evaluations_ac.py +++ b/migrations/versions/ada0d1f3d84f_but_poids_evaluations_ac.py @@ -11,7 +11,7 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. revision = "ada0d1f3d84f" -down_revision = "1efe07413835" +down_revision = "39818df276aa" branch_labels = None depends_on = None diff --git a/scodoc.py b/scodoc.py index 9f84ecdfce..cfea19d7b1 100755 --- a/scodoc.py +++ b/scodoc.py @@ -81,7 +81,8 @@ def make_shell_context(): @app.cli.command() -def sco_db_init(): # sco-db-init +@click.option("--erase/--no-erase", default=False) +def sco_db_init(erase=False): # sco-db-init """Initialize the database. Starts from an existing database and create all the necessary SQL tables and functions. @@ -91,7 +92,7 @@ def sco_db_init(): # sco-db-init """La variable SCODOC_ADMIN_MAIL n'est pas positionnée: vérifier votre .env""" ) return 100 - initialize_scodoc_database() + initialize_scodoc_database(erase=erase) @app.cli.command() diff --git a/tools/configure-scodoc9.sh b/tools/configure-scodoc9.sh index 2e2acb1ff5..251c25581e 100755 --- a/tools/configure-scodoc9.sh +++ b/tools/configure-scodoc9.sh @@ -120,7 +120,7 @@ then echo echo "Création des tables et du compte admin" echo - msg="Saisir le mot de passe de l'administrateur \(admin, via le web\):" + msg="Saisir le mot de passe administrateur \(admin, via le web\):" su -c "(cd /opt/scodoc; source venv/bin/activate; flask db upgrade; flask sco-db-init; echo; echo $msg; flask user-password admin)" "$SCODOC_USER" || die "Erreur: sco-db-init" echo echo "Base initialisée et admin créé." diff --git a/tools/migrate_from_scodoc7.sh b/tools/migrate_from_scodoc7.sh index b213abf22a..59c0cee8a8 100755 --- a/tools/migrate_from_scodoc7.sh +++ b/tools/migrate_from_scodoc7.sh @@ -30,41 +30,72 @@ usage() { echo "Ce script doit être lancé en tant que root sur le nouveau" echo "serveur ScoDoc 9 / Debian 11." echo - echo "Usage: $0 archive" - echo " ou $0 -m" + echo "Usage: $0 [-h] [-m] [-z] archive" echo echo " archive doit être un répertoire exporté via save_scodoc7_data.sh" echo " sur le serveur ScoDoc 7" - echo " Avec l'option -m, effectue une migration \"en place\"" - echo " avec import des données ScoDoc 7 qui étaient sur cette même" - echo " machine." - echo " Dans ce cas, le répertoire /opt/scodoc DOIT avoir été renommé" - echo " /opt/scodoc7" - echo " AVANT l'installation de ScoDoc 9." - echo + echo " Options:" + echo " -m" + echo " effectue une migration \"en place\"" + echo " avec import des données ScoDoc 7 qui étaient sur cette même" + echo " machine." + echo " Dans ce cas, le répertoire /opt/scodoc DOIT avoir été renommé" + echo " /opt/scodoc7" + echo " AVANT l'installation de ScoDoc 9." + echo + echo " -z" + echo " efface la base existante, utilise le scodoc-data existant sans" + echo " l'effacer et tolère les fichiers manquants dans la source." + echo " Utilisée pour reprendre une migration interrompue." exit 1 } -if [ ! $# -eq 1 ] -then + +INPLACE=0 +RESTART=0 +while getopts "hmz" opt; do + case "$opt" in + h) usage -fi -if [ "$1" == "-h" ] || [ "$1" == "--help" ] -then - usage -fi -if [ "$1" == "-m" ] -then + ;; + m) echo "Migration en place" INPLACE=1 SCODOC7_HOME=/opt/scodoc7 # vérifie que ScoDoc7 est bien arrêté: - systemctl is-active scodoc >& /dev/null && systemctl stop scodoc -else + systemctl is-active scodoc >& /dev/null && systemctl stop scodoc + ;; + z) + echo "Mode reprise sur erreur" + RESTART=1 + ;; + \?) + echo "Invalid option: -$OPTARG" >&2 + exit 1 + ;; + :) + echo "Option -$OPTARG requires an argument." >&2 + exit 1 + ;; + esac +done + +shift "$((OPTIND - 1))" + +if [ "$INPLACE" = "0" ] +then echo "Migration depuis archive $1" - INPLACE=0 SCODOC7_HOME="$1" # racine de l'archive importée fi +# --- 0. En mode reprise, efface la base de données. En effet, les base d'origine ne sont pas +# effacées par le script de migration, et en cas d'erreur en cours d'import, il est plus +# sûr de repartir de zéro. +if [ "$RESTART" = "1" ] +then + echo "Efface la base existante" + su -c "(cd /opt/scodoc && source venv/bin/activate && flask sco-db-init --erase)" "$SCODOC_USER" || die "Erreur: sco-db-init" +fi + # --- 1. Vérifie qu'aucun des départements à importer n'existe déjà check_existing_depts() { sco7_depts="" @@ -116,27 +147,33 @@ migrate_database_ownership() { migrate_local_files() { echo "Déplacement des fichiers de configuration et des archives" SCODOC_VAR_DIR_BACKUP="$SCODOC_VAR_DIR".bak - if [ -e "$SCODOC_VAR_DIR_BACKUP" ] + if [ "$RESTART" = "0" ] # ne le fait pas en mode "reprise" then - die "supprimer ou déplacer $SCODOC_VAR_DIR_BACKUP avant de continuer" + if [ -e "$SCODOC_VAR_DIR_BACKUP" ] + then + die "supprimer ou déplacer $SCODOC_VAR_DIR_BACKUP avant de continuer" + fi + if [ -e "$SCODOC_VAR_DIR" ] + then + echo " renomme $SCODOC_VAR_DIR en $SCODOC_VAR_DIR_BACKUP" + mv "$SCODOC_VAR_DIR" "$SCODOC_VAR_DIR_BACKUP" + fi + mkdir "$SCODOC_VAR_DIR" || die "erreur creation repertoire" fi - if [ -e "$SCODOC_VAR_DIR" ] + if [ $(ls "${SCODOC7_HOME}/var/scodoc" | wc -l) -ne 0 ] then - echo " renomme $SCODOC_VAR_DIR en $SCODOC_VAR_DIR_BACKUP" - mv "$SCODOC_VAR_DIR" "$SCODOC_VAR_DIR_BACKUP" + echo " déplace ${SCODOC7_HOME}/var/scodoc/ dans $SCODOC_VAR_DIR..." + mv "${SCODOC7_HOME}"/var/scodoc/* "$SCODOC_VAR_DIR" || die "migrate_local_files failed" fi - mkdir "$SCODOC_VAR_DIR" || die "erreur creation repertoire" - echo " déplace ${SCODOC7_HOME}/var/scodoc/ dans $SCODOC_VAR_DIR..." - mv "${SCODOC7_HOME}"/var/scodoc/* "$SCODOC_VAR_DIR" || die "migrate_local_files failed" # Récupère le .env: normalement ./opt/scodoc/.env est un lien vers # /opt/scodoc-data/.env - # sauf si installation non standard (developeurs) avec .env réelement dans /opt/scodoc - if [ -L "$SCODOC_DIR"/.env ] + # sauf si installation non standard (developeurs) avec .env réellement dans /opt/scodoc + if [ -L "$SCODOC_DIR"/.env ] && [ ! -e "$SCODOC_VAR_DIR"/.env ] then cp -p "$SCODOC_VAR_DIR_BACKUP"/.env "$SCODOC_VAR_DIR" || die "fichier .env manquant dans l'ancien $SCODOC_VAR_DIR !" fi # et les certificats - if [ -d "$SCODOC_VAR_DIR_BACKUP"/certs ] + if [ -d "$SCODOC_VAR_DIR_BACKUP"/certs ] && [ ! -d "$SCODOC_VAR_DIR"/certs ] then cp -rp "$SCODOC_VAR_DIR_BACKUP"/certs "$SCODOC_VAR_DIR" || die "erreur copie certs" fi @@ -144,8 +181,10 @@ migrate_local_files() { old_logs_dest="$SCODOC_VAR_DIR/log/scodoc7" echo "Copie des anciens logs ScoDoc 7 dans $old_logs_dest" mkdir -p "$old_logs_dest" || die "erreur creation $old_logs_dest" - mv "${SCODOC7_HOME}"/log/* "$old_logs_dest" || die "erreur mv" - + if [ $(ls "${SCODOC7_HOME}/log" | wc -l) -ne 0 ] + then + mv "${SCODOC7_HOME}"/log/* "$old_logs_dest" || die "erreur mv" + fi # Le fichier de customization local: # peut être dans .../var/config/scodoc_local.py # ou bien, sur les très anciennes installs, dans Products/ScoDoc/config/scodoc_config.py