Example #1
0
class ReceiptItem(MagModel):
    attendee_id = Column(UUID,
                         ForeignKey('attendee.id', ondelete='SET NULL'),
                         nullable=True)
    attendee = relationship(Attendee,
                            backref='receipt_items',
                            foreign_keys=attendee_id,
                            cascade='save-update,merge,refresh-expire,expunge')

    group_id = Column(UUID,
                      ForeignKey('group.id', ondelete='SET NULL'),
                      nullable=True)
    group = relationship(Group,
                         backref='receipt_items',
                         foreign_keys=group_id,
                         cascade='save-update,merge,refresh-expire,expunge')

    txn_id = Column(UUID,
                    ForeignKey('stripe_transaction.id', ondelete='SET NULL'),
                    nullable=True)
    stripe_transaction = relationship(
        StripeTransaction,
        backref='receipt_items',
        foreign_keys=txn_id,
        cascade='save-update,merge,refresh-expire,expunge')
    txn_type = Column(Choice(c.TRANSACTION_TYPE_OPTS), default=c.PAYMENT)
    payment_method = Column(Choice(c.PAYMENT_METHOD_OPTS), default=c.STRIPE)
    amount = Column(Integer)
    when = Column(UTCDateTime, default=lambda: datetime.now(UTC))
    who = Column(UnicodeText)
    desc = Column(UnicodeText)
    cost_snapshot = Column(JSON, default={}, server_default='{}')
    refund_snapshot = Column(JSON, default={}, server_default='{}')
Example #2
0
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)
    cost_desc = Column(UnicodeText)
    livestream = Column(Choice(c.LIVESTREAM_OPTS), default=c.DONT_CARE)
    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]
Example #3
0
class Shift(MagModel):
    job_id = Column(UUID, ForeignKey('job.id', ondelete='cascade'))
    attendee_id = Column(UUID, ForeignKey('attendee.id', ondelete='cascade'))
    worked = Column(Choice(c.WORKED_STATUS_OPTS), default=c.SHIFT_UNMARKED)
    rating = Column(Choice(c.RATING_OPTS), default=c.UNRATED)
    comment = Column(UnicodeText)

    @property
    def name(self):
        return "{}'s {!r} shift".format(self.attendee.full_name, self.job.name)
Example #4
0
class IndieGameReview(MagModel):
    game_id = Column(UUID, ForeignKey('indie_game.id'))
    judge_id = Column(UUID, ForeignKey('indie_judge.id'))
    video_status = Column(Choice(c.MIVS_VIDEO_REVIEW_STATUS_OPTS),
                          default=c.PENDING)
    game_status = Column(Choice(c.MIVS_GAME_REVIEW_STATUS_OPTS),
                         default=c.PENDING)
    game_content_bad = Column(Boolean, default=False)
    video_score = Column(Choice(c.MIVS_VIDEO_REVIEW_OPTS), default=c.PENDING)
    video_review = Column(UnicodeText)

    # 0 = not reviewed, 1-10 score (10 is best)
    readiness_score = Column(Integer, default=0)
    design_score = Column(Integer, default=0)
    enjoyment_score = Column(Integer, default=0)
    game_review = Column(UnicodeText)
    developer_response = Column(UnicodeText)
    staff_notes = Column(UnicodeText)
    send_to_studio = Column(Boolean, default=False)

    __table_args__ = (UniqueConstraint('game_id',
                                       'judge_id',
                                       name='review_game_judge_uniq'), )

    @presave_adjustment
    def no_score_if_broken(self):
        if self.has_video_issues:
            self.video_score = c.PENDING

    @property
    def game_score(self):
        if self.has_game_issues or not (self.readiness_score
                                        and self.design_score
                                        and self.enjoyment_score):
            return 0
        return sum([
            self.readiness_score, self.design_score, self.enjoyment_score
        ]) / float(3)

    @property
    def has_video_issues(self):
        return self.video_status in c.MIVS_PROBLEM_STATUSES

    @property
    def has_game_issues(self):
        if self.game_status != c.COULD_NOT_PLAY:
            return self.game_status in c.MIVS_PROBLEM_STATUSES

    @property
    def has_issues(self):
        return self.has_video_issues or self.has_game_issues
Example #5
0
class IndieJudge(MagModel, ReviewMixin):
    admin_id = Column(UUID, ForeignKey('admin_account.id'))
    status = Column(Choice(c.MIVS_JUDGE_STATUS_OPTS), default=c.UNCONFIRMED)
    no_game_submission = Column(Boolean, nullable=True)
    genres = Column(MultiChoice(c.MIVS_INDIE_JUDGE_GENRE_OPTS))
    platforms = Column(MultiChoice(c.MIVS_INDIE_PLATFORM_OPTS))
    platforms_text = Column(UnicodeText)
    staff_notes = Column(UnicodeText)

    codes = relationship('IndieGameCode', backref='judge')
    reviews = relationship('IndieGameReview', backref='judge')

    email_model_name = 'judge'

    @property
    def judging_complete(self):
        return len(self.reviews) == len(self.game_reviews)

    @property
    def mivs_all_genres(self):
        return c.MIVS_ALL_GENRES in self.genres_ints

    @property
    def attendee(self):
        return self.admin_account.attendee

    @property
    def full_name(self):
        return self.attendee.full_name

    @property
    def email(self):
        return self.attendee.email
Example #6
0
class Event(MagModel):
    location = Column(Choice(c.EVENT_LOCATION_OPTS))
    start_time = Column(UTCDateTime)
    duration = Column(Integer)  # half-hour increments
    name = Column(UnicodeText, nullable=False)
    description = Column(UnicodeText)

    assigned_panelists = relationship('AssignedPanelist', backref='event')
    applications = relationship('PanelApplication', backref='event')
    panel_feedback = relationship('EventFeedback', backref='event')
    tournaments = relationship('TabletopTournament',
                               backref='event',
                               uselist=False)
    guest = relationship('GuestGroup', backref='event')

    @property
    def half_hours(self):
        half_hours = set()
        for i in range(self.duration):
            half_hours.add(self.start_time + timedelta(minutes=30 * i))
        return half_hours

    @property
    def minutes(self):
        return (self.duration or 0) * 30

    @property
    def start_slot(self):
        if self.start_time:
            start_delta = self.start_time_local - c.EPOCH
            return int(start_delta.total_seconds() / (60 * 30))

    @property
    def end_time(self):
        return self.start_time + timedelta(minutes=self.minutes)
Example #7
0
class MITSApplicant(MagModel):
    team_id = Column(ForeignKey('mits_team.id'))
    attendee_id = Column(ForeignKey('attendee.id'), nullable=True)
    primary_contact = Column(Boolean, default=False)
    first_name = Column(UnicodeText)
    last_name = Column(UnicodeText)
    email = Column(UnicodeText)
    cellphone = Column(UnicodeText)
    contact_method = Column(Choice(c.MITS_CONTACT_OPTS), default=c.TEXTING)

    declined_hotel_space = Column(Boolean, default=False)
    requested_room_nights = Column(MultiChoice(c.MITS_ROOM_NIGHT_OPTS),
                                   default='')

    email_model_name = 'applicant'

    @property
    def email_to_address(self):
        if self.attendee:
            return self.attendee.email
        return self.email

    @property
    def full_name(self):
        return self.first_name + ' ' + self.last_name

    def has_requested(self, night):
        return night in self.requested_room_nights_ints
Example #8
0
class EventFeedback(MagModel):
    event_id = Column(UUID, ForeignKey('event.id'))
    attendee_id = Column(UUID, ForeignKey('attendee.id', ondelete='cascade'))
    headcount_starting = Column(Integer, default=0)
    headcount_during = Column(Integer, default=0)
    comments = Column(UnicodeText)
    rating = Column(Choice(c.PANEL_RATING_OPTS), default=c.UNRATED)
