collapse epic migrations 0007–0022 → 0007_finalize_earthman_deck; add reset_staging_db
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed

- 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:
Disco DeDisco
2026-05-01 02:22:43 -04:00
parent 3410f073f0
commit 8b0ad545c9
20 changed files with 310 additions and 773 deletions

View File

View 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}"
)