コード例 #1
0
ファイル: panels.py プロジェクト: binary1230/ubersystem
class PanelApplicant(SocialMediaMixin, MagModel):
    app_id = Column(UUID, ForeignKey('panel_application.id',
                                     ondelete='cascade'))
    attendee_id = Column(UUID,
                         ForeignKey('attendee.id', ondelete='cascade'),
                         nullable=True)
    submitter = Column(Boolean, default=False)
    first_name = Column(UnicodeText)
    last_name = Column(UnicodeText)
    email = Column(UnicodeText)
    cellphone = Column(UnicodeText)
    communication_pref = Column(MultiChoice(c.COMMUNICATION_PREF_OPTS))
    other_communication_pref = Column(UnicodeText)
    pronouns = Column(MultiChoice(c.PRONOUN_OPTS))
    other_pronouns = Column(UnicodeText)
    occupation = Column(UnicodeText)
    website = Column(UnicodeText)
    other_credentials = Column(UnicodeText)

    @property
    def has_credentials(self):
        return any([self.occupation, self.website, self.other_credentials])

    @property
    def full_name(self):
        return self.first_name + ' ' + self.last_name
コード例 #2
0
ファイル: mivs.py プロジェクト: Travis-Snoozy/ubersystem
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
コード例 #3
0
ファイル: mits.py プロジェクト: Travis-Snoozy/ubersystem
class MITSApplicant(MagModel):
    team_id = Column(ForeignKey('mits_team.id'))
    attendee_id = Column(ForeignKey('attendee.id'), nullable=True)
    primary_contact = Column(Boolean, default=False)
    first_name = Column(UnicodeText)
    last_name = Column(UnicodeText)
    email = Column(UnicodeText)
    cellphone = Column(UnicodeText)
    contact_method = Column(Choice(c.MITS_CONTACT_OPTS), default=c.TEXTING)

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

    email_model_name = 'applicant'

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

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

    def has_requested(self, night):
        return night in self.requested_room_nights_ints
コード例 #4
0
ファイル: hotel.py プロジェクト: parapluiecreative/ubersystem
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)
コード例 #5
0
class AttendeeTournament(MagModel):
    first_name = Column(UnicodeText)
    last_name = Column(UnicodeText)
    email = Column(UnicodeText)
    cellphone = Column(UnicodeText)
    game = Column(UnicodeText)
    availability = Column(MultiChoice(c.TOURNAMENT_AVAILABILITY_OPTS))
    format = Column(UnicodeText)
    experience = Column(UnicodeText)
    needs = Column(UnicodeText)
    why = Column(UnicodeText)
    volunteering = Column(Boolean, default=False)

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

    email_model_name = 'app'

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

    @property
    def matching_attendee(self):
        return self.session.query(Attendee).filter(
            Attendee.first_name == self.first_name.title(),
            Attendee.last_name == self.last_name.title(),
            func.lower(Attendee.email) == self.email.lower()).first()
コード例 #6
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]
コード例 #7
0
ファイル: api.py プロジェクト: NicolasWalker/ubersystem
class ApiToken(MagModel):
    admin_account_id = Column(UUID, ForeignKey('admin_account.id'))
    token = Column(UUID, default=lambda: str(uuid.uuid4()), private=True)
    access = Column(MultiChoice(c.API_ACCESS_OPTS))
    name = Column(UnicodeText)
    description = Column(UnicodeText)
    issued_time = Column(UTCDateTime, default=lambda: datetime.now(UTC))
    revoked_time = Column(UTCDateTime, default=None, nullable=True)
コード例 #8
0
class FoodRestrictions(MagModel):
    attendee_id = Column(UUID, ForeignKey('attendee.id'), unique=True)
    standard = Column(MultiChoice(c.FOOD_RESTRICTION_OPTS))
    sandwich_pref = Column(MultiChoice(c.SANDWICH_OPTS))
    freeform = Column(UnicodeText)

    def __getattr__(self, name):
        try:
            return super(FoodRestrictions, self).__getattr__(name)
        except AttributeError:
            restriction = getattr(c, name.upper())
            if restriction not in c.FOOD_RESTRICTIONS:
                return MagModel.__getattr__(self, name)
            elif restriction == c.VEGAN and c.VEGAN in self.standard_ints:
                return False
            elif restriction == c.PORK and c.VEGAN in self.standard_ints:
                return True
            else:
                return restriction in self.standard_ints
コード例 #9
0
class GuestPanel(MagModel):
    guest_id = Column(UUID, ForeignKey('guest_group.id'), unique=True)
    wants_panel = Column(Choice(c.GUEST_PANEL_OPTS), nullable=True)
    name = Column(UnicodeText)
    length = Column(UnicodeText)
    desc = Column(UnicodeText)
    tech_needs = Column(MultiChoice(c.TECH_NEED_OPTS))
    other_tech_needs = Column(UnicodeText)

    @property
    def status(self):
        return self.wants_panel_label
コード例 #10
0
ファイル: hotel.py プロジェクト: parapluiecreative/ubersystem
class HotelRequests(MagModel, NightsMixin):
    attendee_id = Column(UUID, ForeignKey('attendee.id'), unique=True)
    nights = Column(MultiChoice(c.NIGHT_OPTS))
    wanted_roommates = Column(UnicodeText)
    unwanted_roommates = Column(UnicodeText)
    special_needs = Column(UnicodeText)
    approved = Column(Boolean, default=False, admin_only=True)

    def decline(self):
        nights = [n for n in self.nights.split(',') if int(n) in c.CORE_NIGHTS]
        self.nights = ','.join(nights)

    @presave_adjustment
    def cascading_save(self):
        self.attendee.presave_adjustments()

    def __repr__(self):
        return '<{self.attendee.full_name} Hotel Requests>'.format(self=self)
コード例 #11
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()
コード例 #12
0
class ApiToken(MagModel):
    admin_account_id = Column(UUID, ForeignKey('admin_account.id'))
    token = Column(UUID, default=lambda: str(uuid.uuid4()), private=True)
    access = Column(MultiChoice(c.API_ACCESS_OPTS))
    name = Column(UnicodeText)
    description = Column(UnicodeText)
    issued_time = Column(UTCDateTime, default=lambda: datetime.now(UTC))
    revoked_time = Column(UTCDateTime, default=None, nullable=True)

    @property
    def api_read(self):
        return c.API_READ in self.access_ints

    @property
    def api_update(self):
        return c.API_UPDATE in self.access_ints

    @property
    def api_create(self):
        return c.API_CREATE in self.access_ints

    @property
    def api_delete(self):
        return c.API_DELETE in self.access_ints
コード例 #13
0
ファイル: mits.py プロジェクト: Travis-Snoozy/ubersystem
class MITSTimes(MagModel):
    team_id = Column(ForeignKey('mits_team.id'))
    showcase_availability = Column(MultiChoice(c.MITS_SHOWCASE_SCHEDULE_OPTS))
    availability = Column(MultiChoice(c.MITS_SCHEDULE_OPTS))
