Пример #1
0
class APNDevice(db.Model):
    """
    An APN device to send Push Notifications to.
    """

    __tablename__ = 'apn_devices'
    __table_args__ = (db.UniqueConstraint('device_id', 'provider'), {
        'extend_existing': True
    })

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    uuid = db.Column(db.String(36),
                     unique=True,
                     nullable=False,
                     default=lambda: str(UUID(bytes=rand_bytes(16))))

    device_id = db.Column(db.String(64), nullable=True)
    provider = db.Column(db.Enum(APNProvider), nullable=False)

    user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
    user = db.relationship('User', backref='apn_devices', lazy=True)

    def __repr__(self):
        return "<APNDevice {} for {}>".format(self.device_id,
                                              self.provider.name)
Пример #2
0
class AnswerRevision(db.Model):
    """
    A prior revision of a post
    """

    __tablename__ = 'answer_revision'

    answer_id = db.Column(db.Integer,
                          db.ForeignKey('answers.id'),
                          primary_key=True)
    revision_id = db.Column(db.Integer,
                            primary_key=True,
                            default=get_revision_id)

    language_id = db.Column(db.String(answers['lang_len']),
                            nullable=True,
                            default=None)
    language_name = db.Column(db.String(answers['lang_len']),
                              nullable=True,
                              default=None)

    binary_code = db.Column(db.BLOB, default=None, nullable=True)
    commentary = db.Column(db.Text, default=None, nullable=True)
    encoding = db.Column(db.String(10), default='utf8')
    deleted = db.Column(db.Boolean, nullable=False, default=False)

    revision_time = db.Column(db.DateTime, default=datetime.datetime.now)
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)

    user = db.relationship('User', backref='answer_revisions', lazy=True)
    answer = db.relationship('Answer',
                             backref=backref('answer_revisions',
                                             order_by=revision_id),
                             lazy=True)

    def to_json(self):
        return {
            'answer_id': self.answer_id,
            'revision_id': self.revision_id,
            'language_id': self.language_id,
            'language_name': self.language_name,
            'code': self.binary_code.decode(self.encoding),
            'commentary': self.commentary,
            'encoding': self.encoding,
            'deleted': self.deleted,
            'revision_time': self.revision_time,
            'user_id': self.user_id
        }

    def __repr__(self):
        return repr(self.to_json())
Пример #3
0
class Login(db.Model):
    """
    Represents a login by a user (either creating a new session, or refreshing a stale session)
    """

    __tablename__ = 'logins'
    __table_args__ = {'extend_existing': True}

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    time = db.Column(db.DateTime, default=datetime.datetime.now, nullable=False)
    ip_address = db.Column(db.String(40), nullable=False)
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
    login_method_id = db.Column(db.Integer, db.ForeignKey('user_auth_tokens.id'), nullable=True)

    user = db.relationship('User', backref='logins', order_by='desc(Login.time)', lazy=True)
    login_method = db.relationship('UserAuthToken', backref='logins', order_by='desc(Login.time)', lazy=True)

    def to_json(self):
        return {
            'id': self.id,
            'time': self.time,
            'ip_address': self.ip_address,
            'user_id': self.user_id
        }

    def __repr__(self):
        return "<Login by {} ({}) at {}>".format(self.user_id or 'anonymous user', self.ip_address, self.time)
Пример #4
0
class Answers(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50))

    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return '<Answers %r>' % self.name
