def get_alerts(user_id, page=1, per_page=None): """Return a list of alert objects as a pagination. """ if per_page is None: per_page = app.config.get('ALERT_ITEMS_PER_PAGE') # Get the last time the users checked the alerts # Try and cast the value to an int so we can boolean compare them try: alerts_last_checked = m.db.users.find_one({ '_id': user_id }).get('alerts_last_checked') except (AttributeError, TypeError, ValueError): alerts_last_checked = 0 # Get total number of elements in the sorted set total = r.zcard(k.USER_ALERTS.format(user_id)) aids = r.zrevrange(k.USER_ALERTS.format(user_id), (page - 1) * per_page, (page * per_page) - 1) # Create AlertManager to load the alerts am = AlertManager() alerts = [] for aid in aids: # Load the alert in to the alert manager alert = am.get(aid) if alert: # Check to see if the alert is newer than the time we last checked. # This allows us to highlight in the template # This will assign a new property to the object: `new` if int(alert.timestamp) > alerts_last_checked: alert.new = True # Add the entire alert from the manager on the list alerts.append(alert) else: # Self cleaning zset r.zrem(k.USER_ALERTS.format(user_id), aid) total = r.zcard(k.USER_ALERTS.format(user_id)) # May as well delete the alert if there is one r.delete(k.ALERT.format(aid)) # Update the last time the user checked there alerts # This will allow us to alert a user too new alerts with the /i-has-alerts # url m.db.users.update({'_id': user_id}, {'$set': { 'alerts_last_checked': timestamp() }}) return Pagination(alerts, total, page, per_page)
def get_alerts(user_id, page=1, per_page=None): """Return a list of alert objects as a pagination. """ if per_page is None: per_page = app.config.get('ALERT_ITEMS_PER_PAGE') # Get the last time the users checked the alerts # Try and cast the value to an int so we can boolean compare them try: alerts_last_checked = m.db.users.find_one( {'_id': user_id} ).get('alerts_last_checked') except (AttributeError, TypeError, ValueError): alerts_last_checked = 0 # Get total number of elements in the sorted set total = r.zcard(k.USER_ALERTS.format(user_id)) aids = r.zrevrange(k.USER_ALERTS.format(user_id), (page - 1) * per_page, (page * per_page) - 1) # Create AlertManager to load the alerts am = AlertManager() alerts = [] for aid in aids: # Load the alert in to the alert manager alert = am.get(aid) if alert: # Check to see if the alert is newer than the time we last checked. # This allows us to highlight in the template # This will assign a new property to the object: `new` if int(alert.timestamp) > alerts_last_checked: alert.new = True # Add the entire alert from the manager on the list alerts.append(alert) else: # Self cleaning zset r.zrem(k.USER_ALERTS.format(user_id), aid) total = r.zcard(k.USER_ALERTS.format(user_id)) # May as well delete the alert if there is one r.delete(k.ALERT.format(aid)) # Update the last time the user checked there alerts # This will allow us to alert a user too new alerts with the /i-has-alerts # url m.db.users.update({'_id': user_id}, {'$set': {'alerts_last_checked': timestamp()}}) return Pagination(alerts, total, page, per_page)
def alert_tagees(tagees, user_id, post_id): """Creates a new tagging alert from `user_id` and `post_id` and alerts all in the `tagees` list. This will take the tagees processed as `mentions`, it will ensure no duplication and that the poster is not alerted if they tag themselves. :type tagees: list :type user_id: str :type post_id: str """ alert = TaggingAlert(user_id, post_id) seen_user_ids = [] for tagee in tagees: tagged_user_id = tagee.get('user_id') # Don't alert users more than once if tagged_user_id in seen_user_ids: continue # Don't alert posting user to tag if tagged_user_id == user_id: continue # Subscribe the tagee to the post won't change anything if they are # already subscribed subscribe(tagged_user_id, post_id, SubscriptionReasons.TAGEE) seen_user_ids.append(tagged_user_id) # Get an alert manager to notify all tagees AlertManager().alert(alert, seen_user_ids)
def test_alertmanager(self): """ Test the alert manager. Similar to the above a very simple test. Will check that it can alert users and one can be created. """ # Create our alert manager am = AlertManager() # Try and load a non-existant alert self.assertIsNone(am.get(k.NIL_VALUE)) # Try and alert on something which is not an alert self.assertRaises(ValueError, lambda: am.alert('ALERT', k.NIL_VALUE)) # Test that alerting a single users does not work, they need to be # iterable # Create an alert alert = BaseAlert(k.NIL_VALUE) self.assertRaises(TypeError, lambda: am.alert(alert, 1)) # Create a couple of users user1 = create_account('user1', '*****@*****.**', 'Password') user2 = create_account('user2', '*****@*****.**', 'Password') # Ensure the length of user1's alert feed is 0 self.assertEqual(r.zcard(k.USER_ALERTS.format(user1)), 0) # Create an alert from user2 alert = BaseAlert(user2) # Alert user1 am.alert(alert, [user1]) # Ensure the length of user1's alert feed is 1 self.assertEqual(r.zcard(k.USER_ALERTS.format(user1)), 1) # Get alerts for user1, user Redis directly alerts = r.zrange(k.USER_ALERTS.format(user1), 0, 0) # Get the alert from alerts alert = am.get(alerts[0]) self.assertIsNotNone(alert) self.assertEqual(alert.user.get('username'), 'user2') self.assertEqual(alert.user.get('email'), '*****@*****.**') # Delete test2 and ensure getting the alert returns None delete_account(user2) alert = am.get(alerts[0]) self.assertIsNone(alert)
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
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