1
0
forked from ScoDoc/ScoDoc

EDT: ajout des edt_id dans formsemestre, groupes, modules, users

This commit is contained in:
Emmanuel Viennet 2023-11-06 22:05:38 +01:00
parent 5824b7fb59
commit e71e4b27ec
15 changed files with 259 additions and 148 deletions

View File

@ -12,6 +12,7 @@ from typing import Optional
import cracklib # pylint: disable=import-error import cracklib # pylint: disable=import-error
import flask
from flask import current_app, g from flask import current_app, g
from flask_login import UserMixin, AnonymousUserMixin from flask_login import UserMixin, AnonymousUserMixin
@ -88,7 +89,8 @@ class User(UserMixin, db.Model):
""" """
cas_last_login = db.Column(db.DateTime, nullable=True) cas_last_login = db.Column(db.DateTime, nullable=True)
"""date du dernier login via CAS""" """date du dernier login via CAS"""
edt_id = db.Column(db.Text(), index=True, nullable=True)
"identifiant emplois du temps (unicité non imposée)"
password_hash = db.Column(db.String(128)) password_hash = db.Column(db.String(128))
password_scodoc7 = db.Column(db.String(42)) password_scodoc7 = db.Column(db.String(42))
last_seen = db.Column(db.DateTime, default=datetime.utcnow) last_seen = db.Column(db.DateTime, default=datetime.utcnow)
@ -172,7 +174,8 @@ class User(UserMixin, db.Model):
return False return False
# if CAS activated and forced, allow only super-user and users with cas_allow_scodoc_login # if CAS activated and forced, allow only super-user and users with cas_allow_scodoc_login
if ScoDocSiteConfig.is_cas_enabled() and ScoDocSiteConfig.get("cas_force"): cas_enabled = ScoDocSiteConfig.is_cas_enabled()
if cas_enabled and ScoDocSiteConfig.get("cas_force"):
if (not self.is_administrator()) and not self.cas_allow_scodoc_login: if (not self.is_administrator()) and not self.cas_allow_scodoc_login:
return False return False
@ -182,7 +185,18 @@ class User(UserMixin, db.Model):
return self._migrate_scodoc7_password(password) return self._migrate_scodoc7_password(password)
return False return False
return check_password_hash(self.password_hash, password) password_ok = check_password_hash(self.password_hash, password)
if password_ok and cas_enabled and flask.session.get("CAS_EDT_ID"):
# essaie de récupérer l'edt_id s'il est présent
# cet ID peut être renvoyé par le CAS et extrait par ScoDoc
# via l'expression `cas_edt_id_from_xml_regexp`
# voir flask_cas.routing
edt_id = flask.session.get("CAS_EDT_ID")
log(f"Storing edt_id for {self.user_name}: '{edt_id}'")
self.edt_id = edt_id
db.session.add(self)
db.session.commit()
return password_ok
def _migrate_scodoc7_password(self, password) -> bool: def _migrate_scodoc7_password(self, password) -> bool:
"""After migration, rehash password.""" """After migration, rehash password."""

View File

