diff --git a/alembic/versions/002_add_missing_user_fields.py b/alembic/versions/002_add_missing_user_fields.py index 0e9b2bc..f04aeef 100644 --- a/alembic/versions/002_add_missing_user_fields.py +++ b/alembic/versions/002_add_missing_user_fields.py @@ -24,31 +24,48 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: - """Add missing user fields""" + """Add missing user fields (skip if already exists)""" + from sqlalchemy import inspect + + conn = op.get_bind() + inspector = inspect(conn) + existing_columns = {col['name'] for col in inspector.get_columns('users')} # Add scholarship_reason - op.add_column('users', sa.Column('scholarship_reason', sa.Text(), nullable=True)) + if 'scholarship_reason' not in existing_columns: + op.add_column('users', sa.Column('scholarship_reason', sa.Text(), nullable=True)) # Add directory fields - op.add_column('users', sa.Column('directory_email', sa.String(), nullable=True)) - op.add_column('users', sa.Column('directory_bio', sa.Text(), nullable=True)) - op.add_column('users', sa.Column('directory_address', sa.String(), nullable=True)) - op.add_column('users', sa.Column('directory_phone', sa.String(), nullable=True)) - op.add_column('users', sa.Column('directory_dob', sa.DateTime(), nullable=True)) - op.add_column('users', sa.Column('directory_partner_name', sa.String(), nullable=True)) + if 'directory_email' not in existing_columns: + op.add_column('users', sa.Column('directory_email', sa.String(), nullable=True)) + if 'directory_bio' not in existing_columns: + op.add_column('users', sa.Column('directory_bio', sa.Text(), nullable=True)) + if 'directory_address' not in existing_columns: + op.add_column('users', sa.Column('directory_address', sa.String(), nullable=True)) + if 'directory_phone' not in existing_columns: + op.add_column('users', sa.Column('directory_phone', sa.String(), nullable=True)) + if 'directory_dob' not in existing_columns: + op.add_column('users', sa.Column('directory_dob', sa.DateTime(), nullable=True)) + if 'directory_partner_name' not in existing_columns: + op.add_column('users', sa.Column('directory_partner_name', sa.String(), nullable=True)) - # Rename profile_image_url to profile_photo_url (for consistency with models.py) - op.alter_column('users', 'profile_image_url', new_column_name='profile_photo_url') + # Rename profile_image_url to profile_photo_url (skip if already renamed) + if 'profile_image_url' in existing_columns and 'profile_photo_url' not in existing_columns: + op.alter_column('users', 'profile_image_url', new_column_name='profile_photo_url') # Add social media fields - op.add_column('users', sa.Column('social_media_facebook', sa.String(), nullable=True)) - op.add_column('users', sa.Column('social_media_instagram', sa.String(), nullable=True)) - op.add_column('users', sa.Column('social_media_twitter', sa.String(), nullable=True)) - op.add_column('users', sa.Column('social_media_linkedin', sa.String(), nullable=True)) + if 'social_media_facebook' not in existing_columns: + op.add_column('users', sa.Column('social_media_facebook', sa.String(), nullable=True)) + if 'social_media_instagram' not in existing_columns: + op.add_column('users', sa.Column('social_media_instagram', sa.String(), nullable=True)) + if 'social_media_twitter' not in existing_columns: + op.add_column('users', sa.Column('social_media_twitter', sa.String(), nullable=True)) + if 'social_media_linkedin' not in existing_columns: + op.add_column('users', sa.Column('social_media_linkedin', sa.String(), nullable=True)) - # Add email_verification_expires (exists in DB but not in models.py initially) - # Check if it already exists, if not add it - # This field should already exist from the initial schema, but adding for completeness + # Add email_verification_expires if missing + if 'email_verification_expires' not in existing_columns: + op.add_column('users', sa.Column('email_verification_expires', sa.DateTime(), nullable=True)) def downgrade() -> None: diff --git a/alembic/versions/003_add_user_invitation_fields.py b/alembic/versions/003_add_user_invitation_fields.py index 06a0ad2..4c91a11 100644 --- a/alembic/versions/003_add_user_invitation_fields.py +++ b/alembic/versions/003_add_user_invitation_fields.py @@ -22,11 +22,24 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: - """Add optional pre-filled information fields to user_invitations""" + """Add optional pre-filled information fields to user_invitations (skip if already exists)""" + from sqlalchemy import inspect - op.add_column('user_invitations', sa.Column('first_name', sa.String(), nullable=True)) - op.add_column('user_invitations', sa.Column('last_name', sa.String(), nullable=True)) - op.add_column('user_invitations', sa.Column('phone', sa.String(), nullable=True)) + conn = op.get_bind() + inspector = inspect(conn) + existing_columns = {col['name'] for col in inspector.get_columns('user_invitations')} + + # Add first_name if missing + if 'first_name' not in existing_columns: + op.add_column('user_invitations', sa.Column('first_name', sa.String(), nullable=True)) + + # Add last_name if missing + if 'last_name' not in existing_columns: + op.add_column('user_invitations', sa.Column('last_name', sa.String(), nullable=True)) + + # Add phone if missing + if 'phone' not in existing_columns: + op.add_column('user_invitations', sa.Column('phone', sa.String(), nullable=True)) def downgrade() -> None: diff --git a/alembic/versions/004_add_document_file_sizes.py b/alembic/versions/004_add_document_file_sizes.py index ce5f27f..dbebb09 100644 --- a/alembic/versions/004_add_document_file_sizes.py +++ b/alembic/versions/004_add_document_file_sizes.py @@ -22,16 +22,26 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: - """Add file_size_bytes column to document tables""" + """Add file_size_bytes column to document tables (skip if already exists)""" + from sqlalchemy import inspect - # Add to newsletter_archives - op.add_column('newsletter_archives', sa.Column('file_size_bytes', sa.Integer(), nullable=True)) + conn = op.get_bind() + inspector = inspect(conn) - # Add to financial_reports - op.add_column('financial_reports', sa.Column('file_size_bytes', sa.Integer(), nullable=True)) + # Add to newsletter_archives if missing + existing_columns = {col['name'] for col in inspector.get_columns('newsletter_archives')} + if 'file_size_bytes' not in existing_columns: + op.add_column('newsletter_archives', sa.Column('file_size_bytes', sa.Integer(), nullable=True)) - # Add to bylaws_documents - op.add_column('bylaws_documents', sa.Column('file_size_bytes', sa.Integer(), nullable=True)) + # Add to financial_reports if missing + existing_columns = {col['name'] for col in inspector.get_columns('financial_reports')} + if 'file_size_bytes' not in existing_columns: + op.add_column('financial_reports', sa.Column('file_size_bytes', sa.Integer(), nullable=True)) + + # Add to bylaws_documents if missing + existing_columns = {col['name'] for col in inspector.get_columns('bylaws_documents')} + if 'file_size_bytes' not in existing_columns: + op.add_column('bylaws_documents', sa.Column('file_size_bytes', sa.Integer(), nullable=True)) def downgrade() -> None: diff --git a/alembic/versions/005_fix_subscriptions_and_storage.py b/alembic/versions/005_fix_subscriptions_and_storage.py index 30d5b24..19cef7b 100644 --- a/alembic/versions/005_fix_subscriptions_and_storage.py +++ b/alembic/versions/005_fix_subscriptions_and_storage.py @@ -22,26 +22,44 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: - """Add missing columns and fix naming""" + """Add missing columns and fix naming (skip if already exists)""" + from sqlalchemy import inspect - # Add missing columns to subscriptions table - op.add_column('subscriptions', sa.Column('start_date', sa.DateTime(timezone=True), nullable=True)) - op.add_column('subscriptions', sa.Column('end_date', sa.DateTime(timezone=True), nullable=True)) - op.add_column('subscriptions', sa.Column('amount_paid_cents', sa.Integer(), nullable=True)) - op.add_column('subscriptions', sa.Column('manual_payment_notes', sa.Text(), nullable=True)) - op.add_column('subscriptions', sa.Column('manual_payment_admin_id', UUID(as_uuid=True), nullable=True)) - op.add_column('subscriptions', sa.Column('manual_payment_date', sa.DateTime(timezone=True), nullable=True)) - op.add_column('subscriptions', sa.Column('payment_method', sa.String(50), nullable=True)) + conn = op.get_bind() + inspector = inspect(conn) - # Add foreign key for manual_payment_admin_id - op.create_foreign_key( - 'subscriptions_manual_payment_admin_id_fkey', - 'subscriptions', 'users', - ['manual_payment_admin_id'], ['id'] - ) + # Check existing columns in subscriptions table + existing_columns = {col['name'] for col in inspector.get_columns('subscriptions')} - # Rename storage_usage.last_calculated_at to last_updated - op.alter_column('storage_usage', 'last_calculated_at', new_column_name='last_updated') + # Add missing columns to subscriptions table only if they don't exist + if 'start_date' not in existing_columns: + op.add_column('subscriptions', sa.Column('start_date', sa.DateTime(timezone=True), nullable=True)) + if 'end_date' not in existing_columns: + op.add_column('subscriptions', sa.Column('end_date', sa.DateTime(timezone=True), nullable=True)) + if 'amount_paid_cents' not in existing_columns: + op.add_column('subscriptions', sa.Column('amount_paid_cents', sa.Integer(), nullable=True)) + if 'manual_payment_notes' not in existing_columns: + op.add_column('subscriptions', sa.Column('manual_payment_notes', sa.Text(), nullable=True)) + if 'manual_payment_admin_id' not in existing_columns: + op.add_column('subscriptions', sa.Column('manual_payment_admin_id', UUID(as_uuid=True), nullable=True)) + if 'manual_payment_date' not in existing_columns: + op.add_column('subscriptions', sa.Column('manual_payment_date', sa.DateTime(timezone=True), nullable=True)) + if 'payment_method' not in existing_columns: + op.add_column('subscriptions', sa.Column('payment_method', sa.String(50), nullable=True)) + + # Add foreign key for manual_payment_admin_id if it doesn't exist + existing_fks = [fk['name'] for fk in inspector.get_foreign_keys('subscriptions')] + if 'subscriptions_manual_payment_admin_id_fkey' not in existing_fks: + op.create_foreign_key( + 'subscriptions_manual_payment_admin_id_fkey', + 'subscriptions', 'users', + ['manual_payment_admin_id'], ['id'] + ) + + # Rename storage_usage.last_calculated_at to last_updated (only if needed) + storage_columns = {col['name'] for col in inspector.get_columns('storage_usage')} + if 'last_calculated_at' in storage_columns and 'last_updated' not in storage_columns: + op.alter_column('storage_usage', 'last_calculated_at', new_column_name='last_updated') def downgrade() -> None: diff --git a/alembic/versions/006_rename_is_active.py b/alembic/versions/006_rename_is_active.py index dd90a9a..77e669f 100644 --- a/alembic/versions/006_rename_is_active.py +++ b/alembic/versions/006_rename_is_active.py @@ -20,8 +20,16 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: - """Rename is_active to active""" - op.alter_column('subscription_plans', 'is_active', new_column_name='active') + """Rename is_active to active (skip if already renamed)""" + from sqlalchemy import inspect + + conn = op.get_bind() + inspector = inspect(conn) + + # Check if rename is needed + existing_columns = {col['name'] for col in inspector.get_columns('subscription_plans')} + if 'is_active' in existing_columns and 'active' not in existing_columns: + op.alter_column('subscription_plans', 'is_active', new_column_name='active') def downgrade() -> None: diff --git a/alembic/versions/010_add_email_verification_expires.py b/alembic/versions/010_add_email_verification_expires.py new file mode 100644 index 0000000..a484e4b --- /dev/null +++ b/alembic/versions/010_add_email_verification_expires.py @@ -0,0 +1,37 @@ +"""add_email_verification_expires + +Revision ID: 010_add_email_exp +Revises: 009_add_all_missing +Create Date: 2026-01-05 + +Fixes: +- Add missing email_verification_expires column to users table +""" +from typing import Sequence, Union +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '010_add_email_exp' +down_revision: Union[str, None] = '009_add_all_missing' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Add email_verification_expires column (skip if already exists)""" + from sqlalchemy import inspect + + conn = op.get_bind() + inspector = inspect(conn) + existing_columns = {col['name'] for col in inspector.get_columns('users')} + + # Add email_verification_expires if missing + if 'email_verification_expires' not in existing_columns: + op.add_column('users', sa.Column('email_verification_expires', sa.DateTime(), nullable=True)) + + +def downgrade() -> None: + """Remove email_verification_expires column""" + op.drop_column('users', 'email_verification_expires') diff --git a/server.py b/server.py index 9381b59..b170483 100644 --- a/server.py +++ b/server.py @@ -2814,8 +2814,9 @@ async def verify_invitation_token( if not invitation: raise HTTPException(status_code=404, detail="Invalid or expired invitation token") - # Check expiry - if invitation.expires_at < datetime.now(timezone.utc): + # Check expiry (handle timezone-naive datetime from DB) + expires_at_aware = invitation.expires_at.replace(tzinfo=timezone.utc) if invitation.expires_at.tzinfo is None else invitation.expires_at + if expires_at_aware < datetime.now(timezone.utc): invitation.status = InvitationStatus.expired db.commit() raise HTTPException(status_code=400, detail="Invitation has expired") @@ -2847,8 +2848,9 @@ async def accept_invitation( if not invitation: raise HTTPException(status_code=404, detail="Invalid or expired invitation token") - # Check expiry - if invitation.expires_at < datetime.now(timezone.utc): + # Check expiry (handle timezone-naive datetime from DB) + expires_at_aware = invitation.expires_at.replace(tzinfo=timezone.utc) if invitation.expires_at.tzinfo is None else invitation.expires_at + if expires_at_aware < datetime.now(timezone.utc): invitation.status = InvitationStatus.expired db.commit() raise HTTPException(status_code=400, detail="Invitation has expired")