Example #9
0
class AttendeeTournament(MagModel):
    first_name = Column(UnicodeText)
    last_name = Column(UnicodeText)
    email = Column(UnicodeText)
    cellphone = Column(UnicodeText)
    game = Column(UnicodeText)
    availability = Column(MultiChoice(c.TOURNAMENT_AVAILABILITY_OPTS))
    format = Column(UnicodeText)
    experience = Column(UnicodeText)
    needs = Column(UnicodeText)
    why = Column(UnicodeText)
    volunteering = Column(Boolean, default=False)

    status = Column(Choice(c.TOURNAMENT_STATUS_OPTS),
                    default=c.NEW,
                    admin_only=True)

    email_model_name = 'app'

    @property
    def full_name(self):
        return self.first_name + ' ' + self.last_name

    @property
    def matching_attendee(self):
        return self.session.query(Attendee).filter(
            Attendee.first_name == self.first_name.title(),
            Attendee.last_name == self.last_name.title(),
            func.lower(Attendee.email) == self.email.lower()).first()
Example #10
0
class IndieStudio(MagModel):
    group_id = Column(UUID, ForeignKey('group.id'), nullable=True)
    name = Column(UnicodeText, unique=True)
    address = Column(UnicodeText)
    website = Column(UnicodeText)
    twitter = Column(UnicodeText)
    facebook = Column(UnicodeText)
    status = Column(Choice(c.MIVS_STUDIO_STATUS_OPTS),
                    default=c.NEW,
                    admin_only=True)
    staff_notes = Column(UnicodeText, admin_only=True)
    registered = Column(UTCDateTime, server_default=utcnow())

    games = relationship('IndieGame',
                         backref='studio',
                         order_by='IndieGame.title')
    developers = relationship('IndieDeveloper',
                              backref='studio',
                              order_by='IndieDeveloper.last_name')

    email_model_name = 'studio'

    @property
    def confirm_deadline(self):
        sorted_games = sorted([g for g in self.games if g.accepted],
                              key=lambda g: g.accepted)
        confirm_deadline = timedelta(days=c.MIVS_CONFIRM_DEADLINE)
        return sorted_games[0].accepted + confirm_deadline

    @property
    def after_confirm_deadline(self):
        return self.confirm_deadline < localized_now()

    @property
    def website_href(self):
        return make_url(self.website)

    @property
    def email(self):
        return [dev.email for dev in self.developers if dev.primary_contact]

    @property
    def primary_contact(self):
        return [dev for dev in self.developers if dev.primary_contact][0]

    @property
    def submitted_games(self):
        return [g for g in self.games if g.submitted]

    @property
    def comped_badges(self):
        game_count = len([g for g in self.games if g.status == c.ACCEPTED])
        return c.MIVS_INDIE_BADGE_COMPS * game_count

    @property
    def unclaimed_badges(self):
        claimed_count = len(
            [d for d in self.developers if not d.matching_attendee])
        return max(0, self.comped_badges - claimed_count)
Example #11
0
class Sale(MagModel):
    attendee_id = Column(UUID, ForeignKey('attendee.id', ondelete='set null'), nullable=True)
    what = Column(UnicodeText)
    cash = Column(Integer, default=0)
    mpoints = Column(Integer, default=0)
    when = Column(UTCDateTime, default=lambda: datetime.now(UTC))
    reg_station = Column(Integer, nullable=True)
    payment_method = Column(Choice(c.SALE_OPTS), default=c.MERCH)
Example #12
0
class StripeTransaction(MagModel):
    stripe_id = Column(UnicodeText, nullable=True)
    type = Column(Choice(c.TRANSACTION_TYPE_OPTS), default=c.PENDING)
    amount = Column(Integer)
    when = Column(UTCDateTime, default=lambda: datetime.now(UTC))
    who = Column(UnicodeText)
    desc = Column(UnicodeText)
    attendees = relationship('StripeTransactionAttendee', backref='stripe_transaction')
    groups = relationship('StripeTransactionGroup', backref='stripe_transaction')
Example #13
0
class StripeTransaction(MagModel):
    stripe_id = Column(UnicodeText, nullable=True)
    type = Column(Choice(c.TRANSACTION_TYPE_OPTS), default=c.PAYMENT)
    amount = Column(Integer)
    when = Column(UTCDateTime, default=lambda: datetime.now(UTC))
    who = Column(UnicodeText)
    desc = Column(UnicodeText)
    fk_id = Column(UUID)
    fk_model = Column(UnicodeText)
Example #14
0
class GuestPanel(MagModel):
    guest_id = Column(UUID, ForeignKey('guest_group.id'), unique=True)
    wants_panel = Column(Choice(c.GUEST_PANEL_OPTS), nullable=True)
    name = Column(UnicodeText)
    length = Column(UnicodeText)
    desc = Column(UnicodeText)
    tech_needs = Column(MultiChoice(c.TECH_NEED_OPTS))
    other_tech_needs = Column(UnicodeText)

    @property
    def status(self):
        return self.wants_panel_label
Example #15
0
class GuestCharity(MagModel):
    guest_id = Column(UUID, ForeignKey('guest_group.id'), unique=True)
    donating = Column(Choice(c.GUEST_CHARITY_OPTS), nullable=True)
    desc = Column(UnicodeText)

    @property
    def status(self):
        return self.donating_label

    @presave_adjustment
    def no_desc_if_not_donating(self):
        if self.donating == c.NOT_DONATING:
            self.desc = ''
Example #16
0
class MITSGame(MagModel):
    team_id = Column(ForeignKey('mits_team.id'))
    name = Column(UnicodeText)
    promo_blurb = Column(UnicodeText)
    description = Column(UnicodeText)
    genre = Column(UnicodeText)
    phase = Column(Choice(c.MITS_PHASE_OPTS))
    min_age = Column(Integer)
    min_players = Column(Integer, default=2)
    max_players = Column(Integer, default=4)
    personally_own = Column(Boolean, default=False)
    unlicensed = Column(Boolean, default=False)
    professional = Column(Boolean, default=False)
Example #17
0
class ArtShowPayment(MagModel):
    receipt_id = Column(UUID,
                        ForeignKey('art_show_receipt.id', ondelete='SET NULL'),
                        nullable=True)
    receipt = relationship('ArtShowReceipt',
                           foreign_keys=receipt_id,
                           cascade='save-update, merge',
                           backref=backref('art_show_payments',
                                           cascade='save-update, merge'))
    amount = Column(Integer, default=0)
    type = Column(Choice(c.ART_SHOW_PAYMENT_OPTS),
                  default=c.STRIPE,
                  admin_only=True)
    when = Column(UTCDateTime, default=lambda: datetime.now(UTC))
Example #18
0
class AttractionNotification(MagModel):
    attraction_event_id = Column(UUID, ForeignKey('attraction_event.id'))
    attraction_id = Column(UUID, ForeignKey('attraction.id'))
    attendee_id = Column(UUID, ForeignKey('attendee.id'))

    notification_type = Column(Choice(Attendee._NOTIFICATION_PREF_OPTS))
    ident = Column(UnicodeText, index=True)
    sid = Column(UnicodeText)
    sent_time = Column(UTCDateTime, default=lambda: datetime.now(pytz.UTC))
    subject = Column(UnicodeText)
    body = Column(UnicodeText)

    @presave_adjustment
    def _fix_attraction_id(self):
        if not self.attraction_id and self.event:
            self.attraction_id = self.event.attraction_id
Example #19
0
class AttractionNotificationReply(MagModel):
    attraction_event_id = Column(UUID, ForeignKey('attraction_event.id'), nullable=True)
    attraction_id = Column(UUID, ForeignKey('attraction.id'), nullable=True)
    attendee_id = Column(UUID, ForeignKey('attendee.id'), nullable=True)

    notification_type = Column(Choice(Attendee._NOTIFICATION_PREF_OPTS))
    from_phonenumber = Column(UnicodeText)
    to_phonenumber = Column(UnicodeText)
    sid = Column(UnicodeText, index=True)
    received_time = Column(UTCDateTime, default=lambda: datetime.now(pytz.UTC))
    sent_time = Column(UTCDateTime, default=lambda: datetime.now(pytz.UTC))
    body = Column(UnicodeText)

    @presave_adjustment
    def _fix_attraction_id(self):
        if not self.attraction_id and self.event:
            self.attraction_id = self.event.attraction_id
Example #20
0
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')
    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 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)

    @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])
