Jun 28 - Fix issue with missing table - Tenant branding error
This commit is contained in:
@@ -7,11 +7,14 @@ One row per tenant DB. Created on first save; reads fall back to defaults
|
|||||||
when no row exists so tenant-zero needs zero data migration.
|
when no row exists so tenant-zero needs zero data migration.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
import sqlalchemy.exc
|
import sqlalchemy.exc
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.utils.time_utils import now_eastern
|
from app.utils.time_utils import now_eastern
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class TenantSettings(db.Model):
|
class TenantSettings(db.Model):
|
||||||
__tablename__ = 'tenant_settings'
|
__tablename__ = 'tenant_settings'
|
||||||
@@ -46,9 +49,11 @@ class TenantSettings(db.Model):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
row = cls.query.first()
|
row = cls.query.first()
|
||||||
except (sqlalchemy.exc.ProgrammingError, sqlalchemy.exc.OperationalError):
|
except (sqlalchemy.exc.ProgrammingError, sqlalchemy.exc.OperationalError) as exc:
|
||||||
# Table 'tenant_settings' doesn't exist — phase33 migration not yet applied.
|
# Table 'tenant_settings' doesn't exist — phase33 migration not yet applied.
|
||||||
# Any other DB error (connection failure, permission error) is re-raised.
|
# Any other DB error (connection failure, permission error) is re-raised.
|
||||||
|
logger.warning('TenantSettings.get_or_default: table missing — %s: %s',
|
||||||
|
type(exc).__name__, exc)
|
||||||
return None
|
return None
|
||||||
if row is not None:
|
if row is not None:
|
||||||
return row
|
return row
|
||||||
|
|||||||
@@ -247,6 +247,33 @@ def _cmd_heads(_args):
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _cmd_stamp(args):
|
||||||
|
"""Stamp tenant DB(s) to a specific revision without running migrations.
|
||||||
|
|
||||||
|
Use this to rewind a mistakenly-stamped tenant so a subsequent `upgrade`
|
||||||
|
will actually replay the missing migration. Example:
|
||||||
|
|
||||||
|
python -m control.tenant_migrate stamp --tenant gov phase32_device_token_columns
|
||||||
|
python -m control.tenant_migrate upgrade --tenant gov
|
||||||
|
"""
|
||||||
|
tenants = _select_tenants(args.tenant)
|
||||||
|
if not tenants:
|
||||||
|
print(f'No matching tenants for --tenant {args.tenant}.')
|
||||||
|
return 0
|
||||||
|
failures = 0
|
||||||
|
for t in tenants:
|
||||||
|
try:
|
||||||
|
cfg = _make_config(t.db_uri)
|
||||||
|
with contextlib.redirect_stdout(io.StringIO()):
|
||||||
|
command.stamp(cfg, args.revision)
|
||||||
|
rev = current_revision(t.db_uri)
|
||||||
|
print(f' [ok] {t.slug} (id={t.id}) stamped -> {rev}')
|
||||||
|
except Exception as e:
|
||||||
|
failures += 1
|
||||||
|
print(f' [FAIL] {t.slug} (id={t.id}): {type(e).__name__}: {e}')
|
||||||
|
return 1 if failures else 0
|
||||||
|
|
||||||
|
|
||||||
def main(argv=None):
|
def main(argv=None):
|
||||||
parser = argparse.ArgumentParser(prog='control.tenant_migrate',
|
parser = argparse.ArgumentParser(prog='control.tenant_migrate',
|
||||||
description='Per-tenant migration runner')
|
description='Per-tenant migration runner')
|
||||||
@@ -265,6 +292,11 @@ def main(argv=None):
|
|||||||
p_boot.add_argument('--tenant', required=True, help="'all', or a tenant id or slug")
|
p_boot.add_argument('--tenant', required=True, help="'all', or a tenant id or slug")
|
||||||
p_boot.set_defaults(func=_cmd_bootstrap)
|
p_boot.set_defaults(func=_cmd_bootstrap)
|
||||||
|
|
||||||
|
p_stamp = sub.add_parser('stamp', help='Stamp tenant DB(s) to a specific revision (no migrations run)')
|
||||||
|
p_stamp.add_argument('--tenant', required=True, help="'all', or a tenant id or slug")
|
||||||
|
p_stamp.add_argument('revision', help='Alembic revision id (e.g. phase32_device_token_columns)')
|
||||||
|
p_stamp.set_defaults(func=_cmd_stamp)
|
||||||
|
|
||||||
sub.add_parser('heads', help='Show the chain head revision').set_defaults(func=_cmd_heads)
|
sub.add_parser('heads', help='Show the chain head revision').set_defaults(func=_cmd_heads)
|
||||||
|
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
|
|||||||
@@ -469,10 +469,26 @@ def upgrade():
|
|||||||
if not _index_exists(bind, 'notifications', 'ix_notifications_user_id'):
|
if not _index_exists(bind, 'notifications', 'ix_notifications_user_id'):
|
||||||
op.execute(sa.text("CREATE INDEX ix_notifications_user_id ON notifications (user_id)"))
|
op.execute(sa.text("CREATE INDEX ix_notifications_user_id ON notifications (user_id)"))
|
||||||
|
|
||||||
|
# phase33 — per-tenant branding settings
|
||||||
|
if not _table_exists(bind, 'tenant_settings'):
|
||||||
|
op.execute(sa.text("""CREATE TABLE tenant_settings (
|
||||||
|
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
company_name VARCHAR(150) NULL,
|
||||||
|
logo_url VARCHAR(500) NULL,
|
||||||
|
primary_color VARCHAR(7) NOT NULL DEFAULT '#1a56db',
|
||||||
|
accent_color VARCHAR(7) NOT NULL DEFAULT '#16a34a',
|
||||||
|
support_email VARCHAR(255) NULL,
|
||||||
|
updated_at DATETIME NOT NULL,
|
||||||
|
updated_by INT NULL,
|
||||||
|
CONSTRAINT fk_tenant_settings_user
|
||||||
|
FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"""))
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade():
|
||||||
bind = op.get_bind()
|
bind = op.get_bind()
|
||||||
|
if _table_exists(bind, 'tenant_settings'):
|
||||||
|
op.execute(sa.text('DROP TABLE tenant_settings'))
|
||||||
if _table_exists(bind, 'notifications'):
|
if _table_exists(bind, 'notifications'):
|
||||||
op.execute(sa.text('DROP TABLE notifications'))
|
op.execute(sa.text('DROP TABLE notifications'))
|
||||||
if _table_exists(bind, 'issue_followers'):
|
if _table_exists(bind, 'issue_followers'):
|
||||||
|
|||||||
Reference in New Issue
Block a user