@ -4,8 +4,8 @@
""" """
import json import json
import urllib.parse
import re import re
import urllib.parse
from flask import flash from flask import flash
from app import current_app, db, log from app import current_app, db, log
@ -13,8 +13,6 @@ from app.comp import bonus_spo
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from datetime import time
from app.scodoc.codes_cursus import ( from app.scodoc.codes_cursus import (
ABAN, ABAN,
ABL, ABL,
@ -105,6 +103,7 @@ class ScoDocSiteConfig(db.Model):
"cas_validate_route": str, "cas_validate_route": str,
"cas_attribute_id": str, "cas_attribute_id": str,
"cas_uid_from_mail_regexp": str, "cas_uid_from_mail_regexp": str,
"cas_edt_id_from_xml_regexp": str,
# Assiduité # Assiduité
"morning_time": str, "morning_time": str,
"lunch_time": str, "lunch_time": str,

View File

@ -64,6 +64,8 @@ class FormSemestre(db.Model):
titre = db.Column(db.Text(), nullable=False) titre = db.Column(db.Text(), nullable=False)
date_debut = db.Column(db.Date(), nullable=False) date_debut = db.Column(db.Date(), nullable=False)
date_fin = db.Column(db.Date(), nullable=False) date_fin = db.Column(db.Date(), nullable=False)
edt_id: str | None = db.Column(db.Text(), index=True, nullable=True)
"identifiant emplois du temps (unicité non imposée)"
etat = db.Column(db.Boolean(), nullable=False, default=True, server_default="true") etat = db.Column(db.Boolean(), nullable=False, default=True, server_default="true")
"False si verrouillé" "False si verrouillé"
modalite = db.Column( modalite = db.Column(

View File

@ -213,10 +213,12 @@ class GroupDescr(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
group_id = db.synonym("id") group_id = db.synonym("id")
partition_id = db.Column(db.Integer, db.ForeignKey("partition.id")) partition_id = db.Column(db.Integer, db.ForeignKey("partition.id"))
# "A", "C2", ... (NULL for 'all'):
group_name = db.Column(db.String(GROUPNAME_STR_LEN)) group_name = db.Column(db.String(GROUPNAME_STR_LEN))
# Numero = ordre de presentation """nom du groupe: "A", "C2", ... (NULL for 'all')"""
edt_id: str | None = db.Column(db.Text(), index=True, nullable=True)
"identifiant emplois du temps (unicité non imposée)"
numero = db.Column(db.Integer, nullable=False, default=0) numero = db.Column(db.Integer, nullable=False, default=0)
"Numero = ordre de presentation"
etuds = db.relationship( etuds = db.relationship(
"Identite", "Identite",
@ -272,6 +274,40 @@ class GroupDescr(db.Model):
return False return False
return True return True
def set_name(
self, group_name: str, edt_id: str | bool = False, dest_url: str = None
):
"""Set group name, and optionally edt_id.
Check permission and invalidate caches. Commit session.
dest_url is used for error messages.
"""
if not self.partition.formsemestre.can_change_groups():
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
if self.group_name is None:
raise ValueError("can't set a name to default group")
if group_name:
group_name = group_name.strip()
if not group_name:
raise ScoValueError("nom de groupe vide !", dest_url=dest_url)
if group_name != self.group_name and not GroupDescr.check_name(
self.partition, group_name
):
raise ScoValueError(
"Le nom de groupe existe déjà dans la partition", dest_url=dest_url
)
self.group_name = group_name
if edt_id is not False:
if isinstance(edt_id, str):
edt_id = edt_id.strip() or None
self.edt_id = edt_id
db.session.add(self)
db.session.commit()
sco_cache.invalidate_formsemestre(
formsemestre_id=self.partition.formsemestre_id
)
def remove_etud(self, etud: "Identite"): def remove_etud(self, etud: "Identite"):
"Enlève l'étudiant de ce groupe s'il en fait partie (ne fait rien sinon)" "Enlève l'étudiant de ce groupe s'il en fait partie (ne fait rien sinon)"
if etud in self.etuds: if etud in self.etuds:

View File

@ -34,8 +34,10 @@ class Module(db.Model):
# note: en APC, le semestre qui fait autorité est celui de l'UE # note: en APC, le semestre qui fait autorité est celui de l'UE
semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1") semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1")
numero = db.Column(db.Integer, nullable=False, default=0) # ordre de présentation numero = db.Column(db.Integer, nullable=False, default=0) # ordre de présentation
# id de l'element pedagogique Apogee correspondant:
code_apogee = db.Column(db.String(APO_CODE_STR_LEN)) code_apogee = db.Column(db.String(APO_CODE_STR_LEN))
"id de l'element pedagogique Apogee correspondant"
edt_id: str | None = db.Column(db.Text(), index=True, nullable=True)
"identifiant emplois du temps (unicité non imposée)"
# Type: ModuleType.STANDARD, MALUS, RESSOURCE, SAE (enum) # Type: ModuleType.STANDARD, MALUS, RESSOURCE, SAE (enum)
module_type = db.Column(db.Integer, nullable=False, default=0, server_default="0") module_type = db.Column(db.Integer, nullable=False, default=0, server_default="0")
# Relations: # Relations:

View File

@ -34,16 +34,13 @@ XXX incompatible avec les ics HyperPlanning Paris 13 (était pour GPU).
""" """
import icalendar import icalendar
import pprint
import traceback
import urllib import urllib
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app import log from app import log
from app.scodoc import html_sco_header
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_groups_view
from app.scodoc import sco_preferences from app.scodoc import sco_preferences

View File

@ -42,7 +42,7 @@ from app import cache, db, log
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre, Identite, Scolog from app.models import FormSemestre, Identite, Scolog
from app.models import GROUPNAME_STR_LEN, SHORT_STR_LEN from app.models import SHORT_STR_LEN
from app.models.groups import GroupDescr, Partition from app.models.groups import GroupDescr, Partition
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
@ -1299,85 +1299,6 @@ def partition_set_name(partition_id, partition_name, redirect=1):
) )
def group_set_name(group: GroupDescr, group_name: str, redirect=True):
"""Set group name"""
if not group.partition.formsemestre.can_change_groups():
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
if group.group_name is None:
raise ValueError("can't set a name to default group")
destination = url_for(
"scolar.affect_groups",
scodoc_dept=g.scodoc_dept,
partition_id=group.partition_id,
)
if group_name:
group_name = group_name.strip()
if not group_name:
raise ScoValueError("nom de groupe vide !", dest_url=destination)
if not GroupDescr.check_name(group.partition, group_name):
raise ScoValueError(
"Le nom de groupe existe déjà dans la partition", dest_url=destination
)
redirect = int(redirect)
group.group_name = group_name
db.session.add(group)
db.session.commit()
sco_cache.invalidate_formsemestre(formsemestre_id=group.partition.formsemestre_id)
# redirect to partition edit page:
if redirect:
return flask.redirect(destination)
def group_rename(group_id):
"""Form to rename a group"""
group = GroupDescr.query.get_or_404(group_id)
formsemestre_id = group.partition.formsemestre_id
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
if not formsemestre.can_change_groups():
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
H = [f"<h2>Renommer un groupe de {group.partition.partition_name or '-'}</h2>"]
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
(
("group_id", {"default": group_id, "input_type": "hidden"}),
(
"group_name",
{
"title": "Nouveau nom",
"default": group.group_name,
"size": 12,
"allow_null": False,
"validator": lambda val, _: len(val) < GROUPNAME_STR_LEN,
},
),
),
submitlabel="Renommer",
cancelbutton="Annuler",
)
if tf[0] == 0:
return (
html_sco_header.sco_header()
+ "\n".join(H)
+ "\n"
+ tf[1]
+ html_sco_header.sco_footer()
)
elif tf[0] == -1:
return flask.redirect(
url_for(
"scolar.affect_groups",
scodoc_dept=g.scodoc_dept,
partition_id=group.partition_id,
)
)
else:
# form submission
return group_set_name(group, tf[2]["group_name"])
def groups_auto_repartition(partition: Partition): def groups_auto_repartition(partition: Partition):
"""Réparti les etudiants dans des groupes dans une partition, en respectant le niveau """Réparti les etudiants dans des groupes dans une partition, en respectant le niveau
et la mixité. et la mixité.

View File

@ -27,11 +27,15 @@
"""Formulaires gestion des groupes """Formulaires gestion des groupes
""" """
from flask import render_template import flask
from flask import flash, g, render_template, request, url_for
from app.models import Partition from app.models import FormSemestre, GroupDescr, Partition
from app.models import GROUPNAME_STR_LEN
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc.sco_exceptions import AccessDenied from app.scodoc.sco_exceptions import AccessDenied
import app.scodoc.sco_utils as scu
from app.scodoc.TrivialFormulator import TrivialFormulator
def affect_groups(partition_id): def affect_groups(partition_id):
@ -59,3 +63,64 @@ def affect_groups(partition_id):
), ),
formsemestre_id=formsemestre.id, formsemestre_id=formsemestre.id,
) )
def group_rename(group_id):
"""Form to rename a group"""
group: GroupDescr = GroupDescr.query.get_or_404(group_id)
formsemestre_id = group.partition.formsemestre_id
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
if not formsemestre.can_change_groups():
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
H = [f"<h2>Renommer un groupe de {group.partition.partition_name or '-'}</h2>"]
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
(
("group_id", {"default": group_id, "input_type": "hidden"}),
(
"group_name",
{
"title": "Nouveau nom",
"default": group.group_name,
"size": 12,
"allow_null": False,
"validator": lambda val, _: len(val) < GROUPNAME_STR_LEN,
"explanation": "doit être unique dans cette partition",
},
),
(
"edt_id",
{
"title": "Id EDT",
"default": group.edt_id or "",
"size": 12,
"allow_null": True,
"explanation": "optionnel : identifiant du groupe dans le logiciel d'emploi du temps",
},
),
),
submitlabel="Renommer",
cancelbutton="Annuler",
)
dest_url = url_for(
"scolar.partition_editor",
scodoc_dept=g.scodoc_dept,
formsemestre_id=group.partition.formsemestre_id,
edit_partition=1,
)
if tf[0] == 0:
return (
html_sco_header.sco_header()
+ "\n".join(H)
+ "\n"
+ tf[1]
+ html_sco_header.sco_footer()
)
elif tf[0] == -1:
return flask.redirect(dest_url)
else:
# form submission
group.set_name(tf[2]["group_name"], edt_id=tf[2]["edt_id"], dest_url=dest_url)
flash("groupe modifié")
return flask.redirect(dest_url)

