From a6462f42ce0d47bfa1002e6862e4cdcc60f71875 Mon Sep 17 00:00:00 2001 From: Victoria Earl Date: Fri, 6 Feb 2026 13:38:57 +0100 Subject: [PATCH 01/20] Add basic relationship loading strategies Changes the default relationship loading strategy to raise in order to avoid generating N+1 queries. We also override this default on many relationships based on how we use those relationships. You can currently load and save the attendee form and load the attendee shifts page. --- .pythonstartup.py | 2 +- alembic/env.py | 2 +- ...fc_add_missing_columns_from_sqlalchemy_.py | 65 +++++++++++ .../d0c15c44a031_add_extra_donation_column.py | 2 +- docs/script_example.py | 2 +- tests/uber/models/test_badge_funcs.py | 2 +- tests/uber/models/test_config.py | 20 ++-- tests/uber/models/test_group.py | 2 +- tests/uber/models/test_job.py | 2 +- tests/uber/models/test_watchlist.py | 2 +- uber/models/__init__.py | 15 +++ uber/models/admin.py | 11 +- uber/models/art_show.py | 15 +-- uber/models/attendee.py | 103 +++++------------- uber/models/attraction.py | 30 +++-- uber/models/commerce.py | 12 +- uber/models/department.py | 25 +++-- uber/models/email.py | 6 +- uber/models/group.py | 6 +- uber/models/guests.py | 30 ++--- uber/models/hotel.py | 5 +- uber/models/marketplace.py | 2 +- uber/models/mits.py | 13 ++- uber/models/panels.py | 12 +- uber/models/promo_code.py | 5 +- uber/models/showcase.py | 17 +-- uber/models/tabletop.py | 3 +- uber/models/types.py | 1 + uber/payments.py | 4 +- uber/site_sections/registration.py | 24 ++-- uber/site_sections/staffing_reports.py | 10 +- 31 files changed, 250 insertions(+), 200 deletions(-) create mode 100644 alembic/versions/3d942ce689fc_add_missing_columns_from_sqlalchemy_.py 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..76b031b03 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -24,7 +24,7 @@ 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): 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/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..585018a67 100644 --- a/docs/script_example.py +++ b/docs/script_example.py @@ -6,4 +6,4 @@ with Session() as session: initialize_db() - session = Session().session \ No newline at end of file + session = Session() \ No newline at end of file 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/uber/models/__init__.py b/uber/models/__init__.py index 5c340f090..b30aa4f70 100644 --- a/uber/models/__init__.py +++ b/uber/models/__init__.py @@ -185,6 +185,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(): @@ -2217,6 +2224,13 @@ 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): if model.__name__ in ['SessionMixin', 'QuerySubclass']: @@ -2251,6 +2265,7 @@ 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.SessionMixin = UberSession.SessionMixin diff --git a/uber/models/admin.py b/uber/models/admin.py index 7ed3864ef..9d3cc8a69 100644 --- a/uber/models/admin.py +++ b/uber/models/admin.py @@ -34,13 +34,13 @@ 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', + 'AccessGroup', backref='admin_accounts', lazy='selectin', cascade='save-update,merge,refresh-expire,expunge', secondary='admin_access_group') hashed = Column(String, private=True) - password_reset = relationship('PasswordReset', backref='admin_account', uselist=False) + password_reset = relationship('PasswordReset', backref='admin_account', lazy='select', uselist=False) - api_tokens = relationship('ApiToken', backref='admin_account') + api_tokens = relationship('ApiToken', backref=backref('admin_account', lazy='joined')) active_api_tokens = relationship( 'ApiToken', primaryjoin='and_(' @@ -342,7 +342,8 @@ class WatchList(MagModel): 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') + attendees = relationship('Attendee', lazy='selectin', + backref=backref('watch_list'), cascade='save-update,merge,refresh-expire,expunge') @property def full_name(self): @@ -372,7 +373,7 @@ def fix_birthdate(self): class EscalationTicket(MagModel): attendees = relationship( - 'Attendee', backref='escalation_tickets', order_by='Attendee.full_name', + 'Attendee', lazy='selectin', 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') diff --git a/uber/models/art_show.py b/uber/models/art_show.py index 85d9252da..261129e7b 100644 --- a/uber/models/art_show.py +++ b/uber/models/art_show.py @@ -28,7 +28,7 @@ class ArtShowAgentCode(MagModel): app_id = Column(Uuid(as_uuid=False), ForeignKey('art_show_application.id')) - app = relationship('ArtShowApplication', + app = relationship('ArtShowApplication', lazy='joined', backref=backref('agent_codes', cascade='merge,refresh-expire,expunge'), foreign_keys=app_id, cascade='merge,refresh-expire,expunge') @@ -57,7 +57,7 @@ def attendee_first_name(self): 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', + attendee = relationship('Attendee', lazy='joined', 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) @@ -98,10 +98,11 @@ class ArtShowApplication(MagModel): primaryjoin='and_(remote(ModelReceipt.owner_id) == foreign(ArtShowApplication.id),' 'ModelReceipt.owner_model == "ArtShowApplication",' 'ModelReceipt.closed == None)', + lazy='select', uselist=False) default_cost = Column(Integer, nullable=True) - assignments = relationship('ArtPanelAssignment', backref='app') + assignments = relationship('ArtPanelAssignment', backref=backref('app', lazy='joined')) email_model_name = 'app' @@ -438,7 +439,7 @@ def check_total(self): 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', + cascade='save-update, merge', lazy='joined', 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) @@ -590,7 +591,7 @@ class ArtShowPanel(MagModel): start_label = Column(String) end_label = Column(String) - assignments = relationship('ArtPanelAssignment', backref='panel') + assignments = relationship('ArtPanelAssignment', lazy='selectin', backref=backref('panel', lazy='joined')) __table_args__ = ( UniqueConstraint('gallery', 'surface_type', 'origin_x', 'origin_y', 'terminus_x', 'terminus_y'), @@ -654,9 +655,9 @@ def assignment_str(self): 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, + receipt = relationship('ArtShowReceipt', lazy='joined', foreign_keys=receipt_id, cascade='save-update, merge', - backref=backref('art_show_payments', + backref=backref('art_show_payments', lazy='selectin', cascade='save-update, merge')) amount = Column(Integer, default=0) type = Column(Choice(c.ART_SHOW_PAYMENT_OPTS), default=c.STRIPE, admin_only=True) diff --git a/uber/models/attendee.py b/uber/models/attendee.py index 5f1dd339e..f8bad388b 100644 --- a/uber/models/attendee.py +++ b/uber/models/attendee.py @@ -149,7 +149,7 @@ 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, + foreign_keys=attendee_id, lazy='joined', 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) @@ -204,11 +204,12 @@ 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') + Group, backref=(backref('attendees', lazy='selectin')), + foreign_keys=group_id, lazy='select', cascade='save-update,merge,refresh-expire,expunge') 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, + 'BadgePickupGroup', backref=backref('attendees', lazy='selectin', 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) @@ -235,6 +236,7 @@ class Attendee(MagModel, TakesPaymentMixin): primaryjoin='and_(BadgeInfo.attendee_id == Attendee.id,' 'BadgeInfo.active == True)', uselist=False, + lazy='joined', overlaps="allocated_badges,attendee") # NOTE: The cascade relationships for promo_code do NOT include @@ -250,6 +252,7 @@ class Attendee(MagModel, TakesPaymentMixin): promo_code_id = Column(Uuid(as_uuid=False), ForeignKey('promo_code.id'), nullable=True, index=True) promo_code = relationship( 'PromoCode', + lazy='select', backref=backref('used_by', cascade='merge,refresh-expire,expunge'), foreign_keys=promo_code_id, cascade='merge,refresh-expire,expunge') @@ -325,18 +328,12 @@ class Attendee(MagModel, TakesPaymentMixin): primaryjoin='and_(remote(ModelReceipt.owner_id) == foreign(Attendee.id),' 'ModelReceipt.owner_model == "Attendee",' 'ModelReceipt.closed == None)', + lazy='select', uselist=False) default_cost = Column(Integer, nullable=True) - dept_memberships = relationship('DeptMembership', backref='attendee') + dept_memberships = relationship('DeptMembership', backref='attendee', lazy='select') 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', @@ -346,20 +343,13 @@ class Attendee(MagModel, TakesPaymentMixin): secondary='join(DeptMembership, dept_membership_dept_role)', order_by='DeptRole.name', viewonly=True) - shifts = relationship('Shift', backref='attendee') + shifts = relationship('Shift', backref=backref('attendee', lazy='joined')) 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', @@ -384,18 +374,6 @@ class Attendee(MagModel, TakesPaymentMixin): '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( 'DeptMembership', primaryjoin='and_(' @@ -410,26 +388,6 @@ class Attendee(MagModel, TakesPaymentMixin): '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) @@ -444,23 +402,23 @@ class Attendee(MagModel, TakesPaymentMixin): # 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) + no_shirt = relationship('NoShirt', backref=backref('attendee'), uselist=False) - admin_account = relationship('AdminAccount', backref=backref('attendee', load_on_pending=True), uselist=False) + admin_account = relationship('AdminAccount', backref=backref('attendee', lazy='joined'), uselist=False) food_restrictions = relationship( - 'FoodRestrictions', backref=backref('attendee', load_on_pending=True), uselist=False) + 'FoodRestrictions', backref=backref('attendee', lazy='joined'), 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')) + dept_checklist_items = relationship('DeptChecklistItem', backref=backref('attendee', lazy='selectin')) indie_developer = relationship( - 'IndieDeveloper', backref=backref('attendee', load_on_pending=True), uselist=False) + 'IndieDeveloper', backref=backref('attendee'), 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)) + hotel_requests = relationship('HotelRequests', backref=backref('attendee', lazy='joined'), uselist=False) + room_assignments = relationship('RoomAssignment', backref=backref('attendee', lazy='joined')) # The PIN/password used by third party hotel reservation systems hotel_pin = SQLAlchemyColumn(String, nullable=True, unique=True) @@ -473,8 +431,8 @@ class Attendee(MagModel, TakesPaymentMixin): # ========================= # panels # ========================= - assigned_panelists = relationship('AssignedPanelist', backref='attendee') - panel_applicants = relationship('PanelApplicant', backref='attendee', cascade='save-update,merge,refresh-expire,expunge') + assigned_panelists = relationship('AssignedPanelist', backref=backref('attendee', lazy='joined')) + panel_applicants = relationship('PanelApplicant', backref=backref('attendee', lazy='joined'), cascade='save-update,merge,refresh-expire,expunge') panel_applications = relationship('PanelApplication', backref='poc') panel_feedback = relationship('EventFeedback', backref='attendee') submitted_panels = relationship( @@ -499,34 +457,34 @@ class Attendee(MagModel, TakesPaymentMixin): 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_signups = relationship('AttractionSignup', backref=backref('attendee', lazy='joined'), order_by='AttractionSignup.signup_time') attraction_event_signups = association_proxy('attraction_signups', 'event') attraction_notifications = relationship( - 'AttractionNotification', backref='attendee', order_by='AttractionNotification.sent_time') + 'AttractionNotification', backref=backref('attendee', lazy='joined'), order_by='AttractionNotification.sent_time') # ========================= # tabletop # ========================= - games = relationship('TabletopGame', backref='attendee') - checkouts = relationship('TabletopCheckout', backref='attendee') + games = relationship('TabletopGame', backref=backref('attendee', lazy='joined')) + checkouts = relationship('TabletopCheckout', backref=backref('attendee', lazy='joined')) # ========================= # badge printing # ========================= - print_requests = relationship('PrintJob', backref='attendee', order_by='desc(PrintJob.last_updated)') + print_requests = relationship('PrintJob', backref=backref('attendee', lazy='joined'), order_by='desc(PrintJob.last_updated)') # ========================= # art show # ========================= - art_show_bidder = relationship('ArtShowBidder', backref=backref('attendee', load_on_pending=True), uselist=False) + art_show_bidder = relationship('ArtShowBidder', backref=backref('attendee', lazy='joined'), uselist=False) art_show_purchases = relationship( 'ArtShowPiece', - backref='buyer', + backref=backref('buyer', lazy='joined'), cascade='save-update,merge,refresh-expire,expunge', secondary='art_show_receipt') art_agent_apps = relationship( 'ArtShowApplication', - backref='agents', + backref=backref('agents', lazy='joined'), secondaryjoin='and_(ArtShowAgentCode.app_id == ArtShowApplication.id, ArtShowAgentCode.cancelled == None)', secondary='art_show_agent_code', viewonly=True) @@ -2294,13 +2252,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) @@ -2563,7 +2514,7 @@ 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) + password_reset = relationship('PasswordReset', backref='attendee_account', lazy='select', uselist=False) attendees = relationship( 'Attendee', backref='managers', order_by='Attendee.registered', cascade='save-update,merge,refresh-expire,expunge', diff --git a/uber/models/attraction.py b/uber/models/attraction.py index 9a1417860..e2e4e587c 100644 --- a/uber/models/attraction.py +++ b/uber/models/attraction.py @@ -166,8 +166,8 @@ class Attraction(MagModel, AttractionMixin): uselist=True), order_by='Department.name') features = relationship( - 'AttractionFeature', - backref='attraction', + 'AttractionFeature', lazy='selectin', + backref=backref('attraction', lazy='joined'), order_by='[AttractionFeature.name, AttractionFeature.id]') public_features = relationship( 'AttractionFeature', @@ -178,11 +178,11 @@ class Attraction(MagModel, AttractionMixin): order_by='[AttractionFeature.name, AttractionFeature.id]') events = relationship( 'AttractionEvent', - backref='attraction', + backref=backref('attraction', lazy='joined'), order_by='[AttractionEvent.start_time, AttractionEvent.id]') signups = relationship( 'AttractionSignup', - backref='attraction', + backref=backref('attraction', lazy='joined'), viewonly=True, order_by='[AttractionSignup.checkin_time, AttractionSignup.id]') @@ -351,7 +351,8 @@ class AttractionFeature(MagModel, AttractionMixin): attraction_id = Column(Uuid(as_uuid=False), ForeignKey('attraction.id')) events = relationship( - 'AttractionEvent', backref='feature', order_by='[AttractionEvent.start_time, AttractionEvent.id]') + 'AttractionEvent', backref=backref('feature', lazy='joined'), lazy='selectin', + order_by='[AttractionEvent.start_time, AttractionEvent.id]') __table_args__ = ( UniqueConstraint('name', 'attraction_id'), @@ -467,14 +468,6 @@ 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) @@ -483,14 +476,17 @@ class AttractionEvent(MagModel, AttractionMixin): start_time = Column(DateTime(timezone=True), default=c.EPOCH) duration = Column(Integer, default=60) - signups = relationship('AttractionSignup', backref='event', order_by='AttractionSignup.checkin_time') + signups = relationship('AttractionSignup', backref=backref('event', lazy='joined'), + order_by='AttractionSignup.checkin_time') attendee_signups = association_proxy('signups', 'attendee') - notifications = relationship('AttractionNotification', backref='event', order_by='AttractionNotification.sent_time') + notifications = relationship('AttractionNotification', backref=backref('event', lazy='joined'), + order_by='AttractionNotification.sent_time') notification_replies = relationship( - 'AttractionNotificationReply', backref='event', order_by='AttractionNotificationReply.sid') + 'AttractionNotificationReply', backref=backref('event', lazy='joined'), + order_by='AttractionNotificationReply.sid') attendees = relationship( 'Attendee', @@ -765,7 +761,7 @@ class AttractionSignup(MagModel): notifications = relationship( 'AttractionNotification', backref=backref( - 'signup', + 'signup', lazy='joined', cascade='merge', uselist=False, viewonly=True), diff --git a/uber/models/commerce.py b/uber/models/commerce.py index f9950c49f..ad0e28a8c 100644 --- a/uber/models/commerce.py +++ b/uber/models/commerce.py @@ -331,12 +331,12 @@ class ReceiptTransaction(MagModel): 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')) + cascade='save-update, merge', lazy='joined', + backref=backref('receipt_txns', lazy='selectin', 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')) + backref=backref('receipt_txns', lazy='selectin', 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'), @@ -539,11 +539,11 @@ 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')) + cascade='save-update, merge', lazy='joined', + backref=backref('receipt_items', lazy='selectin', 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', + cascade='save-update, merge', lazy='joined', backref=backref('receipt_items', cascade='save-update, merge')) fk_id = Column(Uuid(as_uuid=False), index=True, nullable=True) fk_model = Column(String) diff --git a/uber/models/department.py b/uber/models/department.py index 9e2d463da..6a85724e1 100644 --- a/uber/models/department.py +++ b/uber/models/department.py @@ -157,7 +157,7 @@ class DeptRole(MagModel): dept_memberships = relationship( 'DeptMembership', - backref='dept_roles', + backref=backref('dept_roles', lazy='selectin'), cascade='save-update,merge,refresh-expire,expunge', secondary='dept_membership_dept_role') @@ -211,13 +211,13 @@ class Department(MagModel): handles_cash = Column(Boolean, default=False) panels_desc = Column(String) - jobs = relationship('Job', backref='department') - job_templates = relationship('JobTemplate', backref='department') + jobs = relationship('Job', backref=backref('department', lazy='joined')) + job_templates = relationship('JobTemplate', backref=backref('department', lazy='joined')) locations = relationship('EventLocation', backref='department') events = relationship('Event', backref='department') - dept_checklist_items = relationship('DeptChecklistItem', backref='department') - dept_roles = relationship('DeptRole', backref='department') + dept_checklist_items = relationship('DeptChecklistItem', backref=backref('department', lazy='joined')) + dept_roles = relationship('DeptRole', backref=backref('department', lazy='joined')) dept_heads = relationship( 'Attendee', backref=backref('headed_depts', order_by='Department.name'), @@ -272,8 +272,9 @@ class Department(MagModel): 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') + memberships = relationship('DeptMembership', backref=backref('department', lazy='joined', overlaps="assigned_depts,members"), + overlaps="assigned_depts,members") + membership_requests = relationship('DeptMembershipRequest', backref=backref('department', lazy='joined')) explicitly_requesting_attendees = relationship( 'Attendee', backref=backref('explicitly_requested_depts', order_by='Department.name', overlaps="attendee,dept_membership_requests,department,membership_requests"), @@ -422,8 +423,9 @@ class Job(MagModel): 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') + 'DeptRole', backref='jobs', lazy='selectin', + cascade='save-update,merge,refresh-expire,expunge', secondary='job_required_role') + shifts = relationship('Shift', backref=backref('job', lazy='joined'), lazy='selectin') __table_args__ = ( Index('ix_job_department_id', department_id), @@ -700,8 +702,9 @@ class JobTemplate(MagModel): 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') + 'DeptRole', backref='job_templates', lazy='selectin', + cascade='save-update,merge,refresh-expire,expunge', secondary='job_template_required_role') + jobs = relationship('Job', backref=backref('template', lazy='select'), cascade='save-update,merge,refresh-expire,expunge') @presave_adjustment def zero_slots(self): diff --git a/uber/models/email.py b/uber/models/email.py index cbbeff7be..9ce674fa3 100644 --- a/uber/models/email.py +++ b/uber/models/email.py @@ -10,7 +10,7 @@ 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.orm import backref from sqlalchemy.schema import ForeignKey from sqlalchemy.types import Boolean, Integer, String, Uuid, DateTime @@ -19,7 +19,7 @@ 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 DefaultColumn as Column, default_relationship as relationship from uber.utils import normalize_newlines, request_cached_context, groupify log = logging.getLogger(__name__) @@ -86,7 +86,7 @@ class AutomatedEmail(MagModel, BaseEmailMixin): active_before = Column(DateTime(timezone=True), nullable=True, default=None) revert_changes = Column(MutableDict.as_mutable(JSONB), default={}) - emails = relationship('Email', backref='automated_email', order_by='Email.id') + emails = relationship('Email', backref=backref('automated_email', lazy='joined'), order_by='Email.id') @presave_adjustment def date_adjustments(self): diff --git a/uber/models/group.py b/uber/models/group.py index 95f9d1b5d..224759984 100644 --- a/uber/models/group.py +++ b/uber/models/group.py @@ -70,14 +70,16 @@ class Group(MagModel, TakesPaymentMixin): 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) + studio = relationship('IndieStudio', uselist=False, backref='group', + cascade='save-update,merge,refresh-expire,expunge') + guest = relationship('GuestGroup', backref=backref('group', lazy='joined'), uselist=False) active_receipt = 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)', + lazy='select', uselist=False) terms_conditions_doc = relationship( 'SignedDocument', diff --git a/uber/models/guests.py b/uber/models/guests.py index ea56abead..c16aefcbc 100644 --- a/uber/models/guests.py +++ b/uber/models/guests.py @@ -41,21 +41,21 @@ class GuestGroup(MagModel): 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) + info = relationship('GuestInfo', backref=backref('guest', lazy='joined'), 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) + 'GuestImage', backref=backref('guest'), order_by='GuestImage.id') + bio = relationship('GuestBio', backref=backref('guest', lazy='joined'), uselist=False) + taxes = relationship('GuestTaxes', backref=backref('guest', lazy='joined'), uselist=False) + stage_plot = relationship('GuestStagePlot', backref=backref('guest', lazy='joined'), uselist=False) + panel = relationship('GuestPanel', backref=backref('guest', lazy='joined'), uselist=False) + merch = relationship('GuestMerch', backref=backref('guest', lazy='joined'), uselist=False) + tracks = relationship('GuestTrack', backref=backref('guest', lazy='joined')) + charity = relationship('GuestCharity', backref=backref('guest', lazy='joined'), uselist=False) + autograph = relationship('GuestAutograph', backref=backref('guest', lazy='joined'), uselist=False) + interview = relationship('GuestInterview', backref=backref('guest', lazy='joined'), uselist=False) + travel_plans = relationship('GuestTravelPlans', backref=backref('guest', lazy='joined'), uselist=False) + hospitality = relationship('GuestHospitality', backref=backref('guest', lazy='joined'), uselist=False) + media_request = relationship('GuestMediaRequest', backref=backref('guest', lazy='joined'), uselist=False) email_model_name = 'guest' @@ -801,7 +801,7 @@ class GuestMediaRequest(MagModel): 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'), + backref=backref('detailed_travel_plans', lazy='selectin'), lazy='joined', cascade='save-update,merge,refresh-expire,expunge') mode = Column(Choice(c.GUEST_TRAVEL_OPTS)) mode_text = Column(String) diff --git a/uber/models/hotel.py b/uber/models/hotel.py index f1fb36772..c8a37f02e 100644 --- a/uber/models/hotel.py +++ b/uber/models/hotel.py @@ -91,7 +91,7 @@ class Room(MagModel, NightsMixin): 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') + assignments = relationship('RoomAssignment', backref=backref('room', lazy='joined')) @property def email(self): @@ -122,7 +122,7 @@ class RoomAssignment(MagModel): 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), + attendee = relationship('Attendee', lazy='joined', 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 @@ -168,6 +168,7 @@ class LotteryApplication(MagModel): parent_application_id = Column(Uuid(as_uuid=False), ForeignKey('lottery_application.id'), nullable=True) parent_application = relationship( 'LotteryApplication', + lazy='joined', foreign_keys='LotteryApplication.parent_application_id', backref=backref('group_members'), cascade='save-update,merge,refresh-expire,expunge', diff --git a/uber/models/marketplace.py b/uber/models/marketplace.py index 6df027134..5fe977a6d 100644 --- a/uber/models/marketplace.py +++ b/uber/models/marketplace.py @@ -19,7 +19,7 @@ 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), + attendee = relationship('Attendee', lazy='joined', backref=backref('marketplace_application', uselist=False), cascade='save-update,merge,refresh-expire,expunge', uselist=False) name = Column(String) diff --git a/uber/models/mits.py b/uber/models/mits.py index ea7f6fbc7..3183552fb 100644 --- a/uber/models/mits.py +++ b/uber/models/mits.py @@ -5,6 +5,7 @@ from pytz import UTC from sqlalchemy import and_ +from sqlalchemy.orm import backref from sqlalchemy.schema import ForeignKey from sqlalchemy.types import Boolean, Integer, Uuid, DateTime, String from sqlalchemy.ext.hybrid import hybrid_property @@ -35,10 +36,10 @@ class MITSTeam(MagModel): 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') + applicants = relationship('MITSApplicant', backref=backref('team', lazy='joined')) + games = relationship('MITSGame', lazy='selectin', backref=backref('team', lazy='joined')) + schedule = relationship('MITSTimes', uselist=False, backref=backref('team', lazy='joined')) + panel_app = relationship('MITSPanelApplication', uselist=False, backref=backref('team', lazy='joined')) duplicate_of = Column(Uuid(as_uuid=False), nullable=True) deleted = Column(Boolean, default=False) @@ -175,8 +176,8 @@ class MITSGame(MagModel): 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') + pictures = relationship('MITSPicture', lazy='selectin', backref=backref('game', lazy='joined')) + documents = relationship('MITSDocument', lazy='selectin', backref=backref('game', lazy='joined')) @hybrid_property def has_been_accepted(self): diff --git a/uber/models/panels.py b/uber/models/panels.py index 0bad7a37d..9ee8ff1cb 100644 --- a/uber/models/panels.py +++ b/uber/models/panels.py @@ -35,9 +35,9 @@ class EventLocation(MagModel): room = Column(String) tracks = Column(MultiChoice(c.EVENT_TRACK_OPTS)) - events = relationship('Event', backref=backref('location', cascade="save-update,merge"), + events = relationship('Event', backref=backref('location', lazy='joined', cascade="save-update,merge"), cascade="save-update,merge", single_parent=True) - attractions = relationship('AttractionEvent', backref=backref('location', cascade="save-update,merge"), + attractions = relationship('AttractionEvent', backref=backref('location', lazy='joined', cascade="save-update,merge"), cascade="save-update,merge", single_parent=True) @property @@ -89,14 +89,14 @@ class Event(MagModel): 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"), + assigned_panelists = relationship('AssignedPanelist', backref=backref('event', lazy='joined')) + applications = relationship('PanelApplication', backref=backref('event', lazy='joined', 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 + 'schedule_item', lazy='joined', cascade="save-update,merge", uselist=False ), cascade='save-update,merge') @property @@ -205,7 +205,7 @@ class PanelApplication(MagModel): comments = Column(String, admin_only=True) tags = Column(UniqueList, admin_only=True) - applicants = relationship('PanelApplicant', backref='applications', + applicants = relationship('PanelApplicant', lazy='selectin', backref=backref('applications', lazy='selectin'), cascade='save-update,merge,refresh-expire,expunge', secondary='panel_applicant_application') diff --git a/uber/models/promo_code.py b/uber/models/promo_code.py index 81fcdc44f..244a2a1fa 100644 --- a/uber/models/promo_code.py +++ b/uber/models/promo_code.py @@ -9,6 +9,7 @@ from dateutil import parser as dateparser from sqlalchemy import exists, func, select, CheckConstraint from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.orm import backref from sqlalchemy.schema import Index, ForeignKey from sqlalchemy.types import Integer, Uuid, String, DateTime @@ -136,7 +137,7 @@ class PromoCodeGroup(MagModel): 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', + 'Attendee', backref='promo_code_groups', lazy='joined', foreign_keys=buyer_id, cascade='save-update,merge,refresh-expire,expunge') @@ -318,7 +319,7 @@ class PromoCode(MagModel): group_id = Column(Uuid(as_uuid=False), ForeignKey('promo_code_group.id', ondelete='SET NULL'), nullable=True) group = relationship( - PromoCodeGroup, backref='promo_codes', + PromoCodeGroup, backref=backref('promo_codes', lazy='selectin'), lazy='joined', foreign_keys=group_id, cascade='save-update,merge,refresh-expire,expunge') diff --git a/uber/models/showcase.py b/uber/models/showcase.py index 1b2508660..b4d5bef15 100644 --- a/uber/models/showcase.py +++ b/uber/models/showcase.py @@ -8,6 +8,7 @@ from markupsafe import Markup from pytz import UTC from sqlalchemy import func, case, or_ +from sqlalchemy.orm import backref from sqlalchemy.schema import ForeignKey, UniqueConstraint from sqlalchemy.types import Boolean, Integer, String, DateTime, Uuid from sqlalchemy.ext.hybrid import hybrid_property @@ -46,8 +47,8 @@ class IndieJudge(MagModel, ReviewMixin): vr_text = Column(String) staff_notes = Column(String) - codes = relationship('IndieGameCode', backref='judge') - reviews = relationship('IndieGameReview', backref='judge') + codes = relationship('IndieGameCode', lazy='selectin', backref=backref('judge', lazy='joined')) + reviews = relationship('IndieGameReview', lazy='selectin', backref=backref('judge', lazy='joined')) email_model_name = 'judge' @@ -152,10 +153,10 @@ class IndieStudio(MagModel): logistics_updated = Column(Boolean, default=False) games = relationship( - 'IndieGame', backref='studio', order_by='IndieGame.title') + 'IndieGame', backref=backref('studio', lazy='joined'), lazy='selectin', order_by='IndieGame.title') developers = relationship( 'IndieDeveloper', - backref='studio', + backref=backref('studio', lazy='joined'), order_by='IndieDeveloper.last_name') email_model_name = 'studio' @@ -347,7 +348,7 @@ def matching_attendee(self): 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', + primary_contact = relationship(IndieDeveloper, backref='arcade_games', lazy='joined', foreign_keys=primary_contact_id, cascade='save-update,merge,refresh-expire,expunge') title = Column(String) @@ -417,10 +418,10 @@ class IndieGame(MagModel, ReviewMixin): waitlisted = Column(DateTime(timezone=True), nullable=True) accepted = Column(DateTime(timezone=True), nullable=True) - codes = relationship('IndieGameCode', backref='game') - reviews = relationship('IndieGameReview', backref='game') + codes = relationship('IndieGameCode', lazy='selectin', backref=backref('game', lazy='joined')) + reviews = relationship('IndieGameReview', backref=backref('game', lazy='joined')) images = relationship( - 'IndieGameImage', backref='game', order_by='IndieGameImage.id') + 'IndieGameImage', lazy='selectin', backref=backref('game', lazy='joined'), order_by='IndieGameImage.id') email_model_name = 'game' diff --git a/uber/models/tabletop.py b/uber/models/tabletop.py index 510616649..dcc98fb9f 100644 --- a/uber/models/tabletop.py +++ b/uber/models/tabletop.py @@ -1,6 +1,7 @@ from datetime import datetime from pytz import UTC +from sqlalchemy.orm import backref from sqlalchemy.schema import ForeignKey from sqlalchemy.types import Boolean, DateTime, String, Uuid @@ -16,7 +17,7 @@ class TabletopGame(MagModel): 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') + checkouts = relationship('TabletopCheckout', order_by='TabletopCheckout.checked_out', backref=backref('game', lazy='joined')) _repr_attr_names = ['name'] diff --git a/uber/models/types.py b/uber/models/types.py index 1fbf2cb74..fe53fce54 100644 --- a/uber/models/types.py +++ b/uber/models/types.py @@ -67,6 +67,7 @@ def default_relationship(*args, **kwargs): kwargs.setdefault('cascade', 'expunge,refresh-expire,merge') else: kwargs.setdefault('cascade', 'all,delete-orphan') + kwargs.setdefault('lazy', 'raise') return SQLAlchemy_relationship(*args, **kwargs) diff --git a/uber/payments.py b/uber/payments.py index fcdaf11ba..2a7ad5d6b 100644 --- a/uber/payments.py +++ b/uber/payments.py @@ -723,7 +723,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 @@ -1808,7 +1808,7 @@ 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 + session = Session() matching_txns = session.query(ReceiptTransaction).filter_by(intent_id=intent_id).filter( ReceiptTransaction.charge_id == '').all() diff --git a/uber/site_sections/registration.py b/uber/site_sections/registration.py index 0832c40e5..a255c2b31 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 @@ -42,14 +42,20 @@ def checking_at_the_door(self, *args, **kwargs): return checking_at_the_door -def load_attendee(session, params): +def load_attendee(session, params, add_to_session=True,): id = params.get('id', None) if id in [None, '', 'None']: attendee = Attendee() - session.add(attendee) + if add_to_session: + session.add(attendee) else: - attendee = session.attendee(id) + attendee = session.query(Attendee).filter(Attendee.id == id).options( + selectinload(Attendee.dept_membership_requests), + selectinload(Attendee.dept_memberships_with_inherent_role), + joinedload(Attendee.shifts), + joinedload(Attendee.panel_applicants), + joinedload(Attendee.admin_account)).one() return attendee @@ -156,10 +162,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, add_to_session=False) if not form_list: form_list = ['PersonalInfo', 'AdminBadgeExtras', 'AdminConsents', 'AdminStaffingInfo', 'AdminBadgeFlags', @@ -1514,7 +1517,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/staffing_reports.py b/uber/site_sections/staffing_reports.py index 3122678e6..eaf1a86b7 100644 --- a/uber/site_sections/staffing_reports.py +++ b/uber/site_sections/staffing_reports.py @@ -6,12 +6,12 @@ from datetime import timedelta from dateutil import parser as dateparser -from sqlalchemy import or_ +from sqlalchemy import and_ from sqlalchemy.orm import subqueryload 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): @@ -152,7 +152,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]) From d9b6210dc448e6a8406a9a8be6d925f3111c9e13 Mon Sep 17 00:00:00 2001 From: Victoria Earl Date: Wed, 11 Feb 2026 02:17:02 +0100 Subject: [PATCH 02/20] Fix server-side pagination 500 errors A lot of paginated pages would break if they had 0 rows to display. This fixes that. --- uber/templates/hotel_lottery_admin/index.html | 2 +- uber/templates/reg_admin/attendee_accounts.html | 2 +- uber/templates/reg_admin/automated_transactions.html | 2 +- uber/templates/registration/index_base.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/uber/templates/hotel_lottery_admin/index.html b/uber/templates/hotel_lottery_admin/index.html index 1edd57afa..396508189 100644 --- a/uber/templates/hotel_lottery_admin/index.html +++ b/uber/templates/hotel_lottery_admin/index.html @@ -109,7 +109,7 @@

