class Username(db.Model): """Represents the username of a user. Multiple names can be tied to one user, but only one of them can be active and nested within the user class. Many to one relationship with `User`. """ __tablename__ = "usernames" # The unique username stored in lowercase. name = db.Column(db.String(80), primary_key=True) # A record of the name as the user typed it. case_name = db.Column(db.String(80)) # The linked user user_id = db.Column(db.Integer, db.ForeignKey("users.id")) user = db.relationship("User", back_populates="username_objects") # Account for case sensitivity in username uniqueness. @classmethod def create(cls, name, user): return cls(name=name.lower(), case_name=name, user=user) # Get username object from username. @classmethod def get(cls, name): return db.session.query(cls).get(name.lower())
class EmailConfirmationCode(db.Model): __tablename__ = "email_confirmation_codes" code = db.Column(db.String(256), primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey("users.id"), primary_key=True) user = db.relationship("User") used = db.Column(db.Boolean, default=False) # Record the email address the user had when making the confirmation request. email = db.Column(db.String(120)) # Use this to determine whether the code is expired. date_created = db.Column(db.DateTime, server_default=db.func.now()) def __init__(self, user_id, email): self.code = random_token() self.user_id = user_id self.email = email def url(self): return '/register/email/?id=%s&code=%s' % (self.user_id, self.code) def expired(self): return self.date_created < (datetime.datetime.now() - datetime.timedelta(days=1)) def invalid(self): return self.expired() or self.used or self.email != self.user.email
class BoardCategory(db.Model): """Forum Board Categories. Many to Many relationship with `Board`. """ __tablename__ = "forum_board_categories" id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(256)) canon_name = db.Column(db.String(256), unique=True) order = db.Column(db.Integer, default=1) boards = db.relationship("Board", back_populates="category", lazy='dynamic' ) def url(self): return '/forums/#' + self.canon_name def get_boards(self, user=None): query = self.boards if not (user and user.has_moderation_access()): query = query.filter(Board.moderators_only == False) return query.order_by(Board.order.asc()).all() @classmethod def get_categories(cls): return db.session.query(cls).order_by(cls.order.asc()).all() @classmethod def by_canon_name(cls, name): return db.session.query(cls).filter(cls.canon_name == name.lower()).one_or_none()
class Item(db.Model): __tablename__ = "items" id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(256), unique=True) canon_name = db.Column(db.String(256), unique=True) description = db.Column(db.String(1024)) buyback_price = db.Column(db.Integer, default=0) category_id = db.Column(db.Integer) def __init__(self, *args, **kwargs): super(Item, self).__init__(*args, **kwargs) if not self.canon_name: self.canon_name = canonize(self.name) def url(self): return '/item/' + self.canon_name def image_url(self): if is_devserver(): category_name = self.category_name() subpath = ("img" + os.sep + "items" + os.sep + category_name + os.sep + self.canon_name + ".png") image_path = (os.path.join(go_up_path(4, (__file__)), "static", subpath)) if os.path.isfile(image_path): return url_for("static", filename=subpath) + "?v=" + str( get_static_version_id()) return (app.config['IMAGE_BUCKET_ROOT'] + "/items/" + self.canon_name + ".png?v=" + str(get_static_version_id())) def category(self): return ItemCategory(self.category_id) def category_name(self): return self.category().name() def give_items(self, user_id, count): return InventoryItem.give_items(user_id, self.id, count) def is_bondable(self): return self.category().name() == "minis" def is_wearable(self): return self.category().name() == "clothes" @classmethod def make_canon_name(cls, name): return canonize(name) @classmethod def by_canon_name(cls, name): return db.session.query(cls).filter( cls.canon_name == name.lower()).one_or_none()
class Notification(db.Model): __tablename__ = "notifications" id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id')) user = db.relationship("User") time = db.Column(db.DateTime, server_default=db.func.now()) text = db.Column(db.Text()) unread = db.Column(db.Boolean, default=True) link = db.Column(db.String(512)) count = db.Column(db.Integer, default=1) @classmethod def send(cls, user_id, text, link): notification = (db.session.query(cls).filter( cls.user_id == user_id).filter(cls.text == text).filter( cls.link == link).filter(cls.unread == True).one_or_none()) if not notification: notification = cls(user_id=user_id, text=text, link=link) else: notification.count += 1 db.session.add(notification) db.session.commit()
class Board(db.Model): """Forum Boards. Container for threads. Many to Many relationship with `BoardCategory`. """ __tablename__ = 'forum_boards' id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(256)) canon_name = db.Column(db.String(256), unique=True) description = db.Column(db.Text()) moderators_only = db.Column(db.Boolean(), default=False) order = db.Column(db.Integer) category_id = db.Column(db.Integer, db.ForeignKey('forum_board_categories.id')) category = db.relationship("BoardCategory", back_populates="boards" ) threads = db.relationship("ForumThread", back_populates="board") def url(self): return "/forums/board/" + self.canon_name + "/" def is_news(self): return self.canon_name == app.config.get('NEWS_BOARD_CANON_NAME') def can_post(self, user): result = user and user.has_communication_access() if self.is_news(): result = result and user.has_admin_access() return result def latest_post(self): return ( db.session.query(ForumPost) .join(ForumThread, ForumPost.thread) .filter(ForumThread.board_id == self.id) .order_by(ForumPost.id.desc()) .first() ) @classmethod def by_canon_name(cls, name): return db.session.query(cls).filter(cls.canon_name == name.lower()).one_or_none()
class SpeciesCoat(db.Model): __tablename__ = "species_coats" id = db.Column(db.Integer, primary_key=True) canon_name = db.Column(db.String(256), unique=True) coat_name = db.Column(db.String(80)) coat_canon_name = db.Column(db.String(80)) species_id = db.Column(db.Integer, db.ForeignKey("species.id")) species = db.relationship("Species") description = db.Column(db.Text) date_discovered = db.Column(db.DateTime, server_default=db.func.now()) def __init__(self, *args, **kwargs): super(SpeciesCoat, self).__init__(*args, **kwargs) if not self.canon_name: self.canon_name = self.species.canon_name + '_' + canonize(self.coat_name) if not self.coat_canon_name: self.coat_canon_name = canonize(self.coat_name) @property def name(self): return self.coat_name + ' ' + self.species.name def url(self): return '/coat/' + self.coat_canon_name + '/' + self.species.canon_name + '/' def image_url(self): if is_devserver(): subpath = ("img" + os.sep + "pets" + os.sep + self.species.canon_name + os.sep + self.coat_name + ".png") image_path = (os.path.join(go_up_path(4, (__file__)), "static", subpath)) if os.path.isfile(image_path): return url_for("static", filename=subpath) + "?v=" + str(get_static_version_id()) return (app.config['IMAGE_BUCKET_ROOT'] + "/pets/" + self.species.canon_name + "/" + self.coat_name + ".png?v=" + str(get_static_version_id())) @classmethod def by_canon_name(cls, name): return db.session.query(cls).filter(cls.canon_name == name.lower()).one_or_none()
class MiniFriendship(db.Model): """ A relationship between a pet and their mini. Note that although a pet can only have one mini equipped at a time, it keeps a memory of its past minis. """ __tablename__ = "mini_friendships" pet_id = db.Column(db.Integer, db.ForeignKey("pets.id"), primary_key=True) mini_id = db.Column(db.Integer, db.ForeignKey("items.id"), primary_key=True) pet = db.relationship("Pet") mini = db.relationship("Item") nickname = db.Column(db.String(80)) description = db.Column(db.String(512)) @property def name(self): if self.nickname: return self.nickname return self.mini.name
class Title(db.Model): """Defines titles that users can hold. Examples of titles: admin, programmer, artist, contributor Many to one with Users. """ __tablename__ = "titles" id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(256), unique=True) canon_name = db.Column(db.String(256), unique=True) users = db.relationship("User", secondary='r_user_titles', back_populates="titles", lazy='dynamic' ) def css_class(self): return 'title-' + self.canon_name
class Species(db.Model): __tablename__ = "species" id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(80), unique=True) canon_name = db.Column(db.String(80), unique=True) description = db.Column(db.Text) date_discovered = db.Column(db.DateTime, server_default=db.func.now()) def __init__(self, *args, **kwargs): super(Species, self).__init__(*args, **kwargs) if not self.canon_name: self.canon_name = canonize(self.name) @property def default_coat(self): coat = db.session.query(SpeciesCoat).filter(SpeciesCoat.coat_canon_name == 'common', SpeciesCoat.species_id == self.id).one_or_none() if not coat: coat = db.session.query(SpeciesCoat).filter( SpeciesCoat.species_id == self.id).limit(1).one_or_none() return coat def url(self): return '/species/' + self.canon_name + '/' def image_url(self): return self.default_coat.image_url() @classmethod def by_canon_name(cls, name): return db.session.query(cls).filter(cls.canon_name == name.lower()).one_or_none()
class LoginSession(db.Model): __tablename__ = "sessions" id = db.Column(db.String(256), primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey("users.id"), primary_key=True) user = db.relationship("User") expires = db.Column(db.DateTime) def __init__(self, user_id, expires): self.id = random_token(128) self.user_id = user_id self.expires = expires @property def active(self): return (self.expires > datetime.datetime.now())
class InviteCode(db.Model): __tablename__ = "invite_codes" code = db.Column(db.String(256), primary_key=True) # An invite code is considered claimed if a recipient user exists. recipient_id = db.Column(db.Integer, db.ForeignKey("users.id")) recipient = db.relationship("User", foreign_keys=[recipient_id]) # The person who originally created the invite code. sender_id = db.Column(db.Integer, db.ForeignKey("users.id")) sender = db.relationship("User", foreign_keys=[sender_id]) disabled = db.Column(db.Integer, default=False) date_created = db.Column(db.DateTime, server_default=db.func.now()) def __init__(self, sender_id): self.code = random_token() self.sender_id = sender_id def url(self): return '/register/?invite_code=' + self.code
class PasswordResetCode(db.Model): __tablename__ = "password_reset_codes" code = db.Column(db.String(256), primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey("users.id"), primary_key=True) user = db.relationship("User") used = db.Column(db.Boolean, default=False) # Use this to determine whether the code is expired. date_created = db.Column(db.DateTime, server_default=db.func.now()) def __init__(self, user_id): self.code = random_token() self.user_id = user_id def expired(self): return self.date_created < (datetime.datetime.now() - datetime.timedelta(hours=1)) def invalid(self): return self.expired() or self.used def url(self): return '/login/reset/%s/%s/' % (self.user_id, self.code)
class Pet(db.Model): __tablename__ = "pets" id = db.Column(db.Integer, primary_key=True) soul_name = db.Column(db.String(80), unique=True) guardian_id = db.Column(db.Integer, db.ForeignKey("users.id", name="guardian_id")) guardian = db.relationship("User", foreign_keys=[guardian_id], back_populates="pets") favorites = db.relationship("Item", secondary="pet_favorites") # Only set if the pet is a variation coat_id = db.Column(db.Integer, db.ForeignKey("species_coats.id")) coat = db.relationship("SpeciesCoat") # The pet's current mini. mini_id = db.Column(db.Integer, db.ForeignKey("items.id")) mini = db.relationship("Item", foreign_keys=[mini_id]) # Which way is the pet's image facing facing_right = db.Column(db.Boolean, default=True) # Personal profile information for the pet name = db.Column(db.String(80), default="") description = db.Column(db.Text, default="") pronouns = db.Column(db.String(80), default="They/them") date_created = db.Column(db.DateTime, server_default=db.func.now()) # If either of these is set to a number other than 0, the pet is for sale ss_price = db.Column(db.Integer, default=0) cc_price = db.Column(db.Integer, default=0) def __init__(self, *args, **kwargs): super(Pet, self).__init__(*args, **kwargs) # Populate pet favorites. if not self.favorites: self.favorites = db.session.query(Item).order_by(db.func.rand()).limit(4).all() if not self.soul_name: self.soul_name = Pet.new_soul_name() if not self.name: self.name = self.soul_name.capitalize() if self.guardian_id: new_friendship = PetFriendship(pet_id=self.id, guardian_id=self.guardian_id) db.session.add(new_friendship) db.session.commit() def transfer_guardian(self, guardian_id): self.guardian_id = guardian_id new_friendship = PetFriendship(pet_id=self.id, guardian_id=guardian_id) db.session.add(new_friendship) db.session.commit() @hybrid_property def bonding_day(self): if not self.friendship: return None return self.friendship.bonding_day @bonding_day.setter def set_bonding_day(self, date): if not self.friendship: return None self.friendship.bonding_day = date return self.friendship @property def friendship(self): return db.session.query(PetFriendship).get((self.id, self.guardian_id)) @property def mini_friendship(self): return db.session.query(MiniFriendship).get((self.id, self.mini_id)) @property def species(self): return self.coat.species def image_url(self): return self.coat.image_url() def url(self): return '/pet/' + self.soul_name + '/' def to_dict(self): data = { 'name': self.name, 'id': self.id, 'image_url': self.image_url(), 'url': self.url(), } return data # Generate a new unique soul name @classmethod def new_soul_name(cls): min_length = 5 new_name = soul_name(min_length) found = db.session.query(cls).filter(cls.soul_name == new_name).one_or_none() while found: min_length += 1 new_name = soul_name(min_length) found = db.session.query(cls).filter(cls.soul_name == new_name).one_or_none() return new_name
class User(db.Model): """The greatest User model of all time. """ __tablename__ = "users" id = db.Column(db.Integer, primary_key=True) active_username = db.Column(db.String(80), unique=True) last_username_change = db.Column(db.DateTime, server_default=db.func.now()) username_objects = db.relationship("Username", back_populates="user") last_action = db.Column(db.DateTime, server_default=db.func.now()) date_joined = db.Column(db.DateTime, server_default=db.func.now()) # Email, Password email = db.Column(db.String(120), unique=True) email_confirmed = db.Column(db.Boolean, default=False) password_hash = db.Column(db.String(200)) # Permissions. can_moderate = db.Column(db.Boolean, default=False) can_admin = db.Column(db.Boolean, default=False) title_id = db.Column(db.Integer, db.ForeignKey("titles.id")) title = db.relationship("Title", foreign_keys=[title_id]) titles = db.relationship("Title", secondary="r_user_titles", back_populates="users") post_count = db.Column(db.Integer, default=0) # Active Companion companion_id = db.Column(db.Integer, db.ForeignKey("pets.id", name="companion_id")) companion = db.relationship("Pet", foreign_keys=[companion_id]) # Human Avatar ha_url = db.Column(db.String(100), default="/api/ha/m/") pets = db.relationship("Pet", primaryjoin='Pet.guardian_id == User.id', back_populates="guardian", lazy='dynamic', passive_deletes=True) # Ban Status ban_id = db.Column(db.Integer, db.ForeignKey("ban_logs.id", name="ban_id")) ban = db.relationship("BanLog", foreign_keys=[ban_id]) # Currency star_shards = db.Column(db.Integer, default=0) cloud_coins = db.Column(db.Integer, default=0) bank_account = db.relationship("BankAccount", uselist=False, back_populates="user") # Misc profile stuff gender = db.Column(db.String(100), default="") pronouns = db.Column(db.String(200), default="") bio = db.Column(db.String(1000), default="Hello, world") status = db.Column(db.String(15), default="") # Settings side_id = db.Column(db.Integer, default=0) theme_id = db.Column(db.Integer, default=0) notified_on_pings = db.Column(db.Boolean, default=True) autosubscribe_threads = db.Column(db.Boolean, default=True) autosubscribe_posts = db.Column(db.Boolean, default=False) # Used to track linear progress. ie: 0-5 are levels of the tutorial. story_level = db.Column(db.Integer, default=0) def __repr__(self): return '<User %r>' % self.name @hybrid_property def name(self): return self.active_username @name.setter def set_name(self, name): self.active_username = name @property def usernames(self): return [u.name for u in self.username_objects] def avatar_url(self): # Until we implement human avatars for real... return '/static/img/avatar/base.png' def url(self): return "/user/" + self.name.lower() + "/" def title_class(self): if self.is_banned(): return 'title-banned' elif self.is_muted(): return 'title-muted' elif self.title: return self.title.css_class() return 'title-user' def story_route(self): if self.story_level == 0: return 'general.intro_side' elif self.story_level == 1: return 'general.intro_companion' elif self.story_level == 2: return 'general.intro_avatar' return None def side_name(self): if self.side_id == 0: return 'Sayleus' elif self.side_id == 1: return 'Luaria' return None def is_banned(self): ban = self.ban return ban and ban.ban_type == BanTypes.BAN and ban.active() def is_muted(self): ban = self.ban return ban and ban.ban_type == BanTypes.MUTE and ban.active() def has_communication_access(self): return self.email_confirmed and not self.is_muted( ) and not self.is_banned() def has_moderation_access(self): return self.can_moderate def has_admin_access(self): return self.can_admin def make_email_confirmation_code(self): code = EmailConfirmationCode(self.id, self.email) db.session.merge(code) db.session.commit() return code def make_password_reset_code(self): code = PasswordResetCode(self.id) db.session.merge(code) db.session.commit() return code @validates('email') def validate_email(self, key, address): return address.lower() @validates('cloud_coins') def validate_cloud_coins(self, key, cc): if cc < 0: raise InvalidCurrencyException("Currency cannot be negative!") return cc @validates('star_shards') def validate_star_shards(self, key, ss): if ss < 0: raise InvalidCurrencyException("Currency cannot be negative!") return ss @classmethod def by_username(cls, username): return (db.session.query(cls).join( Username, cls.id == Username.user_id).filter( Username.name == username.lower()).one_or_none()) @classmethod def from_email(cls, email): return db.session.query(cls).filter( cls.email == email.lower()).one_or_none() @classmethod def hash_password(cls, password, salt=None): if not salt: salt = bcrypt.gensalt() return bcrypt.hashpw(password, salt) @classmethod def check_password(cls, user, password): return cls.hash_password(password, user.password_hash) == user.password_hash @classmethod def transfer_currency(cls, from_id, to_id, cc=0, ss=0): from_user = db.session.query(User).get(from_id) to_user = db.session.query(User).get(to_id) if cc > from_user.cloud_coins or ss > from_user.star_shards: raise InvalidCurrencyException('Insufficient funds!') from_user.star_shards -= ss from_user.cloud_coins -= cc to_user.star_shards += ss to_user.cloud_coins += cc def __init__(self, username, *args, **kwargs): self.active_username = username Username.create(username, self) self.bank_account = BankAccount() super(User, self).__init__(*args, **kwargs)
class ConversationHandle(db.Model): __tablename__ = "conversation_handles" conversation_id = db.Column(db.Integer, db.ForeignKey('conversations.id'), primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), primary_key=True) user = db.relationship("User") title = db.Column(db.String(256)) unread = db.Column(db.Boolean, default=False) hidden = db.Column(db.Boolean, default=False) last_updated = db.Column(db.DateTime, server_default=db.func.now()) def url(self): if self.unread: return '/conversation_read/' + str(self.conversation_id) + '/' return '/conversation/' + str(self.conversation_id) + '/' def status(self): if self.hidden: return 'deleted' if self.unread: return 'unread' return 'read' def recipients(self): return db.session.query(ConversationHandle).filter( ConversationHandle.conversation_id == self.conversation_id).all() @classmethod def read_conversations(cls, keys, user_id): if isinstance(keys, (int, long)): # noqa keys = [keys] for key in keys: try: found_conversation = db.session.query(cls).get((key, user_id)) found_conversation.unread = False db.session.add(found_conversation) except (flask_sqlalchemy.orm.exc.NoResultFound): flash('Message read failed for an unexpected reason.', 'error') return False db.session.commit() return True # This marks a user conversation as hidden, it will be unhidden if a new reply is made. @classmethod def hide_conversations(cls, keys, user_id): if isinstance(keys, (int, long)): # noqa keys = [keys] for key in keys: try: found_conversation = db.session.query(cls).get((key, user_id)) found_conversation.hidden = True db.session.add(found_conversation) except (flask_sqlalchemy.orm.exc.NoResultFound): flash('Message hide failed for an unexpected reason.', 'error') return False db.session.commit() return True
class ForumThread(db.Model): """Forum Threads. Many to One relationship with `Board`. """ __tablename__ = "forum_threads" id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(256)) author_id = db.Column(db.Integer, db.ForeignKey('users.id')) author = db.relationship("User") date_created = db.Column(db.DateTime, server_default=db.func.now()) date_modified = db.Column(db.DateTime, onupdate=db.func.now()) is_pinned = db.Column(db.Boolean(), default=False) is_locked = db.Column(db.Boolean(), default=False) board_id = db.Column(db.Integer, db.ForeignKey('forum_boards.id')) board = db.relationship("Board", back_populates="threads") posts = db.relationship("ForumPost", back_populates="thread", lazy='dynamic') subscribers = db.relationship("User", secondary='forum_thread_subscriptions', lazy='dynamic') def notify_subscribers(self, post): for user in self.subscribers: if user.id == post.author_id: continue Notification.send(user.id, 'Someone has made a new post in the thread: ' + self.title, post.url()) def url(self): return "/forums/thread/" + str(self.id) + "-" + canonize(truncate(self.title, 40)) + "/" def subscription(self, user): return user and db.session.query(ForumSubscription).get((self.id, user.id)) def can_post(self, user): result = user and user.has_communication_access() if self.is_locked: result = result and user.has_moderation_access() return result def can_edit(self, user): return user and user.has_communication_access() and (user.id == self.author.id or user.has_moderation_access()) def first_post(self): return self.posts.order_by(ForumPost.id.asc()).first() def reply_count(self): return ( db.session.query(ForumPost) .filter(ForumPost.thread_id == self.id) .count() - 1 ) def latest_post(self): return ( db.session.query(ForumPost) .filter(ForumPost.thread_id == self.id) .order_by(ForumPost.id.desc()) .first() )