Example #1
0
File: backend.py Project: hnk/pjuu
def vote_post(user_id, post_id, amount=1):
    """Handles voting on posts

    """
    # Get the comment so we can check who the author is
    author_uid = get_post(post_id).get('user_id')

    if not has_voted(user_id, post_id):
        if author_uid != user_id:
            r.zadd(k.POST_VOTES.format(post_id), amount, user_id)
            # Increment the score by amount (can be negative)
            # Post score can go lower than 0
            m.db.posts.update({'_id': post_id},
                              {'$inc': {'score': amount}})

            if amount < 0:
                # Don't decrement the users score if it is already at 0
                # we use a query to ONLY find user if the score if greater than
                # 0. This might seem stange but it is in the only way to keep
                # this atomic
                m.db.users.update({'_id': author_uid, 'score': {'$gt': 0}},
                                  {'$inc': {'score': amount}})
            else:
                # If its an increment it doesn't really matter
                m.db.users.update({'_id': author_uid},
                                  {'$inc': {'score': amount}})
        else:
            raise CantVoteOnOwn
    else:
        raise AlreadyVoted
Example #2
0
def back_feed(who_id, whom_id):
    """Takes 5 lastest posts from user with ``who_id`` places them in user
    with ``whom_id`` feed.

    The reason behind this is that new users may follow someone but still have
    and empty feed, which makes them sad :( so we'll give them some. If the
    posts are to old for a non user they will be removed when the feed is
    trimmed, but they may make it in to the feed but not at the top.

    :param who_id: user who just followed ``who_id``
    :type who_id: str
    :param whom_id: user who was just followed by ``whom_id``
    :type whom_id: str
    :returns: None

    """
    # Get followee's last 5 un-approved posts (doesn't matter if isn't any)
    # We only need the IDs and the created time
    posts = m.db.posts.find(
        {'user_id': whom_id, 'reply_to': None,
         'permission': {'$lte': k.PERM_PJUU}},
        {'_id': True, 'created': True},
    ).sort('created', -1).limit(5)

    # Iterate the cursor and append the posts to the users feed
    for post in posts:
        timestamp = post.get('created')
        post_id = post.get('_id')
        # Place on the feed
        r.zadd(k.USER_FEED.format(who_id), timestamp, post_id)
        # Trim the feed to the 1000 max
        r.zremrangebyrank(k.USER_FEED.format(who_id), 0, -1000)
Example #3
0
File: alerts.py Project: pjuu/pjuu
    def alert(self, alert, user_ids):
        """Will attempt to alert the user with uid to the alert being managed.

        This will call the alerts before_alert() method, which allows you to
        change the alert per user. It's not needed though.

        """
        # Check that the manager actually has an alert
        if not isinstance(alert, BaseAlert):
            raise ValueError('alert must be a BaseAlert object')

        # Ensure uids is iterable
        # Stopped strings being passed in
        if not isinstance(user_ids, Iterable) or isinstance(user_ids, str) or \
           isinstance(user_ids, unicode):
            raise TypeError('user_ids must be iterable')

        # Create the alert object
        r.set(k.ALERT.format(alert.alert_id), jsonpickle.encode(alert))
        # Set the 4WK timeout on it
        r.expire(k.ALERT.format(alert.alert_id), k.EXPIRE_4WKS)

        for user_id in user_ids:
            r.zadd(k.USER_ALERTS.format(user_id), alert.timestamp,
                   alert.alert_id)