View File

@ -31,11 +31,8 @@
# Re-ecriture en 2014 (re-organisation de l'interface, modernisation du code) # Re-ecriture en 2014 (re-organisation de l'interface, modernisation du code)
import collections
import datetime import datetime
import urllib
from urllib.parse import parse_qs from urllib.parse import parse_qs
import time
from flask import url_for, g, request from flask import url_for, g, request
@ -45,7 +42,6 @@ from app import db
from app.models import FormSemestre from app.models import FormSemestre
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_cal
from app.scodoc import sco_excel from app.scodoc import sco_excel
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups from app.scodoc import sco_groups

View File

@ -302,6 +302,10 @@ body.editionActivated .filtres>div>div>div>div {
display: none; display: none;
} }
#zonePartitions span.editing a {
text-decoration: none;
}
.editionActivated #zonePartitions .filtres .config { .editionActivated #zonePartitions .filtres .config {
display: block; display: block;
} }

View File

@ -4834,3 +4834,7 @@ div.cas_etat_certif_ssl {
font-style: italic; font-style: italic;
color: rgb(231, 0, 0); color: rgb(231, 0, 0);
} }
.edt_id {
color: rgb(85, 255, 24);
}

View File

@ -8,7 +8,8 @@
<h2>Filtres</h2> <h2>Filtres</h2>
<div> <div>
<label class="edition"> <label class="edition">
<input type="checkbox" autocomplete="off" id="inputModif"> <input type="checkbox" autocomplete="off" id="inputModif"
{% if edit_partition %}checked{% endif %}>
Modifier les partitions et groupes Modifier les partitions et groupes
</label> </label>
<div class="filtres"></div> <div class="filtres"></div>
@ -246,15 +247,15 @@
let div = document.createElement("button"); let div = document.createElement("button");
div.classList.add("dt-button"); div.classList.add("dt-button");
div.dataset.idgroupe = groupe.id; div.dataset.idgroupe = groupe.id;
let edt_id_str = groupe.edt_id ? `<tt class="edt_id" title="id edt">[${groupe.edt_id}]</tt>` : "";
div.innerHTML = ` div.innerHTML = `
<span class="editing move">||</span> <span class="editing move">||</span>
<span>${groupe.group_name}</span> <span>${groupe.group_name} ${edt_id_str}</span>
<span class="editing modif">✏️</span> <span class="editing"><a href="/ScoDoc/{{formsemestre.departement.acronym}}/Scolarite/group_rename?group_id=${groupe.id}">✏️</a></span>
<span class="editing suppr">❌</span>`; <span class="editing suppr">❌</span>`;
div.addEventListener("click", filtre); div.addEventListener("click", filtre);
div.querySelector(".move").addEventListener("mousedown", moveStart); div.querySelector(".move").addEventListener("mousedown", moveStart);
div.querySelector(".modif").addEventListener("click", editText);
div.querySelector(".suppr").addEventListener("click", suppr); div.querySelector(".suppr").addEventListener("click", suppr);
return div; return div;