class PromoCodeWord(MagModel):
    """
    Words used to generate promo codes.

    Attributes:
        word (str): The text of this promo code word.
        normalized_word (str): A normalized version of `word`, suitable for
            database queries.
        part_of_speech (int): The part of speech that `word` is.
            Valid values are:

            * 0 `_ADJECTIVE`: `word` is an adjective

            * 1 `_NOUN`: `word` is a noun

            * 2 `_VERB`: `word` is a verb

            * 3 `_ADVERB`: `word` is an adverb

        part_of_speech_str (str): A human readable description of
            `part_of_speech`.
    """

    _ADJECTIVE = 0
    _NOUN = 1
    _VERB = 2
    _ADVERB = 3
    _PART_OF_SPEECH_OPTS = [(_ADJECTIVE, 'adjective'), (_NOUN, 'noun'),
                            (_VERB, 'verb'), (_ADVERB, 'adverb')]
    _PARTS_OF_SPEECH = dict(_PART_OF_SPEECH_OPTS)

    word = Column(UnicodeText)
    part_of_speech = Column(Choice(_PART_OF_SPEECH_OPTS), default=_ADJECTIVE)

    __table_args__ = (Index(
        'uq_promo_code_word_normalized_word_part_of_speech',
        func.lower(func.trim(word)),
        part_of_speech,
        unique=True),
                      CheckConstraint(
                          func.trim(word) != '',
                          name='ck_promo_code_word_non_empty_word'))

    _repr_attr_names = ('word', )

    @hybrid_property
    def normalized_word(self):
        return self.normalize_word(self.word)

    @normalized_word.expression
    def normalized_word(cls):
        return func.lower(func.trim(cls.word))

    @property
    def part_of_speech_str(self):
        return self._PARTS_OF_SPEECH[self.part_of_speech].title()

    @presave_adjustment
    def _attribute_adjustments(self):
        # Replace multiple whitespace characters with a single space
        self.word = re.sub(r'\s+', ' ', self.word.strip())

    @classmethod
    def group_by_parts_of_speech(cls, words):
        """
        Groups a list of words by their part_of_speech.

        Arguments:
            words (list): List of `PromoCodeWord`.

        Returns:
            OrderedDict: A dictionary of words mapped to their part of speech,
                like this::

                    OrderedDict([
                        (0, ['adjective1', 'adjective2']),
                        (1, ['noun1', 'noun2']),
                        (2, ['verb1', 'verb2']),
                        (3, ['adverb1', 'adverb2'])
                    ])
        """
        parts_of_speech = OrderedDict([
            (i, []) for (i, _) in PromoCodeWord._PART_OF_SPEECH_OPTS
        ])
        for word in words:
            parts_of_speech[word.part_of_speech].append(word.word)
        return parts_of_speech

    @classmethod
    def normalize_word(cls, word):
        """
        Normalizes a word.

        Arguments:
            word (str): A word as typed by an admin.

        Returns:
            str: A copy of `word` converted to all lowercase, and multiple
                whitespace characters replaced by a single space.
        """
        return re.sub(r'\s+', ' ', word.strip().lower())
