Example #1
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 #2
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 #3
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 #4
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 #5
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 #6
0
class MerchPickup(MagModel):
    picked_up_by_id = Column(UUID, ForeignKey('attendee.id'))
    picked_up_for_id = Column(UUID, ForeignKey('attendee.id'), unique=True)
    picked_up_by = relationship(
        Attendee,
        primaryjoin='MerchPickup.picked_up_by_id == Attendee.id',
        cascade='save-update,merge,refresh-expire,expunge')
    picked_up_for = relationship(
        Attendee,
        primaryjoin='MerchPickup.picked_up_for_id == Attendee.id',
        cascade='save-update,merge,refresh-expire,expunge')
Example #7
0
class Attendee:
    art_show_bidder = relationship('ArtShowBidder',
                                   backref=backref('attendee',
                                                   load_on_pending=True),
                                   uselist=False)
    art_show_purchases = relationship(
        'ArtShowPiece',
        backref='buyer',
        cascade='save-update,merge,refresh-expire,expunge',
        secondary='art_show_receipt')

    @presave_adjustment
    def not_attending_need_not_pay(self):
        if self.badge_status == c.NOT_ATTENDING:
            self.paid = c.NEED_NOT_PAY

    @presave_adjustment
    def add_as_agent(self):
        if self.promo_code:
            art_apps = self.session.lookup_agent_code(self.promo_code.code)
            for app in art_apps:
                app.agent_id = self.id

    @cost_property
    def art_show_app_cost(self):
        cost = 0
        if self.art_show_applications:
            for app in self.art_show_applications:
                cost += app.total_cost
        return cost

    @property
    def art_show_receipt(self):
        open_receipts = [
            receipt for receipt in self.art_show_receipts if not receipt.closed
        ]
        if open_receipts:
            return open_receipts[0]

    @property
    def full_address(self):
        if self.country and self.city and (self.region or self.country not in [
                'United States', 'Canada'
        ]) and self.address1:
            return True

    @property
    def payment_page(self):
        if self.art_show_applications:
            for app in self.art_show_applications:
                if app.total_cost and app.status != c.PAID:
                    return '../art_show_applications/edit?id={}'.format(app.id)
        return 'attendee_donation_form?id={}'.format(self.id)
Example #8
0
class WatchList(MagModel):
    first_names = Column(UnicodeText)
    last_name = Column(UnicodeText)
    email = Column(UnicodeText, default='')
    birthdate = Column(Date, nullable=True, default=None)
    reason = Column(UnicodeText)
    action = Column(UnicodeText)
    active = Column(Boolean, default=True)
    attendees = relationship('Attendee',
                             backref=backref('watch_list',
                                             load_on_pending=True))

    @property
    def full_name(self):
        return '{} {}'.format(self.first_names,
                              self.last_name).strip() or 'Unknown'

    @property
    def first_name_list(self):
        return [name.strip().lower() for name in self.first_names.split(',')]

    @presave_adjustment
    def _fix_birthdate(self):
        if self.birthdate == '':
            self.birthdate = None
Example #9
0
class Room(MagModel, NightsMixin):
    notes = Column(UnicodeText)
    message = Column(UnicodeText)
    locked_in = Column(Boolean, default=False)
    nights = Column(MultiChoice(c.NIGHT_OPTS))
    created = Column(UTCDateTime, server_default=utcnow())
    assignments = relationship('RoomAssignment', backref='room')

    @property
    def email(self):
        return [ra.attendee.email for ra in self.assignments]

    @property
    def first_names(self):
        return [ra.attendee.first_name for ra in self.assignments]

    @property
    def check_in_date(self):
        return c.NIGHT_DATES[self.nights_labels[0]]

    @property
    def check_out_date(self):
        # TODO: Undo this kludgy workaround by fully implementing:
        #       https://github.com/magfest/hotel/issues/39
        if self.nights_labels[-1] == 'Monday':
            return c.ESCHATON.date() + timedelta(days=1)
        else:
            return c.NIGHT_DATES[self.nights_labels[-1]] + timedelta(days=1)
