This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
"""Migration: add the `is_team_representative` flag to `team_players`.
|
||||
|
||||
A team's responsible ("joueur responsable") is a player an admin names to
|
||||
manage that team's matches and practices. The flag lives on the roster row,
|
||||
beside `status` and `position`.
|
||||
|
||||
`db.create_all()` creates missing tables and never alters an existing one, so
|
||||
a database that already has `team_players` needs this script. Fresh installs
|
||||
get the column from the model.
|
||||
|
||||
Where it connects
|
||||
-----------------
|
||||
The script finds its own database, so there is no URL to remember:
|
||||
|
||||
1. If `DATABASE_URL` is already set, it is used and nothing else is touched.
|
||||
That is the production and CI path.
|
||||
2. Otherwise the local values from `app/.env` are used. If the development
|
||||
server in `instance/pgdata/` answers a connection it is reused as-is; if it
|
||||
does not, it is (re)started and stopped again when this script exits. This
|
||||
is the `LOCAL_PG=true` block of `app/.env.example`.
|
||||
3. If neither is available the script explains what to set and stops.
|
||||
|
||||
So a laptop with the `LOCAL_PG=true` block runs the migration with a single
|
||||
command:
|
||||
|
||||
python migrations/add_team_representative.py
|
||||
|
||||
Usage: python migrations/add_team_representative.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, PROJECT_ROOT)
|
||||
|
||||
# Loaded before anything else reads the environment: app.local_postgres reads
|
||||
# LOCAL_PG_PORT / LOCAL_PG_USER / LOCAL_PG_DATABASE at import time, and without
|
||||
# this they would fall back to that module's defaults instead of app/.env.
|
||||
from dotenv import load_dotenv
|
||||
|
||||
_ENV_FILE = os.path.join(PROJECT_ROOT, 'app', '.env')
|
||||
load_dotenv(_ENV_FILE if os.path.exists(_ENV_FILE) else None)
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.extensions import db
|
||||
|
||||
|
||||
def _local_database():
|
||||
"""The development URL and port, read from the LOCAL_PG_* variables."""
|
||||
from app.local_postgres import DATABASE_URL, PORT
|
||||
|
||||
return DATABASE_URL, PORT
|
||||
|
||||
|
||||
def _postgres_answers(url):
|
||||
"""Whether `url` is a working PostgreSQL connection right now.
|
||||
|
||||
A bare TCP probe is not enough here: a server killed part-way through its
|
||||
startup can leave the port listening while every connection is refused,
|
||||
and trusting that evidence fails the migration for a reason that looks
|
||||
nothing like its cause.
|
||||
"""
|
||||
from sqlalchemy import create_engine
|
||||
|
||||
from app.app import normalise_database_url
|
||||
|
||||
try:
|
||||
engine = create_engine(
|
||||
normalise_database_url(url), connect_args={'connect_timeout': 2}
|
||||
)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
try:
|
||||
with engine.connect():
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
finally:
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def _stop_local_server():
|
||||
"""Fast-stop the local development server, if a postmaster is alive.
|
||||
|
||||
Called only before (re)starting it: `pg_ctl status` calls a half-dead
|
||||
postmaster "running", so `start()` would leave the broken one in place.
|
||||
"""
|
||||
from app.local_postgres import DATADIR, _find_binary
|
||||
|
||||
pg_ctl = _find_binary('pg_ctl')
|
||||
if not pg_ctl:
|
||||
return
|
||||
subprocess.run(
|
||||
[str(pg_ctl), 'stop', '-m', 'fast', '-D', str(DATADIR)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
|
||||
def resolve_database_url():
|
||||
"""Pick the database to migrate.
|
||||
|
||||
Returns:
|
||||
tuple[str, str]: the connection URL and a note saying where it came
|
||||
from, for the log line.
|
||||
|
||||
Raises:
|
||||
SystemExit: when neither a URL nor a usable local server is available.
|
||||
"""
|
||||
explicit = os.environ.get('DATABASE_URL')
|
||||
if explicit:
|
||||
return explicit, 'DATABASE_URL'
|
||||
|
||||
url, port = _local_database()
|
||||
if _postgres_answers(url):
|
||||
return url, f'local PostgreSQL already running on port {port}'
|
||||
|
||||
if os.getenv('LOCAL_PG', '').strip().lower() == 'true':
|
||||
from app.local_postgres import start
|
||||
|
||||
_stop_local_server()
|
||||
return start(), f'local PostgreSQL started on port {port}'
|
||||
|
||||
raise SystemExit(
|
||||
'No database to migrate.\n'
|
||||
' Set DATABASE_URL to a connection string, or set LOCAL_PG=true in\n'
|
||||
' app/.env to use the local development server (see app/.env.example,\n'
|
||||
' DEVELOPMENT block).'
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
# A migration must never start the Discord bot: two instances fight over
|
||||
# the same gateway session, and neither belongs to a script whose only job
|
||||
# is to run an ALTER.
|
||||
os.environ['ENABLE_DISCORD_BOT'] = 'false'
|
||||
|
||||
url, source = resolve_database_url()
|
||||
os.environ['DATABASE_URL'] = url
|
||||
|
||||
# Imported here, after the environment is settled, so create_app() reads
|
||||
# the URL this script resolved rather than a missing one.
|
||||
from app.app import create_app
|
||||
|
||||
print(f'[INFO] Database: {source}.')
|
||||
|
||||
app = create_app()
|
||||
with app.app_context():
|
||||
already_there = db.session.execute(
|
||||
text(
|
||||
"SELECT 1 FROM information_schema.columns "
|
||||
"WHERE table_name = 'team_players' "
|
||||
"AND column_name = 'is_team_representative'"
|
||||
)
|
||||
).scalar()
|
||||
|
||||
if already_there:
|
||||
print("Column 'team_players.is_team_representative' already exists. Skipping.")
|
||||
return
|
||||
|
||||
db.session.execute(
|
||||
text(
|
||||
"ALTER TABLE team_players "
|
||||
"ADD COLUMN is_team_representative BOOLEAN NOT NULL DEFAULT FALSE"
|
||||
)
|
||||
)
|
||||
db.session.commit()
|
||||
print("Added 'team_players.is_team_representative'.")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user