class PromoCode(MagModel):
    """
    Promo codes used by attendees to purchase badges at discounted prices.

    Attributes:
        code (str): The actual textual representation of the promo code. This
            is what the attendee would have to type in during registration to
            receive a discount. `code` may not be an empty string or a string
            consisting entirely of whitespace.
        discount (int): The discount amount that should be applied to the
            purchase price of a badge. The interpretation of this value
            depends on the value of `discount_type`. In any case, a value of
            0 equates to a full discount, i.e. a free badge.
        discount_str (str): A human readable description of the discount.
        discount_type (int): The type of discount this promo code will apply.
            Valid values are:

            * 0 `_FIXED_DISCOUNT`: `discount` is interpreted as a fixed
                dollar amount by which the badge price should be reduced. If
                `discount` is 49 and the badge price is normally $100, then
                the discounted badge price would be $51.

            * 1 `_FIXED_PRICE`: `discount` is interpreted as the actual badge
                price. If `discount` is 49, then the discounted badge price
                would be $49.

            * 2 `_PERCENT_DISCOUNT`: `discount` is interpreted as a percentage
                by which the badge price should be reduced. If `discount` is
                20 and the badge price is normally $50, then the discounted
                badge price would $40 ($50 reduced by 20%). If `discount` is
                100, then the price would be 100% off, i.e. a free badge.

        group (relationship): An optional relationship to a PromoCodeGroup
            object, which groups sets of promo codes to make attendee-facing
            "groups"

        cost (int): The cost of this promo code if and when it was bought
          as part of a PromoCodeGroup.

        expiration_date (datetime): The date & time upon which this promo code
            expires. An expired promo code may no longer be used to receive
            discounted badges.
        is_free (bool): True if this promo code will always cause a badge to
            be free. False if this promo code may not cause a badge to be free.

            Note:
                It's possible for this value to be False for a promo code that
                still reduces a badge's price to zero. If there are some other
                discounts that also reduce a badge price (like an age discount)
                then the price may be pushed down to zero.

        is_expired (bool): True if this promo code is expired, False otherwise.
        is_unlimited (bool): True if this promo code may be used an unlimited
            number of times, False otherwise.
        is_valid (bool): True if this promo code is still valid and may be
            used again, False otherwise.
        normalized_code (str): A normalized version of `code` suitable for
            database queries. Normalization converts `code` to all lowercase
            and removes dashes ("-").
        used_by (list): List of attendees that have used this promo code.

            Note:
                This property is declared as a backref in the Attendee class.

        uses_allowed (int): The total number of times this promo code may be
            used. A value of None means this promo code may be used an
            unlimited number of times.
        uses_allowed_str (str): A human readable description of
            uses_allowed.
        uses_count (int): The number of times this promo code has already
            been used.
        uses_count_str (str): A human readable description of uses_count.
        uses_remaining (int): Remaining number of times this promo code may
            be used.
        uses_remaining_str (str): A human readable description of
            uses_remaining.
    """

    _FIXED_DISCOUNT = 0
    _FIXED_PRICE = 1
    _PERCENT_DISCOUNT = 2
    _DISCOUNT_TYPE_OPTS = [(_FIXED_DISCOUNT, 'Fixed Discount'),
                           (_FIXED_PRICE, 'Fixed Price'),
                           (_PERCENT_DISCOUNT, 'Percent Discount')]

    _AMBIGUOUS_CHARS = {
        '0': 'OQD',
        '1': 'IL',
        '2': 'Z',
        '5': 'S',
        '6': 'G',
        '8': 'B'
    }

    _UNAMBIGUOUS_CHARS = string.digits + string.ascii_uppercase
    for _, s in _AMBIGUOUS_CHARS.items():
        _UNAMBIGUOUS_CHARS = re.sub('[{}]'.format(s), '', _UNAMBIGUOUS_CHARS)

    code = Column(UnicodeText)
    discount = Column(Integer, nullable=True, default=None)
    discount_type = Column(Choice(_DISCOUNT_TYPE_OPTS),
                           default=_FIXED_DISCOUNT)
    expiration_date = Column(UTCDateTime, default=c.ESCHATON)
    uses_allowed = Column(Integer, nullable=True, default=None)
    cost = Column(Integer, nullable=True, default=None)

    group_id = Column(UUID,
                      ForeignKey('promo_code_group.id', ondelete='SET NULL'),
                      nullable=True)
    group = relationship(PromoCodeGroup,
                         backref='promo_codes',
                         foreign_keys=group_id,
                         cascade='save-update,merge,refresh-expire,expunge')

    __table_args__ = (Index('uq_promo_code_normalized_code',
                            func.replace(
                                func.replace(func.lower(code), '-', ''), ' ',
                                ''),
                            unique=True),
                      CheckConstraint(func.trim(code) != '',
                                      name='ck_promo_code_non_empty_code'))

    _repr_attr_names = ('code', )

    @classmethod
    def normalize_expiration_date(cls, dt):
        """
        Converts the given datetime to 11:59pm local in the event timezone.
        """
        if isinstance(dt, six.string_types):
            if dt.strip():
                dt = dateparser.parse(dt)
            else:
                dt = c.ESCHATON
        if dt.tzinfo:
            dt = dt.astimezone(c.EVENT_TIMEZONE)
        return c.EVENT_TIMEZONE.localize(
            dt.replace(hour=23, minute=59, second=59, tzinfo=None))

    @property
    def discount_str(self):
        if self.discount_type == self._FIXED_DISCOUNT and self.discount == 0:
            # This is done to account for Art Show Agent codes, which use the PromoCode class
            return 'No discount'
        elif not self.discount:
            return 'Free badge'

        if self.discount_type == self._FIXED_DISCOUNT:
            return '${} discount'.format(self.discount)
        elif self.discount_type == self._FIXED_PRICE:
            return '${} badge'.format(self.discount)
        else:
            return '%{} discount'.format(self.discount)

    @hybrid_property
    def is_expired(self):
        return self.expiration_date < localized_now()

    @is_expired.expression
    def is_expired(cls):
        return cls.expiration_date < localized_now()

    @property
    def is_free(self):
        return not self.discount or (
            self.discount_type == self._PERCENT_DISCOUNT
            and self.discount >= 100) or (self.discount_type
                                          == self._FIXED_DISCOUNT
                                          and self.discount >= c.BADGE_PRICE)

    @hybrid_property
    def is_unlimited(self):
        return not self.uses_allowed

    @is_unlimited.expression
    def is_unlimited(cls):
        return cls.uses_allowed == None  # noqa: E711

    @hybrid_property
    def is_valid(self):
        return not self.is_expired and (self.is_unlimited
                                        or self.uses_remaining > 0)

    @is_valid.expression
    def is_valid(cls):
        return (cls.expiration_date >= localized_now()) \
            & ((cls.uses_allowed == None) | (cls.uses_remaining > 0))  # noqa: E711

    @hybrid_property
    def normalized_code(self):
        return self.normalize_code(self.code)

    @normalized_code.expression
    def normalized_code(cls):
        return func.replace(func.replace(func.lower(cls.code), '-', ''), ' ',
                            '')

    @property
    def uses_allowed_str(self):
        uses = self.uses_allowed
        return 'Unlimited uses' if uses is None else '{} use{} allowed'.format(
            uses, '' if uses == 1 else 's')

    @hybrid_property
    def uses_count(self):
        return len(self.used_by)

    @uses_count.expression
    def uses_count(cls):
        from uber.models.attendee import Attendee
        return select([
            func.count(Attendee.id)
        ]).where(Attendee.promo_code_id == cls.id).label('uses_count')

    @property
    def uses_count_str(self):
        uses = self.uses_count
        return 'Used by {} attendee{}'.format(uses, '' if uses == 1 else 's')

    @hybrid_property
    def uses_remaining(self):
        return None if self.is_unlimited else self.uses_allowed - self.uses_count

    @uses_remaining.expression
    def uses_remaining(cls):
        return cls.uses_allowed - cls.uses_count

    @property
    def uses_remaining_str(self):
        uses = self.uses_remaining
        return 'Unlimited uses' if uses is None else '{} use{} remaining'.format(
            uses, '' if uses == 1 else 's')

    @presave_adjustment
    def _attribute_adjustments(self):
        # If 'uses_allowed' is empty, then this is an unlimited use code
        if not self.uses_allowed:
            self.uses_allowed = None

        # If 'discount' is empty, then this is a full discount, free badge
        if self.discount == '':
            self.discount = None

        self.code = self.code.strip() if self.code else ''
        if not self.code:
            # If 'code' is empty, then generate a random code
            self.code = self.generate_random_code()
        else:
            # Replace multiple whitespace characters with a single space
            self.code = re.sub(r'\s+', ' ', self.code)

        # Always make expiration_date 11:59pm of the given date
        self.expiration_date = self.normalize_expiration_date(
            self.expiration_date)

    def calculate_discounted_price(self, price):
        """
        Returns the discounted price based on the promo code's `discount_type`.

        Args:
            price (int): The badge price in whole dollars.

        Returns:
            int: The discounted price. The returned number will never be
                less than zero or greater than `price`. If `price` is None
                or a negative number, then the return value will always be 0.
        """
        if not self.discount or not price or price < 0:
            return 0

        discounted_price = price
        if self.discount_type == self._FIXED_DISCOUNT:
            discounted_price = price - self.discount
        elif self.discount_type == self._FIXED_PRICE:
            discounted_price = self.discount
        elif self.discount_type == self._PERCENT_DISCOUNT:
            discounted_price = int(price * ((100.0 - self.discount) / 100.0))

        return min(max(discounted_price, 0), price)

    @classmethod
    def _generate_code(cls, generator, count=None):
        """
        Helper method to limit collisions for the other generate() methods.

        Arguments:
            generator (callable): Function that returns a newly generated code.
            count (int): The number of codes to generate. If `count` is `None`,
                then a single code will be generated. Defaults to `None`.

        Returns:
            If an `int` value was passed for `count`, then a `list` of newly
            generated codes is returned. If `count` is `None`, then a single
            `str` is returned.
        """
        from uber.models import Session
        with Session() as session:
            # Kind of inefficient, but doing one big query for all the existing
            # codes will be faster than a separate query for each new code.
            old_codes = set(s for (s, ) in session.query(cls.code).all())

        # Set an upper limit on the number of collisions we'll allow,
        # otherwise this loop could potentially run forever.
        max_collisions = 100
        collisions = 0
        codes = set()
        while len(codes) < (1 if count is None else count):
            code = generator().strip()
            if not code:
                break
            if code in codes or code in old_codes:
                collisions += 1
                if collisions >= max_collisions:
                    break
            else:
                codes.add(code)
        return (codes.pop() if codes else None) if count is None else codes

    @classmethod
    def generate_random_code(cls, count=None, length=9, segment_length=3):
        """
        Generates a random promo code.

        With `length` = 12 and `segment_length` = 3::

            XXX-XXX-XXX-XXX

        With `length` = 6 and `segment_length` = 2::

            XX-XX-XX

        Arguments:
            count (int): The number of codes to generate. If `count` is `None`,
                then a single code will be generated. Defaults to `None`.
            length (int): The number of characters to use for the code.
            segment_length (int): The length of each segment within the code.

        Returns:
            If an `int` value was passed for `count`, then a `list` of newly
            generated codes is returned. If `count` is `None`, then a single
            `str` is returned.
        """

        # The actual generator function, called repeatedly by `_generate_code`
        def _generate_random_code():
            letters = ''.join(
                random.choice(cls._UNAMBIGUOUS_CHARS) for _ in range(length))
            return '-'.join(textwrap.wrap(letters, segment_length))

        return cls._generate_code(_generate_random_code, count=count)

    @classmethod
    def generate_word_code(cls, count=None):
        """
        Generates a promo code consisting of words from `PromoCodeWord`.

        Arguments:
            count (int): The number of codes to generate. If `count` is `None`,
                then a single code will be generated. Defaults to `None`.

        Returns:
            If an `int` value was passed for `count`, then a `list` of newly
            generated codes is returned. If `count` is `None`, then a single
            `str` is returned.
        """
        from uber.models import Session
        with Session() as session:
            words = PromoCodeWord.group_by_parts_of_speech(
                session.query(PromoCodeWord).order_by(
                    PromoCodeWord.normalized_word).all())

        # The actual generator function, called repeatedly by `_generate_code`
        def _generate_word_code():
            code_words = []
            for part_of_speech, _ in PromoCodeWord._PART_OF_SPEECH_OPTS:
                if words[part_of_speech]:
                    code_words.append(random.choice(words[part_of_speech]))
            return ' '.join(code_words)

        return cls._generate_code(_generate_word_code, count=count)

    @classmethod
    def disambiguate_code(cls, code):
        """
        Removes ambiguous characters in a promo code supplied by an attendee.

        Arguments:
            code (str): A promo code as typed by an attendee.

        Returns:
            str: A copy of `code` with all ambiguous characters replaced by
                their unambiguous equivalent.
        """
        code = cls.normalize_code(code)
        if not code:
            return ''
        for unambiguous, ambiguous in cls._AMBIGUOUS_CHARS.items():
            ambiguous_pattern = '[{}]'.format(ambiguous.lower())
            code = re.sub(ambiguous_pattern, unambiguous.lower(), code)
        return code

    @classmethod
    def normalize_code(cls, code):
        """
        Normalizes a promo code supplied by an attendee.

        Arguments:
            code (str): A promo code as typed by an attendee.

        Returns:
            str: A copy of `code` converted to all lowercase, with dashes ("-")
                and whitespace characters removed.
        """
        if not code:
            return ''
        return re.sub(r'[\s\-]+', '', code.lower())