Example #10
0
class DeptRole(MagModel):
    name = Column(UnicodeText)
    description = Column(UnicodeText)
    department_id = Column(UUID, ForeignKey('department.id'))

    dept_memberships = relationship(
        'DeptMembership',
        backref='dept_roles',
        cascade='save-update,merge,refresh-expire,expunge',
        secondary='dept_membership_dept_role')

    __table_args__ = (UniqueConstraint('name', 'department_id'), )

    @hybrid_property
    def dept_membership_count(self):
        return len(self.dept_memberships)

    @dept_membership_count.expression
    def dept_membership_count(cls):
        return func.count(cls.dept_memberships)

    @classproperty
    def extra_apply_attrs(cls):
        return set(['dept_memberships_ids'
                    ]).union(cls.extra_apply_attrs_restricted)

    @property
    def dept_memberships_ids(self):
        _, ids = self._get_relation_ids('dept_memberships')
        return [str(d.id) for d in self.dept_memberships] \
            if ids is None else ids

    @dept_memberships_ids.setter
    def dept_memberships_ids(self, value):
        self._set_relation_ids('dept_memberships', DeptMembership, value)
Example #11
0
class TabletopTournament(MagModel):
    event_id = Column(UUID, ForeignKey('event.id'), unique=True)

    # Separate from the event name for cases where we want a shorter name in our SMS messages.
    name = Column(UnicodeText)

    entrants = relationship('TabletopEntrant', backref='tournament')
Example #12
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 #13
0
class TabletopEntrant(MagModel):
    tournament_id = Column(UUID, ForeignKey('tabletop_tournament.id'))
    attendee_id = Column(UUID, ForeignKey('attendee.id'))
    signed_up = Column(UTCDateTime, default=lambda: datetime.now(UTC))
    confirmed = Column(Boolean, default=False)

    reminder = relationship('TabletopSmsReminder',
                            backref='entrant',
                            uselist=False)
    replies = relationship('TabletopSmsReply', backref='entrant')

    @presave_adjustment
    def _within_cutoff(self):
        if self.is_new:
            tournament = self.tournament or self.session.tabletop_tournament(
                self.tournament_id)
            cutoff = timedelta(minutes=c.TABLETOP_SMS_CUTOFF_MINUTES)
            if self.signed_up > tournament.event.start_time - cutoff:
                self.confirmed = True

    @property
    def should_send_reminder(self):
        stagger = timedelta(minutes=c.TABLETOP_SMS_STAGGER_MINUTES)
        reminder = timedelta(minutes=c.TABLETOP_SMS_REMINDER_MINUTES)
        return not self.confirmed \
            and not self.reminder \
            and localized_now() < self.tournament.event.start_time \
            and localized_now() > self.signed_up + stagger \
            and localized_now() > self.tournament.event.start_time - reminder

    def matches(self, message):
        sent = message.date_sent.replace(tzinfo=UTC)
        start_time_slack = timedelta(minutes=c.TABLETOP_TOURNAMENT_SLACK)
        return normalize_phone(self.attendee.cellphone) == message.from_ \
            and self.reminder and sent > self.reminder.when \
            and sent < self.tournament.event.start_time + start_time_slack

    __table_args__ = (UniqueConstraint('tournament_id',
                                       'attendee_id',
                                       name='_tournament_entrant_uniq'), )
Example #14
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 #15
0
class WatchList(MagModel):
    first_names = Column(UnicodeText)
    last_name = Column(UnicodeText)
    email = Column(UnicodeText, default='')
    birthdate = Column(Date, nullable=True, default=None)
    reason = Column(UnicodeText)
    action = Column(UnicodeText)
    active = Column(Boolean, default=True)
    attendees = relationship(
        'Attendee', backref=backref('watch_list', load_on_pending=True))

    @presave_adjustment
    def _fix_birthdate(self):
        if self.birthdate == '':
            self.birthdate = None
Example #16
0
class TabletopGame(MagModel):
    code = Column(UnicodeText)
    name = Column(UnicodeText)
    attendee_id = Column(UUID, ForeignKey('attendee.id'))
    returned = Column(Boolean, default=False)
    checkouts = relationship('TabletopCheckout', backref='game')

    _repr_attr_names = ['name']

    @property
    def checked_out(self):
        try:
            return [c for c in self.checkouts if not c.returned][0]
        except Exception:
            pass
