class ForumPost(db.Model): """Forum Thread Posts. Many to One relationship with `ForumThread`. """ __tablename__ = "forum_thread_posts" id = db.Column(db.Integer, primary_key=True) author_id = db.Column(db.Integer, db.ForeignKey('users.id')) author = db.relationship("User") body = db.Column(db.Text()) date_created = db.Column(db.DateTime, server_default=db.func.now()) date_modified = db.Column(db.DateTime, onupdate=db.func.now()) thread_id = db.Column(db.Integer, db.ForeignKey('forum_threads.id')) thread = db.relationship("ForumThread", back_populates="posts") def url(self): index = ( db.session.query(ForumPost) .filter(ForumPost.thread_id == self.thread_id) .filter(ForumPost.id < self.id) .count() ) page = (index // app.config.get('POSTS_PER_PAGE')) + 1 return str(self.thread.url()) + str(page) + "/" + self.anchor() def anchor(self): return "#post-" + str(self.id) def can_edit(self, user): return user and user.has_communication_access() and (user.id == self.author.id or user.has_moderation_access())
class ForumSubscription(db.Model): __tablename__ = "forum_thread_subscriptions" thread_id = db.Column(db.Integer, db.ForeignKey('forum_threads.id'), primary_key=True) thread = db.relationship("ForumThread") user_id = db.Column(db.Integer, db.ForeignKey('users.id'), primary_key=True) user = db.relationship("User")
class PetFavorite(db.Model): __tablename__ = "pet_favorites" pet_id = db.Column(db.Integer, db.ForeignKey("pets.id"), primary_key=True) item_id = db.Column(db.Integer, db.ForeignKey("items.id"), primary_key=True) pet = db.relationship("Pet") item = db.relationship("Item") discovered = db.Column(db.Boolean, default=False)
class Message(db.Model): __tablename__ = "messages" id = db.Column(db.Integer, primary_key=True) conversation_id = db.Column(db.Integer, db.ForeignKey('conversations.id')) author_id = db.Column(db.Integer, db.ForeignKey('users.id')) author = db.relationship("User") text = db.Column(db.Text()) date_created = db.Column(db.DateTime, server_default=db.func.now())
class BankTransfer(db.Model): __tablename__ = "bank_transfers" id = db.Column(db.Integer, autoincrement=True, primary_key=True) sender_id = db.Column(db.Integer, db.ForeignKey("users.id")) recipient_id = db.Column(db.Integer, db.ForeignKey("users.id")) star_shards = db.Column(db.Integer, default=0) cloud_coins = db.Column(db.Integer, default=0) date_transferred = db.Column(db.DateTime, server_default=db.func.now())
class Highscore(db.Model): __tablename__ = "highscores" id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id')) user = db.relationship("User") game_log_id = db.Column(db.Integer, db.ForeignKey('gamelogs.id')) game_id = db.Column(db.Integer, nullable=False) score = db.Column(db.Integer) # Highscores are monthly. For an all-time highscore, year and month are set # to zero. year = db.Column(db.Integer) month = db.Column(db.Integer)
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 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 PetFriendship(db.Model): """ A table that stores metadata on the relationship between users and their pets. Note that even if a pet is transferred, it maintains "memory" of their old user. """ __tablename__ = "pet_friendships" pet_id = db.Column(db.Integer, db.ForeignKey("pets.id"), primary_key=True) guardian_id = db.Column(db.Integer, db.ForeignKey("users.id"), primary_key=True) pet = db.relationship("Pet") guardian = db.relationship("User") bonding_day = db.Column(db.DateTime, server_default=db.func.now()) happiness = db.Column(db.Integer, default=0)
class InventoryItem(db.Model): __tablename__ = "inventory_items" # These two keys define the inventory item entry. user_id = db.Column(db.Integer, db.ForeignKey('users.id'), primary_key=True) item_id = db.Column(db.Integer, db.ForeignKey('items.id'), primary_key=True) user = db.relationship("User") item = db.relationship("Item") count = db.Column(db.Integer, default=0) def to_dict(self): data = { 'id': self.item.id, 'name': self.item.name, 'category': self.item.category_name(), 'description': self.item.description, 'count': self.count, 'image_url': self.item.image_url(), 'buyback_price': self.item.buyback_price, 'is_bondable': self.item.is_bondable(), } return data def __repr__(self): return json.dumps(self.to_dict()) @classmethod def give_items(self, user_id, item_id, count): inventory_entry = db.session.query(InventoryItem).get( (user_id, item_id)) if not inventory_entry: inventory_entry = InventoryItem(user_id=user_id, item_id=item_id, count=count) else: inventory_entry.count += count return inventory_entry
class ShopItem(db.Model): """ A model representing a relationship between a shop and its stock. """ __tablename__ = "shop_items" shop_id = db.Column(db.Integer, primary_key=True) item_id = db.Column(db.Integer, db.ForeignKey("items.id"), primary_key=True) item = db.relationship("Item")
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 BankAccount(db.Model): """Permanently assigned bank account for the `User` class. One to one relationship with `User`. """ __tablename__ = "bank_accounts" id = db.Column(db.Integer, db.ForeignKey("users.id"), primary_key=True) user = db.relationship("User", back_populates="bank_account") star_shards = db.Column(db.Integer, default=0) cloud_coins = db.Column(db.Integer, default=0)
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 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 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 GameLog(db.Model): __tablename__ = "gamelogs" id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id')) user = db.relationship("User") game_id = db.Column(db.Integer, nullable=False) score = db.Column(db.Integer) time = db.Column(db.DateTime, server_default=db.func.now()) # Note: Game logs are differently formatted per type of game. game_log = db.Column(db.Text) @classmethod def record_score(cls, user_id, game_id, score): new_record = cls(user_id=user_id, game_id=game_id, score=score) db.session.add(new_record) db.session.commit() @classmethod def get_user_highscore(cls, user_id, game_id): return ( db.session.query(cls) .filter(cls.user_id == user_id) .filter(cls.game_id == game_id) .order_by(cls.score.desc()) .limit(1) .all() ) @classmethod def get_highscores(cls, game_id, count=10): return ( db.session.query(cls) .filter(cls.game_id == game_id) .order_by(cls.score.desc()) .limit(count) .all() )
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 BanLog(db.Model): """A history of times users have been banned or muted. """ __tablename__ = "ban_logs" id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey("users.id", name="banned_user")) user = db.relationship("User", foreign_keys=[user_id]) # 0 is a default ban. ban_type = db.Column(db.Integer) is_permanent = db.Column(db.Boolean, default=False) banned_until = db.Column(db.DateTime) reason = db.Column(db.Text) date_banned = db.Column(db.DateTime, server_default=db.func.now()) date_modified = db.Column(db.DateTime, onupdate=db.func.now()) # Only set if a user is manually unbanned. date_unbanned = db.Column(db.DateTime) def active(self): return self.is_permanent or ( self.banned_until and self.banned_until > datetime.datetime.now()) def verb(self): if self.ban_type == BanTypes.MUTE: return 'mute' return 'ban' def past_tense(self): if self.ban_type == BanTypes.MUTE: return 'muted' return 'banned'
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 RUserTitles(db.Model): __tablename__ = 'r_user_titles' user_id = db.Column('user_id', db.Integer, db.ForeignKey('users.id'), primary_key=True) title_id = db.Column('title_id', db.Integer, db.ForeignKey('titles.id'), primary_key=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() )