Hotel Lottery Applications

{% endif %} {% block table %} -{% set page_count = pages[-1] %} +{% set page_count = pages[-1] if pages else 1 %} {% if page_count <= 50 %} {% set skip_pages = 1 %} {% else %} diff --git a/uber/templates/reg_admin/attendee_accounts.html b/uber/templates/reg_admin/attendee_accounts.html index 30f7f0a46..d2e4ed8fd 100644 --- a/uber/templates/reg_admin/attendee_accounts.html +++ b/uber/templates/reg_admin/attendee_accounts.html @@ -47,7 +47,7 @@ {% endif %} -{% set page_count = pages[-1] %} +{% set page_count = pages[-1] if pages else 1 %} {% if page_count <= 50 %} {% set skip_pages = 1 %} {% else %} diff --git a/uber/templates/reg_admin/automated_transactions.html b/uber/templates/reg_admin/automated_transactions.html index a879f1445..95d38ade3 100644 --- a/uber/templates/reg_admin/automated_transactions.html +++ b/uber/templates/reg_admin/automated_transactions.html @@ -75,7 +75,7 @@

Automated Transactions

{% endif %} {% block table %} -{% set page_count = pages[-1] %} +{% set page_count = pages[-1] if pages else 1 %} {% if page_count <= 50 %} {% set skip_pages = 1 %} {% else %} diff --git a/uber/templates/registration/index_base.html b/uber/templates/registration/index_base.html index 5c1b6dd4a..8adf932fd 100644 --- a/uber/templates/registration/index_base.html +++ b/uber/templates/registration/index_base.html @@ -209,7 +209,7 @@