コード例 #1
0
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'))
コード例 #2
0
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)
コード例 #3
0
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())
コード例 #4
0
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)
コード例 #5
0
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)
コード例 #6
0
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)
コード例 #7
0
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])))
コード例 #8
0
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)
コード例 #9
0
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
コード例 #10
0
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))
コード例 #11
0
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)
コード例 #12
0
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)
コード例 #13
0
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