class Tier(db.Model): """This model defines tier of support that commercial users can sign up to.""" __tablename__ = 'tier' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.Unicode, nullable=False) short_desc = db.Column(db.UnicodeText) long_desc = db.Column(db.UnicodeText) price = db.Column(db.Numeric(11, 2), nullable=False) # per month # Users can sign up only to available tiers on their own. If tier is not # available, it should be hidden from the website. available = db.Column(db.Boolean, nullable=False, default=False) # Primary tiers are shown first on the signup page. Secondary plans (along # with repeating primary plans) are listed on the "view all tiers" page # that lists all available tiers. primary = db.Column(db.Boolean, nullable=False, default=False) users = db.relationship("User", backref='tier', lazy="dynamic") def __unicode__(self): return "%s (#%s)" % (self.name, self.id) def __str__(self): return unicode(self).encode('utf-8') @classmethod def create(cls, **kwargs): new_tier = cls( name=kwargs.pop('name'), short_desc=kwargs.pop('short_desc', None), long_desc=kwargs.pop('long_desc', None), price=kwargs.pop('price'), available=kwargs.pop('available', False), primary=kwargs.pop('primary', False), ) db.session.add(new_tier) db.session.commit() return new_tier @classmethod def get(cls, **kwargs): return cls.query.filter_by(**kwargs).first() @classmethod def get_available(cls, sort=False, sort_desc=False): """Returns list of tiers that are available for sign up. You can also sort returned list by price of the tier. """ query = cls.query.filter(cls.available == True) if sort: query = query.order_by(cls.price.desc()) if sort_desc else \ query.order_by(cls.price.asc()) return query.all() def get_featured_users(self, **kwargs): return User.get_featured(tier_id=self.id, **kwargs)
class User(db.Model, UserMixin): """User model is used for users of MetaBrainz services like Live Data Feed. Users are either commercial or non-commercial (see `is_commercial`). Their access to the API is determined by their `state` (active, pending, waiting, or rejected). All non-commercial users have active state by default, but commercial users need to be approved by one of the admins first. """ __tablename__ = 'user' # Common columns used by both commercial and non-commercial users: id = db.Column(db.Integer, primary_key=True) is_commercial = db.Column(db.Boolean, nullable=False) musicbrainz_id = db.Column( db.Unicode, unique=True) # MusicBrainz account that manages this user created = db.Column(db.DateTime(timezone=True), default=datetime.utcnow) state = db.Column(postgres.ENUM(STATE_ACTIVE, STATE_PENDING, STATE_WAITING, STATE_REJECTED, STATE_LIMITED, name='state_types'), nullable=False) contact_name = db.Column(db.Unicode, nullable=False) contact_email = db.Column(db.Unicode, nullable=False) data_usage_desc = db.Column(db.UnicodeText) # Columns specific to commercial users: org_name = db.Column(db.Unicode) org_logo_url = db.Column(db.Unicode) website_url = db.Column(db.Unicode) api_url = db.Column(db.Unicode) org_desc = db.Column(db.UnicodeText) address_street = db.Column(db.Unicode) address_city = db.Column(db.Unicode) address_state = db.Column(db.Unicode) address_postcode = db.Column(db.Unicode) address_country = db.Column(db.Unicode) tier_id = db.Column( db.Integer, db.ForeignKey('tier.id', ondelete="SET NULL", onupdate="CASCADE")) amount_pledged = db.Column(db.Numeric(11, 2)) # Administrative columns: good_standing = db.Column(db.Boolean, nullable=False, default=True) in_deadbeat_club = db.Column(db.Boolean, nullable=False, default=False) featured = db.Column(db.Boolean, nullable=False, default=False) tokens = db.relationship("Token", backref="owner", lazy="dynamic") token_log_records = db.relationship("TokenLog", backref="user", lazy="dynamic") def __unicode__(self): if self.is_commercial: return "%s (#%s)" % (self.org_name, self.id) else: if self.musicbrainz_id: return "#%s (MBID: %s)" % (self.id, self.musicbrainz_id) else: return str(self.id) @property def token(self): return Token.get(owner_id=self.id, is_active=True) @classmethod def add(cls, **kwargs): new_user = cls( is_commercial=kwargs.pop('is_commercial'), musicbrainz_id=kwargs.pop('musicbrainz_id'), contact_name=kwargs.pop('contact_name'), contact_email=kwargs.pop('contact_email'), data_usage_desc=kwargs.pop('data_usage_desc'), org_desc=kwargs.pop('org_desc', None), org_name=kwargs.pop('org_name', None), org_logo_url=kwargs.pop('org_logo_url', None), website_url=kwargs.pop('website_url', None), api_url=kwargs.pop('api_url', None), address_street=kwargs.pop('address_street', None), address_city=kwargs.pop('address_city', None), address_state=kwargs.pop('address_state', None), address_postcode=kwargs.pop('address_postcode', None), address_country=kwargs.pop('address_country', None), tier_id=kwargs.pop('tier_id', None), amount_pledged=kwargs.pop('amount_pledged', None), ) new_user.state = STATE_ACTIVE if not new_user.is_commercial else STATE_PENDING if kwargs: raise TypeError('Unexpected **kwargs: %r' % kwargs) db.session.add(new_user) db.session.commit() if new_user.is_commercial: send_user_signup_notification(new_user) return new_user @classmethod def get(cls, **kwargs): return cls.query.filter_by(**kwargs).first() @classmethod def get_all(cls, **kwargs): return cls.query.filter_by(**kwargs).all() @classmethod def get_all_commercial(cls, limit=None, offset=None): query = cls.query.filter(cls.is_commercial == True).order_by( cls.org_name) count = query.count() # Total count should be calculated before limits if limit is not None: query = query.limit(limit) if offset is not None: query = query.offset(offset) return query.all(), count @classmethod def get_featured(cls, limit=None, **kwargs): """Get list of featured users which is randomly sorted. Args: limit: Max number of users to return. in_deadbeat_club: Returns only users from deadbeat club if set to True. with_logos: True if need only users with logo URLs specified, False if only users without logo URLs, None if it's irrelevant. tier_id: Returns only users from tier with a specified ID. Returns: List of users according to filters described above. """ query = cls.query.filter(cls.featured == True) query = query.filter( cls.in_deadbeat_club == kwargs.pop('in_deadbeat_club', False)) with_logos = kwargs.pop('with_logos', None) if with_logos: query = query.filter(cls.org_logo_url != None) tier_id = kwargs.pop('tier_id', None) if tier_id: query = query.filter(cls.tier_id == tier_id) if kwargs: raise TypeError('Unexpected **kwargs: %r' % kwargs) return query.order_by(func.random()).limit(limit).all() @classmethod def search(cls, value): """Search users by their musicbrainz_id, org_name, contact_name, or contact_email. """ query = cls.query.filter( or_( cls.musicbrainz_id.ilike('%' + value + '%'), cls.org_name.ilike('%' + value + '%'), cls.contact_name.ilike('%' + value + '%'), cls.contact_email.ilike('%' + value + '%'), )) return query.limit(20).all() def generate_token(self): """Generates new access token for this user.""" if self.state == STATE_ACTIVE: return Token.generate_token(self.id) else: raise InactiveUserException( "Can't generate token for inactive user.") def update(self, **kwargs): contact_name = kwargs.pop('contact_name') if contact_name is not None: self.contact_name = contact_name contact_email = kwargs.pop('contact_email') if contact_email is not None: self.contact_email = contact_email if kwargs: raise TypeError('Unexpected **kwargs: %r' % kwargs) db.session.commit() def set_state(self, state): old_state = self.state self.state = state db.session.commit() if old_state != self.state: # TODO: Send additional info about new state. state_name = "ACTIVE" if self.state == STATE_ACTIVE else \ "REJECTED" if self.state == STATE_REJECTED else \ "PENDING" if self.state == STATE_PENDING else \ "WAITING" if self.state == STATE_WAITING else \ "LIMITED" if self.state == STATE_LIMITED else \ self.state send_mail( subject="[MetaBrainz] Your account has been updated", text= 'State of your MetaBrainz account has been changed to "%s".' % state_name, recipients=[self.contact_email], )
class Token(db.Model): __tablename__ = 'token' value = db.Column(db.String, primary_key=True) is_active = db.Column(db.Boolean, nullable=False, default=True) owner_id = db.Column( db.Integer, db.ForeignKey('user.id', ondelete="SET NULL", onupdate="CASCADE")) created = db.Column(db.DateTime(timezone=True), default=datetime.utcnow) log_records = db.relationship(TokenLog, backref="token", lazy="dynamic") @classmethod def get(cls, **kwargs): return cls.query.filter_by(**kwargs).first() @classmethod def get_all(cls, **kwargs): return cls.query.filter_by(**kwargs).all() @classmethod def search_by_value(cls, value): return cls.query.filter(cls.value.like('%' + value + '%')).all() @classmethod def generate_token(cls, owner_id): """Generates new token for a specified user and revokes all other tokens owned by this user. Returns: Value of the new token. """ if owner_id is not None: last_hour_q = cls.query.filter( cls.owner_id == owner_id, cls.created > datetime.utcnow() - timedelta(hours=1), ) if last_hour_q.count() > 0: raise TokenGenerationLimitException( "Can't generate more than one token per hour.") cls.revoke_tokens(owner_id) new_token = cls( value=generate_string(TOKEN_LENGTH), owner_id=owner_id, ) db.session.add(new_token) db.session.commit() TokenLog.create_record(new_token.value, token_log.ACTION_CREATE) return new_token.value @classmethod def revoke_tokens(cls, owner_id): """Revokes all tokens owned by a specified user. Args: owner_id: ID of a user. """ tokens = db.session.query(cls).filter(cls.owner_id == owner_id, cls.is_active == True) for token in tokens: token.revoke() @classmethod def is_valid(cls, token_value): """Checks if token exists and is active.""" token = cls.get(value=token_value) return token and token.is_active def revoke(self): self.is_active = False db.session.commit() TokenLog.create_record(self.value, token_log.ACTION_DEACTIVATE)