Example #4
0
def vote_comment(uid, cid, amount=1):
    """Handles voting on posts

    """
    # Ensure user has not voted before and ensure its a comment check
    if not has_voted(uid, cid, comment=True):
        author_uid = get_comment_author(cid)
        if author_uid != uid:
            r.zadd(K.COMMENT_VOTES.format(cid), amount, uid)
            # Post scores can be negative.
            # INCRBY with a minus value is the same a DECRBY
            r.hincrby(K.COMMENT.format(cid), 'score', amount=amount)

            # Get the score of the author
            cur_user_score = r.hget(K.USER.format(author_uid), 'score')
            # Stop users scores going lower than 0
            cur_user_score = int(cur_user_score)
            if cur_user_score <= 0 and amount < 0:
                amount = 0

            # Increment the users score
            r.hincrby(K.USER.format(author_uid), 'score', amount=amount)
        else:
            raise CantVoteOnOwn
    else:
        raise AlreadyVoted
Example #5
0
    def alert(self, alert, user_ids):
        """Will attempt to alert the user with uid to the alert being managed.

        This will call the alerts before_alert() method, which allows you to
        change the alert per user. It's not needed though.

        """
        # Check that the manager actually has an alert
        if not isinstance(alert, BaseAlert):
            raise ValueError('alert must be a BaseAlert object')

        # Ensure uids is iterable
        # Stopped strings being passed in
        if not isinstance(user_ids, Iterable) or isinstance(user_ids, str) or \
           isinstance(user_ids, unicode):
            raise TypeError('user_ids must be iterable')

        # Create the alert object
        r.set(k.ALERT.format(alert.alert_id), jsonpickle.encode(alert))
        # Set the 4WK timeout on it
        r.expire(k.ALERT.format(alert.alert_id), k.EXPIRE_4WKS)

        for user_id in user_ids:
            r.zadd(k.USER_ALERTS.format(user_id), alert.timestamp,
                   alert.alert_id)
Example #6
0
def populate_approved_followers_feeds(user_id, post_id, timestamp):
    """Fan out a post_id to all the users approved followers."""
    # Get a list of ALL users who are following a user
    followers = r.zrange(k.USER_APPROVED.format(user_id), 0, -1)
    # This is not transactional as to not hold Redis up.
    for follower_id in followers:
        # Add the pid to the list
        r.zadd(k.USER_FEED.format(follower_id), timestamp, post_id)
        # Stop followers feeds from growing to large, doesn't matter if it
        # doesn't exist
        r.zremrangebyrank(k.USER_FEED.format(follower_id), 0, -1000)
Example #7
0
def approve_user(who_uid, whom_uid):
    """Allow a user to approve a follower"""
    # Check that the user is actually following.
    # Fail if not
    if r.zrank(k.USER_FOLLOWERS.format(who_uid), whom_uid) is None:
        return False

    # Add the user to the approved list
    # No alert is generated
    r.zadd(k.USER_APPROVED.format(who_uid), timestamp(), whom_uid)

    return True
Example #8
0
File: backend.py Project: pjuu/pjuu
def approve_user(who_uid, whom_uid):
    """Allow a user to approve a follower"""
    # Check that the user is actually following.
    # Fail if not
    if r.zrank(k.USER_FOLLOWERS.format(who_uid), whom_uid) is None:
        return False

    # Add the user to the approved list
    # No alert is generated
    r.zadd(k.USER_APPROVED.format(who_uid), timestamp(), whom_uid)

    return True
Example #9
0
def flag_post(user_id, post_id):
    """Flags a post for moderator review.

    :returns: True if flagged, false if removed.
              `CantFlagOwn` in case of error.
    """
    # Get the comment so we can check who the author is
    post = get_post(post_id)

    if post.get('user_id') != user_id:
        if not has_flagged(user_id, post_id):
            # Increment the flag count by one and store the user name
            r.zadd(k.POST_FLAGS.format(post_id), timestamp(), user_id)
            m.db.posts.update({'_id': post_id},
                              {'$inc': {'flags': 1}})
        else:
            raise AlreadyFlagged
    else:
        raise CantFlagOwn