class Attendee:
    comped_reason = Column(UnicodeText, default='', admin_only=True)
    fursuiting = Column(Choice(c.FURSUITING_OPTS), nullable=True)

    @presave_adjustment
    def save_group_cost(self):
        if self.group and self.group.auto_recalc:
            self.group.cost = self.group.default_cost

    @presave_adjustment
    def never_spam(self):
        self.can_spam = False

    @presave_adjustment
    def not_attending_need_not_pay(self):
        if self.badge_status == c.NOT_ATTENDING:
            self.paid = c.NEED_NOT_PAY
            self.comped_reason = "Automated: Not Attending badge status."

    @presave_adjustment
    def staffing_badge_and_ribbon_adjustments(self):
        if self.badge_type == c.STAFF_BADGE or c.STAFF_RIBBON in self.ribbon_ints:
            self.ribbon = remove_opt(self.ribbon_ints, c.VOLUNTEER_RIBBON)

        elif self.staffing and self.badge_type != c.STAFF_BADGE \
                and c.STAFF_RIBBON not in self.ribbon_ints 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 or c.STAFF_RIBBON in self.ribbon_ints:
            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

    @cost_property
    def badge_cost(self):
        registered = self.registered_local if self.registered else None
        if self.paid == c.NEED_NOT_PAY \
                and self.badge_type not in [c.SPONSOR_BADGE, c.SHINY_BADGE]:
            return 0
        elif self.paid == c.NEED_NOT_PAY:
            return c.BADGE_TYPE_PRICES[self.badge_type] \
                   - c.get_attendee_price(registered)
        elif self.overridden_price is not None:
            return self.overridden_price
        elif self.badge_type == c.ONE_DAY_BADGE:
            return c.get_oneday_price(registered)
        elif self.is_presold_oneday:
            return max(0, c.get_presold_oneday_price(self.badge_type) + self.age_discount)
        if 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)
        else:
            return c.get_attendee_price(registered)

    @property
    def age_discount(self):
        if 'val' in self.age_group_conf \
                and self.age_group_conf['val'] == c.UNDER_13 \
                and c.AT_THE_CON:
            if self.badge_type == c.ATTENDEE_BADGE:
                discount = 33
            elif self.badge_type in [c.FRIDAY, c.SUNDAY]:
                discount = 13
            elif self.badge_type == c.SATURDAY:
                discount = 20
            if not self.age_group_conf['discount'] \
                    or self.age_group_conf['discount'] < discount:
                return -discount
        return -self.age_group_conf['discount']

    @property
    def paid_for_a_shirt(self):
        return self.badge_type in [c.SPONSOR_BADGE, c.SHINY_BADGE]

    @property
    def staffing_or_will_be(self):
        return self.staffing or self.badge_type == c.STAFF_BADGE \
               or c.VOLUNTEER_RIBBON in self.ribbon_ints or c.STAFF_RIBBON in self.ribbon_ints

    @property
    def merch_items(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).

        If the c.SEPARATE_STAFF_SWAG setting is true, then this excludes staff
        merch; see the staff_merch property.

        This property returns a list containing strings and sub-lists of each
        donation tier with multiple sub-items, e.g.
            [
                'tshirt',
                'Supporter Pack',
                [
                    'Swag Bag',
                    'Badge Holder'
                ],
                'Season Pass Certificate'
            ]
        """
        merch = []
        for amount, desc in sorted(c.DONATION_TIERS.items()):
            if amount and self.amount_extra >= amount:
                merch.append(desc)
                items = c.DONATION_TIER_ITEMS.get(amount, [])
                if len(items) == 1:
                    merch[-1] = items[0]
                elif len(items) > 1:
                    merch.append(items)

        if self.num_event_shirts_owed == 1 and not self.paid_for_a_shirt:
            merch.append('A T-shirt')
        elif self.num_event_shirts_owed > 1:
            merch.append('A 2nd T-Shirt')

        if merch and self.volunteer_event_shirt_eligible and not self.volunteer_event_shirt_earned:
            merch[-1] += (
                ' (this volunteer must work at least {} hours or they will be reported for picking up their shirt)'
                .format(c.HOURS_FOR_SHIRT))
        
        if self.badge_type == c.SPONSOR_BADGE:
            merch.append('Sponsor merch')
        if self.badge_type == c.SHINY_BADGE:
            merch.append('Shiny Sponsor merch')

        if not c.SEPARATE_STAFF_MERCH:
            merch.extend(self.staff_merch_items)

        if self.extra_merch:
            merch.append(self.extra_merch)

        return merch
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 or 0

    @property
    def total_cost(self):
        if self.status != c.APPROVED:
            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
Example #25
0
class IndieStudio(MagModel):
    group_id = Column(UUID, ForeignKey('group.id'), nullable=True)
    name = Column(UnicodeText, unique=True)
    address = Column(UnicodeText)
    website = Column(UnicodeText)
    twitter = Column(UnicodeText)
    facebook = Column(UnicodeText)
    status = Column(Choice(c.MIVS_STUDIO_STATUS_OPTS),
                    default=c.NEW,
                    admin_only=True)
    staff_notes = Column(UnicodeText, admin_only=True)
    registered = Column(UTCDateTime, server_default=utcnow())

    accepted_core_hours = Column(Boolean, default=False)
    discussion_emails = Column(UnicodeText)
    completed_discussion = Column(Boolean, default=False)
    read_handbook = Column(Boolean, default=False)
    training_password = Column(UnicodeText)
    selling_at_event = Column(
        Boolean, nullable=True,
        admin_only=True)  # "Admin only" preserves null default
    needs_hotel_space = Column(
        Boolean, nullable=True,
        admin_only=True)  # "Admin only" preserves null default
    name_for_hotel = Column(UnicodeText)
    email_for_hotel = Column(UnicodeText)

    games = relationship('IndieGame',
                         backref='studio',
                         order_by='IndieGame.title')
    developers = relationship('IndieDeveloper',
                              backref='studio',
                              order_by='IndieDeveloper.last_name')

    email_model_name = 'studio'

    @property
    def primary_contact_first_names(self):
        if len(self.primary_contacts) == 1:
            return self.primary_contacts[0].first_name

        string = self.primary_contacts[0].first_name
        for dev in self.primary_contacts[1:-1]:
            string += ", " + dev.first_name
        if len(self.primary_contacts) > 2:
            string += ","
        string += " and " + self.primary_contacts[-1].first_name
        return string

    @property
    def confirm_deadline(self):
        sorted_games = sorted([g for g in self.games if g.accepted],
                              key=lambda g: g.accepted)
        confirm_deadline = timedelta(days=c.MIVS_CONFIRM_DEADLINE)
        return sorted_games[0].accepted + confirm_deadline

    @property
    def after_confirm_deadline(self):
        return self.confirm_deadline < localized_now()

    @property
    def discussion_emails_list(self):
        return list(filter(None, self.discussion_emails.split(',')))

    @property
    def core_hours_status(self):
        return "Accepted" if self.accepted_core_hours else None

    @property
    def discussion_status(self):
        return "Completed" if self.completed_discussion else None

    @property
    def handbook_status(self):
        return "Read" if self.read_handbook else None

    @property
    def training_status(self):
        if self.training_password:
            return "Completed" if self.training_password.lower() == c.MIVS_TRAINING_PASSWORD.lower()\
                else "Entered the wrong phrase!"

    @property
    def selling_at_event_status(self):
        if self.selling_at_event is not None:
            return "Expressed interest in selling" if self.selling_at_event else "Opted out"

    @property
    def hotel_space_status(self):
        if self.needs_hotel_space is not None:
            return "Requested hotel space for {} with email {}".format(self.name_for_hotel, self.email_for_hotel)\
                if self.needs_hotel_space else "Opted out"

    def checklist_deadline(self, slug):
        default_deadline = c.MIVS_CHECKLIST[slug]['deadline']
        if self.group and self.group.registered >= default_deadline and slug != 'hotel_space':
            return self.group.registered + timedelta(days=3)
        return default_deadline

    def past_checklist_deadline(self, slug):
        """

        Args:
            slug: A standardized name, which should match the checklist's section name in config.
            E.g., the properties above ending in _status

        Returns: A timedelta object representing how far from the deadline this team is for a particular checklist item

        """
        return localized_now() - self.checklist_deadline(slug)

    @property
    def checklist_items_due_soon_grouped(self):
        two_days = []
        one_day = []
        overdue = []
        for key, val in c.MIVS_CHECKLIST.items():
            if localized_now() >= self.checklist_deadline(key):
                overdue.append([val['name'], "mivs_" + key])
            elif (localized_now() -
                  timedelta(days=1)) >= self.checklist_deadline(key):
                one_day.append([val['name'], "mivs_" + key])
            elif (localized_now() -
                  timedelta(days=2)) >= self.checklist_deadline(key):
                two_days.append([val['name'], "mivs_" + key])

        return two_days, one_day, overdue

    @property
    def website_href(self):
        return make_url(self.website)

    @property
    def email(self):
        return [
            dev.email_to_address for dev in self.developers
            if dev.primary_contact
        ]

    @property
    def primary_contacts(self):
        return [dev for dev in self.developers if dev.primary_contact]

    @property
    def submitted_games(self):
        return [g for g in self.games if g.submitted]

    @property
    def comped_badges(self):
        game_count = len([g for g in self.games if g.status == c.ACCEPTED])
        return c.MIVS_INDIE_BADGE_COMPS * game_count

    @property
    def unclaimed_badges(self):
        claimed_count = len(
            [d for d in self.developers if not d.matching_attendee])
        return max(0, self.comped_badges - claimed_count)
Example #26
0
class AttractionEvent(MagModel):
    attraction_feature_id = Column(UUID, ForeignKey('attraction_feature.id'))
    attraction_id = Column(UUID, ForeignKey('attraction.id'), index=True)

    location = Column(Choice(c.EVENT_LOCATION_OPTS))
    start_time = Column(UTCDateTime, default=c.EPOCH)
    duration = Column(Integer, default=900)  # In seconds
    slots = Column(Integer, default=1)
    signups_open = Column(Boolean, default=True)

    signups = relationship('AttractionSignup', backref='event', order_by='AttractionSignup.checkin_time')

    attendee_signups = association_proxy('signups', 'attendee')

    notifications = relationship('AttractionNotification', backref='event', order_by='AttractionNotification.sent_time')

    notification_replies = relationship(
        'AttractionNotificationReply', backref='event', order_by='AttractionNotificationReply.sid')

    attendees = relationship(
        'Attendee',
        backref='attraction_events',
        cascade='save-update,merge,refresh-expire,expunge',
        secondary='attraction_signup',
        order_by='attraction_signup.c.signup_time')

    @presave_adjustment
    def _fix_attraction_id(self):
        if not self.attraction_id and self.feature:
            self.attraction_id = self.feature.attraction_id

    @classmethod
    def get_ident(cls, id, advance_notice):
        if advance_notice == -1:
            return str(id)
        return '{}_{}'.format(id, advance_notice)

    @hybrid_property
    def end_time(self):
        return self.start_time + timedelta(seconds=self.duration)

    @end_time.expression
    def end_time(cls):
        return cls.start_time + (cls.duration * text("interval '1 second'"))

    @property
    def start_day_local(self):
        return self.start_time_local.strftime('%A')

    @property
    def start_time_label(self):
        if self.start_time:
            return self.start_time_local.strftime('%-I:%M %p %A')
        return 'unknown start time'

    @property
    def checkin_start_time(self):
        advance_checkin = self.attraction.advance_checkin
        if advance_checkin < 0:
            return self.start_time
        else:
            return self.start_time - timedelta(seconds=advance_checkin)

    @property
    def checkin_end_time(self):
        advance_checkin = self.attraction.advance_checkin
        if advance_checkin < 0:
            return self.end_time
        else:
            return self.start_time

    @property
    def checkin_start_time_label(self):
        checkin = self.checkin_start_time_local
        today = datetime.now(c.EVENT_TIMEZONE).date()
        if checkin.date() == today:
            return checkin.strftime('%-I:%M %p')
        return checkin.strftime('%-I:%M %p %a')

    @property
    def checkin_end_time_label(self):
        checkin = self.checkin_end_time_local
        today = datetime.now(c.EVENT_TIMEZONE).date()
        if checkin.date() == today:
            return checkin.strftime('%-I:%M %p')
        return checkin.strftime('%-I:%M %p %a')

    @property
    def time_remaining_to_checkin(self):
        return self.checkin_start_time - datetime.now(pytz.UTC)

    @property
    def time_remaining_to_checkin_label(self):
        return humanize_timedelta(self.time_remaining_to_checkin, granularity='minutes', separator=' ')

    @property
    def is_checkin_over(self):
        return self.checkin_end_time < datetime.now(pytz.UTC)

    @property
    def is_sold_out(self):
        return self.slots <= len(self.attendees)

    @property
    def is_started(self):
        return self.start_time < datetime.now(pytz.UTC)

    @property
    def remaining_slots(self):
        return max(self.slots - len(self.attendees), 0)

    @property
    def time_span_label(self):
        if self.start_time:
            end_time = self.end_time.astimezone(c.EVENT_TIMEZONE)
            start_time = self.start_time.astimezone(c.EVENT_TIMEZONE)
            if start_time.date() == end_time.date():
                return '{} – {}'.format(start_time.strftime('%-I:%M %p'), end_time.strftime('%-I:%M %p %A'))
            return '{} – {}'.format(start_time.strftime('%-I:%M %p %A'), end_time.strftime('%-I:%M %p %A'))
        return 'unknown time span'

    @property
    def duration_label(self):
        if self.duration:
            return humanize_timedelta(seconds=self.duration, separator=' ')
        return 'unknown duration'

    @property
    def location_event_name(self):
        return location_event_name(self.location)

    @property
    def location_room_name(self):
        return location_room_name(self.location)

    @property
    def name(self):
        return self.feature.name

    @property
    def label(self):
        return '{} at {}'.format(self.name, self.start_time_label)

    def overlap(self, event):
        if not event:
            return 0
        latest_start = max(self.start_time, event.start_time)
        earliest_end = min(self.end_time, event.end_time)
        if earliest_end < latest_start:
            return -int((latest_start - earliest_end).total_seconds())
        elif self.start_time < event.start_time and self.end_time > event.end_time:
            return int((self.end_time - event.start_time).total_seconds())
        elif self.start_time > event.start_time and self.end_time < event.end_time:
            return int((event.end_time - self.start_time).total_seconds())
        else:
            return int((earliest_end - latest_start).total_seconds())
Example #27
0
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

    @hybrid_property
    def has_been_accepted(self):
        return self.status == c.ACCEPTED

    @property
    def guidebook_name(self):
        return self.studio.name

    @property
    def guidebook_subtitle(self):
        return self.title

    @property
    def guidebook_desc(self):
        return self.description

    @property
    def guidebook_location(self):
        return ''

    @property
    def guidebook_image(self):
        return self.best_screenshot_download_filenames()[0]

    @property
    def guidebook_thumbnail(self):
        return self.best_screenshot_download_filenames()[1] \
            if len(self.best_screenshot_download_filenames()) > 1 else self.best_screenshot_download_filenames()[0]

    @property
    def guidebook_images(self):
        image_filenames = [self.best_screenshot_download_filenames()[0]]
        images = [self.best_screenshot_downloads()[0]]
        if self.guidebook_image != self.guidebook_thumbnail:
            image_filenames.append(self.guidebook_thumbnail)
            images.append(self.best_screenshot_downloads()[1])

        return image_filenames, images
Example #28
0
class GuestMerch(MagModel):
    _inventory_file_regex = re.compile(r'^(audio|image)(|\-\d+)$')
    _inventory_filename_regex = re.compile(r'^(audio|image)(|\-\d+)_filename$')

    guest_id = Column(UUID, ForeignKey('guest_group.id'), unique=True)
    selling_merch = Column(Choice(c.GUEST_MERCH_OPTS), nullable=True)
    inventory = Column(JSON, default={}, server_default='{}')
    bringing_boxes = Column(UnicodeText)
    extra_info = Column(UnicodeText)
    tax_phone = Column(UnicodeText)

    poc_is_group_leader = Column(Boolean, default=False)
    poc_first_name = Column(UnicodeText)
    poc_last_name = Column(UnicodeText)
    poc_phone = Column(UnicodeText)
    poc_email = Column(UnicodeText)
    poc_zip_code = Column(UnicodeText)
    poc_address1 = Column(UnicodeText)
    poc_address2 = Column(UnicodeText)
    poc_city = Column(UnicodeText)
    poc_region = Column(UnicodeText)
    poc_country = Column(UnicodeText)

    handlers = Column(JSON, default=[], server_default='[]')

    @property
    def full_name(self):
        if self.poc_is_group_leader:
            return self.guest.group.leader.full_name
        elif self.poc_first_name or self.poc_last_name:
            return ' '.join([self.poc_first_name, self.poc_last_name])
        else:
            return ''

    @property
    def first_name(self):
        if self.poc_is_group_leader:
            return self.guest.group.leader.first_name
        return self.poc_first_name

    @property
    def last_name(self):
        if self.poc_is_group_leader:
            return self.guest.group.leader.last_name
        return self.poc_last_name

    @property
    def phone(self):
        if self.poc_is_group_leader:
            return self.guest.group.leader.cellphone or self.tax_phone or self.guest.info.poc_phone
        return self.poc_phone

    @property
    def email(self):
        if self.poc_is_group_leader:
            return self.guest.group.leader.email
        return self.poc_email

    @property
    def rock_island_url(self):
        return '{}/guest_admin/rock_island?id={}'.format(c.PATH, self.guest_id)

    @property
    def rock_island_csv_url(self):
        return '{}/guest_admin/rock_island_csv?id={}'.format(c.PATH, self.guest_id)

    @property
    def status(self):
        if self.selling_merch == c.ROCK_ISLAND:
            return self.selling_merch_label + ('' if self.inventory else ' (No Merch)')
        return self.selling_merch_label

    @presave_adjustment
    def tax_phone_from_poc_phone(self):
        if self.selling_merch == c.OWN_TABLE and not self.tax_phone and self.guest and self.guest.info:
            self.tax_phone = self.guest.info.poc_phone

    @classmethod
    def extract_json_params(cls, params, field):
        multi_param_regex = re.compile(''.join(['^', field, r'_([\w_\-]+?)_(\d+)$']))
        single_param_regex = re.compile(''.join(['^', field, r'_([\w_\-]+?)$']))

        items = defaultdict(dict)
        single_item = dict()
        for param_name, value in filter(lambda i: i[1], params.items()):
            match = multi_param_regex.match(param_name)
            if match:
                name = match.group(1)
                item_number = int(match.group(2))
                items[item_number][name] = value
            else:
                match = single_param_regex.match(param_name)
                if match:
                    name = match.group(1)
                    single_item[name] = value

        if single_item:
            items[len(items)] = single_item

        return [item for item_number, item in sorted(items.items())]

    @classmethod
    def extract_inventory(cls, params):
        inventory = {}
        for item in cls.extract_json_params(params, 'inventory'):
            if not item.get('id'):
                item['id'] = str(uuid.uuid4())
            inventory[item['id']] = item
        return inventory

    @classmethod
    def extract_handlers(cls, params):
        return cls.extract_json_params(params, 'handlers')

    @classmethod
    def validate_inventory(cls, inventory):
        if not inventory:
            return 'You must add some merch to your inventory!'
        messages = []
        for item_id, item in inventory.items():
            quantity = int(item.get('quantity') or 0)
            if quantity <= 0 and cls.total_quantity(item) <= 0:
                messages.append('You must specify some quantity')
            for name, file in [(n, f) for (n, f) in item.items() if f]:
                match = cls._inventory_file_regex.match(name)
                if match and getattr(file, 'filename', None):
                    file_type = match.group(1).upper()
                    config_name = 'ALLOWED_INVENTORY_{}_EXTENSIONS'.format(file_type)
                    extensions = getattr(c, config_name, [])
                    ext = filename_extension(file.filename)
                    if extensions and ext not in extensions:
                        messages.append('{} files must be one of {}'.format(file_type.title(), ', '.join(extensions)))

        return '. '.join(uniquify([s.strip() for s in messages if s.strip()]))

    def _prune_inventory_file(self, item, new_inventory, *, prune_missing=False):

        for name, filename in list(item.items()):
            match = self._inventory_filename_regex.match(name)
            if match and filename:
                new_item = new_inventory.get(item['id'])
                if (prune_missing and not new_item) or (new_item and new_item.get(name) != filename):
                    filepath = self.inventory_path(filename)
                    if os.path.exists(filepath):
                        os.remove(filepath)

    def _prune_inventory_files(self, new_inventory, *, prune_missing=False):
        for item_id, item in self.inventory.items():
            self._prune_inventory_file(item, new_inventory, prune_missing=prune_missing)

    def _save_inventory_files(self, inventory):
        for item_id, item in inventory.items():
            for name, file in [(n, f) for (n, f) in item.items() if f]:
                match = self._inventory_file_regex.match(name)
                if match:
                    download_file_attr = '{}_download_filename'.format(name)
                    file_attr = '{}_filename'.format(name)
                    content_type_attr = '{}_content_type'.format(name)
                    del item[name]
                    if getattr(file, 'filename', None):
                        item[download_file_attr] = file.filename
                        item[file_attr] = str(uuid.uuid4())
                        item[content_type_attr] = file.content_type.value
                        item_path = self.inventory_path(item[file_attr])
                        with open(item_path, 'wb') as f:
                            shutil.copyfileobj(file.file, f)

                    attrs = [download_file_attr, file_attr, content_type_attr]

                    for attr in attrs:
                        if attr in item and not item[attr]:
                            del item[attr]

    @classmethod
    def total_quantity(cls, item):
        total_quantity = 0
        for attr in filter(lambda s: s.startswith('quantity'), item.keys()):
            total_quantity += int(item[attr] if item[attr] else 0)
        return total_quantity

    @classmethod
    def item_subcategories(cls, item_type):
        s = {getattr(c, s): s for s in c.MERCH_TYPES_VARS}[int(item_type)]
        return (
            getattr(c, '{}_VARIETIES'.format(s), defaultdict(lambda: {})),
            getattr(c, '{}_CUTS'.format(s), defaultdict(lambda: {})),
            getattr(c, '{}_SIZES'.format(s), defaultdict(lambda: {})))

    @classmethod
    def item_subcategories_opts(cls, item_type):
        s = {getattr(c, s): s for s in c.MERCH_TYPES_VARS}[int(item_type)]
        return (
            getattr(c, '{}_VARIETIES_OPTS'.format(s), defaultdict(lambda: [])),
            getattr(c, '{}_CUTS_OPTS'.format(s), defaultdict(lambda: [])),
            getattr(c, '{}_SIZES_OPTS'.format(s), defaultdict(lambda: [])))

    @classmethod
    def line_items(cls, item):
        line_items = []
        for attr in filter(lambda s: s.startswith('quantity-'), item.keys()):
            if int(item[attr] if item[attr] else 0) > 0:
                line_items.append(attr)

        varieties, cuts, sizes = [
            [v for (v, _) in x]
            for x in cls.item_subcategories_opts(item['type'])]

        def _line_item_sort_key(line_item):
            variety, cut, size = cls.line_item_to_types(line_item)
            return (
                varieties.index(variety) if variety else 0,
                cuts.index(cut) if cut else 0,
                sizes.index(size) if size else 0)

        return sorted(line_items, key=_line_item_sort_key)

    @classmethod
    def line_item_to_types(cls, line_item):
        return [int(s) for s in line_item.split('-')[1:]]

    @classmethod
    def line_item_to_string(cls, item, line_item):
        variety_val, cut_val, size_val = cls.line_item_to_types(line_item)

        varieties, cuts, sizes = cls.item_subcategories(item['type'])
        variety_label = varieties.get(variety_val, '').strip()
        if not size_val and not cut_val:
            return variety_label + ' - One size only'

        size_label = sizes.get(size_val, '').strip()
        cut_label = cuts.get(cut_val, '').strip()

        parts = [variety_label]
        if cut_label:
            parts.append(cut_label)
        if size_label:
            parts.extend(['-', size_label])
        return ' '.join(parts)

    @classmethod
    def inventory_path(cls, file):
        return os.path.join(c.GUESTS_INVENTORY_DIR, file)

    def inventory_url(self, item_id, name):
        return '{}/guests/view_inventory_file?id={}&item_id={}&name={}'.format(c.PATH, self.id, item_id, name)

    def remove_inventory_item(self, item_id, *, persist_files=True):
        item = None
        if item_id in self.inventory:
            inventory = dict(self.inventory)
            item = inventory[item_id]
            del inventory[item_id]
            if persist_files:
                self._prune_inventory_file(item, inventory, prune_missing=True)
            self.inventory = inventory
        return item

    def set_inventory(self, inventory, *, persist_files=True):
        if persist_files:
            self._save_inventory_files(inventory)
            self._prune_inventory_files(inventory, prune_missing=True)
        self.inventory = inventory

    def update_inventory(self, inventory, *, persist_files=True):
        if persist_files:
            self._save_inventory_files(inventory)
            self._prune_inventory_files(inventory, prune_missing=False)
        self.inventory = dict(self.inventory, **inventory)
Example #29
0
class Event(MagModel):
    location = Column(Choice(c.EVENT_LOCATION_OPTS))
    start_time = Column(UTCDateTime)
    duration = Column(Integer)  # half-hour increments
    name = Column(UnicodeText, nullable=False)
    description = Column(UnicodeText)

    assigned_panelists = relationship('AssignedPanelist', backref='event')
    applications = relationship('PanelApplication', backref='event')
    panel_feedback = relationship('EventFeedback', backref='event')
    tournaments = relationship('TabletopTournament', backref='event', uselist=False)
    guest = relationship('GuestGroup', backref=backref('event', cascade="save-update,merge"),
                         cascade='save-update,merge')

    @property
    def half_hours(self):
        half_hours = set()
        for i in range(self.duration):
            half_hours.add(self.start_time + timedelta(minutes=30 * i))
        return half_hours

    @property
    def minutes(self):
        return (self.duration or 0) * 30

    @property
    def start_slot(self):
        if self.start_time:
            start_delta = self.start_time_local - c.EPOCH
            return int(start_delta.total_seconds() / (60 * 30))

    @property
    def end_time(self):
        return self.start_time + timedelta(minutes=self.minutes)

    @property
    def guidebook_name(self):
        return self.name

    @property
    def guidebook_subtitle(self):
        # Note: not everything on this list is actually exported
        if self.location in c.PANEL_ROOMS:
            return 'Panel'
        if self.location in c.MUSIC_ROOMS:
            return 'Music'
        if self.location in c.TABLETOP_LOCATIONS:
            return 'Tabletop Event'
        if "Autograph" in self.location_label:
            return 'Autograph Session'

    @property
    def guidebook_desc(self):
        panelists_creds = '<br/><br/>' + '<br/><br/>'.join(
            a.other_credentials for a in self.applications[0].applicants if a.other_credentials
        ) if self.applications else ''
        return self.description + panelists_creds

    @property
    def guidebook_location(self):
        return self.event.location_label
Example #30
0
class GuestGroup(MagModel):
    group_id = Column(UUID, ForeignKey('group.id'))
    event_id = Column(UUID, ForeignKey('event.id', ondelete='SET NULL'), nullable=True)
    group_type = Column(Choice(c.GROUP_TYPE_OPTS), default=c.BAND)
    num_hotel_rooms = Column(Integer, default=1, admin_only=True)
    payment = Column(Integer, default=0, admin_only=True)
    vehicles = Column(Integer, default=1, admin_only=True)
    estimated_loadin_minutes = Column(Integer, default=c.DEFAULT_LOADIN_MINUTES, admin_only=True)
    estimated_performance_minutes = Column(Integer, default=c.DEFAULT_PERFORMANCE_MINUTES, admin_only=True)

    wants_mc = Column(Boolean, nullable=True)
    info = relationship('GuestInfo', backref=backref('guest', load_on_pending=True), uselist=False)
    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)
    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)

    email_model_name = 'guest'

    def __getattr__(self, name):
        """
        If someone tries to access a property called, e.g., info_status,
        and the named property doesn't exist, we instead call
        self.status. This allows us to refer to status config options
        indirectly, which in turn allows us to override certain status
        options on a case-by-case basis. This is helpful for a couple of
        properties here, but it's vital to allow events to control group
        checklists with granularity.
        """
        if name.endswith('_status'):
            return self.status(name.rsplit('_', 1)[0])
        else:
            return super(GuestGroup, self).__getattr__(name)

    def deadline_from_model(self, model):
        name = str(self.group_type_label).upper() + "_" + str(model).upper() + "_DEADLINE"
        return getattr(c, name, None)

    @property
    def all_badges_claimed(self):
        return not any(a.is_unassigned for a in self.group.attendees)

    @property
    def estimated_performer_count(self):
        return len([a for a in self.group.attendees if a.badge_type == c.GUEST_BADGE])

    @property
    def performance_minutes(self):
        return self.estimated_performance_minutes

    @property
    def email(self):
        return self.group.email

    @property
    def normalized_group_name(self):
        # Lowercase
        name = self.group.name.strip().lower()

        # Remove all special characters
        name = ''.join(s for s in name if s.isalnum() or s == ' ')

        # Remove extra whitespace & replace spaces with underscores
        return ' '.join(name.split()).replace(' ', '_')

    @property
    def badges_status(self):
        if self.group.unregistered_badges:
            return str(self.group.unregistered_badges) + " Unclaimed"
        return "Yes"

    @property
    def taxes_status(self):
        return "Not Needed" if not self.payment else self.status('taxes')

    @property
    def panel_status(self):
        application_count = len(self.group.leader.panel_applications)
        return '{} Panel Application(s)'.format(application_count) \
            if self.group.leader.panel_applications else self.status('panel')

    @property
    def mc_status(self):
        return None if self.wants_mc is None else yesno(self.wants_mc, 'Yes,No')

    @property
    def checklist_completed(self):
        for list_item in c.GUEST_CHECKLIST_ITEMS:
            item_status = getattr(self, list_item['name'] + '_status', None)
            if self.deadline_from_model(list_item['name']) and not item_status:
                return False
            elif item_status and 'Unclaimed' in item_status:
                return False
        return True

    def status(self, model):
        """
        This is a safe way to check if a step has been completed and
        what its status is for a particular group. It checks for a
        custom 'status' property for the step; if that doesn't exist, it
        will attempt to return True if an ID of the step exists or an
        empty string if not. If there's no corresponding deadline for
        the model we're checking, we return "N/A".

        Args:
         model: This should match one of the relationship columns in the
             GuestGroup class, e.g., 'bio' or 'taxes'.

        Returns:
            Returns either the 'status' property of the model, "N/A,"
            True, or an empty string.
        """

        if not self.deadline_from_model(model):
            return "N/A"

        subclass = getattr(self, model, None)
        if subclass:
            return getattr(subclass, 'status', getattr(subclass, 'id'))
        return ''

    @property
    def guidebook_name(self):
        return self.group.name if self.group else ''

    @property
    def guidebook_subtitle(self):
        return self.group_type_label

    @property
    def guidebook_desc(self):
        return self.bio.desc if self.bio else ''

    @property
    def guidebook_image(self):
        return self.bio.pic_filename if self.bio else ''

    @property
    def guidebook_thumbnail(self):
        return self.bio.pic_filename if self.bio else ''

    @property
    def guidebook_images(self):
        if not self.bio:
            return ['', '']

        return [self.bio.pic_filename], [self.bio]