class User(db.Model, UserMixin): id = db.Column(db.Integer, primary_key=True) email = db.Column(db.String(255), unique=True) password = db.Column(db.String(255)) active = db.Column(db.Boolean()) confirmed_at = db.Column(db.DateTime()) roles = db.relationship('Role', secondary=roles_users, backref=db.backref('users', lazy='dynamic'))
class PrivateMessage(db.Model, SinglePKMixin): __tablename__ = 'pm_messages' __cache_key__ = 'pm_messages_{id}' __cache_key_of_conversation__ = 'pm_messages_conv_{conv_id}' __serializer__ = PrivateMessageSerializer id = db.Column(db.Integer, primary_key=True) conv_id = db.Column( db.Integer, db.ForeignKey('pm_conversations.id'), nullable=False, index=True, ) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) time = db.Column( db.DateTime(timezone=True), nullable=False, index=True, server_default=func.now(), ) contents = db.Column(db.Text, nullable=False) @classmethod def from_conversation(cls, conv_id: int, page: int = 1, limit: int = 50) -> List['PrivateMessage']: """ Get a list of private messages in a conversation. """ return cls.get_many( key=cls.__cache_key_of_conversation__.format(conv_id=conv_id), filter=cls.conv_id == conv_id, order=cls.id.asc(), page=page, limit=limit, ) @classmethod def new(cls, conv_id: int, user_id: int, contents: str) -> Optional['PrivateMessage']: """ Create a message in a PM conversation. """ PrivateConversation.is_valid(conv_id, error=True) User.is_valid(user_id, error=True) PrivateConversationState.update_last_response_time(conv_id, user_id) return super()._new(conv_id=conv_id, user_id=user_id, contents=contents) @cached_property def user(self): return User.from_pk(self.user_id)
class Data(db.Model): id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid4) project_id = db.Column(UUID(as_uuid=True), db.ForeignKey('projects.id')) address = db.Column(db.String(80)) city = db.Column(db.String(80)) square = db.Column(db.Float()) living_square = db.Column(db.Float()) currency_value = db.Column(db.Float()) currency = db.Column(db.String) published_date = db.Column(db.DateTime()) rooms = db.Column(db.Integer()) toilets = db.Column(db.Integer())
class Actor(Model, db.Model): __tablename__ = 'actors' # id -> integer, primary key id = db.Column(db.Integer, primary_key=True) # name -> string, size 50, unique, not nullable name = db.Column(db.String(50), unique=True, nullable=False) # gender -> string, size 11 gender = db.Column(db.String(11)) # date_of_birth -> date date_of_birth = db.Column(db.DateTime()) # Use `db.relationship` method to define the Actor's relationship with Movie. # Set `backref` as 'cast', uselist=True # Set `secondary` as 'association' movies = db.relationship('Movie', backref='cast', uselist=True, secondary='association') def __repr__(self): return '<Actor {}>'.format(self.name)
class ForumThreadNote(db.Model, SinglePKMixin): __tablename__ = 'forums_threads_notes' __serializer__ = ForumThreadNoteSerializer __cache_key__ = 'forums_threads_notes_{id}' __cache_key_of_thread__ = 'forums_threads_notes_thread_{thread_id}' id = db.Column(db.Integer, primary_key=True) thread_id = db.Column( db.Integer, db.ForeignKey('forums_threads.id'), nullable=False, index=True, ) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) note = db.Column(db.Text, nullable=False) time = db.Column( db.DateTime(timezone=True), nullable=False, server_default=func.now() ) @classmethod def from_thread(cls, thread_id: int) -> List['ForumThreadNote']: return cls.get_many( key=cls.__cache_key_of_thread__.format(thread_id=thread_id), filter=cls.thread_id == thread_id, order=cls.time.desc(), ) # type: ignore @classmethod def new( cls, *, thread_id: int, user_id: int, note: str ) -> 'ForumThreadNote': ForumThread.is_valid(thread_id, error=True) User.is_valid(user_id, error=True) return super()._new(thread_id=thread_id, user_id=user_id, note=note) @cached_property def user(self): return User.from_pk(self.user_id)
class ForumPostEditHistory(db.Model, SinglePKMixin): __tablename__ = 'forums_posts_edit_history' __serializer__ = ForumPostEditHistorySerializer __cache_key__ = 'forums_posts_edit_history_{id}' __cache_key_of_post__ = 'forums_posts_edit_history_posts_{id}' id: int = db.Column(db.Integer, primary_key=True) post_id: int = db.Column( db.Integer, db.ForeignKey('forums_posts.id'), nullable=False ) editor_id: int = db.Column(db.Integer, db.ForeignKey('users.id')) contents: str = db.Column(db.Text, nullable=False) time: datetime = db.Column( db.DateTime(timezone=True), nullable=False, server_default=func.now() ) @classmethod def from_post(cls, post_id: int) -> List['ForumPostEditHistory']: return cls.get_many( key=cls.__cache_key_of_post__.format(id=post_id), filter=cls.post_id == post_id, order=cls.id.desc(), ) # type: ignore @classmethod def new( cls, *, post_id: int, editor_id: int, contents: str, time: datetime ) -> Optional[ForumPost]: ForumPost.is_valid(post_id, error=True) User.is_valid(editor_id, error=True) cache.delete(cls.__cache_key_of_post__.format(id=post_id)) return super()._new( post_id=post_id, editor_id=editor_id, contents=contents, time=time ) @cached_property def editor(self) -> User: return User.from_pk(self.editor_id)
class Notification(db.Model, SinglePKMixin): __tablename__ = 'notifications' __serializer__ = NotificationSerializer __cache_key__ = 'notifications_{id}' __cache_key_of_user__ = 'notifications_user_{user_id}_{type}' __cache_key_notification_count__ = ( 'notifications_user_{user_id}_{type}_count') __deletion_attr__ = 'read' id: int = db.Column(db.Integer, primary_key=True) user_id: int = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False, index=True) type_id: int = db.Column( db.Integer, db.ForeignKey('notifications_types.id'), nullable=False, index=True, ) time: datetime = db.Column(db.DateTime(timezone=True), nullable=False, server_default=func.now()) contents: str = db.Column(JSONB, nullable=False) read: bool = db.Column(db.Boolean, nullable=False, server_default='f', index=True) @property def type(self): return NotificationType.from_pk(self.type_id).type @classmethod def new(cls, user_id: int, type: str, contents: Dict[str, Union[Dict, str]]) -> 'Notification': User.is_valid(user_id, error=True) noti_type = NotificationType.from_type(type, create_new=True) cache.delete( cls.__cache_key_of_user__.format(user_id=user_id, type=type)) cache.delete( cls.__cache_key_notification_count__.format(user_id=user_id, type=type)) return super()._new(user_id=user_id, type_id=noti_type.id, contents=contents) @classmethod def get_all_unread(cls, user_id: int, limit: int = 25) -> Dict[str, List['Notification']]: return { t.type: cls.get_many( key=cls.__cache_key_of_user__.format(user_id=user_id, type=t.id), filter=and_(cls.user_id == user_id, cls.type_id == t.id), limit=limit, ) for t in NotificationType.get_all() } @classmethod def from_type( cls, user_id: int, type: str, page: int = 1, limit: int = 50, include_read: bool = False, ) -> List['Notification']: noti_type = NotificationType.from_type(type, error=True) return cls.get_many( key=cls.__cache_key_of_user__.format(user_id=user_id, type=noti_type.id), filter=and_(cls.user_id == user_id, cls.type_id == noti_type.id), page=page, limit=limit, include_dead=include_read, ) @classmethod def get_pks_from_type(cls, user_id: int, type: str, include_read: bool = False): noti_type = NotificationType.from_type(type) if type: filter = and_(cls.user_id == user_id, cls.type_id == noti_type.id) cache_key = cls.__cache_key_of_user__.format(user_id=user_id, type=noti_type.id) else: filter = cls.user_id == user_id cache_key = cls.__cache_key_of_user__.format(user_id=user_id, type='all') return cls.get_pks_of_many(key=cache_key, filter=filter, include_dead=include_read) @classmethod def get_notification_counts(cls, user_id: int) -> Dict[str, int]: return { t.type: cls.count( key=cls.__cache_key_notification_count__.format( user_id=user_id, type=t), attribute=cls.id, filter=and_( cls.user_id == user_id, cls.type_id == t.id, cls.read == 'f', ), ) for t in NotificationType.get_all() } @classmethod def clear_cache_keys(cls, user_id: int, type=None) -> None: types = ([NotificationType.from_type(type, error=True)] if type else NotificationType.get_all()) cache.delete_many(*chain(*chain([( cls.__cache_key_notification_count__.format(user_id=user_id, type=t.id), cls.__cache_key_of_user__.format(user_id=user_id, type=t.id), ) for t in types])))
class APIKey(db.Model, SinglePKMixin): __tablename__: str = 'api_keys' __serializer__ = APIKeySerializer __cache_key__: str = 'api_keys_{hash}' __cache_key_of_user__: str = 'api_keys_user_{user_id}' __deletion_attr__ = 'revoked' hash: str = db.Column(db.String(10), primary_key=True) user_id: int = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False, index=True) keyhashsalt: str = db.Column(db.String(128)) last_used: datetime = db.Column(db.DateTime(timezone=True), nullable=False, server_default=func.now()) ip: str = db.Column(INET, nullable=False, server_default='0.0.0.0') user_agent: str = db.Column(db.Text) revoked: bool = db.Column(db.Boolean, nullable=False, index=True, server_default='f') permanent: bool = db.Column(db.Boolean, nullable=False, index=True, server_default='f') timeout: bool = db.Column(db.Integer, nullable=False, server_default='3600') permissions: str = db.Column(ARRAY(db.String(36))) @classmethod def new( cls, user_id: int, ip: str, user_agent: str, permanent: bool = False, timeout: int = 60 * 30, permissions: List[str] = None, ) -> Tuple[str, 'APIKey']: """ Create a new API Key with randomly generated secret keys and the user details passed in as params. Generated keys are hashed and salted for storage in the database. :param user_id: API Key will belong to this user :param ip: The IP that this session was created with :param user_agent: User Agent the session was created with :return: A tuple containing the identifier and the new API Key """ while True: hash = secrets.token_urlsafe(10)[:10] if not cls.from_pk(hash, include_dead=True): break key = secrets.token_urlsafe(16)[:16] cache.delete(cls.__cache_key_of_user__.format(user_id=user_id)) api_key = super()._new( user_id=user_id, hash=hash, keyhashsalt=generate_password_hash(key), ip=ip, user_agent=user_agent, permanent=permanent, timeout=timeout, permissions=permissions or [], ) return (hash + key, api_key) @classmethod def from_user(cls, user_id: int, include_dead: bool = False) -> List['APIKey']: """ Get all API keys owned by a user. :param user_id: The User ID of the owner :param include_dead: Whether or not to include dead API keys in the search :return: A list of API keys owned by the user """ return cls.get_many( key=cls.__cache_key_of_user__.format(user_id=user_id), filter=cls.user_id == user_id, include_dead=include_dead, ) @classmethod def hashes_from_user(cls, user_id: int) -> List[Union[int, str]]: return cls.get_pks_of_many( key=cls.__cache_key_of_user__.format(user_id=user_id), filter=cls.user_id == user_id, ) def check_key(self, key: str) -> bool: """ Validates the authenticity of an API key against its stored id. :param key: The key to check against the keyhashsalt :return: Whether or not the key matches the keyhashsalt """ return check_password_hash(self.keyhashsalt, key) def has_permission(self, permission: Union[str, Enum]) -> bool: """ Checks if the API key is assigned a permission. If the API key is not assigned any permissions, it checks against the user's permissions instead. :param permission: Permission to search for :return: Whether or not the API Key has the permission """ p = permission.value if isinstance(permission, Enum) else permission if self.permissions: return p in self.permissions user = User.from_pk(self.user_id) return user.has_permission(p)
class Invite(db.Model, SinglePKMixin): __tablename__: str = 'invites' __serializer__ = InviteSerializer __cache_key__: str = 'invites_{code}' __cache_key_of_user__: str = 'invites_user_{user_id}' __deletion_attr__ = 'expired' code: str = db.Column(db.String(24), primary_key=True) inviter_id: int = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False, index=True) invitee_id: int = db.Column(db.Integer, db.ForeignKey('users.id'), index=True) email: str = db.Column(db.String(255), nullable=False) time_sent: datetime = db.Column(db.DateTime(timezone=True), server_default=func.now()) from_ip: str = db.Column(INET, nullable=False, server_default='0.0.0.0') expired: bool = db.Column(db.Boolean, nullable=False, index=True, server_default='f') @classmethod def new(cls, inviter_id: int, email: str, ip: int) -> 'Invite': """ Generate a random invite code. :param inviter_id: User ID of the inviter :param email: E-mail to send the invite to :param ip: IP address the invite was sent from """ while True: code = secrets.token_urlsafe(24)[:24] if not cls.from_pk(code, include_dead=True): break cache.delete(cls.__cache_key_of_user__.format(user_id=inviter_id)) return super()._new( inviter_id=inviter_id, code=code, email=email.lower().strip(), from_ip=ip, ) @classmethod def from_inviter(cls, inviter_id: int, include_dead: bool = False, used: bool = False) -> List['Invite']: """ Get all invites sent by a user. :param inviter_id: The User ID of the inviter. :param include_dead: Whether or not to include dead invites in the list :param used: Whether or not to include used invites in the list :return: A list of invites sent by the inviter """ filter = cls.inviter_id == inviter_id if used: filter = and_(filter, cls.invitee_id.isnot(None)) # type: ignore return cls.get_many( key=cls.__cache_key_of_user__.format(user_id=inviter_id), filter=filter, order=cls.time_sent.desc(), # type: ignore include_dead=include_dead or used, ) @cached_property def invitee(self) -> User: return User.from_pk(self.invitee_id) @cached_property def inviter(self) -> User: return User.from_pk(self.inviter_id) def belongs_to_user(self) -> bool: """Returns whether or not the requesting user matches the inviter.""" return flask.g.user is not None and self.inviter_id == flask.g.user.id
class PrivateConversationState(db.Model, MultiPKMixin): __tablename__ = 'pm_conversations_state' __cache_key__ = 'pm_convesations_state_{conv_id}_{user_id}' __cache_key_members__ = 'pm_conversations_state_{conv_id}_members' conv_id = db.Column(db.Integer, db.ForeignKey('pm_conversations.id'), primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), primary_key=True) original_member = db.Column(db.Boolean, nullable=False) read = db.Column(db.Boolean, nullable=False, server_default='f') sticky = db.Column(db.Boolean, nullable=False, server_default='f', index=True) deleted = db.Column(db.Boolean, nullable=False, server_default='f', index=True) time_added = db.Column(db.DateTime(timezone=True), nullable=False, server_default=func.now()) last_response_time = db.Column(db.DateTime(timezone=True)) @hybrid_property def in_sentbox(cls): return select([ exists().where( and_( PrivateMessage.conv_id == cls.conv_id, PrivateMessage.user_id == cls.user_id, cls.deleted == 'f', )) ]).as_scalar() @classmethod def get_users_in_conversation(cls, conv_id: int) -> List[User]: return User.get_many(pks=cls.get_user_ids_in_conversation(conv_id)) @classmethod def get_user_ids_in_conversation(cls, conv_id: int) -> List[int]: return cls.get_col_from_many( column=cls.user_id, key=cls.__cache_key_members__.format(conv_id=conv_id), filter=and_(cls.conv_id == conv_id, cls.deleted == 'f'), order=cls.time_added.asc(), ) @classmethod def new( cls, conv_id: int, user_id: int, original_member: bool = False, read: bool = False, ) -> Optional['PrivateConversationState']: """ Create a private message object, set states for the sender and receiver, and create the initial message. """ PrivateConversation.is_valid(conv_id, error=True) User.is_valid(user_id, error=True) cache.delete(cls.__cache_key_members__.format(conv_id=conv_id)) return super()._new( conv_id=conv_id, user_id=user_id, original_member=original_member, read=read, ) @classmethod def update_last_response_time(cls, conv_id: int, sender_id: int) -> None: db.session.query(cls).filter( and_(cls.conv_id == conv_id, cls.user_id != sender_id)).update( {'last_response_time': datetime.utcnow()}) db.session.commit() cache.delete_many(*(cls.create_cache_key({ 'conv_id': conv_id, 'user_id': uid }) for uid in cls.get_user_ids_in_conversation(conv_id) if uid != sender_id))
class WikiRevision(db.Model, MultiPKMixin): __tablename__ = 'wiki_revisions' __cache_key__ = 'wiki_revisions_articles_{article_id}_{revision_id}' __cache_key_of_article__ = 'wiki_revisions_of_article_{article_id}' __cache_key_latest_id_of_article__ = 'wiki_revisions_latest_{article_id}' __serializer__ = WikiRevisionSerializer revision_id: int = db.Column(db.Integer, primary_key=True) article_id: int = db.Column( db.Integer, db.ForeignKey('wiki_articles.id'), primary_key=True ) language_id: int = db.Column( db.Integer, db.ForeignKey('wiki_languages.id'), primary_key=True ) title: str = db.Column(db.String(128), nullable=False) editor_id: int = db.Column( db.Integer, db.ForeignKey('users.id'), nullable=False ) time: datetime = db.Column( db.DateTime(timezone=True), nullable=False, server_default=func.now() ) contents: str = db.Column(db.Text, nullable=False) @classmethod def from_article( cls, article_id: int, language_id: int = 1, page: int = 1, limit: int = 50, ) -> List['WikiRevision']: return cls.get_many( key=cls.__cache_key_of_article__.format(article_id=article_id), filter=and_( cls.article_id == article_id, cls.language_id == language_id ), order=cls.time.desc(), # type: ignore page=page, limit=limit, ) @classmethod def new( cls, article_id: int, title: str, language_id: int, editor_id: int, contents: str, ) -> Optional['WikiRevision']: WikiArticle.is_valid(article_id, error=True) WikiLanguage.is_valid(language_id, error=True) try: old_latest_id = ( cls.latest_revision(article_id).revision_id + 1 ) # type: ignore except WikiNoRevisions: old_latest_id = 1 cache.delete_many( cls.__cache_key_of_article__.format(article_id=article_id), cls.__cache_key_latest_id_of_article__.format( article_id=article_id ), ) return super()._new( revision_id=old_latest_id, article_id=article_id, title=title, language_id=language_id, editor_id=editor_id, contents=contents, ) @classmethod def latest_revision(cls, article_id: int, language_id: int = 1) -> int: cache_key = cls.__cache_key_latest_id_of_article__.format( article_id=article_id, language_id=language_id ) revision_id = cache.get(cache_key) if revision_id: return cls.from_attrs( revision_id=revision_id, article_id=article_id, language_id=language_id, ) else: latest_revision = ( cls.query.filter( and_( cls.article_id == article_id, cls.language_id == language_id, ) ) .order_by(cls.revision_id.desc()) # type: ignore .limit(1) .scalar() ) if not latest_revision: raise WikiNoRevisions cache.set(cache_key, latest_revision.revision_id) return latest_revision @property def editor(self): return User.from_pk(self.editor_id) @cached_property def language(self): return WikiLanguage.from_pk(self.language_id) @property def parent_article(self): return WikiArticle.from_pk(self.article_id)
class ForumPost(db.Model, SinglePKMixin): __tablename__ = 'forums_posts' __serializer__ = ForumPostSerializer __cache_key__ = 'forums_posts_{id}' __cache_key_of_thread__ = 'forums_posts_threads_{id}' __deletion_attr__ = 'deleted' id: int = db.Column(db.Integer, primary_key=True) thread_id: int = db.Column( db.Integer, db.ForeignKey('forums_threads.id'), nullable=False, index=True, ) user_id: int = db.Column( db.Integer, db.ForeignKey('users.id'), nullable=False, index=True ) contents: str = db.Column(db.Text, nullable=False) time: datetime = db.Column( db.DateTime(timezone=True), nullable=False, server_default=func.now() ) sticky: bool = db.Column(db.Boolean, nullable=False, server_default='f') edited_user_id: Optional[int] = db.Column( db.Integer, db.ForeignKey('users.id') ) edited_time: Optional[datetime] = db.Column(db.DateTime(timezone=True)) deleted: bool = db.Column( db.Boolean, nullable=False, server_default='f', index=True ) @classmethod def from_thread( cls, thread_id: int, page: int = 1, limit: int = 50, include_dead: bool = False, ) -> List['ForumPost']: return cls.get_many( key=cls.__cache_key_of_thread__.format(id=thread_id), filter=cls.thread_id == thread_id, order=cls.id.asc(), # type: ignore page=page, limit=limit, include_dead=include_dead, ) @classmethod def get_ids_from_thread(cls, id): return cls.get_pks_of_many( key=cls.__cache_key_of_thread__.format(id=id), filter=cls.thread_id == id, order=cls.id.asc(), ) @classmethod def new( cls, *, thread_id: int, user_id: int, contents: str ) -> Optional['ForumPost']: ForumThread.is_valid(thread_id, error=True) User.is_valid(user_id, error=True) cache.delete(cls.__cache_key_of_thread__.format(id=thread_id)) post = super()._new( thread_id=thread_id, user_id=user_id, contents=contents ) send_subscription_notices(post) check_post_contents_for_quotes(post) check_post_contents_for_mentions(post) return post @cached_property def thread(self) -> 'ForumThread': return ForumThread.from_pk(self.thread_id) @cached_property def user(self) -> User: return User.from_pk(self.user_id) @cached_property def editor(self) -> Optional[User]: return User.from_pk(self.edited_user_id) @cached_property def edit_history(self) -> List['ForumPostEditHistory']: return ForumPostEditHistory.from_post(self.id)
class ForumThread(db.Model, SinglePKMixin): __tablename__ = 'forums_threads' __serializer__ = ForumThreadSerializer __cache_key__ = 'forums_threads_{id}' __cache_key_post_count__ = 'forums_threads_{id}_post_count' __cache_key_of_forum__ = 'forums_threads_forums_{id}' __cache_key_last_post__ = 'forums_threads_{id}_last_post' __permission_key__ = 'forumaccess_thread_{id}' __deletion_attr__ = 'deleted' _posts: List['ForumPost'] id: int = db.Column(db.Integer, primary_key=True) topic: str = db.Column(db.String(150), nullable=False) forum_id = db.Column( db.Integer, db.ForeignKey('forums.id'), nullable=False, index=True ) # type: int creator_id: int = db.Column( db.Integer, db.ForeignKey('users.id'), nullable=False, index=True ) created_time: datetime = db.Column( db.DateTime(timezone=True), nullable=False, server_default=func.now() ) locked: bool = db.Column(db.Boolean, nullable=False, server_default='f') sticky: bool = db.Column(db.Boolean, nullable=False, server_default='f') deleted: bool = db.Column( db.Boolean, nullable=False, server_default='f', index=True ) @declared_attr def __table_args__(cls): return (db.Index('ix_forums_threads_topic', func.lower(cls.topic)),) @classmethod def from_forum( cls, forum_id: int, page: int = 1, limit: Optional[int] = 50, include_dead: bool = False, ) -> List['ForumThread']: return cls.get_many( key=cls.__cache_key_of_forum__.format(id=forum_id), filter=cls.forum_id == forum_id, order=cls.last_updated.desc(), page=page, limit=limit, include_dead=include_dead, ) @classmethod def new( cls, topic: str, forum_id: int, creator_id: int, post_contents: str ) -> Optional['ForumThread']: Forum.is_valid(forum_id, error=True) User.is_valid(creator_id, error=True) cache.delete(cls.__cache_key_of_forum__.format(id=forum_id)) thread = super()._new( topic=topic, forum_id=forum_id, creator_id=creator_id ) subscribe_users_to_new_thread(thread) ForumPost.new( thread_id=thread.id, user_id=creator_id, contents=post_contents ) return thread @classmethod def get_ids_from_forum(cls, id): return cls.get_pks_of_many( key=cls.__cache_key_of_forum__.format(id=id), filter=cls.forum_id == id, order=cls.last_updated.desc(), ) @classmethod def from_subscribed_user(cls, user_id: int) -> List['ForumThread']: return cls.get_many(pks=cls.subscribed_ids(user_id)) @classmethod def subscribed_ids(cls, user_id: int) -> List[Union[str, int]]: return cls.get_pks_of_many( key=ForumThreadSubscription.__cache_key_of_user__.format( user_id=user_id ), filter=cls.id.in_( db.session.query( ForumThreadSubscription.thread_id ).filter( # type: ignore ForumThreadSubscription.user_id == user_id ) ), order=ForumThread.id.asc(), ) # type: ignore @hybrid_property def last_updated(cls) -> BinaryExpression: return ( select([func.max(ForumPost.time)]) .where(ForumPost.thread_id == cls.id) .as_scalar() ) @cached_property def last_post(self) -> Optional['ForumPost']: return ForumPost.from_query( key=self.__cache_key_last_post__.format(id=self.id), filter=and_( ForumPost.thread_id == self.id, ForumPost.deleted == 'f' ), order=ForumPost.id.desc(), ) # type: ignore @cached_property def last_viewed_post(self) -> Optional['ForumPost']: return ( ForumLastViewedPost.post_from_attrs( thread_id=self.id, user_id=flask.g.user.id ) if flask.g.user else None ) @cached_property def forum(self) -> 'Forum': return Forum.from_pk(self.forum_id) @cached_property def creator(self) -> User: return User.from_pk(self.creator_id) @cached_property def poll(self) -> 'ForumPoll': return ForumPoll.from_thread(self.id) @cached_property def post_count(self) -> int: return self.count( key=self.__cache_key_post_count__.format(id=self.id), attribute=ForumPost.id, filter=and_( ForumPost.thread_id == self.id, ForumPost.deleted == 'f' ), ) @cached_property def thread_notes(self) -> List['ForumThreadNote']: return ForumThreadNote.from_thread(self.id) @cached_property def subscribed(self) -> bool: return ( self.id in set(self.subscribed_ids(flask.g.user.id)) if flask.g.user else False ) @property def posts(self) -> List['ForumPost']: if not hasattr(self, '_posts'): self._posts = ForumPost.from_thread(self.id, 1, limit=50) return self._posts def set_posts( self, page: int = 1, limit: int = 50, include_dead: bool = False ) -> None: self._posts = ForumPost.from_thread(self.id, page, limit, include_dead) def can_access(self, permission: str = None, error: bool = False) -> bool: """Determines whether or not the user has the permissions to access the thread.""" if flask.g.user is None: # pragma: no cover if error: raise _403Exception return False # Explicit thread access permission_key = self.__permission_key__.format(id=self.id) if flask.g.user.has_permission(permission_key) or ( permission is not None and flask.g.user.has_permission(permission) ): return True # Access to forum gives access to all threads by default. # If user has been ungranted the thread, they cannot view it regardless. ungranted_threads = [ p for p, g in UserPermission.from_user( flask.g.user.id, prefix='forumaccess_thread' ).items() if g is False ] if permission_key not in ungranted_threads and ( flask.g.user.has_permission( Forum.__permission_key__.format(id=self.forum_id) ) ): return True if error: raise _403Exception return False