Example #10
0
def follow_user(who_uid, whom_uid):
    """Add whom to who's following zset and who to whom's followers zset.
    Generate an alert for this action.
    """
    # Check that we are not already following the user
    if r.zrank(k.USER_FOLLOWING.format(who_uid), whom_uid) is not None:
        return False

    # Follow user
    # Score is based on UTC epoch time
    r.zadd(k.USER_FOLLOWING.format(who_uid), timestamp(), whom_uid)
    r.zadd(k.USER_FOLLOWERS.format(whom_uid), timestamp(), who_uid)

    # Create an alert and inform whom that who is now following them
    alert = FollowAlert(who_uid)
    AlertManager().alert(alert, [whom_uid])

    # Back fill the who's feed with some posts from whom
    back_feed(who_uid, whom_uid)

    return True
Example #11
0
def follow_user(who_uid, whom_uid):
    """Add whom to who's following zset and who to whom's followers zset.
    Generate an alert for this action.
    """
    # Check that we are not already following the user
    if r.zrank(k.USER_FOLLOWING.format(who_uid), whom_uid) is not None:
        return False

    # Follow user
    # Score is based on UTC epoch time
    r.zadd(k.USER_FOLLOWING.format(who_uid), timestamp(), whom_uid)
    r.zadd(k.USER_FOLLOWERS.format(whom_uid), timestamp(), who_uid)

    # Create an alert and inform whom that who is now following them
    alert = FollowAlert(who_uid)
    AlertManager().alert(alert, [whom_uid])

    # Back fill the who's feed with some posts from whom
    back_feed(who_uid, whom_uid)

    return True
Example #12
0
def vote_post(user_id, post_id, amount=1, ts=None):
    """Handles voting on posts

    :param user_id: User who is voting
    :type user_id: str
    :param post_id: ID of the post the user is voting on
    :type post_id: int
    :param amount: The way to vote (-1 or 1)
    :type amount: int
    :param ts: Timestamp to use for vote (ONLY FOR TESTING)
    :type ts: int
    :returns: -1 if downvote, 0 if reverse vote and +1 if upvote

    """
    if ts is None:
        ts = timestamp()

    # Get the comment so we can check who the author is
    author_uid = get_post(post_id).get('user_id')

    # Votes can ONLY ever be -1 or 1 and nothing else
    # we use the sign to store the time and score in one zset score
    amount = 1 if amount >= 0 else -1

    voted = has_voted(user_id, post_id)

    if not voted:
        if author_uid != user_id:
            # Store the timestamp of the vote with the sign of the vote
            r.zadd(k.POST_VOTES.format(post_id), amount * timestamp(), user_id)

            # Update post score
            m.db.posts.update({'_id': post_id},
                              {'$inc': {'score': amount}})

            # Update user score
            m.db.users.update({'_id': author_uid},
                              {'$inc': {'score': amount}})

            return amount
        else:
            raise CantVoteOnOwn
    elif voted and abs(voted) + k.VOTE_TIMEOUT > ts:
        # No need to check if user is current user because it can't
        # happen in the first place
        # Remove the vote from Redis
        r.zrem(k.POST_VOTES.format(post_id), user_id)

        previous_vote = -1 if voted < 0 else 1

        # Calculate how much to increment/decrement the scores by
        # Saves multiple trips to Mongo
        if amount == previous_vote:
            if previous_vote < 0:
                amount = 1
                result = 0
            else:
                amount = -1
                result = 0
        else:
            # We will only register the new vote if it is NOT a vote reversal.
            r.zadd(k.POST_VOTES.format(post_id), amount * timestamp(), user_id)

            if previous_vote < 0:
                amount = 2
                result = 1
            else:
                amount = -2
                result = -1

        # Update post score
        m.db.posts.update({'_id': post_id},
                          {'$inc': {'score': amount}})

        # Update user score
        m.db.users.update({'_id': author_uid},
                          {'$inc': {'score': amount}})

        return result
    else:
        raise AlreadyVoted
