forked from andika/membership-be
238 lines
12 KiB
Python
238 lines
12 KiB
Python
"""add_all_missing_columns
|
|
|
|
Revision ID: 009_add_all_missing
|
|
Revises: 008_add_donations
|
|
Create Date: 2026-01-04
|
|
|
|
Fixes:
|
|
- Add ALL remaining missing columns across all tables
|
|
- Users: newsletter preferences, volunteer, scholarship, directory, password reset, ToS, member_since, reminders, rejection, import tracking
|
|
- Events: calendar_uid
|
|
- Subscriptions: base_subscription_cents, donation_cents, manual_payment
|
|
- ImportJobs: WordPress import fields
|
|
- Create ImportRollbackAudit table if not exists
|
|
"""
|
|
from typing import Sequence, Union
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
from sqlalchemy.dialects.postgresql import UUID
|
|
from sqlalchemy import inspect
|
|
|
|
|
|
# revision identifiers, used by Alembic.
|
|
revision: str = '009_add_all_missing'
|
|
down_revision: Union[str, None] = '008_add_donations'
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
"""Add all missing columns across all tables"""
|
|
|
|
conn = op.get_bind()
|
|
inspector = inspect(conn)
|
|
|
|
# ============================================================
|
|
# 1. USERS TABLE - Add ~28 missing columns
|
|
# ============================================================
|
|
users_columns = {col['name'] for col in inspector.get_columns('users')}
|
|
|
|
# Newsletter publication preferences
|
|
if 'newsletter_publish_name' not in users_columns:
|
|
op.add_column('users', sa.Column('newsletter_publish_name', sa.Boolean(), nullable=False, server_default='false'))
|
|
if 'newsletter_publish_photo' not in users_columns:
|
|
op.add_column('users', sa.Column('newsletter_publish_photo', sa.Boolean(), nullable=False, server_default='false'))
|
|
if 'newsletter_publish_birthday' not in users_columns:
|
|
op.add_column('users', sa.Column('newsletter_publish_birthday', sa.Boolean(), nullable=False, server_default='false'))
|
|
if 'newsletter_publish_none' not in users_columns:
|
|
op.add_column('users', sa.Column('newsletter_publish_none', sa.Boolean(), nullable=False, server_default='false'))
|
|
|
|
# Volunteer interests
|
|
if 'volunteer_interests' not in users_columns:
|
|
op.add_column('users', sa.Column('volunteer_interests', sa.JSON(), nullable=True, server_default='[]'))
|
|
|
|
# Scholarship
|
|
if 'scholarship_requested' not in users_columns:
|
|
op.add_column('users', sa.Column('scholarship_requested', sa.Boolean(), nullable=False, server_default='false'))
|
|
|
|
# Directory
|
|
if 'show_in_directory' not in users_columns:
|
|
op.add_column('users', sa.Column('show_in_directory', sa.Boolean(), nullable=False, server_default='false'))
|
|
|
|
# Password reset
|
|
if 'password_reset_token' not in users_columns:
|
|
op.add_column('users', sa.Column('password_reset_token', sa.String(), nullable=True))
|
|
if 'password_reset_expires' not in users_columns:
|
|
op.add_column('users', sa.Column('password_reset_expires', sa.DateTime(), nullable=True))
|
|
if 'force_password_change' not in users_columns:
|
|
op.add_column('users', sa.Column('force_password_change', sa.Boolean(), nullable=False, server_default='false'))
|
|
|
|
# Terms of Service
|
|
if 'accepts_tos' not in users_columns:
|
|
op.add_column('users', sa.Column('accepts_tos', sa.Boolean(), nullable=False, server_default='false'))
|
|
if 'tos_accepted_at' not in users_columns:
|
|
op.add_column('users', sa.Column('tos_accepted_at', sa.DateTime(), nullable=True))
|
|
|
|
# Member since
|
|
if 'member_since' not in users_columns:
|
|
op.add_column('users', sa.Column('member_since', sa.DateTime(), nullable=True))
|
|
|
|
# Email verification reminders
|
|
if 'email_verification_reminders_sent' not in users_columns:
|
|
op.add_column('users', sa.Column('email_verification_reminders_sent', sa.Integer(), nullable=False, server_default='0'))
|
|
if 'last_email_verification_reminder_at' not in users_columns:
|
|
op.add_column('users', sa.Column('last_email_verification_reminder_at', sa.DateTime(), nullable=True))
|
|
|
|
# Event attendance reminders
|
|
if 'event_attendance_reminders_sent' not in users_columns:
|
|
op.add_column('users', sa.Column('event_attendance_reminders_sent', sa.Integer(), nullable=False, server_default='0'))
|
|
if 'last_event_attendance_reminder_at' not in users_columns:
|
|
op.add_column('users', sa.Column('last_event_attendance_reminder_at', sa.DateTime(), nullable=True))
|
|
|
|
# Payment reminders
|
|
if 'payment_reminders_sent' not in users_columns:
|
|
op.add_column('users', sa.Column('payment_reminders_sent', sa.Integer(), nullable=False, server_default='0'))
|
|
if 'last_payment_reminder_at' not in users_columns:
|
|
op.add_column('users', sa.Column('last_payment_reminder_at', sa.DateTime(), nullable=True))
|
|
|
|
# Renewal reminders
|
|
if 'renewal_reminders_sent' not in users_columns:
|
|
op.add_column('users', sa.Column('renewal_reminders_sent', sa.Integer(), nullable=False, server_default='0'))
|
|
if 'last_renewal_reminder_at' not in users_columns:
|
|
op.add_column('users', sa.Column('last_renewal_reminder_at', sa.DateTime(), nullable=True))
|
|
|
|
# Rejection tracking
|
|
if 'rejection_reason' not in users_columns:
|
|
op.add_column('users', sa.Column('rejection_reason', sa.Text(), nullable=True))
|
|
if 'rejected_at' not in users_columns:
|
|
op.add_column('users', sa.Column('rejected_at', sa.DateTime(timezone=True), nullable=True))
|
|
if 'rejected_by' not in users_columns:
|
|
op.add_column('users', sa.Column('rejected_by', UUID(as_uuid=True), nullable=True))
|
|
# Note: Foreign key constraint skipped to avoid circular dependency issues
|
|
|
|
# WordPress import tracking
|
|
if 'import_source' not in users_columns:
|
|
op.add_column('users', sa.Column('import_source', sa.String(50), nullable=True))
|
|
if 'import_job_id' not in users_columns:
|
|
op.add_column('users', sa.Column('import_job_id', UUID(as_uuid=True), nullable=True))
|
|
# Note: Foreign key will be added after import_jobs table is updated
|
|
if 'wordpress_user_id' not in users_columns:
|
|
op.add_column('users', sa.Column('wordpress_user_id', sa.BigInteger(), nullable=True))
|
|
if 'wordpress_registered_date' not in users_columns:
|
|
op.add_column('users', sa.Column('wordpress_registered_date', sa.DateTime(timezone=True), nullable=True))
|
|
|
|
# ============================================================
|
|
# 2. EVENTS TABLE - Add calendar_uid
|
|
# ============================================================
|
|
events_columns = {col['name'] for col in inspector.get_columns('events')}
|
|
|
|
if 'calendar_uid' not in events_columns:
|
|
op.add_column('events', sa.Column('calendar_uid', sa.String(), nullable=True))
|
|
|
|
# ============================================================
|
|
# 3. SUBSCRIPTIONS TABLE - Add donation tracking
|
|
# ============================================================
|
|
subscriptions_columns = {col['name'] for col in inspector.get_columns('subscriptions')}
|
|
|
|
if 'base_subscription_cents' not in subscriptions_columns:
|
|
op.add_column('subscriptions', sa.Column('base_subscription_cents', sa.Integer(), nullable=True))
|
|
# Update existing rows: base_subscription_cents = amount_paid_cents - donation_cents (default 0)
|
|
op.execute("UPDATE subscriptions SET base_subscription_cents = COALESCE(amount_paid_cents, 0) WHERE base_subscription_cents IS NULL")
|
|
# Make it non-nullable after populating
|
|
op.alter_column('subscriptions', 'base_subscription_cents', nullable=False)
|
|
|
|
if 'donation_cents' not in subscriptions_columns:
|
|
op.add_column('subscriptions', sa.Column('donation_cents', sa.Integer(), nullable=False, server_default='0'))
|
|
|
|
if 'manual_payment' not in subscriptions_columns:
|
|
op.add_column('subscriptions', sa.Column('manual_payment', sa.Boolean(), nullable=False, server_default='false'))
|
|
|
|
# ============================================================
|
|
# 4. IMPORT_JOBS TABLE - Add WordPress import fields
|
|
# ============================================================
|
|
import_jobs_columns = {col['name'] for col in inspector.get_columns('import_jobs')}
|
|
|
|
if 'field_mapping' not in import_jobs_columns:
|
|
op.add_column('import_jobs', sa.Column('field_mapping', sa.JSON(), nullable=False, server_default='{}'))
|
|
|
|
if 'wordpress_metadata' not in import_jobs_columns:
|
|
op.add_column('import_jobs', sa.Column('wordpress_metadata', sa.JSON(), nullable=False, server_default='{}'))
|
|
|
|
if 'imported_user_ids' not in import_jobs_columns:
|
|
op.add_column('import_jobs', sa.Column('imported_user_ids', sa.JSON(), nullable=False, server_default='[]'))
|
|
|
|
if 'rollback_at' not in import_jobs_columns:
|
|
op.add_column('import_jobs', sa.Column('rollback_at', sa.DateTime(), nullable=True))
|
|
|
|
if 'rollback_by' not in import_jobs_columns:
|
|
op.add_column('import_jobs', sa.Column('rollback_by', UUID(as_uuid=True), nullable=True))
|
|
# Foreign key will be added if needed
|
|
|
|
# ============================================================
|
|
# 5. CREATE IMPORT_ROLLBACK_AUDIT TABLE
|
|
# ============================================================
|
|
if 'import_rollback_audit' not in inspector.get_table_names():
|
|
op.create_table(
|
|
'import_rollback_audit',
|
|
sa.Column('id', UUID(as_uuid=True), primary_key=True),
|
|
sa.Column('import_job_id', UUID(as_uuid=True), sa.ForeignKey('import_jobs.id'), nullable=False),
|
|
sa.Column('rolled_back_by', UUID(as_uuid=True), sa.ForeignKey('users.id'), nullable=False),
|
|
sa.Column('rolled_back_at', sa.DateTime(), nullable=False),
|
|
sa.Column('deleted_user_count', sa.Integer(), nullable=False),
|
|
sa.Column('deleted_user_ids', sa.JSON(), nullable=False),
|
|
sa.Column('reason', sa.Text(), nullable=True),
|
|
sa.Column('created_at', sa.DateTime(), nullable=False)
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
"""Remove all added columns and tables"""
|
|
|
|
# Drop import_rollback_audit table
|
|
op.drop_table('import_rollback_audit')
|
|
|
|
# Drop import_jobs columns
|
|
op.drop_column('import_jobs', 'rollback_by')
|
|
op.drop_column('import_jobs', 'rollback_at')
|
|
op.drop_column('import_jobs', 'imported_user_ids')
|
|
op.drop_column('import_jobs', 'wordpress_metadata')
|
|
op.drop_column('import_jobs', 'field_mapping')
|
|
|
|
# Drop subscriptions columns
|
|
op.drop_column('subscriptions', 'manual_payment')
|
|
op.drop_column('subscriptions', 'donation_cents')
|
|
op.drop_column('subscriptions', 'base_subscription_cents')
|
|
|
|
# Drop events columns
|
|
op.drop_column('events', 'calendar_uid')
|
|
|
|
# Drop users columns (in reverse order)
|
|
op.drop_column('users', 'wordpress_registered_date')
|
|
op.drop_column('users', 'wordpress_user_id')
|
|
op.drop_column('users', 'import_job_id')
|
|
op.drop_column('users', 'import_source')
|
|
op.drop_column('users', 'rejected_by')
|
|
op.drop_column('users', 'rejected_at')
|
|
op.drop_column('users', 'rejection_reason')
|
|
op.drop_column('users', 'last_renewal_reminder_at')
|
|
op.drop_column('users', 'renewal_reminders_sent')
|
|
op.drop_column('users', 'last_payment_reminder_at')
|
|
op.drop_column('users', 'payment_reminders_sent')
|
|
op.drop_column('users', 'last_event_attendance_reminder_at')
|
|
op.drop_column('users', 'event_attendance_reminders_sent')
|
|
op.drop_column('users', 'last_email_verification_reminder_at')
|
|
op.drop_column('users', 'email_verification_reminders_sent')
|
|
op.drop_column('users', 'member_since')
|
|
op.drop_column('users', 'tos_accepted_at')
|
|
op.drop_column('users', 'accepts_tos')
|
|
op.drop_column('users', 'force_password_change')
|
|
op.drop_column('users', 'password_reset_expires')
|
|
op.drop_column('users', 'password_reset_token')
|
|
op.drop_column('users', 'show_in_directory')
|
|
op.drop_column('users', 'scholarship_requested')
|
|
op.drop_column('users', 'volunteer_interests')
|
|
op.drop_column('users', 'newsletter_publish_none')
|
|
op.drop_column('users', 'newsletter_publish_birthday')
|
|
op.drop_column('users', 'newsletter_publish_photo')
|
|
op.drop_column('users', 'newsletter_publish_name')
|