Пример #5
0
class Theme(db.Model):
    """
    A theme for the web view
    """

    __tablename__ = "themes"
    __table_args__ = {'extend_existing': True}

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    name = db.Column(db.String(20), unique=True, nullable=False)
Пример #6
0
class UserAuthToken(db.Model):
    """
    Represents an authentication scheme for a user based on a JWT-key or OAuth
    style with an issuer and an identity. You **must** validate the key before
    inserting it here.
    """

    __tablename__ = 'user_auth_tokens'

    id = db.Column(db.Integer,
                   primary_key=True,
                   unique=True,
                   autoincrement=True)

    # Auth method type
    auth_method = db.Column(db.Enum(AuthTokenType), nullable=False)

    # Who gives identity and who they are said to be
    identity = db.Column(db.String(255), nullable=False)
    issuer = db.Column(db.String(255), nullable=False)

    # User-facing identifier
    identifier = db.Column(db.String(255), nullable=False)

    # Connects to Axtell user
    user_id = db.Column(db.Integer, db.ForeignKey(User.id), nullable=False)
    user = db.relationship(User, backref=db.backref('auth_tokens', lazy=True))

    def to_json(self):
        # Get the time of most recent login
        latest_login = next(iter(self.logins), None)
        latest_login_time = latest_login and latest_login.time.isoformat()

        return {
            'id': self.id,
            'method': self.auth_method.value,
            'issuer': self.issuer,
            'last_used': latest_login_time,
            'identifier': self.identifier
        }

    def __repr__(self):
        return f'<UserToken for {self.user}>'
Пример #7
0
class PushDevice(db.Model):
    """
    An APN device to send Push Notifications to.
    """

    __tablename__ = 'push_devices'

    id = db.Column(db.Integer, primary_key=True)

    endpoint = db.Column(db.String(400), nullable=False)
    auth = db.Column(db.String(24),
                     nullable=False)  # Maximum unencoded length is 16
    client_pub = db.Column(db.String(88),
                           nullable=False)  # Maximum unencoded length is 65

    user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
    user = db.relationship('User', backref='push_devices', lazy=True)

    def __repr__(self):
        return "<PushDevice for {}>".format(self.user.name)
Пример #8
0
class Category(db.Model):
    """
    A category of a post.
    """

    __tablename__ = 'categories'

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    name = db.Column(db.String(15), nullable=False)

    def to_json(self):
        return {'name': self.name}

    def __repr__(self):
        return '<Tag \'{!r}\'>'.format(self.name)
Пример #9
0
class Question(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(250))
    body = db.Column(db.Text)
    pub_date = db.Column(db.DateTime)

    category_id = db.Column(db.Integer, db.ForeignKey('category.id'))
    category = db.relationship('Category',
                               backref=db.backref('posts', lazy='dynamic'))

    def __init__(self, title, body, question, post_date=None):
        self.title = title
        self.body = body
        if post_date is None:
            post_date = datetime.utcnow()
        self.pub_date = post_date
        self.question = question

    def __repr__(self):
        return '<Questions %r>' % self.title
Пример #10
0
class PostRevision(db.Model):
    """
    A prior revision of a post
    """

    __tablename__ = 'post_revision'

    post_id = db.Column(db.Integer,
                        db.ForeignKey('posts.id'),
                        primary_key=True)
    revision_id = db.Column(db.Integer,
                            primary_key=True,
                            default=get_revision_id)

    title = db.Column(db.String(posts['max_title']), nullable=False)
    body = db.Column(LONGTEXT, nullable=False)
    deleted = db.Column(db.Boolean, nullable=False, default=False)

    revision_time = db.Column(db.DateTime, default=datetime.datetime.now)
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)

    user = db.relationship('User', backref='post_revisions', lazy=True)
    post = db.relationship('Post',
                           backref=backref('post_revisions',
                                           order_by=revision_id),
                           lazy=True)

    def to_json(self):
        return {
            'post_id': self.post_id,
            'revision_id': self.revision_id,
            'title': self.title,
            'body': self.body,
            'deleted': self.deleted,
            'revision_time': self.revision_time,
            'user_id': self.user_id
        }

    def __repr__(self):
        return repr(self.to_json())