Example #17
0
class AdminAccount(MagModel):
    attendee_id = Column(UUID, ForeignKey('attendee.id'), unique=True)
    hashed = Column(UnicodeText)
    access = Column(MultiChoice(c.ACCESS_OPTS))

    password_reset = relationship('PasswordReset',
                                  backref='admin_account',
                                  uselist=False)

    def __repr__(self):
        return '<{}>'.format(self.attendee.full_name)

    @staticmethod
    def is_nick():
        return AdminAccount.admin_name() in c.JERKS

    @staticmethod
    def admin_name():
        try:
            from uber.models import Session
            with Session() as session:
                return session.admin_attendee().full_name
        except:
            return None

    @staticmethod
    def admin_email():
        try:
            from uber.models import Session
            with Session() as session:
                return session.admin_attendee().email
        except:
            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:
            return set()
Example #18
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 #19
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 #20
0
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:
            return None

    @staticmethod
    def admin_email():
        try:
            from uber.models import Session
            with Session() as session:
                return session.admin_attendee().email
        except Exception:
            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:
            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
Example #21
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]
Example #22
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
class PromoCodeGroup(MagModel):
    name = Column(UnicodeText)
    code = Column(UnicodeText, admin_only=True)
    registered = Column(UTCDateTime, server_default=utcnow())
    buyer_id = Column(UUID,
                      ForeignKey('attendee.id', ondelete='SET NULL'),
                      nullable=True)
    buyer = relationship('Attendee',
                         backref='promo_code_groups',
                         foreign_keys=buyer_id,
                         cascade='save-update,merge,refresh-expire,expunge')

    email_model_name = 'group'

    @presave_adjustment
    def group_code(self):
        """
        Promo Code Groups can be used one of two ways: Each promo
        code's unique code can be used to claim a specific badge,
        or the groups' code can be used by multiple people to
        claim random badges in the group.

        We don't want this to clash with any promo codes' existing
        codes, so we use that class' generator method.
        """
        if not self.code:
            self.code = PromoCode.generate_random_code()

    @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 email(self):
        return self.buyer.email if self.buyer else None

    @property
    def total_cost(self):
        return sum(code.cost for code in self.promo_codes if code.cost)

    @property
    def valid_codes(self):
        return [code for code in self.promo_codes if code.is_valid]

    @property
    def sorted_promo_codes(self):
        return list(
            sorted(self.promo_codes,
                   key=lambda pc: (not pc.used_by, pc.used_by[0].full_name
                                   if pc.used_by else pc.code)))

    @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):
        return 1 if self.hours_remaining_in_grace_period > 0 else c.MIN_GROUP_ADDITION
