collapse epic migrations 0007–0022 → 0007_finalize_earthman_deck; add reset_staging_db
- 16 incremental Earthman tweak migrations folded into one end-state finalize migration (rename mechanisms→energies / articulations→operations / reversal→reversal_qualifier; +italic_word; suit court reversals; Schizo energies+operations; card 49 polarity reversal titles; Castanedan Virtues trumps 6–9 + 19–21; trump 8 U+2011 hyphen; trump 9 U+00A0 nbsp; pips → MINOR) - 22 epic migrations → 7; 748 ITs green - new mgmt cmd `reset_staging_db` — drops schema (Postgres) / tables (sqlite) & re-runs migrate; refuses on prod hosts; needs `--i-mean-it` when DEBUG=False; interactive host-name confirmation locally; calls ensure_superuser after Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
0
src/apps/epic/management/__init__.py
Normal file
0
src/apps/epic/management/__init__.py
Normal file
0
src/apps/epic/management/commands/__init__.py
Normal file
0
src/apps/epic/management/commands/__init__.py
Normal file
110
src/apps/epic/management/commands/reset_staging_db.py
Normal file
110
src/apps/epic/management/commands/reset_staging_db.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""Wipe the configured database and re-run all migrations from scratch.
|
||||
|
||||
Intended for ephemeral environments (staging) where losing every user, room,
|
||||
billpost, token, etc. is acceptable. Refuses to run when DEBUG=False unless
|
||||
the operator explicitly confirms with --i-mean-it, and always prints the
|
||||
DB host before doing anything destructive.
|
||||
|
||||
Typical staging usage from the deploy host:
|
||||
|
||||
docker exec -it gamearray python manage.py reset_staging_db --i-mean-it
|
||||
|
||||
Locally (sqlite, DEBUG=True) the safety prompt is skipped:
|
||||
|
||||
python src/manage.py reset_staging_db
|
||||
"""
|
||||
from django.conf import settings
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import connection
|
||||
|
||||
|
||||
PROD_HOST_FRAGMENTS = ("earthmanrpg.me",)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Drop every table in the default DB and re-run migrations. Destructive."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"--i-mean-it",
|
||||
action="store_true",
|
||||
help="Required when DEBUG=False. Bypasses the interactive confirmation.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-superuser",
|
||||
action="store_true",
|
||||
help="Skip the post-migrate ensure_superuser call.",
|
||||
)
|
||||
|
||||
def handle(self, *args, **opts):
|
||||
db_settings = settings.DATABASES["default"]
|
||||
engine = db_settings.get("ENGINE", "")
|
||||
host = db_settings.get("HOST") or db_settings.get("NAME") or "(unknown)"
|
||||
|
||||
# Refuse outright if the host name suggests production
|
||||
if any(frag in str(host) for frag in PROD_HOST_FRAGMENTS) and not host.startswith("staging"):
|
||||
if "staging" not in str(host):
|
||||
raise CommandError(
|
||||
f"Refusing to reset DB at host={host!r} — looks like production. "
|
||||
"Edit PROD_HOST_FRAGMENTS in this command if you really mean it."
|
||||
)
|
||||
|
||||
self.stdout.write(self.style.WARNING(
|
||||
f"\nAbout to wipe DB:\n ENGINE: {engine}\n HOST/NAME: {host}\n"
|
||||
))
|
||||
|
||||
if not settings.DEBUG and not opts["i_mean_it"]:
|
||||
raise CommandError(
|
||||
"DEBUG=False — pass --i-mean-it to confirm. "
|
||||
"(This is the staging-safety check; it does not bypass the prod-host refusal above.)"
|
||||
)
|
||||
|
||||
if settings.DEBUG and not opts["i_mean_it"]:
|
||||
answer = input("Type the DB host/name to confirm: ").strip()
|
||||
if answer != str(host):
|
||||
raise CommandError(f"Got {answer!r}, expected {str(host)!r}. Aborting.")
|
||||
|
||||
# Drop schema. Postgres + sqlite both honor `flush --no-input`'s
|
||||
# truncate-tables-in-place model, but for a *fresh* migration run we
|
||||
# need the migration history wiped too. For Postgres the cleanest
|
||||
# route is `DROP SCHEMA public CASCADE; CREATE SCHEMA public;` —
|
||||
# for sqlite, deleting the file is simpler but Django's connection
|
||||
# has it open. So: introspect the connection and drop all tables.
|
||||
self.stdout.write("Dropping all tables…")
|
||||
self._drop_all_tables()
|
||||
|
||||
self.stdout.write("Running migrate from scratch…")
|
||||
call_command("migrate", verbosity=1, interactive=False)
|
||||
|
||||
if not opts["no_superuser"]:
|
||||
try:
|
||||
call_command("ensure_superuser", verbosity=1)
|
||||
except Exception as exc: # pragma: no cover - depends on env vars
|
||||
self.stdout.write(self.style.WARNING(
|
||||
f"ensure_superuser skipped/failed: {exc}"
|
||||
))
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("\nDB reset complete."))
|
||||
|
||||
def _drop_all_tables(self):
|
||||
vendor = connection.vendor
|
||||
with connection.cursor() as cursor:
|
||||
if vendor == "postgresql":
|
||||
cursor.execute("DROP SCHEMA public CASCADE;")
|
||||
cursor.execute("CREATE SCHEMA public;")
|
||||
cursor.execute("GRANT ALL ON SCHEMA public TO public;")
|
||||
elif vendor == "sqlite":
|
||||
cursor.execute("PRAGMA foreign_keys = OFF;")
|
||||
cursor.execute(
|
||||
"SELECT name FROM sqlite_master "
|
||||
"WHERE type='table' AND name NOT LIKE 'sqlite_%'"
|
||||
)
|
||||
tables = [row[0] for row in cursor.fetchall()]
|
||||
for table in tables:
|
||||
cursor.execute(f'DROP TABLE IF EXISTS "{table}"')
|
||||
cursor.execute("PRAGMA foreign_keys = ON;")
|
||||
else:
|
||||
raise CommandError(
|
||||
f"reset_staging_db only knows postgresql + sqlite, got {vendor!r}"
|
||||
)
|
||||
Reference in New Issue
Block a user