コード例 #14
0
ファイル: admin.py プロジェクト: Travis-Snoozy/ubersystem
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
コード例 #15
0
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
コード例 #16
0
class MITSTimes(MagModel):
    team_id = Column(ForeignKey('mits_team.id'))
    availability = Column(MultiChoice(c.MITS_SCHEDULE_OPTS))
    multiple_tables = Column(MultiChoice(c.MITS_SCHEDULE_OPTS))
コード例 #17
0
ファイル: mivs.py プロジェクト: Travis-Snoozy/ubersystem
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
コード例 #18
0
class GuestTravelPlans(MagModel):
    guest_id = Column(UUID, ForeignKey('guest_group.id'), unique=True)
    modes = Column(MultiChoice(c.GUEST_TRAVEL_OPTS))
    modes_text = Column(UnicodeText)
    details = Column(UnicodeText)
コード例 #19
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_override = Column(Integer,
                                  default=0,
                                  index=True,
                                  admin_only=True)
    amount_refunded_override = Column(Integer, default=0, admin_only=True)
    cost = Column(Integer, default=0, admin_only=True)
    purchased_items = Column(MutableDict.as_mutable(JSONB),
                             default={},
                             server_default='{}')
    refunded_items = Column(MutableDict.as_mutable(JSONB),
                            default={},
                            server_default='{}')
    auto_recalc = Column(Boolean, default=True, admin_only=True)
    stripe_txn_share_logs = relationship('StripeTransactionGroup',
                                         backref='group')

    can_add = Column(Boolean, default=False, admin_only=True)
    admin_notes = Column(UnicodeText, admin_only=True)
    status = Column(Choice(c.DEALER_STATUS_OPTS),
                    default=c.UNAPPROVED,
                    admin_only=True)
    registered = Column(UTCDateTime, server_default=utcnow())
    approved = Column(UTCDateTime, nullable=True)
    leader_id = Column(UUID,
                       ForeignKey('attendee.id',
                                  use_alter=True,
                                  name='fk_leader'),
                       nullable=True)
    creator_id = Column(UUID, ForeignKey('attendee.id'), nullable=True)

    creator = relationship('Attendee',
                           foreign_keys=creator_id,
                           backref=backref('created_groups',
                                           order_by='Group.name',
                                           cascade='all,delete-orphan'),
                           cascade='save-update,merge,refresh-expire,expunge',
                           remote_side='Attendee.id',
                           single_parent=True)
    leader = relationship('Attendee',
                          foreign_keys=leader_id,
                          post_update=True,
                          cascade='all')
    studio = relationship('IndieStudio', uselist=False, backref='group')
    guest = relationship('GuestGroup', backref='group', uselist=False)

    _repr_attr_names = ['name']

    @presave_adjustment
    def _cost_and_leader(self):
        assigned = [a for a in self.attendees if not a.is_unassigned]
        if len(assigned) == 1:
            [self.leader] = assigned
        if self.auto_recalc:
            self.cost = self.default_cost
        elif not self.cost:
            self.cost = 0
        if self.status == c.APPROVED and not self.approved:
            self.approved = datetime.now(UTC)
        if self.leader and self.is_dealer:
            self.leader.ribbon = add_opt(self.leader.ribbon_ints,
                                         c.DEALER_RIBBON)
        if not self.is_unpaid:
            for a in self.attendees:
                a.presave_adjustments()

    @presave_adjustment
    def update_purchased_items(self):
        if self.cost == self.orig_value_of(
                'cost') and self.tables == self.orig_value_of('tables'):
            return

        self.purchased_items.clear()
        if not self.auto_recalc:
            # ¯\_(ツ)_/¯
            if self.cost:
                self.purchased_items['group_total'] = self.cost
        else:
            # Groups tables and paid-by-group badges by cost
            table_count = int(float(self.tables))
            default_price = c.TABLE_PRICES['default_price']
            more_tables = {default_price: 0}
            for i in table_count:
                if c.TABLE_PRICES[i] == default_price:
                    more_tables[default_price] += 1
                else:
                    self.purchased_items['table_' + i] = c.TABLE_PRICES[i]
            if more_tables[default_price]:
                self.purchased_items[
                    more_tables[default_price] + ' extra table(s) at $' +
                    default_price +
                    ' each'] = default_price * more_tables[default_price]

            badges_by_cost = {}
            for attendee in self.attendees:
                if attendee.paid == c.PAID_BY_GROUP:
                    badges_by_cost[attendee.badge_cost] = bool(
                        badges_by_cost.get(attendee.badge_cost)) + 1
            for cost in badges_by_cost:
                self.purchased_items[badges_by_cost[cost] + ' badge(s) at $' +
                                     cost +
                                     ' each'] = cost * badges_by_cost[cost]

    @presave_adjustment
    def assign_creator(self):
        if self.is_new and not self.creator_id:
            self.creator_id = self.session.admin_attendee(
            ).id if self.session.admin_attendee() else None

    @property
    def sorted_attendees(self):
        return list(
            sorted(self.attendees,
                   key=lambda a:
                   (a.is_unassigned, a.id != self.leader_id, a.full_name)))

    @property
    def unassigned(self):
        """
        Returns a list of the unassigned badges for this group, sorted so that
        the paid-by-group badges come last, because when claiming unassigned
        badges we want to claim the "weird" ones first.
        """
        unassigned = [a for a in self.attendees if a.is_unassigned]
        return sorted(unassigned, key=lambda a: a.paid == c.PAID_BY_GROUP)

    @property
    def floating(self):
        """
        Returns the list of paid-by-group unassigned badges for this group.
        This is a separate property from the "Group.unassigned" property
        because when automatically adding or removing unassigned badges, we
        care specifically about paid-by-group badges rather than all unassigned
        badges.
        """
        return [
            a for a in self.attendees
            if a.is_unassigned and a.paid == c.PAID_BY_GROUP
        ]

    @property
    def new_ribbon(self):
        return c.DEALER_RIBBON if self.is_dealer else ''

    @property
    def ribbon_and_or_badge(self):
        badge = self.unassigned[0]
        if badge.ribbon and badge.badge_type != c.ATTENDEE_BADGE:
            return ' / '.join([badge.badge_type_label] + self.ribbon_labels)
        elif badge.ribbon:
            return ' / '.join(badge.ribbon_labels)
        else:
            return badge.badge_type_label

    @hybrid_property
    def is_dealer(self):
        return bool(self.tables and self.tables != '0' and self.tables != '0.0'
                    and (not self.registered or self.amount_paid or self.cost
                         or self.status != c.UNAPPROVED))

    @is_dealer.expression
    def is_dealer(cls):
        return and_(
            cls.tables > 0,
            or_(cls.amount_paid > 0, cls.cost > 0, cls.status != c.UNAPPROVED))

    @hybrid_property
    def is_unpaid(self):
        return self.cost > 0 and self.amount_paid == 0

    @is_unpaid.expression
    def is_unpaid(cls):
        return and_(cls.cost > 0, cls.amount_paid == 0)

    @property
    def email(self):
        if self.studio and self.studio.email:
            return self.studio.email
        elif self.leader and self.leader.email:
            return self.leader.email
        elif self.leader_id:  # unattached groups
            [leader] = [a for a in self.attendees if a.id == self.leader_id]
            return leader.email
        else:
            emails = [a.email for a in self.attendees if a.email]
            if len(emails) == 1:
                return emails[0]

    @hybrid_property
    def badges_purchased(self):
        return len([a for a in self.attendees if a.paid == c.PAID_BY_GROUP])

    @badges_purchased.expression
    def badges_purchased(cls):
        from uber.models import Attendee
        return exists().where(
            and_(Attendee.group_id == cls.id,
                 Attendee.paid == c.PAID_BY_GROUP))

    @property
    def badges(self):
        return len(self.attendees)

    @hybrid_property
    def unregistered_badges(self):
        return len([a for a in self.attendees if a.is_unassigned])

    @unregistered_badges.expression
    def unregistered_badges(cls):
        from uber.models import Attendee
        return exists().where(
            and_(Attendee.group_id == cls.id, Attendee.first_name == ''))

    @cost_property
    def table_cost(self):
        table_count = int(float(self.tables))
        return sum(c.TABLE_PRICES[i] for i in range(1, 1 + table_count))

    @property
    def new_badge_cost(self):
        return c.DEALER_BADGE_PRICE if self.is_dealer else c.get_group_price()

    @cost_property
    def badge_cost(self):
        total = 0
        for attendee in self.attendees:
            if attendee.paid == c.PAID_BY_GROUP:
                total += attendee.badge_cost
        return total

    @property
    def amount_extra(self):
        if self.is_new:
            return sum(a.total_cost - a.badge_cost for a in self.attendees
                       if a.paid == c.PAID_BY_GROUP)
        else:
            return 0

    @property
    def total_cost(self):
        return self.default_cost + self.amount_extra

    @property
    def amount_unpaid(self):
        if self.registered:
            return max(0, ((self.cost * 100) - self.amount_paid) / 100)
        else:
            return self.total_cost

    @hybrid_property
    def amount_paid(self):
        return sum([
            item.amount for item in self.receipt_items
            if item.txn_type == c.PAYMENT
        ])

    @amount_paid.expression
    def amount_paid(cls):
        from uber.models import ReceiptItem

        return select([func.sum(ReceiptItem.amount)]).where(
            and_(ReceiptItem.group_id == cls.id,
                 ReceiptItem.txn_type == c.PAYMENT)).label('amount_paid')

    @hybrid_property
    def amount_refunded(self):
        return sum([
            item.amount for item in self.receipt_items
            if item.txn_type == c.REFUND
        ])

    @amount_refunded.expression
    def amount_refunded(cls):
        from uber.models import ReceiptItem

        return select([func.sum(ReceiptItem.amount)]).where(
            and_(ReceiptItem.group_id == cls.id,
                 ReceiptItem.txn_type == c.REFUND)).label('amount_refunded')

    def balance_by_item_type(self, item_type):
        """
        Return a sum of all the receipt item payments, minus the refunds, for this model by item type
        """
        return sum([amt for type, amt in self.itemized_payments if type == item_type]) \
               - sum([amt for type, amt in self.itemized_refunds if type == item_type])

    @property
    def itemized_payments(self):
        return [(item.item_type, item.amount) for item in self.receipt_items
                if item.txn_type == c.PAYMENT]

    @property
    def itemized_refunds(self):
        return [(item.item_type, item.amount) for item in self.receipt_items
                if item.txn_type == c.REFUND]

    @property
    def dealer_max_badges(self):
        return c.MAX_DEALERS or math.ceil(self.tables) + 1

    @property
    def dealer_badges_remaining(self):
        return self.dealer_max_badges - self.badges

    @property
    def hours_since_registered(self):
        if not self.registered:
            return 0
        delta = datetime.now(UTC) - self.registered
        return max(0, delta.total_seconds()) / 60.0 / 60.0

    @property
    def hours_remaining_in_grace_period(self):
        return max(0,
                   c.GROUP_UPDATE_GRACE_PERIOD - self.hours_since_registered)

    @property
    def is_in_grace_period(self):
        return self.hours_remaining_in_grace_period > 0

    @property
    def min_badges_addable(self):
        if self.is_dealer and not self.dealer_badges_remaining or self.amount_unpaid:
            return 0
        if self.can_add:
            return 1
        elif self.is_dealer:
            return 0
        else:
            return c.MIN_GROUP_ADDITION

    @property
    def requested_hotel_info(self):
        if self.leader:
            return self.leader.requested_hotel_info
        elif self.leader_id:  # unattached groups
            for attendee in self.attendees:
                if attendee.id == self.leader_id:
                    return attendee.requested_hotel_info
        else:
            return any(a.requested_hotel_info for a in self.attendees)

    @property
    def physical_address(self):
        address1 = self.address1.strip()
        address2 = self.address2.strip()
        city = self.city.strip()
        region = self.region.strip()
        zip_code = self.zip_code.strip()
        country = self.country.strip()

        country = '' if country == 'United States' else country.strip()

        if city and region:
            city_region = '{}, {}'.format(city, region)
        else:
            city_region = city or region
        city_region_zip = '{} {}'.format(city_region, zip_code).strip()

        physical_address = [address1, address2, city_region_zip, country]
        return '\n'.join([s for s in physical_address if s])

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

    @property
    def guidebook_subtitle(self):
        category_labels = [
            cat for cat in self.categories_labels if 'Other' not in cat
        ] + [self.categories_text]
        return ', '.join(category_labels[:5])

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

    @property
    def guidebook_location(self):
        return ''