Пример #11
0
class Answer(db.Model):
    """
    An answer posted to a post by a user.
    """

    __tablename__ = 'answers'
    __table_args__ = {'extend_existing': True}

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    post_id = db.Column(db.Integer, db.ForeignKey('posts.id'), nullable=False)

    language_id = db.Column(db.String(answers['lang_len']),
                            nullable=True,
                            default=None)
    language_name = db.Column(db.String(answers['lang_len']),
                              nullable=True,
                              default=None)

    binary_code = db.Column(db.BLOB, default=None, nullable=True)
    commentary = db.Column(db.Text, default=None, nullable=True)
    encoding = db.Column(db.String(30), default='utf8')
    deleted = db.Column(db.Boolean, nullable=False, default=False)

    user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
    date_created = db.Column(db.DateTime, default=datetime.datetime.now)

    index_status = db.Column(db.Enum(IndexStatus),
                             default=IndexStatus.UNSYNCHRONIZED,
                             nullable=False)

    user = db.relationship('User', backref='answers')
    post = db.relationship('Post', backref='answers', lazy=True)

    @property
    def code(self):
        return self.binary_code.decode(self.encoding)

    @code.setter
    def set_code(self, value):
        self.binary_code = value.encode(self.encoding)

    @index_json
    def get_index_json(self):
        last_revision = AnswerRevision.query.filter_by(
            answer_id=self.id).order_by(
                AnswerRevision.revision_time.desc()).first()
        if isinstance(last_revision, AnswerRevision):
            last_modified = last_revision.revision_time
        else:
            last_modified = self.date_created

        language = self.get_language()

        return {
            'objectID': f'answer-{self.id}',
            'id': self.id,
            'code': self.code,
            'date_created': self.date_created.isoformat() + 'Z',
            'last_modified': last_modified.isoformat() + 'Z',
            'language': language.get_id(),
            'language_name': language.get_display_name(),
            'byte_count': self.byte_len,
            'author': self.user.get_index_json(root_object=False),
            'post': {
                'id': self.post.id,
                'name': self.post.title,
                'slug': slugify(self.post.title)
            }
        }

    def should_index(self):
        return not self.deleted

    @classmethod
    @gets_index
    def get_index(cls):
        return 'answers'

    @classmethod
    def get_index_settings(cls):
        return {
            'searchableAttributes':
            ['code', 'language_name', 'author.name', 'post.name'],
            'attributesToSnippet':
            ['code', 'language_name', 'author.name', 'post.name'],
            'separatorsToIndex':
            '!#()[]{}*+-_一,:;<>?@/^|%&~£¥$§€`"\'‘’“”†‡'
        }

    @hybrid_property
    def byte_len(self):
        return len(self.binary_code)

    @byte_len.expression
    def byte_len(cls):
        return func.length(cls.binary_code)

    @hybrid_property
    def score(self):
        ups = sum(1 for vote in self.votes if vote.vote == 1)
        downs = sum(-1 for vote in self.votes if vote.vote == -1)

        n = ups - downs

        if n == 0:
            return 0

        z = 1.0
        phat = ups / n
        return (phat + z * z / (2 * n) - z * sqrt(
            (phat * (1 - phat) + z * z / (4 * n)) / n)) / (1 + z * z / n)

    @score.expression
    def score(cls):
        ups = select([func.sum(AnswerVote.vote)
                      ]).where(AnswerVote.answer_id == cls.id
                               and AnswerVote.vote == 1).label('ups')
        downs = select([func.sum(AnswerVote.vote)
                        ]).where(AnswerVote.answer_id == cls.id
                                 and AnswerVote.vote == -1).label('downs')

        n = ups - downs

        if n == 0:
            return 0

        z = 1.0
        phat = ups / n
        return (phat + z * z / (2 * n) - z * func.sqrt(
            (phat * (1 - phat) + z * z / (4 * n)) / n)) / (1 + z * z / n)

    def to_json(self, no_code=False):
        data = {}

        if not no_code:
            data['code'] = self.code
            data['commentary'] = self.commentary

        data['id'] = self.id
        data['encoding'] = self.encoding
        data['byte_len'] = self.byte_len

        language = self.get_language()
        if language is not None:
            data['lang'] = language.to_json()

        data['owner'] = self.user.to_json()

        data['date_created'] = self.date_created.isoformat()

        data['deleted'] = self.deleted

        return data

    def get_language(self):
        if self.language_id is None:
            return None

        return app.models.Language.Language(self.language_id)

    def revise(self, user, **new_answer_data):
        revision = AnswerRevision(answer_id=self.id,
                                  language_id=self.language_id,
                                  language_name=self.language_name,
                                  binary_code=self.binary_code,
                                  commentary=self.commentary,
                                  encoding=self.encoding,
                                  deleted=self.deleted,
                                  user_id=user.id)

        if 'code' in new_answer_data:
            self.binary_code = new_answer_data['code'].encode(
                new_answer_data.get('encoding', self.encoding) or 'utf8')

        if 'commentary' in new_answer_data:
            self.commentary = new_answer_data['commentary']

        if 'encoding' in new_answer_data:
            self.encoding = new_answer_data['encoding']

        if 'deleted' in new_answer_data:
            self.deleted = new_answer_data['deleted']

        self.index_status = IndexStatus.UNSYNCHRONIZED

        return self, revision

    def __repr__(self):
        return '<Answer(%r) by %r %s>' % (self.id, self.user.name,
                                          "(deleted)" if self.deleted else "")