Example #13
0
def create_post(user_id, username, body, reply_to=None, upload=None,
                permission=k.PERM_PUBLIC):
    """Creates a new post

    This handled both posts and what used to be called comments. If the
    reply_to field is not None then the post will be treat as a comment.
    You will need to make sure the reply_to post exists.

    :param user_id: The user id of the user posting the post
    :type user_id: str
    :param username: The user name of the user posting (saves a lookup)
    :type username: str
    :param body: The content of the post
    :type body: str
    :param reply_to: The post id of the post this is a reply to if any
    :type reply_to: str
    :param upload:
    :returns: The post id of the new post
    :param permission: Who can see/interact with the post you are posting
    :type permission: int
    :rtype: str or None

    """
    # Get a new UUID for the post_id ("_id" in MongoDB)
    post_id = get_uuid()
    # Get the timestamp, we will use this to populate users feeds
    post_time = timestamp()

    post = {
        '_id': post_id,             # Newly created post id
        'user_id': user_id,         # User id of the poster
        'username': username,       # Username of the poster
        'body': body,               # Body of the post
        'created': post_time,       # Unix timestamp for this moment in time
        'score': 0,                 # Atomic score counter
    }

    if reply_to is not None:
        # If the is a reply it must have this property
        post['reply_to'] = reply_to
    else:
        # Replies don't need a comment count
        post['comment_count'] = 0
        # Set the permission a user needs to view
        post['permission'] = permission

    # TODO: Make the upload process better at dealing with issues
    if upload:
        # If there is an upload along with this post it needs to go for
        # processing.
        # process_upload() can throw an Exception of UploadError. We will let
        # it fall through as a 500 is okay I think.
        # TODO: Turn this in to a Queue task at some point
        filename = process_upload(upload)

        if filename is not None:
            # If the upload process was okay attach the filename to the doc
            post['upload'] = filename
        else:
            # Stop the image upload process here if something went wrong.
            return None

    # Process everything thats needed in a post
    links, mentions, hashtags = parse_post(body)

    # Only add the fields if we need too.
    if links:
        post['links'] = links

    if mentions:
        post['mentions'] = mentions

    if hashtags:
        post['hashtags'] = hashtags

    # Add the post to the database
    # If the post isn't stored, result will be None
    result = m.db.posts.insert(post)

    # Only carry out the rest of the actions if the insert was successful
    if result:
        if reply_to is None:
            # Add post to authors feed
            r.zadd(k.USER_FEED.format(user_id), post_time, post_id)
            # Ensure the feed does not grow to large
            r.zremrangebyrank(k.USER_FEED.format(user_id), 0, -1000)

            # Subscribe the poster to there post
            subscribe(user_id, post_id, SubscriptionReasons.POSTER)

            # Alert everyone tagged in the post
            alert_tagees(mentions, user_id, post_id)

            # Append to all followers feeds or approved followers based
            # on the posts permission
            if permission < k.PERM_APPROVED:
                populate_followers_feeds(user_id, post_id, post_time)
            else:
                populate_approved_followers_feeds(user_id, post_id, post_time)

        else:
            # To reduce database look ups on the read path we will increment
            # the reply_to's comment count.
            m.db.posts.update({'_id': reply_to},
                              {'$inc': {'comment_count': 1}})

            # Alert all subscribers to the post that a new comment has been
            # added. We do this before subscribing anyone new
            alert = CommentingAlert(user_id, reply_to)

            subscribers = []
            # Iterate through subscribers and let them know about the comment
            for subscriber_id in get_subscribers(reply_to):
                # Ensure we don't get alerted for our own comments
                if subscriber_id != user_id:
                    subscribers.append(subscriber_id)

            # Push the comment alert out to all subscribers
            AlertManager().alert(alert, subscribers)

            # Subscribe the user to the post, will not change anything if they
            # are already subscribed
            subscribe(user_id, reply_to, SubscriptionReasons.COMMENTER)

            # Alert everyone tagged in the post
            alert_tagees(mentions, user_id, reply_to)

        return post_id

    # If there was a problem putting the post in to Mongo we will return None
    return None  # pragma: no cover