def __init__(self, user_id: str, user_type: str, token: str = None): self.user = Entity(user_id, user_type, token=token) self.token = token self.timeline_storage = MongoTimelineStorage(user_id, user_type) self.activity_storage = MongoActivityStorage() self.timeline = None self.cache = TTLCache(1000, 600)
def test_entity_fetch_name_user(mock_valid_users): user_name = "Fake User" user_id = "fake" mock_valid_users({user_id: user_name}) e = Entity(user_id, "user") e._fetch_name() assert e.name == user_name
def deserialize(cls, serial: str, token: str = None) -> N: """ Deserializes and returns a new Notification instance. """ try: assert serial except AssertionError: raise InvalidNotificationError( "Can't deserialize an input of 'None'") try: struct = json.loads(serial) except json.JSONDecodeError: raise InvalidNotificationError( "Can only deserialize a JSON string") required_keys = set(['a', 'v', 'o', 's', 'l', 't', 'c', 'i', 'e']) missing_keys = required_keys.difference(struct.keys()) if missing_keys: raise InvalidNotificationError( 'Missing keys: {}'.format(missing_keys)) users = [Entity.from_str(u, token=token) for u in struct.get('u', [])] target = [Entity.from_str(t, token=token) for t in struct.get('t', [])] deserial = cls(Entity.from_str(struct['a'], token=token), str(struct['v']), Entity.from_str(struct['o'], token=token), struct['s'], level=str(struct['l']), target=target, context=struct.get('n'), external_key=struct.get('x'), users=users) deserial.created = struct['c'] deserial.id = struct['i'] deserial.expires = struct['e'] return deserial
def test_entity_to_dict(e_id, e_type, name): e = Entity(e_id, e_type, name=name) d = {"id": e_id, "type": e_type} d_name = d.copy() d_name["name"] = name assert e.to_dict() == d assert e.to_dict(with_name=True) == d_name
def update_entity_names(notes: List[N], token: str = None) -> None: entities = list() for n in notes: entities.append(n.actor) entities.append(n.object) entities = entities + n.target entities = entities + n.users Entity.fetch_entity_names(entities, token)
def test_entity_validate_workspace_ok(mock_workspace_info): ws_id = 5 info = [ 5, "A_Workspace", "owner", "Timestamp", 18, "a", "n", "unlocked", {} ] mock_workspace_info(info) e = Entity(ws_id, "workspace") assert e.validate() is True
def test_entity_validate_narrative_ok(mock_workspace_info): ws_id = 5 info = [ 5, "A_Workspace", "owner", "Timestamp", 18, "a", "n", "unlocked", { "narrative": "1", "narrative_nice_name": "My Narrative" } ] mock_workspace_info(info) e = Entity(ws_id, "narrative") assert e.validate() is True
def parse_notification_params(params: dict, is_global: bool=False) -> dict: """ Parses and verifies all the notification params are present. Raises a MissingParameter error otherwise. Returns the params after parsing (currently does nothing, but if transformations are needed in the future, here's where that happens). In total, can raise: MissingParameterError (if required params are missing) IllegalParameterError (if the wrong types are present) EntityValidationError (if an Entity is malformed) """ # * `actor` - an Entity structure - gets turned into an Entity object when returned # * `type` - one of the type keywords # * `target` - optional, a list of Entity structures. This gets turned into a list of # entity object on return # * `object` - object of the notice, an Entity structure. For invitations, the group to be # invited to. For narratives, the narrative UPA. Gets turned into an Entity object # when returned # * `users` - a list of Entity objects, should be of either type user or group # * `level` - alert, error, warning, or request. # * `context` - optional, context of the notification, otherwise it'll be # autogenerated from the info above. if not isinstance(params, dict): raise IllegalParameterError('Expected a JSON object as an input.') required_list = ['verb', 'level'] if not is_global: required_list = required_list + ['actor', 'source', 'object'] missing = [r for r in required_list if r not in params or params.get(r) is None] if missing: raise MissingParameterError("Missing parameter{} - {}".format( "s" if len(missing) > 1 else '', ", ".join(missing) )) if not is_global: # do the entity transformations # If there are any EntityValidationErrors, they'll pop on up. params["actor"] = Entity.from_dict(params["actor"]) params["object"] = Entity.from_dict(params["object"]) if "target" in params: target = params["target"] if isinstance(target, list): params["target"] = [Entity.from_dict(t) for t in target] else: raise IllegalParameterError("Expected target to be a list of Entity structures.") if "users" in params: users = params["users"] if isinstance(users, list): params["users"] = [Entity.from_dict(u) for u in users] else: raise IllegalParameterError("Expected users to be a list of Entity structures.") return params
def test_entity_from_dict(): d = {"id": "foo", "type": "user"} e = Entity.from_dict(d) assert e.to_dict() == d assert e.id == d['id'] assert e.type == d['type'] d = {"id": "bar", "type": "group", "name": "Bar Group"} e = Entity.from_dict(d) assert e.to_dict(with_name=True) == d assert e.id == d['id'] assert e.type == d['type'] assert e.name == d['name']
def test_entity_init_autovalidate(mock_valid_users): user_id = "fake" user_name = "Fake User" mock_valid_users({user_id: user_name}) e = Entity(user_id, "user", auto_validate=True) assert e.id == user_id assert e.type == "user"
def test_entity_from_str(): eid = "foo" etype = "user" s = etype + STR_SEPARATOR + eid e = Entity.from_str(s) assert e.id == eid assert e.type == etype
def add_global_notification(): token = get_auth_token(request) if not is_feeds_admin(token): raise InvalidTokenError( "You do not have permission to create a global notification!") params = parse_notification_params(json.loads(request.get_data()), is_global=True) new_note = Notification(Entity('kbase', 'admin'), params.get('verb'), Entity('kbase', 'admin'), 'kbase', level=params.get('level'), context=params.get('context'), expires=params.get('expires')) global_feed = NotificationFeed(cfg.global_feed, cfg.global_feed_type) global_feed.add_notification(new_note) return (flask.jsonify({'id': new_note.id}), 200)
def test_entity_hash(): """ Don't really care what the hash is, as long as: 1. it's an integer 2. it's consistent """ eid = "foo" etype = "user" eid2 = "bar" etype2 = "group" e1 = Entity(eid, etype) e1_copy = Entity(eid, etype) e2 = Entity(eid2, etype2) e3 = Entity(eid, etype2) assert isinstance(hash(e1), int) assert hash(e1) == hash(e1) assert hash(e1) == hash(e1_copy) assert hash(e1) != hash(e2) assert hash(e1) != hash(e3)
def test_entity_fetch_name_narr(mock_workspace_info): ws_id = 7 info = [ 7, "A_Workspace", "owner", "Timestamp", 18, "a", "n", "unlocked", { "narrative": "1", "narrative_nice_name": "My Narrative" } ] mock_workspace_info(info) e = Entity(ws_id, "narrative") assert e.name == info[8]["narrative_nice_name"]
def test_entity_validate_user_ok(mock_valid_users): mock_valid_users({"fake": "Fake User", "some_admin": "admin"}) e = Entity("fake", "user") assert e.validate() is True e = Entity("some_admin", "admin") assert e.validate() is True
def test_entity_fetch_name_ws(mock_workspace_info): ws_id = 6 info = [ 6, "A_Workspace", "owner", "Timestamp", 18, "a", "n", "unlocked", { "narrative": "1", "narrative_nice_name": "My Narrative" } ] mock_workspace_info(info) e = Entity(ws_id, "workspace") # assert e.name == info[1] # workspace == narrative now. so test that name. assert e.name == info[8]['narrative_nice_name']
def from_dict(cls, serial: dict, token: str = None) -> N: """ Returns a new Notification from a serialized dictionary (e.g. used in Mongo) """ try: assert serial is not None and isinstance(serial, dict) except AssertionError: raise InvalidNotificationError( "Can only run 'from_dict' on a dict.") required_keys = set([ 'actor', 'verb', 'object', 'source', 'level', 'created', 'expires', 'id' ]) missing_keys = required_keys.difference(set(serial.keys())) if missing_keys: raise InvalidNotificationError( 'Missing keys: {}'.format(missing_keys)) deserial = cls(Entity.from_dict(serial['actor'], token=token), str(serial['verb']), Entity.from_dict(serial['object'], token=token), serial['source'], level=str(serial['level']), target=[ Entity.from_dict(t, token=token) for t in serial.get('target', []) ], context=serial.get('context'), external_key=serial.get('external_key'), seen=serial.get('seen', False), users=[ Entity.from_dict(u, token=token) for u in serial.get('users', []) ]) deserial.created = serial['created'] deserial.expires = serial['expires'] deserial.id = serial['id'] return deserial
def set_seen(self, act_ids: List[str], user: Entity) -> None: """ Setting seen just means removing the user from the list of unseens. The query should find all docs in the list of act_ids, where the user is in the list of users, AND the list of unseens. The update should remove the user from the list of unseens. """ u = user.to_dict() coll = get_feeds_collection() coll.update_many({ 'id': { '$in': act_ids }, 'users': u, 'unseen': u }, {'$pull': { 'unseen': u }})
def set_unseen(self, act_ids: List[str], user: Entity) -> None: """ Setting unseen means adding the user to the list of unseens. But we should only do that for docs that the user can't see anyway, so put that in the query. """ u = user.to_dict() coll = get_feeds_collection() coll.update_many( { 'id': { '$in': act_ids }, 'users': u, 'unseen': { '$nin': [u] } }, {'$addToSet': { 'unseen': u }})
def test_entity_eq(): eid1 = "foo" eid2 = "bar" etype1 = "user" etype2 = "group" e11 = Entity(eid1, etype1) e11_2 = Entity(eid1, etype1) e12 = Entity(eid1, etype2) e21 = Entity(eid2, etype1) e22 = Entity(eid2, etype2) assert e11 == e11 assert e11 == e11_2 assert e12 != e11 assert e11 != e12 assert e11 != e22 assert e21 != e11 assert e22 == Entity.from_dict({"id": eid2, "type": etype2})
def test_entity_validate_narrative_fail(mock_workspace_info_invalid): ws_id = 5 mock_workspace_info_invalid(ws_id) e = Entity(ws_id, "narrative") assert e.validate() is False
def test_entity_validate_group_ok(mock_valid_group): g_id = "mygroup" mock_valid_group(g_id) e = Entity(g_id, "group") assert e.validate() is True
def test_entity_init_autovalidate_fail(mock_invalid_user): user_id = "bad_user" mock_invalid_user(user_id) with pytest.raises(EntityValidationError) as e: Entity(user_id, "user", auto_validate=True) assert "Entity of type user with id {} is not valid".format(user_id)
class NotificationFeed(BaseFeed): def __init__(self, user_id: str, user_type: str, token: str = None): self.user = Entity(user_id, user_type, token=token) self.token = token self.timeline_storage = MongoTimelineStorage(user_id, user_type) self.activity_storage = MongoActivityStorage() self.timeline = None self.cache = TTLCache(1000, 600) def _update_timeline(self) -> None: """ Updates a local user timeline cache. This is a list of activity ids that are used for fetching from activity storage (for now). Sorted by newest first. TODO: add metadata to timeline storage - type and verb, first. """ logging.getLogger(__name__).info('Fetching timeline for '.format( self.user)) self.timeline = self.timeline_storage.get_timeline() def get_group_notifications(self, group: Dict[str, str], count: int = 10, include_seen: bool = False, level=None, verb=None, reverse: bool = False) -> dict: """ Returns all notifications (using the get_notifications fn.) for a user, filtered down to those that reference an Entity of type group with the given id. """ notes = self.get_notifications(count=count, include_seen=include_seen, level=level, verb=verb, reverse=reverse) # group has id and name keys group_notes = {"unseen": 0, "name": group.get("name"), "feed": list()} gid = group["id"] def is_group(e: Entity) -> bool: return e.id == gid and e.type == "group" notes_list = list() for n in notes["feed"]: if is_group(n.actor) or is_group(n.object) or any( [is_group(t) for t in n.target]): notes_list.append(n) if not n.seen: group_notes["unseen"] += 1 Notification.update_entity_names(notes_list, token=self.token) for n in notes_list: group_notes["feed"].append(n.user_view()) return group_notes def get_notifications(self, count: int = 10, include_seen: bool = False, level=None, verb=None, reverse: bool = False, user_view: bool = False) -> dict: """ Fetches all activities matching the requested inputs. :param count: max number of most recent notifications to return. default=10 :param include_seen: include notifications that have been seen in the response. default = False :param level: if not None, will only return notifications of the given level. default = None :param verb: if not None, will only return notifications made with the given verb. default = None :param reverse: if True, will reverse the order of the result (default False) :param user_view: if True, will return the user_view dict version of each Notification object. If False, will return a list of Notification objects instead. default False :return: a dict with the requested notifications, and a key with the total number in the feed that are marked unseen :rtype: dict :raises ValueError: if count <= 0 """ activities = self.get_activities(count=count, include_seen=include_seen, verb=verb, level=level, reverse=reverse, user_view=user_view) ret_struct = { "unseen": self.get_unseen_count(), "name": self.user.name } if user_view: ret_struct["feed"] = list() Notification.update_entity_names(activities, token=self.token) for act in activities: ret_struct["feed"].append(act.user_view()) else: ret_struct["feed"] = activities return ret_struct def get_notification(self, note_id): """ Returns a single notification. If it doesn't exist (either the user can't see it, or it's really not there), raises a NotificationNotFoundError. """ note = self.timeline_storage.get_single_activity_from_timeline(note_id) if note is None: raise NotificationNotFoundError( "Cannot find notification with id {}.".format(note_id)) else: return Notification.from_dict(note, self.token) def get_activities(self, count=10, include_seen=False, level=None, verb=None, reverse=False, user_view=False) -> List[Notification]: """ Returns a selection of activities. :param count: Maximum number of Notifications to return (default 10) """ # steps. # 0. If in cache, return them. <-- later # 1. Get storage adapter. # 2. Query it for recent activities from this user. # 3. Cache them here. # 4. Return them. if count < 1 or not isinstance(count, int): raise ValueError("Count must be an integer > 0") serial_notes = self.timeline_storage.get_timeline( count=count, include_seen=include_seen, level=level, verb=verb, reverse=reverse) note_list = list() user_dict = self.user.to_dict() for note in serial_notes: if user_dict in note["unseen"]: note["seen"] = False else: note["seen"] = True note_list.append(Notification.from_dict(note, self.token)) return note_list def mark_activities(self, activity_ids: List[str], seen=False) -> None: """ Marks the given list of activities as either seen (True) or unseen (False). If the owner of this feed is not on the users list for an activity, nothing is changed for that activity. """ if seen: self.activity_storage.set_seen(activity_ids, self.user) else: self.activity_storage.set_unseen(activity_ids, self.user) def add_notification(self, note) -> None: return self.add_activity(note) def add_activity(self, note) -> None: """ Adds an activity to this user's feed """ self.activity_storage.add_to_storage(note, [self.user]) def get_unseen_count(self) -> int: """ Returns the number of unread / unexpired notifications in this feed. """ return self.timeline_storage.get_unseen_count()
def test_entity_validate_workspace_false(mock_workspace_info_invalid): ws_id = 5 mock_workspace_info_invalid(ws_id) e = Entity(ws_id, "workspace") assert e.validate() is False
def test_entity_validate_workspace_fail(mock_workspace_info_error): ws_id = 5 mock_workspace_info_error(ws_id) e = Entity(ws_id, "workspace") assert e.validate() is False
def test_entity_init_ok(e_id, e_type): e = Entity(e_id, e_type) assert e.id == e_id assert e.type == e_type
assert_is_uuid, test_config ) from feeds.exceptions import ( MissingVerbError, MissingLevelError, InvalidExpirationError, InvalidNotificationError ) from feeds.entity.entity import Entity cfg = test_config() # some dummy "good" inputs for testing actor_d = {"id": "test_actor", "type": "user"} actor = Entity.from_dict(actor_d) verb_inf = "invite" verb_past = "invited" verb_id = 1 object_d = {"id": "foo", "type": "workspace"} note_object = Entity.from_dict(object_d) source = "groups" level_name = "warning" level_id = 2 target_d = {"id": "target_actor", "type": "user"} target = [Entity.from_dict(target_d)] context = {"some": "context"} expires = epoch_ms() + (10 * 24 * 60 * 60 * 1000) # 10 days external_key = "an_external_key" user_d = {"id": "user_actor", "type": "user"} users = [Entity.from_dict(user_d)]
def get_target_users(self): cfg = get_config() return [Entity(cfg.global_feed, cfg.global_feed_type)]
def __init__(self, user_id, user_type): assert user_id assert user_type self.user_id = user_id self.user_type = user_type # should align with entity types self.user = Entity(user_id, user_type)