Пример #12
0
class Notification(db.Model):
    """
    Represents a notification sent to a user
    """

    __tablename__ = 'notifications'
    __table_args__ = {'extend_existing': True}

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    uuid = db.Column(db.String(36),
                     unique=True,
                     nullable=False,
                     default=lambda: str(UUID(bytes=urandom(16))))

    recipient_id = db.Column(db.Integer,
                             db.ForeignKey('users.id'),
                             nullable=False)
    recipient = db.relationship('User',
                                foreign_keys=[recipient_id],
                                backref='notifications',
                                lazy='joined')

    sender_id = db.Column(db.Integer,
                          db.ForeignKey('users.id'),
                          nullable=False)
    sender = db.relationship('User',
                             foreign_keys=[sender_id],
                             backref='sent_notifications',
                             lazy='joined')

    # The type of notification see the NotificationType enum class
    notification_type = db.Column(db.Enum(NotificationType), nullable=False)

    # An ID referencing the item which dispatches the notification
    target_id = db.Column(db.Integer, nullable=True)

    # The id of the subscription source
    source_id = db.Column(db.Integer, nullable=True)

    date_created = db.Column(db.DateTime, default=datetime.datetime.now)

    read = db.Column(db.Enum(NotificationStatus),
                     default=NotificationStatus.UNSEEN)

    def get_target_descriptor(self):
        """
        Returns the 'category' of the type's associated
        payload. This is used in the responder router
        """
        return {
            NotificationType.STATUS_UPDATE: 'status',
            NotificationType.NEW_ANSWER: 'answer',
            NotificationType.OUTGOLFED: 'answer',
            NotificationType.NEW_POST_COMMENT: 'post_comment',
            NotificationType.NEW_ANSWER_COMMENT: 'answer_comment',
            NotificationType.ANSWER_VOTE: 'answer',
            NotificationType.POST_VOTE: 'answer'
        }[self.notification_type]

    def get_title(self):
        # TODO: more descriptive titles
        return {
            NotificationType.STATUS_UPDATE:
            lambda: "Status Update",
            NotificationType.NEW_ANSWER:
            lambda: get_title.new_answer(self),
            NotificationType.OUTGOLFED:
            lambda: get_title.outgolfed(self),
            NotificationType.NEW_POST_COMMENT:
            lambda: get_title.post_comment(self),
            NotificationType.NEW_ANSWER_COMMENT:
            lambda: get_title.answer_comment(self),
            NotificationType.ANSWER_VOTE:
            lambda: "Someone voted on your answer",
            NotificationType.POST_VOTE:
            lambda: "Someone voted on your post"
        }[self.notification_type]()

    def get_body(self):
        # TODO: more descriptive bodies
        return {
            NotificationType.STATUS_UPDATE:
            lambda: "You're received a brand new status update.",
            NotificationType.NEW_ANSWER:
            lambda: get_body.new_answer(self),
            NotificationType.OUTGOLFED:
            lambda: get_body.outgolfed(self),
            NotificationType.NEW_POST_COMMENT:
            lambda: "A new comment has been posted on your challenge.",
            NotificationType.NEW_ANSWER_COMMENT:
            lambda: "A new comment has been posted on your answer.",
            NotificationType.ANSWER_VOTE:
            lambda: "A new vote has come upon your answer.",
            NotificationType.POST_VOTE:
            lambda: "A new vote has come upon your challenge."
        }[self.notification_type]()

    def get_plural(self):
        """
        Gets plural of notification type
        """
        return {
            NotificationType.STATUS_UPDATE: lambda: "updates",
            NotificationType.NEW_ANSWER: lambda: "new answers",
            NotificationType.OUTGOLFED: lambda: "outgolfs",
            NotificationType.NEW_POST_COMMENT: lambda: "new comments",
            NotificationType.NEW_ANSWER_COMMENT: lambda: "new comments",
            NotificationType.ANSWER_VOTE: lambda: "votes",
            NotificationType.POST_VOTE: lambda: "votes"
        }[self.notification_type]()

    def is_overwriting(self):
        """
        Gets if this notification type should be grouped and
        have older instances discarded.
        """
        return {
            NotificationType.STATUS_UPDATE: False,
            NotificationType.NEW_ANSWER: False,
            NotificationType.OUTGOLFED: True,
            NotificationType.NEW_POST_COMMENT: False,
            NotificationType.NEW_ANSWER_COMMENT: False,
            NotificationType.ANSWER_VOTE: True,
            NotificationType.POST_VOTE: True
        }[self.notification_type]

    def to_apns_json(self):
        """
        Returns APNS compliant JSON payload
        """
        target = str(self.target_id) if self.target_id is not None else '_'

        return {
            'aps': {
                'alert': {
                    'title': self.get_title(),
                    'body': self.get_body(),
                    'action': 'View'
                },
                'url-args': [self.uuid,
                             self.get_target_descriptor(), target]
            }
        }

    def to_push_json(self):
        return {
            'id': self.uuid,
            'title': self.get_title(),
            'body': self.get_body(),
            'category': self.get_target_descriptor(),
            'source': self.source_id,
            'target': self.target_id,
            'overwriting': self.is_overwriting(),
            'date': self.date_created.isoformat() + 'Z'
        }

    def to_json(self):
        return {
            'id': self.uuid,
            'title': self.get_title(),
            'body': self.get_body(),
            'plural': self.get_plural(),
            'recipient': self.recipient.to_json(),
            'source_id': self.source_id,
            'sender': self.sender.to_json(),
            'target_id': self.target_id,
            'category': self.get_target_descriptor(),
            'date_created': self.date_created.isoformat() + 'Z',
            'type': self.notification_type.value,
            'status': self.read.value
        }

    def __repr__(self):
        return "<Notification about {} for {}>".format(
            self.notification_type.name, self.recipient.name)
