diff --git a/.pythonstartup.py b/.pythonstartup.py index 6cbbea964..cc846f946 100644 --- a/.pythonstartup.py +++ b/.pythonstartup.py @@ -11,7 +11,7 @@ initialize_db() # Make it easier to do session stuff at the command line - session = Session().session + session = Session() if c.DEV_BOX: admin = session.query(AdminAccount).filter( diff --git a/alembic/env.py b/alembic/env.py index 424c0d2b5..ffbdd2044 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -2,6 +2,7 @@ import logging import sys +import sqlmodel from logging.config import fileConfig from alembic import context @@ -24,12 +25,12 @@ logger = logging.getLogger('alembic.env') # Add the model's MetaData object here for "autogenerate" support. -target_metadata = Session.BaseClass.metadata +target_metadata = uber.models.MagModel.metadata def include_object(object, name, type_, reflected, compare_to): """Exclude alembic's own version tables from alembic's consideration.""" - return not name.startswith('alembic_version') + return not name or not name.startswith('alembic_version') def render_item(type_, obj, autogen_context): @@ -37,6 +38,8 @@ def render_item(type_, obj, autogen_context): if type_ == 'type': if isinstance(obj, Choice): return 'sa.Integer()' + if isinstance(obj, sqlmodel.sql.sqltypes.AutoString): + return 'sa.Unicode()' # Default rendering for other objects return False diff --git a/alembic/versions/3d942ce689fc_add_missing_columns_from_sqlalchemy_.py b/alembic/versions/3d942ce689fc_add_missing_columns_from_sqlalchemy_.py new file mode 100644 index 000000000..cd4ebc7db --- /dev/null +++ b/alembic/versions/3d942ce689fc_add_missing_columns_from_sqlalchemy_.py @@ -0,0 +1,65 @@ +"""Add missing columns from SQLAlchemy upgrade + +Revision ID: 3d942ce689fc +Revises: 5a0e898174d4 +Create Date: 2026-02-05 16:34:19.803186 + +""" + + +# revision identifiers, used by Alembic. +revision = '3d942ce689fc' +down_revision = '5a0e898174d4' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + + +try: + is_sqlite = op.get_context().dialect.name == 'sqlite' +except Exception: + is_sqlite = False + +if is_sqlite: + op.get_context().connection.execute('PRAGMA foreign_keys=ON;') + utcnow_server_default = "(datetime('now', 'utc'))" +else: + utcnow_server_default = "timezone('utc', current_timestamp)" + +def sqlite_column_reflect_listener(inspector, table, column_info): + """Adds parenthesis around SQLite datetime defaults for utcnow.""" + if column_info['default'] == "datetime('now', 'utc')": + column_info['default'] = utcnow_server_default + +sqlite_reflect_kwargs = { + 'listeners': [('column_reflect', sqlite_column_reflect_listener)] +} + +# =========================================================================== +# HOWTO: Handle alter statements in SQLite +# +# def upgrade(): +# if is_sqlite: +# with op.batch_alter_table('table_name', reflect_kwargs=sqlite_reflect_kwargs) as batch_op: +# batch_op.alter_column('column_name', type_=sa.Unicode(), server_default='', nullable=False) +# else: +# op.alter_column('table_name', 'column_name', type_=sa.Unicode(), server_default='', nullable=False) +# +# =========================================================================== + + +def upgrade(): + op.add_column('panel_applicant', sa.Column('social_media_info', sa.String(), server_default='', nullable=False)) + op.drop_column('panel_applicant', 'social_media') + op.drop_index(op.f('uq_promo_code_word_normalized_word_part_of_speech'), table_name='promo_code_word') + op.create_index('uq_promo_code_word_normalized_word_part_of_speech', 'promo_code_word', [sa.literal_column('lower(trim(word))'), 'part_of_speech'], unique=True) + + +def downgrade(): + op.drop_index('uq_promo_code_word_normalized_word_part_of_speech', table_name='promo_code_word') + op.create_index(op.f('uq_promo_code_word_normalized_word_part_of_speech'), 'promo_code_word', [sa.literal_column('lower(TRIM(BOTH FROM word))'), 'part_of_speech'], unique=True) + op.add_column('panel_applicant', sa.Column('social_media', postgresql.JSON(astext_type=sa.Text()), server_default=sa.text("'{}'::json"), autoincrement=False, nullable=False)) + op.drop_column('panel_applicant', 'social_media_info') diff --git a/alembic/versions/6902e1cceec6_relationships_overhaul.py b/alembic/versions/6902e1cceec6_relationships_overhaul.py new file mode 100644 index 000000000..63d74d37b --- /dev/null +++ b/alembic/versions/6902e1cceec6_relationships_overhaul.py @@ -0,0 +1,606 @@ +"""Relationships overhaul + +Revision ID: 6902e1cceec6 +Revises: 3d942ce689fc +Create Date: 2026-02-19 23:44:29.239394 + +""" + + +# revision identifiers, used by Alembic. +revision = '6902e1cceec6' +down_revision = '3d942ce689fc' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + + +try: + is_sqlite = op.get_context().dialect.name == 'sqlite' +except Exception: + is_sqlite = False + +if is_sqlite: + op.get_context().connection.execute('PRAGMA foreign_keys=ON;') + utcnow_server_default = "(datetime('now', 'utc'))" +else: + utcnow_server_default = "timezone('utc', current_timestamp)" + +def sqlite_column_reflect_listener(inspector, table, column_info): + """Adds parenthesis around SQLite datetime defaults for utcnow.""" + if column_info['default'] == "datetime('now', 'utc')": + column_info['default'] = utcnow_server_default + +sqlite_reflect_kwargs = { + 'listeners': [('column_reflect', sqlite_column_reflect_listener)] +} + +# =========================================================================== +# HOWTO: Handle alter statements in SQLite +# +# def upgrade(): +# if is_sqlite: +# with op.batch_alter_table('table_name', reflect_kwargs=sqlite_reflect_kwargs) as batch_op: +# batch_op.alter_column('column_name', type_=sa.Unicode(), server_default='', nullable=False) +# else: +# op.alter_column('table_name', 'column_name', type_=sa.Unicode(), server_default='', nullable=False) +# +# =========================================================================== + + +def upgrade(): + op.drop_table('attendee_tournament') + op.drop_constraint(op.f('fk_admin_account_attendee_id_attendee'), 'admin_account', type_='foreignkey') + op.create_foreign_key(op.f('fk_admin_account_attendee_id_attendee'), 'admin_account', 'attendee', ['attendee_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_api_token_admin_account_id_admin_account'), 'api_token', type_='foreignkey') + op.create_foreign_key(op.f('fk_api_token_admin_account_id_admin_account'), 'api_token', 'admin_account', ['admin_account_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_art_panel_assignment_panel_id_art_show_panel'), 'art_panel_assignment', type_='foreignkey') + op.drop_constraint(op.f('fk_art_panel_assignment_app_id_art_show_application'), 'art_panel_assignment', type_='foreignkey') + op.create_foreign_key(op.f('fk_art_panel_assignment_app_id_art_show_application'), 'art_panel_assignment', 'art_show_application', ['app_id'], ['id'], ondelete='CASCADE') + op.create_foreign_key(op.f('fk_art_panel_assignment_panel_id_art_show_panel'), 'art_panel_assignment', 'art_show_panel', ['panel_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_art_show_agent_code_attendee_id_attendee'), 'art_show_agent_code', type_='foreignkey') + op.drop_constraint(op.f('fk_art_show_agent_code_app_id_art_show_application'), 'art_show_agent_code', type_='foreignkey') + op.create_foreign_key(op.f('fk_art_show_agent_code_attendee_id_attendee'), 'art_show_agent_code', 'attendee', ['attendee_id'], ['id']) + op.create_foreign_key(op.f('fk_art_show_agent_code_app_id_art_show_application'), 'art_show_agent_code', 'art_show_application', ['app_id'], ['id'], ondelete='CASCADE') + op.create_unique_constraint(op.f('uq_art_show_application_attendee_id'), 'art_show_application', ['attendee_id']) + op.drop_constraint(op.f('fk_art_show_application_attendee_id_attendee'), 'art_show_application', type_='foreignkey') + op.create_foreign_key(op.f('fk_art_show_application_attendee_id_attendee'), 'art_show_application', 'attendee', ['attendee_id'], ['id']) + op.alter_column('art_show_bidder', 'attendee_id', + existing_type=sa.UUID(), + nullable=False) + op.drop_constraint(op.f('fk_art_show_bidder_attendee_id_attendee'), 'art_show_bidder', type_='foreignkey') + op.create_foreign_key(op.f('fk_art_show_bidder_attendee_id_attendee'), 'art_show_bidder', 'attendee', ['attendee_id'], ['id'], ondelete='CASCADE') + op.alter_column('art_show_payment', 'receipt_id', + existing_type=sa.UUID(), + nullable=False) + op.drop_constraint(op.f('fk_art_show_payment_receipt_id_art_show_receipt'), 'art_show_payment', type_='foreignkey') + op.create_foreign_key(op.f('fk_art_show_payment_receipt_id_art_show_receipt'), 'art_show_payment', 'art_show_receipt', ['receipt_id'], ['id'], ondelete='CASCADE') + op.alter_column('art_show_piece', 'app_id', + existing_type=sa.UUID(), + nullable=False) + op.drop_constraint(op.f('fk_art_show_piece_winning_bidder_id_art_show_bidder'), 'art_show_piece', type_='foreignkey') + op.drop_constraint(op.f('fk_art_show_piece_receipt_id_art_show_receipt'), 'art_show_piece', type_='foreignkey') + op.drop_constraint(op.f('fk_art_show_pieces_app_id_art_show_application'), 'art_show_piece', type_='foreignkey') + op.create_foreign_key(op.f('fk_art_show_piece_receipt_id_art_show_receipt'), 'art_show_piece', 'art_show_receipt', ['receipt_id'], ['id']) + op.create_foreign_key(op.f('fk_art_show_piece_winning_bidder_id_art_show_bidder'), 'art_show_piece', 'art_show_bidder', ['winning_bidder_id'], ['id']) + op.create_foreign_key(op.f('fk_art_show_piece_app_id_art_show_application'), 'art_show_piece', 'art_show_application', ['app_id'], ['id'], ondelete='CASCADE') + op.alter_column('art_show_receipt', 'attendee_id', + existing_type=sa.UUID(), + nullable=False) + op.drop_constraint(op.f('fk_art_show_receipt_attendee_id_attendee'), 'art_show_receipt', type_='foreignkey') + op.create_foreign_key(op.f('fk_art_show_receipt_attendee_id_attendee'), 'art_show_receipt', 'attendee', ['attendee_id'], ['id'], ondelete='CASCADE') + op.create_unique_constraint(op.f('uq_artist_marketplace_application_attendee_id'), 'artist_marketplace_application', ['attendee_id']) + op.drop_constraint(op.f('fk_artist_marketplace_application_attendee_id_attendee'), 'artist_marketplace_application', type_='foreignkey') + op.create_foreign_key(op.f('fk_artist_marketplace_application_attendee_id_attendee'), 'artist_marketplace_application', 'attendee', ['attendee_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_attendee_group_id_group'), 'attendee', type_='foreignkey') + op.drop_constraint(op.f('fk_attendee_creator_id_attendee'), 'attendee', type_='foreignkey') + op.drop_constraint(op.f('fk_attendee_badge_pickup_group_id_badge_pickup_group'), 'attendee', type_='foreignkey') + op.drop_constraint(op.f('fk_attendee_watchlist_id_watch_list'), 'attendee', type_='foreignkey') + op.create_foreign_key(op.f('fk_attendee_watchlist_id_watch_list'), 'attendee', 'watch_list', ['watchlist_id'], ['id']) + op.create_foreign_key(op.f('fk_attendee_badge_pickup_group_id_badge_pickup_group'), 'attendee', 'badge_pickup_group', ['badge_pickup_group_id'], ['id']) + op.create_foreign_key(op.f('fk_attendee_group_id_group'), 'attendee', 'group', ['group_id'], ['id']) + op.create_foreign_key(op.f('fk_attendee_creator_id_attendee'), 'attendee', 'attendee', ['creator_id'], ['id']) + op.drop_column('attendee', 'affiliate') + op.drop_constraint(op.f('attraction_name_key'), 'attraction', type_='unique') + op.create_unique_constraint(op.f('uq_attraction_name'), 'attraction', ['name']) + op.drop_constraint(op.f('fk_attraction_event_location_id_event_location'), 'attraction_event', type_='foreignkey') + op.drop_constraint(op.f('fk_attraction_event_attraction_id_attraction'), 'attraction_event', type_='foreignkey') + op.drop_constraint(op.f('fk_attraction_event_attraction_feature_id_attraction_feature'), 'attraction_event', type_='foreignkey') + op.create_foreign_key(op.f('fk_attraction_event_attraction_id_attraction'), 'attraction_event', 'attraction', ['attraction_id'], ['id'], ondelete='CASCADE') + op.create_foreign_key(op.f('fk_attraction_event_attraction_feature_id_attraction_feature'), 'attraction_event', 'attraction_feature', ['attraction_feature_id'], ['id'], ondelete='CASCADE') + op.create_foreign_key(op.f('fk_attraction_event_event_location_id_event_location'), 'attraction_event', 'event_location', ['event_location_id'], ['id']) + op.drop_constraint(op.f('attraction_feature_name_attraction_id_key'), 'attraction_feature', type_='unique') + op.create_unique_constraint(op.f('uq_attraction_feature_name'), 'attraction_feature', ['name', 'attraction_id']) + op.drop_constraint(op.f('fk_attraction_feature_attraction_id_attraction'), 'attraction_feature', type_='foreignkey') + op.create_foreign_key(op.f('fk_attraction_feature_attraction_id_attraction'), 'attraction_feature', 'attraction', ['attraction_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_attraction_notification_attraction_event_id_attraction_event'), 'attraction_notification', type_='foreignkey') + op.drop_constraint(op.f('fk_attraction_notification_attendee_id_attendee'), 'attraction_notification', type_='foreignkey') + op.drop_constraint(op.f('fk_attraction_notification_attraction_id_attraction'), 'attraction_notification', type_='foreignkey') + op.create_foreign_key(op.f('fk_attraction_notification_attraction_event_id_attraction_event'), 'attraction_notification', 'attraction_event', ['attraction_event_id'], ['id'], ondelete='CASCADE') + op.create_foreign_key(op.f('fk_attraction_notification_attraction_id_attraction'), 'attraction_notification', 'attraction', ['attraction_id'], ['id'], ondelete='CASCADE') + op.create_foreign_key(op.f('fk_attraction_notification_attendee_id_attendee'), 'attraction_notification', 'attendee', ['attendee_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_attraction_signup_attendee_id_attendee'), 'attraction_signup', type_='foreignkey') + op.drop_constraint(op.f('fk_attraction_signup_attraction_id_attraction'), 'attraction_signup', type_='foreignkey') + op.drop_constraint(op.f('fk_attraction_signup_attraction_event_id_attraction_event'), 'attraction_signup', type_='foreignkey') + op.create_foreign_key(op.f('fk_attraction_signup_attraction_id_attraction'), 'attraction_signup', 'attraction', ['attraction_id'], ['id'], ondelete='CASCADE') + op.create_foreign_key(op.f('fk_attraction_signup_attendee_id_attendee'), 'attraction_signup', 'attendee', ['attendee_id'], ['id'], ondelete='CASCADE') + op.create_foreign_key(op.f('fk_attraction_signup_attraction_event_id_attraction_event'), 'attraction_signup', 'attraction_event', ['attraction_event_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_badge_info_attendee_id_attendee'), 'badge_info', type_='foreignkey') + op.create_foreign_key(op.f('fk_badge_info_attendee_id_attendee'), 'badge_info', 'attendee', ['attendee_id'], ['id']) + op.add_column('bulk_printing_request', sa.Column('important', sa.Boolean(), nullable=False)) + op.drop_constraint(op.f('fk_bulk_printing_request_department_id_department'), 'bulk_printing_request', type_='foreignkey') + op.create_foreign_key(op.f('fk_bulk_printing_request_department_id_department'), 'bulk_printing_request', 'department', ['department_id'], ['id'], ondelete='CASCADE') + op.drop_column('bulk_printing_request', 'required') + op.drop_constraint(op.f('department_name_key'), 'department', type_='unique') + op.create_unique_constraint(op.f('uq_department_name'), 'department', ['name']) + op.drop_constraint(op.f('fk_department_parent_id_department'), 'department', type_='foreignkey') + op.create_foreign_key(op.f('fk_department_parent_id_department'), 'department', 'department', ['parent_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_dept_checklist_item_department_id_department'), 'dept_checklist_item', type_='foreignkey') + op.drop_constraint(op.f('fk_dept_checklist_item_attendee_id_attendee'), 'dept_checklist_item', type_='foreignkey') + op.create_foreign_key(op.f('fk_dept_checklist_item_department_id_department'), 'dept_checklist_item', 'department', ['department_id'], ['id'], ondelete='CASCADE') + op.create_foreign_key(op.f('fk_dept_checklist_item_attendee_id_attendee'), 'dept_checklist_item', 'attendee', ['attendee_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_dept_membership_department_id_department'), 'dept_membership', type_='foreignkey') + op.drop_constraint(op.f('fk_dept_membership_attendee_id_attendee'), 'dept_membership', type_='foreignkey') + op.create_foreign_key(op.f('fk_dept_membership_department_id_department'), 'dept_membership', 'department', ['department_id'], ['id'], ondelete='CASCADE') + op.create_foreign_key(op.f('fk_dept_membership_attendee_id_attendee'), 'dept_membership', 'attendee', ['attendee_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_dept_membership_request_department_id_department'), 'dept_membership_request', type_='foreignkey') + op.drop_constraint(op.f('fk_dept_membership_request_attendee_id_attendee'), 'dept_membership_request', type_='foreignkey') + op.create_foreign_key(op.f('fk_dept_membership_request_attendee_id_attendee'), 'dept_membership_request', 'attendee', ['attendee_id'], ['id'], ondelete='CASCADE') + op.create_foreign_key(op.f('fk_dept_membership_request_department_id_department'), 'dept_membership_request', 'department', ['department_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_dept_role_department_id_department'), 'dept_role', type_='foreignkey') + op.create_foreign_key(op.f('fk_dept_role_department_id_department'), 'dept_role', 'department', ['department_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_email_automated_email_id_automated_email'), 'email', type_='foreignkey') + op.create_foreign_key(op.f('fk_email_automated_email_id_automated_email'), 'email', 'automated_email', ['automated_email_id'], ['id']) + op.create_unique_constraint(op.f('uq_event_attraction_event_id'), 'event', ['attraction_event_id']) + op.drop_constraint(op.f('fk_event_department_id_department'), 'event', type_='foreignkey') + op.drop_constraint(op.f('fk_event_location_id_event_location'), 'event', type_='foreignkey') + op.drop_constraint(op.f('fk_event_attraction_event_id_attraction_event'), 'event', type_='foreignkey') + op.create_foreign_key(op.f('fk_event_department_id_department'), 'event', 'department', ['department_id'], ['id']) + op.create_foreign_key(op.f('fk_event_event_location_id_event_location'), 'event', 'event_location', ['event_location_id'], ['id']) + op.create_foreign_key(op.f('fk_event_attraction_event_id_attraction_event'), 'event', 'attraction_event', ['attraction_event_id'], ['id']) + op.drop_constraint(op.f('fk_event_feedback_event_id_event'), 'event_feedback', type_='foreignkey') + op.create_foreign_key(op.f('fk_event_feedback_event_id_event'), 'event_feedback', 'event', ['event_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_event_location_department_id_department'), 'event_location', type_='foreignkey') + op.create_foreign_key(op.f('fk_event_location_department_id_department'), 'event_location', 'department', ['department_id'], ['id']) + op.drop_constraint(op.f('fk_food_restrictions_attendee_id_attendee'), 'food_restrictions', type_='foreignkey') + op.create_foreign_key(op.f('fk_food_restrictions_attendee_id_attendee'), 'food_restrictions', 'attendee', ['attendee_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_group_shared_with_id_group'), 'group', type_='foreignkey') + op.drop_constraint(op.f('fk_leader'), 'group', type_='foreignkey') + op.create_foreign_key(op.f('fk_group_leader_id_attendee'), 'group', 'attendee', ['leader_id'], ['id']) + op.create_foreign_key(op.f('fk_group_shared_with_id_group'), 'group', 'group', ['shared_with_id'], ['id']) + op.drop_constraint(op.f('fk_guest_autograph_guest_id_guest_group'), 'guest_autograph', type_='foreignkey') + op.create_foreign_key(op.f('fk_guest_autograph_guest_id_guest_group'), 'guest_autograph', 'guest_group', ['guest_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_guest_bio_guest_id_guest_group'), 'guest_bio', type_='foreignkey') + op.create_foreign_key(op.f('fk_guest_bio_guest_id_guest_group'), 'guest_bio', 'guest_group', ['guest_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_guest_charity_guest_id_guest_group'), 'guest_charity', type_='foreignkey') + op.create_foreign_key(op.f('fk_guest_charity_guest_id_guest_group'), 'guest_charity', 'guest_group', ['guest_id'], ['id'], ondelete='CASCADE') + op.alter_column('guest_detailed_travel_plan', 'travel_plans_id', + existing_type=sa.UUID(), + nullable=False) + op.drop_constraint(op.f('fk_guest_detailed_travel_plan_travel_plans_id_guest_tra_6ad4'), 'guest_detailed_travel_plan', type_='foreignkey') + op.create_foreign_key(op.f('fk_guest_detailed_travel_plan_travel_plans_id_guest_travel_plans'), 'guest_detailed_travel_plan', 'guest_travel_plans', ['travel_plans_id'], ['id'], ondelete='CASCADE') + op.create_unique_constraint(op.f('uq_guest_group_group_id'), 'guest_group', ['group_id']) + op.drop_constraint(op.f('fk_guest_group_group_id_group'), 'guest_group', type_='foreignkey') + op.drop_constraint(op.f('fk_guest_group_event_id_event'), 'guest_group', type_='foreignkey') + op.create_foreign_key(op.f('fk_guest_group_group_id_group'), 'guest_group', 'group', ['group_id'], ['id'], ondelete='CASCADE') + op.create_foreign_key(op.f('fk_guest_group_event_id_event'), 'guest_group', 'event', ['event_id'], ['id']) + op.drop_constraint(op.f('fk_guest_hospitality_guest_id_guest_group'), 'guest_hospitality', type_='foreignkey') + op.create_foreign_key(op.f('fk_guest_hospitality_guest_id_guest_group'), 'guest_hospitality', 'guest_group', ['guest_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_guest_image_guest_id_guest_group'), 'guest_image', type_='foreignkey') + op.create_foreign_key(op.f('fk_guest_image_guest_id_guest_group'), 'guest_image', 'guest_group', ['guest_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_guest_info_guest_id_guest_group'), 'guest_info', type_='foreignkey') + op.create_foreign_key(op.f('fk_guest_info_guest_id_guest_group'), 'guest_info', 'guest_group', ['guest_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_guest_interview_guest_id_guest_group'), 'guest_interview', type_='foreignkey') + op.create_foreign_key(op.f('fk_guest_interview_guest_id_guest_group'), 'guest_interview', 'guest_group', ['guest_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_guest_media_request_guest_id_guest_group'), 'guest_media_request', type_='foreignkey') + op.create_foreign_key(op.f('fk_guest_media_request_guest_id_guest_group'), 'guest_media_request', 'guest_group', ['guest_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_guest_merch_guest_id_guest_group'), 'guest_merch', type_='foreignkey') + op.create_foreign_key(op.f('fk_guest_merch_guest_id_guest_group'), 'guest_merch', 'guest_group', ['guest_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_guest_panel_guest_id_guest_group'), 'guest_panel', type_='foreignkey') + op.create_foreign_key(op.f('fk_guest_panel_guest_id_guest_group'), 'guest_panel', 'guest_group', ['guest_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_guest_stage_plot_guest_id_guest_group'), 'guest_stage_plot', type_='foreignkey') + op.create_foreign_key(op.f('fk_guest_stage_plot_guest_id_guest_group'), 'guest_stage_plot', 'guest_group', ['guest_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_guest_taxes_guest_id_guest_group'), 'guest_taxes', type_='foreignkey') + op.create_foreign_key(op.f('fk_guest_taxes_guest_id_guest_group'), 'guest_taxes', 'guest_group', ['guest_id'], ['id'], ondelete='CASCADE') + op.create_unique_constraint(op.f('uq_guest_track_guest_id'), 'guest_track', ['guest_id']) + op.drop_constraint(op.f('fk_guest_track_guest_id_guest_group'), 'guest_track', type_='foreignkey') + op.create_foreign_key(op.f('fk_guest_track_guest_id_guest_group'), 'guest_track', 'guest_group', ['guest_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_guest_travel_plans_guest_id_guest_group'), 'guest_travel_plans', type_='foreignkey') + op.create_foreign_key(op.f('fk_guest_travel_plans_guest_id_guest_group'), 'guest_travel_plans', 'guest_group', ['guest_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_hotel_requests_attendee_id_attendee'), 'hotel_requests', type_='foreignkey') + op.create_foreign_key(op.f('fk_hotel_requests_attendee_id_attendee'), 'hotel_requests', 'attendee', ['attendee_id'], ['id'], ondelete='CASCADE') + op.add_column('indie_developer', sa.Column('receives_emails', sa.Boolean(), nullable=False)) + op.drop_constraint(op.f('fk_indie_developer_studio_id_indie_studio'), 'indie_developer', type_='foreignkey') + op.create_foreign_key(op.f('fk_indie_developer_studio_id_indie_studio'), 'indie_developer', 'indie_studio', ['studio_id'], ['id'], ondelete='CASCADE') + op.drop_column('indie_developer', 'gets_emails') + op.drop_constraint(op.f('fk_indie_game_studio_id_indie_studio'), 'indie_game', type_='foreignkey') + op.drop_constraint(op.f('fk_indie_game_primary_contact_id_indie_developer'), 'indie_game', type_='foreignkey') + op.create_foreign_key(op.f('fk_indie_game_primary_contact_id_indie_developer'), 'indie_game', 'indie_developer', ['primary_contact_id'], ['id']) + op.create_foreign_key(op.f('fk_indie_game_studio_id_indie_studio'), 'indie_game', 'indie_studio', ['studio_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_indie_game_code_game_id_indie_game'), 'indie_game_code', type_='foreignkey') + op.create_foreign_key(op.f('fk_indie_game_code_game_id_indie_game'), 'indie_game_code', 'indie_game', ['game_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_indie_game_image_game_id_indie_game'), 'indie_game_image', type_='foreignkey') + op.create_foreign_key(op.f('fk_indie_game_image_game_id_indie_game'), 'indie_game_image', 'indie_game', ['game_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_indie_game_review_game_id_indie_game'), 'indie_game_review', type_='foreignkey') + op.drop_constraint(op.f('fk_indie_game_review_judge_id_indie_judge'), 'indie_game_review', type_='foreignkey') + op.create_foreign_key(op.f('fk_indie_game_review_game_id_indie_game'), 'indie_game_review', 'indie_game', ['game_id'], ['id'], ondelete='CASCADE') + op.create_foreign_key(op.f('fk_indie_game_review_judge_id_indie_judge'), 'indie_game_review', 'indie_judge', ['judge_id'], ['id'], ondelete='CASCADE') + op.alter_column('indie_judge', 'admin_id', + existing_type=sa.UUID(), + nullable=True) + op.create_unique_constraint(op.f('uq_indie_judge_admin_id'), 'indie_judge', ['admin_id']) + op.create_unique_constraint(op.f('uq_indie_studio_group_id'), 'indie_studio', ['group_id']) + op.drop_constraint(op.f('fk_job_department_id_department'), 'job', type_='foreignkey') + op.create_foreign_key(op.f('fk_job_department_id_department'), 'job', 'department', ['department_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_job_template_department_id_department'), 'job_template', type_='foreignkey') + op.create_foreign_key(op.f('fk_job_template_department_id_department'), 'job_template', 'department', ['department_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_m_points_for_cash_attendee_id_attendee'), 'm_points_for_cash', type_='foreignkey') + op.create_foreign_key(op.f('fk_m_points_for_cash_attendee_id_attendee'), 'm_points_for_cash', 'attendee', ['attendee_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_merch_discount_attendee_id_attendee'), 'merch_discount', type_='foreignkey') + op.create_foreign_key(op.f('fk_merch_discount_attendee_id_attendee'), 'merch_discount', 'attendee', ['attendee_id'], ['id'], ondelete='CASCADE') + op.alter_column('merch_pickup', 'picked_up_by_id', + existing_type=sa.UUID(), + nullable=True) + op.alter_column('merch_pickup', 'picked_up_for_id', + existing_type=sa.UUID(), + nullable=True) + op.drop_constraint(op.f('fk_mits_applicant_team_id_mits_team'), 'mits_applicant', type_='foreignkey') + op.create_foreign_key(op.f('fk_mits_applicant_team_id_mits_team'), 'mits_applicant', 'mits_team', ['team_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_mits_document_game_id_mits_game'), 'mits_document', type_='foreignkey') + op.create_foreign_key(op.f('fk_mits_document_game_id_mits_game'), 'mits_document', 'mits_game', ['game_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_mits_game_team_id_mits_team'), 'mits_game', type_='foreignkey') + op.create_foreign_key(op.f('fk_mits_game_team_id_mits_team'), 'mits_game', 'mits_team', ['team_id'], ['id'], ondelete='CASCADE') + op.create_unique_constraint(op.f('uq_mits_panel_application_team_id'), 'mits_panel_application', ['team_id']) + op.drop_constraint(op.f('fk_mits_panel_application_team_id_mits_team'), 'mits_panel_application', type_='foreignkey') + op.create_foreign_key(op.f('fk_mits_panel_application_team_id_mits_team'), 'mits_panel_application', 'mits_team', ['team_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_mits_picture_game_id_mits_game'), 'mits_picture', type_='foreignkey') + op.create_foreign_key(op.f('fk_mits_picture_game_id_mits_game'), 'mits_picture', 'mits_game', ['game_id'], ['id'], ondelete='CASCADE') + op.create_unique_constraint(op.f('uq_mits_times_team_id'), 'mits_times', ['team_id']) + op.drop_constraint(op.f('fk_mits_times_team_id_mits_team'), 'mits_times', type_='foreignkey') + op.create_foreign_key(op.f('fk_mits_times_team_id_mits_team'), 'mits_times', 'mits_team', ['team_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_no_shirt_attendee_id_attendee'), 'no_shirt', type_='foreignkey') + op.create_foreign_key(op.f('fk_no_shirt_attendee_id_attendee'), 'no_shirt', 'attendee', ['attendee_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_old_m_point_exchange_attendee_id_attendee'), 'old_m_point_exchange', type_='foreignkey') + op.create_foreign_key(op.f('fk_old_m_point_exchange_attendee_id_attendee'), 'old_m_point_exchange', 'attendee', ['attendee_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_panel_application_submitter_id_panel_applicant'), 'panel_application', type_='foreignkey') + op.drop_constraint(op.f('fk_panel_application_poc_id_attendee'), 'panel_application', type_='foreignkey') + op.drop_constraint(op.f('fk_panel_application_event_id_event'), 'panel_application', type_='foreignkey') + op.create_foreign_key(op.f('fk_panel_application_poc_id_attendee'), 'panel_application', 'attendee', ['poc_id'], ['id']) + op.create_foreign_key(op.f('fk_panel_application_event_id_event'), 'panel_application', 'event', ['event_id'], ['id']) + op.create_foreign_key(op.f('fk_panel_application_submitter_id_panel_applicant'), 'panel_application', 'panel_applicant', ['submitter_id'], ['id']) + op.alter_column('password_reset', 'admin_id', + existing_type=sa.UUID(), + nullable=False) + op.alter_column('password_reset', 'attendee_id', + existing_type=sa.UUID(), + nullable=False) + op.drop_constraint(op.f('fk_password_reset_admin_id_admin_account'), 'password_reset', type_='foreignkey') + op.drop_constraint(op.f('fk_password_reset_attendee_id_attendee_account'), 'password_reset', type_='foreignkey') + op.create_foreign_key(op.f('fk_password_reset_admin_id_admin_account'), 'password_reset', 'admin_account', ['admin_id'], ['id'], ondelete='CASCADE') + op.create_foreign_key(op.f('fk_password_reset_attendee_id_attendee_account'), 'password_reset', 'attendee_account', ['attendee_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_print_queue_attendee_id_attendee'), 'print_job', type_='foreignkey') + op.create_foreign_key(op.f('fk_print_job_attendee_id_attendee'), 'print_job', 'attendee', ['attendee_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_promo_code_group_id_promo_code_group'), 'promo_code', type_='foreignkey') + op.create_foreign_key(op.f('fk_promo_code_group_id_promo_code_group'), 'promo_code', 'promo_code_group', ['group_id'], ['id']) + op.drop_constraint(op.f('fk_promo_code_group_buyer_id_attendee'), 'promo_code_group', type_='foreignkey') + op.create_foreign_key(op.f('fk_promo_code_group_buyer_id_attendee'), 'promo_code_group', 'attendee', ['buyer_id'], ['id']) + op.alter_column('receipt_item', 'receipt_id', + existing_type=sa.UUID(), + nullable=False) + op.drop_constraint(op.f('fk_receipt_item_receipt_id_model_receipt'), 'receipt_item', type_='foreignkey') + op.drop_constraint(op.f('fk_receipt_item_txn_id_receipt_transaction'), 'receipt_item', type_='foreignkey') + op.create_foreign_key(op.f('fk_receipt_item_receipt_id_model_receipt'), 'receipt_item', 'model_receipt', ['receipt_id'], ['id'], ondelete='CASCADE') + op.create_foreign_key(op.f('fk_receipt_item_txn_id_receipt_transaction'), 'receipt_item', 'receipt_transaction', ['txn_id'], ['id']) + op.alter_column('receipt_transaction', 'receipt_id', + existing_type=sa.UUID(), + nullable=False) + op.drop_constraint(op.f('fk_receipt_transaction_refunded_txn_id_receipt_transaction'), 'receipt_transaction', type_='foreignkey') + op.drop_constraint(op.f('fk_receipt_transaction_receipt_info_id_receipt_info'), 'receipt_transaction', type_='foreignkey') + op.drop_constraint(op.f('fk_receipt_transaction_receipt_id_model_receipt'), 'receipt_transaction', type_='foreignkey') + op.create_foreign_key(op.f('fk_receipt_transaction_refunded_txn_id_receipt_transaction'), 'receipt_transaction', 'receipt_transaction', ['refunded_txn_id'], ['id']) + op.create_foreign_key(op.f('fk_receipt_transaction_receipt_info_id_receipt_info'), 'receipt_transaction', 'receipt_info', ['receipt_info_id'], ['id']) + op.create_foreign_key(op.f('fk_receipt_transaction_receipt_id_model_receipt'), 'receipt_transaction', 'model_receipt', ['receipt_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_room_assignment_room_id_room'), 'room_assignment', type_='foreignkey') + op.drop_constraint(op.f('fk_room_assignment_attendee_id_attendee'), 'room_assignment', type_='foreignkey') + op.create_foreign_key(op.f('fk_room_assignment_room_id_room'), 'room_assignment', 'room', ['room_id'], ['id'], ondelete='CASCADE') + op.create_foreign_key(op.f('fk_room_assignment_attendee_id_attendee'), 'room_assignment', 'attendee', ['attendee_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_sale_attendee_id_attendee'), 'sale', type_='foreignkey') + op.create_foreign_key(op.f('fk_sale_attendee_id_attendee'), 'sale', 'attendee', ['attendee_id'], ['id']) + op.drop_constraint(op.f('fk_tabletop_checkout_game_id_tabletop_game'), 'tabletop_checkout', type_='foreignkey') + op.drop_constraint(op.f('fk_tabletop_checkout_attendee_id_attendee'), 'tabletop_checkout', type_='foreignkey') + op.create_foreign_key(op.f('fk_tabletop_checkout_game_id_tabletop_game'), 'tabletop_checkout', 'tabletop_game', ['game_id'], ['id'], ondelete='CASCADE') + op.create_foreign_key(op.f('fk_tabletop_checkout_attendee_id_attendee'), 'tabletop_checkout', 'attendee', ['attendee_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('fk_tabletop_game_attendee_id_attendee'), 'tabletop_game', type_='foreignkey') + op.create_foreign_key(op.f('fk_tabletop_game_attendee_id_attendee'), 'tabletop_game', 'attendee', ['attendee_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(op.f('txn_request_tracking_incr_id_key'), 'txn_request_tracking', type_='unique') + op.create_unique_constraint(op.f('uq_txn_request_tracking_incr_id'), 'txn_request_tracking', ['incr_id']) + + +def downgrade(): + op.drop_constraint(op.f('uq_txn_request_tracking_incr_id'), 'txn_request_tracking', type_='unique') + op.create_unique_constraint(op.f('txn_request_tracking_incr_id_key'), 'txn_request_tracking', ['incr_id'], postgresql_nulls_not_distinct=False) + op.drop_constraint(op.f('fk_tabletop_game_attendee_id_attendee'), 'tabletop_game', type_='foreignkey') + op.create_foreign_key(op.f('fk_tabletop_game_attendee_id_attendee'), 'tabletop_game', 'attendee', ['attendee_id'], ['id']) + op.drop_constraint(op.f('fk_tabletop_checkout_attendee_id_attendee'), 'tabletop_checkout', type_='foreignkey') + op.drop_constraint(op.f('fk_tabletop_checkout_game_id_tabletop_game'), 'tabletop_checkout', type_='foreignkey') + op.create_foreign_key(op.f('fk_tabletop_checkout_attendee_id_attendee'), 'tabletop_checkout', 'attendee', ['attendee_id'], ['id']) + op.create_foreign_key(op.f('fk_tabletop_checkout_game_id_tabletop_game'), 'tabletop_checkout', 'tabletop_game', ['game_id'], ['id']) + op.drop_constraint(op.f('fk_sale_attendee_id_attendee'), 'sale', type_='foreignkey') + op.create_foreign_key(op.f('fk_sale_attendee_id_attendee'), 'sale', 'attendee', ['attendee_id'], ['id'], ondelete='SET NULL') + op.drop_constraint(op.f('fk_room_assignment_attendee_id_attendee'), 'room_assignment', type_='foreignkey') + op.drop_constraint(op.f('fk_room_assignment_room_id_room'), 'room_assignment', type_='foreignkey') + op.create_foreign_key(op.f('fk_room_assignment_attendee_id_attendee'), 'room_assignment', 'attendee', ['attendee_id'], ['id']) + op.create_foreign_key(op.f('fk_room_assignment_room_id_room'), 'room_assignment', 'room', ['room_id'], ['id']) + op.drop_constraint(op.f('fk_receipt_transaction_receipt_id_model_receipt'), 'receipt_transaction', type_='foreignkey') + op.drop_constraint(op.f('fk_receipt_transaction_receipt_info_id_receipt_info'), 'receipt_transaction', type_='foreignkey') + op.drop_constraint(op.f('fk_receipt_transaction_refunded_txn_id_receipt_transaction'), 'receipt_transaction', type_='foreignkey') + op.create_foreign_key(op.f('fk_receipt_transaction_receipt_id_model_receipt'), 'receipt_transaction', 'model_receipt', ['receipt_id'], ['id'], ondelete='SET NULL') + op.create_foreign_key(op.f('fk_receipt_transaction_receipt_info_id_receipt_info'), 'receipt_transaction', 'receipt_info', ['receipt_info_id'], ['id'], ondelete='SET NULL') + op.create_foreign_key(op.f('fk_receipt_transaction_refunded_txn_id_receipt_transaction'), 'receipt_transaction', 'receipt_transaction', ['refunded_txn_id'], ['id'], ondelete='SET NULL') + op.alter_column('receipt_transaction', 'receipt_id', + existing_type=sa.UUID(), + nullable=True) + op.drop_constraint(op.f('fk_receipt_item_txn_id_receipt_transaction'), 'receipt_item', type_='foreignkey') + op.drop_constraint(op.f('fk_receipt_item_receipt_id_model_receipt'), 'receipt_item', type_='foreignkey') + op.create_foreign_key(op.f('fk_receipt_item_txn_id_receipt_transaction'), 'receipt_item', 'receipt_transaction', ['txn_id'], ['id'], ondelete='SET NULL') + op.create_foreign_key(op.f('fk_receipt_item_receipt_id_model_receipt'), 'receipt_item', 'model_receipt', ['receipt_id'], ['id'], ondelete='SET NULL') + op.alter_column('receipt_item', 'receipt_id', + existing_type=sa.UUID(), + nullable=True) + op.drop_constraint(op.f('fk_promo_code_group_buyer_id_attendee'), 'promo_code_group', type_='foreignkey') + op.create_foreign_key(op.f('fk_promo_code_group_buyer_id_attendee'), 'promo_code_group', 'attendee', ['buyer_id'], ['id'], ondelete='SET NULL') + op.drop_constraint(op.f('fk_promo_code_group_id_promo_code_group'), 'promo_code', type_='foreignkey') + op.create_foreign_key(op.f('fk_promo_code_group_id_promo_code_group'), 'promo_code', 'promo_code_group', ['group_id'], ['id'], ondelete='SET NULL') + op.drop_constraint(op.f('fk_print_job_attendee_id_attendee'), 'print_job', type_='foreignkey') + op.create_foreign_key(op.f('fk_print_queue_attendee_id_attendee'), 'print_job', 'attendee', ['attendee_id'], ['id']) + op.drop_constraint(op.f('fk_password_reset_attendee_id_attendee_account'), 'password_reset', type_='foreignkey') + op.drop_constraint(op.f('fk_password_reset_admin_id_admin_account'), 'password_reset', type_='foreignkey') + op.create_foreign_key(op.f('fk_password_reset_attendee_id_attendee_account'), 'password_reset', 'attendee_account', ['attendee_id'], ['id']) + op.create_foreign_key(op.f('fk_password_reset_admin_id_admin_account'), 'password_reset', 'admin_account', ['admin_id'], ['id']) + op.alter_column('password_reset', 'attendee_id', + existing_type=sa.UUID(), + nullable=True) + op.alter_column('password_reset', 'admin_id', + existing_type=sa.UUID(), + nullable=True) + op.drop_constraint(op.f('fk_panel_application_submitter_id_panel_applicant'), 'panel_application', type_='foreignkey') + op.drop_constraint(op.f('fk_panel_application_event_id_event'), 'panel_application', type_='foreignkey') + op.drop_constraint(op.f('fk_panel_application_poc_id_attendee'), 'panel_application', type_='foreignkey') + op.create_foreign_key(op.f('fk_panel_application_event_id_event'), 'panel_application', 'event', ['event_id'], ['id'], ondelete='SET NULL') + op.create_foreign_key(op.f('fk_panel_application_poc_id_attendee'), 'panel_application', 'attendee', ['poc_id'], ['id'], ondelete='SET NULL') + op.create_foreign_key(op.f('fk_panel_application_submitter_id_panel_applicant'), 'panel_application', 'panel_applicant', ['submitter_id'], ['id'], ondelete='SET NULL') + op.drop_constraint(op.f('fk_old_m_point_exchange_attendee_id_attendee'), 'old_m_point_exchange', type_='foreignkey') + op.create_foreign_key(op.f('fk_old_m_point_exchange_attendee_id_attendee'), 'old_m_point_exchange', 'attendee', ['attendee_id'], ['id']) + op.drop_constraint(op.f('fk_no_shirt_attendee_id_attendee'), 'no_shirt', type_='foreignkey') + op.create_foreign_key(op.f('fk_no_shirt_attendee_id_attendee'), 'no_shirt', 'attendee', ['attendee_id'], ['id']) + op.drop_constraint(op.f('fk_mits_times_team_id_mits_team'), 'mits_times', type_='foreignkey') + op.create_foreign_key(op.f('fk_mits_times_team_id_mits_team'), 'mits_times', 'mits_team', ['team_id'], ['id']) + op.drop_constraint(op.f('uq_mits_times_team_id'), 'mits_times', type_='unique') + op.drop_constraint(op.f('fk_mits_picture_game_id_mits_game'), 'mits_picture', type_='foreignkey') + op.create_foreign_key(op.f('fk_mits_picture_game_id_mits_game'), 'mits_picture', 'mits_game', ['game_id'], ['id']) + op.drop_constraint(op.f('fk_mits_panel_application_team_id_mits_team'), 'mits_panel_application', type_='foreignkey') + op.create_foreign_key(op.f('fk_mits_panel_application_team_id_mits_team'), 'mits_panel_application', 'mits_team', ['team_id'], ['id']) + op.drop_constraint(op.f('uq_mits_panel_application_team_id'), 'mits_panel_application', type_='unique') + op.drop_constraint(op.f('fk_mits_game_team_id_mits_team'), 'mits_game', type_='foreignkey') + op.create_foreign_key(op.f('fk_mits_game_team_id_mits_team'), 'mits_game', 'mits_team', ['team_id'], ['id']) + op.drop_constraint(op.f('fk_mits_document_game_id_mits_game'), 'mits_document', type_='foreignkey') + op.create_foreign_key(op.f('fk_mits_document_game_id_mits_game'), 'mits_document', 'mits_game', ['game_id'], ['id']) + op.drop_constraint(op.f('fk_mits_applicant_team_id_mits_team'), 'mits_applicant', type_='foreignkey') + op.create_foreign_key(op.f('fk_mits_applicant_team_id_mits_team'), 'mits_applicant', 'mits_team', ['team_id'], ['id']) + op.alter_column('merch_pickup', 'picked_up_for_id', + existing_type=sa.UUID(), + nullable=False) + op.alter_column('merch_pickup', 'picked_up_by_id', + existing_type=sa.UUID(), + nullable=False) + op.drop_constraint(op.f('fk_merch_discount_attendee_id_attendee'), 'merch_discount', type_='foreignkey') + op.create_foreign_key(op.f('fk_merch_discount_attendee_id_attendee'), 'merch_discount', 'attendee', ['attendee_id'], ['id']) + op.drop_constraint(op.f('fk_m_points_for_cash_attendee_id_attendee'), 'm_points_for_cash', type_='foreignkey') + op.create_foreign_key(op.f('fk_m_points_for_cash_attendee_id_attendee'), 'm_points_for_cash', 'attendee', ['attendee_id'], ['id']) + op.drop_constraint(op.f('fk_job_template_department_id_department'), 'job_template', type_='foreignkey') + op.create_foreign_key(op.f('fk_job_template_department_id_department'), 'job_template', 'department', ['department_id'], ['id']) + op.drop_constraint(op.f('fk_job_department_id_department'), 'job', type_='foreignkey') + op.create_foreign_key(op.f('fk_job_department_id_department'), 'job', 'department', ['department_id'], ['id']) + op.drop_constraint(op.f('uq_indie_studio_group_id'), 'indie_studio', type_='unique') + op.drop_constraint(op.f('uq_indie_judge_admin_id'), 'indie_judge', type_='unique') + op.alter_column('indie_judge', 'admin_id', + existing_type=sa.UUID(), + nullable=False) + op.drop_constraint(op.f('fk_indie_game_review_judge_id_indie_judge'), 'indie_game_review', type_='foreignkey') + op.drop_constraint(op.f('fk_indie_game_review_game_id_indie_game'), 'indie_game_review', type_='foreignkey') + op.create_foreign_key(op.f('fk_indie_game_review_judge_id_indie_judge'), 'indie_game_review', 'indie_judge', ['judge_id'], ['id']) + op.create_foreign_key(op.f('fk_indie_game_review_game_id_indie_game'), 'indie_game_review', 'indie_game', ['game_id'], ['id']) + op.drop_constraint(op.f('fk_indie_game_image_game_id_indie_game'), 'indie_game_image', type_='foreignkey') + op.create_foreign_key(op.f('fk_indie_game_image_game_id_indie_game'), 'indie_game_image', 'indie_game', ['game_id'], ['id']) + op.drop_constraint(op.f('fk_indie_game_code_game_id_indie_game'), 'indie_game_code', type_='foreignkey') + op.create_foreign_key(op.f('fk_indie_game_code_game_id_indie_game'), 'indie_game_code', 'indie_game', ['game_id'], ['id']) + op.drop_constraint(op.f('fk_indie_game_studio_id_indie_studio'), 'indie_game', type_='foreignkey') + op.drop_constraint(op.f('fk_indie_game_primary_contact_id_indie_developer'), 'indie_game', type_='foreignkey') + op.create_foreign_key(op.f('fk_indie_game_primary_contact_id_indie_developer'), 'indie_game', 'indie_developer', ['primary_contact_id'], ['id'], ondelete='SET NULL') + op.create_foreign_key(op.f('fk_indie_game_studio_id_indie_studio'), 'indie_game', 'indie_studio', ['studio_id'], ['id']) + op.add_column('indie_developer', sa.Column('gets_emails', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=False)) + op.drop_constraint(op.f('fk_indie_developer_studio_id_indie_studio'), 'indie_developer', type_='foreignkey') + op.create_foreign_key(op.f('fk_indie_developer_studio_id_indie_studio'), 'indie_developer', 'indie_studio', ['studio_id'], ['id']) + op.drop_column('indie_developer', 'receives_emails') + op.drop_constraint(op.f('fk_hotel_requests_attendee_id_attendee'), 'hotel_requests', type_='foreignkey') + op.create_foreign_key(op.f('fk_hotel_requests_attendee_id_attendee'), 'hotel_requests', 'attendee', ['attendee_id'], ['id']) + op.drop_constraint(op.f('fk_guest_travel_plans_guest_id_guest_group'), 'guest_travel_plans', type_='foreignkey') + op.create_foreign_key(op.f('fk_guest_travel_plans_guest_id_guest_group'), 'guest_travel_plans', 'guest_group', ['guest_id'], ['id']) + op.drop_constraint(op.f('fk_guest_track_guest_id_guest_group'), 'guest_track', type_='foreignkey') + op.create_foreign_key(op.f('fk_guest_track_guest_id_guest_group'), 'guest_track', 'guest_group', ['guest_id'], ['id']) + op.drop_constraint(op.f('uq_guest_track_guest_id'), 'guest_track', type_='unique') + op.drop_constraint(op.f('fk_guest_taxes_guest_id_guest_group'), 'guest_taxes', type_='foreignkey') + op.create_foreign_key(op.f('fk_guest_taxes_guest_id_guest_group'), 'guest_taxes', 'guest_group', ['guest_id'], ['id']) + op.drop_constraint(op.f('fk_guest_stage_plot_guest_id_guest_group'), 'guest_stage_plot', type_='foreignkey') + op.create_foreign_key(op.f('fk_guest_stage_plot_guest_id_guest_group'), 'guest_stage_plot', 'guest_group', ['guest_id'], ['id']) + op.drop_constraint(op.f('fk_guest_panel_guest_id_guest_group'), 'guest_panel', type_='foreignkey') + op.create_foreign_key(op.f('fk_guest_panel_guest_id_guest_group'), 'guest_panel', 'guest_group', ['guest_id'], ['id']) + op.drop_constraint(op.f('fk_guest_merch_guest_id_guest_group'), 'guest_merch', type_='foreignkey') + op.create_foreign_key(op.f('fk_guest_merch_guest_id_guest_group'), 'guest_merch', 'guest_group', ['guest_id'], ['id']) + op.drop_constraint(op.f('fk_guest_media_request_guest_id_guest_group'), 'guest_media_request', type_='foreignkey') + op.create_foreign_key(op.f('fk_guest_media_request_guest_id_guest_group'), 'guest_media_request', 'guest_group', ['guest_id'], ['id']) + op.drop_constraint(op.f('fk_guest_interview_guest_id_guest_group'), 'guest_interview', type_='foreignkey') + op.create_foreign_key(op.f('fk_guest_interview_guest_id_guest_group'), 'guest_interview', 'guest_group', ['guest_id'], ['id']) + op.drop_constraint(op.f('fk_guest_info_guest_id_guest_group'), 'guest_info', type_='foreignkey') + op.create_foreign_key(op.f('fk_guest_info_guest_id_guest_group'), 'guest_info', 'guest_group', ['guest_id'], ['id']) + op.drop_constraint(op.f('fk_guest_image_guest_id_guest_group'), 'guest_image', type_='foreignkey') + op.create_foreign_key(op.f('fk_guest_image_guest_id_guest_group'), 'guest_image', 'guest_group', ['guest_id'], ['id']) + op.drop_constraint(op.f('fk_guest_hospitality_guest_id_guest_group'), 'guest_hospitality', type_='foreignkey') + op.create_foreign_key(op.f('fk_guest_hospitality_guest_id_guest_group'), 'guest_hospitality', 'guest_group', ['guest_id'], ['id']) + op.drop_constraint(op.f('fk_guest_group_event_id_event'), 'guest_group', type_='foreignkey') + op.drop_constraint(op.f('fk_guest_group_group_id_group'), 'guest_group', type_='foreignkey') + op.create_foreign_key(op.f('fk_guest_group_event_id_event'), 'guest_group', 'event', ['event_id'], ['id'], ondelete='SET NULL') + op.create_foreign_key(op.f('fk_guest_group_group_id_group'), 'guest_group', 'group', ['group_id'], ['id']) + op.drop_constraint(op.f('uq_guest_group_group_id'), 'guest_group', type_='unique') + op.drop_constraint(op.f('fk_guest_detailed_travel_plan_travel_plans_id_guest_travel_plans'), 'guest_detailed_travel_plan', type_='foreignkey') + op.create_foreign_key(op.f('fk_guest_detailed_travel_plan_travel_plans_id_guest_tra_6ad4'), 'guest_detailed_travel_plan', 'guest_travel_plans', ['travel_plans_id'], ['id']) + op.alter_column('guest_detailed_travel_plan', 'travel_plans_id', + existing_type=sa.UUID(), + nullable=True) + op.drop_constraint(op.f('fk_guest_charity_guest_id_guest_group'), 'guest_charity', type_='foreignkey') + op.create_foreign_key(op.f('fk_guest_charity_guest_id_guest_group'), 'guest_charity', 'guest_group', ['guest_id'], ['id']) + op.drop_constraint(op.f('fk_guest_bio_guest_id_guest_group'), 'guest_bio', type_='foreignkey') + op.create_foreign_key(op.f('fk_guest_bio_guest_id_guest_group'), 'guest_bio', 'guest_group', ['guest_id'], ['id']) + op.drop_constraint(op.f('fk_guest_autograph_guest_id_guest_group'), 'guest_autograph', type_='foreignkey') + op.create_foreign_key(op.f('fk_guest_autograph_guest_id_guest_group'), 'guest_autograph', 'guest_group', ['guest_id'], ['id']) + op.drop_constraint(op.f('fk_group_shared_with_id_group'), 'group', type_='foreignkey') + op.drop_constraint(op.f('fk_group_leader_id_attendee'), 'group', type_='foreignkey') + op.create_foreign_key(op.f('fk_leader'), 'group', 'attendee', ['leader_id'], ['id'], ondelete='SET NULL') + op.create_foreign_key(op.f('fk_group_shared_with_id_group'), 'group', 'group', ['shared_with_id'], ['id'], ondelete='SET NULL') + op.drop_constraint(op.f('fk_food_restrictions_attendee_id_attendee'), 'food_restrictions', type_='foreignkey') + op.create_foreign_key(op.f('fk_food_restrictions_attendee_id_attendee'), 'food_restrictions', 'attendee', ['attendee_id'], ['id']) + op.drop_constraint(op.f('fk_event_location_department_id_department'), 'event_location', type_='foreignkey') + op.create_foreign_key(op.f('fk_event_location_department_id_department'), 'event_location', 'department', ['department_id'], ['id'], ondelete='SET NULL') + op.drop_constraint(op.f('fk_event_feedback_event_id_event'), 'event_feedback', type_='foreignkey') + op.create_foreign_key(op.f('fk_event_feedback_event_id_event'), 'event_feedback', 'event', ['event_id'], ['id']) + op.drop_constraint(op.f('fk_event_attraction_event_id_attraction_event'), 'event', type_='foreignkey') + op.drop_constraint(op.f('fk_event_event_location_id_event_location'), 'event', type_='foreignkey') + op.drop_constraint(op.f('fk_event_department_id_department'), 'event', type_='foreignkey') + op.create_foreign_key(op.f('fk_event_attraction_event_id_attraction_event'), 'event', 'attraction_event', ['attraction_event_id'], ['id'], ondelete='SET NULL') + op.create_foreign_key(op.f('fk_event_location_id_event_location'), 'event', 'event_location', ['event_location_id'], ['id'], ondelete='SET NULL') + op.create_foreign_key(op.f('fk_event_department_id_department'), 'event', 'department', ['department_id'], ['id'], ondelete='SET NULL') + op.drop_constraint(op.f('uq_event_attraction_event_id'), 'event', type_='unique') + op.drop_constraint(op.f('fk_email_automated_email_id_automated_email'), 'email', type_='foreignkey') + op.create_foreign_key(op.f('fk_email_automated_email_id_automated_email'), 'email', 'automated_email', ['automated_email_id'], ['id'], ondelete='SET NULL') + op.drop_constraint(op.f('fk_dept_role_department_id_department'), 'dept_role', type_='foreignkey') + op.create_foreign_key(op.f('fk_dept_role_department_id_department'), 'dept_role', 'department', ['department_id'], ['id']) + op.drop_constraint(op.f('fk_dept_membership_request_department_id_department'), 'dept_membership_request', type_='foreignkey') + op.drop_constraint(op.f('fk_dept_membership_request_attendee_id_attendee'), 'dept_membership_request', type_='foreignkey') + op.create_foreign_key(op.f('fk_dept_membership_request_attendee_id_attendee'), 'dept_membership_request', 'attendee', ['attendee_id'], ['id']) + op.create_foreign_key(op.f('fk_dept_membership_request_department_id_department'), 'dept_membership_request', 'department', ['department_id'], ['id']) + op.drop_constraint(op.f('fk_dept_membership_attendee_id_attendee'), 'dept_membership', type_='foreignkey') + op.drop_constraint(op.f('fk_dept_membership_department_id_department'), 'dept_membership', type_='foreignkey') + op.create_foreign_key(op.f('fk_dept_membership_attendee_id_attendee'), 'dept_membership', 'attendee', ['attendee_id'], ['id']) + op.create_foreign_key(op.f('fk_dept_membership_department_id_department'), 'dept_membership', 'department', ['department_id'], ['id']) + op.drop_constraint(op.f('fk_dept_checklist_item_attendee_id_attendee'), 'dept_checklist_item', type_='foreignkey') + op.drop_constraint(op.f('fk_dept_checklist_item_department_id_department'), 'dept_checklist_item', type_='foreignkey') + op.create_foreign_key(op.f('fk_dept_checklist_item_attendee_id_attendee'), 'dept_checklist_item', 'attendee', ['attendee_id'], ['id']) + op.create_foreign_key(op.f('fk_dept_checklist_item_department_id_department'), 'dept_checklist_item', 'department', ['department_id'], ['id']) + op.drop_constraint(op.f('fk_department_parent_id_department'), 'department', type_='foreignkey') + op.create_foreign_key(op.f('fk_department_parent_id_department'), 'department', 'department', ['parent_id'], ['id']) + op.drop_constraint(op.f('uq_department_name'), 'department', type_='unique') + op.create_unique_constraint(op.f('department_name_key'), 'department', ['name'], postgresql_nulls_not_distinct=False) + op.add_column('bulk_printing_request', sa.Column('required', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=False)) + op.drop_constraint(op.f('fk_bulk_printing_request_department_id_department'), 'bulk_printing_request', type_='foreignkey') + op.create_foreign_key(op.f('fk_bulk_printing_request_department_id_department'), 'bulk_printing_request', 'department', ['department_id'], ['id']) + op.drop_column('bulk_printing_request', 'important') + op.drop_constraint(op.f('fk_badge_info_attendee_id_attendee'), 'badge_info', type_='foreignkey') + op.create_foreign_key(op.f('fk_badge_info_attendee_id_attendee'), 'badge_info', 'attendee', ['attendee_id'], ['id'], ondelete='SET NULL') + op.drop_constraint(op.f('fk_attraction_signup_attraction_event_id_attraction_event'), 'attraction_signup', type_='foreignkey') + op.drop_constraint(op.f('fk_attraction_signup_attendee_id_attendee'), 'attraction_signup', type_='foreignkey') + op.drop_constraint(op.f('fk_attraction_signup_attraction_id_attraction'), 'attraction_signup', type_='foreignkey') + op.create_foreign_key(op.f('fk_attraction_signup_attraction_event_id_attraction_event'), 'attraction_signup', 'attraction_event', ['attraction_event_id'], ['id']) + op.create_foreign_key(op.f('fk_attraction_signup_attraction_id_attraction'), 'attraction_signup', 'attraction', ['attraction_id'], ['id']) + op.create_foreign_key(op.f('fk_attraction_signup_attendee_id_attendee'), 'attraction_signup', 'attendee', ['attendee_id'], ['id']) + op.drop_constraint(op.f('fk_attraction_notification_attendee_id_attendee'), 'attraction_notification', type_='foreignkey') + op.drop_constraint(op.f('fk_attraction_notification_attraction_id_attraction'), 'attraction_notification', type_='foreignkey') + op.drop_constraint(op.f('fk_attraction_notification_attraction_event_id_attraction_event'), 'attraction_notification', type_='foreignkey') + op.create_foreign_key(op.f('fk_attraction_notification_attraction_id_attraction'), 'attraction_notification', 'attraction', ['attraction_id'], ['id']) + op.create_foreign_key(op.f('fk_attraction_notification_attendee_id_attendee'), 'attraction_notification', 'attendee', ['attendee_id'], ['id']) + op.create_foreign_key(op.f('fk_attraction_notification_attraction_event_id_attraction_event'), 'attraction_notification', 'attraction_event', ['attraction_event_id'], ['id']) + op.drop_constraint(op.f('fk_attraction_feature_attraction_id_attraction'), 'attraction_feature', type_='foreignkey') + op.create_foreign_key(op.f('fk_attraction_feature_attraction_id_attraction'), 'attraction_feature', 'attraction', ['attraction_id'], ['id']) + op.drop_constraint(op.f('uq_attraction_feature_name'), 'attraction_feature', type_='unique') + op.create_unique_constraint(op.f('attraction_feature_name_attraction_id_key'), 'attraction_feature', ['name', 'attraction_id'], postgresql_nulls_not_distinct=False) + op.drop_constraint(op.f('fk_attraction_event_event_location_id_event_location'), 'attraction_event', type_='foreignkey') + op.drop_constraint(op.f('fk_attraction_event_attraction_feature_id_attraction_feature'), 'attraction_event', type_='foreignkey') + op.drop_constraint(op.f('fk_attraction_event_attraction_id_attraction'), 'attraction_event', type_='foreignkey') + op.create_foreign_key(op.f('fk_attraction_event_attraction_feature_id_attraction_feature'), 'attraction_event', 'attraction_feature', ['attraction_feature_id'], ['id']) + op.create_foreign_key(op.f('fk_attraction_event_attraction_id_attraction'), 'attraction_event', 'attraction', ['attraction_id'], ['id']) + op.create_foreign_key(op.f('fk_attraction_event_location_id_event_location'), 'attraction_event', 'event_location', ['event_location_id'], ['id'], ondelete='SET NULL') + op.drop_constraint(op.f('uq_attraction_name'), 'attraction', type_='unique') + op.create_unique_constraint(op.f('attraction_name_key'), 'attraction', ['name'], postgresql_nulls_not_distinct=False) + op.add_column('attendee', sa.Column('affiliate', sa.VARCHAR(), server_default=sa.text("''::character varying"), autoincrement=False, nullable=False)) + op.drop_constraint(op.f('fk_attendee_creator_id_attendee'), 'attendee', type_='foreignkey') + op.drop_constraint(op.f('fk_attendee_group_id_group'), 'attendee', type_='foreignkey') + op.drop_constraint(op.f('fk_attendee_badge_pickup_group_id_badge_pickup_group'), 'attendee', type_='foreignkey') + op.drop_constraint(op.f('fk_attendee_watchlist_id_watch_list'), 'attendee', type_='foreignkey') + op.create_foreign_key(op.f('fk_attendee_watchlist_id_watch_list'), 'attendee', 'watch_list', ['watchlist_id'], ['id'], ondelete='SET NULL') + op.create_foreign_key(op.f('fk_attendee_badge_pickup_group_id_badge_pickup_group'), 'attendee', 'badge_pickup_group', ['badge_pickup_group_id'], ['id'], ondelete='SET NULL') + op.create_foreign_key(op.f('fk_attendee_creator_id_attendee'), 'attendee', 'attendee', ['creator_id'], ['id'], ondelete='SET NULL') + op.create_foreign_key(op.f('fk_attendee_group_id_group'), 'attendee', 'group', ['group_id'], ['id'], ondelete='SET NULL') + op.drop_constraint(op.f('fk_artist_marketplace_application_attendee_id_attendee'), 'artist_marketplace_application', type_='foreignkey') + op.create_foreign_key(op.f('fk_artist_marketplace_application_attendee_id_attendee'), 'artist_marketplace_application', 'attendee', ['attendee_id'], ['id']) + op.drop_constraint(op.f('uq_artist_marketplace_application_attendee_id'), 'artist_marketplace_application', type_='unique') + op.drop_constraint(op.f('fk_art_show_receipt_attendee_id_attendee'), 'art_show_receipt', type_='foreignkey') + op.create_foreign_key(op.f('fk_art_show_receipt_attendee_id_attendee'), 'art_show_receipt', 'attendee', ['attendee_id'], ['id'], ondelete='SET NULL') + op.alter_column('art_show_receipt', 'attendee_id', + existing_type=sa.UUID(), + nullable=True) + op.drop_constraint(op.f('fk_art_show_piece_app_id_art_show_application'), 'art_show_piece', type_='foreignkey') + op.drop_constraint(op.f('fk_art_show_piece_winning_bidder_id_art_show_bidder'), 'art_show_piece', type_='foreignkey') + op.drop_constraint(op.f('fk_art_show_piece_receipt_id_art_show_receipt'), 'art_show_piece', type_='foreignkey') + op.create_foreign_key(op.f('fk_art_show_pieces_app_id_art_show_application'), 'art_show_piece', 'art_show_application', ['app_id'], ['id'], ondelete='SET NULL') + op.create_foreign_key(op.f('fk_art_show_piece_receipt_id_art_show_receipt'), 'art_show_piece', 'art_show_receipt', ['receipt_id'], ['id'], ondelete='SET NULL') + op.create_foreign_key(op.f('fk_art_show_piece_winning_bidder_id_art_show_bidder'), 'art_show_piece', 'art_show_bidder', ['winning_bidder_id'], ['id'], ondelete='SET NULL') + op.alter_column('art_show_piece', 'app_id', + existing_type=sa.UUID(), + nullable=True) + op.drop_constraint(op.f('fk_art_show_payment_receipt_id_art_show_receipt'), 'art_show_payment', type_='foreignkey') + op.create_foreign_key(op.f('fk_art_show_payment_receipt_id_art_show_receipt'), 'art_show_payment', 'art_show_receipt', ['receipt_id'], ['id'], ondelete='SET NULL') + op.alter_column('art_show_payment', 'receipt_id', + existing_type=sa.UUID(), + nullable=True) + op.drop_constraint(op.f('fk_art_show_bidder_attendee_id_attendee'), 'art_show_bidder', type_='foreignkey') + op.create_foreign_key(op.f('fk_art_show_bidder_attendee_id_attendee'), 'art_show_bidder', 'attendee', ['attendee_id'], ['id'], ondelete='SET NULL') + op.alter_column('art_show_bidder', 'attendee_id', + existing_type=sa.UUID(), + nullable=True) + op.drop_constraint(op.f('fk_art_show_application_attendee_id_attendee'), 'art_show_application', type_='foreignkey') + op.create_foreign_key(op.f('fk_art_show_application_attendee_id_attendee'), 'art_show_application', 'attendee', ['attendee_id'], ['id'], ondelete='SET NULL') + op.drop_constraint(op.f('uq_art_show_application_attendee_id'), 'art_show_application', type_='unique') + op.drop_constraint(op.f('fk_art_show_agent_code_app_id_art_show_application'), 'art_show_agent_code', type_='foreignkey') + op.drop_constraint(op.f('fk_art_show_agent_code_attendee_id_attendee'), 'art_show_agent_code', type_='foreignkey') + op.create_foreign_key(op.f('fk_art_show_agent_code_app_id_art_show_application'), 'art_show_agent_code', 'art_show_application', ['app_id'], ['id']) + op.create_foreign_key(op.f('fk_art_show_agent_code_attendee_id_attendee'), 'art_show_agent_code', 'attendee', ['attendee_id'], ['id'], ondelete='SET NULL') + op.drop_constraint(op.f('fk_art_panel_assignment_panel_id_art_show_panel'), 'art_panel_assignment', type_='foreignkey') + op.drop_constraint(op.f('fk_art_panel_assignment_app_id_art_show_application'), 'art_panel_assignment', type_='foreignkey') + op.create_foreign_key(op.f('fk_art_panel_assignment_app_id_art_show_application'), 'art_panel_assignment', 'art_show_application', ['app_id'], ['id']) + op.create_foreign_key(op.f('fk_art_panel_assignment_panel_id_art_show_panel'), 'art_panel_assignment', 'art_show_panel', ['panel_id'], ['id']) + op.drop_constraint(op.f('fk_api_token_admin_account_id_admin_account'), 'api_token', type_='foreignkey') + op.create_foreign_key(op.f('fk_api_token_admin_account_id_admin_account'), 'api_token', 'admin_account', ['admin_account_id'], ['id']) + op.drop_constraint(op.f('fk_admin_account_attendee_id_attendee'), 'admin_account', type_='foreignkey') + op.create_foreign_key(op.f('fk_admin_account_attendee_id_attendee'), 'admin_account', 'attendee', ['attendee_id'], ['id']) + op.create_table('attendee_tournament', + sa.Column('id', sa.UUID(), autoincrement=False, nullable=False), + sa.Column('first_name', sa.VARCHAR(), server_default=sa.text("''::character varying"), autoincrement=False, nullable=False), + sa.Column('last_name', sa.VARCHAR(), server_default=sa.text("''::character varying"), autoincrement=False, nullable=False), + sa.Column('email', sa.VARCHAR(), server_default=sa.text("''::character varying"), autoincrement=False, nullable=False), + sa.Column('cellphone', sa.VARCHAR(), server_default=sa.text("''::character varying"), autoincrement=False, nullable=False), + sa.Column('game', sa.VARCHAR(), server_default=sa.text("''::character varying"), autoincrement=False, nullable=False), + sa.Column('availability', sa.VARCHAR(), server_default=sa.text("''::character varying"), autoincrement=False, nullable=False), + sa.Column('format', sa.VARCHAR(), server_default=sa.text("''::character varying"), autoincrement=False, nullable=False), + sa.Column('experience', sa.VARCHAR(), server_default=sa.text("''::character varying"), autoincrement=False, nullable=False), + sa.Column('needs', sa.VARCHAR(), server_default=sa.text("''::character varying"), autoincrement=False, nullable=False), + sa.Column('why', sa.VARCHAR(), server_default=sa.text("''::character varying"), autoincrement=False, nullable=False), + sa.Column('volunteering', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=False), + sa.Column('status', sa.INTEGER(), server_default=sa.text('239694250'), autoincrement=False, nullable=False), + sa.Column('created', postgresql.TIMESTAMP(timezone=True), server_default=sa.text("timezone('utc'::text, CURRENT_TIMESTAMP)"), autoincrement=False, nullable=False), + sa.Column('last_updated', postgresql.TIMESTAMP(timezone=True), server_default=sa.text("timezone('utc'::text, CURRENT_TIMESTAMP)"), autoincrement=False, nullable=False), + sa.Column('external_id', postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text("'{}'::jsonb"), autoincrement=False, nullable=False), + sa.Column('last_synced', postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text("'{}'::jsonb"), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('id', name=op.f('pk_attendee_tournament')) + ) diff --git a/alembic/versions/d0c15c44a031_add_extra_donation_column.py b/alembic/versions/d0c15c44a031_add_extra_donation_column.py index 6c2495ea9..7d51943f6 100644 --- a/alembic/versions/d0c15c44a031_add_extra_donation_column.py +++ b/alembic/versions/d0c15c44a031_add_extra_donation_column.py @@ -58,7 +58,7 @@ def upgrade(): batch_op.add_column(sa.Column('extra_donation', sa.Integer(), server_default='0', nullable=False)) else: # Because this column used to be in an event plugin, we check for its existence before making it - exists = conn.execute("SELECT COLUMN_NAME FROM information_schema.columns where TABLE_NAME = 'attendee' and COLUMN_NAME = 'extra_donation'").fetchall() + exists = conn.execute(sa.text("SELECT COLUMN_NAME FROM information_schema.columns where TABLE_NAME = 'attendee' and COLUMN_NAME = 'extra_donation'")).fetchall() if not exists: op.add_column('attendee', sa.Column('extra_donation', sa.Integer(), server_default='0', nullable=False)) diff --git a/docs/script_example.py b/docs/script_example.py index 4d3ab8544..1d2e37cba 100644 --- a/docs/script_example.py +++ b/docs/script_example.py @@ -6,4 +6,3 @@ with Session() as session: initialize_db() - session = Session().session \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e8dcc6c8e..509b4dfe7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,7 @@ configobj==5.0.9 email_validator==2.2.0 fpdf2==2.8.3 geopy==2.4.1 +TatSu==5.7.4 ics==0.7.2 Jinja2==3.1.6 ortools==9.14.6206 @@ -31,6 +32,7 @@ rpctools @ git+https://github.com/appliedsec/rpctools.git@4e4108c3b7b4b6c482e515 sentry-sdk==2.32.0 signnow_python_sdk==2.0.1 SQLAlchemy==2.0.46 +sqlmodel==0.0.37 stripe==9.1.0 twilio==9.6.5 uszipcode==1.0.1 diff --git a/tests/uber/models/test_badge_funcs.py b/tests/uber/models/test_badge_funcs.py index afccca8a6..3c50abfae 100644 --- a/tests/uber/models/test_badge_funcs.py +++ b/tests/uber/models/test_badge_funcs.py @@ -12,7 +12,7 @@ @pytest.fixture def session(request): - session = Session().session + session = Session() request.addfinalizer(session.close) check_ranges(session) for badge_type, badge_name in [(c.STAFF_BADGE, 'Staff'), (c.CONTRACTOR_BADGE, 'Contractor')]: diff --git a/tests/uber/models/test_config.py b/tests/uber/models/test_config.py index 19bb22059..774f652db 100644 --- a/tests/uber/models/test_config.py +++ b/tests/uber/models/test_config.py @@ -44,7 +44,7 @@ def test_under_limit_no_price_bump(self): def test_over_limit_price_bump_before_event(self, monkeypatch): monkeypatch.setattr(c, 'EPOCH', localized_now() + timedelta(days=1)) - session = Session().session + session = Session() assert c.BADGES_SOLD == 0 with request_cached_context(): @@ -57,7 +57,7 @@ def test_over_limit_price_bump_before_event(self, monkeypatch): def test_over_limit_price_bump_during_event(self, monkeypatch): monkeypatch.setattr(c, 'EPOCH', localized_now() - timedelta(days=1)) - session = Session().session + session = Session() assert c.BADGES_SOLD == 0 with request_cached_context(): @@ -69,7 +69,7 @@ def test_over_limit_price_bump_during_event(self, monkeypatch): def test_refunded_badge_price_bump_before_event(self, monkeypatch): monkeypatch.setattr(c, 'EPOCH', localized_now() + timedelta(days=1)) - session = Session().session + session = Session() assert c.BADGES_SOLD == 0 with request_cached_context(): @@ -81,7 +81,7 @@ def test_refunded_badge_price_bump_before_event(self, monkeypatch): def test_refunded_badge_price_bump_during_event(self, monkeypatch): monkeypatch.setattr(c, 'EPOCH', localized_now() - timedelta(days=1)) - session = Session().session + session = Session() assert c.BADGES_SOLD == 0 with request_cached_context(): @@ -92,7 +92,7 @@ def test_refunded_badge_price_bump_during_event(self, monkeypatch): assert 40 == c.get_attendee_price() def test_invalid_badge_no_price_bump(self): - session = Session().session + session = Session() assert c.BADGES_SOLD == 0 with request_cached_context(): @@ -103,7 +103,7 @@ def test_invalid_badge_no_price_bump(self): assert 40 == c.get_attendee_price() def test_free_badge_no_price_bump(self): - session = Session().session + session = Session() assert c.BADGES_SOLD == 0 with request_cached_context(): @@ -249,7 +249,7 @@ def test_dealer_reg_soft_closed_after_deadline(self, monkeypatch): assert c.DEALER_REG_SOFT_CLOSED def test_dealer_app(self): - session = Session().session + session = Session() with request_cached_context(): session.add(Group(tables=1, cost=10, status=c.UNAPPROVED)) session.commit() @@ -257,7 +257,7 @@ def test_dealer_app(self): assert c.DEALER_APPS == 1 def test_waitlisted_dealer_not_app(self): - session = Session().session + session = Session() with request_cached_context(): session.add(Group(tables=1, cost=10, status=c.WAITLISTED)) session.commit() @@ -265,7 +265,7 @@ def test_waitlisted_dealer_not_app(self): assert c.DEALER_APPS == 0 def test_free_dealer_no_app(self): - session = Session().session + session = Session() with request_cached_context(): session.add(Group(tables=1, cost=0, auto_recalc=False, status=c.UNAPPROVED)) session.commit() @@ -273,7 +273,7 @@ def test_free_dealer_no_app(self): assert c.DEALER_APPS == 0 def test_not_a_dealer_no_app(self): - session = Session().session + session = Session() with request_cached_context(): session.add(Group(tables=0, cost=10, status=c.UNAPPROVED)) session.commit() diff --git a/tests/uber/models/test_group.py b/tests/uber/models/test_group.py index 9a3cecb30..fff71f65b 100644 --- a/tests/uber/models/test_group.py +++ b/tests/uber/models/test_group.py @@ -11,7 +11,7 @@ @pytest.fixture def session(request, monkeypatch): - session = Session().session + session = Session() request.addfinalizer(session.close) monkeypatch.setattr(session, 'add', Mock()) monkeypatch.setattr(session, 'delete', Mock()) diff --git a/tests/uber/models/test_job.py b/tests/uber/models/test_job.py index 420f2fc45..22509235b 100644 --- a/tests/uber/models/test_job.py +++ b/tests/uber/models/test_job.py @@ -31,7 +31,7 @@ def test_total_hours(monkeypatch): @pytest.fixture def session(request): - session = Session().session + session = Session() for num in ['One', 'Two', 'Three', 'Four', 'Five', 'Six']: setattr(session, 'job_' + num.lower(), session.job(name='Job ' + num)) for num in ['One', 'Two', 'Three', 'Four', 'Five']: diff --git a/tests/uber/models/test_watchlist.py b/tests/uber/models/test_watchlist.py index b643a37e2..536c9fcc3 100644 --- a/tests/uber/models/test_watchlist.py +++ b/tests/uber/models/test_watchlist.py @@ -8,7 +8,7 @@ @pytest.fixture() def session(request): - session = Session().session + session = Session() request.addfinalizer(session.close) setattr(session, 'watchlist_entry', session.watch_list(first_names='Banned, Alias, Nickname', last_name='Attendee')) return session diff --git a/tests/uber/test_custom_tags.py b/tests/uber/test_custom_tags.py index fa0a822dd..5a484a90a 100644 --- a/tests/uber/test_custom_tags.py +++ b/tests/uber/test_custom_tags.py @@ -37,7 +37,7 @@ def test_filters_allow_empty_arg(self, filter_function, test_input, expected): ([1, 30, 100, 100, 20, 12, 2], {}), ]) def test_timedelta_filter(self, timedelta_args, timedelta_kwargs): - dt = datetime.utcnow() + dt = datetime.now(UTC) td = timedelta(*timedelta_args, **timedelta_kwargs) expected = dt + td assert expected == timedelta_filter(dt, *timedelta_args, **timedelta_kwargs) @@ -49,7 +49,7 @@ def test_timedelta_filter_with_empty_date(self): assert timedelta_filter('', 1, 3600) is None def test_timedelta_filter_in_template(self): - dt = datetime.utcnow() + dt = datetime.now(UTC) env = JinjaEnv.env() template = env.from_string('{{ dt|timedelta(days=-5)|datetime("%A, %B %-e") }}') expected = (dt + timedelta(days=-5)).strftime("%A, %B %-e") diff --git a/uber/api.py b/uber/api.py index e3f4ea426..c834ad38e 100644 --- a/uber/api.py +++ b/uber/api.py @@ -1,7 +1,6 @@ import re import uuid from collections import defaultdict -from collections.abc import Iterable from datetime import datetime from functools import wraps @@ -16,7 +15,7 @@ from dateutil import parser as dateparser from time import mktime from sqlalchemy import and_, func, or_, not_ -from sqlalchemy.orm import subqueryload +from sqlalchemy.orm import subqueryload, joinedload, selectinload from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound from sqlalchemy.types import Boolean, Date, DateTime @@ -29,7 +28,7 @@ GuestGroup, Room, HotelRequests, RoomAssignment) from uber.models.badge_printing import PrintJob from uber.serializer import serializer -from uber.utils import check, check_csrf, normalize_email_legacy, normalize_newlines +from uber.utils import check, check_csrf, normalize_email_legacy, normalize_newlines, is_listy log = logging.getLogger(__name__) @@ -77,7 +76,7 @@ def error(status, code, message): def success(result): response = {'jsonrpc': '2.0', 'id': id, 'result': result} log.debug('Returning success message: {}', { - 'jsonrpc': '2.0', 'id': id, 'result': len(result) if isinstance(result, Iterable) and not isinstance(result, str) else str(result).encode('utf-8')}) + 'jsonrpc': '2.0', 'id': id, 'result': len(result) if is_listy(result) else str(result).encode('utf-8')}) cherrypy.response.status = 200 return response @@ -161,13 +160,15 @@ def _attendee_fields_and_query(full, query, only_valid=True): if full: fields = AttendeeLookup.fields_full query = query.options( - subqueryload(Attendee.dept_memberships), - subqueryload(Attendee.assigned_depts), - subqueryload(Attendee.food_restrictions), - subqueryload(Attendee.shifts).subqueryload(Shift.job)) + selectinload(Attendee.dept_memberships).joinedload(DeptMembership.department), + selectinload(Attendee.assigned_depts), + selectinload(Attendee.dept_roles).joinedload(DeptRole.department), + selectinload(Attendee.shifts).joinedload(Shift.job), + selectinload(Attendee.food_restrictions), + selectinload(Attendee.managers), joinedload(Attendee.group)) else: fields = AttendeeLookup.fields - query = query.options(subqueryload(Attendee.dept_memberships)) + query = query.options(selectinload(Attendee.dept_memberships), selectinload(Attendee.shifts).joinedload(Shift.job),) return (fields, query) @@ -219,8 +220,8 @@ def _prepare_attendees_export(attendees, include_account_ids=False, include_apps d['attendee_account_ids'] = [m.id for m in a.managers] if include_apps: - if a.art_show_applications: - d['art_show_app'] = a.art_show_applications[0].to_dict(art_show_import_fields) + if a.art_show_application: + d['art_show_app'] = a.art_show_application.to_dict(art_show_import_fields) if a.marketplace_application: d['marketplace_app'] = a.marketplace_application.to_dict(marketplace_import_fields) @@ -310,7 +311,7 @@ def _parse_datetime(d): def _parse_if_datetime(key, val): # This should be in the DateTime and Date classes, but they're not defined in this app if hasattr(getattr(Attendee, key), 'type') and ( - isinstance(getattr(Attendee, key).type, DateTime) or isinstance(getattr(Attendee, key).type, sa.types.time) or isinstance(getattr(Attendee, key).type, Date)): + isinstance(getattr(Attendee, key).type, DateTime) or isinstance(getattr(Attendee, key).type, Date) or isinstance(getattr(Attendee, key).type, Date)): return _parse_datetime(val) return val @@ -508,7 +509,6 @@ def export_judges(self): with Session() as session: judges = session.query(IndieJudge).filter(not_(IndieJudge.status.in_([c.CANCELLED, c.DISQUALIFIED]))) - for judge in judges: fields = AttendeeLookup.attendee_import_fields + Attendee.import_fields judges_list.append((judge.to_dict(), judge.attendee.to_dict(fields))) @@ -693,10 +693,16 @@ def export(self, query, full=False): with Session() as session: if full: options = [ - subqueryload(Attendee.dept_memberships).subqueryload(DeptMembership.department), - subqueryload(Attendee.dept_roles).subqueryload(DeptRole.department)] + selectinload(Attendee.dept_memberships).joinedload(DeptMembership.department), + selectinload(Attendee.dept_roles).joinedload(DeptRole.department), + selectinload(Attendee.shifts).joinedload(Shift.job), + selectinload(Attendee.food_restrictions), + selectinload(Attendee.managers), joinedload(Attendee.group) + ] else: - options = [] + options = [ + selectinload(Attendee.shifts).joinedload(Shift.job), + ] email_attendees = [] if emails: @@ -867,9 +873,22 @@ def export_attendees(self, id, full=False, include_group=False): if not account: raise HTTPError(404, 'No attendee account found with this ID') - - attendees_to_export = account.valid_attendees if include_group \ - else [a for a in account.valid_attendees if not a.group] + + filters = [Attendee.is_valid == True] + if not include_group: + filters.append(Attendee.group_id == None) + + attendees_to_export = session.query(Attendee).join(Attendee.managers).filter( + AttendeeAccount.id == id).filter(*filters).options( + selectinload(Attendee.dept_memberships).joinedload(DeptMembership.department), + selectinload(Attendee.dept_roles).joinedload(DeptRole.department), + selectinload(Attendee.shifts).joinedload(Shift.job), + selectinload(Attendee.food_restrictions), + selectinload(Attendee.managers), + joinedload(Attendee.group), + joinedload(Attendee.art_show_application), + joinedload(Attendee.marketplace_application) + ) attendees = _prepare_attendees_export(attendees_to_export, include_apps=full) return { @@ -900,7 +919,7 @@ def export(self, query, all=False): if emails: email_accounts = session.query(AttendeeAccount).filter( AttendeeAccount.email.in_(list(emails.keys())) - ).options(subqueryload(AttendeeAccount.attendees) + ).options(selectinload(AttendeeAccount.attendees) ).order_by(AttendeeAccount.email, AttendeeAccount.id).all() known_emails = set(a.normalized_email for a in email_accounts) @@ -909,7 +928,7 @@ def export(self, query, all=False): id_accounts = [] if ids: id_accounts = session.query(AttendeeAccount).filter( - AttendeeAccount.id.in_(ids)).options(subqueryload(AttendeeAccount.attendees) + AttendeeAccount.id.in_(ids)).options(selectinload(AttendeeAccount.attendees) ).order_by(AttendeeAccount.email, AttendeeAccount.id).all() @@ -1699,7 +1718,7 @@ def get_pending(self, printer_ids='', restart=False, dry_run=False): if not restart or not errors: results[job.id] = self._build_job_json_data(job) if not dry_run: - job.queued = datetime.utcnow() + job.queued = datetime.now(UTC) session.add(job) session.commit() @@ -1793,7 +1812,7 @@ def mark_complete(self, job_ids=''): for job in jobs: results[job.id] = self._build_job_json_data(job) - job.printed = datetime.utcnow() + job.printed = datetime.now(UTC) session.add(job) session.commit() @@ -1839,7 +1858,7 @@ def clear_jobs(self, printer_ids='', all=False, invalidate=False, error=''): else: job.errors = error else: - job.printed = datetime.utcnow() + job.printed = datetime.now(UTC) session.add(job) session.commit() diff --git a/uber/config.py b/uber/config.py index ad7e4b90d..be51d2785 100644 --- a/uber/config.py +++ b/uber/config.py @@ -28,7 +28,7 @@ import cherrypy import signnow_python_sdk from sqlalchemy import or_, func -from sqlalchemy.orm import joinedload, subqueryload +from sqlalchemy.orm import joinedload, selectinload import uber @@ -1033,11 +1033,12 @@ def CURRENT_ADMIN(self): from uber.models import Session, AdminAccount, Attendee with Session() as session: attrs = Attendee.to_dict_default_attrs + ['admin_account', 'assigned_depts', 'logged_in_name'] - admin_account = session.query(AdminAccount) \ - .filter_by(id=cherrypy.session.get('account_id')) \ - .options(subqueryload(AdminAccount.attendee).subqueryload(Attendee.assigned_depts)).one() - - return admin_account.attendee.to_dict(attrs) + admin_attendee = session.query(Attendee).join(Attendee.admin_account) \ + .filter(AdminAccount.id == cherrypy.session.get('account_id')) \ + .options( + joinedload(Attendee.admin_account), + selectinload(Attendee.assigned_depts)).one() + return admin_attendee.to_dict(attrs) except Exception: return {} @@ -2110,27 +2111,6 @@ def _unrepr(d): setattr(c, _attr + '_NAMES', [c.NIGHTS[night] for night in getattr(c, _attr + 'S')]) -# ============================= -# attendee_tournaments -# -# NO LONGER USED. -# -# The attendee_tournaments module is no longer used, but has been -# included for backward compatibility with legacy servers. -# ============================= - -c.TOURNAMENT_AVAILABILITY_OPTS = [] -_val = 0 -for _day in range((c.ESCHATON - c.EPOCH).days): - for _when in ['Morning (8am-12pm)', 'Afternoon (12pm-6pm)', 'Evening (6pm-10pm)', 'Night (10pm-2am)']: - c.TOURNAMENT_AVAILABILITY_OPTS.append([ - _val, - _when + ' of ' + (c.EPOCH + timedelta(days=_day)).strftime('%A %B %d') - ]) - _val += 1 -c.TOURNAMENT_AVAILABILITY_OPTS.append([_val, 'Morning (8am-12pm) of ' + c.ESCHATON.strftime('%A %B %d')]) - - # ============================= # mivs # ============================= diff --git a/uber/configspec.ini b/uber/configspec.ini index 61da7051e..74d4a3a69 100644 --- a/uber/configspec.ini +++ b/uber/configspec.ini @@ -619,16 +619,6 @@ enable_pending_emails_report = boolean(default=True) # when we have these number of attendee badges left. badges_left_alerts = string_list(default=list('1500', '250')) - -# NO LONGER USED. -# -# The attendee_tournaments module is no longer used, but has been -# included for backward compatibility with legacy servers. -# -# Side note: I'm on the fence about using "art" as an abbreviation for Attendee-Run Tournament. -art_email = string(default="MAGFest Console Department ") -art_email_signature = string(default="- The MAGFest Console Department") - # This URL is printed in the hotel lottery pages to direct attendees to read more about the lottery. hotel_lottery_url = string(default="") @@ -1735,15 +1725,6 @@ __many__ = string # Keys should be the first part of the email address to attach this signature to __many__ = string -# NO LONGER USED. -# -# The attendee_tournaments module is no longer used, but has been -# included for backward compatibility with legacy servers. -[[tournament_status]] -new = string(default="New") -accepted = string(default="Accepted") -declined = string(default="Declined") - [[showcase_game_type]] mivs = string(default="MIVS") indie_arcade = string(default="Indie Arcade") diff --git a/uber/custom_tags.py b/uber/custom_tags.py index 96e2c23c2..0d7a13341 100644 --- a/uber/custom_tags.py +++ b/uber/custom_tags.py @@ -117,7 +117,7 @@ def full_date_local(dt): @JinjaEnv.jinja_export def now(): - return datetime.utcnow() + return datetime.now(UTC) @JinjaEnv.jinja_export diff --git a/uber/forms/__init__.py b/uber/forms/__init__.py index 9172faac3..173f0c68f 100644 --- a/uber/forms/__init__.py +++ b/uber/forms/__init__.py @@ -422,7 +422,10 @@ def process(self, formdata={}, obj=None, data=None, extra_filters=None, elif obj_data: formdata[prefixed_name] = getattr(obj, name) elif isinstance(field.widget, DateMaskInput) and not field_in_formdata and getattr(obj, name, None): - formdata[prefixed_name] = getattr(obj, name).strftime('%m/%d/%Y') + obj_date = getattr(obj, name) + if isinstance(obj_date, six.string_types): + obj_date = dateparser.parse(obj_date) + formdata[prefixed_name] = obj_date.strftime('%m/%d/%Y') elif isinstance(field, DateField) and not field_in_formdata and getattr(obj, name, None): formdata[prefixed_name] = str(getattr(obj, name)) elif isinstance(field.widget, UniqueList) and field_in_formdata and cherrypy.request.method == 'POST': @@ -465,7 +468,7 @@ def populate_obj(self, obj, is_admin=False): else: try: setattr(obj, name, field.data) - except AttributeError as e: + except (AttributeError, ValueError) as e: pass # Indicates collision between a property name and a field name, like 'badges' for GroupInfo for model_field_name, aliases in self.field_aliases.items(): diff --git a/uber/forms/department.py b/uber/forms/department.py index 5560d9103..840a6cbe7 100644 --- a/uber/forms/department.py +++ b/uber/forms/department.py @@ -121,7 +121,8 @@ class BulkPrintingRequestInfo(MagForm): description="If you want the same content on the front and back, please make sure your document has each page duplicated.") stapled = BooleanField("Please staple the pages of this document together.") notes = TextAreaField("Additional Information") - required = BooleanField("This document is vital to my department.", - description="We will do our best to print all submitted documents, but this will help us prioritize the most important documents to print.") + important = BooleanField( + "This document is vital to my department.", + description="We will do our best to print all submitted documents, but this will help us prioritize the most important documents to print.") link_is_shared = BooleanField( Markup("I verify that I have checked the permissions of the link provided and made sure it is publicly accessible.")) \ No newline at end of file diff --git a/uber/forms/showcase.py b/uber/forms/showcase.py index 55db359e6..efced3ef4 100644 --- a/uber/forms/showcase.py +++ b/uber/forms/showcase.py @@ -44,7 +44,7 @@ class DeveloperInfo(MagForm): last_name = StringField('Last Name', render_kw={'autocomplete': "lname"}) email = EmailField('Email Address', render_kw={'placeholder': 'test@example.com'}) cellphone = TelField('Phone Number') - gets_emails = BooleanField('I want to receive emails about my studio\'s showcase submissions.') + receives_emails = BooleanField('I want to receive emails about my studio\'s showcase submissions.') agreed_coc = BooleanField() agreed_data_policy = BooleanField() diff --git a/uber/model_checks.py b/uber/model_checks.py index 934e5983b..115430df4 100644 --- a/uber/model_checks.py +++ b/uber/model_checks.py @@ -26,7 +26,7 @@ from uber.custom_tags import format_currency, full_date_local from uber.decorators import prereg_validation, validation from uber.models import (AccessGroup, AdminAccount, ApiToken, Attendee, ArtShowApplication, ArtShowPiece, - AttendeeTournament, Attraction, AttractionFeature, ArtShowBidder, DeptRole, Event, + Attraction, AttractionFeature, ArtShowBidder, DeptRole, Event, GuestDetailedTravelPlan, IndieDeveloper, IndieGame, IndieGameCode, IndieJudge, IndieStudio, Job, ArtistMarketplaceApplication, MITSApplicant, MITSDocument, MITSGame, MITSPicture, MITSTeam, PromoCode, PromoCodeGroup, Sale, Session, WatchList) @@ -271,35 +271,6 @@ def no_dupe_code(promo_code): ('name', 'Name') ] -# ============================= -# tournaments -# ============================= - -AttendeeTournament.required = [ - ('first_name', 'First Name'), - ('last_name', 'Last Name'), - ('email', 'Email Address'), - ('game', 'Game Title'), - ('availability', 'Your Availability'), - ('format', 'Tournament Format'), - ('experience', 'Past Experience'), - ('needs', 'Your Needs'), - ('why', '"Why?"'), -] - - -@validation.AttendeeTournament -def attendee_tournament_email(app): - if not re.match(c.EMAIL_RE, app.email): - return 'You did not enter a valid email address' - - -@validation.AttendeeTournament -def attendee_tournament_cellphone(app): - if app.cellphone and invalid_phone_number(app.cellphone): - return 'You did not enter a valid cellphone number' - - @validation.LotteryApplication def room_meets_night_requirements(app): if app.any_dates_different and (app.entry_type == c.ROOM_ENTRY or diff --git a/uber/models/__init__.py b/uber/models/__init__.py index 5c340f090..b4fdd2483 100644 --- a/uber/models/__init__.py +++ b/uber/models/__init__.py @@ -9,8 +9,10 @@ from datetime import date, datetime, timedelta from functools import wraps from itertools import chain +from pydantic import ConfigDict from uuid import uuid4 from types import MethodType +from typing import Any, ClassVar import cherrypy import six @@ -21,18 +23,21 @@ from sqlalchemy.dialects.postgresql.json import JSONB from sqlalchemy.event import listen from sqlalchemy.exc import IntegrityError, NoResultFound +from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property from sqlalchemy.ext.mutable import MutableDict -from sqlalchemy.orm import Query, joinedload, subqueryload, DeclarativeBase, declared_attr, sessionmaker, scoped_session +from sqlalchemy.orm import Query, joinedload, selectinload, subqueryload, contains_eager, declared_attr, sessionmaker, scoped_session import sqlalchemy.orm from sqlalchemy.orm.attributes import get_history, instance_state -from sqlalchemy.schema import MetaData +from sqlalchemy.orm.collections import InstrumentedList +from sqlalchemy.schema import MetaData, UniqueConstraint from sqlalchemy.types import Boolean, Integer, Float, Date, Numeric, DateTime, Uuid, JSON +from sqlmodel import SQLModel import uber from uber.config import c, create_namespace_uuid from uber.errors import HTTPRedirect from uber.decorators import cost_property, presave_adjustment, suffix_property, cached_classproperty, classproperty -from uber.models.types import Choice, DefaultColumn as Column, MultiChoice, utcnow, UniqueList +from uber.models.types import Choice, MultiChoice, utcnow, UniqueList, DefaultField as Field from uber.utils import check_csrf, normalize_email_legacy, create_new_hash, DeptChecklistConf, \ RegistrationCode, listify from uber.payments import ReceiptManager @@ -125,8 +130,18 @@ def uncamel(s, sep='_'): """ return RE_UNCAMEL.sub(r'{0}\1'.format(sep), s).lower() -DeclarativeBase.metadata = MetaData() -class MagModel(DeclarativeBase): +SQLModel.metadata.naming_convention = { + "ix": "ix_%(column_0_label)s", + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s", +} +class MagModel(SQLModel): + model_config: ClassVar = ConfigDict( + extra='allow', + ignored_types=(hybrid_method, hybrid_property)) + @declared_attr.directive def __tablename__(cls) -> str: # Convert the model name from camel to snake-case to name the db table @@ -135,27 +150,44 @@ def __tablename__(cls) -> str: def to_dict(self, fields=None): data = {} enabled_fields = [] + disabled_fields = [] + if fields: if isinstance(fields, str): enabled_fields.append(fields) elif isinstance(fields, list): enabled_fields = fields elif isinstance(fields, dict): - enabled_fields = [x for x in fields.keys() if fields[x]] + enabled_fields = [x for x in fields.keys() if fields[x] is True] + disabled_fields = [x for x in fields.keys() if fields[x] is False] - for col in (enabled_fields or self.__table__.columns): - val = getattr(self, col.name) + for field in (enabled_fields or self.to_dict_default_attrs): + val = getattr(self, field) if isinstance(val, (datetime, date)): val = val.isoformat() elif isinstance(val, uuid.UUID): val = str(val) - - data[col.name] = val + + if isinstance(val, InstrumentedList): + data[field] = [] + for model in val: + obj_fields = fields[field] if isinstance(fields, dict) else None + data[field].append(model.to_dict(obj_fields)) + if data[field] == []: + del data[field] # Empty instrumented lists are not pickleable + else: + data[field] = val + + if '_model' not in disabled_fields: + data['_model'] = self.__class__.__name__ + if 'id' not in disabled_fields: + data['id'] = self.id + return data - - id = Column(Uuid(as_uuid=False), primary_key=True, default=lambda: str(uuid4())) - created = Column(DateTime(timezone=True), server_default=utcnow(), default=lambda: datetime.now(UTC)) - last_updated = Column(DateTime(timezone=True), server_default=utcnow(), default=lambda: datetime.now(UTC)) + + id: str | None = Field(sa_type=Uuid(as_uuid=False), default_factory=lambda: str(uuid4()), primary_key=True) + created: datetime = Field(sa_type=DateTime(timezone=True), sa_column_kwargs={'server_default': utcnow()}, default_factory=lambda: datetime.now(UTC)) + last_updated: datetime = Field(sa_type=DateTime(timezone=True), sa_column_kwargs={'server_default': utcnow()}, default_factory=lambda: datetime.now(UTC)) """ The two columns below allow tracking any object in external sources, @@ -164,11 +196,55 @@ def to_dict(self, fields=None): dictionary (if there are multiple objects to track in the external service) or just strings and datetime objects, respectively. """ - external_id = Column(MutableDict.as_mutable(JSONB), server_default='{}', default={}) - last_synced = Column(MutableDict.as_mutable(JSONB), server_default='{}', default={}) + external_id: dict[str, Any] = Field(sa_type=MutableDict.as_mutable(JSONB), default_factory=dict) + last_synced: dict[str, Any] = Field(sa_type=MutableDict.as_mutable(JSONB), default_factory=dict) + + required: ClassVar = () + is_actually_old: ClassVar = False # Set to true to force preview models to return False for `is_new` + _repr_attr_names: ClassVar = () + + def __repr__(self): + """ + Useful string representation for logging. + + Note: + __repr__ does NOT return unicode on Python 2, since python decodes + it using the default encoding: http://bugs.python.org/issue5876. + + """ + # If no repr attr names have been set, default to the set of all + # unique constraints. This is unordered normally, so we'll order and + # use it here. + if not self._repr_attr_names: + unique_constraint_column_names = [[column.name for column in constraint.columns] + for constraint in self.__table__.constraints + if isinstance(constraint, UniqueConstraint)] + + # this flattens the unique constraint list + _unique_attrs = chain.from_iterable(unique_constraint_column_names) + _primary_keys = [column.name for column in self.__table__.primary_key.columns] + + attr_names = tuple(sorted(set(chain(_unique_attrs, + _primary_keys)))) + else: + attr_names = self._repr_attr_names - required = () - is_actually_old = False # Set to true to force preview models to return False for `is_new` + if not attr_names and hasattr(self, 'id'): + # there should be SOMETHING, so use id as a fallback + attr_names = ('id',) + + if attr_names: + _kwarg_list = ' '.join('%s=%s' % (name, repr(getattr(self, name, 'undefined'))) + for name in attr_names) + kwargs_output = ' %s' % _kwarg_list + else: + kwargs_output = '' + + # specifically using the string interpolation operator and the repr of + # getattr so as to avoid any "hilarious" encode errors for non-ascii + # characters + u = '<%s%s>' % (self.__class__.__name__, kwargs_output) + return u if six.PY3 else u.encode('utf-8') @cached_classproperty def NAMESPACE(cls): @@ -185,6 +261,13 @@ def _class_attr_names(cls): def _class_attrs(cls): return {s: getattr(cls, s) for s in cls._class_attr_names} + @cached_classproperty + def to_dict_default_attrs(cls): + try: + return list(cls.__table__.columns.keys()) + except AttributeError: + raise NotImplementedError("to_dict_default_attrs is only availale for tables") + def _invoke_adjustment_callbacks(self, label): callbacks = [] for name, attr in self._class_attrs.items(): @@ -483,6 +566,8 @@ def _label(self, name, val): @suffix_property def _local(self, name, val): + if isinstance(val, six.string_types): + val = dateparser.parse(val) return val.astimezone(c.EVENT_TIMEZONE) @suffix_property @@ -493,6 +578,12 @@ def _labels(self, name, val): return [labels[i].get('name', '') for i in ints] else: return sorted(labels[i] for i in ints) + + def __setattr__(self, name, value): + # Work around an issue in Pydantic where you can't assign extra properties after init + if self.__pydantic_extra__ is None: + super().__setattr__('__pydantic_extra__', {}) + super().__setattr__(name, value) def __getattr__(self, name): suffixed = suffix_property.check(self, name) @@ -506,9 +597,6 @@ def __getattr__(self, name): if choice in multi.type.choices_dict: return choice in getattr(self, multi.name + '_ints') - if name.startswith('is_'): - return self.__class__.__name__.lower() == name[3:] - if name.startswith('default_') and name.endswith('_cost'): if self.active_receipt: log.debug('Cost property {} was called for object {}, \ @@ -530,7 +618,7 @@ def __getattr__(self, name): except Exception: pass - raise AttributeError(self.__class__.__name__ + '.' + name) + return super().__getattr__(name) def get_tracking_by_instance(self, instance, action, last_only=True): from uber.models.tracking import Tracking @@ -545,7 +633,7 @@ def coerce_column_data(self, column, value): if value is None: return # Totally fine for value to be None - elif value == '' and isinstance(column.type, (Uuid(as_uuid=False), Float, Numeric, Choice, Integer, DateTime, Date)): + elif value == '' and isinstance(column.type, (Uuid, Float, Numeric, Choice, Integer, DateTime, Date)): return None elif isinstance(column.type, Boolean): @@ -687,7 +775,6 @@ def minutestr(dt): from uber.models.types import * # noqa: F401,E402,F403 from uber.models.api import * # noqa: F401,E402,F403 from uber.models.hotel import * # noqa: F401,E402,F403 -from uber.models.attendee_tournaments import * # noqa: F401,E402,F403 from uber.models.marketplace import * # noqa: F401,E402,F403 from uber.models.showcase import * # noqa: F401,E402,F403 from uber.models.mits import * # noqa: F401,E402,F403 @@ -716,7 +803,7 @@ def minutestr(dt): class UberSession(sqlalchemy.orm.Session): engine = engine - BaseClass = DeclarativeBase + BaseClass = SQLModel def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -797,7 +884,10 @@ def current_supervisor_admin(self): def admin_attendee(self): if getattr(cherrypy, 'session', {}).get('account_id'): try: - return self.admin_account(cherrypy.session.get('account_id')).attendee + return self.query(Attendee).join(Attendee.admin_account).filter( + AdminAccount.id == cherrypy.session.get('account_id')).options( + contains_eager(Attendee.admin_account) + ).one() except NoResultFound: return @@ -827,7 +917,10 @@ def get_attendee_account_by_attendee(self, attendee): return attendee.managers[0] def logged_in_volunteer(self): - return self.attendee(cherrypy.session.get('staffer_id')) + return self.query(Attendee).filter(Attendee.id == cherrypy.session.get('staffer_id')).options( + selectinload(Attendee.hotel_requests), selectinload(Attendee.food_restrictions), + selectinload(Attendee.shifts) + ).one() def admin_has_staffer_access(self, staffer, access="view"): admin = self.current_admin_account() @@ -969,7 +1062,7 @@ def access_query_matrix(self): ).outerjoin(ArtShowBidder).filter( or_(Attendee.art_show_bidder != None, # noqa: E711 Attendee.art_show_purchases != None, # noqa: E711 - Attendee.art_show_applications != None, # noqa: E711 + Attendee.art_show_application != None, # noqa: E711 Attendee.art_agent_apps != None) # noqa: E711 ).outerjoin(ArtShowAgentCode).filter( ArtShowAgentCode.attendee_id == Attendee.id, @@ -1009,7 +1102,9 @@ def checklist_status(self, slug, department_id): if not department_id: return {'conf': conf, 'relevant': False, 'completed': None} - department = self.query(Department).get(department_id) + department = self.query(Department).filter(Department.id == department_id).options( + selectinload(Department.dept_checklist_items) + ).first() if department: return { 'conf': conf, @@ -1335,7 +1430,7 @@ def attendee_from_art_show_app(self, **params): attendee, message = self.create_or_find_attendee_by_id(**params) if message: return attendee, message - elif attendee.art_show_applications: + elif attendee.art_show_application: return attendee, \ 'There is already an art show application for that badge!' @@ -2103,14 +2198,15 @@ def bulk_insert(self, models): # ======================== def logged_in_judge(self): - judge = self.admin_attendee().admin_account.judge - if judge: - return judge - else: - raise HTTPRedirect( - '../accounts/homepage?message={}', - 'You have been given judge access but not had a judge entry created for you - ' - 'please contact a MIVS admin to correct this.') + if getattr(cherrypy, 'session', {}).get('account_id'): + try: + return self.query(IndieJudge).join(IndieJudge.admin_account).filter( + AdminAccount.id == cherrypy.session.get('account_id')).one() + except NoResultFound: + raise HTTPRedirect( + '../accounts/homepage?message={}', + 'You have been given judge access but not had a judge entry created for you - ' + 'please contact a MIVS admin to correct this.') def code_for(self, game): if game.unlimited_code: @@ -2217,8 +2313,17 @@ def collect_subclasses(klass): return list(subclasses) return collect_subclasses(cls.BaseClass) + @classmethod + def resolve_model(cls, name): + models_by_class = {ModelClass.__name__: ModelClass for ModelClass in cls.all_models()} + if name in models_by_class: + return models_by_class[name] + raise ValueError('Unrecognized model: {}'.format(name)) + @classmethod def model_mixin(cls, model): + from sqlmodel.main import get_column_from_field + if model.__name__ in ['SessionMixin', 'QuerySubclass']: target = getattr(cls, model.__name__) else: @@ -2237,7 +2342,11 @@ def model_mixin(cls, model): attr.table = target.__table__ target.__table__.append_column(attr, replace_existing=True) else: - setattr(target, name, attr) + try: + col = get_column_from_field(attr) + setattr(target, name, col) + except AttributeError: + setattr(target, name, attr) return target SessionFactory = sessionmaker( @@ -2251,8 +2360,9 @@ def model_mixin(cls, model): _ScopedSession = scoped_session(SessionFactory) _ScopedSession.model_mixin = UberSession.model_mixin _ScopedSession.all_models = UberSession.all_models +_ScopedSession.resolve_model = UberSession.resolve_model _ScopedSession.engine = engine -_ScopedSession.BaseClass = DeclarativeBase +_ScopedSession.BaseClass = SQLModel _ScopedSession.SessionMixin = UberSession.SessionMixin _ScopedSession.session_factory = SessionFactory @@ -2281,7 +2391,6 @@ def initialize_db(): log.info(f"Initializing model {str(model)}") if not hasattr(Session.SessionMixin, model.__tablename__): setattr(Session.SessionMixin, model.__tablename__, _make_getter(model)) - DeclarativeBase.metadata.create_all(engine) cherrypy.engine.subscribe('start', initialize_db, priority=97) def _attendee_validity_check(): diff --git a/uber/models/admin.py b/uber/models/admin.py index 7ed3864ef..82082073c 100644 --- a/uber/models/admin.py +++ b/uber/models/admin.py @@ -5,13 +5,14 @@ from sqlalchemy import Sequence, Uuid, String, DateTime from sqlalchemy.dialects.postgresql.json import JSONB from sqlalchemy.ext.mutable import MutableDict -from sqlalchemy.orm import backref from sqlalchemy.schema import ForeignKey, Table, UniqueConstraint, Index from sqlalchemy.types import Boolean, Date, Integer +from typing import ClassVar from uber.config import c from uber.decorators import presave_adjustment, classproperty -from uber.models.types import default_relationship as relationship, utcnow, DefaultColumn as Column +from uber.models.types import (default_relationship as relationship, utcnow, + DefaultColumn as Column, DefaultField as Field, DefaultRelationship as Relationship) from uber.models import MagModel from uber.utils import listify @@ -31,27 +32,33 @@ ) -class AdminAccount(MagModel): - attendee_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id'), unique=True) - access_groups = relationship( - 'AccessGroup', backref='admin_accounts', cascade='save-update,merge,refresh-expire,expunge', - secondary='admin_access_group') - hashed = Column(String, private=True) +class AdminAccount(MagModel, table=True): + attendee_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', ondelete='CASCADE', unique=True) + attendee: 'Attendee' = Relationship(back_populates="admin_account", sa_relationship_kwargs={'lazy': 'joined'}) - password_reset = relationship('PasswordReset', backref='admin_account', uselist=False) + hashed: str = Field(sa_type=String, private=True) - api_tokens = relationship('ApiToken', backref='admin_account') - active_api_tokens = relationship( - 'ApiToken', + access_groups: list['AccessGroup'] = Relationship( + back_populates='admin_accounts', + sa_relationship_kwargs={'lazy': 'selectin', 'secondary': 'admin_access_group'}) + password_reset: "PasswordReset" = Relationship( + back_populates='admin_account', sa_relationship_kwargs={'lazy': 'select', 'cascade': 'all,delete-orphan', 'passive_deletes': True}) + api_tokens: list['ApiToken'] = Relationship(back_populates="admin_account", + sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + active_api_tokens: list['ApiToken'] = Relationship(sa_relationship=relationship( + 'ApiToken', lazy='select', primaryjoin='and_(' 'AdminAccount.id == ApiToken.admin_account_id, ' 'ApiToken.revoked_time == None)', - overlaps="admin_account,api_tokens") - - judge = relationship('IndieJudge', uselist=False, backref='admin_account') - print_requests = relationship('PrintJob', backref='admin_account', - cascade='save-update,merge,refresh-expire,expunge') - api_jobs = relationship('ApiJob', backref='admin_account', cascade='save-update,merge,refresh-expire,expunge') + overlaps="admin_account,api_tokens")) + api_jobs: list['ApiJob'] = Relationship( + back_populates="admin_account") + + attractions: list['Attraction'] = Relationship( + back_populates="owner", sa_relationship_kwargs={'order_by': 'Attraction.name'}) + judge: 'IndieJudge' = Relationship(back_populates="admin_account") + print_requests: list['PrintJob'] = Relationship( + back_populates="admin_account") def __repr__(self): return f"" @@ -184,6 +191,10 @@ def is_mivs_judge_or_admin(self, id=None): except Exception: return None + def can_admin_attraction(self, attraction): + return self.id == attraction.owner_id or ( + self.full_dept_admin or self.attendee.has_inherent_role_in(attraction.department_id)) + @property def api_read(self): return any([group.has_any_access('api', read_only=True) for group in self.access_groups]) @@ -236,7 +247,7 @@ def disable_api_access(self): self.remove_disabled_api_keys(invalid_api) def remove_disabled_api_keys(self, invalid_api): - revoked_time = datetime.utcnow() + revoked_time = datetime.now(UTC) for api_token in self.active_api_tokens: if invalid_api.intersection(api_token.access_ints): api_token.revoked_time = revoked_time @@ -254,44 +265,53 @@ def invalid_api_accesses(self): return removed_api -class PasswordReset(MagModel): - admin_id = Column(Uuid(as_uuid=False), ForeignKey('admin_account.id'), unique=True, nullable=True) - attendee_id = Column(Uuid(as_uuid=False), ForeignKey('attendee_account.id'), unique=True, nullable=True) - generated = Column(DateTime(timezone=True), server_default=utcnow(), default=lambda: datetime.now(UTC)) - hashed = Column(String, private=True) +class PasswordReset(MagModel, table=True): + admin_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='admin_account.id', ondelete='CASCADE', unique=True) + admin_account: 'AdminAccount' = Relationship( + back_populates="password_reset", sa_relationship_kwargs={'single_parent': True}) + + attendee_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee_account.id', ondelete='CASCADE', unique=True) + attendee_account: 'AttendeeAccount' = Relationship(back_populates="password_reset", sa_relationship_kwargs={'single_parent': True}) + + generated: datetime = Field(sa_type=DateTime(timezone=True), default_factory=lambda: datetime.now(UTC)) + hashed: str = Column(String, private=True) @property def is_expired(self): return self.generated < datetime.now(UTC) - timedelta(hours=c.PASSWORD_RESET_HOURS) -class AccessGroup(MagModel): +class AccessGroup(MagModel, table=True): """ Sets of accesses to grant to admin accounts. """ - NONE = 0 - LIMITED = 1 - CONTACT = 2 - DEPT = 3 - FULL = 5 - READ_LEVEL_OPTS = [ + NONE: ClassVar = 0 + LIMITED: ClassVar = 1 + CONTACT: ClassVar = 2 + DEPT: ClassVar = 3 + FULL: ClassVar = 5 + READ_LEVEL_OPTS: ClassVar = [ (NONE, 'Same as Read-Write Access'), (LIMITED, 'Limited'), (CONTACT, 'Contact Info'), (DEPT, 'All Info in Own Dept(s)'), (FULL, 'All Info')] - WRITE_LEVEL_OPTS = [ + WRITE_LEVEL_OPTS: ClassVar = [ (NONE, 'No Access'), (LIMITED, 'Limited'), (CONTACT, 'Contact Info'), (DEPT, 'All Info in Own Dept(s)'), (FULL, 'All Info')] + + admin_accounts: list['AdminAccount'] = Relationship( + back_populates='access_groups', + sa_relationship_kwargs={'secondary': 'admin_access_group'}) - name = Column(String) - access = Column(MutableDict.as_mutable(JSONB), default={}) - read_only_access = Column(MutableDict.as_mutable(JSONB), default={}) - start_time = Column(DateTime(timezone=True), nullable=True) - end_time = Column(DateTime(timezone=True), nullable=True) + name: str = '' + access: dict[str, int] = Field(sa_type=MutableDict.as_mutable(JSONB), default_factory=dict) + read_only_access: dict[str, int] = Field(sa_type=MutableDict.as_mutable(JSONB), default_factory=dict) + start_time: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True) + end_time: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True) def __repr__(self): return f"" @@ -310,9 +330,9 @@ def has_any_access(self, access_to, read_only=False): @property def is_valid(self): - if self.start_time and self.start_time > datetime.utcnow().replace(tzinfo=UTC): + if self.start_time and self.start_time > datetime.now(UTC): return False - if self.end_time and self.end_time < datetime.utcnow().replace(tzinfo=UTC): + if self.end_time and self.end_time < datetime.now(UTC): return False return True @@ -333,16 +353,18 @@ def has_access_level(self, access_to, access_level, read_only=False, max_level=F return compare(int(self.access.get(access_to, 0)), access_level) -class WatchList(MagModel): - first_names = Column(String) - last_name = Column(String) - email = Column(String, default='') - birthdate = Column(Date, nullable=True, default=None) - reason = Column(String) - action = Column(String) - expiration = Column(Date, nullable=True, default=None) - active = Column(Boolean, default=True) - attendees = relationship('Attendee', backref=backref('watch_list'), cascade='save-update,merge,refresh-expire,expunge') +class WatchList(MagModel, table=True): + first_names: str = '' + last_name: str = '' + email: str = '' + birthdate: datetime | None = Field(sa_type=Date, nullable=True, default=None) + reason: str = '' + action: str = '' + expiration: datetime | None = Field(sa_type=Date, nullable=True, default=None) + active: bool = True + attendees: list['Attendee'] = Relationship( + back_populates="watch_list", + sa_relationship_kwargs={'lazy': 'selectin'}) @property def full_name(self): @@ -370,28 +392,28 @@ def fix_birthdate(self): ) -class EscalationTicket(MagModel): - attendees = relationship( - 'Attendee', backref='escalation_tickets', order_by='Attendee.full_name', - cascade='save-update,merge,refresh-expire,expunge', - secondary='attendee_escalation_ticket') - ticket_id_seq = Sequence('escalation_ticket_ticket_id_seq') - ticket_id = Column(Integer, ticket_id_seq, server_default=ticket_id_seq.next_value(), unique=True) - who = Column(String) - description = Column(String) - admin_notes = Column(String) - resolved = Column(DateTime(timezone=True), nullable=True) +class EscalationTicket(MagModel, table=True): + attendees: list['Attendee'] = Relationship( + back_populates="escalation_tickets", + sa_relationship_kwargs={'lazy': 'selectin', 'order_by': 'Attendee.full_name', + 'secondary': 'attendee_escalation_ticket'}) + ticket_id_seq: ClassVar = Sequence('escalation_ticket_ticket_id_seq') + ticket_id: int = Field(sa_column=Column(Integer, ticket_id_seq, server_default=ticket_id_seq.next_value(), unique=True)) + who: str = '' + description: str = '' + admin_notes: str = '' + resolved: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True) @property def attendee_names(self): return [a.full_name for a in self.attendees] -class WorkstationAssignment(MagModel): - reg_station_id = Column(Integer) - printer_id = Column(String) - minor_printer_id = Column(String) - terminal_id = Column(String) +class WorkstationAssignment(MagModel, table=True): + reg_station_id: int = 0 + printer_id: str = '' + minor_printer_id: str = '' + terminal_id: str = '' @property def separate_printers(self): diff --git a/uber/models/api.py b/uber/models/api.py index fb1a40015..1801031a4 100644 --- a/uber/models/api.py +++ b/uber/models/api.py @@ -3,26 +3,28 @@ from pytz import UTC from sqlalchemy import String, Uuid, DateTime -from sqlalchemy.schema import ForeignKey from sqlalchemy.dialects.postgresql.json import JSONB from sqlalchemy.ext.mutable import MutableDict +from typing import Any from uber.config import c from uber.models import MagModel -from uber.models.types import DefaultColumn as Column, MultiChoice +from uber.models.types import DefaultColumn as Column, MultiChoice, DefaultField as Field, DefaultRelationship as Relationship __all__ = ['ApiToken', 'ApiJob'] -class ApiToken(MagModel): - admin_account_id = Column(Uuid(as_uuid=False), ForeignKey('admin_account.id')) - token = Column(Uuid(as_uuid=False), default=lambda: str(uuid.uuid4()), private=True) - access = Column(MultiChoice(c.API_ACCESS_OPTS)) - name = Column(String) - description = Column(String) - issued_time = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC)) - revoked_time = Column(DateTime(timezone=True), default=None, nullable=True) +class ApiToken(MagModel, table=True): + admin_account_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='admin_account.id', ondelete='CASCADE') + admin_account: "AdminAccount" = Relationship(back_populates="api_tokens", sa_relationship_kwargs={'lazy': 'joined'}) + + token: str | None = Field(sa_type=Uuid(as_uuid=False), default_factory=lambda: str(uuid.uuid4()), private=True) + access: str = Field(sa_type=MultiChoice(c.API_ACCESS_OPTS), default='') + name: str = '' + description: str = '' + issued_time: datetime = Field(sa_type=DateTime(timezone=True), default_factory=lambda: datetime.now(UTC)) + revoked_time: datetime = Field(sa_type=DateTime(timezone=True), default=None, nullable=True) @property def api_read(self): @@ -41,15 +43,17 @@ def api_delete(self): return c.API_DELETE in self.access_ints -class ApiJob(MagModel): - admin_id = Column(Uuid(as_uuid=False), ForeignKey('admin_account.id'), nullable=True) - admin_name = Column(String) # Preserve admin's name in case their account is removed - queued = Column(DateTime(timezone=True), nullable=True, default=None) - completed = Column(DateTime(timezone=True), nullable=True, default=None) - cancelled = Column(DateTime(timezone=True), nullable=True, default=None) - job_name = Column(String) - target_server = Column(String) - query = Column(String) - api_token = Column(String) - errors = Column(String) - json_data = Column(MutableDict.as_mutable(JSONB), default={}) +class ApiJob(MagModel, table=True): + admin_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='admin_account.id', nullable=True) + admin_account: "AdminAccount" = Relationship(back_populates="api_jobs") + + admin_name: str = '' # Preserve admin's name in case their account is removed + queued: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True, default=None) + completed: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True, default=None) + cancelled: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True, default=None) + job_name: str = '' + target_server: str = '' + query: str = '' + api_token: str = '' + errors: str = '' + json_data: dict[str, Any] = Field(sa_type=MutableDict.as_mutable(JSONB), default_factory=dict) diff --git a/uber/models/art_show.py b/uber/models/art_show.py index 85d9252da..ddcf5a767 100644 --- a/uber/models/art_show.py +++ b/uber/models/art_show.py @@ -11,13 +11,14 @@ from uber.config import c from uber.models import MagModel from uber.decorators import presave_adjustment, classproperty -from uber.models.types import Choice, DefaultColumn as Column, default_relationship as relationship +from uber.models.types import (Choice, DefaultColumn as Column, default_relationship as relationship, + DefaultField as Field, DefaultRelationship as Relationship) from uber.utils import RegistrationCode, get_static_file_path from sqlalchemy.ext.hybrid import hybrid_property -from sqlalchemy.orm import backref -from sqlalchemy.types import Integer, Boolean, String, Uuid, DateTime -from sqlalchemy.schema import ForeignKey, UniqueConstraint, Index +from sqlalchemy.types import Integer, Uuid, DateTime +from sqlalchemy.schema import UniqueConstraint, Index +from typing import ClassVar log = logging.getLogger(__name__) @@ -26,20 +27,15 @@ 'ArtShowPanel', 'ArtPanelAssignment'] -class ArtShowAgentCode(MagModel): - app_id = Column(Uuid(as_uuid=False), ForeignKey('art_show_application.id')) - app = relationship('ArtShowApplication', - backref=backref('agent_codes', cascade='merge,refresh-expire,expunge'), - foreign_keys=app_id, - cascade='merge,refresh-expire,expunge') - attendee_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id', ondelete='SET NULL'), - nullable=True) - attendee = relationship('Attendee', - backref=backref('agent_codes', cascade='merge,refresh-expire,expunge'), - foreign_keys=attendee_id, - cascade='merge,refresh-expire,expunge') - code = Column(String) - cancelled = Column(DateTime(timezone=True), nullable=True) +class ArtShowAgentCode(MagModel, table=True): + app_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='art_show_application.id', ondelete='CASCADE') + app: 'ArtShowApplication' = Relationship(back_populates="agent_codes", sa_relationship_kwargs={'lazy': 'joined'}) + + attendee_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', nullable=True) + attendee: 'Attendee' = Relationship(back_populates="agent_codes") + + code: str = '' + cancelled: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True) @hybrid_property def normalized_code(self): @@ -54,56 +50,66 @@ def attendee_first_name(self): return self.attendee.first_name if self.attendee else None -class ArtShowApplication(MagModel): - attendee_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id', ondelete='SET NULL'), - nullable=True) - attendee = relationship('Attendee', foreign_keys=attendee_id, cascade='save-update, merge', - backref=backref('art_show_applications', cascade='save-update, merge')) - checked_in = Column(DateTime(timezone=True), nullable=True) - checked_out = Column(DateTime(timezone=True), nullable=True) - locations = Column(String) - artist_name = Column(String) - artist_id = Column(String, admin_only=True) - payout_method = Column(Choice(c.ARTIST_PAYOUT_METHOD_OPTS), default=c.CHECK) - banner_name = Column(String) - banner_name_ad = Column(String) - artist_id_ad = Column(String, admin_only=True) - check_payable = Column(String) - contact_at_con = Column(String) - panels = Column(Integer, default=0) - panels_ad = Column(Integer, default=0) - tables = Column(Integer, default=0) - tables_ad = Column(Integer, default=0) - description = Column(String) - business_name = Column(String) - zip_code = Column(String) - address1 = Column(String) - address2 = Column(String) - city = Column(String) - region = Column(String) - country = Column(String) - paypal_address = Column(String) # TODO: Move to AC plugin - website = Column(String) - special_needs = Column(String) - status = Column(Choice(c.ART_SHOW_STATUS_OPTS), default=c.UNAPPROVED) - decline_reason = Column(String) - delivery_method = Column(Choice(c.ART_SHOW_DELIVERY_OPTS), default=c.BRINGING_IN) - us_only = Column(Boolean, default=False) - admin_notes = Column(String, admin_only=True) - check_in_notes = Column(String) - overridden_price = Column(Integer, nullable=True, admin_only=True) - active_receipt = relationship( +class ArtShowApplication(MagModel, table=True): + attendee_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', nullable=True, unique=True) + attendee: 'Attendee' = Relationship( + back_populates="art_show_application", sa_relationship_kwargs={'lazy': 'joined', 'single_parent': True}) + + checked_in: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True) + checked_out: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True) + locations: str = '' + artist_name: str = '' + artist_id: str = '' + payout_method: int = Field(sa_column=Column(Choice(c.ARTIST_PAYOUT_METHOD_OPTS)), default=c.CHECK) + banner_name: str = '' + banner_name_ad: str = '' + artist_id_ad: str = '' + check_payable: str = '' + contact_at_con: str = '' + panels: int = 0 + panels_ad: int = 0 + tables: int = 0 + tables_ad: int = 0 + description: str = '' + business_name: str = '' + zip_code: str = '' + address1: str = '' + address2: str = '' + city: str = '' + region: str = '' + country: str = '' + paypal_address: str = '' # TODO: Move to AC plugin + website: str = '' + special_needs: str = '' + status: int = Field(sa_column=Column(Choice(c.ART_SHOW_STATUS_OPTS)), default=c.UNAPPROVED) + decline_reason: str = '' + delivery_method: int = Field(sa_column=Column(Choice(c.ART_SHOW_DELIVERY_OPTS)), default=c.BRINGING_IN) + us_only: bool = False + admin_notes: str = '' + check_in_notes: str = '' + overridden_price: int = Field(nullable=True, default=0) + active_receipt: 'ModelReceipt' = Relationship(sa_relationship=relationship( 'ModelReceipt', - cascade='save-update,merge,refresh-expire,expunge', primaryjoin='and_(remote(ModelReceipt.owner_id) == foreign(ArtShowApplication.id),' 'ModelReceipt.owner_model == "ArtShowApplication",' 'ModelReceipt.closed == None)', - uselist=False) - default_cost = Column(Integer, nullable=True) - - assignments = relationship('ArtPanelAssignment', backref='app') - - email_model_name = 'app' + lazy='select')) + default_cost: int | None = Field(nullable=True) + + agents: list['Attendee'] = Relationship( + back_populates="art_agent_apps", + sa_relationship_kwargs={ + 'secondaryjoin': 'and_(ArtShowAgentCode.app_id == ArtShowApplication.id, ArtShowAgentCode.cancelled == None)', + 'secondary': 'art_show_agent_code', 'viewonly': True + }) + art_show_pieces: list['ArtShowPiece'] = Relationship( + back_populates="app", sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + agent_codes: list['ArtShowAgentCode'] = Relationship( + back_populates="app", sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + assignments: list['ArtPanelAssignment'] = Relationship( + back_populates="app", sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + + email_model_name: ClassVar = 'app' @presave_adjustment def _cost_adjustments(self): @@ -435,38 +441,34 @@ def check_total(self): return round(self.total_sales - self.commission) -class ArtShowPiece(MagModel): - app_id = Column(Uuid(as_uuid=False), ForeignKey('art_show_application.id', ondelete='SET NULL'), nullable=True) - app = relationship('ArtShowApplication', foreign_keys=app_id, - cascade='save-update, merge', - backref=backref('art_show_pieces', - cascade='save-update, merge')) - receipt_id = Column(Uuid(as_uuid=False), ForeignKey('art_show_receipt.id', ondelete='SET NULL'), nullable=True) - receipt = relationship('ArtShowReceipt', foreign_keys=receipt_id, - cascade='save-update, merge', - overlaps="art_show_purchases,buyer", - backref=backref('pieces', cascade='save-update, merge', overlaps="art_show_purchases,buyer")) - winning_bidder_id = Column(Uuid(as_uuid=False), ForeignKey('art_show_bidder.id', ondelete='SET NULL'), nullable=True) - winning_bidder = relationship('ArtShowBidder', foreign_keys=winning_bidder_id, - cascade='save-update, merge', - backref=backref('art_show_pieces', - cascade='save-update, merge')) - piece_id = Column(Integer) - name = Column(String) - for_sale = Column(Boolean, default=False) - type = Column(Choice(c.ART_PIECE_TYPE_OPTS), default=c.PRINT) - gallery = Column(Choice(c.ART_PIECE_GALLERY_OPTS), default=c.GENERAL) - media = Column(String) - print_run_num = Column(Integer, default=0, nullable=True) - print_run_total = Column(Integer, default=0, nullable=True) - opening_bid = Column(Integer, default=0, nullable=True) - quick_sale_price = Column(Integer, default=0, nullable=True) - winning_bid = Column(Integer, default=0, nullable=True) - no_quick_sale = Column(Boolean, default=False) - voice_auctioned = Column(Boolean, default=False) - - status = Column(Choice(c.ART_PIECE_STATUS_OPTS), default=c.EXPECTED, - admin_only=True) +class ArtShowPiece(MagModel, table=True): + app_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='art_show_application.id', ondelete='CASCADE') + app: 'ArtShowApplication' = Relationship(back_populates="art_show_pieces", sa_relationship_kwargs={'lazy': 'joined'}) + + receipt_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='art_show_receipt.id', nullable=True) + receipt: 'ArtShowReceipt' = Relationship(back_populates="pieces", sa_relationship_kwargs={'overlaps': 'art_show_purchases,buyer'}) + + winning_bidder_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='art_show_bidder.id', nullable=True) + winning_bidder: 'ArtShowBidder' = Relationship(back_populates="art_show_pieces") + + piece_id: int = 1 + name: str = '' + for_sale: bool = False + type: int = Field(sa_column=Column(Choice(c.ART_PIECE_TYPE_OPTS)), default=c.PRINT) + gallery: int = Field(sa_column=Column(Choice(c.ART_PIECE_GALLERY_OPTS)), default=c.GENERAL) + media: str = '' + print_run_num: int | None = Field(default=0, nullable=True) + print_run_total: int | None = Field(default=0, nullable=True) + opening_bid: int | None = Field(default=0, nullable=True) + quick_sale_price: int | None = Field(default=0, nullable=True) + winning_bid: int | None = Field(default=0, nullable=True) + no_quick_sale: bool = False + voice_auctioned: bool = False + status: int = Field(sa_column=Column(Choice(c.ART_PIECE_STATUS_OPTS)), default=c.EXPECTED) + + buyer: 'Attendee' = Relationship( + back_populates="art_show_purchases", + sa_relationship_kwargs={'lazy': 'joined', 'secondary': 'art_show_receipt'}) @presave_adjustment def create_piece_id(self): @@ -579,20 +581,22 @@ def print_bidsheet(self, pdf, sheet_num, normal_font_name, bold_font_name, set_f 53, 14, txt=('${:,.2f}'.format(self.quick_sale_price)) if self.valid_quick_sale else 'NFS', ln=1) -class ArtShowPanel(MagModel): - gallery = Column(Choice(c.ART_PIECE_GALLERY_OPTS), default=c.GENERAL) - surface_type = Column(Choice(c.ART_SHOW_PANEL_TYPE_OPTS), default=c.PANEL) - origin_x = Column(Integer, default=0) - origin_y = Column(Integer, default=0) - terminus_x = Column(Integer, default=0) - terminus_y = Column(Integer, default=0) - assignable_sides = Column(Choice(c.ART_SHOW_PANEL_SIDE_OPTS), default=c.BOTH) - start_label = Column(String) - end_label = Column(String) +class ArtShowPanel(MagModel, table=True): + gallery: int = Field(sa_column=Column(Choice(c.ART_PIECE_GALLERY_OPTS)), default=c.GENERAL) + surface_type: int = Field(sa_column=Column(Choice(c.ART_SHOW_PANEL_TYPE_OPTS)), default=c.PANEL) + origin_x: int = 0 + origin_y: int = 0 + terminus_x: int = 0 + terminus_y: int = 0 + assignable_sides: int = Field(sa_column=Column(Choice(c.ART_SHOW_PANEL_SIDE_OPTS)), default=c.BOTH) + start_label: str = '' + end_label: str = '' - assignments = relationship('ArtPanelAssignment', backref='panel') + assignments: list['ArtPanelAssignment'] = Relationship( + back_populates="panel", + sa_relationship_kwargs={'lazy': 'selectin', 'cascade': 'all,delete-orphan', 'passive_deletes': True}) - __table_args__ = ( + __table_args__: ClassVar = ( UniqueConstraint('gallery', 'surface_type', 'origin_x', 'origin_y', 'terminus_x', 'terminus_y'), ) @@ -620,13 +624,17 @@ def directional_usability(self): return 'u' if self.assignable_sides == c.START else 'd' -class ArtPanelAssignment(MagModel): - panel_id = Column(Uuid(as_uuid=False), ForeignKey('art_show_panel.id')) - app_id = Column(Uuid(as_uuid=False), ForeignKey('art_show_application.id')) - manual = Column(Boolean, default=False) - assigned_side = Column(Choice(c.ART_SHOW_PANEL_SIDE_OPTS), default=c.START) +class ArtPanelAssignment(MagModel, table=True): + app_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='art_show_application.id', ondelete='CASCADE') + app: 'ArtShowApplication' = Relationship(back_populates="assignments", sa_relationship_kwargs={'lazy': 'joined'}) - __table_args__ = ( + panel_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='art_show_panel.id', ondelete='CASCADE') + panel: 'ArtShowPanel' = Relationship(back_populates="assignments", sa_relationship_kwargs={'lazy': 'joined'}) + + manual: bool = False + assigned_side: int = Field(sa_column=Column(Choice(c.ART_SHOW_PANEL_SIDE_OPTS)), default=c.START) + + __table_args__: ClassVar = ( UniqueConstraint('panel_id', 'assigned_side'), Index('ix_art_panel_assignment_panel_id', 'panel_id'), Index('ix_art_panel_assignment_assigned_side', 'assigned_side'), @@ -652,27 +660,28 @@ def assignment_str(self): return f"{self.panel.origin_x}_{self.panel.origin_y}|{self.panel.terminus_x}_{self.panel.terminus_y}|{self.directional_assigned_side}" -class ArtShowPayment(MagModel): - receipt_id = Column(Uuid(as_uuid=False), ForeignKey('art_show_receipt.id', ondelete='SET NULL'), nullable=True) - receipt = relationship('ArtShowReceipt', foreign_keys=receipt_id, - cascade='save-update, merge', - backref=backref('art_show_payments', - cascade='save-update, merge')) - amount = Column(Integer, default=0) - type = Column(Choice(c.ART_SHOW_PAYMENT_OPTS), default=c.STRIPE, admin_only=True) - when = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC)) - - -class ArtShowReceipt(MagModel): - invoice_num = Column(Integer, default=0) - attendee_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id', ondelete='SET NULL'), nullable=True) - attendee = relationship('Attendee', foreign_keys=attendee_id, - cascade='save-update, merge', - overlaps="art_show_purchases,buyer", - backref=backref('art_show_receipts', - cascade='save-update, merge', - overlaps="art_show_purchases,buyer")) - closed = Column(DateTime(timezone=True), nullable=True) +class ArtShowPayment(MagModel, table=True): + receipt_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='art_show_receipt.id', ondelete='CASCADE') + receipt: 'ArtShowReceipt' = Relationship(back_populates="art_show_payments", sa_relationship_kwargs={'lazy': 'joined'}) + + amount: int = 0 + type: int = Field(sa_column=Column(Choice(c.ART_SHOW_PAYMENT_OPTS)), default=c.STRIPE) + when: datetime = Field(sa_type=DateTime(timezone=True), default_factory=lambda: datetime.now(UTC)) + + +class ArtShowReceipt(MagModel, table=True): + attendee_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', ondelete='CASCADE') + attendee: 'Attendee' = Relationship( + back_populates="art_show_receipts", sa_relationship_kwargs={'lazy': 'joined', 'overlaps': 'art_show_purchases,buyer'}) + + invoice_num: int = 0 + closed: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True) + + pieces: list['ArtShowPiece'] = Relationship( + back_populates="receipt", + sa_relationship_kwargs={'lazy': 'selectin', 'overlaps': 'art_show_purchases,buyer'}) + art_show_payments: list['ArtShowPayment'] = Relationship( + back_populates="receipt", sa_relationship_kwargs={'lazy': 'selectin', 'cascade': 'all,delete-orphan', 'passive_deletes': True}) @presave_adjustment def add_invoice_num(self): @@ -726,15 +735,21 @@ def cash_total(self): [payment.amount for payment in self.art_show_payments if payment.type == c.REFUND]) -class ArtShowBidder(MagModel): - attendee_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id', ondelete='SET NULL'), nullable=True) - bidder_num = Column(String) - admin_notes = Column(String) - signed_up = Column(DateTime(timezone=True), nullable=True) - email_won_bids = Column(Boolean, default=False) - contact_type = Column(Choice(c.ART_SHOW_CONTACT_TYPE_OPTS), default=c.EMAIL) +class ArtShowBidder(MagModel, table=True): + attendee_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', ondelete='CASCADE') + attendee: 'Attendee' = Relationship( + back_populates="art_show_bidder", sa_relationship_kwargs={'lazy': 'joined'}) + + bidder_num: str = '' + admin_notes: str = '' + signed_up: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True) + email_won_bids: bool = False + contact_type: int = Field(sa_column=Column(Choice(c.ART_SHOW_CONTACT_TYPE_OPTS)), default=c.EMAIL) + + art_show_pieces: list['ArtShowPiece'] = Relationship( + back_populates="winning_bidder") - email_model_name = 'bidder' + email_model_name: ClassVar = 'bidder' @presave_adjustment def zfill_bidder_num(self): diff --git a/uber/models/attendee.py b/uber/models/attendee.py index 5f1dd339e..caf07cf37 100644 --- a/uber/models/attendee.py +++ b/uber/models/attendee.py @@ -1,27 +1,26 @@ import json import math import re -from datetime import datetime, timedelta +from datetime import datetime, timedelta, date from uuid import uuid4 import logging from pytz import UTC from sqlalchemy import and_, case, exists, func, or_, select, not_ -from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.hybrid import hybrid_property -from sqlalchemy.orm import backref, subqueryload, aliased -from sqlalchemy.schema import Column as SQLAlchemyColumn, ForeignKey, Index, Table, UniqueConstraint -from sqlalchemy.types import Boolean, Date, Integer, Uuid, String, DateTime +from sqlalchemy.orm import subqueryload, aliased, selectinload, joinedload +from sqlalchemy.schema import ForeignKey, Index, Table, UniqueConstraint +from sqlalchemy.types import Boolean, Uuid, String, DateTime +from typing import ClassVar import uber from uber.config import c from uber.custom_tags import safe_string, time_day_local, readable_join -from uber.decorators import predelete_adjustment, presave_adjustment, \ - render, cached_property, classproperty +from uber.decorators import presave_adjustment, render, cached_property, classproperty from uber.models import MagModel from uber.models.group import Group -from uber.models.types import default_relationship as relationship, utcnow, Choice, DefaultColumn as Column, \ - MultiChoice, TakesPaymentMixin +from uber.models.types import default_relationship as relationship, Choice, DefaultColumn as Column, \ + MultiChoice, TakesPaymentMixin, DefaultField as Field, DefaultRelationship as Relationship from uber.utils import add_opt, get_age_from_birthday, get_age_conf_from_birthday, hour_day_format, \ localized_now, mask_string, normalize_email, normalize_email_legacy, remove_opt, RegistrationCode, listify, groupify @@ -146,19 +145,17 @@ normalized_name_suffixes = [re.sub(r'[,\.]', '', s.lower()) for s in name_suffixes] -class BadgeInfo(MagModel): - attendee_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id', ondelete='SET NULL'), nullable=True, default=None) - attendee = relationship('Attendee', backref=backref('allocated_badges', cascade='merge,refresh-expire,expunge'), - foreign_keys=attendee_id, - cascade='save-update,merge,refresh-expire,expunge', single_parent=True) - active = Column(Boolean, default=False) - picked_up = Column(DateTime(timezone=True), nullable=True, default=None) - reported_lost = Column(DateTime(timezone=True), nullable=True, default=None) - ident = Column(Integer, default=0, index=True) +class BadgeInfo(MagModel, table=True): + """ + Attendee: joined + """ - __table_args__ = ( - Index('ix_badge_info_attendee_id', attendee_id.desc()), - ) + attendee_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', nullable=True) + attendee: 'Attendee' = Relationship(back_populates="allocated_badges", sa_relationship_kwargs={'lazy': 'joined'}) + active: bool = False + picked_up: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True, default=None) + reported_lost: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True, default=None) + ident: int = Field(default=0, index=True) def __repr__(self): return f"" @@ -200,42 +197,42 @@ def check_in(self): self.picked_up = self.attendee.checked_in or datetime.now(UTC) -class Attendee(MagModel, TakesPaymentMixin): - watchlist_id = Column(Uuid(as_uuid=False), ForeignKey('watch_list.id', ondelete='set null'), nullable=True, default=None) - group_id = Column(Uuid(as_uuid=False), ForeignKey('group.id', ondelete='SET NULL'), nullable=True) - group = relationship( - Group, backref='attendees', foreign_keys=group_id, cascade='save-update,merge,refresh-expire,expunge') +Index('ix_badge_info_attendee_id', BadgeInfo.attendee_id.desc()) + + +class Attendee(MagModel, TakesPaymentMixin, table=True): + watchlist_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='watch_list.id', nullable=True) + watch_list: "WatchList" = Relationship(back_populates="attendees", sa_relationship_kwargs={'lazy': 'select'}) + + group_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='group.id', nullable=True) + group: 'Group' = Relationship(back_populates="attendees", sa_relationship_kwargs={'foreign_keys': 'Attendee.group_id', 'lazy': 'select'}) - badge_pickup_group_id = Column(Uuid(as_uuid=False), ForeignKey('badge_pickup_group.id', ondelete='SET NULL'), nullable=True) - badge_pickup_group = relationship( - 'BadgePickupGroup', backref=backref('attendees', order_by='Attendee.full_name'), foreign_keys=badge_pickup_group_id, - cascade='save-update,merge,refresh-expire,expunge', single_parent=True) - - creator_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id', ondelete='set null'), nullable=True) - creator = relationship( - 'Attendee', - foreign_keys='Attendee.creator_id', - backref=backref('created_badges', order_by='Attendee.full_name'), - cascade='save-update,merge,refresh-expire,expunge', - remote_side='Attendee.id', - single_parent=True) - - current_attendee_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id'), nullable=True) - current_attendee = relationship( - 'Attendee', - foreign_keys='Attendee.current_attendee_id', - backref=backref('old_badges', order_by='Attendee.badge_status', cascade='all,delete-orphan'), - cascade='save-update,merge,refresh-expire,expunge', - remote_side='Attendee.id', - single_parent=True) + badge_pickup_group_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='badge_pickup_group.id', nullable=True) + badge_pickup_group: 'BadgePickupGroup' = Relationship(back_populates="attendees", sa_relationship_kwargs={'lazy': 'select'}) + + creator_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', nullable=True) + creator: 'Attendee' = Relationship( + back_populates="created_badges", + sa_relationship_kwargs={'foreign_keys': 'Attendee.creator_id', 'lazy': 'select', 'remote_side': 'Attendee.id'}) + created_badges: list['Attendee'] = Relationship( + back_populates="creator", + sa_relationship_kwargs={'foreign_keys': 'Attendee.creator_id', 'order_by': 'Attendee.full_name'}) + + current_attendee_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', nullable=True) + current_attendee: 'Attendee' = Relationship( + back_populates="old_badges", + sa_relationship_kwargs={'foreign_keys': 'Attendee.current_attendee_id', 'lazy': 'select', 'remote_side': 'Attendee.id'}) + old_badges: list['Attendee'] = Relationship( + back_populates="current_attendee", + sa_relationship_kwargs={'foreign_keys': 'Attendee.current_attendee_id', 'order_by': 'Attendee.badge_status'}) - active_badge = relationship( + allocated_badges: list['BadgeInfo'] = Relationship(back_populates="attendee") + active_badge: 'BadgeInfo' = Relationship(sa_relationship=relationship( 'BadgeInfo', - cascade='save-update,merge,refresh-expire,expunge', primaryjoin='and_(BadgeInfo.attendee_id == Attendee.id,' 'BadgeInfo.active == True)', - uselist=False, - overlaps="allocated_badges,attendee") + lazy='joined', + overlaps="allocated_badges,attendee")) # NOTE: The cascade relationships for promo_code do NOT include # "save-update". During the preregistration workflow, before an Attendee @@ -247,302 +244,332 @@ class Attendee(MagModel, TakesPaymentMixin): # # The practical result of this is that we must manually set promo_code_id # in order for the relationship to be persisted. - promo_code_id = Column(Uuid(as_uuid=False), ForeignKey('promo_code.id'), nullable=True, index=True) - promo_code = relationship( - 'PromoCode', - backref=backref('used_by', cascade='merge,refresh-expire,expunge'), - foreign_keys=promo_code_id, - cascade='merge,refresh-expire,expunge') + promo_code_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='promo_code.id', nullable=True, index=True) + promo_code: 'PromoCode' = Relationship(back_populates="used_by", sa_relationship_kwargs={'lazy': 'select'}) - transfer_code = Column(String) - - placeholder = Column(Boolean, default=False, admin_only=True, index=True) - first_name = Column(String) - last_name = Column(String) - legal_name = Column(String) - email = Column(String) - birthdate = Column(Date, nullable=True, default=None) - age_group = Column(Choice(c.AGE_GROUPS), default=c.AGE_UNKNOWN, nullable=True) - - international = Column(Boolean, default=False) - zip_code = Column(String) - address1 = Column(String) - address2 = Column(String) - city = Column(String) - region = Column(String) - country = Column(String) - ec_name = Column(String) - ec_phone = Column(String) - onsite_contact = Column(String) - no_onsite_contact = Column(Boolean, default=False) - cellphone = Column(String) - no_cellphone = Column(Boolean, default=False) - - requested_accessibility_services = Column(Boolean, default=False) - - interests = Column(MultiChoice(c.INTEREST_OPTS)) - found_how = Column(String) # TODO: Remove? - comments = Column(String) # TODO: Remove? - for_review = Column(String, admin_only=True) - admin_notes = Column(String, admin_only=True) - - public_id = Column(Uuid(as_uuid=False), default=lambda: str(uuid4())) - badge_type = Column(Choice(c.BADGE_OPTS), default=c.ATTENDEE_BADGE) - badge_status = Column(Choice(c.BADGE_STATUS_OPTS), default=c.NEW_STATUS, index=True, admin_only=True) - ribbon = Column(MultiChoice(c.RIBBON_OPTS), admin_only=True) - - affiliate = Column(String) # TODO: Remove + transfer_code: str = '' + + placeholder: bool = Field(default=False, index=True) + first_name: str = '' + last_name: str = '' + legal_name: str = '' + email: str = '' + birthdate: date | None = None + age_group: int | None = Field(sa_column=Column(Choice(c.AGE_GROUPS), nullable=True), default=c.AGE_UNKNOWN) + + international: bool = False + zip_code: str = '' + address1: str = '' + address2: str = '' + city: str = '' + region: str = '' + country: str = '' + ec_name: str = '' + ec_phone: str = '' + onsite_contact: str = '' + no_onsite_contact: bool = False + cellphone: str = '' + no_cellphone: bool = False + + requested_accessibility_services: bool = False + + interests: str = Field(sa_type=MultiChoice(c.INTEREST_OPTS), default='') + found_how: str = '' + comments: str = '' + for_review: str = '' + admin_notes: str = '' + + public_id: str | None = Field(sa_type=Uuid(as_uuid=False), default_factory=lambda: str(uuid4())) + badge_type: int = Field(sa_column=Column(Choice(c.BADGE_OPTS)), default=c.ATTENDEE_BADGE) + badge_status: int = Field(sa_column=Column(Choice(c.BADGE_STATUS_OPTS), index=True, admin_only=True), default=c.NEW_STATUS) + ribbon: str = Field(sa_type=MultiChoice(c.RIBBON_OPTS), default='', admin_only=True) # If [[staff_shirt]] is the same as [[shirt]], we only use the shirt column - shirt = Column(Choice(c.SHIRT_OPTS), default=c.NO_SHIRT) - staff_shirt = Column(Choice(c.STAFF_SHIRT_OPTS), default=c.NO_SHIRT) - num_event_shirts = Column(Choice(c.STAFF_EVENT_SHIRT_OPTS, allow_unspecified=True), default=-1) - shirt_opt_out = Column(Choice(c.SHIRT_OPT_OUT_OPTS), default=c.OPT_IN) - can_spam = Column(Boolean, default=False) - regdesk_info = Column(String, admin_only=True) - extra_merch = Column(String, admin_only=True) - got_merch = Column(Boolean, default=False, admin_only=True) - got_staff_merch = Column(Boolean, default=False, admin_only=True) - got_swadge = Column(Boolean, default=False, admin_only=True) - can_transfer = Column(Boolean, default=False, admin_only=True) - - reg_station = Column(Integer, nullable=True, admin_only=True) - registered = Column(DateTime(timezone=True), server_default=utcnow(), default=lambda: datetime.now(UTC)) - confirmed = Column(DateTime(timezone=True), nullable=True, default=None) - checked_in = Column(DateTime(timezone=True), nullable=True) - - paid = Column(Choice(c.PAYMENT_OPTS), default=c.NOT_PAID, index=True, admin_only=True) - badge_cost = Column(Integer, nullable=True, admin_only=True) - overridden_price = Column(Integer, nullable=True, admin_only=True) - amount_extra = Column(Choice(c.DONATION_TIER_OPTS, allow_unspecified=True), default=0) - extra_donation = Column(Integer, default=0) - - badge_printed_name = Column(String) - - active_receipt = relationship( + shirt: int = Field(sa_column=Column(Choice(c.SHIRT_OPTS)), default=c.NO_SHIRT) + staff_shirt: int = Field(sa_column=Column(Choice(c.STAFF_SHIRT_OPTS)), default=c.NO_SHIRT) + num_event_shirts: int = Field(sa_column=Column(Choice(c.STAFF_EVENT_SHIRT_OPTS, allow_unspecified=True)), default=-1) + shirt_opt_out: int = Field(sa_column=Column(Choice(c.SHIRT_OPT_OUT_OPTS)), default=c.OPT_IN) + can_spam: bool = False + regdesk_info: str = '' + extra_merch: str = '' + got_merch: bool = False + got_staff_merch: bool = False + got_swadge: bool = False + can_transfer: bool = False + + reg_station: int | None + registered: datetime = Field(sa_type=DateTime(timezone=True), default_factory=lambda: datetime.now(UTC)) + confirmed: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True, default=None) + checked_in: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True) + + paid: int = Field(sa_column=Column(Choice(c.PAYMENT_OPTS), index=True, admin_only=True), default=c.NOT_PAID) + badge_cost: int | None + overridden_price: int | None + amount_extra: int = Field(sa_column=Column(Choice(c.DONATION_TIER_OPTS, allow_unspecified=True)), default=0) + extra_donation: int = 0 + + badge_printed_name: str = '' + + active_receipt: 'ModelReceipt' = Relationship(sa_relationship=relationship( 'ModelReceipt', - cascade='save-update,merge,refresh-expire,expunge', primaryjoin='and_(remote(ModelReceipt.owner_id) == foreign(Attendee.id),' 'ModelReceipt.owner_model == "Attendee",' 'ModelReceipt.closed == None)', - uselist=False) - default_cost = Column(Integer, nullable=True) - - dept_memberships = relationship('DeptMembership', backref='attendee') - dept_membership_requests = relationship('DeptMembershipRequest', backref='attendee') - anywhere_dept_membership_request = relationship( - 'DeptMembershipRequest', - primaryjoin='and_(' - 'DeptMembershipRequest.attendee_id == Attendee.id, ' - 'DeptMembershipRequest.department_id == None)', - uselist=False, - viewonly=True) - dept_roles = relationship( - 'DeptRole', - backref='attendees', - secondaryjoin='and_(' - 'dept_membership_dept_role.c.dept_role_id == DeptRole.id, ' - 'dept_membership_dept_role.c.dept_membership_id == DeptMembership.id)', - secondary='join(DeptMembership, dept_membership_dept_role)', - order_by='DeptRole.name', - viewonly=True) - shifts = relationship('Shift', backref='attendee') - jobs = relationship( - 'Job', - backref='attendees_working_shifts', - secondary='shift', - order_by='Job.name', - viewonly=True) - jobs_in_assigned_depts = relationship( - 'Job', - backref='attendees_in_dept', - secondaryjoin='DeptMembership.department_id == Job.department_id', - secondary='dept_membership', - order_by='Job.name', - viewonly=True) - depts_where_working = relationship( - 'Department', - backref='attendees_working_shifts', - secondary='join(Shift, Job)', - order_by='Department.name', - viewonly=True) - dept_memberships_with_inherent_role = relationship( + lazy='select')) + default_cost: int | None + + created_groups: list['Group'] = Relationship( + back_populates="creator", + sa_relationship_kwargs={'foreign_keys': 'Group.creator_id', 'order_by': 'Group.name'}) + + dept_memberships: list['DeptMembership'] = Relationship( + back_populates="attendee", + sa_relationship_kwargs={'lazy': 'select', 'cascade': 'all,delete-orphan', 'passive_deletes': True}) + dept_membership_requests: list['DeptMembershipRequest'] = Relationship( + back_populates="attendee", sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'lazy': 'select', 'passive_deletes': True}) + assigned_depts: list['Department'] = Relationship( + back_populates='members', + sa_relationship_kwargs={ + 'order_by': 'Department.name', 'overlaps': 'dept_memberships,attendee', + 'lazy': 'select', 'secondary': 'dept_membership'}) + dept_roles: list['DeptRole'] = Relationship( + back_populates="attendees", + sa_relationship_kwargs={ + 'secondaryjoin': 'and_(dept_membership_dept_role.c.dept_role_id == DeptRole.id, ' + 'dept_membership_dept_role.c.dept_membership_id == DeptMembership.id)', + 'secondary': 'join(DeptMembership, dept_membership_dept_role)', + 'order_by': 'DeptRole.name', 'viewonly': True}) + shifts: list['Shift'] = Relationship( + back_populates="attendee", sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + jobs: list['Job'] = Relationship( + back_populates="attendees_working_shifts", + sa_relationship_kwargs={'secondary': 'shift', 'order_by': 'Job.name', 'viewonly': True}) + depts_where_working: list['Department'] = Relationship( + back_populates="attendees_working_shifts", + sa_relationship_kwargs={'secondary': 'join(Shift, Job)', 'order_by': 'Department.name', 'viewonly': True}) + dept_memberships_with_inherent_role: list['DeptMembership'] = Relationship(sa_relationship=relationship( 'DeptMembership', primaryjoin='and_(' 'Attendee.id == DeptMembership.attendee_id, ' 'DeptMembership.has_inherent_role)', - viewonly=True) - dept_memberships_with_dept_role = relationship( + lazy='select', + viewonly=True)) + dept_memberships_with_dept_role: list['DeptMembership'] = Relationship(sa_relationship=relationship( 'DeptMembership', primaryjoin='and_(' 'Attendee.id == DeptMembership.attendee_id, ' 'DeptMembership.has_dept_role)', - viewonly=True) - dept_memberships_with_role = relationship( + viewonly=True)) + dept_memberships_with_role: list['DeptMembership'] = Relationship(sa_relationship=relationship( 'DeptMembership', primaryjoin='and_(' 'Attendee.id == DeptMembership.attendee_id, ' 'DeptMembership.has_role)', - viewonly=True) - dept_memberships_as_dept_head = relationship( - 'DeptMembership', - primaryjoin='and_(' - 'Attendee.id == DeptMembership.attendee_id, ' - 'DeptMembership.is_dept_head == True)', - viewonly=True) - dept_memberships_as_poc = relationship( - 'DeptMembership', - primaryjoin='and_(' - 'Attendee.id == DeptMembership.attendee_id, ' - 'DeptMembership.is_poc == True)', - viewonly=True) - dept_memberships_where_can_admin_checklist = relationship( + viewonly=True)) + dept_memberships_where_can_admin_checklist: list['DeptMembership'] = Relationship(sa_relationship=relationship( 'DeptMembership', primaryjoin='and_(' 'Attendee.id == DeptMembership.attendee_id, ' 'or_(' 'DeptMembership.is_dept_head == True,' 'DeptMembership.is_checklist_admin == True))', - viewonly=True) - dept_memberships_as_checklist_admin = relationship( + viewonly=True)) + dept_memberships_as_checklist_admin: list['DeptMembership'] = Relationship(sa_relationship=relationship( 'DeptMembership', primaryjoin='and_(' 'Attendee.id == DeptMembership.attendee_id, ' 'DeptMembership.is_checklist_admin == True)', - viewonly=True) - pocs_for_depts_where_working = relationship( - 'Attendee', - primaryjoin='Attendee.id == Shift.attendee_id', - secondaryjoin='and_(' - 'DeptMembership.attendee_id == Attendee.id, ' - 'DeptMembership.is_poc == True)', - secondary='join(Shift, Job).join(DeptMembership, ' - 'DeptMembership.department_id == Job.department_id)', - order_by='Attendee.full_name', - viewonly=True) - dept_heads_for_depts_where_working = relationship( - 'Attendee', - primaryjoin='Attendee.id == Shift.attendee_id', - secondaryjoin='and_(' - 'DeptMembership.attendee_id == Attendee.id, ' - 'DeptMembership.is_dept_head == True)', - secondary='join(Shift, Job).join(DeptMembership, ' - 'DeptMembership.department_id == Job.department_id)', - order_by='Attendee.full_name', - viewonly=True) - - staffing = Column(Boolean, default=False) - agreed_to_volunteer_agreement = Column(Boolean, default=False) - reviewed_emergency_procedures = Column(Boolean, default=False) - reviewed_cash_handling = Column(DateTime(timezone=True), nullable=True, default=None) - name_in_credits = Column(String, nullable=True) - walk_on_volunteer = Column(Boolean, default=False) - nonshift_minutes = Column(Integer, default=0, admin_only=True) - past_years = Column(String, admin_only=True) - can_work_setup = Column(Boolean, default=False, admin_only=True) - can_work_teardown = Column(Boolean, default=False, admin_only=True) - - # TODO: a record of when an attendee is unable to pickup a shirt - # (which type? swag or staff? prob swag) - no_shirt = relationship('NoShirt', backref=backref('attendee', load_on_pending=True), uselist=False) - - admin_account = relationship('AdminAccount', backref=backref('attendee', load_on_pending=True), uselist=False) - food_restrictions = relationship( - 'FoodRestrictions', backref=backref('attendee', load_on_pending=True), uselist=False) - - sales = relationship('Sale', backref='attendee', cascade='save-update,merge,refresh-expire,expunge') - mpoints_for_cash = relationship('MPointsForCash', backref='attendee') - old_mpoint_exchanges = relationship('OldMPointExchange', backref='attendee') - dept_checklist_items = relationship('DeptChecklistItem', backref=backref('attendee', lazy='subquery')) - - indie_developer = relationship( - 'IndieDeveloper', backref=backref('attendee', load_on_pending=True), uselist=False) - - hotel_eligible = Column(Boolean, default=False, admin_only=True) - hotel_requests = relationship('HotelRequests', backref=backref('attendee', load_on_pending=True), uselist=False) - room_assignments = relationship('RoomAssignment', backref=backref('attendee', load_on_pending=True)) + viewonly=True)) + + checklist_admin_depts: list['Department'] = Relationship( + back_populates="checklist_admins", + sa_relationship_kwargs={ + 'primaryjoin': 'and_(Department.id == DeptMembership.department_id, ' + 'DeptMembership.is_checklist_admin == True)', + 'secondary': 'dept_membership', 'viewonly': True, 'order_by': 'Department.name'}) + depts_with_inherent_role: list['Department'] = Relationship( + back_populates="members_with_inherent_role", + sa_relationship_kwargs={ + 'primaryjoin': 'and_(Department.id == DeptMembership.department_id, ' + 'DeptMembership.has_inherent_role)', + 'secondary': 'dept_membership', + 'order_by': 'Department.name', 'viewonly': True}) + can_admin_checklist_depts: list['Department'] = Relationship( + back_populates="members_who_can_admin_checklist", + sa_relationship_kwargs={ + 'primaryjoin': 'and_(Department.id == DeptMembership.department_id, ' + 'or_(DeptMembership.is_checklist_admin == True, ' + 'DeptMembership.is_dept_head == True))', + 'secondary': 'dept_membership', 'viewonly': True, 'order_by': 'Department.name'}) + poc_depts: list['Department'] = Relationship( + back_populates="pocs", + sa_relationship_kwargs={ + 'primaryjoin': 'and_(Department.id == DeptMembership.department_id, ' + 'DeptMembership.is_poc == True)', + 'secondary': 'dept_membership', 'viewonly': True, 'order_by': 'Department.name'}) + explicitly_requested_depts: list['Department'] = Relationship( + back_populates="explicitly_requesting_attendees", + sa_relationship_kwargs={ + 'secondary': 'dept_membership_request', 'order_by': 'Department.name', + 'overlaps': "attendee,dept_membership_requests,department,membership_requests"}) + requested_depts: list['Department'] = Relationship( + back_populates="requesting_attendees", + sa_relationship_kwargs={ + 'primaryjoin': 'or_(DeptMembershipRequest.department_id == Department.id, ' + 'DeptMembershipRequest.department_id == None)', + 'secondary': 'dept_membership_request', 'order_by': 'Department.name', 'viewonly': True}) + + staffing: bool = False + agreed_to_volunteer_agreement: bool = False + reviewed_emergency_procedures: bool = False + reviewed_cash_handling: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True, default=None) + name_in_credits: str | None + walk_on_volunteer: bool = False + nonshift_minutes: int = 0 + past_years: str = '' + can_work_setup: bool = False + can_work_teardown: bool = False + + admin_account: 'AdminAccount' = Relationship( + back_populates="attendee", + sa_relationship_kwargs={'lazy': 'select', 'cascade': 'all,delete-orphan', 'passive_deletes': True}) + managers: list['AttendeeAccount'] = Relationship( + back_populates="attendees", + sa_relationship_kwargs={'secondary': 'attendee_attendee_account'}) + + agent_codes: list['ArtShowAgentCode'] = Relationship(back_populates="attendee") + art_show_application: 'ArtShowApplication' = Relationship( + back_populates="attendee") + art_show_receipts: list['ArtShowReceipt'] = Relationship( + back_populates="attendee", + sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True, 'overlaps': 'art_show_purchases,buyer'}) + + marketplace_application: 'ArtistMarketplaceApplication' = Relationship( + back_populates="attendee", sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + + escalation_tickets: list['EscalationTicket'] = Relationship( + back_populates="attendees", sa_relationship_kwargs={'lazy': 'select', 'secondary': "attendee_escalation_ticket"}) + food_restrictions: 'FoodRestrictions' = Relationship(back_populates="attendee", + sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + + promo_code_groups: list['PromoCodeGroup'] = Relationship(back_populates="buyer", sa_relationship_kwargs={'lazy': 'select'}) + + no_shirt: 'NoShirt' = Relationship(back_populates="attendee", + sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + sales: list['Sale'] = Relationship(back_populates="attendee") + mpoints_for_cash: list['MPointsForCash'] = Relationship(back_populates="attendee", + sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + old_mpoint_exchanges: list['OldMPointExchange'] = Relationship( + back_populates="attendee", sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + dept_checklist_items: list['DeptChecklistItem'] = Relationship( + back_populates="attendee", sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + + indie_developer: 'IndieDeveloper' = Relationship( + back_populates="attendee") + + hotel_eligible: bool = False + hotel_requests: 'HotelRequests' = Relationship(back_populates="attendee", + sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + room_assignments: list['RoomAssignment'] = Relationship(back_populates="attendee", + sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + lottery_application: 'LotteryApplication' = Relationship( + back_populates="attendee") # The PIN/password used by third party hotel reservation systems - hotel_pin = SQLAlchemyColumn(String, nullable=True, unique=True) + hotel_pin: str | None = Field(nullable=True, unique=True) # ========================= # mits # ========================= - mits_applicants = relationship('MITSApplicant', backref='attendee') + mits_applicants: list['MITSApplicant'] = Relationship( + back_populates="attendee") # ========================= # panels # ========================= - assigned_panelists = relationship('AssignedPanelist', backref='attendee') - panel_applicants = relationship('PanelApplicant', backref='attendee', cascade='save-update,merge,refresh-expire,expunge') - panel_applications = relationship('PanelApplication', backref='poc') - panel_feedback = relationship('EventFeedback', backref='attendee') - submitted_panels = relationship( + assigned_panelists: list['AssignedPanelist'] = Relationship(back_populates="attendee", + sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + panel_applicants: list['PanelApplicant'] = Relationship( + back_populates="attendee") + panel_applications: list['PanelApplication'] = Relationship( + back_populates="poc") + submitted_panels: list['PanelApplication'] = Relationship(sa_relationship=relationship( 'PanelApplication', secondary='panel_applicant', secondaryjoin='and_(PanelApplicant.id == PanelApplication.submitter_id)', primaryjoin='and_(Attendee.id == PanelApplicant.attendee_id, PanelApplicant.submitter == True)', viewonly=True - ) + )) + panel_feedback: 'EventFeedback' = Relationship(back_populates="attendee", + sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) # ========================= # attractions # ========================= - _NOTIFICATION_EMAIL = 0 - _NOTIFICATION_TEXT = 1 - _NOTIFICATION_NONE = 2 - _NOTIFICATION_PREF_OPTS = [ + _NOTIFICATION_EMAIL: ClassVar = 0 + _NOTIFICATION_TEXT: ClassVar = 1 + _NOTIFICATION_NONE: ClassVar = 2 + _NOTIFICATION_PREF_OPTS: ClassVar = [ (_NOTIFICATION_EMAIL, 'Email'), (_NOTIFICATION_TEXT, 'Text'), (_NOTIFICATION_NONE, 'None')] - notification_pref = Column(Choice(_NOTIFICATION_PREF_OPTS), default=_NOTIFICATION_EMAIL) - attractions_opt_out = Column(Boolean, default=False) - - attraction_signups = relationship('AttractionSignup', backref='attendee', order_by='AttractionSignup.signup_time') - attraction_event_signups = association_proxy('attraction_signups', 'event') - attraction_notifications = relationship( - 'AttractionNotification', backref='attendee', order_by='AttractionNotification.sent_time') + notification_pref: int = Field(sa_column=Column(Choice(_NOTIFICATION_PREF_OPTS)), default=_NOTIFICATION_EMAIL) + attractions_opt_out: bool = False + + attraction_events: list['AttractionEvent'] = Relationship( + back_populates="attendees", + sa_relationship_kwargs={ + 'secondary': 'attraction_signup', + 'overlaps': 'attendee,attraction_signups,event,signups'}) + attraction_signups: list['AttractionSignup'] = Relationship( + back_populates="attendee", + sa_relationship_kwargs={'order_by': 'AttractionSignup.signup_time', 'cascade': 'all,delete-orphan', 'passive_deletes': True}) + attraction_notifications: list['AttractionNotification'] = Relationship( + back_populates="attendee", + sa_relationship_kwargs={'order_by': 'AttractionNotification.sent_time', 'cascade': 'all,delete-orphan', 'passive_deletes': True}) # ========================= # tabletop # ========================= - games = relationship('TabletopGame', backref='attendee') - checkouts = relationship('TabletopCheckout', backref='attendee') + games: list['TabletopGame'] = Relationship(back_populates="attendee", + sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + checkouts: list['TabletopCheckout'] = Relationship(back_populates="attendee", + sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) # ========================= # badge printing # ========================= - print_requests = relationship('PrintJob', backref='attendee', order_by='desc(PrintJob.last_updated)') + print_requests: list['PrintJob'] = Relationship( + back_populates="attendee", + sa_relationship_kwargs={'order_by': 'desc(PrintJob.last_updated)', 'cascade': 'all,delete-orphan', 'passive_deletes': True}) # ========================= # art show # ========================= - art_show_bidder = relationship('ArtShowBidder', backref=backref('attendee', load_on_pending=True), uselist=False) - art_show_purchases = relationship( - 'ArtShowPiece', - backref='buyer', - cascade='save-update,merge,refresh-expire,expunge', - secondary='art_show_receipt') - art_agent_apps = relationship( - 'ArtShowApplication', - backref='agents', - secondaryjoin='and_(ArtShowAgentCode.app_id == ArtShowApplication.id, ArtShowAgentCode.cancelled == None)', - secondary='art_show_agent_code', - viewonly=True) - - _attendee_table_args = [ - Index('ix_attendee_paid_group_id', paid, group_id), - Index('ix_attendee_badge_status_badge_type', badge_status, badge_type), + art_show_bidder: 'ArtShowBidder' = Relationship(back_populates="attendee", + sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + art_show_purchases: list['ArtShowPiece'] = Relationship( + back_populates="buyer", + sa_relationship_kwargs={'secondary': 'art_show_receipt'}) + art_agent_apps: list['ArtShowApplication'] = Relationship( + back_populates="agents", + sa_relationship_kwargs={ + 'secondaryjoin': 'and_(ArtShowAgentCode.app_id == ArtShowApplication.id, ArtShowAgentCode.cancelled == None)', + 'secondary': 'art_show_agent_code', 'viewonly': True + }) + + _attendee_table_args: ClassVar = [ + Index('ix_attendee_paid_group_id', 'paid', 'group_id'), + Index('ix_attendee_badge_status_badge_type', 'badge_status', 'badge_type'), ] - __table_args__ = tuple(_attendee_table_args) - _repr_attr_names = ['full_name'] + __table_args__: ClassVar = tuple(_attendee_table_args) + _repr_attr_names: ClassVar = ['full_name'] def to_dict(self, *args, **kwargs): # Kludgey fix for SQLAlchemy breaking our stuff d = super().to_dict(*args, **kwargs) - d.pop('attraction_event_signups', None) d.pop('receipt_changes', None) d['badge_num'] = self.badge_num return d @@ -571,8 +598,8 @@ def _misc_adjustments(self): self.age_group = self.age_group_conf['val'] for attr in ['first_name', 'last_name']: - value = getattr(self, attr) - if (value.isupper() or value.islower()): + value = getattr(self, attr, '') + if value and (value.isupper() or value.islower()): setattr(self, attr, value.title()) if self.legal_name and self.full_name == self.legal_name: @@ -629,6 +656,8 @@ def _status_adjustments(self): @presave_adjustment def _staffing_adjustments(self): + import six + if self.is_dept_head: self.staffing = True if self.paid == c.NOT_PAID: @@ -638,7 +667,9 @@ def _staffing_adjustments(self): self.staffing = True if not self.is_new: - old_ribbon = map(int, self.orig_value_of('ribbon').split(',')) if self.orig_value_of('ribbon') else [] + old_ribbon = self.orig_value_of('ribbon') + if old_ribbon and isinstance(old_ribbon, six.string_types): + old_ribbon = map(int, self.orig_value_of('ribbon').split(',')) old_staffing = self.orig_value_of('staffing') if old_staffing and not self.staffing or c.VOLUNTEER_RIBBON not in self.ribbon_ints \ @@ -793,13 +824,6 @@ def badge_num(self, value): self.session.add(badge) self.active_badge = badge - @property - def last_badge_num(self): - if self.active_badge: - return self.badge_num - if self.lost_badges: - return self.lost_badges[0].ident - @property def lost_badges(self): return [badge for badge in self.allocated_badges if badge.active == False and badge.reported_lost != None] @@ -907,7 +931,7 @@ def access_sections(self): section_list.append('mits_admin') if c.MIVS in self.ribbon_ints or self.group and self.group.guest and self.group.guest.group_type == c.MIVS: section_list.append('showcase_admin') - if self.art_show_applications or self.art_show_bidder or self.art_show_purchases or self.art_agent_apps: + if self.art_show_application or self.art_show_bidder or self.art_show_purchases or self.art_agent_apps: section_list.append('art_show_admin') if self.marketplace_application: section_list.append('marketplace_admin') @@ -1396,7 +1420,7 @@ def cannot_abandon_badge_check(self, including_last_adult=True): email_only(c.REGDESK_EMAIL), " to cancel your badge") - if self.art_show_applications and self.art_show_applications[0].is_valid: + if self.art_show_application and self.art_show_application.is_valid: return f"Please contact {email_only(c.ART_SHOW_EMAIL)} to cancel your art show application first." if self.art_agent_apps and any(app.is_valid for app in self.art_agent_apps): return "Please ask the artist you're agenting for to {} first.".format( @@ -1626,7 +1650,7 @@ def is_inherently_transferable(self): and not self.overridden_price \ and not self.admin_account \ and not self.dept_memberships_with_inherent_role \ - and (not self.art_show_applications or not self.art_show_applications[0].is_valid) \ + and (not self.art_show_application or not self.art_show_application.is_valid) \ and (not self.art_agent_apps or not any(app.is_valid for app in self.art_agent_apps)) \ and (not self.lottery_application or self.lottery_application.status not in self.dq_lottery_statuses) @@ -1637,7 +1661,7 @@ def transferable_actions(self): if self.lottery_application and self.lottery_application.status == c.COMPLETE: can_do.append("withdraw your hotel lottery entry") - if self.art_show_applications and self.art_show_applications[0].is_valid: + if self.art_show_application and self.art_show_application.is_valid: can_do.append(f"contact {email_only(c.ART_SHOW_EMAIL)} to cancel your art show application") if self.art_agent_apps and any(app.is_valid for app in self.art_agent_apps): can_do.append("ask the artist you're agenting for to {} first.".format( @@ -1751,7 +1775,7 @@ def has_personalized_badge(self): @property def donation_swag(self): donation_items = [ - desc for amount, desc in sorted(c.DONATION_TIERS.items()) if amount and self.amount_extra >= amount] + desc for amount, desc in sorted(c.DONATION_TIERS.items()) if amount and (self.amount_extra or 0) >= amount] extra_donations = ['Extra donation of ${}'.format(self.extra_donation)] if self.extra_donation else [] return donation_items + extra_donations @@ -1792,7 +1816,7 @@ def merch_items(self): """ merch = [] for amount, desc in sorted(c.DONATION_TIERS.items()): - if amount and self.amount_extra >= amount: + if amount and (self.amount_extra or 0) >= amount: merch.append(desc) items = c.DONATION_TIER_ITEMS.get(amount, []) if len(items) == 1: @@ -2073,12 +2097,12 @@ def worked_shifts(self): @property def weighted_hours(self): weighted_hours = sum(s.job.weighted_hours for s in self.shifts) - return weighted_hours + self.nonshift_minutes / 60 + return weighted_hours + (self.nonshift_minutes or 0) / 60 @property def unweighted_hours(self): unweighted_hours = sum(s.job.real_duration for s in self.shifts) / 60 - return unweighted_hours + self.nonshift_minutes / 60 + return unweighted_hours + (self.nonshift_minutes or 0) / 60 def weighted_hours_in(self, department_id): if not department_id: @@ -2093,12 +2117,12 @@ def unweighted_hours_in(self, department_id): @property def worked_hours(self): weighted_hours = sum(s.job.weighted_hours for s in self.worked_shifts) - return weighted_hours + self.nonshift_minutes / 60 + return weighted_hours + (self.nonshift_minutes or 0) / 60 @property def unweighted_worked_hours(self): unweighted_hours = sum(s.job.real_duration / 60 for s in self.worked_shifts) - return unweighted_hours + self.nonshift_minutes / 60 + return unweighted_hours + (self.nonshift_minutes or 0) / 60 def worked_hours_in(self, department_id): if not department_id: @@ -2225,7 +2249,9 @@ def has_role_somewhere(self): def depts_where_can_admin(self): if self.admin_account and self.admin_account.full_dept_admin: from uber.models.department import Department - return self.session.query(Department).order_by(Department.name).all() + return self.session.query(Department).options( + selectinload(Department.dept_roles), selectinload(Department.job_templates) + ).order_by(Department.name).all() return self.depts_with_inherent_role def has_shifts_in(self, department): @@ -2294,13 +2320,6 @@ def staffer_hotel_eligibility(self): if self.badge_type == c.STAFF_BADGE and (self.is_new or self.orig_value_of('badge_type') != c.STAFF_BADGE): self.hotel_eligible = True - @presave_adjustment - def staffer_setup_teardown(self): - if self.setup_hotel_approved: - self.can_work_setup = True - if self.teardown_hotel_approved: - self.can_work_teardown = True - @property def hotel_shifts_required(self): return bool(c.VOLUNTEER_CHECKLIST_OPEN and self.hotel_nights and not self.is_dept_head and self.takes_shifts) @@ -2529,11 +2548,6 @@ def is_signed_up_for_attraction(self, attraction): def is_signed_up_for_attraction_feature(self, feature): return feature in self.attraction_features - def can_admin_attraction(self, attraction): - if not self.admin_account: - return False - return self.admin_account.id == attraction.owner_id or self.can_admin_dept_for(attraction.department_id) - # ========================= # guests # ========================= @@ -2559,19 +2573,22 @@ def guest_group(self): ) -class AttendeeAccount(MagModel): - public_id = Column(Uuid(as_uuid=False), default=lambda: str(uuid4()), nullable=True) - email = Column(String) - hashed = Column(String, private=True) - password_reset = relationship('PasswordReset', backref='attendee_account', uselist=False) - attendees = relationship( - 'Attendee', backref='managers', order_by='Attendee.registered', - cascade='save-update,merge,refresh-expire,expunge', - secondary='attendee_attendee_account') - imported = Column(Boolean, default=False) - unused_years = Column(Integer, default=0) +class AttendeeAccount(MagModel, table=True): + public_id: str | None = Field(sa_type=Uuid(as_uuid=False), default_factory=lambda: str(uuid4()), nullable=True) + email: str = '' + hashed: str = Field(default='', private=True) + password_reset: 'PasswordReset' = Relationship( + back_populates="attendee_account", + sa_relationship_kwargs={'lazy': 'select', 'cascade': 'all,delete-orphan', 'passive_deletes': True}) + attendees: list['Attendee'] = Relationship( + back_populates="managers", + sa_relationship_kwargs={ + 'order_by': 'Attendee.registered', + 'secondary': 'attendee_attendee_account'}) + imported: bool = False + unused_years: int = 0 - email_model_name = 'account' + email_model_name: ClassVar = 'account' @presave_adjustment def strip_email(self): @@ -2685,9 +2702,13 @@ def refunded_deferred_attendees(self): and not attendee.current_attendee] -class BadgePickupGroup(MagModel): - public_id = Column(Uuid(as_uuid=False), default=lambda: str(uuid4()), nullable=True) - account_id = Column(String) +class BadgePickupGroup(MagModel, table=True): + public_id: str | None = Field(sa_type=Uuid(as_uuid=False), default_factory=lambda: str(uuid4()), nullable=True) + account_id: str = '' + + attendees: list['Attendee'] = Relationship( + back_populates="badge_pickup_group", + sa_relationship_kwargs={'lazy': 'selectin', 'order_by': 'Attendee.full_name'}) def build_from_account(self, account): for attendee in account.attendees: @@ -2762,11 +2783,13 @@ def under_18_badges(self): return [attendee for attendee in self.check_inable_attendees if attendee.age_now_or_at_con < 18] -class FoodRestrictions(MagModel): - attendee_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id'), unique=True) - standard = Column(MultiChoice(c.FOOD_RESTRICTION_OPTS)) - sandwich_pref = Column(MultiChoice(c.SANDWICH_OPTS)) - freeform = Column(String) +class FoodRestrictions(MagModel, table=True): + attendee_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', ondelete='CASCADE', unique=True) + attendee: 'Attendee' = Relationship(back_populates="food_restrictions", sa_relationship_kwargs={'lazy': 'joined'}) + + standard: str = Field(sa_type=MultiChoice(c.FOOD_RESTRICTION_OPTS), default='') + sandwich_pref: str = Field(sa_type=MultiChoice(c.SANDWICH_OPTS), default='') + freeform: str = '' def __getattr__(self, name): try: diff --git a/uber/models/attendee_tournaments.py b/uber/models/attendee_tournaments.py deleted file mode 100644 index f8c074389..000000000 --- a/uber/models/attendee_tournaments.py +++ /dev/null @@ -1,46 +0,0 @@ -""" -NO LONGER USED. - -The attendee_tournaments module is no longer used, but has been -included for backward compatibility with legacy servers. -""" - -from sqlalchemy import func -from sqlalchemy.types import Boolean, String - -from uber.config import c -from uber.models import MagModel, Attendee -from uber.models.types import Choice, DefaultColumn as Column, MultiChoice - - -__all__ = ['AttendeeTournament'] - - -class AttendeeTournament(MagModel): - first_name = Column(String) - last_name = Column(String) - email = Column(String) - cellphone = Column(String) - game = Column(String) - availability = Column(MultiChoice(c.TOURNAMENT_AVAILABILITY_OPTS)) - format = Column(String) - experience = Column(String) - needs = Column(String) - why = Column(String) - volunteering = Column(Boolean, default=False) - - status = Column(Choice(c.TOURNAMENT_STATUS_OPTS), default=c.NEW, admin_only=True) - - email_model_name = 'app' - - @property - def full_name(self): - return self.first_name + ' ' + self.last_name - - @property - def matching_attendee(self): - return self.session.query(Attendee).filter( - Attendee.first_name == self.first_name.title(), - Attendee.last_name == self.last_name.title(), - func.lower(Attendee.email) == self.email.lower() - ).first() diff --git a/uber/models/attraction.py b/uber/models/attraction.py index 9a1417860..ebb52af20 100644 --- a/uber/models/attraction.py +++ b/uber/models/attraction.py @@ -3,19 +3,20 @@ import pytz from sqlalchemy import and_, cast, exists, func, not_ -from sqlalchemy.ext.associationproxy import association_proxy +from sqlalchemy.ext.associationproxy import AssociationProxy, association_proxy from sqlalchemy.ext.hybrid import hybrid_property -from sqlalchemy.orm import backref -from sqlalchemy.schema import ForeignKey, UniqueConstraint +from sqlalchemy.schema import UniqueConstraint from sqlalchemy.sql import text from sqlalchemy.sql.expression import bindparam -from sqlalchemy.types import Boolean, Integer, Uuid, String, DateTime +from sqlalchemy.types import Uuid, String, DateTime +from typing import ClassVar from uber.config import c from uber.custom_tags import humanize_timedelta, location_event_name, location_room_name from uber.decorators import presave_adjustment, render, classproperty from uber.models import MagModel, Attendee -from uber.models.types import default_relationship as relationship, Choice, DefaultColumn as Column, utcmin +from uber.models.types import (DefaultColumn as Column, default_relationship as relationship, Choice, utcmin, + DefaultField as Field, DefaultRelationship as Relationship) from uber.utils import evening_datetime, noon_datetime, localized_now, slugify, listify, groupify @@ -25,13 +26,13 @@ class AttractionMixin(): - populate_schedule = Column(Boolean, default=True) - no_notifications = Column(Boolean, default=False) - waitlist_available = Column(Boolean, default=True) - waitlist_slots = Column(Integer, default=10) - signups_open_relative = Column(Integer, default=c.DEFAULT_ATTRACTIONS_SIGNUPS_MINUTES) - signups_open_time = Column(DateTime(timezone=True), nullable=True) - slots = Column(Integer, default=1) + populate_schedule: bool = True + no_notifications: bool = False + waitlist_available: bool = True + waitlist_slots: int = 10 + signups_open_relative: int = c.DEFAULT_ATTRACTIONS_SIGNUPS_MINUTES + signups_open_time: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True) + slots: int = 1 @classproperty def inherited_cols(cls): @@ -92,11 +93,11 @@ def get_updated_signup_vals(self): return same_time_settings, update_attrs -class Attraction(MagModel, AttractionMixin): - _NONE = 0 - _PER_FEATURE = 1 - _PER_ATTRACTION = 2 - _RESTRICTION_OPTS = [( +class Attraction(MagModel, AttractionMixin, table=True): + _NONE: ClassVar = 0 + _PER_FEATURE: ClassVar = 1 + _PER_ATTRACTION: ClassVar = 2 + _RESTRICTION_OPTS: ClassVar = [( _NONE, 'Attendees can attend as many events as they wish ' '(least restrictive)' @@ -108,9 +109,9 @@ class Attraction(MagModel, AttractionMixin): 'Attendees can only attend one event in this attraction ' '(most restrictive)' )] - _RESTRICTIONS = dict(_RESTRICTION_OPTS) + _RESTRICTIONS: ClassVar = dict(_RESTRICTION_OPTS) - _ADVANCE_CHECKIN_OPTS = [ + _ADVANCE_CHECKIN_OPTS: ClassVar = [ (-1, 'Anytime during event'), (0, 'When the event starts'), (5, '5 minutes before'), @@ -121,7 +122,7 @@ class Attraction(MagModel, AttractionMixin): (45, '45 minutes before'), (60, '1 hour before')] - _ADVANCE_NOTICES_OPTS = [ + _ADVANCE_NOTICES_OPTS: ClassVar = [ ('', 'Never'), (0, 'When checkin starts'), (5, '5 minutes before checkin'), @@ -130,61 +131,46 @@ class Attraction(MagModel, AttractionMixin): (60, '1 hour before checkin'), (120, '2 hours before checkin'), (1440, '1 day before checkin')] - - name = Column(String, unique=True) - slug = Column(String, unique=True) - description = Column(String) - full_description = Column(String) - is_public = Column(Boolean, default=False) - checkin_reminder = Column(Integer, default=None, nullable=True) - advance_checkin = Column(Integer, default=0) - restriction = Column(Choice(_RESTRICTION_OPTS), default=_NONE) - badge_num_required = Column(Boolean, default=False) - department_id = Column(Uuid(as_uuid=False), ForeignKey('department.id'), nullable=True) - owner_id = Column(Uuid(as_uuid=False), ForeignKey('admin_account.id'), nullable=True) - - owner = relationship( - 'AdminAccount', - cascade='save-update,merge', - backref=backref( - 'attractions', - cascade='save-update,merge,refresh-expire,expunge', - uselist=True, - order_by='Attraction.name')) - owner_attendee = relationship( + + department_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='department.id', nullable=True) + department: 'Department' = Relationship(back_populates="attractions", sa_relationship_kwargs={'order_by': 'Department.name'}) + + owner_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='admin_account.id', nullable=True) + owner: 'AdminAccount' = Relationship(back_populates="attractions") + owner_attendee: 'Attendee' = Relationship(sa_relationship=relationship( 'Attendee', cascade='merge', secondary='admin_account', - uselist=False, - viewonly=True) - department = relationship( - 'Department', - cascade='save-update,merge', - backref=backref( - 'attractions', - cascade='save-update,merge', - uselist=True), - order_by='Department.name') - features = relationship( - 'AttractionFeature', - backref='attraction', - order_by='[AttractionFeature.name, AttractionFeature.id]') - public_features = relationship( + viewonly=True)) + + name: str = Field(default='', unique=True) + slug: str = Field(default='', unique=True) + description: str = '' + full_description: str = '' + is_public: bool = False + checkin_reminder: int | None = Field(default=None, nullable=True) + advance_checkin: int = 0 + restriction: int = Field(sa_column=Column(Choice(_RESTRICTION_OPTS)), default=_NONE) + badge_num_required: bool = False + + features: list['AttractionFeature'] = Relationship( + back_populates="attraction", + sa_relationship_kwargs={'lazy': 'selectin', 'order_by': '[AttractionFeature.name, AttractionFeature.id]', + 'cascade': 'all,delete-orphan', 'passive_deletes': True}) + public_features: list['AttractionFeature'] = Relationship(sa_relationship=relationship( 'AttractionFeature', primaryjoin='and_(' 'AttractionFeature.attraction_id == Attraction.id,' 'AttractionFeature.is_public == True)', viewonly=True, - order_by='[AttractionFeature.name, AttractionFeature.id]') - events = relationship( - 'AttractionEvent', - backref='attraction', - order_by='[AttractionEvent.start_time, AttractionEvent.id]') - signups = relationship( - 'AttractionSignup', - backref='attraction', - viewonly=True, - order_by='[AttractionSignup.checkin_time, AttractionSignup.id]') + order_by='[AttractionFeature.name, AttractionFeature.id]')) + events: list['AttractionEvent'] = Relationship( + back_populates="attraction", + sa_relationship_kwargs={'order_by': '[AttractionEvent.start_time, AttractionEvent.id]', + 'cascade': 'all,delete-orphan', 'passive_deletes': True}) + signups: list['AttractionSignup'] = Relationship( + back_populates="attraction", + sa_relationship_kwargs={'viewonly': True, 'order_by': '[AttractionSignup.checkin_time, AttractionSignup.id]'}) @presave_adjustment def slugify_name(self): @@ -342,18 +328,22 @@ def signups_requiring_notification(self, session, from_time, to_time, options=No return groupify(query, lambda x: x[0], lambda x: x[1]) -class AttractionFeature(MagModel, AttractionMixin): - name = Column(String) - slug = Column(String) - description = Column(String) - is_public = Column(Boolean, default=False) - badge_num_required = Column(Boolean, default=False) - attraction_id = Column(Uuid(as_uuid=False), ForeignKey('attraction.id')) +class AttractionFeature(MagModel, AttractionMixin, table=True): + attraction_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attraction.id', ondelete='CASCADE') + attraction: 'Attraction' = Relationship(back_populates="features", sa_relationship_kwargs={'lazy': 'joined'}) - events = relationship( - 'AttractionEvent', backref='feature', order_by='[AttractionEvent.start_time, AttractionEvent.id]') + name: str = '' + slug: str = '' + description: str = '' + is_public: bool = False + badge_num_required: bool = False - __table_args__ = ( + events: list['AttractionEvent'] = Relationship( + back_populates="feature", + sa_relationship_kwargs={'lazy': 'selectin', 'order_by': '[AttractionEvent.start_time, AttractionEvent.id]', + 'cascade': 'all,delete-orphan', 'passive_deletes': True}) + + __table_args__: ClassVar = ( UniqueConstraint('name', 'attraction_id'), UniqueConstraint('slug', 'attraction_id'), ) @@ -467,38 +457,37 @@ def available_events_by_day(self): return groupify(self.available_events, 'start_day_local') -# ===================================================================== -# TODO: This, along with the panels.models.Event class, should be -# refactored into a more generic "SchedulableMixin". Any model -# class that has a location, a start time, and a duration would -# inherit from the SchedulableMixin. Unfortunately the -# panels.models.Event stores its duration as an integer number -# of half hours, thus is not usable by Attractions. -# ===================================================================== -class AttractionEvent(MagModel, AttractionMixin): - attraction_feature_id = Column(Uuid(as_uuid=False), ForeignKey('attraction_feature.id')) - attraction_id = Column(Uuid(as_uuid=False), ForeignKey('attraction.id'), index=True) - event_location_id = Column(Uuid(as_uuid=False), ForeignKey('event_location.id', ondelete='SET NULL'), nullable=True) - - start_time = Column(DateTime(timezone=True), default=c.EPOCH) - duration = Column(Integer, default=60) - - signups = relationship('AttractionSignup', backref='event', order_by='AttractionSignup.checkin_time') - - attendee_signups = association_proxy('signups', 'attendee') - - notifications = relationship('AttractionNotification', backref='event', order_by='AttractionNotification.sent_time') - - notification_replies = relationship( - 'AttractionNotificationReply', backref='event', order_by='AttractionNotificationReply.sid') - - attendees = relationship( - 'Attendee', - backref=backref('attraction_events', overlaps="attendee,attraction_signups,event,signups"), - cascade='save-update,merge,refresh-expire,expunge', - secondary='attraction_signup', - order_by='attraction_signup.c.signup_time', - overlaps="signups,event,attraction_signups,attendee") +class AttractionEvent(MagModel, AttractionMixin, table=True): + attraction_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attraction.id', ondelete='CASCADE', index=True) + attraction: 'Attraction' = Relationship(back_populates="events", sa_relationship_kwargs={'lazy': 'joined'}) + + attraction_feature_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attraction_feature.id', ondelete='CASCADE') + feature: 'AttractionFeature' = Relationship(back_populates="events", sa_relationship_kwargs={'lazy': 'joined'}) + + event_location_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='event_location.id', nullable=True) + location: 'EventLocation' = Relationship( + back_populates="attractions", sa_relationship_kwargs={'lazy': 'joined'}) + + start_time: datetime = Field(sa_type=DateTime(timezone=True), default=c.EPOCH) + duration: int = 60 + + schedule_item: 'Event' = Relationship( + back_populates="attraction", sa_relationship_kwargs={'lazy': 'joined'}) + signups: list['AttractionSignup'] = Relationship( + back_populates="event", + sa_relationship_kwargs={'order_by': 'AttractionSignup.checkin_time', 'cascade': 'all,delete-orphan', 'passive_deletes': True}) + attendee_signups: ClassVar = association_proxy('signups', 'attendee') + notifications: list['AttractionNotification'] = Relationship( + back_populates="event", + sa_relationship_kwargs={'order_by': 'AttractionNotification.sent_time', 'cascade': 'all,delete-orphan', 'passive_deletes': True}) + notification_replies: list['AttractionNotificationReply'] = Relationship( + back_populates="event", + sa_relationship_kwargs={'order_by': 'AttractionNotificationReply.sid'}) + attendees: list['Attendee'] = Relationship( + back_populates="attraction_events", + sa_relationship_kwargs={ + 'secondary': 'attraction_signup', + 'order_by': 'attraction_signup.c.signup_time', 'overlaps': 'signups,event,attraction_signups,attendee'}) @presave_adjustment def _fix_attraction_id(self): @@ -753,27 +742,27 @@ def overlap(self, event): return int((earliest_end - latest_start).total_seconds()) -class AttractionSignup(MagModel): - attraction_event_id = Column(Uuid(as_uuid=False), ForeignKey('attraction_event.id')) - attraction_id = Column(Uuid(as_uuid=False), ForeignKey('attraction.id')) - attendee_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id')) +class AttractionSignup(MagModel, table=True): + attraction_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attraction.id', ondelete='CASCADE') + attraction: 'Attraction' = Relationship( + back_populates="signups", sa_relationship_kwargs={'lazy': 'joined'}) + + attraction_event_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attraction_event.id', ondelete='CASCADE') + event: 'AttractionEvent' = Relationship(back_populates="signups", sa_relationship_kwargs={'lazy': 'joined'}) - signup_time = Column(DateTime(timezone=True), default=lambda: datetime.now(pytz.UTC)) - checkin_time = Column(DateTime(timezone=True), default=lambda: utcmin.datetime, index=True) - on_waitlist = Column(Boolean, default=False) + attendee_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', ondelete='CASCADE') + attendee: 'Attendee' = Relationship(back_populates="attraction_signups", sa_relationship_kwargs={'lazy': 'joined'}) - notifications = relationship( - 'AttractionNotification', - backref=backref( - 'signup', - cascade='merge', - uselist=False, - viewonly=True), - primaryjoin='and_(' - 'AttractionSignup.attendee_id == foreign(AttractionNotification.attendee_id),' - 'AttractionSignup.attraction_event_id == foreign(AttractionNotification.attraction_event_id))', - order_by='AttractionNotification.sent_time', - viewonly=True) + signup_time: datetime = Field(sa_type=DateTime(timezone=True), default_factory=lambda: datetime.now(pytz.UTC)) + checkin_time: datetime = Field(sa_type=DateTime(timezone=True), default_factory=lambda: utcmin.datetime, index=True) + on_waitlist: bool = False + + notifications: list['AttractionNotification'] = Relationship( + back_populates="signup", + sa_relationship_kwargs={ + 'order_by': 'AttractionNotification.sent_time', 'viewonly': True, + 'primaryjoin': 'and_(AttractionSignup.attendee_id == foreign(AttractionNotification.attendee_id),' + 'AttractionSignup.attraction_event_id == foreign(AttractionNotification.attraction_event_id))'}) __mapper_args__ = {'confirm_deleted_rows': False} __table_args__ = (UniqueConstraint('attraction_event_id', 'attendee_id'),) @@ -852,17 +841,28 @@ def notify_waitlist(self): # TODO: Handle text notifs too -class AttractionNotification(MagModel): - attraction_event_id = Column(Uuid(as_uuid=False), ForeignKey('attraction_event.id')) - attraction_id = Column(Uuid(as_uuid=False), ForeignKey('attraction.id')) - attendee_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id')) +class AttractionNotification(MagModel, table=True): + attraction_event_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attraction_event.id', ondelete='CASCADE') + event: 'AttractionEvent' = Relationship(back_populates="notifications", sa_relationship_kwargs={'lazy': 'joined'}) - notification_type = Column(Choice(Attendee._NOTIFICATION_PREF_OPTS)) - ident = Column(String, index=True) - sid = Column(String) - sent_time = Column(DateTime(timezone=True), default=lambda: datetime.now(pytz.UTC)) - subject = Column(String) - body = Column(String) + attraction_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attraction.id', ondelete='CASCADE') + + attendee_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', ondelete='CASCADE') + attendee: 'Attendee' = Relationship(back_populates="attraction_notifications", sa_relationship_kwargs={'lazy': 'joined'}) + + notification_type: int = Field(sa_column=Column(Choice(Attendee._NOTIFICATION_PREF_OPTS)), default=0) + ident: str = Field(default='', index=True) + sid: str = '' + sent_time: datetime = Field(sa_type=DateTime(timezone=True), default=lambda: datetime.now(pytz.UTC)) + subject: str = '' + body: str = '' + + signup: 'AttractionSignup' = Relationship( + back_populates="notifications", + sa_relationship_kwargs={ + 'lazy': 'joined', 'viewonly': True, + 'primaryjoin': 'and_(AttractionSignup.attendee_id == foreign(AttractionNotification.attendee_id),' + 'AttractionSignup.attraction_event_id == foreign(AttractionNotification.attraction_event_id))'}) @presave_adjustment def _fix_attraction_id(self): @@ -870,18 +870,20 @@ def _fix_attraction_id(self): self.attraction_id = self.event.attraction_id -class AttractionNotificationReply(MagModel): - attraction_event_id = Column(Uuid(as_uuid=False), ForeignKey('attraction_event.id'), nullable=True) - attraction_id = Column(Uuid(as_uuid=False), ForeignKey('attraction.id'), nullable=True) - attendee_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id'), nullable=True) - - notification_type = Column(Choice(Attendee._NOTIFICATION_PREF_OPTS)) - from_phonenumber = Column(String) - to_phonenumber = Column(String) - sid = Column(String, index=True) - received_time = Column(DateTime(timezone=True), default=lambda: datetime.now(pytz.UTC)) - sent_time = Column(DateTime(timezone=True), default=lambda: datetime.now(pytz.UTC)) - body = Column(String) +class AttractionNotificationReply(MagModel, table=True): + attraction_event_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attraction_event.id', nullable=True) + event: 'AttractionEvent' = Relationship(back_populates="notification_replies", sa_relationship_kwargs={'lazy': 'joined'}) + + attraction_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attraction.id', nullable=True) + attendee_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', nullable=True) + + notification_type: int = Field(sa_column=Column(Choice(Attendee._NOTIFICATION_PREF_OPTS)), default=0) + from_phonenumber: str = '' + to_phonenumber: str = '' + sid: str = Field(default='', index=True) + received_time: datetime = Field(sa_type=DateTime(timezone=True), default_factory=lambda: datetime.now(pytz.UTC)) + sent_time: datetime = Field(sa_type=DateTime(timezone=True), default_factory=lambda: datetime.now(pytz.UTC)) + body: str = '' @presave_adjustment def _fix_attraction_id(self): diff --git a/uber/models/badge_printing.py b/uber/models/badge_printing.py index a35382fd2..3950a15d5 100644 --- a/uber/models/badge_printing.py +++ b/uber/models/badge_printing.py @@ -1,32 +1,36 @@ +from datetime import datetime from sqlalchemy.dialects.postgresql.json import JSONB from sqlalchemy.ext.mutable import MutableDict -from sqlalchemy.schema import ForeignKey from sqlalchemy.types import Boolean, Integer, Uuid, String, DateTime - +from typing import Any from uber.models import MagModel -from uber.models.types import DefaultColumn as Column, default_relationship as relationship +from uber.models.types import default_relationship as relationship, DefaultField as Field, DefaultRelationship as Relationship __all__ = ['PrintJob'] -class PrintJob(MagModel): - attendee_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id')) - admin_id = Column(Uuid(as_uuid=False), ForeignKey('admin_account.id'), nullable=True) - admin_name = Column(String) # Preserve admin's name in case their account is removed - printer_id = Column(String) - reg_station = Column(Integer, nullable=True) - print_fee = Column(Integer, default=0) - queued = Column(DateTime(timezone=True), nullable=True, default=None) - printed = Column(DateTime(timezone=True), nullable=True, default=None) - ready = Column(Boolean, default=True) - errors = Column(String) - is_minor = Column(Boolean) - json_data = Column(MutableDict.as_mutable(JSONB), default={}) - receipt_item = relationship('ReceiptItem', +class PrintJob(MagModel, table=True): + attendee_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', ondelete='CASCADE') + attendee: 'Attendee' = Relationship(back_populates="print_requests", sa_relationship_kwargs={'lazy': 'joined'}) + + admin_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='admin_account.id', nullable=True) + admin_account: "AdminAccount" = Relationship(back_populates="print_requests") + + admin_name: str = '' # Preserve admin's name in case their account is removed + printer_id: str = '' + reg_station: int | None = Field(nullable=True) + print_fee: int = 0 + queued: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True, default=None) + printed: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True, default=None) + ready: bool = True + errors: str = '' + is_minor: bool = False + json_data: dict[str, Any] = Field(sa_type=MutableDict.as_mutable(JSONB), default_factory=dict) + receipt_item: 'ReceiptItem' = Relationship(sa_relationship=relationship('ReceiptItem', primaryjoin='and_(' 'ReceiptItem.fk_model == "PrintJob", ' 'ReceiptItem.fk_id == foreign(PrintJob.id))', viewonly=True, - uselist=False) + uselist=False)) diff --git a/uber/models/commerce.py b/uber/models/commerce.py index f9950c49f..8b5988692 100644 --- a/uber/models/commerce.py +++ b/uber/models/commerce.py @@ -6,18 +6,18 @@ from sqlalchemy import func, or_ from sqlalchemy.sql.functions import coalesce -from sqlalchemy.schema import ForeignKey -from sqlalchemy.types import Boolean, Integer, String, DateTime, Uuid, JSON +from sqlalchemy.types import DateTime, Uuid, JSON from sqlalchemy.dialects.postgresql.json import JSONB from sqlalchemy.ext.mutable import MutableDict -from sqlalchemy.orm import backref +from typing import Any, ClassVar from uber.config import c from uber.custom_tags import format_currency from uber.decorators import presave_adjustment, classproperty from uber.models import MagModel from uber.models.attendee import Attendee -from uber.models.types import default_relationship as relationship, Choice, DefaultColumn as Column +from uber.models.types import (DefaultColumn as Column, default_relationship as relationship, Choice, + DefaultField as Field, DefaultRelationship as Relationship) from uber.payments import ReceiptManager log = logging.getLogger(__name__) @@ -28,65 +28,71 @@ 'NoShirt', 'OldMPointExchange', 'ReceiptInfo', 'ReceiptItem', 'ReceiptTransaction', 'Sale', 'TerminalSettlement'] -class ArbitraryCharge(MagModel): - amount = Column(Integer) - what = Column(String) - when = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC)) - reg_station = Column(Integer, nullable=True) +class ArbitraryCharge(MagModel, table=True): + amount: int = 0 + what: str = '' + when: datetime = Field(sa_type=DateTime(timezone=True), default_factory=lambda: datetime.now(UTC)) + reg_station: int | None = Field(default=0, nullable=True) - _repr_attr_names = ['what'] + _repr_attr_names: ClassVar = ['what'] -class MerchDiscount(MagModel): +class MerchDiscount(MagModel, table=True): """Staffers can apply a single-use discount to any merch purchases.""" - attendee_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id'), unique=True) - uses = Column(Integer) + attendee_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', ondelete='CASCADE', unique=True) + uses: int = 0 -class MerchPickup(MagModel): - picked_up_by_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id')) - picked_up_for_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id'), unique=True) - picked_up_by = relationship( +class MerchPickup(MagModel, table=True): + picked_up_by_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', nullable=True) + picked_up_for_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', unique=True, nullable=True) + picked_up_by: 'Attendee' = Relationship(sa_relationship=relationship( Attendee, - primaryjoin='MerchPickup.picked_up_by_id == Attendee.id', - cascade='save-update,merge,refresh-expire,expunge') - picked_up_for = relationship( + primaryjoin='MerchPickup.picked_up_by_id == Attendee.id')) + picked_up_for: 'Attendee' = Relationship(sa_relationship=relationship( Attendee, - primaryjoin='MerchPickup.picked_up_for_id == Attendee.id', - cascade='save-update,merge,refresh-expire,expunge') + primaryjoin='MerchPickup.picked_up_for_id == Attendee.id')) -class MPointsForCash(MagModel): - attendee_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id')) - amount = Column(Integer) - when = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC)) +class MPointsForCash(MagModel, table=True): + attendee_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', ondelete='CASCADE') + attendee: 'Attendee' = Relationship(back_populates="mpoints_for_cash") + amount: int = 0 + when: datetime = Field(sa_type=DateTime(timezone=True), default_factory=lambda: datetime.now(UTC)) -class NoShirt(MagModel): + +class NoShirt(MagModel, table=True): """ Used to track when someone tried to pick up a shirt they were owed when we were out of stock, so that we can contact them later. """ - attendee_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id'), unique=True) + attendee_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', ondelete='CASCADE', unique=True) + attendee: 'Attendee' = Relationship(back_populates="no_shirt") + + +class OldMPointExchange(MagModel, table=True): + attendee_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', ondelete='CASCADE') + attendee: 'Attendee' = Relationship(back_populates="old_mpoint_exchanges") -class OldMPointExchange(MagModel): - attendee_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id')) - amount = Column(Integer) - when = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC)) + amount: int = 0 + when: datetime = Field(sa_type=DateTime(timezone=True), default_factory=lambda: datetime.now(UTC)) -class Sale(MagModel): - attendee_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id', ondelete='set null'), nullable=True) - what = Column(String) - cash = Column(Integer, default=0) - mpoints = Column(Integer, default=0) - when = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC)) - reg_station = Column(Integer, nullable=True) - payment_method = Column(Choice(c.SALE_OPTS), default=c.MERCH) +class Sale(MagModel, table=True): + attendee_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', nullable=True) + attendee: 'Attendee' = Relationship(back_populates="sales") + what: str = '' + cash: int = 0 + mpoints: int = 0 + when: datetime = Field(sa_type=DateTime(timezone=True), default_factory=lambda: datetime.now(UTC)) + reg_station: int | None = Field(nullable=True) + payment_method: int = Field(sa_column=Column(Choice(c.SALE_OPTS)), default=c.MERCH) -class ModelReceipt(MagModel): + +class ModelReceipt(MagModel, table=True): """ Attendees, groups, and art show apps have a running receipt that has items and transactions added to it dynamically. @@ -97,10 +103,15 @@ class ModelReceipt(MagModel): as during prereg, there may be multiple receipt transactions created with the same reference ID across multiple receipts. """ - invoice_num = Column(Integer, default=0) - owner_id = Column(Uuid(as_uuid=False), index=True) - owner_model = Column(String) - closed = Column(DateTime(timezone=True), nullable=True) + invoice_num: int = 0 + owner_id: str = Field(sa_type=Uuid(as_uuid=False), index=True) + owner_model: str = '' + closed: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True) + + receipt_txns: list['ReceiptTransaction'] = Relationship( + back_populates="receipt", sa_relationship_kwargs={'lazy': 'selectin', 'cascade': 'all,delete-orphan', 'passive_deletes': True}) + receipt_items: list['ReceiptItem'] = Relationship( + back_populates="receipt", sa_relationship_kwargs={'lazy': 'selectin', 'cascade': 'all,delete-orphan', 'passive_deletes': True}) def close_all_items(self, session): for item in self.open_receipt_items: @@ -312,7 +323,7 @@ def process_full_refund(self, session, model, who='non-admin', exclude_fees=Fals return refund_total, '' -class ReceiptTransaction(MagModel): +class ReceiptTransaction(MagModel, table=True): """ Transactions have two key properties: whether or not they were done through Stripe, and whether they represent a payment or a refund. @@ -329,34 +340,37 @@ class ReceiptTransaction(MagModel): plus it allows admins to refund Stripe payments per item. """ - receipt_id = Column(Uuid(as_uuid=False), ForeignKey('model_receipt.id', ondelete='SET NULL'), nullable=True) - receipt = relationship('ModelReceipt', foreign_keys=receipt_id, - cascade='save-update, merge', - backref=backref('receipt_txns', cascade='save-update, merge')) - receipt_info_id = Column(Uuid(as_uuid=False), ForeignKey('receipt_info.id', ondelete='SET NULL'), nullable=True) - receipt_info = relationship('ReceiptInfo', foreign_keys=receipt_info_id, - cascade='save-update, merge', - backref=backref('receipt_txns', cascade='save-update, merge')) - refunded_txn_id = Column(Uuid(as_uuid=False), ForeignKey('receipt_transaction.id', ondelete='SET NULL'), nullable=True) - refunded_txn = relationship('ReceiptTransaction', foreign_keys='ReceiptTransaction.refunded_txn_id', - backref=backref('refund_txns', order_by='ReceiptTransaction.added'), - cascade='save-update,merge,refresh-expire,expunge', - remote_side='ReceiptTransaction.id', - single_parent=True) - refunded = Column(Integer, default=0) - intent_id = Column(String) - charge_id = Column(String) - refund_id = Column(String) - method = Column(Choice(c.PAYMENT_METHOD_OPTS), default=c.STRIPE) - department = Column(Choice(c.RECEIPT_ITEM_DEPT_OPTS), default=c.OTHER_RECEIPT_ITEM) - amount = Column(Integer) - txn_total = Column(Integer, default=0) - processing_fee = Column(Integer, default=0) - added = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC)) - on_hold = Column(Boolean, default=False) - cancelled = Column(DateTime(timezone=True), nullable=True) - who = Column(String) - desc = Column(String) + receipt_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='model_receipt.id', ondelete='CASCADE') + receipt: 'ModelReceipt' = Relationship(back_populates="receipt_txns", sa_relationship_kwargs={'lazy': 'joined'}) + + receipt_info_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='receipt_info.id', nullable=True) + receipt_info: 'ReceiptInfo' = Relationship(back_populates="receipt_txns", sa_relationship_kwargs={'lazy': 'select'}) + + refunded_txn_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='receipt_transaction.id', nullable=True) + refunded_txn: 'ReceiptTransaction' = Relationship( + back_populates="refund_txns", + sa_relationship_kwargs={'foreign_keys': 'ReceiptTransaction.refunded_txn_id', 'remote_side': 'ReceiptTransaction.id'}) + refund_txns: list['ReceiptTransaction'] = Relationship( + back_populates="refunded_txn", + sa_relationship_kwargs={'order_by': 'ReceiptTransaction.added'}) + + refunded: int = 0 + intent_id: str = '' + charge_id: str = '' + refund_id: str = '' + method: int = Field(sa_column=Column(Choice(c.PAYMENT_METHOD_OPTS)), default=c.STRIPE) + department: int = Field(sa_column=Column(Choice(c.RECEIPT_ITEM_DEPT_OPTS)), default=c.OTHER_RECEIPT_ITEM) + amount: int = 0 + txn_total: int = 0 + processing_fee: int = 0 + added: datetime = Field(sa_type=DateTime(timezone=True), default_factory=lambda: datetime.now(UTC)) + on_hold: bool = False + cancelled: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True) + who: str = '' + desc: str = '' + + receipt_items: list['ReceiptItem'] = Relationship( + back_populates="receipt_txn") @property def available_actions(self): @@ -535,30 +549,28 @@ def cannot_delete_reason(self): return "You cannot delete Stripe transactions." -class ReceiptItem(MagModel): - purchaser_id = Column(Uuid(as_uuid=False), index=True, nullable=True) - receipt_id = Column(Uuid(as_uuid=False), ForeignKey('model_receipt.id', ondelete='SET NULL'), nullable=True) - receipt = relationship('ModelReceipt', foreign_keys=receipt_id, - cascade='save-update, merge', - backref=backref('receipt_items', cascade='save-update, merge')) - txn_id = Column(Uuid(as_uuid=False), ForeignKey('receipt_transaction.id', ondelete='SET NULL'), nullable=True) - receipt_txn = relationship('ReceiptTransaction', foreign_keys=txn_id, - cascade='save-update, merge', - backref=backref('receipt_items', cascade='save-update, merge')) - fk_id = Column(Uuid(as_uuid=False), index=True, nullable=True) - fk_model = Column(String) - department = Column(Choice(c.RECEIPT_ITEM_DEPT_OPTS), default=c.OTHER_RECEIPT_ITEM) - category = Column(Choice(c.RECEIPT_CATEGORY_OPTS), default=c.OTHER) - amount = Column(Integer) - comped = Column(Boolean, default=False) - reverted = Column(Boolean, default=False) - count = Column(Integer, default=1) - added = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC)) - closed = Column(DateTime(timezone=True), nullable=True) - who = Column(String) - desc = Column(String) - admin_notes = Column(String) - revert_change = Column(JSON, default={}, server_default='{}') +class ReceiptItem(MagModel, table=True): + receipt_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='model_receipt.id', ondelete='CASCADE') + receipt: 'ModelReceipt' = Relationship(back_populates="receipt_items", sa_relationship_kwargs={'lazy': 'joined'}) + + txn_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='receipt_transaction.id', nullable=True) + receipt_txn: 'ReceiptTransaction' = Relationship(back_populates="receipt_items", sa_relationship_kwargs={'lazy': 'joined'}) + + purchaser_id: str | None = Field(sa_type=Uuid(as_uuid=False), index=True, nullable=True) + fk_id: str | None = Field(sa_type=Uuid(as_uuid=False), index=True, nullable=True) + fk_model: str = '' + department: int = Field(sa_column=Column(Choice(c.RECEIPT_ITEM_DEPT_OPTS)), default=c.OTHER_RECEIPT_ITEM) + category: int = Field(sa_column=Column(Choice(c.RECEIPT_CATEGORY_OPTS)), default=c.OTHER) + amount: int = 0 + comped: bool = False + reverted: bool = False + count: int = 1 + added: datetime = Field(sa_type=DateTime(timezone=True), default_factory=lambda: datetime.now(UTC)) + closed: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True) + who: str = '' + desc: str = '' + admin_notes: str = '' + revert_change: dict[str, Any] = Field(sa_type=JSON, default_factory=dict) @presave_adjustment def process_item_close(self): @@ -618,18 +630,21 @@ def cannot_delete_reason(self): If necessary, please delete or cancel the payment first." -class ReceiptInfo(MagModel): - fk_email_model = Column(String) - fk_email_id = Column(String) - terminal_id = Column(String) - reference_id = Column(String) - charged = Column(DateTime(timezone=True)) - voided = Column(DateTime(timezone=True), nullable=True) - card_data = Column(MutableDict.as_mutable(JSONB), default={}) - emv_data = Column(MutableDict.as_mutable(JSONB), default={}) - txn_info = Column(MutableDict.as_mutable(JSONB), default={}) - signature = Column(String) - receipt_html = Column(String) +class ReceiptInfo(MagModel, table=True): + fk_email_model: str = '' + fk_email_id: str = '' + terminal_id: str = '' + reference_id: str = '' + charged: datetime = Field(sa_type=DateTime(timezone=True)) + voided: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True) + card_data: dict[str, Any] = Field(sa_type=MutableDict.as_mutable(JSONB), default_factory=dict) + emv_data: dict[str, Any] = Field(sa_type=MutableDict.as_mutable(JSONB), default_factory=dict) + txn_info: dict[str, Any] = Field(sa_type=MutableDict.as_mutable(JSONB), default_factory=dict) + signature: str = '' + receipt_html: str = '' + + receipt_txns: list['ReceiptTransaction'] = Relationship( + back_populates="receipt_info", sa_relationship_kwargs={'lazy': 'selectin'}) @property def response_code_str(self): @@ -741,11 +756,11 @@ def cavv_str(self): return "CAVV not validated." -class TerminalSettlement(MagModel): - batch_timestamp = Column(String) - batch_who = Column(String) - requested = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC)) - workstation_num = Column(Integer, default=0) - terminal_id = Column(String) - response = Column(MutableDict.as_mutable(JSONB), default={}) - error = Column(String) +class TerminalSettlement(MagModel, table=True): + batch_timestamp: str = '' + batch_who: str = '' + requested: datetime = Field(sa_type=DateTime(timezone=True), default_factory=lambda: datetime.now(UTC)) + workstation_num: int = 0 + terminal_id: str = '' + response: dict[str, Any] = Field(sa_type=MutableDict.as_mutable(JSONB), default_factory=dict) + error: str = '' diff --git a/uber/models/department.py b/uber/models/department.py index 9e2d463da..7255ef0ba 100644 --- a/uber/models/department.py +++ b/uber/models/department.py @@ -3,10 +3,10 @@ import six from sqlalchemy import and_, exists, func, or_, select from sqlalchemy.ext.hybrid import hybrid_property -from sqlalchemy.orm import backref from sqlalchemy.schema import ForeignKey, Table, UniqueConstraint, Index from sqlalchemy.sql import text -from sqlalchemy.types import Boolean, Float, Integer, Time, Uuid, String, DateTime +from sqlalchemy.types import Uuid, DateTime +from typing import ClassVar from uber.config import c from uber.custom_tags import readable_join @@ -14,7 +14,8 @@ from uber.utils import groupify from uber.models import MagModel from uber.models.attendee import Attendee -from uber.models.types import default_relationship as relationship, Choice, DefaultColumn as Column, UniqueList, MultiChoice +from uber.models.types import (default_relationship as relationship, Choice, DefaultColumn as Column, MultiChoice, + DefaultField as Field, DefaultRelationship as Relationship) __all__ = [ @@ -61,44 +62,59 @@ ) -class DeptChecklistItem(MagModel): - department_id = Column(Uuid(as_uuid=False), ForeignKey('department.id')) - attendee_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id')) - slug = Column(String) - comments = Column(String, default='') +class DeptChecklistItem(MagModel, table=True): + department_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='department.id', ondelete='CASCADE') + department: 'Department' = Relationship(back_populates="dept_checklist_items", sa_relationship_kwargs={'lazy': 'joined'}) - __table_args__ = ( + attendee_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', ondelete='CASCADE') + attendee: 'Attendee' = Relationship(back_populates="dept_checklist_items", sa_relationship_kwargs={'lazy': 'joined'}) + + slug: str = '' + comments: str = '' + + __table_args__: ClassVar = ( UniqueConstraint('department_id', 'attendee_id', 'slug'), ) -class BulkPrintingRequest(MagModel): - department_id = Column(Uuid(as_uuid=False), ForeignKey('department.id')) - link = Column(String) - copies = Column(Integer) - print_orientation = Column(Choice(c.PRINT_ORIENTATION_OPTS), default=c.PORTRAIT) - cut_orientation = Column(Choice(c.CUT_ORIENTATION_OPTS), default=c.NONE) - color = Column(Choice(c.PRINT_REQUEST_COLOR_OPTS), default=c.BW) - paper_type = Column(Choice(c.PRINT_REQUEST_PAPER_TYPE_OPTS), default=c.STANDARD) - paper_type_text = Column(String) - size = Column(Choice(c.PRINT_REQUEST_SIZE_OPTS), default=c.STANDARD) - size_text = Column(String) - double_sided = Column(Boolean, default=False) - stapled = Column(Boolean, default=False) - notes = Column(String) - required = Column(Boolean, default=False) - link_is_shared = Column(Boolean, default=False) - - -class DeptMembership(MagModel): - is_dept_head = Column(Boolean, default=False) - is_poc = Column(Boolean, default=False) - is_checklist_admin = Column(Boolean, default=False) - attendee_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id')) - department_id = Column(Uuid(as_uuid=False), ForeignKey('department.id')) - - __mapper_args__ = {'confirm_deleted_rows': False} - __table_args__ = ( +class BulkPrintingRequest(MagModel, table=True): + department_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='department.id', ondelete='CASCADE') + + link: str = '' + copies: int = 0 + print_orientation: int = Field(sa_column=Column(Choice(c.PRINT_ORIENTATION_OPTS)), default=c.PORTRAIT) + cut_orientation: int = Field(sa_column=Column(Choice(c.CUT_ORIENTATION_OPTS)), default=c.NONE) + color: int = Field(sa_column=Column(Choice(c.PRINT_REQUEST_COLOR_OPTS)), default=c.BW) + paper_type: int = Field(sa_column=Column(Choice(c.PRINT_REQUEST_PAPER_TYPE_OPTS)), default=c.STANDARD) + paper_type_text: str = '' + size: int = Field(sa_column=Column(Choice(c.PRINT_REQUEST_SIZE_OPTS)), default=c.STANDARD) + size_text: str = '' + double_sided: bool = False + stapled: bool = False + notes: str = '' + important: bool = False + link_is_shared: bool = False + + +class DeptMembership(MagModel, table=True): + attendee_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', ondelete='CASCADE') + attendee: 'Attendee' = Relationship(back_populates="dept_memberships", sa_relationship_kwargs={'lazy': 'joined'}) + + department_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='department.id', ondelete='CASCADE') + department: 'Department' = Relationship( + back_populates="memberships", sa_relationship_kwargs={'lazy': 'joined', 'overlaps': 'assigned_depts,members'}) + + is_dept_head: bool = False + is_poc: bool = False + is_checklist_admin: bool = False + + dept_roles: list['DeptRole'] = Relationship( + back_populates="dept_memberships", + sa_relationship_kwargs={ + 'lazy': 'selectin', 'secondary': 'dept_membership_dept_role'}) + + __mapper_args__: ClassVar = {'confirm_deleted_rows': False} + __table_args__: ClassVar = ( UniqueConstraint('attendee_id', 'department_id'), Index('ix_dept_membership_attendee_id', 'attendee_id'), Index('ix_dept_membership_department_id', 'department_id'), @@ -134,34 +150,49 @@ def dept_roles_names(self): return readable_join([role.name for role in self.dept_roles]) -class DeptMembershipRequest(MagModel): - attendee_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id')) +class DeptMembershipRequest(MagModel, table=True): + attendee_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', ondelete='CASCADE') + attendee: 'Attendee' = Relationship(back_populates="dept_membership_requests", sa_relationship_kwargs={'lazy': 'joined'}) # A NULL value for the department_id indicates the attendee is willing # to volunteer for any department (they checked "Anything" for # "Where do you want to help?"). - department_id = Column(Uuid(as_uuid=False), ForeignKey('department.id'), nullable=True) + department_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='department.id', ondelete='CASCADE', nullable=True) + department: 'Department' = Relationship(back_populates="membership_requests", sa_relationship_kwargs={'lazy': 'joined'}) - __mapper_args__ = {'confirm_deleted_rows': False} - __table_args__ = ( + __mapper_args__: ClassVar = {'confirm_deleted_rows': False} + __table_args__: ClassVar = ( UniqueConstraint('attendee_id', 'department_id'), Index('ix_dept_membership_request_attendee_id', 'attendee_id'), Index('ix_dept_membership_request_department_id', 'department_id'), ) -class DeptRole(MagModel): - name = Column(String) - description = Column(String) - department_id = Column(Uuid(as_uuid=False), ForeignKey('department.id')) - - dept_memberships = relationship( - 'DeptMembership', - backref='dept_roles', - cascade='save-update,merge,refresh-expire,expunge', - secondary='dept_membership_dept_role') - - __table_args__ = ( +class DeptRole(MagModel, table=True): + department_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='department.id', ondelete='CASCADE') + department: 'Department' = Relationship(back_populates="dept_roles", sa_relationship_kwargs={'lazy': 'joined'}) + + name: str = '' + description: str = '' + + jobs: list['Job'] = Relationship( + back_populates="required_roles", + sa_relationship_kwargs={'secondary': 'job_required_role'}) + job_templates: list['JobTemplate'] = Relationship( + back_populates="required_roles", + sa_relationship_kwargs={'secondary': 'job_template_required_role'}) + dept_memberships: list['DeptMembership'] = Relationship( + back_populates="dept_roles", + sa_relationship_kwargs={'secondary': 'dept_membership_dept_role'}) + attendees: list['Attendee'] = Relationship( + back_populates="dept_roles", + sa_relationship_kwargs={ + 'secondaryjoin': 'and_(dept_membership_dept_role.c.dept_role_id == DeptRole.id, ' + 'dept_membership_dept_role.c.dept_membership_id == DeptMembership.id)', + 'secondary': 'join(DeptMembership, dept_membership_dept_role)', + 'viewonly': True}) + + __table_args__: ClassVar = ( UniqueConstraint('name', 'department_id'), Index('ix_dept_role_department_id', 'department_id'), ) @@ -200,97 +231,93 @@ def dept_memberships_ids(self, value): self._set_relation_ids('dept_memberships', DeptMembership, value) -class Department(MagModel): - name = Column(String, unique=True) - description = Column(String) - solicits_volunteers = Column(Boolean, default=True) - parent_id = Column(Uuid(as_uuid=False), ForeignKey('department.id'), nullable=True) - max_consecutive_minutes = Column(Integer, default=0) - from_email = Column(String) - manages_panels = Column(Boolean, default=False) - handles_cash = Column(Boolean, default=False) - panels_desc = Column(String) - - jobs = relationship('Job', backref='department') - job_templates = relationship('JobTemplate', backref='department') - locations = relationship('EventLocation', backref='department') - events = relationship('Event', backref='department') - - dept_checklist_items = relationship('DeptChecklistItem', backref='department') - dept_roles = relationship('DeptRole', backref='department') - dept_heads = relationship( +class Department(MagModel, table=True): + parent_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='department.id', ondelete='CASCADE', nullable=True) + parent: 'Department' = Relationship(back_populates="sub_depts", sa_relationship_kwargs={'remote_side': 'Department.id'}) + sub_depts: list['Department'] = Relationship(back_populates="parent", sa_relationship_kwargs={'order_by': 'Department.name'}) + + name: str = Field(unique=True, default='') + description: str = '' + solicits_volunteers: bool = True + max_consecutive_minutes: int = 0 + from_email: str = '' + manages_panels: bool = False + handles_cash: bool = False + panels_desc: str = '' + + locations: list['EventLocation'] = Relationship( + back_populates="department") + events: list['Event'] = Relationship( + back_populates="department") + attractions: list['Attraction'] = Relationship( + back_populates="department") + + jobs: list['Job'] = Relationship(back_populates="department", + sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + job_templates: list['JobTemplate'] = Relationship(back_populates="department", + sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + attendees_working_shifts: list['Attendee'] = Relationship( + back_populates="depts_where_working", + sa_relationship_kwargs={'secondary': 'join(Shift, Job)', 'viewonly': True}) + + dept_checklist_items: list['DeptChecklistItem'] = Relationship( + back_populates="department", sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + dept_roles: list['DeptRole'] = Relationship(back_populates="department", + sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + dept_heads: list['Attendee'] = Relationship(sa_relationship=relationship( 'Attendee', - backref=backref('headed_depts', order_by='Department.name'), primaryjoin='and_(' 'Department.id == DeptMembership.department_id, ' 'DeptMembership.is_dept_head == True)', secondary='dept_membership', order_by='Attendee.full_name', - viewonly=True) - checklist_admins = relationship( - 'Attendee', - backref=backref('checklist_admin_depts', order_by='Department.name'), - primaryjoin='and_(' - 'Department.id == DeptMembership.department_id, ' - 'DeptMembership.is_checklist_admin == True)', - secondary='dept_membership', - order_by='Attendee.full_name', - viewonly=True) - members_with_inherent_role = relationship( - 'Attendee', - backref=backref('depts_with_inherent_role', order_by='Department.name'), - primaryjoin='and_(' - 'Department.id == DeptMembership.department_id, ' - 'DeptMembership.has_inherent_role)', - secondary='dept_membership', - order_by='Attendee.full_name', - viewonly=True) - members_who_can_admin_checklist = relationship( - 'Attendee', - backref=backref('can_admin_checklist_depts', order_by='Department.name'), - primaryjoin='and_(' - 'Department.id == DeptMembership.department_id, ' - 'or_(' - 'DeptMembership.is_checklist_admin == True, ' - 'DeptMembership.is_dept_head == True))', - secondary='dept_membership', - order_by='Attendee.full_name', - viewonly=True) - pocs = relationship( - 'Attendee', - backref=backref('poc_depts', order_by='Department.name'), - primaryjoin='and_(' - 'Department.id == DeptMembership.department_id, ' - 'DeptMembership.is_poc == True)', - secondary='dept_membership', - order_by='Attendee.full_name', - viewonly=True) - members = relationship( - 'Attendee', - backref=backref('assigned_depts', order_by='Department.name', overlaps="dept_memberships,attendee"), - cascade='save-update,merge,refresh-expire,expunge', - order_by='Attendee.full_name', - overlaps="attendee,dept_memberships", - secondary='dept_membership') - memberships = relationship('DeptMembership', backref=backref('department', overlaps="assigned_depts,members"), overlaps="assigned_depts,members") - membership_requests = relationship('DeptMembershipRequest', backref='department') - explicitly_requesting_attendees = relationship( - 'Attendee', - backref=backref('explicitly_requested_depts', order_by='Department.name', overlaps="attendee,dept_membership_requests,department,membership_requests"), - cascade='save-update,merge,refresh-expire,expunge', - secondary='dept_membership_request', - order_by='Attendee.full_name', - overlaps="membership_requests,department,attendee,dept_membership_requests") - requesting_attendees = relationship( - 'Attendee', - backref=backref('requested_depts', order_by='Department.name'), - primaryjoin='or_(' - 'DeptMembershipRequest.department_id == Department.id, ' - 'DeptMembershipRequest.department_id == None)', - secondary='dept_membership_request', - order_by='Attendee.full_name', - viewonly=True) - unassigned_requesting_attendees = relationship( + viewonly=True)) + checklist_admins: list['Attendee'] = Relationship( + back_populates="checklist_admin_depts", + sa_relationship_kwargs={ + 'primaryjoin': 'and_(Department.id == DeptMembership.department_id, ' + 'DeptMembership.is_checklist_admin == True)', + 'secondary': 'dept_membership', 'viewonly': True, 'order_by': 'Attendee.full_name'}) + members_with_inherent_role: list['Attendee'] = Relationship( + sa_relationship_kwargs={ + 'primaryjoin': 'and_(Department.id == DeptMembership.department_id, ' + 'DeptMembership.has_inherent_role)', + 'secondary': 'dept_membership', 'viewonly': True, 'order_by': 'Attendee.full_name'}) + members_who_can_admin_checklist: list['Attendee'] = Relationship( + back_populates="can_admin_checklist_depts", + sa_relationship_kwargs={ + 'primaryjoin': 'and_(Department.id == DeptMembership.department_id, ' + 'or_(DeptMembership.is_checklist_admin == True, ' + 'DeptMembership.is_dept_head == True))', + 'secondary': 'dept_membership', 'viewonly': True, 'order_by': 'Attendee.full_name'}) + pocs: list['Attendee'] = Relationship( + back_populates="poc_depts", + sa_relationship_kwargs={ + 'primaryjoin': 'and_(Department.id == DeptMembership.department_id, ' + 'DeptMembership.is_poc == True)', + 'secondary': 'dept_membership', 'viewonly': True, 'order_by': 'Attendee.full_name'}) + members: list['Attendee'] = Relationship( + back_populates='assigned_depts', + sa_relationship_kwargs={'secondary': 'dept_membership', + 'order_by': 'Attendee.full_name', 'overlaps': 'attendee,dept_memberships'}) + memberships: list['DeptMembership'] = Relationship( + back_populates="department", + sa_relationship_kwargs={'overlaps': 'assigned_depts,members', 'cascade': 'all,delete-orphan', 'passive_deletes': True}) + membership_requests: list['DeptMembershipRequest'] = Relationship( + back_populates="department", + sa_relationship_kwargs={'cascade': 'all', 'cascade': 'all,delete-orphan', 'passive_deletes': True}) + explicitly_requesting_attendees: list['Attendee'] = Relationship( + back_populates="explicitly_requested_depts", + sa_relationship_kwargs={ + 'secondary': 'dept_membership_request', + 'order_by': 'Attendee.full_name', 'overlaps': 'membership_requests,department,attendee,dept_membership_requests'}) + requesting_attendees: list['Attendee'] = Relationship( + back_populates="requested_depts", + sa_relationship_kwargs={ + 'primaryjoin': 'or_(DeptMembershipRequest.department_id == Department.id, ' + 'DeptMembershipRequest.department_id == None)', + 'secondary': 'dept_membership_request', 'order_by': 'Attendee.full_name', 'viewonly': True}) + unassigned_requesting_attendees: list['Attendee'] = Relationship(sa_relationship=relationship( 'Attendee', primaryjoin='and_(or_(' 'DeptMembershipRequest.department_id == Department.id, ' @@ -300,8 +327,8 @@ class Department(MagModel): 'DeptMembership.attendee_id == DeptMembershipRequest.attendee_id))))', secondary='dept_membership_request', order_by='Attendee.full_name', - viewonly=True) - unassigned_explicitly_requesting_attendees = relationship( + viewonly=True)) + unassigned_explicitly_requesting_attendees: list['Attendee'] = Relationship(sa_relationship=relationship( 'Attendee', primaryjoin='and_(' 'DeptMembershipRequest.department_id == Department.id, ' @@ -310,13 +337,7 @@ class Department(MagModel): 'DeptMembership.attendee_id == DeptMembershipRequest.attendee_id))))', secondary='dept_membership_request', order_by='Attendee.full_name', - viewonly=True) - parent = relationship( - 'Department', - backref=backref('sub_depts', order_by='Department.name', cascade='all,delete-orphan'), - cascade='save-update,merge,refresh-expire,expunge', - remote_side='Department.id', - single_parent=True) + viewonly=True)) @hybrid_property def member_count(self): @@ -375,7 +396,7 @@ def normalized_name(cls): @classmethod def normalize_name(cls, name): return name.lower().replace('_', '').replace(' ', '') - + @property def dept_roles_choices(self): return [(role.id, role.name) for role in self.dept_roles] @@ -401,35 +422,42 @@ def job_templates_by_name(self): return groupify(self.job_templates, 'template_name') -class Job(MagModel): - _ONLY_MEMBERS = 0 - _ALL_VOLUNTEERS = 2 - _VISIBILITY_OPTS = [ +class Job(MagModel, table=True): + _ONLY_MEMBERS: ClassVar = 0 + _ALL_VOLUNTEERS: ClassVar = 2 + _VISIBILITY_OPTS: ClassVar = [ (_ONLY_MEMBERS, 'Members of this department'), (_ALL_VOLUNTEERS, 'All volunteers')] - job_template_id = Column(Uuid(as_uuid=False), ForeignKey('job_template.id'), nullable=True) - department_id = Column(Uuid(as_uuid=False), ForeignKey('department.id')) - - name = Column(String) - description = Column(String) - start_time = Column(DateTime(timezone=True)) - duration = Column(Integer) - weight = Column(Float, default=1) - slots = Column(Integer) - extra15 = Column(Boolean, default=False) - visibility = Column(Choice(_VISIBILITY_OPTS), default=_ONLY_MEMBERS) - all_roles_required = Column(Boolean, default=True) - - required_roles = relationship( - 'DeptRole', backref='jobs', cascade='save-update,merge,refresh-expire,expunge', secondary='job_required_role') - shifts = relationship('Shift', backref='job') - - __table_args__ = ( - Index('ix_job_department_id', department_id), + job_template_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='job_template.id', nullable=True) + template: 'JobTemplate' = Relationship(back_populates="jobs", sa_relationship_kwargs={'lazy': 'select'}) + + department_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='department.id', ondelete='CASCADE') + department: 'Department' = Relationship(back_populates="jobs", sa_relationship_kwargs={'lazy': 'joined'}) + + name: str = '' + description: str = '' + start_time: datetime = Field(sa_type=DateTime(timezone=True)) + duration: int = 0 + weight: float = 1 + slots: int = 1 + extra15: bool = False + visibility: int = Field(sa_column=Column(Choice(_VISIBILITY_OPTS)), default=_ONLY_MEMBERS) + all_roles_required: bool = True + + attendees_working_shifts: list['Attendee'] = Relationship( + back_populates="jobs", sa_relationship_kwargs={'secondary': 'shift', 'viewonly': True}) + required_roles: list['DeptRole'] = Relationship( + back_populates="jobs", + sa_relationship_kwargs={'lazy': 'selectin', 'secondary': 'job_required_role'}) + shifts: list['Shift'] = Relationship(back_populates="job", + sa_relationship_kwargs={'lazy': 'selectin', 'cascade': 'all,delete-orphan', 'passive_deletes': True}) + + __table_args__: ClassVar = ( + Index('ix_job_department_id', 'department_id'), ) - _repr_attr_names = ['name'] + _repr_attr_names: ClassVar = ['name'] @presave_adjustment def zero_slots(self): @@ -472,7 +500,7 @@ def restricted(self): @restricted.expression def restricted(cls): - return exists([job_required_role.c.dept_role_id]) \ + return exists(job_required_role.c.dept_role_id) \ .where(job_required_role.c.job_id == cls.id).label('restricted') @property @@ -644,10 +672,10 @@ def _potential_volunteers(self, staffing_only=False, order_by=Attendee.full_name query = query.filter(Attendee.staffing == True) # noqa: E712 if self.required_roles: - query = query.join(Attendee.dept_roles, aliased=True).filter( + query = query.join(Attendee.dept_roles).filter( and_(*[DeptRole.id == r.id for r in self.required_roles])) else: - query = query.join(Attendee.dept_memberships, aliased=True).filter( + query = query.join(Attendee.dept_memberships).filter( DeptMembership.department_id == self.department_id) return query.order_by(order_by).all() @@ -680,28 +708,31 @@ def available_volunteers(self): and self.working_limit_ok(s)] -class JobTemplate(MagModel): - department_id = Column(Uuid(as_uuid=False), ForeignKey('department.id')) - - template_name = Column(String) - type = Column(Choice(c.JOB_TEMPLATE_TYPE_OPTS), default=c.FILL_GAPS) - name = Column(String) - description = Column(String) - duration = Column(Integer) - weight = Column(Float, default=1) - extra15 = Column(Boolean, default=False) - visibility = Column(Choice(Job._VISIBILITY_OPTS), default=Job._ONLY_MEMBERS) - all_roles_required = Column(Boolean, default=True) - - min_slots = Column(Integer) # Future improvement: a bulk-edit for slots in jobs by time of day - days = Column(MultiChoice(c.JOB_DAY_OPTS)) - open_time = Column(Time, nullable=True) - close_time = Column(Time, nullable=True) - interval = Column(Integer, nullable=True) - - required_roles = relationship( - 'DeptRole', backref='job_templates', cascade='save-update,merge,refresh-expire,expunge', secondary='job_template_required_role') - jobs = relationship('Job', backref='template', cascade='save-update,merge,refresh-expire,expunge') +class JobTemplate(MagModel, table=True): + department_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='department.id', ondelete='CASCADE') + department: 'Department' = Relationship(back_populates="job_templates", sa_relationship_kwargs={'lazy': 'joined'}) + + template_name: str = '' + type: int = Field(sa_column=Column(Choice(c.JOB_TEMPLATE_TYPE_OPTS)), default=c.FILL_GAPS) + name: str = '' + description: str = '' + duration: int = 0 + weight: float = 1 + extra15: bool = False + visibility: int = Field(sa_column=Column(Choice(Job._VISIBILITY_OPTS)), default=Job._ONLY_MEMBERS) + all_roles_required: bool = True + + min_slots: int = 1 # Future improvement: a bulk-edit for slots in jobs by time of day + days: str = Field(sa_type=MultiChoice(c.JOB_DAY_OPTS), default='') + open_time: time | None = Field(nullable=True) + close_time: time | None = Field(nullable=True) + interval: int = Field(nullable=True) + + jobs: list['Job'] = Relationship(back_populates="template") + required_roles: list['DeptRole'] = Relationship( + back_populates="job_templates", + sa_relationship_kwargs={ + 'lazy': 'selectin', 'secondary': 'job_template_required_role'}) @presave_adjustment def zero_slots(self): @@ -911,16 +942,20 @@ def real_cutoff_time(self, day_int): return cutoff_time -class Shift(MagModel): - job_id = Column(Uuid(as_uuid=False), ForeignKey('job.id', ondelete='cascade')) - attendee_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id', ondelete='cascade')) - worked = Column(Choice(c.WORKED_STATUS_OPTS), default=c.SHIFT_UNMARKED) - rating = Column(Choice(c.RATING_OPTS), default=c.UNRATED) - comment = Column(String) +class Shift(MagModel, table=True): + job_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='job.id', ondelete='CASCADE') + job: 'Job' = Relationship(back_populates="shifts", sa_relationship_kwargs={'lazy': 'joined'}) + + attendee_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', ondelete='CASCADE') + attendee: 'Attendee' = Relationship(back_populates="shifts", sa_relationship_kwargs={'lazy': 'joined'}) + + worked: int = Field(sa_column=Column(Choice(c.WORKED_STATUS_OPTS)), default=c.SHIFT_UNMARKED) + rating: int = Field(sa_column=Column(Choice(c.RATING_OPTS)), default=c.UNRATED) + comment: str = '' - __table_args__ = ( - Index('ix_shift_job_id', job_id), - Index('ix_shift_attendee_id', attendee_id), + __table_args__: ClassVar = ( + Index('ix_shift_job_id', 'job_id'), + Index('ix_shift_attendee_id', 'attendee_id'), ) @property diff --git a/uber/models/email.py b/uber/models/email.py index cbbeff7be..960097c33 100644 --- a/uber/models/email.py +++ b/uber/models/email.py @@ -10,16 +10,15 @@ from sqlalchemy.dialects.postgresql.json import JSONB from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.ext.mutable import MutableDict -from sqlalchemy.orm import relationship -from sqlalchemy.schema import ForeignKey -from sqlalchemy.types import Boolean, Integer, String, Uuid, DateTime +from sqlalchemy.types import Uuid, DateTime +from typing import Any, ClassVar from uber import utils from uber.config import c from uber.decorators import presave_adjustment, renderable_data, cached_property, classproperty from uber.jinja import JinjaEnv from uber.models import MagModel -from uber.models.types import DefaultColumn as Column +from uber.models.types import DefaultField as Field, DefaultRelationship as Relationship from uber.utils import normalize_newlines, request_cached_context, groupify log = logging.getLogger(__name__) @@ -29,17 +28,17 @@ class BaseEmailMixin(object): - model = Column(String) + model: str = '' - subject = Column(String) - body = Column(String) + subject: str = '' + body: str = '' - sender = Column(String) - cc = Column(String) - bcc = Column(String) - replyto = Column(String) + sender: str = '' + cc: str = '' + bcc: str = '' + replyto: str = '' - _repr_attr_names = ['subject'] + _repr_attr_names: ClassVar = ['subject'] @property def body_with_body_tag_stripped(self): @@ -66,27 +65,29 @@ def model_class(self): return None -class AutomatedEmail(MagModel, BaseEmailMixin): - _fixtures = OrderedDict() - email_overrides = [] # Used in plugins, list of (ident, key, val) tuples +class AutomatedEmail(MagModel, BaseEmailMixin, table=True): + _fixtures: ClassVar = OrderedDict() + email_overrides: ClassVar = [] # Used in plugins, list of (ident, key, val) tuples - format = Column(String, default='text') - ident = Column(String, unique=True) + format: str = 'text' + ident: str = Field(default='', unique=True) - approved = Column(Boolean, default=False) - needs_approval = Column(Boolean, default=True) - unapproved_count = Column(Integer, default=0) - currently_sending = Column(Boolean, default=False) - last_send_time = Column(DateTime(timezone=True), nullable=True, default=None) + approved: bool = False + needs_approval: bool = True + unapproved_count: int = 0 + currently_sending: bool = False + last_send_time: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True, default=None) - allow_at_the_con = Column(Boolean, default=False) - allow_post_con = Column(Boolean, default=False) + allow_at_the_con: bool = False + allow_post_con: bool = False - active_after = Column(DateTime(timezone=True), nullable=True, default=None) - active_before = Column(DateTime(timezone=True), nullable=True, default=None) - revert_changes = Column(MutableDict.as_mutable(JSONB), default={}) + active_after: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True, default=None) + active_before: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True, default=None) + revert_changes: dict[str, Any] = Field(sa_type=MutableDict.as_mutable(JSONB), default_factory=dict) - emails = relationship('Email', backref='automated_email', order_by='Email.id') + emails: list['Email'] = Relationship( + back_populates="automated_email", + sa_relationship_kwargs={'order_by': 'Email.id'}) @presave_adjustment def date_adjustments(self): @@ -346,18 +347,18 @@ def would_send_if_approved(self, model_instance): return model_instance and getattr(model_instance, 'email_to_address', False) and self.filter(model_instance) -class Email(MagModel, BaseEmailMixin): - automated_email_id = Column( - Uuid(as_uuid=False), ForeignKey('automated_email.id', ondelete='set null'), nullable=True, default=None, index=True) +class Email(MagModel, BaseEmailMixin, table=True): + automated_email_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='automated_email.id', nullable=True, index=True) + automated_email: 'AutomatedEmail' = Relationship(back_populates="emails", sa_relationship_kwargs={'lazy': 'joined'}) - fk_id = Column(Uuid(as_uuid=False), nullable=True) - ident = Column(String) - to = Column(String) - when = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC)) + fk_id: str | None = Field(sa_type=Uuid(as_uuid=False), nullable=True) + ident: str = '' + to: str = '' + when: str = Field(sa_type=DateTime(timezone=True), default_factory=lambda: datetime.now(UTC)) @cached_property def fk(self): - return self.session.query(self.model_class).filter_by(id=self.fk_id).first() \ + return self.session.query(self.model_class).filter(self.model_class.id == self.fk_id).first() \ if self.session and self.fk_id else None @property diff --git a/uber/models/group.py b/uber/models/group.py index 95f9d1b5d..c5416138f 100644 --- a/uber/models/group.py +++ b/uber/models/group.py @@ -2,92 +2,86 @@ from datetime import datetime from uuid import uuid4 +from decimal import Decimal from pytz import UTC from sqlalchemy import and_, exists, or_, func, select, not_ from sqlalchemy.ext.hybrid import hybrid_property -from sqlalchemy.orm import backref -from sqlalchemy.schema import ForeignKey -from sqlalchemy.types import Boolean, Integer, Numeric, String, DateTime, Uuid +from sqlalchemy.types import DateTime, Uuid +from typing import ClassVar from uber.config import c from uber.custom_tags import format_currency from uber.decorators import presave_adjustment from uber.models import MagModel -from uber.models.types import default_relationship as relationship, utcnow, Choice, DefaultColumn as Column, \ - MultiChoice, TakesPaymentMixin +from uber.models.types import (Choice, default_relationship as relationship, DefaultColumn as Column, MultiChoice, TakesPaymentMixin, + DefaultField as Field, DefaultRelationship as Relationship) from uber.utils import add_opt __all__ = ['Group'] -class Group(MagModel, TakesPaymentMixin): - public_id = Column(Uuid(as_uuid=False), default=lambda: str(uuid4())) - shared_with_id = Column(Uuid(as_uuid=False), ForeignKey('group.id', ondelete='SET NULL'), nullable=True) - shared_with = relationship( - 'Group', - foreign_keys='Group.shared_with_id', - backref=backref('table_shares', viewonly=True), - cascade='save-update,merge,refresh-expire,expunge', - remote_side='Group.id', - single_parent=True) - name = Column(String) - tables = Column(Numeric, default=0) - zip_code = Column(String) - address1 = Column(String) - address2 = Column(String) - city = Column(String) - region = Column(String) - country = Column(String) - email_address = Column(String) - phone = Column(String) - website = Column(String) - wares = Column(String) - categories = Column(MultiChoice(c.DEALER_WARES_OPTS)) - categories_text = Column(String) - description = Column(String) - special_needs = Column(String) - - cost = Column(Integer, default=0, admin_only=True) - auto_recalc = Column(Boolean, default=True, admin_only=True) - - can_add = Column(Boolean, default=False, admin_only=True) - is_dealer = Column(Boolean, default=False, admin_only=True) - convert_badges = Column(Boolean, default=False, admin_only=True) - admin_notes = Column(String, admin_only=True) - status = Column(Choice(c.DEALER_STATUS_OPTS), default=c.UNAPPROVED, admin_only=True) - registered = Column(DateTime(timezone=True), server_default=utcnow(), default=lambda: datetime.now(UTC)) - approved = Column(DateTime(timezone=True), nullable=True) - leader_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id', use_alter=True, name='fk_leader', ondelete='SET NULL'), - nullable=True) - creator_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id'), nullable=True) - - creator = relationship( - 'Attendee', - foreign_keys=creator_id, - backref=backref('created_groups', order_by='Group.name'), - cascade='save-update,merge,refresh-expire,expunge', - remote_side='Attendee.id', - single_parent=True) - leader = relationship('Attendee', foreign_keys=leader_id, post_update=True, cascade='all') - studio = relationship('IndieStudio', uselist=False, backref='group', cascade='save-update,merge,refresh-expire,expunge') - guest = relationship('GuestGroup', backref='group', uselist=False) - active_receipt = relationship( +class Group(MagModel, TakesPaymentMixin, table=True): + leader_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', nullable=True) + leader: 'Attendee' = Relationship(sa_relationship=relationship('Attendee', foreign_keys='Group.leader_id', + lazy='select', post_update=True)) + + creator_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', nullable=True) + creator: 'Attendee' = Relationship( + back_populates="created_groups", sa_relationship_kwargs={'foreign_keys': 'Group.creator_id', 'remote_side': 'Attendee.id'}) + + shared_with_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='group.id', nullable=True) + shared_with: 'Group' = Relationship( + back_populates="table_shares", sa_relationship_kwargs={'foreign_keys': 'Group.shared_with_id', 'remote_side': 'Group.id'}) + table_shares: list['Group'] = Relationship(back_populates="shared_with", sa_relationship_kwargs={'viewonly': True}) + + public_id: str | None = Field(sa_type=Uuid(as_uuid=False), default_factory=lambda: str(uuid4())) + name: str = '' + tables: Decimal = 0 + zip_code: str = '' + address1: str = '' + address2: str = '' + city: str = '' + region: str = '' + country:str = '' + email_address: str = '' + phone: str = '' + website: str = '' + wares: str = '' + categories: str = Field(sa_type=MultiChoice(c.DEALER_WARES_OPTS), default='') + categories_text: str = '' + description: str = '' + special_needs: str = '' + + cost: int = 0 + auto_recalc: bool = True + + can_add: bool = False + is_dealer: bool = False + convert_badges: bool = False + admin_notes: str = '' + status: int = Field(sa_column=Column(Choice(c.DEALER_STATUS_OPTS)), default=c.UNAPPROVED) + registered: datetime = Field(sa_type=DateTime(timezone=True), default_factory=lambda: datetime.now(UTC)) + approved: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True) + + attendees: list['Attendee'] = Relationship(back_populates="group", + sa_relationship_kwargs={'foreign_keys': 'Attendee.group_id', 'lazy': 'selectin'}) + studio: 'IndieStudio' = Relationship(back_populates="group") + guest: 'GuestGroup' = Relationship(back_populates="group", sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + + active_receipt: 'ModelReceipt' = Relationship(sa_relationship=relationship( 'ModelReceipt', - cascade='save-update,merge,refresh-expire,expunge', primaryjoin='and_(remote(ModelReceipt.owner_id) == foreign(Group.id),' 'ModelReceipt.owner_model == "Group",' 'ModelReceipt.closed == None)', - uselist=False) - terms_conditions_doc = relationship( + lazy='select')) + terms_conditions_doc: 'SignedDocument' = Relationship(sa_relationship=relationship( 'SignedDocument', - cascade='save-update,merge,refresh-expire,expunge', primaryjoin='and_(SignedDocument.fk_id == foreign(Group.id),' 'SignedDocument.model == "Group")', - uselist=False, - overlaps="active_receipt") + overlaps="active_receipt")) - _repr_attr_names = ['name'] + _repr_attr_names: ClassVar = ['name'] @presave_adjustment def _cost_and_leader(self): diff --git a/uber/models/guests.py b/uber/models/guests.py index ea56abead..8267a2ed8 100644 --- a/uber/models/guests.py +++ b/uber/models/guests.py @@ -7,16 +7,15 @@ from datetime import datetime, timedelta from markupsafe import Markup -from sqlalchemy.orm import backref -from sqlalchemy.schema import ForeignKey -from sqlalchemy.types import Boolean, Integer, String, DateTime, Uuid, JSON +from sqlalchemy.types import Boolean, DateTime, Uuid, JSON +from typing import Any, ClassVar from uber.config import c from uber.custom_tags import yesno from uber.decorators import presave_adjustment, classproperty from uber.models import MagModel -from uber.models.types import (default_relationship as relationship, Choice, DefaultColumn as Column, - MultiChoice, GuidebookImageMixin) +from uber.models.types import (Choice, MultiChoice, GuidebookImageMixin, DefaultColumn as Column, + DefaultField as Field, DefaultRelationship as Relationship) from uber.utils import filename_extension, slugify log = logging.getLogger(__name__) @@ -28,36 +27,53 @@ 'GuestInterview', 'GuestTravelPlans', 'GuestDetailedTravelPlan', 'GuestHospitality', 'GuestTrack'] -class GuestGroup(MagModel): - group_id = Column(Uuid(as_uuid=False), ForeignKey('group.id')) - event_id = Column(Uuid(as_uuid=False), ForeignKey('event.id', ondelete='SET NULL'), nullable=True) - group_type = Column(Choice(c.GROUP_TYPE_OPTS), default=c.BAND) - num_hotel_rooms = Column(Integer, default=1, admin_only=True) - payment = Column(Integer, default=0, admin_only=True) - vehicles = Column(Integer, default=1, admin_only=True) - estimated_loadin_minutes = Column(Integer, default=c.DEFAULT_LOADIN_MINUTES, admin_only=True) - estimated_performance_minutes = Column(Integer, default=c.DEFAULT_PERFORMANCE_MINUTES, admin_only=True) - - wants_mc = Column(Boolean, nullable=True) - needs_rehearsal = Column(Choice(c.GUEST_REHEARSAL_OPTS), nullable=True) - badges_assigned = Column(Boolean, default=False) - info = relationship('GuestInfo', backref=backref('guest', load_on_pending=True), uselist=False) - images = relationship( - 'GuestImage', backref=backref('guest', load_on_pending=True), order_by='GuestImage.id') - bio = relationship('GuestBio', backref=backref('guest', load_on_pending=True), uselist=False) - taxes = relationship('GuestTaxes', backref=backref('guest', load_on_pending=True), uselist=False) - stage_plot = relationship('GuestStagePlot', backref=backref('guest', load_on_pending=True), uselist=False) - panel = relationship('GuestPanel', backref=backref('guest', load_on_pending=True), uselist=False) - merch = relationship('GuestMerch', backref=backref('guest', load_on_pending=True), uselist=False) - tracks = relationship('GuestTrack', backref=backref('guest', load_on_pending=True)) - charity = relationship('GuestCharity', backref=backref('guest', load_on_pending=True), uselist=False) - autograph = relationship('GuestAutograph', backref=backref('guest', load_on_pending=True), uselist=False) - interview = relationship('GuestInterview', backref=backref('guest', load_on_pending=True), uselist=False) - travel_plans = relationship('GuestTravelPlans', backref=backref('guest', load_on_pending=True), uselist=False) - hospitality = relationship('GuestHospitality', backref=backref('guest', load_on_pending=True), uselist=False) - media_request = relationship('GuestMediaRequest', backref=backref('guest', load_on_pending=True), uselist=False) - - email_model_name = 'guest' +class GuestGroup(MagModel, table=True): + group_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='group.id', ondelete='CASCADE', unique=True) + group: 'Group' = Relationship(back_populates="guest", sa_relationship_kwargs={'lazy': 'joined', 'single_parent': True}) + + event_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='event.id', nullable=True) + event: 'Event' = Relationship(back_populates="guest") + + group_type: int = Field(sa_column=Column(Choice(c.GROUP_TYPE_OPTS)), default=c.BAND) + num_hotel_rooms: int = Field(default=1, admin_only=True) + payment: int = Field(default=0, admin_only=True) + vehicles: int = Field(default=1, admin_only=True) + estimated_loadin_minutes: int = Field(default=c.DEFAULT_LOADIN_MINUTES, admin_only=True) + estimated_performance_minutes: int = Field(default=c.DEFAULT_PERFORMANCE_MINUTES, admin_only=True) + wants_mc: bool | None = Field(nullable=True) + needs_rehearsal: int | None = Field(sa_column=Column(Choice(c.GUEST_REHEARSAL_OPTS), nullable=True)) + badges_assigned: bool = False + + info: 'GuestInfo' = Relationship( + back_populates="guest", sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + images: list['GuestImage'] = Relationship( + back_populates="guest", sa_relationship_kwargs={'order_by': 'GuestImage.id', 'cascade': 'all,delete-orphan', 'passive_deletes': True}) + bio: 'GuestBio' = Relationship( + back_populates="guest", sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + taxes: 'GuestTaxes' = Relationship( + back_populates="guest", sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + stage_plot: 'GuestStagePlot' = Relationship( + back_populates="guest", sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + panel: 'GuestPanel' = Relationship( + back_populates="guest", sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + merch: 'GuestMerch' = Relationship( + back_populates="guest", sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + tracks: list['GuestTrack'] = Relationship( + back_populates="guest", sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + charity: 'GuestCharity' = Relationship( + back_populates="guest", sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + autograph: 'GuestAutograph' = Relationship( + back_populates="guest", sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + interview: 'GuestInterview' = Relationship( + back_populates="guest", sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + travel_plans: 'GuestTravelPlans' = Relationship( + back_populates="guest", sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + hospitality: 'GuestHospitality' = Relationship( + back_populates="guest", sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + media_request: 'GuestMediaRequest' = Relationship( + back_populates="guest", sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + + email_model_name: ClassVar = 'guest' def __getattr__(self, name): """ @@ -290,21 +306,24 @@ def guidebook_images(self): return [header_name, thumbnail_name], [header, thumbnail] -class GuestInfo(MagModel): - guest_id = Column(Uuid(as_uuid=False), ForeignKey('guest_group.id'), unique=True) - poc_phone = Column(String) - performer_count = Column(Integer, default=0) - bringing_vehicle = Column(Boolean, default=False) - vehicle_info = Column(String) - arrival_time = Column(String) +class GuestInfo(MagModel, table=True): + guest_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='guest_group.id', ondelete='CASCADE', unique=True) + guest: 'GuestGroup' = Relationship(back_populates="info", sa_relationship_kwargs={'lazy': 'joined', 'single_parent': True}) + + poc_phone: str = '' + performer_count: int = 0 + bringing_vehicle: bool = False + vehicle_info: str = '' + arrival_time: str = '' @property def status(self): return "Yes" if self.poc_phone else "" -class GuestImage(MagModel, GuidebookImageMixin): - guest_id = Column(Uuid(as_uuid=False), ForeignKey('guest_group.id')) +class GuestImage(MagModel, GuidebookImageMixin, table=True): + guest_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='guest_group.id', ondelete='CASCADE') + guest: 'GuestGroup' = Relationship(back_populates="images") @property def url(self): @@ -320,40 +339,46 @@ def download_filename(self): return name + '.' + self.pic_extension -class GuestBio(MagModel): - guest_id = Column(Uuid(as_uuid=False), ForeignKey('guest_group.id'), unique=True) - desc = Column(String) - member_info = Column(String) - website = Column(String) - facebook = Column(String) - twitter = Column(String) - instagram = Column(String) - twitch = Column(String) - bandcamp = Column(String) - discord = Column(String) - spotify = Column(String) - other_social_media = Column(String) - teaser_song_url = Column(String) +class GuestBio(MagModel, table=True): + guest_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='guest_group.id', ondelete='CASCADE', unique=True) + guest: 'GuestGroup' = Relationship(back_populates="bio", sa_relationship_kwargs={'lazy': 'joined', 'single_parent': True}) + + desc: str = '' + member_info: str = '' + website: str = '' + facebook: str = '' + twitter: str = '' + instagram: str = '' + twitch: str = '' + bandcamp: str = '' + discord: str = '' + spotify: str = '' + other_social_media: str = '' + teaser_song_url: str = '' @property def status(self): return 'Yes' if self.desc else '' -class GuestTaxes(MagModel): - guest_id = Column(Uuid(as_uuid=False), ForeignKey('guest_group.id'), unique=True) - w9_sent = Column(Boolean, default=False) +class GuestTaxes(MagModel, table=True): + guest_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='guest_group.id', ondelete='CASCADE', unique=True) + guest: 'GuestGroup' = Relationship(back_populates="taxes", sa_relationship_kwargs={'lazy': 'joined', 'single_parent': True}) + + w9_sent: bool = False @property def status(self): return str(self.w9_sent) -class GuestStagePlot(MagModel): - guest_id = Column(Uuid(as_uuid=False), ForeignKey('guest_group.id'), unique=True) - filename = Column(String) - content_type = Column(String) - notes = Column(String) +class GuestStagePlot(MagModel, table=True): + guest_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='guest_group.id', ondelete='CASCADE', unique=True) + guest: 'GuestGroup' = Relationship(back_populates="stage_plot", sa_relationship_kwargs={'lazy': 'joined', 'single_parent': True}) + + filename: str = '' + content_type: str = '' + notes: str = '' @property def url(self): @@ -385,25 +410,29 @@ def status(self): return self.notes -class GuestPanel(MagModel): - guest_id = Column(Uuid(as_uuid=False), ForeignKey('guest_group.id'), unique=True) - wants_panel = Column(Choice(c.GUEST_PANEL_OPTS), nullable=True) - name = Column(String) - length = Column(String) - desc = Column(String) - tech_needs = Column(MultiChoice(c.TECH_NEED_OPTS)) - other_tech_needs = Column(String) +class GuestPanel(MagModel, table=True): + guest_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='guest_group.id', ondelete='CASCADE', unique=True) + guest: 'GuestGroup' = Relationship(back_populates="panel", sa_relationship_kwargs={'lazy': 'joined', 'single_parent': True}) + + wants_panel: int | None = Field(sa_column=Column(Choice(c.GUEST_PANEL_OPTS), nullable=True)) + name: str = '' + length: str = '' + desc: str = '' + tech_needs: str = Field(sa_type=MultiChoice(c.TECH_NEED_OPTS), default='') + other_tech_needs: str = '' @property def status(self): return self.wants_panel_label -class GuestTrack(MagModel): - guest_id = Column(Uuid(as_uuid=False), ForeignKey('guest_group.id')) - filename = Column(String) - content_type = Column(String) - extension = Column(String) +class GuestTrack(MagModel, table=True): + guest_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='guest_group.id', ondelete='CASCADE', unique=True) + guest: 'GuestGroup' = Relationship(back_populates="tracks", sa_relationship_kwargs={'lazy': 'joined', 'single_parent': True}) + + filename: str = '' + content_type: str = '' + extension: str = '' @property def file(self): @@ -437,45 +466,47 @@ def filepath(self): return os.path.join(c.GUESTS_INVENTORY_DIR, str('track_' + self.id)) -class GuestMerch(MagModel): - _inventory_file_regex = re.compile(r'^(audio|image)(|\-\d+)$') - _inventory_filename_regex = re.compile(r'^(audio|image)(|\-\d+)_filename$') - - guest_id = Column(Uuid(as_uuid=False), ForeignKey('guest_group.id'), unique=True) - selling_merch = Column(Choice(c.GUEST_MERCH_OPTS), nullable=True) - delivery_method = Column(Choice(c.GUEST_MERCH_DELIVERY_OPTS), nullable=True) - payout_method = Column(Choice(c.GUEST_MERCH_PAYOUT_METHOD_OPTS), nullable=True) - paypal_email = Column(String) - check_payable = Column(String) - check_zip_code = Column(String) - check_address1 = Column(String) - check_address2 = Column(String) - check_city = Column(String) - check_region = Column(String) - check_country = Column(String) - - arrival_plans = Column(String) - checkin_time = Column(Choice(c.GUEST_MERCH_CHECKIN_TIMES), nullable=True) - checkout_time = Column(Choice(c.GUEST_MERCH_CHECKOUT_TIMES), nullable=True) - merch_events = Column(String) - inventory = Column(JSON, default={}, server_default='{}') - inventory_updated = Column(DateTime(timezone=True), nullable=True) - extra_info = Column(String) - tax_phone = Column(String) - - poc_is_group_leader = Column(Boolean, default=False) - poc_first_name = Column(String) - poc_last_name = Column(String) - poc_phone = Column(String) - poc_email = Column(String) - poc_zip_code = Column(String) - poc_address1 = Column(String) - poc_address2 = Column(String) - poc_city = Column(String) - poc_region = Column(String) - poc_country = Column(String) - - handlers = Column(JSON, default=[], server_default='[]') +class GuestMerch(MagModel, table=True): + _inventory_file_regex: ClassVar = re.compile(r'^(audio|image)(|\-\d+)$') + _inventory_filename_regex: ClassVar = re.compile(r'^(audio|image)(|\-\d+)_filename$') + + guest_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='guest_group.id', ondelete='CASCADE', unique=True) + guest: 'GuestGroup' = Relationship(back_populates="merch", sa_relationship_kwargs={'lazy': 'joined', 'single_parent': True}) + + selling_merch: int | None = Field(sa_column=Column(Choice(c.GUEST_MERCH_OPTS), nullable=True)) + delivery_method: int | None = Field(sa_column=Column(Choice(c.GUEST_MERCH_DELIVERY_OPTS), nullable=True)) + payout_method: int | None = Field(sa_column=Column(Choice(c.GUEST_MERCH_PAYOUT_METHOD_OPTS), nullable=True)) + paypal_email: str = '' + check_payable: str = '' + check_zip_code: str = '' + check_address1: str = '' + check_address2: str = '' + check_city: str = '' + check_region: str = '' + check_country: str = '' + + arrival_plans: str = '' + checkin_time: int | None = Field(sa_column=Column(Choice(c.GUEST_MERCH_CHECKIN_TIMES), nullable=True)) + checkout_time: int | None = Field(sa_column=Column(Choice(c.GUEST_MERCH_CHECKOUT_TIMES), nullable=True)) + merch_events: str = '' + inventory: dict[Any, Any] = Field(sa_type=JSON, default_factory=dict) + inventory_updated: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True) + extra_info: str = '' + tax_phone: str = '' + + poc_is_group_leader: bool = Field(sa_type=Boolean, default=False) + poc_first_name: str = '' + poc_last_name: str = '' + poc_phone: str = '' + poc_email: str = '' + poc_zip_code: str = '' + poc_address1: str = '' + poc_address2: str = '' + poc_city: str = '' + poc_region: str = '' + poc_country: str = '' + + handlers: dict[str, Any] = Field(sa_type=JSON, default_factory=dict) @property def full_name(self): @@ -735,10 +766,12 @@ def update_inventory(self, inventory, *, persist_files=True): self.inventory_updated = datetime.now() -class GuestCharity(MagModel): - guest_id = Column(Uuid(as_uuid=False), ForeignKey('guest_group.id'), unique=True) - donating = Column(Choice(c.GUEST_CHARITY_OPTS), nullable=True) - desc = Column(String) +class GuestCharity(MagModel, table=True): + guest_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='guest_group.id', ondelete='CASCADE', unique=True) + guest: 'GuestGroup' = Relationship(back_populates="charity", sa_relationship_kwargs={'lazy': 'joined', 'single_parent': True}) + + donating: int | None = Field(sa_column=Column(Choice(c.GUEST_CHARITY_OPTS), nullable=True)) + desc: str = '' @property def status(self): @@ -750,12 +783,14 @@ def no_desc_if_not_donating(self): self.desc = '' -class GuestAutograph(MagModel): - guest_id = Column(Uuid(as_uuid=False), ForeignKey('guest_group.id'), unique=True) - num = Column(Integer, default=0) - length = Column(Integer, default=60) # session length in minutes - rock_island_autographs = Column(Boolean, nullable=True) - rock_island_length = Column(Integer, default=60) # session length in minutes +class GuestAutograph(MagModel, table=True): + guest_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='guest_group.id', ondelete='CASCADE', unique=True) + guest: 'GuestGroup' = Relationship(back_populates="autograph", sa_relationship_kwargs={'lazy': 'joined', 'single_parent': True}) + + num: int = 0 + length: int = 60 # session length in minutes + rock_island_autographs: bool | None = Field(nullable=True) + rock_island_length: int = 60 # session length in minutes @presave_adjustment def no_length_if_zero_autographs(self): @@ -763,11 +798,13 @@ def no_length_if_zero_autographs(self): self.length = 0 -class GuestInterview(MagModel): - guest_id = Column(Uuid(as_uuid=False), ForeignKey('guest_group.id'), unique=True) - will_interview = Column(Boolean, default=False) - email = Column(String) - direct_contact = Column(Boolean, default=False) +class GuestInterview(MagModel, table=True): + guest_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='guest_group.id', ondelete='CASCADE', unique=True) + guest: 'GuestGroup' = Relationship(back_populates="interview", sa_relationship_kwargs={'lazy': 'joined', 'single_parent': True}) + + will_interview: bool = False + email: str = '' + direct_contact: bool = False @presave_adjustment def no_details_if_no_interview(self): @@ -776,45 +813,54 @@ def no_details_if_no_interview(self): self.direct_contact = False -class GuestTravelPlans(MagModel): - guest_id = Column(Uuid(as_uuid=False), ForeignKey('guest_group.id'), unique=True) - modes = Column(MultiChoice(c.GUEST_TRAVEL_OPTS), default=c.OTHER) - modes_text = Column(String) - details = Column(String) - completed = Column(Boolean, default=False) +class GuestTravelPlans(MagModel, table=True): + guest_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='guest_group.id', ondelete='CASCADE', unique=True) + guest: 'GuestGroup' = Relationship(back_populates="travel_plans", sa_relationship_kwargs={'lazy': 'joined', 'single_parent': True}) + + modes: str = Field(sa_type=MultiChoice(c.GUEST_TRAVEL_OPTS), default=c.OTHER) + modes_text: str = '' + details: str = '' + completed: bool = False + + detailed_travel_plans: list['GuestDetailedTravelPlan'] = Relationship( + back_populates="travel_plans", + sa_relationship_kwargs={'lazy': 'selectin', 'cascade': 'all,delete-orphan', 'passive_deletes': True}) @property def num_detailed_travel_plans(self): return len(self.detailed_travel_plans) -class GuestHospitality(MagModel): - guest_id = Column(Uuid(as_uuid=False), ForeignKey('guest_group.id'), unique=True) - completed = Column(Boolean, default=False) - - -class GuestMediaRequest(MagModel): - guest_id = Column(Uuid(as_uuid=False), ForeignKey('guest_group.id'), unique=True) - completed = Column(Boolean, default=False) - - -class GuestDetailedTravelPlan(MagModel): - travel_plans_id = Column(Uuid(as_uuid=False), ForeignKey('guest_travel_plans.id'), nullable=True) - travel_plans = relationship('GuestTravelPlans', foreign_keys=travel_plans_id, single_parent=True, - backref=backref('detailed_travel_plans'), - cascade='save-update,merge,refresh-expire,expunge') - mode = Column(Choice(c.GUEST_TRAVEL_OPTS)) - mode_text = Column(String) - traveller = Column(String) - companions = Column(String) - luggage_needs = Column(String) - contact_email = Column(String) - contact_phone = Column(String) - arrival_time = Column(DateTime(timezone=True)) - arrival_details = Column(String) - departure_time = Column(DateTime(timezone=True)) - departure_details = Column(String) - extra_details = Column(String) +class GuestHospitality(MagModel, table=True): + guest_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='guest_group.id', ondelete='CASCADE', unique=True) + guest: 'GuestGroup' = Relationship(back_populates="hospitality", sa_relationship_kwargs={'lazy': 'joined', 'single_parent': True}) + + completed: bool = False + + +class GuestMediaRequest(MagModel, table=True): + guest_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='guest_group.id', ondelete='CASCADE', unique=True) + guest: 'GuestGroup' = Relationship(back_populates="media_request", sa_relationship_kwargs={'lazy': 'joined', 'single_parent': True}) + + completed: bool = False + + +class GuestDetailedTravelPlan(MagModel, table=True): + travel_plans_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='guest_travel_plans.id', ondelete='CASCADE') + travel_plans: 'GuestTravelPlans' = Relationship(back_populates="detailed_travel_plans", sa_relationship_kwargs={'lazy': 'joined'}) + + mode: int = Field(sa_column=Column(Choice(c.GUEST_TRAVEL_OPTS)), default=0) + mode_text: str = '' + traveller: str = '' + companions: str = '' + luggage_needs: str = '' + contact_email: str = '' + contact_phone: str = '' + arrival_time: datetime = Field(sa_type=DateTime(timezone=True)) + arrival_details: str = '' + departure_time: datetime = Field(sa_type=DateTime(timezone=True)) + departure_details: str = '' + extra_details: str = '' @classproperty def min_arrival_time(self): diff --git a/uber/models/hotel.py b/uber/models/hotel.py index f1fb36772..135154ef6 100644 --- a/uber/models/hotel.py +++ b/uber/models/hotel.py @@ -2,22 +2,21 @@ import random import checkdigit.verhoeff as verhoeff -from datetime import timedelta, datetime +from datetime import timedelta, datetime, date from pytz import UTC from markupsafe import Markup from sqlalchemy import Sequence, case from sqlalchemy.dialects.postgresql.json import JSONB from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.ext.mutable import MutableDict -from sqlalchemy.orm import backref -from sqlalchemy.schema import ForeignKey -from sqlalchemy.types import Boolean, Date, Integer, String, DateTime, Uuid +from sqlalchemy.types import Date, Integer, DateTime, Uuid +from typing import Any, ClassVar from uber.config import c from uber.custom_tags import readable_join, datetime_local_filter from uber.decorators import presave_adjustment from uber.models import MagModel -from uber.models.types import Choice, default_relationship as relationship, utcnow, DefaultColumn as Column, MultiChoice +from uber.models.types import Choice, DefaultColumn as Column, MultiChoice, DefaultField as Field, DefaultRelationship as Relationship from uber.utils import RegistrationCode log = logging.getLogger(__name__) @@ -65,13 +64,15 @@ def setup_teardown(self): for mutate in [str.upper, str.lower]}) -class HotelRequests(MagModel, NightsMixin): - attendee_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id'), unique=True) - nights = Column(MultiChoice(c.NIGHT_OPTS)) - wanted_roommates = Column(String) - unwanted_roommates = Column(String) - special_needs = Column(String) - approved = Column(Boolean, default=False, admin_only=True) +class HotelRequests(MagModel, NightsMixin, table=True): + attendee_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', ondelete='CASCADE', unique=True) + attendee: 'Attendee' = Relationship(back_populates="hotel_requests", sa_relationship_kwargs={'lazy': 'joined'}) + + nights: str = Field(sa_type=MultiChoice(c.NIGHT_OPTS), default='') + wanted_roommates: str = '' + unwanted_roommates: str = '' + special_needs: str = '' + approved: bool = Field(default=False, admin_only=True) def decline(self): nights = [n for n in self.nights.split(',') if int(n) in c.CORE_NIGHTS] @@ -85,13 +86,15 @@ def __repr__(self): return '<{self.attendee.full_name} Hotel Requests>'.format(self=self) -class Room(MagModel, NightsMixin): - notes = Column(String) - message = Column(String) - locked_in = Column(Boolean, default=False) - nights = Column(MultiChoice(c.NIGHT_OPTS)) - created = Column(DateTime(timezone=True), server_default=utcnow(), default=lambda: datetime.now(UTC)) - assignments = relationship('RoomAssignment', backref='room') +class Room(MagModel, NightsMixin, table=True): + notes: str = '' + message: str = '' + locked_in: bool = False + nights: str = Field(sa_type=MultiChoice(c.NIGHT_OPTS), default='') + created: datetime = Field(sa_type=DateTime(timezone=True), default_factory=lambda: datetime.now(UTC)) + + assignments: list['RoomAssignment'] = Relationship(back_populates="room", + sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) @property def email(self): @@ -115,77 +118,77 @@ def check_out_date(self): return c.NIGHT_DATES[self.nights_labels[-1]] + timedelta(days=1) -class RoomAssignment(MagModel): - room_id = Column(Uuid(as_uuid=False), ForeignKey('room.id')) - attendee_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id')) - - -class LotteryApplication(MagModel): - attendee_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id'), unique=True, nullable=True) - attendee = relationship('Attendee', backref=backref('lottery_application', uselist=False), - cascade='save-update,merge,refresh-expire,expunge', - uselist=False) - invite_code = Column(String) # Not used for now but we're keeping it for later - confirmation_num = Column(String) - - response_id_seq = Sequence('lottery_application_response_id_seq') - response_id = Column(Integer, response_id_seq, server_default=response_id_seq.next_value(), unique=True) - status = Column(Choice(c.HOTEL_LOTTERY_STATUS_OPTS), default=c.PARTIAL, admin_only=True) - entry_started = Column(DateTime(timezone=True), nullable=True) - entry_metadata = Column(MutableDict.as_mutable(JSONB), server_default='{}', default={}) - entry_type = Column(Choice(c.HOTEL_LOTTERY_ENTRY_TYPE_OPTS), nullable=True) - current_step = Column(Integer, default=0) - last_submitted = Column(DateTime(timezone=True), nullable=True) - admin_notes = Column(String) - is_staff_entry = Column(Boolean, default=False) - - legal_first_name = Column(String) - legal_last_name = Column(String) - cellphone = Column(String) - earliest_checkin_date = Column(Date, nullable=True) - latest_checkin_date = Column(Date, nullable=True) - earliest_checkout_date = Column(Date, nullable=True) - latest_checkout_date = Column(Date, nullable=True) - selection_priorities = Column(MultiChoice(c.HOTEL_LOTTERY_PRIORITIES_OPTS)) - - hotel_preference = Column(MultiChoice(c.HOTEL_LOTTERY_HOTELS_OPTS)) - room_type_preference = Column(MultiChoice(c.HOTEL_LOTTERY_ROOM_TYPES_OPTS)) - wants_ada = Column(Boolean, default=False) - ada_requests = Column(String) - - room_opt_out = Column(Boolean, default=False) - suite_type_preference = Column(MultiChoice(c.HOTEL_LOTTERY_SUITE_ROOM_TYPES_OPTS)) - - terms_accepted = Column(Boolean, default=False) - data_policy_accepted = Column(Boolean, default=False) - suite_terms_accepted = Column(Boolean, default=False) - guarantee_policy_accepted = Column(Boolean, default=False) - can_edit = Column(Boolean, default=False) - final_status_hidden = Column(Boolean, default=True) - booking_url_hidden = Column(Boolean, default=True) +class RoomAssignment(MagModel, table=True): + room_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='room.id', ondelete='CASCADE') + room: 'Room' = Relationship(back_populates="assignments", sa_relationship_kwargs={'lazy': 'joined'}) + + attendee_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', ondelete='CASCADE') + attendee: 'Attendee' = Relationship(back_populates="room_assignments", sa_relationship_kwargs={'lazy': 'joined'}) + + +class LotteryApplication(MagModel, table=True): + attendee_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', nullable=True, unique=True) + attendee: 'Attendee' = Relationship(back_populates="lottery_application", sa_relationship_kwargs={'lazy': 'joined', 'single_parent': True}) + + invite_code: str = '' # Not used for now but we're keeping it for later + confirmation_num: str = '' + response_id_seq: ClassVar = Sequence('lottery_application_response_id_seq') + response_id: int = Field(sa_column=Column(Integer, response_id_seq, server_default=response_id_seq.next_value(), unique=True)) + status: int = Field(sa_column=Column(Choice(c.HOTEL_LOTTERY_STATUS_OPTS), admin_only=True), default=c.PARTIAL) + entry_started: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True) + entry_metadata: dict[str, Any] = Field(sa_type=MutableDict.as_mutable(JSONB), default_factory=dict) + entry_type: int | None = Field(sa_column=Column(Choice(c.HOTEL_LOTTERY_ENTRY_TYPE_OPTS), nullable=True)) + current_step: int = 0 + last_submitted: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True) + admin_notes: str = '' + is_staff_entry: bool = False + + legal_first_name: str = '' + legal_last_name: str = '' + cellphone: str = '' + earliest_checkin_date: date | None = Field(sa_type=Date, nullable=True) + latest_checkin_date: date | None = Field(sa_type=Date, nullable=True) + earliest_checkout_date: date | None = Field(sa_type=Date, nullable=True) + latest_checkout_date: date | None = Field(sa_type=Date, nullable=True) + selection_priorities: str = Field(sa_type=MultiChoice(c.HOTEL_LOTTERY_PRIORITIES_OPTS), default='') + + hotel_preference: str = Field(sa_type=MultiChoice(c.HOTEL_LOTTERY_HOTELS_OPTS), default='') + room_type_preference: str = Field(sa_type=MultiChoice(c.HOTEL_LOTTERY_ROOM_TYPES_OPTS), default='') + wants_ada: bool = False + ada_requests: str = '' + + room_opt_out: bool = False + suite_type_preference: str = Field(sa_type=MultiChoice(c.HOTEL_LOTTERY_SUITE_ROOM_TYPES_OPTS), default='') + + terms_accepted: bool = False + data_policy_accepted: bool = False + suite_terms_accepted: bool = False + guarantee_policy_accepted: bool = False + can_edit: bool = False + final_status_hidden: bool = True + booking_url_hidden: bool = True # If this is set then the above values are ignored - parent_application_id = Column(Uuid(as_uuid=False), ForeignKey('lottery_application.id'), nullable=True) - parent_application = relationship( - 'LotteryApplication', - foreign_keys='LotteryApplication.parent_application_id', - backref=backref('group_members'), - cascade='save-update,merge,refresh-expire,expunge', - remote_side='LotteryApplication.id', - single_parent=True) - former_parent_id = Column(Uuid(as_uuid=False), nullable=True) - - room_group_name = Column(String) - email_model_name = 'app' - - assigned_hotel = Column(Choice(c.HOTEL_LOTTERY_HOTELS_OPTS), nullable=True) - assigned_room_type = Column(Choice(c.HOTEL_LOTTERY_ROOM_TYPES_OPTS), nullable=True) - assigned_suite_type = Column(Choice(c.HOTEL_LOTTERY_SUITE_ROOM_TYPES_OPTS), nullable=True) - assigned_check_in_date = Column(Date, nullable=True) - assigned_check_out_date = Column(Date, nullable=True) - deposit_cutoff_date = Column(Date, nullable=True) - lottery_name = Column(String) - booking_url = Column(String) + parent_application_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='lottery_application.id', nullable=True) + parent_application: 'LotteryApplication' = Relationship( + back_populates="group_members", + sa_relationship_kwargs={'lazy': 'joined', 'foreign_keys': 'LotteryApplication.parent_application_id', + 'remote_side': 'LotteryApplication.id'}) + group_members: list['LotteryApplication'] = Relationship( + back_populates="parent_application") + former_parent_id: str | None = Field(sa_type=Uuid(as_uuid=False), nullable=True) + + room_group_name: str = '' + email_model_name: ClassVar = 'app' + + assigned_hotel: int | None = Field(sa_column=Column(Choice(c.HOTEL_LOTTERY_HOTELS_OPTS), nullable=True)) + assigned_room_type: int | None = Field(sa_column=Column(Choice(c.HOTEL_LOTTERY_ROOM_TYPES_OPTS), nullable=True)) + assigned_suite_type: int | None = Field(sa_column=Column(Choice(c.HOTEL_LOTTERY_SUITE_ROOM_TYPES_OPTS), nullable=True)) + assigned_check_in_date: date | None = Field(sa_type=Date, nullable=True) + assigned_check_out_date: date | None = Field(sa_type=Date, nullable=True) + deposit_cutoff_date: date | None = Field(sa_type=Date, nullable=True) + lottery_name: str = '' + booking_url: str = '' @presave_adjustment def unset_entry_type(self): diff --git a/uber/models/legal.py b/uber/models/legal.py index 2d8ded147..701698f84 100644 --- a/uber/models/legal.py +++ b/uber/models/legal.py @@ -1,21 +1,22 @@ +from datetime import datetime from sqlalchemy.types import Uuid, String, DateTime from uber.decorators import presave_adjustment from uber.models import MagModel -from uber.models.types import DefaultColumn as Column +from uber.models.types import DefaultField as Field __all__ = ['SignedDocument'] -class SignedDocument(MagModel): - fk_id = Column(Uuid(as_uuid=False), index=True) - model = Column(String) - document_id = Column(String) - last_emailed = Column(DateTime(timezone=True), nullable=True, default=None) - link = Column(String) - ident = Column(String) - signed = Column(DateTime(timezone=True), nullable=True, default=None) - declined = Column(DateTime(timezone=True), nullable=True, default=None) +class SignedDocument(MagModel, table=True): + fk_id: str = Field(sa_type=Uuid(as_uuid=False), index=True) + model: str = '' + document_id: str = '' + last_emailed: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True, default=None) + link: str = '' + ident: str = '' + signed: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True, default=None) + declined: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True, default=None) @presave_adjustment def null_to_strings(self): diff --git a/uber/models/marketplace.py b/uber/models/marketplace.py index 6df027134..a1e2088bb 100644 --- a/uber/models/marketplace.py +++ b/uber/models/marketplace.py @@ -2,49 +2,48 @@ from uber.custom_tags import email_only, email_to_link from uber.models import MagModel from uber.decorators import presave_adjustment -from uber.models.types import Choice, DefaultColumn as Column, default_relationship as relationship, MultiChoice, utcnow +from uber.models.types import (Choice, default_relationship as relationship, DefaultColumn as Column, + DefaultField as Field, DefaultRelationship as Relationship) from datetime import datetime from markupsafe import Markup from pytz import UTC from sqlalchemy.orm import backref -from sqlalchemy.types import Boolean, Integer, Uuid, String, DateTime -from sqlalchemy.schema import ForeignKey +from sqlalchemy.types import Uuid, DateTime +from typing import ClassVar __all__ = ['ArtistMarketplaceApplication'] -class ArtistMarketplaceApplication(MagModel): - MATCHING_DEALER_FIELDS = ['email_address', 'website', 'name'] - - attendee_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id')) - attendee = relationship('Attendee', backref=backref('marketplace_application', uselist=False), - cascade='save-update,merge,refresh-expire,expunge', - uselist=False) - name = Column(String) - display_name = Column(String) - email_address = Column(String) - website = Column(String) - tax_number = Column(String) - terms_accepted = Column(Boolean, default=False) - seating_requests = Column(String) - accessibility_requests = Column(String) - - status = Column(Choice(c.MARKETPLACE_STATUS_OPTS), default=c.PENDING, admin_only=True) - registered = Column(DateTime(timezone=True), server_default=utcnow(), default=lambda: datetime.now(UTC)) - accepted = Column(DateTime(timezone=True), nullable=True) - receipt_items = relationship('ReceiptItem', +class ArtistMarketplaceApplication(MagModel, table=True): + MATCHING_DEALER_FIELDS: ClassVar = ['email_address', 'website', 'name'] + + attendee_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', ondelete='CASCADE', unique=True) + attendee: 'Attendee' = Relationship(back_populates="marketplace_application", sa_relationship_kwargs={'lazy': 'joined', 'single_parent': True}) + name: str = '' + display_name: str = '' + email_address: str = '' + website: str = '' + tax_number: str = '' + terms_accepted: bool = False + seating_requests: str = '' + accessibility_requests: str = '' + + status: int = Field(sa_column=Column(Choice(c.MARKETPLACE_STATUS_OPTS)), default=c.PENDING) + registered: datetime = Field(sa_type=DateTime(timezone=True), default_factory=lambda: datetime.now(UTC)) + accepted: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True) + admin_notes: str = '' + overridden_price: int | None = Field(default=0, nullable=True) + + receipt_items: list['ReceiptItem'] = Relationship(sa_relationship=relationship('ReceiptItem', primaryjoin='and_(' 'ReceiptItem.fk_model == "ArtistMarketplaceApplication", ' 'remote(ReceiptItem.fk_id) == foreign(ArtistMarketplaceApplication.id))', viewonly=True, - uselist=True) - - admin_notes = Column(String, admin_only=True) - overridden_price = Column(Integer, nullable=True, admin_only=True) + uselist=True)) - email_model_name = 'app' + email_model_name: ClassVar = 'app' @presave_adjustment def _cost_adjustments(self): diff --git a/uber/models/mits.py b/uber/models/mits.py index ea7f6fbc7..d5cab5a3e 100644 --- a/uber/models/mits.py +++ b/uber/models/mits.py @@ -5,43 +5,47 @@ from pytz import UTC from sqlalchemy import and_ -from sqlalchemy.schema import ForeignKey -from sqlalchemy.types import Boolean, Integer, Uuid, DateTime, String +from sqlalchemy.types import Uuid, DateTime from sqlalchemy.ext.hybrid import hybrid_property +from typing import ClassVar from uber.config import c from uber.models import MagModel -from uber.models.types import (default_relationship as relationship, utcnow, Choice, DefaultColumn as Column, - MultiChoice, GuidebookImageMixin) +from uber.models.types import (Choice, MultiChoice, GuidebookImageMixin, DefaultColumn as Column, + DefaultField as Field, DefaultRelationship as Relationship) from uber.utils import slugify __all__ = ['MITSTeam', 'MITSApplicant', 'MITSGame', 'MITSPicture', 'MITSDocument', 'MITSTimes'] -class MITSTeam(MagModel): - name = Column(String) - days_available = Column(Integer, nullable=True) - hours_available = Column(Integer, nullable=True) - concurrent_attendees = Column(Integer, default=0) - panel_interest = Column(Boolean, nullable=True, admin_only=True) - showcase_interest = Column(Boolean, nullable=True, admin_only=True) - want_to_sell = Column(Boolean, default=False) - address = Column(String) - submitted = Column(DateTime(timezone=True), nullable=True) - waiver_signature = Column(String) - waiver_signed = Column(DateTime(timezone=True), nullable=True) - - applied = Column(DateTime(timezone=True), server_default=utcnow(), default=lambda: datetime.now(UTC)) - status = Column(Choice(c.MITS_APP_STATUS), default=c.PENDING, admin_only=True) - - applicants = relationship('MITSApplicant', backref='team') - games = relationship('MITSGame', backref='team') - schedule = relationship('MITSTimes', uselist=False, backref='team') - panel_app = relationship('MITSPanelApplication', uselist=False, backref='team') - - duplicate_of = Column(Uuid(as_uuid=False), nullable=True) - deleted = Column(Boolean, default=False) +class MITSTeam(MagModel, table=True): + name: str = '' + days_available: int | None = Field(nullable=True) + hours_available: int | None = Field(nullable=True) + concurrent_attendees: int = 0 + panel_interest: bool | None = Field(nullable=True, admin_only=True) + showcase_interest: bool | None = Field(nullable=True, admin_only=True) + want_to_sell: bool = False + address: str = '' + submitted: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True) + waiver_signature: str = '' + waiver_signed: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True) + + applied: datetime = Field(sa_type=DateTime(timezone=True), default_factory=lambda: datetime.now(UTC)) + status: int = Field(sa_column=Column(Choice(c.MITS_APP_STATUS), admin_only=True), default=c.PENDING) + + applicants: list['MITSApplicant'] = Relationship( + back_populates="team", sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + games: list['MITSGame'] = Relationship( + back_populates="team", sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + schedule: 'MITSTimes' = Relationship( + back_populates="team", sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + panel_app: 'MITSPanelApplication' = Relationship( + back_populates="team", sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + + duplicate_of: str | None = Field(sa_type=Uuid(as_uuid=False), nullable=True) + deleted: bool = False # We've found that a lot of people start filling out an application and # then instead of continuing their application just start over fresh and # fill out a new one. In these cases we mark the application as @@ -49,7 +53,7 @@ class MITSTeam(MagModel): # applicant tries to log into the original application, we can redirect # them to the correct application. - email_model_name = 'team' + email_model_name: ClassVar = 'team' @property def accepted(self): @@ -130,20 +134,24 @@ def completion_percentage(self): return 100 * self.steps_completed // c.MITS_APPLICATION_STEPS -class MITSApplicant(MagModel): - team_id = Column(ForeignKey('mits_team.id')) - attendee_id = Column(ForeignKey('attendee.id'), nullable=True) - primary_contact = Column(Boolean, default=False) - first_name = Column(String) - last_name = Column(String) - email = Column(String) - cellphone = Column(String) - contact_method = Column(Choice(c.MITS_CONTACT_OPTS), default=c.TEXTING) +class MITSApplicant(MagModel, table=True): + team_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='mits_team.id', ondelete='CASCADE') + team: 'MITSTeam' = Relationship(back_populates="applicants", sa_relationship_kwargs={'lazy': 'joined'}) - declined_hotel_space = Column(Boolean, default=False) - requested_room_nights = Column(MultiChoice(c.MITS_ROOM_NIGHT_OPTS), default='') + attendee_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', nullable=True) + attendee: 'Attendee' = Relationship(back_populates="mits_applicants") - email_model_name = 'applicant' + primary_contact: bool = False + first_name: str = '' + last_name: str = '' + email: str = '' + cellphone: str = '' + contact_method: int = Field(sa_column=Column(Choice(c.MITS_CONTACT_OPTS)), default=c.TEXTING) + + declined_hotel_space: bool = False + requested_room_nights: str = Field(sa_type=MultiChoice(c.MITS_ROOM_NIGHT_OPTS), default='') + + email_model_name: ClassVar = 'applicant' @property def email_to_address(self): @@ -159,24 +167,29 @@ def has_requested(self, night): return night in self.requested_room_nights_ints -class MITSGame(MagModel): - team_id = Column(ForeignKey('mits_team.id')) - name = Column(String) - promo_blurb = Column(String) - description = Column(String) - genre = Column(String) - phase = Column(Choice(c.MITS_PHASE_OPTS), default=c.DEVELOPMENT) - min_age = Column(Choice(c.MITS_AGE_OPTS), default=c.CHILD) - age_explanation = Column(String) - min_players = Column(Integer, default=2) - max_players = Column(Integer, default=4) - copyrighted = Column(Choice(c.MITS_COPYRIGHT_OPTS), nullable=True) - personally_own = Column(Boolean, default=False) - unlicensed = Column(Boolean, default=False) - professional = Column(Boolean, default=False) - tournament = Column(Boolean, default=False) - pictures = relationship('MITSPicture', backref='game') - documents = relationship('MITSDocument', backref='game') +class MITSGame(MagModel, table=True): + team_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='mits_team.id', ondelete='CASCADE') + team: 'MITSTeam' = Relationship(back_populates="games", sa_relationship_kwargs={'lazy': 'joined'}) + + name: str = '' + promo_blurb: str = '' + description: str = '' + genre: str = '' + phase: int = Field(sa_column=Column(Choice(c.MITS_PHASE_OPTS)), default=c.DEVELOPMENT) + min_age: int = Field(sa_column=Column(Choice(c.MITS_AGE_OPTS)), default=c.CHILD) + age_explanation: str = '' + min_players: int = 2 + max_players: int = 4 + copyrighted: int | None = Field(sa_column=Column(Choice(c.MITS_COPYRIGHT_OPTS), nullable=True)) + personally_own: bool = False + unlicensed: bool = False + professional: bool = False + tournament: bool = False + + pictures: list['MITSPicture'] = Relationship( + back_populates="game", sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + documents: list['MITSDocument'] = Relationship( + back_populates="game", sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) @hybrid_property def has_been_accepted(self): @@ -230,9 +243,11 @@ def guidebook_images(self): return [header_name, thumbnail_name], [header, thumbnail] -class MITSPicture(MagModel, GuidebookImageMixin): - game_id = Column(Uuid(as_uuid=False), ForeignKey('mits_game.id')) - description = Column(String) +class MITSPicture(MagModel, GuidebookImageMixin, table=True): + game_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='mits_game.id', ondelete='CASCADE') + game: 'MITSGame' = Relationship(back_populates="pictures", sa_relationship_kwargs={'lazy': 'joined'}) + + description: str = '' @property def url(self): @@ -243,10 +258,12 @@ def filepath(self): return os.path.join(c.MITS_PICTURE_DIR, str(self.id)) -class MITSDocument(MagModel): - game_id = Column(Uuid(as_uuid=False), ForeignKey('mits_game.id')) - filename = Column(String) - description = Column(String) +class MITSDocument(MagModel, table=True): + game_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='mits_game.id', ondelete='CASCADE') + game: 'MITSGame' = Relationship(back_populates="documents", sa_relationship_kwargs={'lazy': 'joined'}) + + filename: str = '' + description: str = '' @property def url(self): @@ -257,18 +274,22 @@ def filepath(self): return os.path.join(c.MITS_PICTURE_DIR, str(self.id)) -class MITSTimes(MagModel): - team_id = Column(ForeignKey('mits_team.id')) - showcase_availability = Column(MultiChoice(c.MITS_SHOWCASE_SCHEDULE_OPTS)) - availability = Column(MultiChoice(c.MITS_SCHEDULE_OPTS)) +class MITSTimes(MagModel, table=True): + team_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='mits_team.id', ondelete='CASCADE', unique=True) + team: 'MITSTeam' = Relationship(back_populates="schedule", sa_relationship_kwargs={'lazy': 'joined', 'single_parent': True}) + + showcase_availability: str = Field(sa_type=MultiChoice(c.MITS_SHOWCASE_SCHEDULE_OPTS), default='') + availability: str = Field(sa_type=MultiChoice(c.MITS_SCHEDULE_OPTS), default='') + +class MITSPanelApplication(MagModel, table=True): + team_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='mits_team.id', ondelete='CASCADE', unique=True) + team: 'MITSTeam' = Relationship(back_populates="panel_app", sa_relationship_kwargs={'lazy': 'joined', 'single_parent': True}) -class MITSPanelApplication(MagModel): - team_id = Column(ForeignKey('mits_team.id')) - name = Column(String) - description = Column(String) - length = Column(Choice(c.PANEL_STRICT_LENGTH_OPTS), default=c.SIXTY_MIN) - participation_interest = Column(Boolean, default=False) + name: str = '' + description: str = '' + length: int = Field(sa_column=Column(Choice(c.PANEL_STRICT_LENGTH_OPTS)), default=c.SIXTY_MIN) + participation_interest: bool = False def add_applicant_restriction(): diff --git a/uber/models/panels.py b/uber/models/panels.py index 0bad7a37d..5930b5ad2 100644 --- a/uber/models/panels.py +++ b/uber/models/panels.py @@ -2,16 +2,16 @@ from datetime import datetime, timedelta from pytz import UTC -from sqlalchemy.orm import backref from sqlalchemy.schema import ForeignKey, Table, UniqueConstraint, Index from sqlalchemy.types import Boolean, Integer, Uuid, String, DateTime from sqlalchemy.ext.hybrid import hybrid_property +from typing import ClassVar from uber.config import c from uber.decorators import presave_adjustment from uber.models import MagModel -from uber.models.types import default_relationship as relationship, utcnow, Choice, DefaultColumn as Column, \ - MultiChoice, UniqueList +from uber.models.types import (utcnow, Choice, DefaultColumn as Column, MultiChoice, UniqueList, + DefaultField as Field, DefaultRelationship as Relationship) __all__ = ['AssignedPanelist', 'Event', 'EventLocation', 'EventFeedback', 'PanelApplicant', 'PanelApplication'] @@ -29,16 +29,18 @@ ) -class EventLocation(MagModel): - department_id = Column(Uuid(as_uuid=False), ForeignKey('department.id', ondelete='SET NULL'), nullable=True) - name = Column(String) - room = Column(String) - tracks = Column(MultiChoice(c.EVENT_TRACK_OPTS)) +class EventLocation(MagModel, table=True): + department_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='department.id', nullable=True) + department: 'Department' = Relationship(back_populates="locations") - events = relationship('Event', backref=backref('location', cascade="save-update,merge"), - cascade="save-update,merge", single_parent=True) - attractions = relationship('AttractionEvent', backref=backref('location', cascade="save-update,merge"), - cascade="save-update,merge", single_parent=True) + name: str = '' + room: str = '' + tracks: str = Field(sa_column=Column(MultiChoice(c.EVENT_TRACK_OPTS)), default='') + + events: list['Event'] = Relationship( + back_populates="location") + attractions: list['AttractionEvent'] = Relationship( + back_populates="location") @property def schedule_name(self): @@ -78,26 +80,30 @@ def update_events(self, session): session.add(event) -class Event(MagModel): - event_location_id = Column(Uuid(as_uuid=False), ForeignKey('event_location.id', ondelete='SET NULL'), nullable=True) - department_id = Column(Uuid(as_uuid=False), ForeignKey('department.id', ondelete='SET NULL'), nullable=True) - attraction_event_id = Column(Uuid(as_uuid=False), ForeignKey('attraction_event.id', ondelete='SET NULL'), nullable=True) - start_time = Column(DateTime(timezone=True)) - duration = Column(Integer, default=60) - name = Column(String, nullable=False) - description = Column(String) - public_description = Column(String) - tracks = Column(MultiChoice(c.EVENT_TRACK_OPTS)) - - assigned_panelists = relationship('AssignedPanelist', backref='event') - applications = relationship('PanelApplication', backref=backref('event', cascade="save-update,merge"), - cascade="save-update,merge") - panel_feedback = relationship('EventFeedback', backref='event') - guest = relationship('GuestGroup', backref=backref('event', cascade="save-update,merge"), - cascade='save-update,merge') - attraction = relationship('AttractionEvent', backref=backref( - 'schedule_item', cascade="save-update,merge", uselist=False - ), cascade='save-update,merge') +class Event(MagModel, table=True): + event_location_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='event_location.id', nullable=True) + location: 'EventLocation' = Relationship(back_populates="events", sa_relationship_kwargs={'lazy': 'joined'}) + + department_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='department.id', nullable=True) + department: 'Department' = Relationship(back_populates="events") + + attraction_event_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attraction_event.id', nullable=True, unique=True) + attraction: 'AttractionEvent' = Relationship(back_populates="schedule_item", sa_relationship_kwargs={'lazy': 'joined', 'single_parent': True}) + + start_time: datetime = Field(sa_type=DateTime(timezone=True)) + duration: int = 60 + name: str = False + description: str = '' + public_description: str = '' + tracks: str = Field(sa_column=Column(MultiChoice(c.EVENT_TRACK_OPTS)), default='') + + assigned_panelists: list['AssignedPanelist'] = Relationship( + back_populates="event", sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + applications: list['PanelApplication'] = Relationship( + back_populates="event") + panel_feedback: list['EventFeedback'] = Relationship( + back_populates="event", sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + guest: 'GuestGroup' = Relationship(back_populates="event") @property def minutes(self): @@ -153,9 +159,12 @@ def guidebook_desc(self): return self.public_description or self.description -class AssignedPanelist(MagModel): - attendee_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id', ondelete='cascade')) - event_id = Column(Uuid(as_uuid=False), ForeignKey('event.id', ondelete='cascade')) +class AssignedPanelist(MagModel, table=True): + attendee_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', ondelete='CASCADE') + attendee: 'Attendee' = Relationship(back_populates="assigned_panelists", sa_relationship_kwargs={'lazy': 'joined'}) + + event_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='event.id', ondelete='CASCADE') + event: 'Event' = Relationship(back_populates="assigned_panelists", sa_relationship_kwargs={'lazy': 'joined'}) def __repr__(self): if self.attendee: @@ -165,51 +174,56 @@ def __repr__(self): return super(AssignedPanelist, self).__repr__() -class PanelApplication(MagModel): - event_id = Column(Uuid(as_uuid=False), ForeignKey('event.id', ondelete='SET NULL'), nullable=True) - poc_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id', ondelete='SET NULL'), nullable=True) - submitter_id = Column(Uuid(as_uuid=False), ForeignKey('panel_applicant.id', ondelete='SET NULL'), nullable=True) - name = Column(String) - length = Column(Choice(c.PANEL_LENGTH_OPTS), default=c.SIXTY_MIN) - length_text = Column(String) - length_reason = Column(String) - description = Column(String) - public_description = Column(String) - unavailable = Column(String) - available = Column(String) - affiliations = Column(String) - past_attendance = Column(String) - department = Column(UniqueList) - department_name = Column(String) - rating = Column(Choice(c.PANEL_RATING_OPTS), default=c.UNRATED) - granular_rating = Column(MultiChoice(c.PANEL_CONTENT_OPTS)) - presentation = Column(Choice(c.PRESENTATION_OPTS)) - other_presentation = Column(String) - noise_level = Column(Choice(c.NOISE_LEVEL_OPTS)) - tech_needs = Column(MultiChoice(c.TECH_NEED_OPTS)) - other_tech_needs = Column(String) - need_tables = Column(Boolean, default=False) - tables_desc = Column(String) - has_cost = Column(Boolean, default=False) - is_loud = Column(Boolean, default=False) - tabletop = Column(Boolean, default=False) - cost_desc = Column(String) - livestream = Column(Choice(c.LIVESTREAM_OPTS), default=c.OPT_IN) - record = Column(Choice(c.LIVESTREAM_OPTS), default=c.OPT_IN) - panelist_bringing = Column(String) - extra_info = Column(String) - applied = Column(DateTime(timezone=True), server_default=utcnow(), default=lambda: datetime.now(UTC)) - accepted = Column(DateTime(timezone=True), nullable=True) - confirmed = Column(DateTime(timezone=True), nullable=True) - status = Column(Choice(c.PANEL_APP_STATUS_OPTS), default=c.PENDING, admin_only=True) - comments = Column(String, admin_only=True) - tags = Column(UniqueList, admin_only=True) - - applicants = relationship('PanelApplicant', backref='applications', - cascade='save-update,merge,refresh-expire,expunge', - secondary='panel_applicant_application') - - email_model_name = 'app' +class PanelApplication(MagModel, table=True): + event_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='event.id', nullable=True) + event: 'Event' = Relationship(back_populates="applications", sa_relationship_kwargs={'lazy': 'joined'}) + + poc_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', nullable=True) + poc: 'Attendee' = Relationship(back_populates="panel_applications") + + submitter_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='panel_applicant.id', nullable=True) + + name: str = '' + length: int = Field(sa_column=Column(Choice(c.PANEL_LENGTH_OPTS)), default=c.SIXTY_MIN) + length_text: str = '' + length_reason: str = '' + description: str = '' + public_description: str = '' + unavailable: str = '' + available: str = '' + affiliations: str = '' + past_attendance: str = '' + department: str = Field(sa_type=UniqueList, default='') + department_name: str = '' + rating: int = Field(sa_column=Column(Choice(c.PANEL_RATING_OPTS)), default=c.UNRATED) + granular_rating: str = Field(sa_column=Column(MultiChoice(c.PANEL_CONTENT_OPTS)), default='') + presentation: int = Field(sa_column=Column(Choice(c.PRESENTATION_OPTS))) + other_presentation: str = '' + noise_level: int = Field(sa_column=Column(Choice(c.NOISE_LEVEL_OPTS))) + tech_needs: str = Field(sa_column=Column(MultiChoice(c.TECH_NEED_OPTS)), default='') + other_tech_needs: str = '' + need_tables: bool = False + tables_desc: str = '' + has_cost: bool = False + is_loud: bool = False + tabletop: bool = False + cost_desc: str = '' + livestream: int = Field(sa_column=Column(Choice(c.LIVESTREAM_OPTS)), default=c.OPT_IN) + record: int = Field(sa_column=Column(Choice(c.LIVESTREAM_OPTS)), default=c.OPT_IN) + panelist_bringing: str = '' + extra_info: str = '' + applied: datetime = Field(sa_type=DateTime(timezone=True), default_factory=lambda: datetime.now(UTC)) + accepted: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True) + confirmed: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True) + status: int = Field(sa_column=Column(Choice(c.PANEL_APP_STATUS_OPTS)), default=c.PENDING, admin_only=True) + comments: str = '' + tags: str = Field(sa_type=UniqueList, default='') + + applicants: list['PanelApplicant'] = Relationship( + back_populates="applications", + sa_relationship_kwargs={'lazy': 'selectin', 'secondary': 'panel_applicant_application'}) + + email_model_name: ClassVar = 'app' @presave_adjustment def update_event_info(self): @@ -317,24 +331,30 @@ def has_been_accepted(self): return self.status == c.ACCEPTED -class PanelApplicant(MagModel): - attendee_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id', ondelete='SET NULL'), nullable=True) - submitter = Column(Boolean, default=False) - first_name = Column(String) - last_name = Column(String) - email = Column(String) - cellphone = Column(String) - communication_pref = Column(MultiChoice(c.COMMUNICATION_PREF_OPTS)) - other_communication_pref = Column(String) - requested_accessibility_services = Column(Boolean, default=False) - pronouns = Column(MultiChoice(c.PRONOUN_OPTS)) - other_pronouns = Column(String) - occupation = Column(String) - website = Column(String) - other_credentials = Column(String) - guidebook_bio = Column(String) - display_name = Column(String) - social_media_info = Column(String) +class PanelApplicant(MagModel, table=True): + attendee_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', ondelete='SET NULL', nullable=True) + attendee: 'Attendee' = Relationship(back_populates="panel_applicants", sa_relationship_kwargs={'lazy': 'joined'}) + + submitter: bool = False + first_name: str = '' + last_name: str = '' + email: str = '' + cellphone: str = '' + communication_pref: str = Field(sa_column=Column(MultiChoice(c.COMMUNICATION_PREF_OPTS)), default='') + other_communication_pref: str = '' + requested_accessibility_services: bool = False + pronouns: str = Field(sa_column=Column(MultiChoice(c.PRONOUN_OPTS)), default='') + other_pronouns: str = '' + occupation: str = '' + website: str = '' + other_credentials: str = '' + guidebook_bio: str = '' + display_name: str = '' + social_media_info: str = '' + + applications: list['PanelApplication'] = Relationship( + back_populates="applicants", + sa_relationship_kwargs={'lazy': 'selectin', 'secondary': 'panel_applicant_application'}) @property def has_credentials(self): @@ -359,10 +379,14 @@ def check_if_still_submitter(self, app_id): self.submitter = False -class EventFeedback(MagModel): - event_id = Column(Uuid(as_uuid=False), ForeignKey('event.id')) - attendee_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id', ondelete='cascade')) - headcount_starting = Column(Integer, default=0) - headcount_during = Column(Integer, default=0) - comments = Column(String) - rating = Column(Choice(c.PANEL_FEEDBACK_OPTS), default=c.UNRATED) +class EventFeedback(MagModel, table=True): + event_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='event.id', ondelete='CASCADE') + event: 'Event' = Relationship(back_populates="panel_feedback") + + attendee_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', ondelete='CASCADE') + attendee: 'Attendee' = Relationship(back_populates="panel_feedback") + + headcount_starting: int = 0 + headcount_during: int = 0 + comments: str = '' + rating: int = Field(sa_column=Column(Choice(c.PANEL_FEEDBACK_OPTS)), default=c.UNRATED) diff --git a/uber/models/promo_code.py b/uber/models/promo_code.py index 81fcdc44f..929e7cbee 100644 --- a/uber/models/promo_code.py +++ b/uber/models/promo_code.py @@ -9,20 +9,21 @@ from dateutil import parser as dateparser from sqlalchemy import exists, func, select, CheckConstraint from sqlalchemy.ext.hybrid import hybrid_property -from sqlalchemy.schema import Index, ForeignKey +from sqlalchemy.schema import Index from sqlalchemy.types import Integer, Uuid, String, DateTime +from typing import ClassVar from uber.config import c from uber.decorators import presave_adjustment from uber.models import MagModel -from uber.models.types import default_relationship as relationship, utcnow, DefaultColumn as Column, Choice +from uber.models.types import utcnow, DefaultColumn as Column, Choice, DefaultField as Field, DefaultRelationship as Relationship from uber.utils import localized_now, RegistrationCode __all__ = ['PromoCodeWord', 'PromoCodeGroup', 'PromoCode'] -class PromoCodeWord(MagModel): +class PromoCodeWord(MagModel, table=True): """ Words used to generate promo codes. @@ -45,30 +46,21 @@ class PromoCodeWord(MagModel): `part_of_speech`. """ - _ADJECTIVE = 0 - _NOUN = 1 - _VERB = 2 - _ADVERB = 3 - _PART_OF_SPEECH_OPTS = [ + _ADJECTIVE: ClassVar = 0 + _NOUN: ClassVar = 1 + _VERB: ClassVar = 2 + _ADVERB: ClassVar = 3 + _PART_OF_SPEECH_OPTS: ClassVar = [ (_ADJECTIVE, 'adjective'), (_NOUN, 'noun'), (_VERB, 'verb'), (_ADVERB, 'adverb')] - _PARTS_OF_SPEECH = dict(_PART_OF_SPEECH_OPTS) - - word = Column(String) - part_of_speech = Column(Choice(_PART_OF_SPEECH_OPTS), default=_ADJECTIVE) - - __table_args__ = ( - Index( - 'uq_promo_code_word_normalized_word_part_of_speech', - func.lower(func.trim(word)), - part_of_speech, - unique=True), - CheckConstraint(func.trim(word) != '', name='ck_promo_code_word_non_empty_word') - ) + _PARTS_OF_SPEECH: ClassVar = dict(_PART_OF_SPEECH_OPTS) + + word: str = '' + part_of_speech: int = Field(sa_column=Column(Choice(_PART_OF_SPEECH_OPTS)), default=_ADJECTIVE) - _repr_attr_names = ('word',) + _repr_attr_names: ClassVar = ('word',) @hybrid_property def normalized_word(self): @@ -128,19 +120,26 @@ def normalize_word(cls, word): c.PROMO_CODE_WORD_PART_OF_SPEECH_OPTS = PromoCodeWord._PART_OF_SPEECH_OPTS c.PROMO_CODE_WORD_PARTS_OF_SPEECH = PromoCodeWord._PARTS_OF_SPEECH +Index( + 'uq_promo_code_word_normalized_word_part_of_speech', + func.lower(func.trim(PromoCodeWord.word)), + PromoCodeWord.part_of_speech, + unique=True), +CheckConstraint(func.trim(PromoCodeWord.word) != '', name='ck_promo_code_word_non_empty_word') + +class PromoCodeGroup(MagModel, table=True): + buyer_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', nullable=True) + buyer: 'Attendee' = Relationship(back_populates="promo_code_groups", sa_relationship_kwargs={'lazy': 'joined'}) -class PromoCodeGroup(MagModel): - name = Column(String) - code = Column(String, admin_only=True) - registered = Column(DateTime(timezone=True), server_default=utcnow(), default=lambda: datetime.now(UTC)) - buyer_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id', ondelete='SET NULL'), nullable=True) - buyer = relationship( - 'Attendee', backref='promo_code_groups', - foreign_keys=buyer_id, - cascade='save-update,merge,refresh-expire,expunge') + name: str = '' + code: str = '' + registered: datetime = Field(sa_type=DateTime(timezone=True), default_factory=lambda: datetime.now(UTC)) - email_model_name = 'group' + promo_codes: list['PromoCode'] = Relationship( + back_populates="group", sa_relationship_kwargs={'lazy': 'selectin'}) + + email_model_name: ClassVar = 'group' @presave_adjustment def group_code(self): @@ -222,7 +221,7 @@ def min_badges_addable(self): return 1 if self.hours_remaining_in_grace_period > 0 else c.MIN_GROUP_ADDITION -class PromoCode(MagModel): +class PromoCode(MagModel, table=True): """ Promo codes used by attendees to purchase badges at discounted prices. @@ -299,38 +298,30 @@ class PromoCode(MagModel): uses_remaining. """ - _FIXED_DISCOUNT = 0 - _FIXED_PRICE = 1 - _PERCENT_DISCOUNT = 2 - _DISCOUNT_TYPE_OPTS = [ + _FIXED_DISCOUNT: ClassVar = 0 + _FIXED_PRICE: ClassVar = 1 + _PERCENT_DISCOUNT: ClassVar = 2 + _DISCOUNT_TYPE_OPTS: ClassVar = [ (_FIXED_DISCOUNT, 'Fixed Discount'), (_FIXED_PRICE, 'Fixed Price'), (_PERCENT_DISCOUNT, 'Percent Discount')] + group_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='promo_code_group.id', nullable=True) + group: 'PromoCodeGroup' = Relationship(back_populates="promo_codes", sa_relationship_kwargs={'lazy': 'joined'}) + + code: str = '' + discount: int | None = Field(nullable=True, default=None) + discount_type: int = Field(sa_column=Column(Choice(_DISCOUNT_TYPE_OPTS)), default=_FIXED_DISCOUNT) + expiration_date: datetime = Field(sa_type=DateTime(timezone=True), default=c.ESCHATON) + uses_allowed: int | None = Field(nullable=True, default=None) + cost: int | None = Field(nullable=True, default=None) + admin_notes: str = '' - code = Column(String) - discount = Column(Integer, nullable=True, default=None) - discount_type = Column(Choice(_DISCOUNT_TYPE_OPTS), default=_FIXED_DISCOUNT) - expiration_date = Column(DateTime(timezone=True), default=c.ESCHATON) - uses_allowed = Column(Integer, nullable=True, default=None) - cost = Column(Integer, nullable=True, default=None) - admin_notes = Column(String) - - group_id = Column(Uuid(as_uuid=False), ForeignKey('promo_code_group.id', ondelete='SET NULL'), nullable=True) - group = relationship( - PromoCodeGroup, backref='promo_codes', - foreign_keys=group_id, - cascade='save-update,merge,refresh-expire,expunge') - - __table_args__ = ( - Index( - 'uq_promo_code_normalized_code', - func.replace(func.replace(func.lower(code), '-', ''), ' ', ''), - unique=True), - CheckConstraint(func.trim(code) != '', name='ck_promo_code_non_empty_code') + used_by: list['Attendee'] = Relationship( + back_populates="promo_code", sa_relationship_kwargs={'lazy': 'selectin', 'cascade': 'merge,refresh-expire,expunge'} ) - _repr_attr_names = ('code',) + _repr_attr_names: ClassVar = ('code',) @classmethod def normalize_expiration_date(cls, dt): @@ -508,3 +499,8 @@ def calculate_discounted_price(self, price): c.PROMO_CODE_DISCOUNT_TYPE_OPTS = PromoCode._DISCOUNT_TYPE_OPTS +Index( + 'uq_promo_code_normalized_code', + func.replace(func.replace(func.lower(PromoCode.code), '-', ''), ' ', ''), + unique=True), +CheckConstraint(func.trim(PromoCode.code) != '', name='ck_promo_code_non_empty_code') diff --git a/uber/models/showcase.py b/uber/models/showcase.py index 1b2508660..2bc5f02a6 100644 --- a/uber/models/showcase.py +++ b/uber/models/showcase.py @@ -8,16 +8,17 @@ from markupsafe import Markup from pytz import UTC from sqlalchemy import func, case, or_ -from sqlalchemy.schema import ForeignKey, UniqueConstraint +from sqlalchemy.schema import UniqueConstraint from sqlalchemy.types import Boolean, Integer, String, DateTime, Uuid from sqlalchemy.ext.hybrid import hybrid_property +from typing import ClassVar from uber.config import c from uber.custom_tags import readable_join, datetime_local_filter from uber.decorators import presave_adjustment from uber.models import MagModel, Attendee -from uber.models.types import default_relationship as relationship, utcnow, \ - Choice, DefaultColumn as Column, MultiChoice, GuidebookImageMixin, UniqueList +from uber.models.types import (utcnow, Choice, DefaultColumn as Column, MultiChoice, GuidebookImageMixin, UniqueList, + DefaultField as Field, DefaultRelationship as Relationship) from uber.utils import localized_now, make_url, remove_opt, slugify log = logging.getLogger(__name__) @@ -34,22 +35,26 @@ def game_reviews(self): return [r for r in self.reviews if r.game_status != c.PENDING] -class IndieJudge(MagModel, ReviewMixin): - admin_id = Column(Uuid(as_uuid=False), ForeignKey('admin_account.id')) - status = Column(Choice(c.MIVS_JUDGE_STATUS_OPTS), default=c.UNCONFIRMED) - assignable_showcases = Column(MultiChoice(c.SHOWCASE_GAME_TYPE_OPTS)) - all_games_showcases = Column(MultiChoice(c.SHOWCASE_GAME_TYPE_OPTS)) - no_game_submission = Column(Boolean, nullable=True) - genres = Column(MultiChoice(c.MIVS_JUDGE_GENRE_OPTS)) - platforms = Column(MultiChoice(c.MIVS_PLATFORM_OPTS)) - platforms_text = Column(String) - vr_text = Column(String) - staff_notes = Column(String) +class IndieJudge(MagModel, ReviewMixin, table=True): + admin_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='admin_account.id', nullable=True, unique=True) + admin_account: "AdminAccount" = Relationship(back_populates="judge", sa_relationship_kwargs={'single_parent': True}) - codes = relationship('IndieGameCode', backref='judge') - reviews = relationship('IndieGameReview', backref='judge') + status: int = Field(sa_column=Column(Choice(c.MIVS_JUDGE_STATUS_OPTS)), default=c.UNCONFIRMED) + assignable_showcases: str = Field(sa_column=Column(MultiChoice(c.SHOWCASE_GAME_TYPE_OPTS)), default='') + all_games_showcases: str = Field(sa_column=Column(MultiChoice(c.SHOWCASE_GAME_TYPE_OPTS)), default='') + no_game_submission: bool | None = True + genres: str = Field(sa_column=Column(MultiChoice(c.MIVS_JUDGE_GENRE_OPTS)), default='') + platforms: str = Field(sa_column=Column(MultiChoice(c.MIVS_PLATFORM_OPTS)), default='') + platforms_text: str = '' + vr_text: str = '' + staff_notes: str = '' - email_model_name = 'judge' + codes: list['IndieGameCode'] = Relationship( + back_populates="judge", sa_relationship_kwargs={'lazy': 'selectin'}) + reviews: list['IndieGameReview'] = Relationship( + back_populates="judge", sa_relationship_kwargs={'lazy': 'selectin', 'cascade': 'all,delete-orphan', 'passive_deletes': True}) + + email_model_name: ClassVar = 'judge' @presave_adjustment def only_one_showcase(self): @@ -127,38 +132,39 @@ def get_code_for(self, game_id): return codes_for_game[0] if codes_for_game else '' -class IndieStudio(MagModel): - group_id = Column(Uuid(as_uuid=False), ForeignKey('group.id'), nullable=True) - name = Column(String, unique=True) - website = Column(String) - other_links = Column(UniqueList) - - status = Column( - Choice(c.MIVS_STUDIO_STATUS_OPTS), default=c.NEW, admin_only=True) - staff_notes = Column(String, admin_only=True) - registered = Column(DateTime(timezone=True), server_default=utcnow(), default=lambda: datetime.now(UTC)) - - accepted_core_hours = Column(Boolean, default=False) - discussion_emails = Column(String) - completed_discussion = Column(Boolean, default=False) - read_handbook = Column(Boolean, default=False) - training_password = Column(String) - selling_merch = Column(Choice(c.MIVS_MERCH_OPTS), nullable=True) - needs_hotel_space = Column(Boolean, nullable=True, admin_only=True) # "Admin only" preserves null default - name_for_hotel = Column(String) - email_for_hotel = Column(String) - contact_phone = Column(String) - show_info_updated = Column(Boolean, default=False) - logistics_updated = Column(Boolean, default=False) - - games = relationship( - 'IndieGame', backref='studio', order_by='IndieGame.title') - developers = relationship( - 'IndieDeveloper', - backref='studio', - order_by='IndieDeveloper.last_name') - - email_model_name = 'studio' +class IndieStudio(MagModel, table=True): + group_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='group.id', nullable=True, unique=True) + group: 'Group' = Relationship(back_populates="studio", sa_relationship_kwargs={'single_parent': True}) + + name: str = Field(default='', unique=True) + website: str = '' + other_links: str = Field(sa_type=UniqueList, default='') + + status: int = Field(sa_column=Choice(c.MIVS_STUDIO_STATUS_OPTS), default=c.NEW) + staff_notes: str = '' + registered: datetime = Field(sa_type=DateTime(timezone=True), default_factory=lambda: datetime.now(UTC)) + + accepted_core_hours: bool = False + discussion_emails: str = '' + completed_discussion: bool = False + read_handbook: bool = False + training_password: str = '' + selling_merch: int | None = Field(sa_column=Column(Choice(c.MIVS_MERCH_OPTS), nullable=True)) + needs_hotel_space: bool | None = Field(nullable=True) + name_for_hotel: str = '' + email_for_hotel: str = '' + contact_phone: str = '' + show_info_updated: bool = False + logistics_updated: bool = False + + games: list['IndieGame'] = Relationship( + back_populates="studio", + sa_relationship_kwargs={'lazy': 'selectin', 'order_by': 'IndieGame.title', 'cascade': 'all,delete-orphan', 'passive_deletes': True}) + developers: list['IndieDeveloper'] = Relationship( + back_populates="studio", + sa_relationship_kwargs={'order_by': 'IndieDeveloper.last_name', 'cascade': 'all,delete-orphan', 'passive_deletes': True}) + + email_model_name: ClassVar = 'studio' @property def primary_contact_first_names(self): @@ -281,11 +287,11 @@ def website_href(self): @property def email(self): - return [dev.email_to_address for dev in self.developers if dev.gets_emails] + return [dev.email_to_address for dev in self.developers if dev.receives_emails] @property def primary_contacts(self): - return [dev for dev in self.developers if dev.gets_emails] + return [dev for dev in self.developers if dev.receives_emails] @property def submitted_games(self): @@ -311,17 +317,22 @@ def unclaimed_badges(self): return max(0, self.comped_badges - claimed_count) -class IndieDeveloper(MagModel): - studio_id = Column(Uuid(as_uuid=False), ForeignKey('indie_studio.id')) - attendee_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id'), nullable=True) +class IndieDeveloper(MagModel, table=True): + studio_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='indie_studio.id', ondelete='CASCADE') + studio: 'IndieStudio' = Relationship(back_populates="developers", sa_relationship_kwargs={'lazy': 'joined'}) + + attendee_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', nullable=True) + attendee: 'Attendee' = Relationship(back_populates="indie_developer") - gets_emails = Column(Boolean, default=False) - first_name = Column(String) - last_name = Column(String) - email = Column(String) - cellphone = Column(String) - agreed_coc = Column(Boolean, default=False) - agreed_data_policy = Column(Boolean, default=False) + receives_emails: bool = False + first_name: str = '' + last_name: str = '' + email: str = '' + cellphone: str = '' + agreed_coc: bool = False + agreed_data_policy: bool = False + + arcade_games: list['IndieGame'] = Relationship(back_populates="primary_contact") @property def email_to_address(self): @@ -344,85 +355,87 @@ def matching_attendee(self): ).first() -class IndieGame(MagModel, ReviewMixin): - studio_id = Column(Uuid(as_uuid=False), ForeignKey('indie_studio.id')) - primary_contact_id = Column(Uuid(as_uuid=False), ForeignKey('indie_developer.id', ondelete='SET NULL'), nullable=True) - primary_contact = relationship(IndieDeveloper, backref='arcade_games', - foreign_keys=primary_contact_id, cascade='save-update,merge,refresh-expire,expunge') - - title = Column(String) - brief_description = Column(String) - description = Column(String) - genres = Column(MultiChoice(c.MIVS_GENRE_OPTS)) - genres_text = Column(String) - platforms = Column(MultiChoice(c.MIVS_PLATFORM_OPTS + c.INDIE_RETRO_PLATFORM_OPTS)) - platforms_text = Column(String) - player_count = Column(String) - how_to_play = Column(String) - link_to_video = Column(String) - link_to_game = Column(String) - - requires_gamepad = Column(Boolean, default=False) - is_alumni = Column(Boolean, default=False) - content_warning = Column(Boolean, default=False) - warning_desc = Column(String) - photosensitive_warning = Column(Boolean, default=False) - has_multiplayer = Column(Boolean, default=False) - password_to_game = Column(String) - code_type = Column(Choice(c.MIVS_CODE_TYPE_OPTS), default=c.NO_CODE) - code_instructions = Column(String) - build_status = Column( - Choice(c.MIVS_BUILD_STATUS_OPTS), default=c.PRE_ALPHA) - build_notes = Column(String) - - game_hours = Column(String) - game_hours_text = Column(String) - game_end_time = Column(Boolean, default=False) - floorspace = Column(Choice(c.INDIE_ARCADE_FLOORSPACE_OPTS), nullable=True) - floorspace_text = Column(String) - cabinet_type = Column(Choice(c.INDIE_ARCADE_CABINET_OPTS), nullable=True) - cabinet_type_text = Column(String) - sanitation_requests = Column(String) - transit_needs = Column(String) - found_how = Column(String) - read_faq = Column(String) - mailing_list = Column(Boolean, default=False) - - publisher_name = Column(String) - release_date = Column(String) - other_assets = Column(String) - in_person = Column(Boolean, default=False) - delivery_method = Column(Choice(c.INDIE_RETRO_DELIVERY_OPTS), nullable=True) - - agreed_liability = Column(Boolean, default=False) - agreed_showtimes = Column(Boolean, default=False) - agreed_equipment = Column(Boolean, default=False) - - link_to_promo_video = Column(String) - link_to_webpage = Column(String) - link_to_store = Column(String) - other_social_media = Column(String) - - tournament_at_event = Column(Boolean, default=False) - tournament_prizes = Column(String) - multiplayer_game_length = Column(Integer, nullable=True) # Length in minutes - leaderboard_challenge = Column(Boolean, default=False) - - showcase_type = Column(Choice(c.SHOWCASE_GAME_TYPE_OPTS), default=c.MIVS) - submitted = Column(Boolean, default=False) - status = Column( - Choice(c.MIVS_GAME_STATUS_OPTS), default=c.NEW, admin_only=True) - judge_notes = Column(String, admin_only=True) - registered = Column(DateTime(timezone=True), server_default=utcnow(), default=lambda: datetime.now(UTC)) - waitlisted = Column(DateTime(timezone=True), nullable=True) - accepted = Column(DateTime(timezone=True), nullable=True) - - codes = relationship('IndieGameCode', backref='game') - reviews = relationship('IndieGameReview', backref='game') - images = relationship( - 'IndieGameImage', backref='game', order_by='IndieGameImage.id') - - email_model_name = 'game' +class IndieGame(MagModel, ReviewMixin, table=True): + studio_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='indie_studio.id', ondelete='CASCADE') + studio: 'IndieStudio' = Relationship(back_populates="games", sa_relationship_kwargs={'lazy': 'joined'}) + + primary_contact_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='indie_developer.id', nullable=True) + primary_contact: 'IndieDeveloper' = Relationship(back_populates="arcade_games", sa_relationship_kwargs={'lazy': 'joined'}) + + title: str = '' + brief_description: str = '' + description: str = '' + genres: str = Field(sa_column=Column(MultiChoice(c.MIVS_GENRE_OPTS)), default='') + genres_text: str = '' + platforms: str = Field(sa_column=Column(MultiChoice(c.MIVS_PLATFORM_OPTS + c.INDIE_RETRO_PLATFORM_OPTS)), default='') + platforms_text: str = '' + player_count: str = '' + how_to_play: str = '' + link_to_video: str = '' + link_to_game: str = '' + + requires_gamepad: bool = False + is_alumni: bool = False + content_warning: bool = False + warning_desc: str = '' + photosensitive_warning: bool = False + has_multiplayer: bool = False + password_to_game: str = '' + code_type: int = Field(sa_column=Column(Choice(c.MIVS_CODE_TYPE_OPTS)), default=c.NO_CODE) + code_instructions: str = '' + build_status: int = Field(sa_column=Column(Choice(c.MIVS_BUILD_STATUS_OPTS)), default=c.PRE_ALPHA) + build_notes: str = '' + + game_hours: str = '' + game_hours_text: str = '' + game_end_time: bool = False + floorspace: int | None = Field(sa_column=Column(Choice(c.INDIE_ARCADE_FLOORSPACE_OPTS), nullable=True)) + floorspace_text: str = '' + cabinet_type: int | None = Field(sa_column=Column(Choice(c.INDIE_ARCADE_CABINET_OPTS), nullable=True)) + cabinet_type_text: str = '' + sanitation_requests: str = '' + transit_needs: str = '' + found_how: str = '' + read_faq: str = '' + mailing_list: bool = False + + publisher_name: str = '' + release_date: str = '' + other_assets: str = '' + in_person: bool = False + delivery_method: int | None = Field(sa_column=Column(Choice(c.INDIE_RETRO_DELIVERY_OPTS), nullable=True)) + + agreed_liability: bool = False + agreed_showtimes: bool = False + agreed_equipment: bool = False + + link_to_promo_video: str = '' + link_to_webpage: str = '' + link_to_store: str = '' + other_social_media: str = '' + + tournament_at_event: bool = False + tournament_prizes: str = '' + multiplayer_game_length: int = Field(nullable=True) # Length in minutes + leaderboard_challenge: bool = False + + showcase_type: int = Field(sa_column=Column(Choice(c.SHOWCASE_GAME_TYPE_OPTS), default=c.MIVS)) + submitted: bool = False + status: int = Field(sa_column=Column(Choice(c.MIVS_GAME_STATUS_OPTS), default=c.NEW)) + judge_notes: str = '' + registered: datetime = Field(sa_type=DateTime(timezone=True), default_factory=lambda: datetime.now(UTC)) + waitlisted: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True) + accepted: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True) + + codes: list['IndieGameCode'] = Relationship( + back_populates="game", sa_relationship_kwargs={'lazy': 'selectin', 'cascade': 'all,delete-orphan', 'passive_deletes': True}) + reviews: list['IndieGameReview'] = Relationship( + back_populates="game", sa_relationship_kwargs={'cascade': 'all,delete-orphan', 'passive_deletes': True}) + images: list['IndieGameImage'] = Relationship( + back_populates="game", sa_relationship_kwargs={'lazy': 'selectin', 'order_by': 'IndieGameImage.id', + 'cascade': 'all,delete-orphan', 'passive_deletes': True}) + + email_model_name: ClassVar = 'game' @presave_adjustment def accepted_time(self): @@ -655,11 +668,13 @@ def guidebook_images(self): return [header_name, thumbnail_name], [header, thumbnail] -class IndieGameImage(MagModel, GuidebookImageMixin): - game_id = Column(Uuid(as_uuid=False), ForeignKey('indie_game.id')) - description = Column(String) - use_in_promo = Column(Boolean, default=False) - is_screenshot = Column(Boolean, default=True) +class IndieGameImage(MagModel, GuidebookImageMixin, table=True): + game_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='indie_game.id', ondelete='CASCADE') + game: 'IndieGame' = Relationship(back_populates="images", sa_relationship_kwargs={'lazy': 'joined'}) + + description: str = '' + use_in_promo: bool = False + is_screenshot: bool = True @property def image(self): @@ -695,39 +710,45 @@ def filepath(self): return os.path.join(c.MIVS_GAME_IMAGE_DIR, str(self.id)) -class IndieGameCode(MagModel): - game_id = Column(Uuid(as_uuid=False), ForeignKey('indie_game.id')) - judge_id = Column(Uuid(as_uuid=False), ForeignKey('indie_judge.id'), nullable=True) - code = Column(String) - unlimited_use = Column(Boolean, default=False) - judge_notes = Column(String, admin_only=True) # TODO: Remove? +class IndieGameCode(MagModel, table=True): + game_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='indie_game.id', ondelete='CASCADE') + game: 'IndieGame' = Relationship(back_populates="codes", sa_relationship_kwargs={'lazy': 'joined'}) + + judge_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='indie_judge.id', nullable=True) + judge: 'IndieJudge' = Relationship(back_populates="codes", sa_relationship_kwargs={'lazy': 'joined'}) + + code: str = '' + unlimited_use: bool = False + judge_notes: str = '' @property def type_label(self): return 'Unlimited-Use' if self.unlimited_use else 'Single-Person' -class IndieGameReview(MagModel): - game_id = Column(Uuid(as_uuid=False), ForeignKey('indie_game.id')) - judge_id = Column(Uuid(as_uuid=False), ForeignKey('indie_judge.id')) - video_status = Column( - Choice(c.MIVS_VIDEO_REVIEW_STATUS_OPTS), default=c.PENDING) - game_status = Column( - Choice(c.MIVS_GAME_REVIEW_STATUS_OPTS), default=c.PENDING) - game_status_text = Column(String) - game_content_bad = Column(Boolean, default=False) - read_how_to_play = Column(Boolean, default=False) +class IndieGameReview(MagModel, table=True): + game_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='indie_game.id', ondelete='CASCADE') + game: 'IndieGame' = Relationship(back_populates="reviews", sa_relationship_kwargs={'lazy': 'joined'}) + + judge_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='indie_judge.id', ondelete='CASCADE') + judge: 'IndieJudge' = Relationship(back_populates="reviews", sa_relationship_kwargs={'lazy': 'joined'}) + + video_status: int = Field(sa_column=Column(Choice(c.MIVS_VIDEO_REVIEW_STATUS_OPTS), default=c.PENDING)) + game_status: int = Field(sa_column=Column(Choice(c.MIVS_GAME_REVIEW_STATUS_OPTS), default=c.PENDING)) + game_status_text: str = '' + game_content_bad: bool = False + read_how_to_play: bool = False # 0 = not reviewed, 1-10 score (10 is best) - readiness_score = Column(Integer, default=0) - design_score = Column(Integer, default=0) - enjoyment_score = Column(Integer, default=0) - game_review = Column(String) - developer_response = Column(String) - staff_notes = Column(String) - send_to_studio = Column(Boolean, default=False) - - __table_args__ = ( + readiness_score: int = 0 + design_score: int = 0 + enjoyment_score: int = 0 + game_review: str = '' + developer_response: str = '' + staff_notes: str = '' + send_to_studio: bool = False + + __table_args__: ClassVar = ( UniqueConstraint('game_id', 'judge_id', name='review_game_judge_uniq'), ) diff --git a/uber/models/tabletop.py b/uber/models/tabletop.py index 510616649..3928f48ca 100644 --- a/uber/models/tabletop.py +++ b/uber/models/tabletop.py @@ -1,24 +1,29 @@ from datetime import datetime from pytz import UTC -from sqlalchemy.schema import ForeignKey from sqlalchemy.types import Boolean, DateTime, String, Uuid +from typing import ClassVar from uber.models import MagModel -from uber.models.types import default_relationship as relationship, DefaultColumn as Column +from uber.models.types import DefaultField as Field, DefaultRelationship as Relationship __all__ = ['TabletopGame', 'TabletopCheckout'] -class TabletopGame(MagModel): - code = Column(String) - name = Column(String) - attendee_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id')) - returned = Column(Boolean, default=False) - checkouts = relationship('TabletopCheckout', order_by='TabletopCheckout.checked_out', backref='game') +class TabletopGame(MagModel, table=True): + attendee_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', ondelete='CASCADE') + attendee: 'Attendee' = Relationship(back_populates="games") - _repr_attr_names = ['name'] + code: str = '' + name: str = '' + returned: bool = False + + checkouts: list['TabletopCheckout'] = Relationship( + back_populates="game", sa_relationship_kwargs={'order_by': 'TabletopCheckout.checked_out', + 'cascade': 'all,delete-orphan', 'passive_deletes': True}) + + _repr_attr_names: ClassVar = ['name'] @property def checked_out(self): @@ -28,8 +33,12 @@ def checked_out(self): pass -class TabletopCheckout(MagModel): - game_id = Column(Uuid(as_uuid=False), ForeignKey('tabletop_game.id')) - attendee_id = Column(Uuid(as_uuid=False), ForeignKey('attendee.id')) - checked_out = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC)) - returned = Column(DateTime(timezone=True), nullable=True) +class TabletopCheckout(MagModel, table=True): + game_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='tabletop_game.id', ondelete='CASCADE') + game: 'TabletopGame' = Relationship(back_populates="checkouts", sa_relationship_kwargs={'lazy': 'joined'}) + + attendee_id: str | None = Field(sa_type=Uuid(as_uuid=False), foreign_key='attendee.id', ondelete='CASCADE') + attendee: 'Attendee' = Relationship(back_populates="checkouts", sa_relationship_kwargs={'lazy': 'joined'}) + + checked_out: datetime = Field(sa_type=DateTime(timezone=True), default_factory=lambda: datetime.now(UTC)) + returned: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True) diff --git a/uber/models/tracking.py b/uber/models/tracking.py index 72a5257a9..5decb82fc 100644 --- a/uber/models/tracking.py +++ b/uber/models/tracking.py @@ -15,6 +15,7 @@ from sqlalchemy.dialects.postgresql.json import JSONB from sqlalchemy.ext.mutable import MutableDict from sqlalchemy.orm.exc import NoResultFound +from typing import Any, ClassVar from uber.serializer import serializer from uber.config import c @@ -22,7 +23,7 @@ from uber.models import MagModel from uber.models.admin import AdminAccount from uber.models.email import Email -from uber.models.types import Choice, DefaultColumn as Column, MultiChoice, utcnow +from uber.models.types import Choice, DefaultColumn as Column, MultiChoice, utcnow, DefaultField as Field log = logging.getLogger(__name__) @@ -31,12 +32,12 @@ serializer.register(associationproxy._AssociationList, list) -class ReportTracking(MagModel): - when = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC)) - who = Column(String) - supervisor = Column(String) - page = Column(String) - params = Column(MutableDict.as_mutable(JSONB), default={}) +class ReportTracking(MagModel, table=True): + when: datetime = Field(sa_type=DateTime(timezone=True), default_factory=lambda: datetime.now(UTC)) + who: str = '' + supervisor: str = '' + page: str = '' + params: dict[str, Any] = Field(sa_type=MutableDict.as_mutable(JSONB), default_factory=dict) @property def who_repr(self): @@ -56,12 +57,12 @@ def track_report(cls, params): session.commit() -class PageViewTracking(MagModel): - when = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC)) - who = Column(String) - supervisor = Column(String) - page = Column(String) - which = Column(String) +class PageViewTracking(MagModel, table=True): + when: datetime = Field(sa_type=DateTime(timezone=True), default_factory=lambda: datetime.now(UTC)) + who: str = '' + supervisor: str = '' + page: str = '' + which: str = '' @property def who_repr(self): @@ -78,28 +79,28 @@ def track_pageview(cls): which = "Budget page" else: # Only log the page view if there's a valid model ID - if 'id' not in params or params['id'] == 'None': + if 'id' not in params or params['id'] in [None, '', 'None']: return - from uber.models import Session - with Session() as session: - # Get instance repr - model = None - id = params.get('id') - try: - model = session.attendee(id) - except NoResultFound: + from uber.models import Session + with Session() as session: + # Get instance repr + model = None + id = params.get('id') try: - model = session.group(id) + model = session.attendee(id) except NoResultFound: try: - model = session.art_show_application(id) + model = session.group(id) except NoResultFound: - pass - if model: - which = repr(model) - else: - return + try: + model = session.art_show_application(id) + except NoResultFound: + pass + if model: + which = repr(model) + else: + return session.add(PageViewTracking( who=AdminAccount.admin_or_volunteer_name(), @@ -108,18 +109,18 @@ def track_pageview(cls): session.commit() -class Tracking(MagModel): - fk_id = Column(Uuid(as_uuid=False), index=True) - model = Column(String) - when = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC), index=True) - who = Column(String, index=True) - supervisor = Column(String) - page = Column(String) - which = Column(String) - links = Column(String) - action = Column(Choice(c.TRACKING_OPTS)) - data = Column(String) - snapshot = Column(String) +class Tracking(MagModel, table=True): + fk_id: str = Field(sa_type=Uuid(as_uuid=False), index=True) + model: str = '' + when: datetime = Field(sa_type=DateTime(timezone=True), default_factory=lambda: datetime.now(UTC), index=True) + who: str = Field(default='', index=True) + supervisor: str = '' + page: str = '' + which: str = '' + links: str = '' + action: int = Field(sa_column=Column(Choice(c.TRACKING_OPTS))) + data: str = '' + snapshot: str = '' @property def who_repr(self): @@ -283,18 +284,18 @@ def _insert(session): _insert(session) -class TxnRequestTracking(MagModel): - incr_id_seq = Sequence('txn_request_tracking_incr_id_seq') - incr_id = Column(Integer, incr_id_seq, server_default=incr_id_seq.next_value(), unique=True) - fk_id = Column(Uuid(as_uuid=False), nullable=True) - workstation_num = Column(Integer, default=0) - terminal_id = Column(String) - who = Column(String) - requested = Column(DateTime(timezone=True), server_default=utcnow(), default=lambda: datetime.now(UTC)) - resolved = Column(DateTime(timezone=True), nullable=True) - success = Column(Boolean, default=False) - response = Column(MutableDict.as_mutable(JSONB), default={}) - internal_error = Column(String) +class TxnRequestTracking(MagModel, table=True): + incr_id_seq: ClassVar = Sequence('txn_request_tracking_incr_id_seq') + incr_id: int = Field(sa_column=Column(Integer, incr_id_seq, server_default=incr_id_seq.next_value(), unique=True)) + fk_id: str | None = Field(sa_type=Uuid(as_uuid=False), nullable=True) + workstation_num: int = 0 + terminal_id: str = '' + who: str = '' + requested: datetime = Field(sa_type=DateTime(timezone=True), default_factory=lambda: datetime.now(UTC)) + resolved: datetime | None = Field(sa_type=DateTime(timezone=True), nullable=True) + success: bool = False + response: dict[Any, Any] = Field(sa_type=MutableDict.as_mutable(JSONB), default_factory=dict) + internal_error: str = '' @presave_adjustment def log_internal_error(self): diff --git a/uber/models/types.py b/uber/models/types.py index 1fbf2cb74..00cee6e45 100644 --- a/uber/models/types.py +++ b/uber/models/types.py @@ -12,13 +12,14 @@ from sqlalchemy.schema import Column from sqlalchemy.sql.expression import FunctionElement from sqlalchemy.types import Boolean, Integer, TypeDecorator, String, DateTime, Uuid, JSON +from sqlmodel import Field, Relationship from uber.config import c, _config as config -from uber.utils import url_domain, listify, camel +from uber.utils import listify __all__ = [ 'default_relationship', 'relationship', 'utcmin', 'utcnow', 'Choice', - 'Column', 'DefaultColumn', 'MultiChoice', + 'Column', 'DefaultColumn', 'DefaultField', 'DefaultRelationship', 'MultiChoice', 'TakesPaymentMixin', 'GuidebookImageMixin'] @@ -52,13 +53,55 @@ def DefaultColumn(*args, admin_only=False, private=False, **kwargs): return col +def DefaultField(*args, admin_only=False, private=False, **kwargs): + """ + Returns a SQLModel Field with the given parameters, except that instead + of the regular defaults, we've overridden the following defaults if no + value is provided for the following parameters: + + Field Old Default New Default + ----- ------------ ----------- + nullable True False + default None '' (only for String fields) + server_default None + + We also have an "admin_only" parameter, which is set as an attribute on + the column instance, indicating whether the column should be settable by + regular attendees filling out one of the registration forms or if only a + logged-in admin user should be able to set it. This is deprecated but + kept for compatibility with older forms. + """ + sa_column = kwargs.get('sa_column', None) + if sa_column is not None: + # DefaultColumn does what we need here + return SQLModelField(*args, **kwargs) + + sa_kwargs = kwargs.pop('sa_column_kwargs', {}) + + # Not a typo -- some of our kwargs are handled by SQLModel.Field + # and others are stored in a dictionary that gets passed to SQLAlchemy + kwargs.setdefault('nullable', False) + col_type = kwargs.get('sa_type', None) + if col_type is String or isinstance(col_type, (String, MultiChoice)): + kwargs.setdefault('default', '') + + default = kwargs.get('default') + if isinstance(default, (int, str)): + sa_kwargs.setdefault('server_default', str(default)) + + col = SQLModelField(*args, **kwargs) + col.admin_only = admin_only + col.private = private + return col + + def default_relationship(*args, **kwargs): """ Returns a SQLAlchemy relationship with the given parameters, except that instead of the regular defaults, we've overridden the following defaults if no value is provided for the following parameters: load_on_pending now defaults to True - cascade now defaults to 'all,delete-orphan' + cascade now defaults to 'save-update,merge,refresh-expire,expunge' """ kwargs.setdefault('load_on_pending', True) if kwargs.get("viewonly", False): @@ -66,17 +109,43 @@ def default_relationship(*args, **kwargs): # on viewonly relationships. kwargs.setdefault('cascade', 'expunge,refresh-expire,merge') else: - kwargs.setdefault('cascade', 'all,delete-orphan') + kwargs.setdefault('cascade', 'save-update,merge,refresh-expire,expunge') + #kwargs.setdefault('lazy', 'raise') return SQLAlchemy_relationship(*args, **kwargs) +def DefaultRelationship(*args, **kwargs): + """ + Returns a SQLModel Relationship with the given parameters, except that + instead of the regular defaults, we've overridden the following defaults + if no value is provided for the following parameters: + cascade now defaults to 'save-update,merge,refresh-expire,expunge' + """ + sa_relationship = kwargs.get('sa_relationship', None) + if sa_relationship is not None: + # default_relationship does what we need here + return SQLModelRelationship(*args, **kwargs) + + sa_kwargs = kwargs.pop('sa_relationship_kwargs', {}) + if sa_kwargs.get("viewonly", False): + # Recent versions of SQLAlchemy won't allow cascades that cause writes + # on viewonly relationships. + sa_kwargs.setdefault('cascade', 'expunge,refresh-expire,merge') + else: + sa_kwargs.setdefault('cascade', 'save-update,merge,refresh-expire,expunge') + #sa_kwargs.setdefault('lazy', 'raise') + return SQLModelRelationship(*args, **kwargs, sa_relationship_kwargs=sa_kwargs) + + # Alias Column and relationship to maintain backwards compatibility class SQLAlchemy_Column(Column): admin_only = None Column = DefaultColumn -SQLAlchemy_relationship, relationship = relationship, default_relationship +SQLModelField, Field = Field, DefaultField +SQLAlchemy_relationship, relationship = relationship, default_relationship +SQLModelRelationship, Relationship = Relationship, DefaultRelationship class utcmax(FunctionElement): """ @@ -293,11 +362,11 @@ def convert_if_labels(self, value): class GuidebookImageMixin(): - filename = Column(String) - content_type = Column(String) - extension = Column(String) - is_header = Column(Boolean, default=False) - is_thumbnail = Column(Boolean, default=False) + filename: str = Column(String) + content_type: str = Column(String) + extension: str = Column(String) + is_header: bool = Column(Boolean, default=False) + is_thumbnail: bool = Column(Boolean, default=False) @property def url(self): diff --git a/uber/payments.py b/uber/payments.py index fcdaf11ba..f171eca90 100644 --- a/uber/payments.py +++ b/uber/payments.py @@ -5,6 +5,7 @@ from datetime import datetime, timedelta from dateutil.parser import parse from uuid import uuid4 +from sqlalchemy.orm import selectinload import logging import cherrypy @@ -14,7 +15,7 @@ import uber from uber.config import c from uber.custom_tags import format_currency, email_only -from uber.utils import report_critical_exception, listify +from uber.utils import report_critical_exception, listify, is_listy import uber.spin_rest_utils as spin_rest_utils from uber.decorators import cached_property, classproperty @@ -53,6 +54,7 @@ class PreregCart: the payment process is started. This class helps manage them in the session instead. """ def __init__(self, targets=()): + log.error(targets) self._targets = listify(targets) self._current_cost = 0 @@ -119,7 +121,7 @@ def get_unpaid_promo_code_uses_count(cls, id, already_counted_attendee_ids=None) @classmethod def to_sessionized(cls, m, **params): from uber.models import Attendee, Group - if isinstance(m, Iterable): + if is_listy(m): return [cls.to_sessionized(t) for t in m] elif isinstance(m, dict): return m @@ -147,7 +149,7 @@ def to_sessionized(cls, m, **params): @classmethod def from_sessionized(cls, d): - if isinstance(d, Iterable): + if is_listy(d): return [cls.from_sessionized(t) for t in d] elif isinstance(d, dict): assert d['_model'] in {'Attendee', 'Group'} @@ -723,7 +725,7 @@ def send_authorizenet_txn(self, txn_type=c.AUTHCAPTURE, **params): def log_authorizenet_response(self, intent_id, txn_info, card_info): from uber.models import ReceiptInfo, ReceiptTransaction, Session - session = Session().session + session = Session() matching_txns = session.query(ReceiptTransaction).filter_by(intent_id=intent_id).all() # AuthNet returns "StringElement" but we want strings @@ -1059,7 +1061,7 @@ def process_spin_refund(self, department=None): return_response_json = return_response.json() self.tracker.response = return_response_json - self.tracker.resolved = datetime.utcnow() + self.tracker.resolved = datetime.now(UTC) self.spin_request.log_api_response(return_response_json) @@ -1167,7 +1169,7 @@ def process_sale_response(self, session, response): except AttributeError: response_json = response self.tracker.response = response_json - self.tracker.resolved = datetime.utcnow() + self.tracker.resolved = datetime.now(UTC) receipt_items_to_add = self.get_receipt_items_to_add() if receipt_items_to_add: @@ -1191,7 +1193,7 @@ def process_sale_response(self, session, response): if self.tracker: self.tracker.response = void_response_json - self.tracker.resolved = datetime.utcnow() + self.tracker.resolved = datetime.now(UTC) self.log_api_response(void_response_json) if self.api_response_successful(void_response_json): @@ -1808,9 +1810,11 @@ def mark_paid_from_ids(intent_id, charge_id, put_on_hold=False): from uber.tasks.email import send_email from uber.decorators import render - session = Session().session - matching_txns = session.query(ReceiptTransaction).filter_by(intent_id=intent_id).filter( - ReceiptTransaction.charge_id == '').all() + session = Session() + matching_txns = session.query(ReceiptTransaction).filter( + ReceiptTransaction.intent_id == intent_id, + ReceiptTransaction.charge_id == '').options( + selectinload(ReceiptTransaction.receipt_items)).all() if not matching_txns: log.debug(f"Tried to mark payments with intent ID {intent_id} as paid but we couldn't find any!") diff --git a/uber/redis_session.py b/uber/redis_session.py index 58956db65..1d85eb009 100644 --- a/uber/redis_session.py +++ b/uber/redis_session.py @@ -59,6 +59,10 @@ def _load(self): except TypeError: # if id not defined pickle can't load None and raise TypeError return None + except Exception as e: + # Keep the entire thread from getting stuck + self._delete() + raise e def _save(self, expiration_time): pickled_data = pickle.dumps( diff --git a/uber/site_sections/accounts.py b/uber/site_sections/accounts.py index 096407022..0485e8bf5 100644 --- a/uber/site_sections/accounts.py +++ b/uber/site_sections/accounts.py @@ -2,7 +2,7 @@ import bcrypt import cherrypy -from sqlalchemy.orm import subqueryload +from sqlalchemy.orm import joinedload, selectinload from sqlalchemy.orm.exc import NoResultFound from uber.config import c @@ -46,7 +46,7 @@ def index(self, session, message=''): 'message': message, 'accounts': (session.query(AdminAccount) .join(Attendee) - .options(subqueryload(AdminAccount.attendee).subqueryload(Attendee.assigned_depts)) + .options(joinedload(AdminAccount.attendee).selectinload(Attendee.assigned_depts)) .order_by(Attendee.last_first).all()), 'all_attendees': attendees, } @@ -112,7 +112,9 @@ def delete(self, session, id, **params): def bulk(self, session, department_id=None, **params): department_id = None if department_id == 'All' else department_id attendee_filters = [Attendee.dept_memberships.any(department_id=department_id)] if department_id else [] - attendees = session.staffers().filter(*attendee_filters).all() + attendees = session.staffers().filter(*attendee_filters).options( + selectinload(Attendee.dept_memberships_with_role), joinedload(Attendee.admin_account) + ).all() for attendee in attendees: attendee.trusted_here = attendee.trusted_in(department_id) if department_id else attendee.has_role_somewhere attendee.hours_here = attendee.weighted_hours_in(department_id) diff --git a/uber/site_sections/api.py b/uber/site_sections/api.py index 444da0204..3ce3751a9 100644 --- a/uber/site_sections/api.py +++ b/uber/site_sections/api.py @@ -5,7 +5,7 @@ import pytz import inspect import stripe -from sqlalchemy.orm import subqueryload +from sqlalchemy.orm import joinedload from uber.config import c from uber.decorators import ajax, all_renderable, not_site_mappable, public, site_mappable @@ -26,8 +26,8 @@ def index(self, session, show_revoked=False, message='', **params): if not show_revoked: api_tokens = api_tokens.filter(ApiToken.revoked_time == None) # noqa: E711 api_tokens = api_tokens.options( - subqueryload(ApiToken.admin_account) - .subqueryload(AdminAccount.attendee)) \ + joinedload(ApiToken.admin_account) + .selectinload(AdminAccount.attendee)) \ .order_by(ApiToken.issued_time).all() return { 'message': message, diff --git a/uber/site_sections/art_show_admin.py b/uber/site_sections/art_show_admin.py index e74d6bae8..b25328f3c 100644 --- a/uber/site_sections/art_show_admin.py +++ b/uber/site_sections/art_show_admin.py @@ -12,7 +12,7 @@ from datetime import datetime from decimal import Decimal from sqlalchemy import or_, and_ -from sqlalchemy.orm import joinedload, contains_eager +from sqlalchemy.orm import joinedload, selectinload from sqlalchemy.orm.exc import NoResultFound from io import BytesIO @@ -35,18 +35,18 @@ class Root: def index(self, session, message=''): return { 'message': message, - 'applications': session.query(ArtShowApplication).options(joinedload(ArtShowApplication.attendee)) + 'applications': session.query(ArtShowApplication).options(joinedload(ArtShowApplication.active_receipt)) } def form(self, session, new_app='', message='', **params): if new_app and 'attendee_id' in params: - app = session.art_show_application(params, ignore_csrf=True, bools=['us_only']) + app = ArtShowApplication(attendee_id = params['attendee_id']) else: + app = session.query(ArtShowApplication).filter(ArtShowApplication.id == params['id']).options( + selectinload(ArtShowApplication.art_show_pieces), joinedload(ArtShowApplication.active_receipt)).one() if cherrypy.request.method == 'POST' and params.get('id') not in [None, '', 'None']: - app = session.art_show_application(params.get('id')) - receipt_items = ReceiptManager.auto_update_receipt(app, session.get_receipt_by_model(app), params.copy()) + receipt_items = ReceiptManager.auto_update_receipt(app, app.active_receipt, params.copy()) session.add_all(receipt_items) - app = session.art_show_application(params, bools=['us_only']) attendee = None app_paid = 0 if new_app else app.amount_paid @@ -71,7 +71,6 @@ def form(self, session, new_app='', message='', **params): session.attendee_from_art_show_app(**params) else: attendee = app.attendee - message = message or check(app) if not message: if attendee: @@ -121,7 +120,8 @@ def validate_app(self, session, form_list=[], **params): return {"success": True} def pieces(self, session, id, message=''): - app = session.art_show_application(id) + app = session.query(ArtShowApplication).filter(ArtShowApplication.id == id).options( + selectinload(ArtShowApplication.art_show_pieces)).one() return { 'app': app, 'message': message, @@ -250,12 +250,12 @@ def close_out(self, session, message='', piece_code='', bidder_num='', winning_b message = "ERROR: This bidder number does not have an attendee attached so we cannot sell anything to them." if found_bidder and not message: - if not found_bidder.attendee.art_show_receipt: + receipt = session.query(ArtShowReceipt).filter( + ArtShowReceipt.attendee_id == found_bidder.attendee.id).first() + if not receipt: receipt = ArtShowReceipt(attendee=found_bidder.attendee) session.add(receipt) session.commit() - else: - receipt = found_bidder.attendee.art_show_receipt if not message: found_piece.status = c.SOLD @@ -311,7 +311,7 @@ def artist_check_in_out(self, session, checkout=False, hanging=False, mailin=Fal applications = session.query(ArtShowApplication).join(ArtShowApplication.attendee)\ .filter(*filters).filter(or_(*search_filters))\ .order_by(Attendee.first_name.desc() if '-' in str(order) else Attendee.first_name).options( - joinedload(ArtShowApplication.art_show_pieces)) + selectinload(ArtShowApplication.art_show_pieces)) count = applications.count() page = int(page) or 1 @@ -358,7 +358,8 @@ def artist_check_in_out(self, session, checkout=False, hanging=False, mailin=Fal @public def print_check_in_out_form(self, session, id, checkout='', **params): - app = session.art_show_application(id) + app = session.query(ArtShowApplication).filter(ArtShowApplication.id == id).options( + selectinload(ArtShowApplication.art_show_pieces)).one() attendee = app.attendee # We want to always use these properties for the printed forms as they have useful fallbacks @@ -581,7 +582,9 @@ def assignment_map(self, session, message='', gallery=c.GENERAL, surface_type=c. artists_json = [] valid_panel_ids = [] - valid_apps = session.query(ArtShowApplication).filter(ArtShowApplication.status == c.APPROVED) + valid_apps = session.query(ArtShowApplication).filter(ArtShowApplication.status == c.APPROVED).options( + selectinload(ArtShowApplication.assignments) + ) panels = session.query(ArtShowPanel).filter(ArtShowPanel.gallery == gallery, ArtShowPanel.surface_type == surface_type) for panel in panels: @@ -692,8 +695,7 @@ def check_new_assignments(origin, terminus, panel): # Update/remove existing panel assignments for assignment in session.query(ArtPanelAssignment).join(ArtPanelAssignment.panel - ).filter(ArtShowPanel.gallery == gallery, ArtShowPanel.surface_type == surface_type - ).options(contains_eager(ArtPanelAssignment.panel)): + ).filter(ArtShowPanel.gallery == gallery, ArtShowPanel.surface_type == surface_type): panel_json_str = f"{assignment.panel.origin_x}_{assignment.panel.origin_y}|{assignment.panel.terminus_x}_{assignment.panel.terminus_y}" json_str = f"{panel_json_str}|{assignment.assigned_side}" # We might have assignments uploaded with no corresponding panels @@ -771,7 +773,7 @@ def bid_sheet_barcode_generator(self, data): def bid_sheet_pdf(self, session, id, **params): import fpdf - app = session.art_show_application(id) + app = session.query(ArtShowApplication.id == id).options(selectinload(ArtShowApplication.art_show_pieces)).one() if 'piece_id' in params: pieces = [session.art_show_piece(params['piece_id'])] @@ -846,6 +848,8 @@ def bidder_signup(self, session, message='', page=1, search_text='', order=''): if error: raise HTTPRedirect('bidder_signup?search_text={}&order={}&message={}' ).format(search_text, order, error) + else: + attendees = attendees.options(joinedload(Attendee.art_show_bidder)) else: # For systems that run registration, search is limited for data privacy try: @@ -857,7 +861,8 @@ def bidder_signup(self, session, message='', page=1, search_text='', order=''): and_(Attendee.art_show_bidder != None, ArtShowBidder.bidder_num.ilike('%{search_text}%')))) attendees = session.query(Attendee).join(BadgeInfo).outerjoin( - ArtShowBidder).filter(*filters).filter(Attendee.is_valid == True) # noqa: E712 + ArtShowBidder).filter(*filters).filter(Attendee.is_valid == True).options( # noqa: E712 + joinedload(Attendee.art_show_bidder)) else: attendees = session.query(Attendee).join(Attendee.art_show_bidder) @@ -900,7 +905,8 @@ def bidder_signup(self, session, message='', page=1, search_text='', order=''): @ajax def validate_bidder_signup(self, session, form_list=[], **params): try: - attendee = session.attendee(params['attendee_id']) + attendee = session.query(Attendee).filter( + Attendee.id == params['attendee_id']).options(joinedload(Attendee.art_show_bidder)).one() except NoResultFound: if c.INDEPENDENT_ART_SHOW: attendee = Attendee( @@ -938,7 +944,8 @@ def validate_bidder_signup(self, session, form_list=[], **params): @ajax def sign_up_bidder(self, session, **params): try: - attendee = session.attendee(params['attendee_id']) + attendee = session.query(Attendee).filter( + Attendee.id == params['attendee_id']).options(joinedload(Attendee.art_show_bidder)).one() except NoResultFound: if c.INDEPENDENT_ART_SHOW: attendee = Attendee( @@ -946,12 +953,12 @@ def sign_up_bidder(self, session, **params): placeholder=True, badge_status=c.NOT_ATTENDING, ) - session.add(attendee) else: return {'success': False, 'error': "No attendee found for this bidder!"} bidder = attendee.art_show_bidder or ArtShowBidder(attendee_id=attendee.id) attendee.art_show_bidder = bidder + session.add(attendee) success = 'Bidder updated.' signed_up_str = '' @@ -979,7 +986,8 @@ def sign_up_bidder(self, session, **params): } def print_bidder_form(self, session, attendee_id, **params): - attendee = session.attendee(attendee_id) + attendee = attendee = session.query(Attendee).filter( + Attendee.id == attendee_id).options(joinedload(Attendee.art_show_bidder)).one() bidder = attendee.art_show_bidder forms = load_forms(params, bidder, ['AdminBidderSignup'], field_prefix=attendee.id, @@ -1008,7 +1016,9 @@ def sales_search(self, session, message='', page=1, search_text='', order='badge raise HTTPRedirect('sales_search?message={}', 'Please search by bidder number or badge number.') else: filters.append(or_(BadgeInfo.ident == badge_num)) - attendees = session.query(Attendee).filter(*filters) + attendees = session.query(Attendee).filter(*filters).options( + joinedload(Attendee.art_show_bidder), + selectinload(Attendee.art_show_receipts)) else: attendees = session.query(Attendee).join(Attendee.art_show_receipts) @@ -1045,17 +1055,17 @@ def sales_search(self, session, message='', page=1, search_text='', order='badge def pieces_bought(self, session, id, search_text='', message='', **params): try: - receipt = session.art_show_receipt(id) - except Exception: - attendee = session.attendee(id) - if not attendee.art_show_receipt: - receipt = ArtShowReceipt(attendee=attendee) - session.add(receipt) - session.commit() - else: - receipt = attendee.art_show_receipt + receipt = session.query(ArtShowReceipt).filter(or_(ArtShowReceipt.id == id, + ArtShowReceipt.attendee_id == id)).one() + except NoResultFound: + attendee = session.query(Attendee).filter(Attendee.id == id).options( + selectinload(Attendee.art_show_purchases)).one() + receipt = ArtShowReceipt(attendee=attendee) + session.add(receipt) + session.commit() else: - attendee = receipt.attendee + attendee = session.query(Attendee).filter(Attendee.id == receipt.attendee_id).options( + selectinload(Attendee.art_show_purchases)).first() must_choose = False unclaimed_pieces = [] @@ -1076,12 +1086,14 @@ def pieces_bought(self, session, id, search_text='', message='', **params): else: pieces = session.query(ArtShowPiece).filter(ArtShowPiece.name.ilike('%{}%'.format(search_text))) - unclaimed_pieces = pieces.filter(ArtShowPiece.buyer == None, # noqa: E711 - ArtShowPiece.status != c.RETURN) - unclaimed_pieces = [piece for piece in unclaimed_pieces if piece.sale_price > 0] - unpaid_pieces = pieces.join(ArtShowReceipt).filter(ArtShowReceipt.closed != None, # noqa: E711 - ArtShowPiece.status != c.PAID) - unpaid_pieces = [piece for piece in unpaid_pieces if piece.sale_price > 0] + unpaid_pieces_query = pieces.join(ArtShowReceipt).filter(ArtShowReceipt.closed != None, # noqa: E711 + ArtShowPiece.status != c.PAID) + unpaid_pieces = [piece for piece in unpaid_pieces_query if piece.sale_price > 0] + + pieces = pieces.options(joinedload(ArtShowPiece.receipt)) + unclaimed_pieces_query = pieces.filter(ArtShowPiece.buyer == None, # noqa: E711 + ArtShowPiece.status != c.RETURN) + unclaimed_pieces = [piece for piece in unclaimed_pieces_query if piece.sale_price > 0] if pieces.count() == 0: message = "No pieces found with ID or title {}.".format(search_text) @@ -1135,7 +1147,7 @@ def pieces_bought(self, session, id, search_text='', message='', **params): def unclaim_piece(self, session, id, piece_id, **params): receipt = session.art_show_receipt(id) - piece = session.art_show_piece(piece_id) + piece = session.query(ArtShowPiece).filter(ArtShowPiece.id == piece_id).options(joinedload(ArtShowPiece.receipt)) if receipt.closed: raise HTTPRedirect('pieces_bought?id={}&message={}', receipt.id, diff --git a/uber/site_sections/art_show_reports.py b/uber/site_sections/art_show_reports.py index dd4a19fe9..b4cfbd109 100644 --- a/uber/site_sections/art_show_reports.py +++ b/uber/site_sections/art_show_reports.py @@ -3,7 +3,7 @@ from collections import defaultdict from sqlalchemy import func, or_, and_ -from sqlalchemy.orm import joinedload +from sqlalchemy.orm import joinedload, lazyload from uber.custom_tags import format_currency from uber.models import ArtShowApplication, ArtShowBidder, ArtShowPiece, ArtShowReceipt, Attendee, ModelReceipt @@ -252,7 +252,9 @@ def artist_receipt_discrepancies(self, session): ArtShowApplication.status == c.APPROVED ).join(ArtShowApplication.active_receipt).outerjoin(ModelReceipt.receipt_items).group_by( ModelReceipt.id).group_by(ArtShowApplication.id).having( - ArtShowApplication.true_default_cost_cents != ModelReceipt.fkless_item_total_sql) + ArtShowApplication.true_default_cost_cents != ModelReceipt.fkless_item_total_sql).options( + lazyload("*") + ) return { 'apps': apps, @@ -274,7 +276,7 @@ def artists_nonzero_balance(self, session, include_no_receipts=False, include_di ModelReceipt.receipt_txns).join(item_subquery, ArtShowApplication.id == item_subquery.c.owner_id).group_by( ModelReceipt.id).group_by(ArtShowApplication.id).group_by(item_subquery.c.item_total).having( and_((ModelReceipt.payment_total_sql - ModelReceipt.refund_total_sql) != item_subquery.c.item_total, - filter)) + filter)).options(lazyload("*")) if include_no_receipts: apps_no_receipts = session.query(ArtShowApplication).outerjoin( @@ -474,8 +476,8 @@ def bidder_csv(self, out, session): ]) for bidder in session.query(ArtShowBidder).join(ArtShowBidder.attendee): - if bidder.attendee.badge_status == c.NOT_ATTENDING and bidder.attendee.art_show_applications: - address_model = bidder.attendee.art_show_applications[0] + if bidder.attendee.badge_status == c.NOT_ATTENDING and bidder.attendee.art_show_application: + address_model = bidder.attendee.art_show_application else: address_model = bidder.attendee diff --git a/uber/site_sections/attractions_admin.py b/uber/site_sections/attractions_admin.py index 953d2892a..69f6ef5fa 100644 --- a/uber/site_sections/attractions_admin.py +++ b/uber/site_sections/attractions_admin.py @@ -4,7 +4,7 @@ import cherrypy from dateutil import parser as dateparser import pytz -from sqlalchemy.orm import subqueryload +from sqlalchemy.orm import subqueryload, joinedload, selectinload, defaultload from uber.config import c from uber.custom_tags import readable_join @@ -216,7 +216,10 @@ def form(self, session, message='', **params): if not attraction_id or attraction_id == 'None': raise HTTPRedirect('index') - attraction = session.attraction(attraction_id) + attraction = session.query(Attraction).filter(Attraction.id == attraction_id).options( + joinedload(Attraction.department), selectinload(Attraction.events), + defaultload(Attraction.features).defaultload(AttractionFeature.events).selectinload(AttractionEvent.signups), + ).first() forms = load_forms(params, attraction, ['AttractionInfo']) @@ -234,15 +237,6 @@ def form(self, session, message='', **params): 'form?id={}&message={}', attraction.id, '{} updated successfully.'.format(attraction.name)) - else: - attraction = session.query(Attraction) \ - .filter_by(id=attraction_id) \ - .options( - subqueryload(Attraction.department), - subqueryload(Attraction.features) - .subqueryload(AttractionFeature.events) - .subqueryload(AttractionEvent.attendees)) \ - .order_by(Attraction.id).one() return { 'admin_account': session.current_admin_account(), @@ -330,8 +324,7 @@ def new(self, session, message='', **params): def delete(self, session, id, message=''): if cherrypy.request.method == 'POST': attraction = session.query(Attraction).get(id) - attendee = session.admin_attendee() - if not attendee.can_admin_attraction(attraction): + if not session.current_admin_account().can_admin_attraction(attraction): raise HTTPRedirect( 'form?id={}&message={}', id, @@ -503,7 +496,8 @@ def event(self, session, previous_id=None, delay=0, message='', **params): 'form?id={}&message={}', feature.attraction_id, message) return { - 'attraction': feature.attraction, + 'attraction': session.query(Attraction).filter(Attraction.id == feature.attraction_id).options( + selectinload(Attraction.events)).first(), 'feature': feature, 'event': event, 'forms': forms, @@ -572,7 +566,7 @@ def update_locations(self, session, feature_id, old_location, new_location): message = '' if cherrypy.request.method == 'POST': feature = session.query(AttractionFeature).get(feature_id) - if not session.admin_attendee().can_admin_attraction(feature.attraction): + if not session.current_admin_account().can_admin_attraction(feature.attraction): message = "You cannot update rooms for an attraction you don't own" else: for event in feature.events: @@ -589,7 +583,7 @@ def delete_event(self, session, id): event = session.query(AttractionEvent).get(id) attraction_id = event.feature.attraction_id attraction = session.query(Attraction).get(attraction_id) - if not session.admin_attendee().can_admin_attraction(attraction): + if not session.current_admin_account().can_admin_attraction(attraction): message = "You cannot delete a event from an attraction you don't own." else: session.delete(event) @@ -598,13 +592,13 @@ def delete_event(self, session, id): return {'error': message} @ajax - def delete_event(self, session, id): + def delete_event_cascade(self, session, id): message = '' if cherrypy.request.method == 'POST': event = session.query(AttractionEvent).get(id) attraction_id = event.feature.attraction_id attraction = session.query(Attraction).get(attraction_id) - if not session.admin_attendee().can_admin_attraction(attraction): + if not session.current_admin_account().can_admin_attraction(attraction): message = "You cannot delete a event from an attraction you don't own." else: if event.schedule_item: @@ -621,7 +615,7 @@ def delete_feature(self, session, id): message = '' if cherrypy.request.method == 'POST': attraction = session.query(Attraction).get(attraction_id) - if not session.admin_attendee().can_admin_attraction(attraction): + if not session.current_admin_account().can_admin_attraction(attraction): message = "You cannot delete a feature from an attraction you don't own." else: session.delete(feature) @@ -637,7 +631,7 @@ def delete_feature_cascade(self, session, id): message = '' if cherrypy.request.method == 'POST': attraction = session.query(Attraction).get(attraction_id) - if not session.admin_attendee().can_admin_attraction(attraction): + if not session.current_admin_account().can_admin_attraction(attraction): message = "You cannot delete a feature from an attraction you don't own." else: for event in feature.events: @@ -658,7 +652,7 @@ def cancel_signup(self, session, id): signup = session.query(AttractionSignup).get(id) attraction_id = signup.event.feature.attraction_id attraction = session.query(Attraction).get(attraction_id) - if not session.admin_attendee().can_admin_attraction(attraction): + if not session.current_admin_account().can_admin_attraction(attraction): message = "You cannot cancel a signup for an attraction you don't own." elif signup.is_checked_in: message = "You cannot cancel a signup that has already checked in." @@ -692,7 +686,9 @@ def checkin(self, session, message='', **params): except Exception: filters = [Attraction.slug.startswith(slugify(id))] - attraction = session.query(Attraction).filter(*filters).first() + attraction = session.query(Attraction).filter(*filters).options( + joinedload(Attraction.department) + ).first() if not attraction: raise HTTPRedirect('index') diff --git a/uber/site_sections/badge_printing.py b/uber/site_sections/badge_printing.py index 3ad582740..8ff2eab5c 100644 --- a/uber/site_sections/badge_printing.py +++ b/uber/site_sections/badge_printing.py @@ -88,8 +88,8 @@ def print_next_badge(self, session, printer_id=''): ribbon = ' / '.join(attendee.ribbon_labels) if attendee.ribbon else '' - badge.queued = datetime.utcnow() - badge.printed = datetime.utcnow() + badge.queued = datetime.now(UTC) + badge.printed = datetime.now(UTC) session.add(attendee) session.commit() @@ -203,7 +203,7 @@ def mark_as_printed(self, session, id): else: success = True message = "Job marked as printed." - job.printed = datetime.utcnow() + job.printed = datetime.now(UTC) session.add(job) session.commit() diff --git a/uber/site_sections/budget.py b/uber/site_sections/budget.py index fc5dad541..7c8240b13 100644 --- a/uber/site_sections/budget.py +++ b/uber/site_sections/budget.py @@ -5,6 +5,7 @@ from collections import defaultdict from sqlalchemy.orm import joinedload from sqlalchemy import or_, func, not_, and_ +from typing import Iterable from uber.config import c from uber.decorators import all_renderable, log_pageview @@ -28,7 +29,10 @@ def get_grouped_costs(session, filters=[], joins=[], selector=Attendee.badge_cos # Returns a defaultdict with the {int(cost): count} of badges query = session.query(selector, func.count(selector)) for join in joins: - query = query.join(join) + if isinstance(join, Iterable): + query = query.join(*join) + else: + query = query.join(join) if filters: query = query.filter(*filters) return defaultdict(int, query.group_by(selector).order_by(selector).all()) diff --git a/uber/site_sections/dept_admin.py b/uber/site_sections/dept_admin.py index 6c350d07b..f6692c757 100644 --- a/uber/site_sections/dept_admin.py +++ b/uber/site_sections/dept_admin.py @@ -1,5 +1,5 @@ import cherrypy -from sqlalchemy.orm import subqueryload +from sqlalchemy.orm import joinedload, selectinload from datetime import timedelta from uber.config import c @@ -26,8 +26,9 @@ def index(self, session, filtered=False, message='', **params): forms = load_forms({}, Department(), ['DepartmentInfo']) - departments = session.query(Department).filter(*dept_filter) \ - .order_by(Department.name).all() + departments = session.query(Department).filter(*dept_filter).options( + selectinload(Department.memberships) + ).order_by(Department.name).all() return { 'filtered': filtered, 'message': message, @@ -41,7 +42,8 @@ def form(self, session, message='', **params): if not department_id or department_id == 'None': raise HTTPRedirect('index') - department = session.department(department_id) + department = session.query(Department).filter( + Department.id == department_id).options(selectinload(Department.attendees_working_shifts)).first() forms = load_forms(params, department, ['DepartmentInfo']) if cherrypy.request.method == 'POST': @@ -60,12 +62,13 @@ def form(self, session, message='', **params): .filter_by(id=department_id) \ .order_by(Department.id) \ .options( - subqueryload(Department.dept_roles).subqueryload(DeptRole.dept_memberships), - subqueryload(Department.members).subqueryload(Attendee.shifts).subqueryload(Shift.job), - subqueryload(Department.members).subqueryload(Attendee.admin_account), - subqueryload(Department.dept_heads).subqueryload(Attendee.dept_memberships), - subqueryload(Department.pocs).subqueryload(Attendee.dept_memberships), - subqueryload(Department.checklist_admins).subqueryload(Attendee.dept_memberships)) \ + selectinload(Department.memberships), + selectinload(Department.dept_roles).selectinload(DeptRole.dept_memberships), + selectinload(Department.members).selectinload(Attendee.shifts).joinedload(Shift.job), + selectinload(Department.members).joinedload(Attendee.admin_account), + selectinload(Department.dept_heads).selectinload(Attendee.dept_memberships), + selectinload(Department.pocs).selectinload(Attendee.dept_memberships), + selectinload(Department.checklist_admins).selectinload(Attendee.dept_memberships)) \ .one() return { @@ -177,7 +180,9 @@ def requests(self, session, department_id=None, requested_any=False, message='', if not department_id: raise HTTPRedirect('index') - department = session.query(Department).get(department_id) + department = session.query(Department).filter(Department.id == department_id).options( + selectinload(Department.unassigned_explicitly_requesting_attendees) + ).first() if cherrypy.request.method == 'POST': attendee_ids = [s for s in params.get('attendee_ids', []) if s] if attendee_ids: @@ -203,7 +208,10 @@ def requests(self, session, department_id=None, requested_any=False, message='', @csv_file def dept_requests_export(self, out, session, department_id, requested_any=False, message='', **params): - department = session.query(Department).get(department_id) + department = session.query(Department).filter(Department.id == department_id).options( + selectinload(Department.unassigned_explicitly_requesting_attendees), + selectinload(Department.unassigned_requesting_attendees), + ).first() requesting_attendees = department.unassigned_requesting_attendees \ if requested_any else department.unassigned_explicitly_requesting_attendees diff --git a/uber/site_sections/dept_checklist.py b/uber/site_sections/dept_checklist.py index ade562913..a51a7ffe1 100644 --- a/uber/site_sections/dept_checklist.py +++ b/uber/site_sections/dept_checklist.py @@ -343,7 +343,7 @@ def bulk_print_jobs_csv(self, out, session): print_size = f"Custom: {request.size_text}" if request.size == c.CUSTOM else request.size_label out.writerow([ dept_name, request.link, request.copies, request.print_orientation_label, request.cut_orientation_label, - request.color_label, paper_type, print_size, request.double_sided, request.stapled, request.required, + request.color_label, paper_type, print_size, request.double_sided, request.stapled, request.important, request.notes, short_datetime_local(request.last_updated) ]) diff --git a/uber/site_sections/devtools.py b/uber/site_sections/devtools.py index b183de492..ba3ec6c64 100644 --- a/uber/site_sections/devtools.py +++ b/uber/site_sections/devtools.py @@ -16,7 +16,7 @@ from sqlalchemy.dialects.postgresql.json import JSONB from pytz import UTC -from sqlalchemy.types import Date, Boolean, Integer +from sqlalchemy.types import DateTime from sqlalchemy import text from uber.decorators import all_renderable, csv_file, public, site_mappable @@ -234,7 +234,7 @@ def health(self, session): db_read_time += time.perf_counter() return json.dumps({ - 'server_current_timestamp': int(datetime.utcnow().timestamp()), + 'server_current_timestamp': int(datetime.now(UTC).timestamp()), 'session_read_count': read_count, 'session_commit_time': session_commit_time, 'db_read_time': db_read_time, diff --git a/uber/site_sections/panels_admin.py b/uber/site_sections/panels_admin.py index 50d668e24..0a11ee5fe 100644 --- a/uber/site_sections/panels_admin.py +++ b/uber/site_sections/panels_admin.py @@ -9,7 +9,7 @@ from uber.config import c from uber.decorators import ajax, all_renderable, csrf_protected, csv_file, render from uber.errors import HTTPRedirect -from uber.models import AssignedPanelist, Attendee, AutomatedEmail, Event, EventFeedback, \ +from uber.models import AssignedPanelist, Attendee, AutomatedEmail, Event, EventFeedback, EventLocation, Department, \ PanelApplicant, PanelApplication, GuestGroup from uber.utils import add_opt, check, localized_now, validate_model, groupify from uber.forms import load_forms @@ -290,7 +290,8 @@ def associate(self, session, message='', **params): return { 'app': app, 'message': message, - 'panels': session.query(Event).filter(Event.location.in_(c.PANEL_ROOMS)).order_by('name') + 'panels': session.query(Event).join(Event.location).join( + EventLocation.department).filter(Department.manages_panels == True).order_by('name') } def badges(self, session): @@ -386,7 +387,8 @@ def feedback_report(self, session): feedback[fb.event].append(fb) events = [] - for event in session.query(Event).filter(Event.location.in_(c.PANEL_ROOMS)).order_by('name'): + for event in session.query(Event).join(Event.location).join( + EventLocation.department).filter(Department.manages_panels == True).order_by('name'): events.append([event, feedback[event]]) for event, fb in feedback.items(): diff --git a/uber/site_sections/preregistration.py b/uber/site_sections/preregistration.py index f7590fa51..59adcecd2 100644 --- a/uber/site_sections/preregistration.py +++ b/uber/site_sections/preregistration.py @@ -10,6 +10,7 @@ import cherrypy from collections import defaultdict from sqlalchemy import func, or_ +from sqlalchemy.orm import selectinload, joinedload from sqlalchemy.orm.exc import NoResultFound from uber.config import c @@ -216,7 +217,7 @@ def index(self, session, message='', account_email='', account_password='', **pa if not PreregCart.unpaid_preregs: raise HTTPRedirect('form?message={}', message) if message else HTTPRedirect('form') else: - cart = PreregCart(listify(PreregCart.unpaid_preregs.values())) + cart = PreregCart(list(PreregCart.unpaid_preregs.values())) cart.set_total_cost() for attendee in cart.attendees: if attendee.promo_code: @@ -735,13 +736,13 @@ def banned(self, **params): } def at_door_confirmation(self, session, message='', qr_code_id='', **params): - cart = PreregCart(listify(PreregCart.unpaid_preregs.values())) + cart = PreregCart(list(PreregCart.unpaid_preregs.values())) registrations_list = [] account = session.current_attendee_account() if c.ATTENDEE_ACCOUNTS_ENABLED else None account_pickup_group = session.query(BadgePickupGroup).filter_by(account_id=account.id).first() if account else None pickup_group = None - if not listify(PreregCart.unpaid_preregs.values()): + if not list(PreregCart.unpaid_preregs.values()): if qr_code_id: current_pickup_group = session.query(BadgePickupGroup).filter_by(public_id=qr_code_id).first() for attendee in current_pickup_group.attendees: @@ -814,7 +815,7 @@ def at_door_confirmation(self, session, message='', qr_code_id='', **params): } def process_free_prereg(self, session, message='', **params): - cart = PreregCart(listify(PreregCart.unpaid_preregs.values())) + cart = PreregCart(list(PreregCart.unpaid_preregs.values())) cart.set_total_cost() if cart.total_cost <= 0: prereg_cart_error = cart.prereg_cart_checks(session) @@ -859,7 +860,7 @@ def prereg_payment(self, session, message='', **params): if errors: return errors update_prereg_cart(session) - cart = PreregCart(listify(PreregCart.unpaid_preregs.values())) + cart = PreregCart(list(PreregCart.unpaid_preregs.values())) cart.set_total_cost() pickup_group_id = None @@ -967,7 +968,8 @@ def prereg_payment(self, session, message='', **params): for attendee in cart.attendees: pending_attendee = session.query(Attendee).filter_by(id=attendee.id).first() if pending_attendee: - pending_attendee.apply(PreregCart.to_sessionized(attendee), restricted=True) + for key, val in PreregCart.to_sessionized(attendee).items(): + setattr(pending_attendee, key, val) if attendee.badges and pending_attendee.promo_code_groups: pc_group = pending_attendee.promo_code_groups[0] pc_group.name = attendee.name @@ -2176,7 +2178,15 @@ def logout(self, return_to=''): @log_pageview def confirm(self, session, message='', return_to='confirm', undoing_extra='', **params): if params.get('id') not in [None, '', 'None']: - attendee = session.attendee(params.get('id')) + attendee = session.query(Attendee).filter(Attendee.id == params.get('id')).options( + selectinload(Attendee.dept_membership_requests), + selectinload(Attendee.art_agent_apps), + selectinload(Attendee.promo_code_groups), + selectinload(Attendee.shifts), + joinedload(Attendee.lottery_application), + joinedload(Attendee.art_show_application), + joinedload(Attendee.marketplace_application), + ).first() receipt = session.get_receipt_by_model(attendee) if cherrypy.request.method == 'POST': receipt_items = ReceiptManager.auto_update_receipt(attendee, receipt, params.copy()) @@ -2284,13 +2294,15 @@ def validate_attendee(self, session, form_list=[], is_prereg=False, **params): attendee = Attendee() else: try: - attendee = session.attendee(id) + attendee = session.query(Attendee).filter(Attendee.id == id).options( + selectinload(Attendee.promo_code_groups)).one() except NoResultFound: if is_prereg: attendee = self._get_unsaved( id, if_not_found=HTTPRedirect('form?message={}', 'That preregistration expired or has already been finalized.')) + log.error(attendee) else: return {"error": {'': ["We could not find the badge you're trying to update."]}} diff --git a/uber/site_sections/reg_admin.py b/uber/site_sections/reg_admin.py index c93d25a67..f7caac7b6 100644 --- a/uber/site_sections/reg_admin.py +++ b/uber/site_sections/reg_admin.py @@ -278,15 +278,14 @@ def receipt_items(self, session, id, message='', highlight_id=''): other_receipts = set() if isinstance(model, Attendee): - for app in model.art_show_applications: - other_receipt = session.get_receipt_by_model(app, options=options) - if other_receipt: - other_receipt.changes = session.query(Tracking).filter( - or_(Tracking.links.like('%model_receipt({})%' - .format(other_receipt.id)), - and_(Tracking.model == 'ModelReceipt', - Tracking.fk_id == other_receipt.id))).order_by(Tracking.when).all() - other_receipts.add(other_receipt) + other_receipt = session.get_receipt_by_model(model.art_show_application, options=options) + if other_receipt: + other_receipt.changes = session.query(Tracking).filter( + or_(Tracking.links.like('%model_receipt({})%' + .format(other_receipt.id)), + and_(Tracking.model == 'ModelReceipt', + Tracking.fk_id == other_receipt.id))).order_by(Tracking.when).all() + other_receipts.add(other_receipt) closed_receipts = set() closed_receipt_query = session.query(ModelReceipt).filter(ModelReceipt.owner_id == id, @@ -333,6 +332,7 @@ def receipt_items_guide(self, session, message=''): c.MANUAL: "Stripe"} } + @not_site_mappable def create_receipt(self, session, id='', blank=False): try: model = session.attendee(id) diff --git a/uber/site_sections/reg_reports.py b/uber/site_sections/reg_reports.py index 21c7bf94a..36ba5603e 100644 --- a/uber/site_sections/reg_reports.py +++ b/uber/site_sections/reg_reports.py @@ -2,6 +2,7 @@ import calendar from collections import defaultdict from sqlalchemy import or_, and_ +from sqlalchemy.orm import lazyload, joinedload from sqlalchemy.sql import func from sqlalchemy.sql.expression import literal @@ -30,15 +31,19 @@ def checkins_by_hour_query(session): @all_renderable() class Root: def comped_badges(self, session, message='', show='all'): - regular_comped = session.attendees_with_badges().filter(Attendee.paid == c.NEED_NOT_PAY, - Attendee.promo_code == None) # noqa: E711 - promo_comped = session.query(Attendee).join(PromoCode).filter(Attendee.has_badge == True, # noqa: E712 - Attendee.paid == c.NEED_NOT_PAY, - or_(PromoCode.cost == None, # noqa: E711 - PromoCode.cost == 0)) - group_comped = session.query(Attendee).join(Group, Attendee.group_id == Group.id)\ - .filter(Attendee.has_badge == True, # noqa: E712 - Attendee.paid == c.PAID_BY_GROUP, Group.cost == 0) + regular_comped = session.attendees_with_badges().filter( + Attendee.paid == c.NEED_NOT_PAY, Attendee.promo_code == None).options( # noqa: E711 + joinedload(Attendee.creator) + ) + promo_comped = session.query(Attendee).join(PromoCode).filter( + Attendee.has_badge == True, Attendee.paid == c.NEED_NOT_PAY, # noqa: E712 + or_(PromoCode.cost == None, PromoCode.cost == 0)).options( # noqa: E711 + joinedload(Attendee.creator) + ) + group_comped = session.query(Attendee).join( + Group, Attendee.group_id == Group.id).filter( + Attendee.has_badge == True, # noqa: E712 + Attendee.paid == c.PAID_BY_GROUP, Group.cost == 0).options(joinedload(Attendee.creator)) all_comped = regular_comped.union(promo_comped, group_comped) claimed_comped = all_comped.filter(Attendee.placeholder == False) # noqa: E712 unclaimed_comped = all_comped.filter(Attendee.placeholder == True) # noqa: E712 @@ -86,7 +91,7 @@ def attendee_receipt_discrepancies(self, session, include_pending=False): attendees = session.query(Attendee).filter( filter).join(Attendee.active_receipt).outerjoin(ModelReceipt.receipt_items).group_by( ModelReceipt.id).group_by(Attendee.id).having( - Attendee.default_cost_cents != ModelReceipt.fkless_item_total_sql) + Attendee.default_cost_cents != ModelReceipt.fkless_item_total_sql).options(lazyload("*")) return { 'attendees': attendees, @@ -109,7 +114,7 @@ def attendees_nonzero_balance(self, session, include_no_receipts=False, include_ ModelReceipt.receipt_txns).join(item_subquery, Attendee.id == item_subquery.c.owner_id).group_by( ModelReceipt.id).group_by(Attendee.id).group_by(item_subquery.c.item_total).having( and_((ModelReceipt.payment_total_sql - ModelReceipt.refund_total_sql) != item_subquery.c.item_total, - filter)) + filter)).options(lazyload("*")) if include_no_receipts: attendees_no_receipts = session.query(Attendee).outerjoin( diff --git a/uber/site_sections/registration.py b/uber/site_sections/registration.py index 0832c40e5..4a8544c95 100644 --- a/uber/site_sections/registration.py +++ b/uber/site_sections/registration.py @@ -13,7 +13,7 @@ from aztec_code_generator import AztecCode from pytz import UTC from sqlalchemy import and_, func, or_ -from sqlalchemy.orm import joinedload +from sqlalchemy.orm import joinedload, selectinload from sqlalchemy.orm.exc import NoResultFound from uber.config import c @@ -47,14 +47,26 @@ def load_attendee(session, params): if id in [None, '', 'None']: attendee = Attendee() - session.add(attendee) else: - attendee = session.attendee(id) + attendee = session.query(Attendee).filter(Attendee.id == id).options( + selectinload(Attendee.promo_code_groups), + selectinload(Attendee.allocated_badges), + selectinload(Attendee.escalation_tickets), + selectinload(Attendee.assigned_depts), + selectinload(Attendee.dept_membership_requests), + selectinload(Attendee.dept_memberships_with_role), + selectinload(Attendee.dept_memberships_with_inherent_role), + joinedload(Attendee.lottery_application), + joinedload(Attendee.watch_list), + joinedload(Attendee.shifts), + joinedload(Attendee.panel_applicants), + joinedload(Attendee.admin_account)).one() return attendee def save_attendee(session, attendee, params): + session.add(attendee) if cherrypy.request.method == 'POST': receipt_items = ReceiptManager.auto_update_receipt( attendee, session.get_receipt_by_model(attendee), params.copy(), who=AdminAccount.admin_name() or 'non-admin') @@ -134,6 +146,9 @@ def index(self, session, message='', page='0', search_text='', uploaded_id='', o 'This attendee was the only{} search result'.format('' if invalid else ' valid')) pages = range(1, int(math.ceil(count / 100)) + 1) + attendees = attendees.options( + selectinload(Attendee.promo_code_groups), + selectinload(Attendee.allocated_badges)) attendees = attendees[-100 + 100*page: 100*page] if page else [] return { @@ -156,10 +171,7 @@ def index(self, session, message='', page='0', search_text='', uploaded_id='', o @ajax @any_admin_access def validate_attendee(self, session, form_list=[], **params): - if params.get('id') in [None, '', 'None']: - attendee = Attendee() - else: - attendee = session.attendee(params.get('id')) + attendee = load_attendee(session, params) if not form_list: form_list = ['PersonalInfo', 'AdminBadgeExtras', 'AdminConsents', 'AdminStaffingInfo', 'AdminBadgeFlags', @@ -377,7 +389,7 @@ def check_terminal_payment(self, session, model_id, model_name, **params): if response: response_json = response.json() if req.api_response_successful(response_json): - tracker.resolved = datetime.utcnow() + tracker.resolved = datetime.now(UTC) tracker.response = response_json tracker.internal_error = '' session.add(tracker) @@ -390,7 +402,7 @@ def check_terminal_payment(self, session, model_id, model_name, **params): error = req.error_message_from_response(response_json) if error != "Not found": return {'success': False, 'message': f"Error checking status of last transaction: {error}"} - tracker.resolved = datetime.utcnow() + tracker.resolved = datetime.now(UTC) session.add(tracker) session.commit() prior_error = terminal_status.get('last_error') @@ -1452,7 +1464,7 @@ def stats(self): 'badges_sold': c.BADGES_SOLD, 'remaining_badges': c.REMAINING_BADGES, 'badges_price': c.BADGE_PRICE, - 'server_current_timestamp': int(datetime.utcnow().timestamp()), + 'server_current_timestamp': int(datetime.now(UTC).timestamp()), 'warn_if_server_browser_time_mismatch': c.WARN_IF_SERVER_BROWSER_TIME_MISMATCH }) @@ -1467,12 +1479,7 @@ def price(self): @attendee_view @cherrypy.expose(['attendee_data']) def attendee_form(self, session, message='', tab_view=None, **params): - id = params.get('id', None) - - if id in [None, '', 'None']: - attendee = Attendee() - else: - attendee = session.attendee(id) + attendee = load_attendee(session, params) forms = load_forms(params, attendee, ['PersonalInfo', 'AdminBadgeExtras', 'AdminConsents', 'AdminStaffingInfo', 'AdminBadgeFlags', 'BadgeAdminNotes', 'OtherInfo']) @@ -1514,7 +1521,10 @@ def attendee_history(self, session, id, **params): @attendee_view @cherrypy.expose(['shifts']) def attendee_shifts(self, session, id, **params): - attendee = session.attendee(id, allow_invalid=True) + attendee = session.query(Attendee).filter(Attendee.id == id).options( + joinedload(Attendee.shifts), + selectinload(Attendee.dept_membership_requests), + selectinload(Attendee.dept_memberships_with_dept_role)).first() attrs = Shift.to_dict_default_attrs + ['worked_label'] return_dict = { diff --git a/uber/site_sections/schedule.py b/uber/site_sections/schedule.py index 202fb5e3f..75a1163c3 100644 --- a/uber/site_sections/schedule.py +++ b/uber/site_sections/schedule.py @@ -8,13 +8,13 @@ from datetime import datetime, time, timedelta from dateutil import parser as dateparser from time import mktime -from sqlalchemy.orm import joinedload +from sqlalchemy.orm import joinedload, selectinload from uber.config import c from uber.decorators import ajax, ajax_gettable, all_renderable, cached, csrf_protected, csv_file, render, schedule_view, site_mappable from uber.errors import HTTPRedirect from uber.forms import load_forms -from uber.models import AssignedPanelist, Attendee, Event, EventLocation, PanelApplication +from uber.models import AssignedPanelist, Attendee, Event, EventLocation, PanelApplication, Department from uber.utils import check, localized_now, normalize_newlines, validate_model, load_locations_from_config, listify log = logging.getLogger(__name__) @@ -121,16 +121,16 @@ def now(self, session, when=None): now = c.EVENT_TIMEZONE.localize(datetime.combine(localized_now().date(), time(localized_now().hour))) current, upcoming = [], [] - for loc, desc in c.SCHEDULE_LOCATION_OPTS: - approx = session.query(Event).filter(Event.location == loc, - Event.start_time >= now - timedelta(hours=6), - Event.start_time <= now).all() + for location_id, name in c.SCHEDULE_LOCATION_OPTS: + approx = session.query(Event).join(Event.location).filter( + EventLocation.id == location_id, + Event.start_time >= now - timedelta(hours=6), Event.start_time <= now).all() for event in approx: if now in event.minutes: current.append(event) - next_events = session.query(Event).filter( - Event.location == loc, + next_events = session.query(Event).join(Event.location).filter( + EventLocation.id == location_id, Event.start_time >= now + timedelta(minutes=30), Event.start_time <= now + timedelta(hours=4)).order_by('start_time').all() @@ -325,7 +325,9 @@ def edit(self, session, message='', view_date=c.PANELS_EPOCH.date(), view_event= locations = [] - event_locations = session.query(EventLocation) + event_locations = session.query(EventLocation).options( + selectinload(EventLocation.events), joinedload(EventLocation.department) + ) if not event_locations.first(): load_locations_from_config(session) @@ -428,8 +430,13 @@ def event_panel_info(self, out, session): @csv_file def panel_tech_needs(self, out, session): panels = defaultdict(dict) - panel_applications = session.query(PanelApplication).filter( - PanelApplication.event_id == Event.id, Event.location.in_(c.PANEL_ROOMS)) + panel_applications = session.query(PanelApplication).join(PanelApplication.event).join( + Event.location).join(EventLocation.department).filter( + Department.manages_panels == True, + PanelApplication.event_id == Event.id) + + panel_rooms = session.query(EventLocation).join(EventLocation.department).filter( + Department.manages_panels == True) for panel in panel_applications: panels[panel.event.start_time_local][panel.event.event_location_id] = panel @@ -441,7 +448,7 @@ def panel_tech_needs(self, out, session): out.writerow(['Panel Starts'] + [c.SCHEDULE_LOCATIONS[room] for room in c.PANEL_ROOMS]) while curr_time <= last_time: row = [curr_time.strftime('%H:%M %a')] - for room in c.PANEL_ROOMS: + for room in panel_rooms: p = panels[curr_time].get(room) row.append('' if not p else '{}\n{}\n{}\n{}'.format( p.event.name, diff --git a/uber/site_sections/shifts_admin.py b/uber/site_sections/shifts_admin.py index e4e699216..c62421bfa 100644 --- a/uber/site_sections/shifts_admin.py +++ b/uber/site_sections/shifts_admin.py @@ -5,7 +5,7 @@ import logging from sqlalchemy import select, func, literal from sqlalchemy.dialects.postgresql import aggregate_order_by -from sqlalchemy.orm import subqueryload +from sqlalchemy.orm import selectinload, joinedload, defaultload from uber.config import c from uber.decorators import ajax, all_renderable, csrf_protected, csv_file, \ @@ -87,7 +87,12 @@ def index(self, session, department_id=None, message='', time=None): if time: initial_date = max(initial_date, datetime.strptime(time, "%Y-%m-%dT%H:%M:%S%z")) - department = session.query(Department).get(department_id) if department_id else None + if department_id: + department = session.query(Department).filter(Department.id == department_id).options( + selectinload(Department.dept_checklist_items), selectinload(Department.job_templates) + ).first() + else: + department = None by_start = defaultdict(list) times = [c.EPOCH + timedelta(hours=i) for i in range(c.CON_LENGTH)] @@ -236,14 +241,18 @@ def staffers(self, session, department_id=None, message=''): requested_count = 0 if department_id: - department = session.query(Department).filter_by(id=department_id).first() + department = session.query(Department).filter_by(id=department_id).options( + selectinload(Department.unassigned_explicitly_requesting_attendees) + ).first() if not department: department_id = '' if department_id != '': dept_filter = [] if department_id == None else [ # noqa: E711 Attendee.dept_memberships.any(department_id=department_id)] - attendees = session.staffers(pending=True).filter(*dept_filter).all() + attendees = session.staffers(pending=True).filter(*dept_filter).options( + selectinload(Attendee.dept_memberships_with_role) + ).all() requested_count = None if not department_id else len( [a for a in department.unassigned_explicitly_requesting_attendees if a.is_valid]) for attendee in attendees: @@ -318,11 +327,16 @@ def form(self, session, message='', **params): if params.get('id') in [None, '', 'None']: job = Job() else: - job = session.job(params.get('id')) + job = session.query(Job).filter(Job.id == params.get('id')).options( + defaultload(Job.department).selectinload(Department.dept_roles), + defaultload(Job.department).selectinload(Department.job_templates) + ).first() department = job.department if params.get('department_id'): - department = session.department(params['department_id']) + department = session.query(Department).filter(Department.id == params['department_id']).options( + selectinload(Department.dept_roles), selectinload(Department.job_templates) + ).first() if not department: raise HTTPRedirect('index?message={}', "Please select a department.") @@ -377,15 +391,21 @@ def validate_job(self, session, form_list=[], **params): def template(self, session, message='', **params): default_depts = session.admin_attendee().depts_where_can_admin department = default_depts[0] if default_depts else None + num_jobs = 0 if params.get('id') in [None, '', 'None']: job_template = JobTemplate() else: - job_template = session.job_template(params.get('id')) + job_template = session.query(JobTemplate).filter(JobTemplate.id == params.get('id')).options( + defaultload(JobTemplate.department).selectinload(Department.dept_roles) + ).first() + num_jobs = session.query(Job).filter(Job.job_template_id == params.get('id')).count() department = job_template.department if params.get('department_id'): - department = session.department(params['department_id']) + department = session.query(Department).filter(Department.id == params['department_id']).options( + selectinload(Department.dept_roles) + ).first() if not department: raise HTTPRedirect('index?message={}', "Please select a department.") @@ -408,6 +428,7 @@ def template(self, session, message='', **params): return { 'job_template': job_template, + 'num_jobs': num_jobs, 'department': department, 'forms': forms, 'message': message, @@ -545,10 +566,9 @@ def rate(self, session, shift_id, rating, comment=''): else: return shift_dict(shift) - def summary(self, session): departments = defaultdict(lambda: defaultdict(int)) - for job in session.jobs().options(subqueryload(Job.department)): + for job in session.jobs(): update_counts(job, departments[job.department_name]) update_counts(job, departments['All Departments Combined']) @@ -557,12 +577,11 @@ def summary(self, session): def all_shifts(self, session): departments = session.query(Department).options( - subqueryload(Department.jobs)).order_by(Department.name) + selectinload(Department.jobs)).order_by(Department.name) return { 'depts': [(d.name, d.jobs) for d in departments] } - @site_mappable @csv_file def shift_schedule_csv(self, out, session, department_id, day='all', **params): filters = [] diff --git a/uber/site_sections/showcase.py b/uber/site_sections/showcase.py index 0a46075d9..7af0a28a3 100644 --- a/uber/site_sections/showcase.py +++ b/uber/site_sections/showcase.py @@ -191,7 +191,7 @@ def validate_developer(self, session, form_list=[], **params): def delete_developer(self, session, id, **params): developer = session.indie_developer(id) studio = developer.studio - if developer.gets_emails and len(studio.primary_contacts) == 1: + if developer.receives_emails and len(studio.primary_contacts) == 1: raise HTTPRedirect('index?id={}&message={}', studio.id, 'You cannot delete the only presenter who receives email updates.') @@ -223,7 +223,7 @@ def confirm(self, session, id, decision=None, **params): has_leader = False badges_remaining = studio.comped_badges - developers = sorted(studio.developers, key=lambda d: (not d.gets_emails, d.full_name)) + developers = sorted(studio.developers, key=lambda d: (not d.receives_emails, d.full_name)) for dev in developers: if not dev.matching_attendee and badges_remaining: dev.comped = True diff --git a/uber/site_sections/staffing_admin.py b/uber/site_sections/staffing_admin.py index ec2b20ce4..5ea11bbb8 100644 --- a/uber/site_sections/staffing_admin.py +++ b/uber/site_sections/staffing_admin.py @@ -3,6 +3,7 @@ import cherrypy from dateutil import parser as dateparser from pytz import UTC +from sqlalchemy.orm import joinedload, selectinload from uber.config import c from uber.decorators import all_renderable, ajax, ajax_gettable, site_mappable @@ -20,6 +21,9 @@ def _create_copy_department(from_department): # Convert old years' max hours to minutes, this can eventually be removed if 'max_consecutive_hours' in from_department: setattr(to_department, 'max_consecutive_minutes', int(from_department['max_consecutive_hours']) * 60) + to_department.dept_roles = [] + to_department.job_templates = [] + return to_department @@ -135,9 +139,15 @@ def import_shifts( api_token, "Cannot create a department with the same name as an existing department") to_department = _create_copy_department(from_department) + if shifts_text: + to_department.jobs = [] session.add(to_department) else: - to_department = session.query(Department).get(to_department_id) + to_department = session.query(Department).filter(Department.id == to_department_id).options( + selectinload(Department.dept_roles), selectinload(Department.job_templates), + selectinload(Department.jobs) + ).first() + session.add(to_department) dept_role_map = _copy_department_roles(to_department, from_department) dept_template_map = _copy_department_templates(to_department, from_department) @@ -197,7 +207,10 @@ def bulk_dept_import( for id, name in from_departments: from_department = service.dept.jobs(department_id=id) - to_department = session.query(Department).filter_by(name=from_department['name']).first() + to_department = session.query(Department).filter_by(name=from_department['name']).options( + selectinload(Department.dept_roles), selectinload(Department.job_templates), + selectinload(Department.attractions), + ).first() if not to_department: to_department = _create_copy_department(from_department) session.add(to_department) diff --git a/uber/site_sections/staffing_reports.py b/uber/site_sections/staffing_reports.py index 3122678e6..2a4f4ceeb 100644 --- a/uber/site_sections/staffing_reports.py +++ b/uber/site_sections/staffing_reports.py @@ -6,19 +6,22 @@ from datetime import timedelta from dateutil import parser as dateparser -from sqlalchemy import or_ -from sqlalchemy.orm import subqueryload +from sqlalchemy import and_ +from sqlalchemy.orm import subqueryload, selectinload from uber.config import c from uber.decorators import all_renderable, csv_file, render -from uber.models import Attendee, Department, Job +from uber.models import Attendee, Department, DeptMembership, Job def volunteer_checklists(session): attendees = session.query(Attendee) \ .filter( Attendee.staffing == True, # noqa: E712 - Attendee.badge_status.in_([c.NEW_STATUS, c.COMPLETED_STATUS])) \ + Attendee.badge_status.in_([c.NEW_STATUS, c.COMPLETED_STATUS])).options( + selectinload(Attendee.hotel_requests), selectinload(Attendee.food_restrictions), + selectinload(Attendee.shifts) + ) \ .order_by(Attendee.full_name, Attendee.id).all() checklist_items = OrderedDict() @@ -152,7 +155,11 @@ def volunteer_food(self, session, message='', department_id=None, start_time=Non @csv_file def dept_head_contact_info(self, out, session): out.writerow(["Full Name", "Email", "Phone", "Department(s)"]) - for a in session.query(Attendee).filter(Attendee.dept_memberships_as_dept_head.any()).order_by('last_name'): + dept_heads = session.query(Attendee).join(DeptMembership, + and_( + Attendee.id == DeptMembership.attendee_id, + DeptMembership.is_dept_head == True)) + for a in dept_heads.order_by(Attendee.last_name): for label in a.assigned_depts_labels: out.writerow([a.full_name, a.email, a.cellphone, label]) diff --git a/uber/tasks/redis.py b/uber/tasks/redis.py index 456fc62ad..58d8b1a7f 100644 --- a/uber/tasks/redis.py +++ b/uber/tasks/redis.py @@ -23,7 +23,7 @@ def expire_processed_saml_assertions(): rsession = c.REDIS_STORE.pipeline() for key, val in c.REDIS_STORE.hscan(c.REDIS_PREFIX + 'processed_saml_assertions')[1].items(): - if int(val) < datetime.utcnow().timestamp(): + if int(val) < datetime.now(UTC).timestamp(): rsession.hdel(c.REDIS_PREFIX + 'processed_saml_assertions', key) rsession.execute() diff --git a/uber/templates/api/reference.html b/uber/templates/api/reference.html index 8b7c3c86a..cbad590c4 100644 --- a/uber/templates/api/reference.html +++ b/uber/templates/api/reference.html @@ -354,10 +354,17 @@

Note on 404 Not Found

{%- set examples = { 'attendee': '{"method": "attendee.search", "params": ["' ~ admin_account.attendee.full_name ~ '"]}', + 'attendee_account': '{"method": "attendee_account.export", "params": ["query", "all"]}', + 'attraction': '{"method": "attraction.list"}', 'barcode': '{"method": "barcode.lookup_attendee_from_barcode", "params": ["~v22pcw"]}', 'config': '{"method": "config.info"}', 'dept': '{"method": "dept.jobs", "params": {"department_id": "' ~ c.DEFAULT_DEPARTMENT_ID ~ '"}}', + 'group': '{"method": "group.dealers", "params": ["New"]}', 'guest': '{"method": "guest.list", "params": ["guest"]}', + 'hotel': '{"method": "hotel.eligible_attendees"}', + 'mivs': '{"method": "mivs.list", "params": [""]}', + 'print_job': '{"method": "print_job.get_pending", "params": ["", "", "true"]}', + 'schedule': '{"method": "schedule.schedule"}', 'shifts': '{"method": "shifts.lookup", "params": {"department_id": "' ~ c.DEFAULT_DEPARTMENT_ID ~ '", "start_time": "now"}}' } -%} diff --git a/uber/templates/art_show_admin/bidder_signup.html b/uber/templates/art_show_admin/bidder_signup.html index 412dbf6cd..0cca6f0ba 100644 --- a/uber/templates/art_show_admin/bidder_signup.html +++ b/uber/templates/art_show_admin/bidder_signup.html @@ -110,12 +110,12 @@

Bidder Management

{% if c.PREASSIGNED_BADGE_TYPES %}{{ attendee.badge_printed_name }}{% endif %} {% if c.NUMBERED_BADGES %}{{ attendee.badge_num }}{% endif %} - - {% for app in attendee.art_show_applications %} - {{ app.display_name }} + + {% if attendee.art_show_application %} + {{ attendee.art_show_application.display_name }} {% else %} N/A - {% endfor %} + {% endif %} {% set show_edit_btn = attendee.art_show_bidder and attendee.art_show_bidder.signed_up %} @@ -182,11 +182,11 @@

Bidder Management

{% if c.PREASSIGNED_BADGE_TYPES %}{{ attendee.badge_printed_name }}{% endif %} {% if c.NUMBERED_BADGES %}{{ attendee.badge_num }}{% endif %} - {% for app in attendee.art_show_applications %} - {{ app.display_name }} + {% if attendee.art_show_application %} + {{ attendee.art_show_application.display_name }} {% else %} N/A - {% endfor %} + {% endif %} {% set show_edit_btn = attendee.art_show_bidder and attendee.art_show_bidder.signed_up %} diff --git a/uber/templates/attractions_admin/form.html b/uber/templates/attractions_admin/form.html index 1737ca5b9..4a6a7ddf4 100644 --- a/uber/templates/attractions_admin/form.html +++ b/uber/templates/attractions_admin/form.html @@ -660,7 +660,7 @@ } -{%- set can_admin_attraction = admin_account.attendee.can_admin_attraction(attraction) -%} +{%- set can_admin_attraction = admin_account.can_admin_attraction(attraction) -%} {% if can_admin_attraction -%} - {% if job_template.jobs %} + {% if num_jobs %}
{{ csrf_token() }} diff --git a/uber/templates/showcase/index.html b/uber/templates/showcase/index.html index d79c0b56b..907426202 100644 --- a/uber/templates/showcase/index.html +++ b/uber/templates/showcase/index.html @@ -55,7 +55,7 @@

Team Members

{{ dev.full_name }} {{ dev.email }} {{ dev.cellphone }} - {{ dev.gets_emails|yesno|title }} + {{ dev.receives_emails|yesno|title }}
diff --git a/uber/templates/showcase_judging/studio.html b/uber/templates/showcase_judging/studio.html index 14b35a162..086e3468c 100644 --- a/uber/templates/showcase_judging/studio.html +++ b/uber/templates/showcase_judging/studio.html @@ -70,7 +70,7 @@

{{ studio.name }}

{{ dev.email }} {{ dev.cellphone_num }} - {% if dev.gets_emails %} + {% if dev.receives_emails %} Primary Contact {% endif %} diff --git a/uber/utils.py b/uber/utils.py index 38e79c884..3264665c6 100644 --- a/uber/utils.py +++ b/uber/utils.py @@ -14,10 +14,10 @@ import urllib import logging import warnings -import functools -import inspect +import six from collections import defaultdict, OrderedDict +from collections.abc import Iterable, Mapping, Sized from datetime import date, datetime, timedelta, timezone from glob import glob from os.path import basename @@ -581,6 +581,41 @@ def groupify(items, keys, val_key=None): current.append(value) return groupified +def is_listy(x): + """ + Return True if `x` is "listy", i.e. a list-like object. + + "Listy" is defined as a sized iterable which is neither a map nor a string: + + >>> is_listy(['a', 'b']) + True + >>> is_listy(set()) + True + >>> is_listy(iter(['a', 'b'])) + False + >>> is_listy({'a': 'b'}) + False + >>> is_listy('a regular string') + False + + Note: + Iterables and generators fail the "listy" test because they + are not sized. + + Args: + x (any value): The object to test. + + Returns: + bool: True if `x` is "listy", False otherwise. + + """ + return ( + isinstance(x, Sized) + and isinstance(x, Iterable) + and not isinstance(x, (Mapping, type(b""))) + and not isinstance(x, six.string_types) + ) + # ====================================================================== # Datetime functions # ====================================================================== @@ -663,12 +698,12 @@ def get_age_from_birthday(birthdate, today=None): if not today: today = date.today() - if isinstance(birthdate, str): - birthdate_col = Attendee.__table__.columns.get('birthdate') + birthdate_col = Attendee.__table__.columns.get('birthdate') + + if isinstance(birthdate, six.string_types): birthdate = Attendee().coerce_column_data(birthdate_col, birthdate) - if isinstance(today, str): - birthdate_col = Attendee.__table__.columns.get('birthdate') + if isinstance(today, six.string_types): today = Attendee().coerce_column_data(birthdate_col, today) # int(True) == 1 and int(False) == 0