Example #24
0
class AdminAccount(MagModel):
    attendee_id = Column(UUID, ForeignKey('attendee.id'), unique=True)
    access_groups = relationship(
        'AccessGroup',
        backref='admin_accounts',
        cascade='save-update,merge,refresh-expire,expunge',
        secondary='admin_access_group')
    hashed = Column(UnicodeText, private=True)

    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:
            return None

    @staticmethod
    def admin_email():
        try:
            from uber.models import Session
            with Session() as session:
                return session.admin_attendee().email
        except Exception:
            return None

    @staticmethod
    def get_access_set(id=None, include_read_only=False):
        try:
            from uber.models import Session
            with Session() as session:
                id = id or cherrypy.session.get('account_id')
                account = session.admin_account(id)
                if include_read_only:
                    return account.read_or_write_access_set
                return account.write_access_set
        except Exception:
            return set()

    @classproperty
    def _extra_apply_attrs(cls):
        return set(['access_groups_ids'])

    @property
    def write_access_set(self):
        access_list = [list(group.access) for group in self.access_groups]
        return set([item for sublist in access_list for item in sublist])

    @property
    def read_access_set(self):
        access_list = [
            list(group.read_only_access) for group in self.access_groups
        ]
        return set([item for sublist in access_list for item in sublist])

    @property
    def read_or_write_access_set(self):
        return self.read_access_set.union(self.write_access_set)

    @property
    def access_groups_labels(self):
        return [d.name for d in self.access_groups]

    @property
    def access_groups_ids(self):
        _, ids = self._get_relation_ids('access_groups')
        return [str(a.id) for a in self.access_groups] if ids is None else ids

    @access_groups_ids.setter
    def access_groups_ids(self, value):
        values = set(s for s in listify(value) if s)
        for group in list(self.access_groups):
            if group.id not in values:
                # Manually remove the group to ensure the associated
                # rows in the admin_access_group table are deleted.
                self.access_groups.remove(group)
        self._set_relation_ids('access_groups', AccessGroup, list(values))

    @property
    def allowed_access_opts(self):
        return self.session.query(AccessGroup).all()

    @property
    def allowed_api_access_opts(self):
        no_access_set = self.invalid_api_accesses()
        return [(access, label) for access, label in c.API_ACCESS_OPTS
                if access not in no_access_set]

    @property
    def viewable_guest_group_types(self):
        return [
            opt for opt in c.GROUP_TYPE_VARS
            if opt.lower() + "_admin" in self.read_or_write_access_set
        ]

    @property
    def is_admin(self):
        return 'devtools' in self.write_access_set

    @property
    def is_mivs_judge_or_admin(self, id=None):
        try:
            from uber.models import Session
            with Session() as session:
                id = id or cherrypy.session.get('account_id')
                admin_account = session.admin_account(id)
                return admin_account.judge or 'mivs_judging' in admin_account.read_or_write_access_set
        except Exception:
            return None

    @property
    def api_read(self):
        return any([
            group.has_any_access('api', read_only=True)
            for group in self.access_groups
        ])

    @property
    def api_update(self):
        return any([
            group.has_access_level('api', AccessGroup.LIMITED)
            for group in self.access_groups
        ])

    @property
    def api_create(self):
        return any([
            group.has_access_level('api', AccessGroup.CONTACT)
            for group in self.access_groups
        ])

    @property
    def api_delete(self):
        return any(
            [group.has_full_access('api') for group in self.access_groups])

    @property
    def full_dept_admin(self):
        return any([
            group.has_full_access('dept_admin') for group in self.access_groups
        ])

    @property
    def full_shifts_admin(self):
        return any([
            group.has_full_access('shifts_admin')
            for group in self.access_groups
        ])

    @property
    def full_dept_checklist_admin(self):
        return any([
            group.has_full_access('dept_checklist')
            for group in self.access_groups
        ])

    @property
    def full_attractions_admin(self):
        return any([
            group.has_full_access('attractions_admin')
            for group in self.access_groups
        ])

    @property
    def full_email_admin(self):
        return any([
            group.has_full_access('email_admin')
            for group in self.access_groups
        ])

    @property
    def full_registration_admin(self):
        return any([
            group.has_full_access('registration')
            for group in self.access_groups
        ])

    def max_level_access(self, site_section_or_page, read_only=False):
        write_access_list = [
            int(group.access.get(site_section_or_page, 0))
            for group in self.access_groups
        ]
        read_access_list = [
            int(group.read_only_access.get(site_section_or_page, 0))
            for group in self.access_groups
        ]
        return max(write_access_list +
                   read_access_list) if read_only else max(write_access_list)

    @presave_adjustment
    def disable_api_access(self):
        invalid_api = self.invalid_api_accesses()
        if invalid_api:
            self.remove_disabled_api_keys(invalid_api)

    def remove_disabled_api_keys(self, invalid_api):
        revoked_time = datetime.utcnow()
        for api_token in self.active_api_tokens:
            if invalid_api.intersection(api_token.access_ints):
                api_token.revoked_time = revoked_time

    def invalid_api_accesses(self):
        """
        Builds and returns a set of API accesses that this account does not have.
        Designed to help remove/hide API keys/options that accounts do not have permissions for.
        """
        removed_api = set(c.API_ACCESS.keys())
        for access, label in c.API_ACCESS_OPTS:
            access_name = 'api_' + label.lower()
            if getattr(self, access_name, None):
                removed_api.remove(access)
        return removed_api
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 #26
0
class Attraction(MagModel):
    _NONE = 0
    _PER_FEATURE = 1
    _PER_ATTRACTION = 2
    _RESTRICTION_OPTS = [(
        _NONE,
        'None – '
        'Attendees can attend as many events as they wish '
        '(least restrictive)'
    ), (
        _PER_FEATURE,
        'Once Per Feature – '
        'Attendees can only attend each feature once'
    ), (
        _PER_ATTRACTION,
        'Once Per Attraction – '
        'Attendees can only attend this attraction once '
        '(most restrictive)'
    )]
    _RESTRICTIONS = dict(_RESTRICTION_OPTS)

    _ADVANCE_CHECKIN_OPTS = [
        (-1, 'Anytime during event'),
        (0, 'When the event starts'),
        (300, '5 minutes before'),
        (600, '10 minutes before'),
        (900, '15 minutes before'),
        (1200, '20 minutes before'),
        (1800, '30 minutes before'),
        (2700, '45 minutes before'),
        (3600, '1 hour before')]

    _ADVANCE_NOTICES_OPTS = [
        ('', 'Never'),
        (0, 'When checkin starts'),
        (300, '5 minutes before checkin'),
        (900, '15 minutes before checkin'),
        (1800, '30 minutes before checkin'),
        (3600, '1 hour before checkin'),
        (7200, '2 hours before checkin'),
        (86400, '1 day before checkin')]

    name = Column(UnicodeText, unique=True)
    slug = Column(UnicodeText, unique=True)
    description = Column(UnicodeText)
    is_public = Column(Boolean, default=False)
    advance_notices = Column(JSON, default=[], server_default='[]')
    advance_checkin = Column(Integer, default=0)  # In seconds
    restriction = Column(Choice(_RESTRICTION_OPTS), default=_NONE)
    badge_num_required = Column(Boolean, default=False)
    department_id = Column(UUID, ForeignKey('department.id'), nullable=True)
    owner_id = Column(UUID, ForeignKey('admin_account.id'))

    owner = relationship(
        'AdminAccount',
        cascade='save-update,merge',
        backref=backref(
            'attractions',
            cascade='all,delete-orphan',
            uselist=True,
            order_by='Attraction.name'))
    owner_attendee = relationship(
        'Attendee',
        cascade='save-update,merge',
        secondary='admin_account',
        uselist=False,
        viewonly=True)
    department = relationship(
        'Department',
        cascade='save-update,merge',
        backref=backref(
            'attractions',
            cascade='save-update,merge',
            uselist=True),
        order_by='Department.name')
    features = relationship(
        'AttractionFeature',
        backref='attraction',
        order_by='[AttractionFeature.name, AttractionFeature.id]')
    public_features = relationship(
        'AttractionFeature',
        primaryjoin='and_('
                    'AttractionFeature.attraction_id == Attraction.id,'
                    'AttractionFeature.is_public == True)',
        viewonly=True,
        order_by='[AttractionFeature.name, AttractionFeature.id]')
    events = relationship(
        'AttractionEvent',
        backref='attraction',
        viewonly=True,
        order_by='[AttractionEvent.start_time, AttractionEvent.id]')
    signups = relationship(
        'AttractionSignup',
        backref='attraction',
        viewonly=True,
        order_by='[AttractionSignup.checkin_time, AttractionSignup.id]')

    @presave_adjustment
    def _sluggify_name(self):
        self.slug = sluggify(self.name)

    @property
    def feature_opts(self):
        return [(f.id, f.name) for f in self.features]

    @property
    def feature_names_by_id(self):
        return OrderedDict(self.feature_opts)

    @property
    def used_location_opts(self):
        locs = set(e.location for e in self.events)
        sorted_locs = sorted(locs, key=lambda l: c.EVENT_LOCATIONS[l])
        return [(l, c.EVENT_LOCATIONS[l]) for l in sorted_locs]

    @property
    def unused_location_opts(self):
        locs = set(e.location for e in self.events)
        return [(l, s) for l, s in c.EVENT_LOCATION_OPTS if l not in locs]

    @property
    def advance_checkin_label(self):
        if self.advance_checkin < 0:
            return 'anytime during the event'
        return humanize_timedelta(
            seconds=self.advance_checkin,
            separator=' ',
            now='by the time the event starts',
            prefix='at least ',
            suffix=' before the event starts')

    @property
    def location_opts(self):
        locations = map(lambda e: (e.location, c.EVENT_LOCATIONS[e.location]), self.events)
        return [(l, s) for l, s in sorted(locations, key=lambda l: l[1])]

    @property
    def locations(self):
        return OrderedDict(self.location_opts)

    @property
    def locations_by_feature_id(self):
        return groupify(self.features, 'id', lambda f: f.locations)

    def signups_requiring_notification(self, session, from_time, to_time, options=None):
        """
        Returns a dict of AttractionSignups that require notification.

        The keys of the returned dict are the amount of advanced notice, given
        in seconds. A key of -1 indicates confirmation notices after a signup.

        The query generated by this method looks horrific, but is surprisingly
        efficient.
        """
        advance_checkin = max(0, self.advance_checkin)
        subqueries = []
        for advance_notice in sorted(set([-1] + self.advance_notices)):
            event_filters = [AttractionEvent.attraction_id == self.id]
            if advance_notice == -1:
                notice_ident = cast(AttractionSignup.attraction_event_id, UnicodeText)
                notice_param = bindparam('confirm_notice', advance_notice).label('advance_notice')
            else:
                advance_notice = max(0, advance_notice) + advance_checkin
                notice_delta = timedelta(seconds=advance_notice)
                event_filters += [
                    AttractionEvent.start_time >= from_time + notice_delta,
                    AttractionEvent.start_time < to_time + notice_delta]
                notice_ident = func.concat(AttractionSignup.attraction_event_id, '_{}'.format(advance_notice))
                notice_param = bindparam(
                    'advance_notice_{}'.format(advance_notice), advance_notice).label('advance_notice')

            subquery = session.query(AttractionSignup, notice_param).filter(
                AttractionSignup.is_unchecked_in,
                AttractionSignup.attraction_event_id.in_(
                    session.query(AttractionEvent.id).filter(*event_filters)),
                not_(exists().where(and_(
                    AttractionNotification.ident == notice_ident,
                    AttractionNotification.attraction_event_id == AttractionSignup.attraction_event_id,
                    AttractionNotification.attendee_id == AttractionSignup.attendee_id)))).with_labels()
            subqueries.append(subquery)

        query = subqueries[0].union(*subqueries[1:])
        if options:
            query = query.options(*listify(options))
        query.order_by(AttractionSignup.id)
        return groupify(query, lambda x: x[0], lambda x: x[1])
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())
Example #28
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 #29
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])
Example #30
0
class AttractionSignup(MagModel):
    attraction_event_id = Column(UUID, ForeignKey('attraction_event.id'))
    attraction_id = Column(UUID, ForeignKey('attraction.id'))
    attendee_id = Column(UUID, ForeignKey('attendee.id'))

    signup_time = Column(UTCDateTime, default=lambda: datetime.now(pytz.UTC))
    checkin_time = Column(UTCDateTime, default=lambda: utcmin.datetime, index=True)

    notifications = relationship(
        'AttractionNotification',
        cascade='save-update, merge, refresh-expire, expunge',
        backref=backref(
            'signup',
            cascade='save-update,merge',
            uselist=False,
            viewonly=True),
        primaryjoin='and_('
                    'AttractionSignup.attendee_id == foreign(AttractionNotification.attendee_id),'
                    'AttractionSignup.attraction_event_id == foreign(AttractionNotification.attraction_event_id))',
        order_by='AttractionNotification.sent_time',
        viewonly=True)

    __mapper_args__ = {'confirm_deleted_rows': False}
    __table_args__ = (UniqueConstraint('attraction_event_id', 'attendee_id'),)

    def __init__(self, attendee=None, event=None, **kwargs):
        super(AttractionSignup, self).__init__(**kwargs)
        if attendee:
            self.attendee = attendee
        if event:
            self.event = event
        if not self.attraction_id and self.event:
            self.attraction_id = self.event.attraction_id

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

    @property
    def checkin_time_local(self):
        if self.is_checked_in:
            return self.checkin_time.astimezone(c.EVENT_TIMEZONE)
        return None

    @property
    def checkin_time_label(self):
        if self.is_checked_in:
            return self.checkin_time_local.strftime('%-I:%M %p %A')
        return 'Not checked in'

    @property
    def signup_time_label(self):
        return self.signup_time_local.strftime('%-I:%M %p %A')

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

    @property
    def email_model_name(self):
        return 'signup'

    @hybrid_property
    def is_checked_in(self):
        return self.checkin_time > utcmin.datetime

    @is_checked_in.expression
    def is_checked_in(cls):
        return cls.checkin_time > utcmin.datetime

    @hybrid_property
    def is_unchecked_in(self):
        return self.checkin_time <= utcmin.datetime

    @is_unchecked_in.expression
    def is_unchecked_in(cls):
        return cls.checkin_time <= utcmin.datetime