Пример #13
0
class User(db.Model):
    """
    Self-explanatory, a user.
    """

    __tablename__ = 'users'

    id = db.Column(db.Integer,
                   primary_key=True,
                   unique=True,
                   autoincrement=True)
    name = db.Column(db.String(config.users['max_name_len']), nullable=False)
    email = db.Column(db.String(320))
    avatar = db.Column(db.String(256), nullable=True)

    posts = db.relationship('Post', backref='user')
    theme = db.Column(db.Integer, db.ForeignKey('themes.id'), nullable=True)
    receive_notifications = db.Column(db.Boolean, nullable=False, default=True)

    index_status = db.Column(db.Enum(IndexStatus),
                             default=IndexStatus.UNSYNCHRONIZED,
                             nullable=False)

    following_public = db.Column(db.Boolean, nullable=False, default=True)
    linked_stackexchange_public = db.Column(db.Boolean,
                                            nullable=False,
                                            default=False)

    is_admin = db.Column(db.Boolean, nullable=False, default=False)

    deleted = db.Column(db.Boolean, nullable=False, default=False)

    @index_json
    def get_index_json(self):
        return {
            'objectID': f'user-{self.id}',
            'id': self.id,
            'name': self.name,
            'avatar': self.avatar_url()
        }

    def should_index(self):
        return not self.deleted

    @classmethod
    @gets_index
    def get_index(cls):
        return 'users'

    @classmethod
    def get_index_settings(cls):
        return {
            'searchableAttributes': ['name'],
            'attributesToSnippet': ['name']
        }

    def follow(self, user):
        """
        Makes (self) User follow the given user. You can't follow yourself

        :param User user: The user which this instance _will_ follow
        """
        if not user.followed_by(self) and user.id != self.id:
            self.following.append(user)

    def unfollow(self, user):
        """
        Makes (self) User not follow the given user.

        :param User user: The user which this instance _will not_ follow
        """

        if user.followed_by(self):
            self.following.remove(user)

    def followed_by(self, user):
        """
        Checks if a provided user follows the user referenced by `self`

        :param User user: The user which will be checked if following
        :return: boolean indicating
        """
        return self.followers.filter_by(id=user.id).scalar() is not None

    def avatar_url(self):
        if self.avatar is not None:
            return self.avatar
        else:
            return gravatar(self.email)

    def to_json(self, current_user=None, own=False, bio=False):
        data = {
            'id': self.id,
            'name': self.name,
            'avatar': self.avatar_url(),
            'is_admin': self.is_admin
        }

        if current_user is not None:
            data['is_following'] = self.followed_by(current_user)

        if own:
            data['email'] = self.email
            data['receive_notifications'] = self.receive_notifications

        if bio:
            data['post_count'] = len(self.posts)
            data['answer_count'] = len(self.answers)

        return data

    def __repr__(self):
        return '<User({!r}) "{!r}">'.format(self.id, self.name)
