class Group(MagModel, TakesPaymentMixin): public_id = Column(UUID, default=lambda: str(uuid4())) name = Column(UnicodeText) tables = Column(Numeric, default=0) zip_code = Column(UnicodeText) address1 = Column(UnicodeText) address2 = Column(UnicodeText) city = Column(UnicodeText) region = Column(UnicodeText) country = Column(UnicodeText) website = Column(UnicodeText) wares = Column(UnicodeText) categories = Column(MultiChoice(c.DEALER_WARES_OPTS)) categories_text = Column(UnicodeText) description = Column(UnicodeText) special_needs = Column(UnicodeText) amount_paid_override = Column(Integer, default=0, index=True, admin_only=True) amount_refunded_override = Column(Integer, default=0, admin_only=True) cost = Column(Integer, default=0, admin_only=True) purchased_items = Column(MutableDict.as_mutable(JSONB), default={}, server_default='{}') refunded_items = Column(MutableDict.as_mutable(JSONB), default={}, server_default='{}') auto_recalc = Column(Boolean, default=True, admin_only=True) stripe_txn_share_logs = relationship('StripeTransactionGroup', backref='group') can_add = Column(Boolean, default=False, admin_only=True) admin_notes = Column(UnicodeText, admin_only=True) status = Column(Choice(c.DEALER_STATUS_OPTS), default=c.UNAPPROVED, admin_only=True) registered = Column(UTCDateTime, server_default=utcnow()) approved = Column(UTCDateTime, nullable=True) leader_id = Column(UUID, ForeignKey('attendee.id', use_alter=True, name='fk_leader'), nullable=True) creator_id = Column(UUID, ForeignKey('attendee.id'), nullable=True) creator = relationship('Attendee', foreign_keys=creator_id, backref=backref('created_groups', order_by='Group.name', cascade='all,delete-orphan'), 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') guest = relationship('GuestGroup', backref='group', uselist=False) _repr_attr_names = ['name'] @presave_adjustment def _cost_and_leader(self): assigned = [a for a in self.attendees if not a.is_unassigned] if len(assigned) == 1: [self.leader] = assigned if self.auto_recalc: self.cost = self.default_cost elif not self.cost: self.cost = 0 if self.status == c.APPROVED and not self.approved: self.approved = datetime.now(UTC) if self.leader and self.is_dealer: self.leader.ribbon = add_opt(self.leader.ribbon_ints, c.DEALER_RIBBON) if not self.is_unpaid: for a in self.attendees: a.presave_adjustments() @presave_adjustment def update_purchased_items(self): if self.cost == self.orig_value_of( 'cost') and self.tables == self.orig_value_of('tables'): return self.purchased_items.clear() if not self.auto_recalc: # ¯\_(ツ)_/¯ if self.cost: self.purchased_items['group_total'] = self.cost else: # Groups tables and paid-by-group badges by cost table_count = int(float(self.tables)) default_price = c.TABLE_PRICES['default_price'] more_tables = {default_price: 0} for i in table_count: if c.TABLE_PRICES[i] == default_price: more_tables[default_price] += 1 else: self.purchased_items['table_' + i] = c.TABLE_PRICES[i] if more_tables[default_price]: self.purchased_items[ more_tables[default_price] + ' extra table(s) at $' + default_price + ' each'] = default_price * more_tables[default_price] badges_by_cost = {} for attendee in self.attendees: if attendee.paid == c.PAID_BY_GROUP: badges_by_cost[attendee.badge_cost] = bool( badges_by_cost.get(attendee.badge_cost)) + 1 for cost in badges_by_cost: self.purchased_items[badges_by_cost[cost] + ' badge(s) at $' + cost + ' each'] = cost * badges_by_cost[cost] @presave_adjustment def assign_creator(self): if self.is_new and not self.creator_id: self.creator_id = self.session.admin_attendee( ).id if self.session.admin_attendee() else None @property def sorted_attendees(self): return list( sorted(self.attendees, key=lambda a: (a.is_unassigned, a.id != self.leader_id, a.full_name))) @property def unassigned(self): """ Returns a list of the unassigned badges for this group, sorted so that the paid-by-group badges come last, because when claiming unassigned badges we want to claim the "weird" ones first. """ unassigned = [a for a in self.attendees if a.is_unassigned] return sorted(unassigned, key=lambda a: a.paid == c.PAID_BY_GROUP) @property def floating(self): """ Returns the list of paid-by-group unassigned badges for this group. This is a separate property from the "Group.unassigned" property because when automatically adding or removing unassigned badges, we care specifically about paid-by-group badges rather than all unassigned badges. """ return [ a for a in self.attendees if a.is_unassigned and a.paid == c.PAID_BY_GROUP ] @property def new_ribbon(self): return c.DEALER_RIBBON if self.is_dealer else '' @property def ribbon_and_or_badge(self): badge = self.unassigned[0] if badge.ribbon and badge.badge_type != c.ATTENDEE_BADGE: return ' / '.join([badge.badge_type_label] + self.ribbon_labels) elif badge.ribbon: return ' / '.join(badge.ribbon_labels) else: return badge.badge_type_label @hybrid_property def is_dealer(self): return bool(self.tables and self.tables != '0' and self.tables != '0.0' and (not self.registered or self.amount_paid or self.cost or self.status != c.UNAPPROVED)) @is_dealer.expression def is_dealer(cls): return and_( cls.tables > 0, or_(cls.amount_paid > 0, cls.cost > 0, cls.status != c.UNAPPROVED)) @hybrid_property def is_unpaid(self): return self.cost > 0 and self.amount_paid == 0 @is_unpaid.expression def is_unpaid(cls): return and_(cls.cost > 0, cls.amount_paid == 0) @property def email(self): if self.studio and self.studio.email: return self.studio.email elif self.leader and self.leader.email: return self.leader.email elif self.leader_id: # unattached groups [leader] = [a for a in self.attendees if a.id == self.leader_id] return leader.email else: emails = [a.email for a in self.attendees if a.email] if len(emails) == 1: return emails[0] @hybrid_property def badges_purchased(self): return len([a for a in self.attendees if a.paid == c.PAID_BY_GROUP]) @badges_purchased.expression def badges_purchased(cls): from uber.models import Attendee return exists().where( and_(Attendee.group_id == cls.id, Attendee.paid == c.PAID_BY_GROUP)) @property def badges(self): return len(self.attendees) @hybrid_property def unregistered_badges(self): return len([a for a in self.attendees if a.is_unassigned]) @unregistered_badges.expression def unregistered_badges(cls): from uber.models import Attendee return exists().where( and_(Attendee.group_id == cls.id, Attendee.first_name == '')) @cost_property def table_cost(self): table_count = int(float(self.tables)) return sum(c.TABLE_PRICES[i] for i in range(1, 1 + table_count)) @property def new_badge_cost(self): return c.DEALER_BADGE_PRICE if self.is_dealer else c.get_group_price() @cost_property def badge_cost(self): total = 0 for attendee in self.attendees: if attendee.paid == c.PAID_BY_GROUP: total += attendee.badge_cost return total @property def amount_extra(self): if self.is_new: return sum(a.total_cost - a.badge_cost for a in self.attendees if a.paid == c.PAID_BY_GROUP) else: return 0 @property def total_cost(self): return self.default_cost + self.amount_extra @property def amount_unpaid(self): if self.registered: return max(0, ((self.cost * 100) - self.amount_paid) / 100) else: return self.total_cost @hybrid_property def amount_paid(self): return sum([ item.amount for item in self.receipt_items if item.txn_type == c.PAYMENT ]) @amount_paid.expression def amount_paid(cls): from uber.models import ReceiptItem return select([func.sum(ReceiptItem.amount)]).where( and_(ReceiptItem.group_id == cls.id, ReceiptItem.txn_type == c.PAYMENT)).label('amount_paid') @hybrid_property def amount_refunded(self): return sum([ item.amount for item in self.receipt_items if item.txn_type == c.REFUND ]) @amount_refunded.expression def amount_refunded(cls): from uber.models import ReceiptItem return select([func.sum(ReceiptItem.amount)]).where( and_(ReceiptItem.group_id == cls.id, ReceiptItem.txn_type == c.REFUND)).label('amount_refunded') def balance_by_item_type(self, item_type): """ Return a sum of all the receipt item payments, minus the refunds, for this model by item type """ return sum([amt for type, amt in self.itemized_payments if type == item_type]) \ - sum([amt for type, amt in self.itemized_refunds if type == item_type]) @property def itemized_payments(self): return [(item.item_type, item.amount) for item in self.receipt_items if item.txn_type == c.PAYMENT] @property def itemized_refunds(self): return [(item.item_type, item.amount) for item in self.receipt_items if item.txn_type == c.REFUND] @property def dealer_max_badges(self): return c.MAX_DEALERS or math.ceil(self.tables) + 1 @property def dealer_badges_remaining(self): return self.dealer_max_badges - self.badges @property def hours_since_registered(self): if not self.registered: return 0 delta = datetime.now(UTC) - self.registered return max(0, delta.total_seconds()) / 60.0 / 60.0 @property def hours_remaining_in_grace_period(self): return max(0, c.GROUP_UPDATE_GRACE_PERIOD - self.hours_since_registered) @property def is_in_grace_period(self): return self.hours_remaining_in_grace_period > 0 @property def min_badges_addable(self): if self.is_dealer and not self.dealer_badges_remaining or self.amount_unpaid: return 0 if self.can_add: return 1 elif self.is_dealer: return 0 else: return c.MIN_GROUP_ADDITION @property def requested_hotel_info(self): if self.leader: return self.leader.requested_hotel_info elif self.leader_id: # unattached groups for attendee in self.attendees: if attendee.id == self.leader_id: return attendee.requested_hotel_info else: return any(a.requested_hotel_info for a in self.attendees) @property def physical_address(self): address1 = self.address1.strip() address2 = self.address2.strip() city = self.city.strip() region = self.region.strip() zip_code = self.zip_code.strip() country = self.country.strip() country = '' if country == 'United States' else country.strip() if city and region: city_region = '{}, {}'.format(city, region) else: city_region = city or region city_region_zip = '{} {}'.format(city_region, zip_code).strip() physical_address = [address1, address2, city_region_zip, country] return '\n'.join([s for s in physical_address if s]) @property def guidebook_name(self): return self.name @property def guidebook_subtitle(self): category_labels = [ cat for cat in self.categories_labels if 'Other' not in cat ] + [self.categories_text] return ', '.join(category_labels[:5]) @property def guidebook_desc(self): return self.description @property def guidebook_location(self): return ''
class Attendee(MagModel, TakesPaymentMixin): watchlist_id = Column(UUID, ForeignKey('watch_list.id', ondelete='set null'), nullable=True, default=None) group_id = Column(UUID, ForeignKey('group.id', ondelete='SET NULL'), nullable=True) group = relationship(Group, backref='attendees', foreign_keys=group_id, cascade='save-update,merge,refresh-expire,expunge') # NOTE: The cascade relationships for promo_code do NOT include # "save-update". During the preregistration workflow, before an Attendee # has paid, we create ephemeral Attendee objects that are saved in the # cherrypy session, but are NOT saved in the database. If the cascade # relationships specified "save-update" then the Attendee would # automatically be inserted in the database when the promo_code is set on # the Attendee object (which we do not want until the attendee pays). # # 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, 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') placeholder = Column(Boolean, default=False, admin_only=True) first_name = Column(UnicodeText) last_name = Column(UnicodeText) legal_name = Column(UnicodeText) email = Column(UnicodeText) 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(UnicodeText) address1 = Column(UnicodeText) address2 = Column(UnicodeText) city = Column(UnicodeText) region = Column(UnicodeText) country = Column(UnicodeText) no_cellphone = Column(Boolean, default=False) ec_name = Column(UnicodeText) ec_phone = Column(UnicodeText) cellphone = Column(UnicodeText) # Represents a request for hotel booking info during preregistration requested_hotel_info = Column(Boolean, default=False) interests = Column(MultiChoice(c.INTEREST_OPTS)) found_how = Column(UnicodeText) comments = Column(UnicodeText) for_review = Column(UnicodeText, admin_only=True) admin_notes = Column(UnicodeText, admin_only=True) public_id = Column(UUID, default=lambda: str(uuid4())) badge_num = Column(Integer, default=None, nullable=True, admin_only=True) 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(UnicodeText) # attendee shirt size for both swag and staff shirts shirt = Column(Choice(c.SHIRT_OPTS), default=c.NO_SHIRT) second_shirt = Column(Choice(c.SECOND_SHIRT_OPTS), default=c.UNKNOWN) can_spam = Column(Boolean, default=False) regdesk_info = Column(UnicodeText, admin_only=True) extra_merch = Column(UnicodeText, admin_only=True) got_merch = Column(Boolean, default=False, admin_only=True) reg_station = Column(Integer, nullable=True, admin_only=True) registered = Column(UTCDateTime, server_default=utcnow()) confirmed = Column(UTCDateTime, nullable=True, default=None) checked_in = Column(UTCDateTime, nullable=True) paid = Column(Choice(c.PAYMENT_OPTS), default=c.NOT_PAID, index=True, admin_only=True) overridden_price = Column(Integer, nullable=True, admin_only=True) base_badge_price = Column(Integer, default=0, admin_only=True) amount_paid = Column(Integer, default=0, admin_only=True) amount_extra = Column(Choice(c.DONATION_TIER_OPTS, allow_unspecified=True), default=0) extra_donation = Column(Integer, default=0) payment_method = Column(Choice(c.PAYMENT_METHOD_OPTS), nullable=True) amount_refunded = Column(Integer, default=0, admin_only=True) badge_printed_name = Column(UnicodeText) dept_checklist_items = relationship('DeptChecklistItem', backref='attendee') 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', cascade='save-update,merge,refresh-expire,expunge', 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_in_assigned_depts = relationship( 'Job', backref='attendees_in_dept', cascade='save-update,merge,refresh-expire,expunge', 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', cascade='save-update,merge,refresh-expire,expunge', secondary='join(Shift, Job)', order_by='Department.name', viewonly=True) dept_memberships_with_inherent_role = relationship( 'DeptMembership', primaryjoin='and_(' 'Attendee.id == DeptMembership.attendee_id, ' 'DeptMembership.has_inherent_role == True)', viewonly=True) dept_memberships_with_role = relationship( 'DeptMembership', primaryjoin='and_(' 'Attendee.id == DeptMembership.attendee_id, ' 'DeptMembership.has_role == True)', 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_(' 'Attendee.id == DeptMembership.attendee_id, ' 'or_(' 'DeptMembership.is_dept_head == True,' 'DeptMembership.is_checklist_admin == True))', viewonly=True) dept_memberships_as_checklist_admin = relationship( 'DeptMembership', primaryjoin='and_(' 'Attendee.id == DeptMembership.attendee_id, ' 'DeptMembership.is_checklist_admin == True)', viewonly=True) pocs_for_depts_where_working = relationship( 'Attendee', cascade='save-update,merge,refresh-expire,expunge', 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', cascade='save-update,merge,refresh-expire,expunge', 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) nonshift_hours = Column(Integer, default=0, admin_only=True) past_years = Column(UnicodeText, 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')) _attendee_table_args = [Index('ix_attendee_paid_group_id', paid, group_id)] if not c.SQLALCHEMY_URL.startswith('sqlite'): _attendee_table_args.append( UniqueConstraint('badge_num', deferrable=True, initially='DEFERRED')) __table_args__ = tuple(_attendee_table_args) _repr_attr_names = ['full_name'] @predelete_adjustment def _shift_badges(self): is_skipped = getattr(self, '_skip_badge_shift_on_delete', False) if self.badge_num and not is_skipped: self.session.shift_badges(self.badge_type, self.badge_num + 1, down=True) @presave_adjustment def _misc_adjustments(self): if not self.amount_extra: self.affiliate = '' if self.birthdate == '': self.birthdate = None if not self.extra_donation: self.extra_donation = 0 if not self.gets_any_kind_of_shirt: self.shirt = c.NO_SHIRT if self.paid != c.REFUNDED: self.amount_refunded = 0 if self.badge_cost == 0 and self.paid in [c.NOT_PAID, c.PAID_BY_GROUP]: self.paid = c.NEED_NOT_PAY if not self.base_badge_price: self.base_badge_price = self.new_badge_cost if c.AT_THE_CON and self.badge_num and not self.checked_in and \ self.is_new and \ self.badge_type not in c.PREASSIGNED_BADGE_TYPES: self.checked_in = datetime.now(UTC) if self.birthdate: 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(): setattr(self, attr, value.title()) if self.legal_name and self.full_name == self.legal_name: self.legal_name = '' @presave_adjustment def _status_adjustments(self): if self.badge_status == c.NEW_STATUS and self.banned: self.badge_status = c.WATCHED_STATUS try: send_email(c.SECURITY_EMAIL, [c.REGDESK_EMAIL, c.SECURITY_EMAIL], c.EVENT_NAME + ' WatchList Notification', render('emails/reg_workflow/attendee_watchlist.txt', {'attendee': self}), model='n/a') except Exception as ex: log.error('unable to send banned email about {}', self) elif self.badge_status == c.NEW_STATUS and not self.placeholder and \ self.first_name and ( self.paid in [c.HAS_PAID, c.NEED_NOT_PAY] or self.paid == c.PAID_BY_GROUP and self.group_id and not self.group.is_unpaid): self.badge_status = c.COMPLETED_STATUS @presave_adjustment def _staffing_adjustments(self): if self.is_dept_head: self.staffing = True if c.SHIFT_CUSTOM_BADGES or \ c.STAFF_BADGE not in c.PREASSIGNED_BADGE_TYPES: self.badge_type = c.STAFF_BADGE if self.paid == c.NOT_PAID: self.paid = c.NEED_NOT_PAY elif c.VOLUNTEER_RIBBON in self.ribbon_ints and self.is_new: 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_staffing = self.orig_value_of('staffing') if self.staffing and not old_staffing or \ c.VOLUNTEER_RIBBON in self.ribbon_ints and \ c.VOLUNTEER_RIBBON not in old_ribbon: self.staffing = True elif old_staffing and not self.staffing \ or c.VOLUNTEER_RIBBON not in self.ribbon_ints \ and c.VOLUNTEER_RIBBON in old_ribbon \ and not self.is_dept_head: self.unset_volunteering() if self.badge_type == c.STAFF_BADGE: self.ribbon = remove_opt(self.ribbon_ints, c.VOLUNTEER_RIBBON) elif self.staffing and self.badge_type != c.STAFF_BADGE and \ c.VOLUNTEER_RIBBON not in self.ribbon_ints: self.ribbon = add_opt(self.ribbon_ints, c.VOLUNTEER_RIBBON) if self.badge_type == c.STAFF_BADGE: self.staffing = True if not self.overridden_price \ and self.paid in [c.NOT_PAID, c.PAID_BY_GROUP]: self.paid = c.NEED_NOT_PAY @presave_adjustment def _badge_adjustments(self): from uber.badge_funcs import needs_badge_num if self.badge_type == c.PSEUDO_DEALER_BADGE: self.ribbon = add_opt(self.ribbon_ints, c.DEALER_RIBBON) self.badge_type = self.badge_type_real old_type = self.orig_value_of('badge_type') old_num = self.orig_value_of('badge_num') if not needs_badge_num(self): self.badge_num = None if old_type != self.badge_type or old_num != self.badge_num: self.session.update_badge(self, old_type, old_num) elif needs_badge_num(self) and not self.badge_num: self.badge_num = self.session.get_next_badge_num(self.badge_type) @presave_adjustment def _use_promo_code(self): if c.BADGE_PROMO_CODES_ENABLED and self.promo_code and \ not self.overridden_price and self.is_unpaid: if self.badge_cost > 0: self.overridden_price = self.badge_cost else: self.paid = c.NEED_NOT_PAY def unset_volunteering(self): self.staffing = False self.dept_membership_requests = [] self.requested_depts = [] self.dept_memberships = [] self.ribbon = remove_opt(self.ribbon_ints, c.VOLUNTEER_RIBBON) if self.badge_type == c.STAFF_BADGE: self.badge_type = c.ATTENDEE_BADGE self.badge_num = None del self.shifts[:] @property def ribbon_labels(self): labels = super(Attendee, self)._labels('ribbon', self.ribbon) if c.DEPT_HEAD_RIBBON in self.ribbon_ints or not self.is_dept_head: return labels labels.append(c.RIBBONS[c.DEPT_HEAD_RIBBON]) return sorted(labels) @property def ribbon_and_or_badge(self): if self.ribbon and self.badge_type != c.ATTENDEE_BADGE: return ' / '.join([self.badge_type_label] + self.ribbon_labels) elif self.ribbon: return ' / '.join(self.ribbon_labels) else: return self.badge_type_label @property def badge_type_real(self): return get_real_badge_type(self.badge_type) @cost_property def badge_cost(self): return self.calculate_badge_cost() @property def badge_cost_without_promo_code(self): return self.calculate_badge_cost(use_promo_code=False) def calculate_badge_cost(self, use_promo_code=True): if self.paid == c.NEED_NOT_PAY: return 0 elif self.overridden_price is not None: return self.overridden_price elif self.base_badge_price: cost = self.base_badge_price else: cost = self.new_badge_cost if c.BADGE_PROMO_CODES_ENABLED and self.promo_code and use_promo_code: return self.promo_code.calculate_discounted_price(cost) else: return cost @property def new_badge_cost(self): # What this badge would cost if it were new, i.e., not taking into # account special overrides registered = self.registered_local if self.registered else None if self.is_dealer: return c.DEALER_BADGE_PRICE elif self.badge_type == c.ONE_DAY_BADGE: return c.get_oneday_price(registered) elif self.is_presold_oneday: return c.get_presold_oneday_price(self.badge_type) elif self.badge_type in c.BADGE_TYPE_PRICES: return int(c.BADGE_TYPE_PRICES[self.badge_type]) elif self.age_discount != 0: return max(0, c.get_attendee_price(registered) + self.age_discount) elif self.group and self.paid == c.PAID_BY_GROUP: return c.get_attendee_price(registered) - c.GROUP_DISCOUNT else: return c.get_attendee_price(registered) @property def promo_code_code(self): """ Convenience property for accessing `promo_code.code` if available. Returns: str: `promo_code.code` if `promo_code` is not `None`, empty string otherwise. """ return self.promo_code.code if self.promo_code else '' @property def age_discount(self): return -self.age_group_conf['discount'] @property def age_group_conf(self): if self.birthdate: day = c.EPOCH.date() \ if date.today() <= c.EPOCH.date() \ else localized_now().date() attendee_age = get_age_from_birthday(self.birthdate, day) for val, age_group in c.AGE_GROUP_CONFIGS.items(): if val != c.AGE_UNKNOWN and \ age_group['min_age'] <= attendee_age and \ attendee_age <= age_group['max_age']: return age_group return c.AGE_GROUP_CONFIGS[int(self.age_group or c.AGE_UNKNOWN)] @property def total_cost(self): return self.default_cost + self.amount_extra @property def total_donation(self): return self.total_cost - self.badge_cost @cost_property def donation_cost(self): return self.extra_donation or 0 @property def amount_unpaid(self): if self.paid == c.PAID_BY_GROUP: personal_cost = max(0, self.total_cost - self.badge_cost) else: personal_cost = self.total_cost return max(0, personal_cost - self.amount_paid) @property def is_unpaid(self): return self.paid == c.NOT_PAID @property def is_unassigned(self): return not self.first_name @property def is_dealer(self): return c.DEALER_RIBBON in self.ribbon_ints or \ self.badge_type == c.PSEUDO_DEALER_BADGE or ( self.group and self.group.is_dealer and self.paid == c.PAID_BY_GROUP) @property def is_checklist_admin(self): return any(m.is_checklist_admin for m in self.dept_memberships) @property def is_dept_head(self): return any(m.is_dept_head for m in self.dept_memberships) @property def is_presold_oneday(self): """ Returns a boolean indicating whether this is a c.FRIDAY/c.SATURDAY/etc badge; see the presell_one_days config option for a full explanation. """ return self.badge_type_label in c.DAYS_OF_WEEK @property def is_not_ready_to_checkin(self): """ Returns None if we are ready for checkin, otherwise a short error message why we can't check them in. """ if self.paid == c.NOT_PAID: return "Not paid" # When someone claims an unassigned group badge on-site, they first # fill out a new registration which is paid-by-group but isn't assigned # to a group yet (the admin does that when they check in). if self.badge_status != c.COMPLETED_STATUS and not ( self.badge_status == c.NEW_STATUS and self.paid == c.PAID_BY_GROUP and not self.group_id): return "Badge status" if self.is_unassigned: return "Badge not assigned" if self.is_presold_oneday: if self.badge_type_label != localized_now().strftime('%A'): return "Wrong day" return None @property def shirt_size_marked(self): return self.shirt not in [c.NO_SHIRT, c.SIZE_UNKNOWN] @property def shirt_info_marked(self): return self.shirt_size_marked and (not self.gets_staff_shirt or self.second_shirt != c.UNKNOWN) @property def is_group_leader(self): return self.group and self.id == self.group.leader_id @property def unassigned_name(self): if self.group_id and self.is_unassigned: return '[Unassigned {self.badge}]'.format(self=self) @hybrid_property def full_name(self): return self.unassigned_name or \ '{self.first_name} {self.last_name}'.format(self=self) @full_name.expression def full_name(cls): return case( [( or_(cls.first_name == None, cls.first_name == ''), # noqa: E711 'zzz')], else_=func.lower(cls.first_name + ' ' + cls.last_name)) @hybrid_property def last_first(self): return self.unassigned_name or \ '{self.last_name}, {self.first_name}'.format(self=self) @last_first.expression def last_first(cls): return case( [( or_(cls.first_name == None, cls.first_name == ''), # noqa: E711 'zzz')], else_=func.lower(cls.last_name + ', ' + cls.first_name)) @hybrid_property def normalized_email(self): return self.normalize_email(self.email) @normalized_email.expression def normalized_email(cls): return func.replace(func.lower(func.trim(cls.email)), '.', '') @classmethod def normalize_email(cls, email): return email.strip().lower().replace('.', '') @property def watchlist_guess(self): try: from uber.models import Session with Session() as session: watchentries = session.guess_attendee_watchentry(self) return [w.to_dict() for w in watchentries] except Exception as ex: log.warning('Error guessing watchlist entry: {}', ex) return None @property def banned(self): return listify(self.watch_list or self.watchlist_guess) @property def badge(self): if self.paid == c.NOT_PAID: badge = 'Unpaid ' + self.badge_type_label elif self.badge_num: badge = '{} #{}'.format(self.badge_type_label, self.badge_num) else: badge = self.badge_type_label if self.ribbon: badge += ' ({})'.format(", ".join(self.ribbon_labels)) return badge @property def is_transferable(self): return not self.is_new and \ not self.checked_in and \ self.paid in [c.HAS_PAID, c.PAID_BY_GROUP] and \ self.badge_type in c.TRANSFERABLE_BADGE_TYPES and \ not self.admin_account and \ not self.has_role_somewhere @property def paid_for_a_shirt(self): return self.amount_extra >= c.SHIRT_LEVEL @property def volunteer_event_shirt_eligible(self): """ Returns a truthy value if this attendee either automatically gets a complementary event shirt for being staff OR if they've eligible for a complementary event shirt if they end up working enough volunteer hours """ # Some events want to exclude staff badges from getting event shirts # (typically because they are getting staff uniform shirts instead). if self.badge_type == c.STAFF_BADGE: return c.STAFF_ELIGIBLE_FOR_SWAG_SHIRT else: return c.VOLUNTEER_RIBBON in self.ribbon_ints @property def volunteer_event_shirt_earned(self): return self.volunteer_event_shirt_eligible and ( not self.takes_shifts or self.worked_hours >= 6) @property def replacement_staff_shirts(self): """ Staffers can choose whether or not they want to swap out one of their staff shirts for an event shirt. By default and if the staffer opts into this, we deduct 1 staff shirt from the staff shirt count and add 1 to the event shirt count. """ is_replaced = self.second_shirt in [c.UNKNOWN, c.STAFF_AND_EVENT_SHIRT] return 1 if self.gets_staff_shirt and is_replaced else 0 @property def num_event_shirts_owed(self): return sum([ int(self.paid_for_a_shirt), int(self.volunteer_event_shirt_eligible), self.replacement_staff_shirts ]) @property def gets_staff_shirt(self): return self.badge_type == c.STAFF_BADGE @property def num_staff_shirts_owed(self): return 0 if not self.gets_staff_shirt else ( c.SHIRTS_PER_STAFFER - self.replacement_staff_shirts) @property def gets_any_kind_of_shirt(self): return self.gets_staff_shirt or self.num_event_shirts_owed > 0 @property def has_personalized_badge(self): return self.badge_type in c.PREASSIGNED_BADGE_TYPES @property def donation_swag(self): donation_items = [ desc for amount, desc in sorted(c.DONATION_TIERS.items()) if amount and self.amount_extra >= amount ] extra_donations = \ ['Extra donation of ${}'.format(self.extra_donation)] \ if self.extra_donation else [] return donation_items + extra_donations @property def merch(self): """ Here is the business logic surrounding shirts: - People who kick in enough to get a shirt get an event shirt. - People with staff badges get a configurable number of staff shirts. - Volunteers who meet the requirements get a complementary event shirt (NOT a staff shirt). """ merch = self.donation_swag if self.volunteer_event_shirt_eligible: shirt = c.DONATION_TIERS[c.SHIRT_LEVEL] if self.paid_for_a_shirt: shirt = 'a 2nd ' + shirt if not self.volunteer_event_shirt_earned: shirt += (' (this volunteer must work at least 6 hours or ' 'they will be reported for picking up their shirt)') merch.append(shirt) if self.gets_staff_shirt: staff_shirts = '{} Staff Shirt{}'.format( c.SHIRTS_PER_STAFFER, 's' if c.SHIRTS_PER_STAFFER > 1 else '') if self.shirt_size_marked: staff_shirts += ' [{}]'.format(c.SHIRTS[self.shirt]) merch.append(staff_shirts) if self.staffing: merch.append('Staffer Info Packet') if self.extra_merch: merch.append(self.extra_merch) return comma_and(merch) @property def accoutrements(self): stuff = [] \ if not self.ribbon \ else ['a ' + s + ' ribbon' for s in self.ribbon_labels] if c.WRISTBANDS_ENABLED: stuff.append('a {} wristband'.format( c.WRISTBAND_COLORS[self.age_group])) if self.regdesk_info: stuff.append(self.regdesk_info) return (' with ' if stuff else '') + comma_and(stuff) @property def multiply_assigned(self): return len(self.dept_memberships) > 1 @property def takes_shifts(self): return bool(self.staffing and any(not d.is_shiftless for d in self.assigned_depts)) @property def hours(self): all_hours = set() for shift in self.shifts: all_hours.update(shift.job.hours) return all_hours @property def hour_map(self): all_hours = {} for shift in self.shifts: for hour in shift.job.hours: all_hours[hour] = shift.job return all_hours @cached_property def available_jobs(self): if not self.dept_memberships and not c.AT_THE_CON: return [] def _get_available_jobs(session, attendee_id): from uber.models.department import DeptMembership, Job, Shift job_filters = [] if c.AT_THE_CON else [ Job.department_id == DeptMembership.department_id, DeptMembership.attendee_id == self.id ] return session.query(Job) \ .outerjoin(Job.shifts) \ .filter(*job_filters) \ .group_by(Job.id) \ .having(func.count(Shift.id) < Job.slots) \ .options( subqueryload(Job.shifts), subqueryload(Job.required_roles)) \ .order_by(Job.start_time, Job.department_id).all() if self.session: jobs = _get_available_jobs(self.session, self.id) else: from uber.models import Session with Session() as session: jobs = _get_available_jobs(session, self.id) return [job for job in jobs if self.has_required_roles(job)] @cached_property def possible(self): assert self.session, ('{}.possible property may only be accessed for ' 'objects attached to a session'.format( self.__class__.__name__)) if not self.dept_memberships and not c.AT_THE_CON: return [] else: from uber.models.department import DeptMembership, Job job_filters = [] if c.AT_THE_CON else [ Job.department_id == DeptMembership.department_id, DeptMembership.attendee_id == self.id ] job_query = self.session.query(Job) \ .filter(*job_filters) \ .options( subqueryload(Job.department), subqueryload(Job.shifts), subqueryload(Job.department), subqueryload(Job.required_roles)) \ .order_by(Job.start_time, Job.department_id) return [ job for job in job_query if job.slots > len(job.shifts) and job.no_overlap(self) and ( job.type != c.SETUP or self.can_work_setup or job.department.is_setup_approval_exempt) and (job.type != c.TEARDOWN or self.can_work_teardown or job.department.is_teardown_approval_exempt) and ( not job.required_roles or self.has_required_roles(job)) ] @property def possible_opts(self): return [(job.id, '({}) [{}] {}'.format(hour_day_format(job.start_time), job.department_name, job.name)) for job in self.possible if localized_now() < job.start_time] @property def possible_and_current(self): jobs = [s.job for s in self.shifts] for job in jobs: job.taken = True jobs.extend(self.possible) return sorted(jobs, key=lambda j: j.start_time) # ======================================================================== # TODO: Refactor all this stuff regarding assigned_depts and # requested_depts. Maybe a @suffix_property with a setter for the # *_ids fields? The hardcoded *_labels props are also not great. # There's a bigger feature here that I haven't wrapped my head # around yet. A generic way to lazily set relations using ids. # ======================================================================== @classproperty def extra_apply_attrs(cls): return set(['assigned_depts_ids' ]).union(cls.extra_apply_attrs_restricted) @classproperty def extra_apply_attrs_restricted(cls): return set(['requested_depts_ids']) @property def assigned_depts_labels(self): return [d.name for d in self.assigned_depts] @property def requested_depts_labels(self): return [d.name for d in self.requested_depts] @property def assigned_depts_ids(self): _, ids = self._get_relation_ids('assigned_depts') return [str(d.id) for d in self.assigned_depts] if ids is None else ids @assigned_depts_ids.setter def assigned_depts_ids(self, value): values = set(s for s in listify(value) if s) for membership in list(self.dept_memberships): if membership.department_id not in values: # Manually remove dept_memberships to ensure the associated # rows in the dept_membership_dept_role table are deleted. self.dept_memberships.remove(membership) from uber.models.department import Department self._set_relation_ids('assigned_depts', Department, list(values)) @property def requested_depts_ids(self): return [ d.department_id or 'All' for d in self.dept_membership_requests ] @requested_depts_ids.setter def requested_depts_ids(self, value): from uber.models.department import DeptMembershipRequest values = set(None if s in ('None', 'All') else s for s in listify(value) if s != '') for membership in list(self.dept_membership_requests): if membership.department_id not in values: self.dept_membership_requests.remove(membership) department_ids = set( str(d.department_id) for d in self.dept_membership_requests) for department_id in values: if department_id not in department_ids: self.dept_membership_requests.append( DeptMembershipRequest(department_id=department_id, attendee_id=self.id)) @property def worked_shifts(self): return [s for s in self.shifts if s.worked == c.SHIFT_WORKED] @property def weighted_hours(self): weighted_hours = sum(s.job.weighted_hours for s in self.shifts) return weighted_hours + self.nonshift_hours @department_id_adapter def weighted_hours_in(self, department_id): if not department_id: return self.weighted_hours return sum(shift.job.weighted_hours for shift in self.shifts if shift.job.department_id == department_id) @property def worked_hours(self): weighted_hours = sum(s.job.real_duration * s.job.weight for s in self.worked_shifts) return weighted_hours + self.nonshift_hours @department_id_adapter def dept_membership_for(self, department_id): if not department_id: return None for m in self.dept_memberships: if m.department_id == department_id: return m return None @department_id_adapter def requested(self, department_id): if not department_id or department_id == 'All': department_id = None return any(m.department_id == department_id for m in self.dept_membership_requests) @department_id_adapter def assigned_to(self, department_id): if not department_id: return False return any(m.department_id == department_id for m in self.dept_memberships) def trusted_in(self, department): return self.has_role_in(department) def can_admin_dept_for(self, department): return (self.admin_account and c.ACCOUNTS in self.admin_account.access_ints) \ or self.has_inherent_role_in(department) def can_dept_head_for(self, department): return (self.admin_account and c.ACCOUNTS in self.admin_account.access_ints) \ or self.is_dept_head_of(department) @property def can_admin_checklist(self): return (self.admin_account and c.ACCOUNTS in self.admin_account.access_ints) \ or bool(self.dept_memberships_where_can_admin_checklist) @department_id_adapter def can_admin_checklist_for(self, department_id): if not department_id: return False return (self.admin_account and c.ACCOUNTS in self.admin_account.access_ints) \ or any( m.department_id == department_id for m in self.dept_memberships_where_can_admin_checklist) @department_id_adapter def is_checklist_admin_of(self, department_id): if not department_id: return False return any(m.department_id == department_id and m.is_checklist_admin for m in self.dept_memberships) @department_id_adapter def is_dept_head_of(self, department_id): if not department_id: return False return any(m.department_id == department_id and m.is_dept_head for m in self.dept_memberships) @department_id_adapter def is_poc_of(self, department_id): if not department_id: return False return any(m.department_id == department_id and m.is_poc for m in self.dept_memberships) def checklist_item_for_slug(self, slug): for item in self.dept_checklist_items: if item.slug == slug: return item return None def completed_every_checklist_for(self, slug): return all( d.checklist_item_for_slug(slug) for d in self.checklist_depts) @property def gets_any_checklist(self): return bool(self.dept_memberships_as_checklist_admin) def has_role(self, role): return any(r.id == role.id for r in self.dept_roles) @department_id_adapter def has_inherent_role_in(self, department_id): if not department_id: return False return any(m.department_id == department_id for m in self.dept_memberships_with_inherent_role) @department_id_adapter def has_role_in(self, department_id): if not department_id: return False return any(m.department_id == department_id for m in self.dept_memberships_with_role) def has_required_roles(self, job): if not job.required_roles: return True required_role_ids = set(r.id for r in job.required_roles) role_ids = set(r.id for r in self.dept_roles) return required_role_ids.issubset(role_ids) @property def has_role_somewhere(self): """ Returns True if at least one of the following is true for at least one department: - is a department head - is a point of contact - is a checklist admin - has a dept role """ return bool(self.dept_memberships_with_role) def has_shifts_in(self, department): return department in self.depts_where_working @property def food_restrictions_filled_out(self): return self.food_restrictions if c.STAFF_GET_FOOD else True @property def shift_prereqs_complete(self): return not self.placeholder and \ self.food_restrictions_filled_out and self.shirt_info_marked @property def past_years_json(self): return json.loads(self.past_years or '[]') @property def must_contact(self): dept_chairs = [] for dept in self.depts_where_working: poc_names = ' / '.join(sorted(poc.full_name for poc in dept.pocs)) dept_chairs.append('({}) {}'.format(dept.name, poc_names)) return safe_string('<br/>'.join(sorted(dept_chairs)))
class MarketplaceApplication(MagModel): MATCHING_DEALER_FIELDS = [ 'categories', 'categories_text', 'description', 'special_needs' ] attendee_id = Column(UUID, ForeignKey('attendee.id', ondelete='SET NULL'), nullable=True) attendee = relationship('Attendee', foreign_keys=attendee_id, cascade='save-update, merge', backref=backref('marketplace_applications', cascade='save-update, merge')) business_name = Column(UnicodeText) status = Column(Choice(c.MARKETPLACE_STATUS_OPTS), default=c.UNAPPROVED, admin_only=True) registered = Column(UTCDateTime, server_default=utcnow()) approved = Column(UTCDateTime, nullable=True) categories = Column(MultiChoice(c.DEALER_WARES_OPTS)) categories_text = Column(UnicodeText) description = Column(UnicodeText) special_needs = Column(UnicodeText) admin_notes = Column(UnicodeText, admin_only=True) base_price = Column(Integer, default=0, admin_only=True) overridden_price = Column(Integer, nullable=True, admin_only=True) amount_paid = Column(Integer, default=0, index=True, admin_only=True) email_model_name = 'app' @presave_adjustment def _cost_adjustments(self): self.base_price = self.default_cost if self.overridden_price == '': self.overridden_price = None @property def incomplete_reason(self): if self.status not in [c.APPROVED, c.PAID]: return self.status_label if self.attendee.placeholder: return "Missing registration info" @cost_property def app_fee(self): return c.MARKETPLACE_FEE @property def total_cost(self): if self.status not in [c.APPROVED, c.PAID]: return 0 else: return self.potential_cost @property def potential_cost(self): if self.overridden_price is not None: return self.overridden_price else: return self.base_price or self.default_cost or 0 @property def amount_unpaid(self): return max(0, self.total_cost - self.amount_paid) @property def email(self): return self.attendee.email @property def is_unpaid(self): return self.status == c.APPROVED
class AdminAccount(MagModel): attendee_id = Column(UUID, ForeignKey('attendee.id'), unique=True) hashed = Column(UnicodeText, private=True) access = Column(MultiChoice(c.ACCESS_OPTS)) password_reset = relationship('PasswordReset', backref='admin_account', uselist=False) api_tokens = relationship('ApiToken', backref='admin_account') active_api_tokens = relationship( 'ApiToken', primaryjoin='and_(' 'AdminAccount.id == ApiToken.admin_account_id, ' 'ApiToken.revoked_time == None)') judge = relationship('IndieJudge', uselist=False, backref='admin_account') def __repr__(self): return '<{}>'.format(self.attendee.full_name) @staticmethod def admin_name(): try: from uber.models import Session with Session() as session: return session.admin_attendee().full_name except Exception as ex: return None @staticmethod def admin_email(): try: from uber.models import Session with Session() as session: return session.admin_attendee().email except Exception as ex: return None @staticmethod def access_set(id=None): try: from uber.models import Session with Session() as session: id = id or cherrypy.session['account_id'] return set(session.admin_account(id).access_ints) except Exception as ex: return set() def _allowed_opts(self, opts, required_access): access_opts = [] admin_access = set(self.access_ints) for access, label in opts: required = set(required_access.get(access, [])) if not required or any(a in required for a in admin_access): access_opts.append((access, label)) return access_opts @property def allowed_access_opts(self): return self._allowed_opts(c.ACCESS_OPTS, c.REQUIRED_ACCESS) @property def allowed_api_access_opts(self): required_access = {a: [a] for a in c.API_ACCESS.keys()} return self._allowed_opts(c.API_ACCESS_OPTS, required_access) @property def is_admin(self): return c.ADMIN in self.access_ints @presave_adjustment def _disable_api_access(self): new_access = set(int(s) for s in self.access.split(',') if s) old_access = set( int(s) for s in self.orig_value_of('access').split(',') if s) removed = old_access.difference(new_access) removed_api = set(a for a in c.API_ACCESS.keys() if a in removed) if removed_api: revoked_time = datetime.utcnow() for api_token in self.active_api_tokens: if removed_api.intersection(api_token.access_ints): api_token.revoked_time = revoked_time
class PanelApplication(MagModel): event_id = Column(UUID, ForeignKey('event.id', ondelete='SET NULL'), nullable=True) poc_id = Column(UUID, ForeignKey('attendee.id', ondelete='SET NULL'), nullable=True) name = Column(UnicodeText) length = Column(Choice(c.PANEL_LENGTH_OPTS), default=c.SIXTY_MIN) length_text = Column(UnicodeText) length_reason = Column(UnicodeText) description = Column(UnicodeText) unavailable = Column(UnicodeText) available = Column(UnicodeText) affiliations = Column(UnicodeText) past_attendance = Column(UnicodeText) presentation = Column(Choice(c.PRESENTATION_OPTS)) other_presentation = Column(UnicodeText) tech_needs = Column(MultiChoice(c.TECH_NEED_OPTS)) other_tech_needs = Column(UnicodeText) need_tables = Column(Boolean, default=False) tables_desc = Column(UnicodeText) has_cost = Column(Boolean, default=False) is_loud = Column(Boolean, default=False) tabletop = Column(Boolean, default=False) cost_desc = Column(UnicodeText) livestream = Column(Choice(c.LIVESTREAM_OPTS), default=c.OPT_IN) panelist_bringing = Column(UnicodeText) extra_info = Column(UnicodeText) applied = Column(UTCDateTime, server_default=utcnow()) status = Column(Choice(c.PANEL_APP_STATUS_OPTS), default=c.PENDING, admin_only=True) comments = Column(UnicodeText, admin_only=True) applicants = relationship('PanelApplicant', backref='application') email_model_name = 'app' @property def email(self): return self.submitter and self.submitter.email @property def submitter(self): for a in self.applicants: if a.submitter: return a return None @property def other_panelists(self): return [a for a in self.applicants if not a.submitter] @property def matched_attendees(self): return [a.attendee for a in self.applicants if a.attendee_id] @property def unmatched_applicants(self): return [a for a in self.applicants if not a.attendee_id] @hybrid_property def has_been_accepted(self): return self.status == c.ACCEPTED
class Group(MagModel, TakesPaymentMixin): public_id = Column(UUID, default=lambda: str(uuid4())) name = Column(UnicodeText) tables = Column(Numeric, default=0) zip_code = Column(UnicodeText) address1 = Column(UnicodeText) address2 = Column(UnicodeText) city = Column(UnicodeText) region = Column(UnicodeText) country = Column(UnicodeText) website = Column(UnicodeText) wares = Column(UnicodeText) categories = Column(MultiChoice(c.DEALER_WARES_OPTS)) categories_text = Column(UnicodeText) description = Column(UnicodeText) special_needs = Column(UnicodeText) amount_paid = Column(Integer, default=0, index=True, admin_only=True) amount_refunded = Column(Integer, default=0, admin_only=True) 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) admin_notes = Column(UnicodeText, admin_only=True) status = Column(Choice(c.DEALER_STATUS_OPTS), default=c.UNAPPROVED, admin_only=True) registered = Column(UTCDateTime, server_default=utcnow()) approved = Column(UTCDateTime, nullable=True) leader_id = Column(UUID, ForeignKey('attendee.id', use_alter=True, name='fk_leader'), nullable=True) leader = relationship('Attendee', foreign_keys=leader_id, post_update=True, cascade='all') _repr_attr_names = ['name'] @presave_adjustment def _cost_and_leader(self): assigned = [a for a in self.attendees if not a.is_unassigned] if len(assigned) == 1: [self.leader] = assigned if self.auto_recalc: self.cost = self.default_cost elif not self.cost: self.cost = 0 if not self.amount_paid: self.amount_paid = 0 if not self.amount_refunded: self.amount_refunded = 0 if self.status == c.APPROVED and not self.approved: self.approved = datetime.now(UTC) if self.leader and self.is_dealer: self.leader.ribbon = add_opt(self.leader.ribbon_ints, c.DEALER_RIBBON) if not self.is_unpaid: for a in self.attendees: a.presave_adjustments() @property def sorted_attendees(self): return list( sorted(self.attendees, key=lambda a: (a.is_unassigned, a.id != self.leader_id, a.full_name))) @property def unassigned(self): """ Returns a list of the unassigned badges for this group, sorted so that the paid-by-group badges come last, because when claiming unassigned badges we want to claim the "weird" ones first. """ unassigned = [a for a in self.attendees if a.is_unassigned] return sorted(unassigned, key=lambda a: a.paid == c.PAID_BY_GROUP) @property def floating(self): """ Returns the list of paid-by-group unassigned badges for this group. This is a separate property from the "Group.unassigned" property because when automatically adding or removing unassigned badges, we care specifically about paid-by-group badges rather than all unassigned badges. """ return [ a for a in self.attendees if a.is_unassigned and a.paid == c.PAID_BY_GROUP ] @property def new_ribbon(self): return c.DEALER_RIBBON if self.is_dealer else '' @property def ribbon_and_or_badge(self): badge = self.unassigned[0] if badge.ribbon and badge.badge_type != c.ATTENDEE_BADGE: return ' / '.join([badge.badge_type_label] + self.ribbon_labels) elif badge.ribbon: return ' / '.join(badge.ribbon_labels) else: return badge.badge_type_label @property def is_dealer(self): return bool(self.tables and self.tables != '0' and self.tables != '0.0' and (not self.registered or self.amount_paid or self.cost)) @property def is_unpaid(self): return self.cost > 0 and self.amount_paid == 0 @property def email(self): if self.leader and self.leader.email: return self.leader.email elif self.leader_id: # unattached groups [leader] = [a for a in self.attendees if a.id == self.leader_id] return leader.email else: emails = [a.email for a in self.attendees if a.email] if len(emails) == 1: return emails[0] @property def badges_purchased(self): return len([a for a in self.attendees if a.paid == c.PAID_BY_GROUP]) @property def badges(self): return len(self.attendees) @property def unregistered_badges(self): return len([a for a in self.attendees if a.is_unassigned]) @cost_property def table_cost(self): table_count = int(float(self.tables)) return sum(c.TABLE_PRICES[i] for i in range(1, 1 + table_count)) @property def new_badge_cost(self): return c.DEALER_BADGE_PRICE if self.is_dealer else c.get_group_price() @cost_property def badge_cost(self): total = 0 for attendee in self.attendees: if attendee.paid == c.PAID_BY_GROUP: total += attendee.badge_cost return total @property def amount_extra(self): if self.is_new: return sum(a.total_cost - a.badge_cost for a in self.attendees if a.paid == c.PAID_BY_GROUP) else: return 0 @property def total_cost(self): return self.default_cost + self.amount_extra @property def amount_unpaid(self): if self.registered: return max(0, self.cost - self.amount_paid) else: return self.total_cost @property def dealer_max_badges(self): return math.ceil(self.tables) + 1 @property def dealer_badges_remaining(self): return self.dealer_max_badges - self.badges @property def hours_since_registered(self): if not self.registered: return 0 delta = datetime.now(UTC) - self.registered return max(0, delta.total_seconds()) / 60.0 / 60.0 @property def hours_remaining_in_grace_period(self): return max(0, c.GROUP_UPDATE_GRACE_PERIOD - self.hours_since_registered) @property def is_in_grace_period(self): return self.hours_remaining_in_grace_period > 0 @property def min_badges_addable(self): if self.can_add: return 1 elif self.is_dealer: return 0 else: return c.MIN_GROUP_ADDITION @property def requested_hotel_info(self): if self.leader: return self.leader.requested_hotel_info elif self.leader_id: # unattached groups for attendee in self.attendees: if attendee.id == self.leader_id: return attendee.requested_hotel_info else: return any(a.requested_hotel_info for a in self.attendees)
class MITSTimes(MagModel): team_id = Column(ForeignKey('mits_team.id')) availability = Column(MultiChoice(c.MITS_SCHEDULE_OPTS)) multiple_tables = Column(MultiChoice(c.MITS_SCHEDULE_OPTS))
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 GuestTravelPlans(MagModel): guest_id = Column(UUID, ForeignKey('guest_group.id'), unique=True) modes = Column(MultiChoice(c.GUEST_TRAVEL_OPTS)) modes_text = Column(UnicodeText) details = Column(UnicodeText)
class IndieGame(MagModel, ReviewMixin): studio_id = Column(UUID, ForeignKey('indie_studio.id')) title = Column(UnicodeText) brief_description = Column(UnicodeText) # 140 max genres = Column(MultiChoice(c.MIVS_INDIE_GENRE_OPTS)) platforms = Column(MultiChoice(c.MIVS_INDIE_PLATFORM_OPTS)) platforms_text = Column(UnicodeText) description = Column(UnicodeText) # 500 max how_to_play = Column(UnicodeText) # 1000 max link_to_video = Column(UnicodeText) link_to_game = Column(UnicodeText) password_to_game = Column(UnicodeText) code_type = Column(Choice(c.MIVS_CODE_TYPE_OPTS), default=c.NO_CODE) code_instructions = Column(UnicodeText) build_status = Column(Choice(c.MIVS_BUILD_STATUS_OPTS), default=c.PRE_ALPHA) build_notes = Column(UnicodeText) # 500 max shown_events = Column(UnicodeText) video_submitted = Column(Boolean, default=False) submitted = Column(Boolean, default=False) agreed_liability = Column(Boolean, default=False) agreed_showtimes = Column(Boolean, default=False) agreed_reminder1 = Column(Boolean, default=False) agreed_reminder2 = Column(Boolean, default=False) alumni_years = Column(MultiChoice(c.PREV_MIVS_YEAR_OPTS)) alumni_update = Column(UnicodeText) link_to_promo_video = Column(UnicodeText) link_to_webpage = Column(UnicodeText) twitter = Column(UnicodeText) facebook = Column(UnicodeText) other_social_media = Column(UnicodeText) tournament_at_event = Column(Boolean, default=False) tournament_prizes = Column(UnicodeText) has_multiplayer = Column(Boolean, default=False) player_count = Column(UnicodeText) # Length in minutes multiplayer_game_length = Column(Integer, nullable=True) leaderboard_challenge = Column(Boolean, default=False) status = Column(Choice(c.MIVS_GAME_STATUS_OPTS), default=c.NEW, admin_only=True) judge_notes = Column(UnicodeText, admin_only=True) registered = Column(UTCDateTime, server_default=utcnow()) waitlisted = Column(UTCDateTime, nullable=True) accepted = Column(UTCDateTime, 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' @presave_adjustment def accepted_time(self): if self.status == c.ACCEPTED and not self.accepted: self.accepted = datetime.now(UTC) @presave_adjustment def waitlisted_time(self): if self.status == c.WAITLISTED and not self.waitlisted: self.waitlisted = datetime.now(UTC) @property def email(self): return self.studio.email @property def reviews_to_email(self): return [review for review in self.reviews if review.send_to_studio] @property def video_href(self): return make_url(self.link_to_video) @property def href(self): return make_url(self.link_to_game) @property def screenshots(self): return [img for img in self.images if img.is_screenshot] @property def best_screenshots(self): return [ img for img in self.images if img.is_screenshot and img.use_in_promo ] def best_screenshot_downloads(self, count=2): all_images = reversed( sorted(self.images, key=lambda img: (img.is_screenshot and img.use_in_promo, img .is_screenshot, img.use_in_promo))) screenshots = [] for i, screenshot in enumerate(all_images): if os.path.exists(screenshot.filepath): screenshots.append(screenshot) if len(screenshots) >= count: break return screenshots def best_screenshot_download_filenames(self, count=2): nonchars = re.compile(r'[\W]+') best_screenshots = self.best_screenshot_downloads(count) screenshots = [] for i, screenshot in enumerate(best_screenshots): if os.path.exists(screenshot.filepath): name = '_'.join([s for s in self.title.lower().split() if s]) name = nonchars.sub('', name) filename = '{}_{}.{}'.format(name, len(screenshots) + 1, screenshot.extension.lower()) screenshots.append(filename) if len(screenshots) >= count: break return screenshots + ([''] * (count - len(screenshots))) @property def promo_image(self): return next( iter([img for img in self.images if not img.is_screenshot]), None) @property def missing_steps(self): steps = [] if not self.link_to_game: steps.append( 'You have not yet included a link to where the judges can ' 'access your game') if self.code_type != c.NO_CODE and self.link_to_game: if not self.codes: steps.append( 'You have not yet attached any codes to this game for ' 'our judges to use') elif not self.unlimited_code \ and len(self.codes) < c.MIVS_CODES_REQUIRED: steps.append( 'You have not attached the {} codes you must provide ' 'for our judges'.format(c.MIVS_CODES_REQUIRED)) if not self.agreed_showtimes: steps.append( 'You must agree to the showtimes detailed on the game form') if not self.agreed_liability: steps.append( 'You must check the box that agrees to our liability waiver') return steps @property def video_broken(self): for r in self.reviews: if r.video_status == c.BAD_LINK: return True @property def unlimited_code(self): for code in self.codes: if code.unlimited_use: return code @property def video_submittable(self): return bool(self.link_to_video) @property def submittable(self): return not self.missing_steps @property def scores(self): return [r.game_score for r in self.reviews if r.game_score] @property def score_sum(self): return sum(self.scores, 0) @property def average_score(self): return (self.score_sum / len(self.scores)) if self.scores else 0 @property def has_issues(self): return any(r.has_issues for r in self.reviews) @property def confirmed(self): return self.status == c.ACCEPTED \ and self.studio \ and self.studio.group_id
class GuestMerch: extra_merch_time = Column(MultiChoice(c.EXTRA_MERCH_TIME_OPTS))