コード例 #20
0
class Attendee(MagModel, TakesPaymentMixin):
    watchlist_id = Column(UUID,
                          ForeignKey('watch_list.id', ondelete='set null'),
                          nullable=True,
                          default=None)

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

    # NOTE: The cascade relationships for promo_code do NOT include
    # "save-update". During the preregistration workflow, before an Attendee
    # has paid, we create ephemeral Attendee objects that are saved in the
    # cherrypy session, but are NOT saved in the database. If the cascade
    # relationships specified "save-update" then the Attendee would
    # automatically be inserted in the database when the promo_code is set on
    # the Attendee object (which we do not want until the attendee pays).
    #
    # The practical result of this is that we must manually set promo_code_id
    # in order for the relationship to be persisted.
    promo_code_id = Column(UUID,
                           ForeignKey('promo_code.id'),
                           nullable=True,
                           index=True)
    promo_code = relationship('PromoCode',
                              backref=backref(
                                  'used_by',
                                  cascade='merge,refresh-expire,expunge'),
                              foreign_keys=promo_code_id,
                              cascade='merge,refresh-expire,expunge')

    placeholder = Column(Boolean, default=False, admin_only=True)
    first_name = Column(UnicodeText)
    last_name = Column(UnicodeText)
    legal_name = Column(UnicodeText)
    email = Column(UnicodeText)
    birthdate = Column(Date, nullable=True, default=None)
    age_group = Column(Choice(c.AGE_GROUPS),
                       default=c.AGE_UNKNOWN,
                       nullable=True)

    international = Column(Boolean, default=False)
    zip_code = Column(UnicodeText)
    address1 = Column(UnicodeText)
    address2 = Column(UnicodeText)
    city = Column(UnicodeText)
    region = Column(UnicodeText)
    country = Column(UnicodeText)
    no_cellphone = Column(Boolean, default=False)
    ec_name = Column(UnicodeText)
    ec_phone = Column(UnicodeText)
    cellphone = Column(UnicodeText)

    # Represents a request for hotel booking info during preregistration
    requested_hotel_info = Column(Boolean, default=False)

    interests = Column(MultiChoice(c.INTEREST_OPTS))
    found_how = Column(UnicodeText)
    comments = Column(UnicodeText)
    for_review = Column(UnicodeText, admin_only=True)
    admin_notes = Column(UnicodeText, admin_only=True)

    public_id = Column(UUID, default=lambda: str(uuid4()))
    badge_num = Column(Integer, default=None, nullable=True, admin_only=True)
    badge_type = Column(Choice(c.BADGE_OPTS), default=c.ATTENDEE_BADGE)
    badge_status = Column(Choice(c.BADGE_STATUS_OPTS),
                          default=c.NEW_STATUS,
                          index=True,
                          admin_only=True)
    ribbon = Column(MultiChoice(c.RIBBON_OPTS), admin_only=True)

    affiliate = Column(UnicodeText)

    # attendee shirt size for both swag and staff shirts
    shirt = Column(Choice(c.SHIRT_OPTS), default=c.NO_SHIRT)
    can_spam = Column(Boolean, default=False)
    regdesk_info = Column(UnicodeText, admin_only=True)
    extra_merch = Column(UnicodeText, admin_only=True)
    got_merch = Column(Boolean, default=False, admin_only=True)

    reg_station = Column(Integer, nullable=True, admin_only=True)
    registered = Column(UTCDateTime, server_default=utcnow())
    confirmed = Column(UTCDateTime, nullable=True, default=None)
    checked_in = Column(UTCDateTime, nullable=True)

    paid = Column(Choice(c.PAYMENT_OPTS),
                  default=c.NOT_PAID,
                  index=True,
                  admin_only=True)
    overridden_price = Column(Integer, nullable=True, admin_only=True)
    base_badge_price = Column(Integer, default=0, admin_only=True)
    amount_paid = Column(Integer, default=0, admin_only=True)
    amount_extra = Column(Choice(c.DONATION_TIER_OPTS, allow_unspecified=True),
                          default=0)
    extra_donation = Column(Integer, default=0)
    payment_method = Column(Choice(c.PAYMENT_METHOD_OPTS), nullable=True)
    amount_refunded = Column(Integer, default=0, admin_only=True)

    badge_printed_name = Column(UnicodeText)

    requested_any_dept = Column(Boolean, default=False)

    dept_memberships = relationship('DeptMembership', backref='attendee')
    dept_roles = relationship(
        'DeptRole',
        backref='attendees',
        cascade='save-update,merge,refresh-expire,expunge',
        secondaryjoin='and_('
        'dept_membership_dept_role.c.dept_role_id '
        '== DeptRole.id, '
        'dept_membership_dept_role.c.dept_membership_id '
        '== DeptMembership.id)',
        secondary='join(DeptMembership, dept_membership_dept_role)',
        order_by='DeptRole.name',
        viewonly=True)
    shifts = relationship('Shift', backref='attendee')
    jobs_in_assigned_depts = relationship(
        'Job',
        backref='attendees_in_dept',
        cascade='save-update,merge,refresh-expire,expunge',
        secondaryjoin='DeptMembership.department_id == Job.department_id',
        secondary='dept_membership',
        order_by='Job.name',
        viewonly=True)
    depts_where_working = relationship(
        'Department',
        backref='attendees_working_shifts',
        cascade='save-update,merge,refresh-expire,expunge',
        secondary='join(Shift, Job)',
        order_by='Department.name',
        viewonly=True)
    dept_memberships_with_role = relationship(
        'DeptMembership',
        primaryjoin='and_('
        'Attendee.id == DeptMembership.attendee_id, '
        'DeptMembership.has_role == True)',
        viewonly=True)
    dept_memberships_as_dept_head = relationship(
        'DeptMembership',
        primaryjoin='and_('
        'Attendee.id == DeptMembership.attendee_id, '
        'DeptMembership.is_dept_head == True)',
        viewonly=True)
    dept_memberships_as_poc = relationship(
        'DeptMembership',
        primaryjoin='and_('
        'Attendee.id == DeptMembership.attendee_id, '
        'DeptMembership.is_poc == True)',
        viewonly=True)
    dept_memberships_where_can_admin_checklist = relationship(
        'DeptMembership',
        primaryjoin='and_('
        'Attendee.id == DeptMembership.attendee_id, '
        'or_('
        'DeptMembership.is_dept_head == True,'
        'DeptMembership.is_checklist_admin == True))',
        viewonly=True)
    dept_memberships_as_checklist_admin = relationship(
        'DeptMembership',
        primaryjoin='and_('
        'Attendee.id == DeptMembership.attendee_id, '
        'DeptMembership.is_checklist_admin == True)',
        viewonly=True)
    pocs_for_depts_where_working = relationship(
        'Attendee',
        cascade='save-update,merge,refresh-expire,expunge',
        primaryjoin='Attendee.id == Shift.attendee_id',
        secondaryjoin='and_('
        'DeptMembership.attendee_id == Attendee.id, '
        'DeptMembership.is_poc == True)',
        secondary='join(Shift, Job).join(DeptMembership, '
        'DeptMembership.department_id == Job.department_id)',
        order_by='Attendee.full_name',
        viewonly=True)
    dept_heads_for_depts_where_working = relationship(
        'Attendee',
        cascade='save-update,merge,refresh-expire,expunge',
        primaryjoin='Attendee.id == Shift.attendee_id',
        secondaryjoin='and_('
        'DeptMembership.attendee_id == Attendee.id, '
        'DeptMembership.is_dept_head == True)',
        secondary='join(Shift, Job).join(DeptMembership, '
        'DeptMembership.department_id == Job.department_id)',
        order_by='Attendee.full_name',
        viewonly=True)

    staffing = Column(Boolean, default=False)
    nonshift_hours = Column(Integer, default=0, admin_only=True)
    past_years = Column(UnicodeText, admin_only=True)
    can_work_setup = Column(Boolean, default=False, admin_only=True)
    can_work_teardown = Column(Boolean, default=False, admin_only=True)

    # TODO: a record of when an attendee is unable to pickup a shirt
    # (which type? swag or staff? prob swag)
    no_shirt = relationship('NoShirt',
                            backref=backref('attendee', load_on_pending=True),
                            uselist=False)

    admin_account = relationship('AdminAccount',
                                 backref=backref('attendee',
                                                 load_on_pending=True),
                                 uselist=False)
    food_restrictions = relationship('FoodRestrictions',
                                     backref=backref('attendee',
                                                     load_on_pending=True),
                                     uselist=False)

    sales = relationship('Sale',
                         backref='attendee',
                         cascade='save-update,merge,refresh-expire,expunge')
    mpoints_for_cash = relationship('MPointsForCash', backref='attendee')
    old_mpoint_exchanges = relationship('OldMPointExchange',
                                        backref='attendee')
    dept_checklist_items = relationship('DeptChecklistItem',
                                        backref=backref('attendee',
                                                        lazy='subquery'))

    _attendee_table_args = [Index('ix_attendee_paid_group_id', paid, group_id)]
    if not c.SQLALCHEMY_URL.startswith('sqlite'):
        _attendee_table_args.append(
            UniqueConstraint('badge_num',
                             deferrable=True,
                             initially='DEFERRED'))

    __table_args__ = tuple(_attendee_table_args)
    _repr_attr_names = ['full_name']

    @predelete_adjustment
    def _shift_badges(self):
        if self.badge_num:
            self.session.shift_badges(self.badge_type,
                                      self.badge_num + 1,
                                      down=True)

    @presave_adjustment
    def _misc_adjustments(self):
        if not self.amount_extra:
            self.affiliate = ''

        if self.birthdate == '':
            self.birthdate = None

        if not self.extra_donation:
            self.extra_donation = 0

        if not self.gets_any_kind_of_shirt:
            self.shirt = c.NO_SHIRT

        if self.paid != c.REFUNDED:
            self.amount_refunded = 0

        if self.badge_cost == 0 and self.paid in [c.NOT_PAID, c.PAID_BY_GROUP]:
            self.paid = c.NEED_NOT_PAY

        if not self.base_badge_price:
            self.base_badge_price = self.new_badge_cost

        if c.AT_THE_CON and self.badge_num and not self.checked_in and \
                self.is_new and \
                self.badge_type not in c.PREASSIGNED_BADGE_TYPES:
            self.checked_in = datetime.now(UTC)

        if self.birthdate:
            self.age_group = self.age_group_conf['val']

        for attr in ['first_name', 'last_name']:
            value = getattr(self, attr)
            if value.isupper() or value.islower():
                setattr(self, attr, value.title())

        if self.legal_name and self.full_name == self.legal_name:
            self.legal_name = ''

    @presave_adjustment
    def _status_adjustments(self):
        if self.badge_status == c.NEW_STATUS and self.banned:
            self.badge_status = c.WATCHED_STATUS
            try:
                send_email(c.SECURITY_EMAIL,
                           [c.REGDESK_EMAIL, c.SECURITY_EMAIL],
                           c.EVENT_NAME + ' WatchList Notification',
                           render('emails/reg_workflow/attendee_watchlist.txt',
                                  {'attendee': self}),
                           model='n/a')
            except:
                log.error('unable to send banned email about {}', self)

        elif self.badge_status == c.NEW_STATUS and not self.placeholder and \
                self.first_name and (
                    self.paid in [c.HAS_PAID, c.NEED_NOT_PAY] or
                    self.paid == c.PAID_BY_GROUP and
                    self.group_id and
                    not self.group.is_unpaid):
            self.badge_status = c.COMPLETED_STATUS

    @presave_adjustment
    def _staffing_adjustments(self):
        if self.is_dept_head:
            self.staffing = True
            if c.SHIFT_CUSTOM_BADGES or \
                    c.STAFF_BADGE not in c.PREASSIGNED_BADGE_TYPES:
                self.badge_type = c.STAFF_BADGE
            if self.paid == c.NOT_PAID:
                self.paid = c.NEED_NOT_PAY
        elif c.VOLUNTEER_RIBBON in self.ribbon_ints and self.is_new:
            self.staffing = True

        if not self.is_new:
            old_ribbon = map(int, self.orig_value_of('ribbon').split(',')) \
                if self.orig_value_of('ribbon') else []
            old_staffing = self.orig_value_of('staffing')

            if self.staffing and not old_staffing or \
                    c.VOLUNTEER_RIBBON in self.ribbon_ints and \
                    c.VOLUNTEER_RIBBON not in old_ribbon:
                self.staffing = True

            elif old_staffing and not self.staffing \
                    or c.VOLUNTEER_RIBBON not in self.ribbon_ints \
                    and c.VOLUNTEER_RIBBON in old_ribbon \
                    and not self.is_dept_head:
                self.unset_volunteering()

        if self.badge_type == c.STAFF_BADGE:
            self.ribbon = remove_opt(self.ribbon_ints, c.VOLUNTEER_RIBBON)

        elif self.staffing and self.badge_type != c.STAFF_BADGE and \
                c.VOLUNTEER_RIBBON not in self.ribbon_ints:
            self.ribbon = add_opt(self.ribbon_ints, c.VOLUNTEER_RIBBON)

        if self.badge_type == c.STAFF_BADGE:
            self.staffing = True
            if not self.overridden_price \
                    and self.paid in [c.NOT_PAID, c.PAID_BY_GROUP]:
                self.paid = c.NEED_NOT_PAY

    @presave_adjustment
    def _badge_adjustments(self):
        from uber.badge_funcs import needs_badge_num
        if self.badge_type == c.PSEUDO_DEALER_BADGE:
            self.ribbon = add_opt(self.ribbon_ints, c.DEALER_RIBBON)

        self.badge_type = self.badge_type_real

        old_type = self.orig_value_of('badge_type')
        old_num = self.orig_value_of('badge_num')

        if not needs_badge_num(self):
            self.badge_num = None

        if old_type != self.badge_type or old_num != self.badge_num:
            self.session.update_badge(self, old_type, old_num)
        elif needs_badge_num(self) and not self.badge_num:
            self.badge_num = self.session.get_next_badge_num(self.badge_type)

    @presave_adjustment
    def _use_promo_code(self):
        if c.BADGE_PROMO_CODES_ENABLED and self.promo_code and \
                not self.overridden_price and self.is_unpaid:
            if self.badge_cost > 0:
                self.overridden_price = self.badge_cost
            else:
                self.paid = c.NEED_NOT_PAY

    def unset_volunteering(self):
        self.staffing = False
        self.requested_any_dept = False
        self.requested_depts = []
        self.assigned_depts = []
        self.ribbon = remove_opt(self.ribbon_ints, c.VOLUNTEER_RIBBON)
        if self.badge_type == c.STAFF_BADGE:
            self.badge_type = c.ATTENDEE_BADGE
            self.badge_num = None
        del self.shifts[:]

    @property
    def ribbon_labels(self):
        labels = super(Attendee, self)._labels('ribbon', self.ribbon)
        if c.DEPT_HEAD_RIBBON in self.ribbon_ints or not self.is_dept_head:
            return labels
        labels.append(c.RIBBONS[c.DEPT_HEAD_RIBBON])
        return sorted(labels)

    @property
    def ribbon_and_or_badge(self):
        if self.ribbon and self.badge_type != c.ATTENDEE_BADGE:
            return ' / '.join([self.badge_type_label] + self.ribbon_labels)
        elif self.ribbon:
            return ' / '.join(self.ribbon_labels)
        else:
            return self.badge_type_label

    @property
    def badge_type_real(self):
        return get_real_badge_type(self.badge_type)

    @cost_property
    def badge_cost(self):
        return self.calculate_badge_cost()

    @property
    def badge_cost_without_promo_code(self):
        return self.calculate_badge_cost(use_promo_code=False)

    def calculate_badge_cost(self, use_promo_code=True):
        if self.paid == c.NEED_NOT_PAY:
            return 0
        elif self.overridden_price is not None:
            return self.overridden_price
        elif self.base_badge_price:
            cost = self.base_badge_price
        else:
            cost = self.new_badge_cost

        if c.BADGE_PROMO_CODES_ENABLED and self.promo_code and use_promo_code:
            return self.promo_code.calculate_discounted_price(cost)
        else:
            return cost

    @property
    def new_badge_cost(self):
        # What this badge would cost if it were new, i.e., not taking into
        # account special overrides
        registered = self.registered_local if self.registered else None
        if self.is_dealer:
            return c.DEALER_BADGE_PRICE
        elif self.badge_type == c.ONE_DAY_BADGE:
            return c.get_oneday_price(registered)
        elif self.is_presold_oneday:
            return c.get_presold_oneday_price(self.badge_type)
        elif self.badge_type in c.BADGE_TYPE_PRICES:
            return int(c.BADGE_TYPE_PRICES[self.badge_type])
        elif self.age_discount != 0:
            return max(0, c.get_attendee_price(registered) + self.age_discount)
        elif self.group and self.paid == c.PAID_BY_GROUP:
            return c.get_attendee_price(registered) - c.GROUP_DISCOUNT
        else:
            return c.get_attendee_price(registered)

    @property
    def promo_code_code(self):
        """
        Convenience property for accessing `promo_code.code` if available.

        Returns:
            str: `promo_code.code` if `promo_code` is not `None`, empty string
                otherwise.
        """
        return self.promo_code.code if self.promo_code else ''

    @property
    def age_discount(self):
        return -self.age_group_conf['discount']

    @property
    def age_group_conf(self):
        if self.birthdate:
            day = c.EPOCH.date() \
                if date.today() <= c.EPOCH.date() \
                else localized_now().date()

            attendee_age = get_age_from_birthday(self.birthdate, day)
            for val, age_group in c.AGE_GROUP_CONFIGS.items():
                if val != c.AGE_UNKNOWN and \
                        age_group['min_age'] <= attendee_age and \
                        attendee_age <= age_group['max_age']:
                    return age_group

        return c.AGE_GROUP_CONFIGS[int(self.age_group or c.AGE_UNKNOWN)]

    @property
    def total_cost(self):
        return self.default_cost + self.amount_extra

    @property
    def total_donation(self):
        return self.total_cost - self.badge_cost

    @cost_property
    def donation_cost(self):
        return self.extra_donation or 0

    @property
    def amount_unpaid(self):
        if self.paid == c.PAID_BY_GROUP:
            personal_cost = max(0, self.total_cost - self.badge_cost)
        else:
            personal_cost = self.total_cost
        return max(0, personal_cost - self.amount_paid)

    @property
    def is_unpaid(self):
        return self.paid == c.NOT_PAID

    @property
    def is_unassigned(self):
        return not self.first_name

    @property
    def is_dealer(self):
        return c.DEALER_RIBBON in self.ribbon_ints or \
            self.badge_type == c.PSEUDO_DEALER_BADGE or (
                self.group and
                self.group.is_dealer and
                self.paid == c.PAID_BY_GROUP)

    @property
    def is_checklist_admin(self):
        return any(m.is_checklist_admin for m in self.dept_memberships)

    @property
    def is_dept_head(self):
        return any(m.is_dept_head for m in self.dept_memberships)

    @property
    def is_presold_oneday(self):
        """
        Returns a boolean indicating whether this is a c.FRIDAY/c.SATURDAY/etc
        badge; see the presell_one_days config option for a full explanation.
        """
        return self.badge_type_label in c.DAYS_OF_WEEK

    @property
    def is_not_ready_to_checkin(self):
        """
        Returns None if we are ready for checkin, otherwise a short error
        message why we can't check them in.
        """
        if self.paid == c.NOT_PAID:
            return "Not paid"

        # When someone claims an unassigned group badge on-site, they first
        # fill out a new registration which is paid-by-group but isn't assigned
        # to a group yet (the admin does that when they check in).
        if self.badge_status != c.COMPLETED_STATUS and not (
                self.badge_status == c.NEW_STATUS
                and self.paid == c.PAID_BY_GROUP and not self.group_id):
            return "Badge status"

        if self.is_unassigned:
            return "Badge not assigned"

        if self.is_presold_oneday:
            if self.badge_type_label != localized_now().strftime('%A'):
                return "Wrong day"

        return None

    @property
    # should be OK
    def shirt_size_marked(self):
        return self.shirt not in [c.NO_SHIRT, c.SIZE_UNKNOWN]

    @property
    def is_group_leader(self):
        return self.group and self.id == self.group.leader_id

    @property
    def unassigned_name(self):
        if self.group_id and self.is_unassigned:
            return '[Unassigned {self.badge}]'.format(self=self)

    @hybrid_property
    def full_name(self):
        return self.unassigned_name or \
            '{self.first_name} {self.last_name}'.format(self=self)

    @full_name.expression
    def full_name(cls):
        return case(
            [(
                or_(cls.first_name == None, cls.first_name
                    == ''),  # noqa: E711
                'zzz')],
            else_=func.lower(cls.first_name + ' ' + cls.last_name))

    @hybrid_property
    def last_first(self):
        return self.unassigned_name or \
            '{self.last_name}, {self.first_name}'.format(self=self)

    @last_first.expression
    def last_first(cls):
        return case(
            [(
                or_(cls.first_name == None, cls.first_name
                    == ''),  # noqa: E711
                'zzz')],
            else_=func.lower(cls.last_name + ', ' + cls.first_name))

    @hybrid_property
    def normalized_email(self):
        return self.normalize_email(self.email)

    @normalized_email.expression
    def normalized_email(cls):
        return func.replace(func.lower(func.trim(cls.email)), '.', '')

    @classmethod
    def normalize_email(cls, email):
        return email.strip().lower().replace('.', '')

    @property
    def watchlist_guess(self):
        try:
            from uber.models import Session
            with Session() as session:
                watchentries = session.guess_attendee_watchentry(self)
                return [w.to_dict() for w in watchentries]
        except Exception as ex:
            log.warning('Error guessing watchlist entry: {}', ex)
            return None

    @property
    def banned(self):
        return listify(self.watch_list or self.watchlist_guess)

    @property
    def badge(self):
        if self.paid == c.NOT_PAID:
            badge = 'Unpaid ' + self.badge_type_label
        elif self.badge_num:
            badge = '{} #{}'.format(self.badge_type_label, self.badge_num)
        else:
            badge = self.badge_type_label

        if self.ribbon:
            badge += ' ({})'.format(", ".join(self.ribbon_labels))

        return badge

    @property
    def is_transferable(self):
        return not self.is_new and \
            not self.checked_in and \
            self.paid in [c.HAS_PAID, c.PAID_BY_GROUP] and \
            self.badge_type in c.TRANSFERABLE_BADGE_TYPES and \
            not self.admin_account and \
            not self.has_role_somewhere

    @property
    def paid_for_a_swag_shirt(self):
        return self.amount_extra >= c.SHIRT_LEVEL

    @property
    def volunteer_swag_shirt_eligible(self):
        """
        Returns: True if this attendee is eligible for a swag shirt *due to
            their status as a volunteer or staff*. They may additionally be
            eligible for a swag shirt for other reasons too.
        """

        # Some events want to exclude staff badges from getting swag shirts
        # (typically because they are getting staff uniform shirts instead).
        if self.badge_type == c.STAFF_BADGE:
            return c.STAFF_ELIGIBLE_FOR_SWAG_SHIRT
        else:
            return c.VOLUNTEER_RIBBON in self.ribbon_ints

    @property
    def volunteer_swag_shirt_earned(self):
        return self.volunteer_swag_shirt_eligible and (not self.takes_shifts or
                                                       self.worked_hours >= 6)

    @property
    def num_swag_shirts_owed(self):
        swag_shirts = int(self.paid_for_a_swag_shirt)
        volunteer_shirts = int(self.volunteer_swag_shirt_eligible)
        return swag_shirts + volunteer_shirts

    @property
    def gets_staff_shirt(self):
        return self.badge_type == c.STAFF_BADGE

    @property
    def gets_any_kind_of_shirt(self):
        return self.gets_staff_shirt or self.num_swag_shirts_owed > 0

    @property
    def has_personalized_badge(self):
        return self.badge_type in c.PREASSIGNED_BADGE_TYPES

    @property
    def donation_swag(self):
        donation_items = [
            desc for amount, desc in sorted(c.DONATION_TIERS.items())
            if amount and self.amount_extra >= amount
        ]

        extra_donations = \
            ['Extra donation of ${}'.format(self.extra_donation)] \
            if self.extra_donation else []

        return donation_items + extra_donations

    @property
    def merch(self):
        """
        Here is the business logic surrounding shirts:

            - People who kick in enough to get a shirt get a shirt.
            - People with staff badges get a configurable number of staff
              shirts.
            - Volunteers who meet the requirements get a complementary swag
              shirt (NOT a staff shirt).

        """
        merch = self.donation_swag

        if self.volunteer_swag_shirt_eligible:
            shirt = c.DONATION_TIERS[c.SHIRT_LEVEL]
            if self.paid_for_a_swag_shirt:
                shirt = 'a 2nd ' + shirt
            if not self.volunteer_swag_shirt_earned:
                shirt += (' (this volunteer must work at least 6 hours or '
                          'they will be reported for picking up their shirt)')
            merch.append(shirt)

        if self.gets_staff_shirt:
            staff_shirts = '{} Staff Shirt{}'.format(
                c.SHIRTS_PER_STAFFER, 's' if c.SHIRTS_PER_STAFFER > 1 else '')
            if self.shirt_size_marked:
                staff_shirts += ' [{}]'.format(c.SHIRTS[self.shirt])
            merch.append(staff_shirts)

        if self.staffing:
            merch.append('Staffer Info Packet')

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

        return comma_and(merch)

    @property
    def accoutrements(self):
        stuff = [] \
            if not self.ribbon \
            else ['a ' + s + ' ribbon' for s in self.ribbon_labels]

        if c.WRISTBANDS_ENABLED:
            stuff.append('a {} wristband'.format(
                c.WRISTBAND_COLORS[self.age_group]))
        if self.regdesk_info:
            stuff.append(self.regdesk_info)
        return (' with ' if stuff else '') + comma_and(stuff)

    @property
    def multiply_assigned(self):
        return len(self.dept_memberships) > 1

    @property
    def takes_shifts(self):
        return bool(self.staffing
                    and any(not d.is_shiftless for d in self.assigned_depts))

    @property
    def hours(self):
        all_hours = set()
        for shift in self.shifts:
            all_hours.update(shift.job.hours)
        return all_hours

    @property
    def hour_map(self):
        all_hours = {}
        for shift in self.shifts:
            for hour in shift.job.hours:
                all_hours[hour] = shift.job
        return all_hours

    @cached_property
    def available_jobs(self):
        if not self.dept_memberships:
            return []

        def _get_available_jobs(session, attendee_id):
            from uber.models.department import DeptMembership, Job, Shift
            return session.query(Job) \
                .outerjoin(Job.shifts) \
                .filter(
                    Job.department_id == DeptMembership.department_id,
                    DeptMembership.attendee_id == attendee_id) \
                .group_by(Job.id) \
                .having(func.count(Shift.id) < Job.slots) \
                .order_by(Job.start_time, Job.department_id).all()

        if self.session:
            jobs = _get_available_jobs(self.session, self.id)
        else:
            from uber.models import Session
            with Session() as session:
                jobs = _get_available_jobs(session, self.id)

        return [job for job in jobs if self.has_required_roles(job)]

    @cached_property
    def possible(self):
        assert self.session, ('{}.possible property may only be accessed for '
                              'objects attached to a session'.format(
                                  self.__class__.__name__))

        if not self.dept_memberships and not c.AT_THE_CON:
            return []
        else:
            from uber.models.department import DeptMembership, Job
            job_query = self.session.query(Job) \
                .filter(
                    Job.department_id == DeptMembership.department_id,
                    DeptMembership.attendee_id == self.id) \
                .options(
                    subqueryload(Job.shifts),
                    subqueryload(Job.required_roles)) \
                .order_by(Job.start_time, Job.department_id)

            return [
                job for job in job_query
                if job.slots > len(job.shifts) and job.no_overlap(self) and (
                    job.type != c.SETUP or self.can_work_setup) and (
                        job.type != c.TEARDOWN or self.can_work_teardown)
                and self.has_required_roles(job)
            ]

    @property
    def possible_opts(self):
        return [(job.id, '({}) [{}] {}'.format(hour_day_format(job.start_time),
                                               job.department_name, job.name))
                for job in self.possible if localized_now() < job.start_time]

    @property
    def possible_and_current(self):
        jobs = [s.job for s in self.shifts]
        for job in jobs:
            job.taken = True
        jobs.extend(self.possible)
        return sorted(jobs, key=lambda j: j.start_time)

    # ========================================================================
    # TODO: Refactor all this stuff regarding assigned_depts and
    #       requested_depts. Maybe a @suffix_property with a setter for the
    #       *_ids fields? The hardcoded *_labels props are also not great.
    #       There's a bigger feature here that I haven't wrapped my head
    #       around yet. A generic way to lazily set relations using ids.
    # ========================================================================

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

    @classproperty
    def extra_apply_attrs_restricted(cls):
        return set(['requested_depts_ids'])

    @property
    def assigned_depts_labels(self):
        return [d.name for d in self.assigned_depts]

    @property
    def requested_depts_labels(self):
        return [d.name for d in self.requested_depts]

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

    @assigned_depts_ids.setter
    def assigned_depts_ids(self, value):
        values = set(s for s in listify(value) if s)
        for membership in list(self.dept_memberships):
            if membership.department_id not in values:
                # Manually remove dept_memberships to ensure the associated
                # rows in the dept_membership_dept_role table are deleted.
                self.dept_memberships.remove(membership)
        from uber.models.department import Department
        self._set_relation_ids('assigned_depts', Department, list(values))

    @property
    def requested_depts_ids(self):
        any_dept = ['All'] if self.requested_any_dept else []
        _, ids = self._get_relation_ids('requested_depts')
        return any_dept + ([str(d.id) for d in self.requested_depts]
                           if ids is None else ids)

    @requested_depts_ids.setter
    def requested_depts_ids(self, value):
        values = set(s for s in listify(value) if s)
        self.requested_any_dept = 'All' in values
        if self.requested_any_dept:
            values.remove('All')
        from uber.models.department import Department
        self._set_relation_ids('requested_depts', Department, list(values))

    @property
    def worked_shifts(self):
        return [s for s in self.shifts if s.worked == c.SHIFT_WORKED]

    @property
    def weighted_hours(self):
        weighted_hours = sum(s.job.weighted_hours for s in self.shifts)
        return weighted_hours + self.nonshift_hours

    @department_id_adapter
    def weighted_hours_in(self, department_id):
        if not department_id:
            return self.weighted_hours
        return sum(shift.job.weighted_hours for shift in self.shifts
                   if shift.job.department_id == department_id)

    @property
    def worked_hours(self):
        weighted_hours = sum(s.job.real_duration * s.job.weight
                             for s in self.worked_shifts)
        return weighted_hours + self.nonshift_hours

    @department_id_adapter
    def dept_membership_for(self, department_id):
        if not department_id:
            return None
        for m in self.dept_memberships:
            if m.department_id == department_id:
                return m
        return None

    @department_id_adapter
    def requested(self, department_id):
        if self.requested_any_dept:
            return True
        if not department_id:
            return False
        return any(d.id == department_id for d in self.requested_depts)

    @department_id_adapter
    def assigned_to(self, department_id):
        if not department_id:
            return False
        return any(m.department_id == department_id
                   for m in self.dept_memberships)

    def trusted_in(self, department):
        return self.has_role_in(department)

    def can_admin_dept_for(self, department):
        return (self.admin_account
                and c.ACCOUNTS in self.admin_account.access_ints) \
                    or self.is_dept_head_of(department)

    @department_id_adapter
    def can_admin_checklist_for(self, department_id):
        if not department_id:
            return False
        return (self.admin_account
                and c.ACCOUNTS in self.admin_account.access_ints) \
            or any(
                m.department_id == department_id
                for m in self.dept_memberships_where_can_admin_checklist)

    def is_checklist_admin_of(self, department_id):
        if not department_id:
            return False
        return any(m.department_id == department_id and m.is_checklist_admin
                   for m in self.dept_memberships)

    def is_dept_head_of(self, department_id):
        if not department_id:
            return False
        return any(m.department_id == department_id and m.is_dept_head
                   for m in self.dept_memberships)

    def is_poc_of(self, department_id):
        if not department_id:
            return False
        return any(m.department_id == department_id and m.is_poc
                   for m in self.dept_memberships)

    def completed_every_checklist_for(self, slug):
        return all(
            d.checklist_item_for_slug(slug) for d in self.checklist_depts)

    @property
    def gets_any_checklist(self):
        return bool(self.dept_memberships_as_checklist_admin)

    def has_role(self, role):
        return any(r.id == role.id for r in self.dept_roles)

    @department_id_adapter
    def has_role_in(self, department_id):
        if not department_id:
            return False
        return any(m.department_id == department_id
                   for m in self.dept_memberships_with_role)

    def has_required_roles(self, job):
        if not job.required_roles:
            return True
        required_role_ids = set(r.id for r in job.required_roles)
        role_ids = set(r.id for r in self.dept_roles)
        return required_role_ids.issubset(role_ids)

    @property
    def has_role_somewhere(self):
        """
        Returns True if at least one of the following is true for at least
        one department:
            - is a department head
            - is a point of contact
            - is a checklist admin
            - has a dept role
        """
        return bool(self.dept_memberships_with_role)

    def has_shifts_in(self, department):
        return department in self.depts_where_working

    @property
    def food_restrictions_filled_out(self):
        return self.food_restrictions if c.STAFF_GET_FOOD else True

    @property
    def shift_prereqs_complete(self):
        return not self.placeholder and \
            self.food_restrictions_filled_out and self.shirt_size_marked

    @property
    def past_years_json(self):
        return json.loads(self.past_years or '[]')

    @property
    def must_contact(self):
        dept_chairs = []
        for dept in self.depts_where_working:
            poc_names = ' / '.join(sorted(poc.full_name for poc in dept.pocs))
            dept_chairs.append('({}) {}'.format(dept.name, poc_names))
        return safe_string('<br/>'.join(sorted(dept_chairs)))
コード例 #21
0
ファイル: group.py プロジェクト: floraline/ubersystem
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])
コード例 #22
0
ファイル: models.py プロジェクト: magfest/magwest
class GuestMerch:
    extra_merch_time = Column(MultiChoice(c.EXTRA_MERCH_TIME_OPTS))