Пример #14
0
class PostComment(db.Model):

    __tablename__ = 'post_comments'
    __table_args__ = {'extend_existing': True}

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    post_id = db.Column(db.Integer, db.ForeignKey('posts.id'), nullable=False)
    parent_id = db.Column(db.Integer,
                          db.ForeignKey('post_comments.id'),
                          nullable=True)
    text = db.Column(db.String(comments['max_len']), nullable=False)
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
    date_created = db.Column(db.DateTime, default=datetime.datetime.utcnow)
    deleted = db.Column(db.Boolean, default=False, nullable=False)

    user = db.relationship('User', backref='post_comments')
    post = db.relationship('Post', backref='comments')
    parent = db.relationship('PostComment',
                             backref='children',
                             remote_side=[id])

    def to_json(self, show_children=True, show_parent=False):
        data = {
            'id':
            self.id,
            'ty':
            'post',
            'source_id':
            self.post_id,
            'text':
            self.text,
            'date':
            self.date_created.isoformat(),
            'owner':
            self.user.to_json(),
            'parent':
            self.parent and show_parent
            and self.parent.to_json(show_children=show_children),
            'children':
            show_children and [
                child.to_json(show_parent=show_parent)
                for child in self.children
            ],
            'deleted':
            self.deleted
        }

        return data

    def comment_tree(self, nest_depth=None):
        if len(self.children) == 0:
            return self
        if nest_depth is None:
            return [
                self,
                [
                    child.comment_tree() for child in self.children
                    if child.deleted is False
                ]
            ]
        else:
            if nest_depth > 1:
                return [
                    self,
                    [
                        child.comment_tree(nest_depth - 1)
                        for child in self.children if child.deleted is False
                    ]
                ]

    def __repr__(self):
        return '<PostComment(%r) by %r>' % (self.id, self.user.name)