View File

@ -842,7 +842,7 @@ sco_publish("/setGroups", sco_groups.setGroups, Permission.ScoView, methods=["PO
sco_publish( sco_publish(
"/group_rename", "/group_rename",
sco_groups.group_rename, sco_groups_edit.group_rename,
Permission.ScoView, Permission.ScoView,
methods=["GET", "POST"], methods=["GET", "POST"],
) )

View File

@ -1,19 +1,21 @@
"""
Routes for CAS authentication
Modified for ScoDoc
"""
import re
import ssl import ssl
from urllib.error import URLError
from urllib.request import urlopen
import flask import flask
from xmltodict import parse
from flask import current_app from flask import current_app
from xmltodict import parse
from .cas_urls import create_cas_login_url from .cas_urls import create_cas_login_url
from .cas_urls import create_cas_logout_url from .cas_urls import create_cas_logout_url
from .cas_urls import create_cas_validate_url from .cas_urls import create_cas_validate_url
try:
from urllib import urlopen # python 2
except ImportError:
from urllib.request import urlopen # python 3
from urllib.error import URLError
blueprint = flask.Blueprint("cas", __name__) blueprint = flask.Blueprint("cas", __name__)
@ -53,7 +55,6 @@ def login():
flask.session[cas_token_session_key] = flask.request.args["ticket"] flask.session[cas_token_session_key] = flask.request.args["ticket"]
if cas_token_session_key in flask.session: if cas_token_session_key in flask.session:
if validate(flask.session[cas_token_session_key]): if validate(flask.session[cas_token_session_key]):
if "CAS_AFTER_LOGIN_SESSION_URL" in flask.session: if "CAS_AFTER_LOGIN_SESSION_URL" in flask.session:
redirect_url = flask.session.pop("CAS_AFTER_LOGIN_SESSION_URL") redirect_url = flask.session.pop("CAS_AFTER_LOGIN_SESSION_URL")
@ -64,7 +65,7 @@ def login():
else: else:
flask.session.pop(cas_token_session_key, None) flask.session.pop(cas_token_session_key, None)
current_app.logger.debug("Redirecting to: {redirect_url}") current_app.logger.debug(f"cas.login: redirecting to {redirect_url}")
return flask.redirect(redirect_url) return flask.redirect(redirect_url)
@ -84,6 +85,7 @@ def logout():
flask.session.pop(cas_username_session_key, None) flask.session.pop(cas_username_session_key, None)
flask.session.pop(cas_attributes_session_key, None) flask.session.pop(cas_attributes_session_key, None)
flask.session.pop(cas_token_session_key, None) # added by EV flask.session.pop(cas_token_session_key, None) # added by EV
flask.session.pop("CAS_EDT_ID", None) # added by EV
cas_after_logout = current_app.config["CAS_AFTER_LOGOUT"] cas_after_logout = current_app.config["CAS_AFTER_LOGOUT"]
if cas_after_logout is not None: if cas_after_logout is not None:
@ -102,7 +104,7 @@ def logout():
else: else:
redirect_url = create_cas_logout_url(current_app.config["CAS_SERVER"], None) redirect_url = create_cas_logout_url(current_app.config["CAS_SERVER"], None)
current_app.logger.debug(f"Redirecting to: {redirect_url}") current_app.logger.debug(f"cas.logout: redirecting to {redirect_url}")
return flask.redirect(redirect_url) return flask.redirect(redirect_url)
@ -114,11 +116,12 @@ def validate(ticket):
key `CAS_USERNAME_SESSION_KEY` while the validated attributes dictionary key `CAS_USERNAME_SESSION_KEY` while the validated attributes dictionary
is saved under the key 'CAS_ATTRIBUTES_SESSION_KEY'. is saved under the key 'CAS_ATTRIBUTES_SESSION_KEY'.
""" """
from app.models.config import ScoDocSiteConfig
cas_username_session_key = current_app.config["CAS_USERNAME_SESSION_KEY"] cas_username_session_key = current_app.config["CAS_USERNAME_SESSION_KEY"]
cas_attributes_session_key = current_app.config["CAS_ATTRIBUTES_SESSION_KEY"] cas_attributes_session_key = current_app.config["CAS_ATTRIBUTES_SESSION_KEY"]
cas_error_callback = current_app.config.get("CAS_ERROR_CALLBACK") cas_error_callback = current_app.config.get("CAS_ERROR_CALLBACK")
current_app.logger.debug("validating token {0}".format(ticket)) current_app.logger.debug(f"validating token {ticket}")
cas_validate_url = create_cas_validate_url( cas_validate_url = create_cas_validate_url(
current_app.config["CAS_SERVER"], current_app.config["CAS_SERVER"],
@ -182,7 +185,7 @@ def validate(ticket):
attributes = xml_from_dict.get("cas:attributes", {}) attributes = xml_from_dict.get("cas:attributes", {})
if attributes and "cas:memberOf" in attributes: if attributes and "cas:memberOf" in attributes:
if isinstance(attributes["cas:memberOf"], basestring): if isinstance(attributes["cas:memberOf"], str):
attributes["cas:memberOf"] = ( attributes["cas:memberOf"] = (
attributes["cas:memberOf"].lstrip("[").rstrip("]").split(",") attributes["cas:memberOf"].lstrip("[").rstrip("]").split(",")
) )
@ -190,6 +193,15 @@ def validate(ticket):
attributes["cas:memberOf"][group_number] = ( attributes["cas:memberOf"][group_number] = (
attributes["cas:memberOf"][group_number].lstrip(" ").rstrip(" ") attributes["cas:memberOf"][group_number].lstrip(" ").rstrip(" ")
) )
# Extract auxiliary informations (utilisé pour edt_id)
exp = ScoDocSiteConfig.get("cas_edt_id_from_xml_regexp")
if exp:
m = re.search(exp, xmldump)
if m and len(m.groups()) > 0:
cas_edt_id = m.group(1)
if cas_edt_id:
flask.session["CAS_EDT_ID"] = cas_edt_id
flask.session[cas_username_session_key] = username flask.session[cas_username_session_key] = username
flask.session[cas_attributes_session_key] = attributes flask.session[cas_attributes_session_key] = attributes
else: else:

View File

@ -0,0 +1,58 @@
"""edt_id
Revision ID: 6fb956addd69
Revises: fd805feb7ba8
Create Date: 2023-11-06 12:14:42.808476
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "6fb956addd69"
down_revision = "fd805feb7ba8"
branch_labels = None
depends_on = None
def upgrade():
with op.batch_alter_table("group_descr", schema=None) as batch_op:
batch_op.add_column(sa.Column("edt_id", sa.Text(), nullable=True))
batch_op.create_index(
batch_op.f("ix_group_descr_edt_id"), ["edt_id"], unique=False
)
with op.batch_alter_table("notes_formsemestre", schema=None) as batch_op:
batch_op.add_column(sa.Column("edt_id", sa.Text(), nullable=True))
batch_op.create_index(
batch_op.f("ix_notes_formsemestre_edt_id"), ["edt_id"], unique=False
)
with op.batch_alter_table("notes_modules", schema=None) as batch_op:
batch_op.add_column(sa.Column("edt_id", sa.Text(), nullable=True))
batch_op.create_index(
batch_op.f("ix_notes_modules_edt_id"), ["edt_id"], unique=False
)
with op.batch_alter_table("user", schema=None) as batch_op:
batch_op.add_column(sa.Column("edt_id", sa.Text(), nullable=True))
batch_op.create_index(batch_op.f("ix_user_edt_id"), ["edt_id"], unique=False)
def downgrade():
with op.batch_alter_table("user", schema=None) as batch_op:
batch_op.drop_index(batch_op.f("ix_user_edt_id"))
batch_op.drop_column("edt_id")
with op.batch_alter_table("notes_modules", schema=None) as batch_op:
batch_op.drop_index(batch_op.f("ix_notes_modules_edt_id"))
batch_op.drop_column("edt_id")
with op.batch_alter_table("notes_formsemestre", schema=None) as batch_op:
batch_op.drop_index(batch_op.f("ix_notes_formsemestre_edt_id"))
batch_op.drop_column("edt_id")
with op.batch_alter_table("group_descr", schema=None) as batch_op:
batch_op.drop_index(batch_op.f("ix_group_descr_edt_id"))
batch_op.drop_column("edt_id")