Пример #15
0
class Post(db.Model):
    """
    Represnts a post (e.g. challenge)
    """

    __tablename__ = 'posts'
    __table_args__ = {'extend_existing': True}

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    title = db.Column(db.String(posts['max_title']), nullable=False)
    body = db.Column(LONGTEXT, nullable=False)
    deleted = db.Column(db.Boolean, nullable=False, default=False)

    date_created = db.Column(db.DateTime, default=datetime.datetime.now)

    user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)

    index_status = db.Column(db.Enum(IndexStatus),
                             default=IndexStatus.UNSYNCHRONIZED,
                             nullable=False)

    ppcg_id = db.Column(db.Integer, nullable=True)

    @index_json
    def get_index_json(self):
        last_revision = PostRevision.query.filter_by(post_id=self.id).order_by(
            PostRevision.revision_time.desc()).first()
        if isinstance(last_revision, PostRevision):
            last_modified = last_revision.revision_time
        else:
            last_modified = self.date_created

        return {
            'objectID': f'post-{self.id}',
            'id': self.id,
            'title': self.title,
            'body': render_markdown(self.body, render_math=False),
            'slug': slugify(self.title),
            'date_created': self.date_created.isoformat() + 'Z',
            'last_modified': last_modified.isoformat() + 'Z',
            'score': self.score,
            'author': self.user.get_index_json(root_object=False)
        }

    def should_index(self):
        return not self.deleted

    def get_view_count(self):
        return get_post_views(self.id)

    def get_hotness(self, views, answers):
        """
        Gets hotness value on scale of [0, 1]
        """

        time_delta = datetime.datetime.now() - self.date_created

        view_component = 1 - exp(-views / hotness['view_weight']) * 0.5
        answer_component = 1 - exp(-answers / hotness['answer_weight']) * 2
        time_component = exp(-time_delta.days / hotness['time_weight'])

        return (view_component + answer_component + time_component) / 3

    @classmethod
    @gets_index
    def get_index(cls):
        return 'posts'

    @classmethod
    def get_index_settings(cls):
        return {
            'searchableAttributes': ['body', 'title', 'author.name'],
            'attributesToSnippet': ['title', 'body:15', 'author.name']
        }

    @hybrid_property
    def score(self):
        ups = sum(1 for vote in self.votes if vote.vote == 1)
        downs = sum(-1 for vote in self.votes if vote.vote == -1)

        n = ups - downs

        if n == 0:
            return 0

        z = 1.0
        phat = ups / n
        return (phat + z * z / (2 * n) - z * sqrt(
            (phat * (1 - phat) + z * z / (4 * n)) / n)) / (1 + z * z / n)

    @score.expression
    def score(cls):
        ups = select([func.sum(PostVote.vote)
                      ]).where(PostVote.post_id == cls.id
                               and PostVote.vote == 1).label('ups')
        downs = select([func.sum(PostVote.vote)
                        ]).where(PostVote.post_id == cls.id
                                 and PostVote.vote == -1).label('downs')

        n = ups - downs

        if n == 0:
            return 0

        z = 1.0
        phat = ups / n
        return (phat + z * z / (2 * n) - z * func.sqrt(
            (phat * (1 - phat) + z * z / (4 * n)) / n)) / (1 + z * z / n)

    @hybrid_property
    def age(self):
        last_revision = PostRevision.query.filter_by(post_id=self.id).order_by(
            PostRevision.revision_time.desc()).first()
        if isinstance(last_revision, PostRevision):
            last_modified = last_revision.revision_time
        else:
            last_modified = self.date_created

        return last_modified

    @age.expression
    def age(cls):
        last_revision = select([func.coalesce(PostRevision.revision_time, cls.date_created)]).\
                            where(PostRevision.post_id == cls.id).\
                            order_by(PostRevision.revision_time.desc()).\
                            limit(1)[0]
        return last_revision[0]

    @hybrid_property
    def activity(self):
        last_answer_revision = select([func.coalesce(AnswerRevision.revision_time, Answer.date_created)].label('last_revision')).\
                                select_from(Answer.join(AnswerRevision)).\
                                where(Answer.post_id == self.id).\
                                order_by(desc('last_revision')).\
                                limit(1)
        if len(last_answer_revision) == 0:
            return self.age()
        else:
            return last_answer_revision[0][0]

    def to_json(self, no_body=False):
        json = {
            'id': self.id,
            'title': self.title,
            'owner': self.user.to_json(),
            'slug': slugify(self.title),
            'date_created': self.date_created.isoformat(),
            'deleted': self.deleted
        }

        if not no_body:
            json['body'] = self.body

        return json

    def revise(self, user, **new_post_data):
        revision = PostRevision(post_id=self.id,
                                title=self.title,
                                body=self.body,
                                deleted=self.deleted,
                                user_id=user.id)

        self.title = new_post_data.get('title', self.title)
        self.body = new_post_data.get('body', self.body)
        self.deleted = new_post_data.get('deleted', self.deleted)
        self.ppcg_id = new_post_data.get('ppcg_id', self.ppcg_id)

        self.index_status = IndexStatus.UNSYNCHRONIZED

        return self, revision

    def __repr__(self):
        return '<Post(%r) by %r %s>' % (self.id, self.user.name,
                                        "(deleted)" if self.deleted else "")