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 test_validate_ok(requests_mock): user_id = "foo" user_display = "Foo Bar" requests_mock.get('{}/api/V2/users?list={}'.format(cfg.get('feeds', 'auth-url'), user_id), text=json.dumps({user_id: user_display})) note = Notification(user_id, verb_inf, note_object, source) # If this doesn't throw any errors, then it passes! note.validate()
def test_note_new_bad_level(): with pytest.raises(AssertionError) as e: Notification(actor, verb_inf, note_object, source, level=None) assert "level must not be None" in str(e.value) with pytest.raises(MissingLevelError) as e: Notification(actor, verb_inf, note_object, source, level="foobar") assert 'Level "foobar" not found' in str(e.value)
def test_note_new_bad_verb(): with pytest.raises(AssertionError) as e: Notification(actor, None, note_object, source) assert "verb must not be None" in str(e.value) with pytest.raises(MissingVerbError) as e: Notification(actor, "foobar", note_object, source) assert 'Verb "foobar" not found' in str(e.value)
def test_note_new_bad_expires(): bad_expires = ["foo", {}, []] for bad in bad_expires: with pytest.raises(InvalidExpirationError) as e: Notification(actor, verb_inf, note_object, source, expires=bad) assert "Expiration time should be the number of milliseconds" in str(e.value) bad_expires = [123, True, False] for bad in bad_expires: with pytest.raises(InvalidExpirationError) as e: Notification(actor, verb_inf, note_object, source, expires=bad) assert "Notifications should expire sometime after they are created" in str(e.value)
def test_from_dict_missing_keys(): d = { "actor": actor } with pytest.raises(InvalidNotificationError) as e: Notification.from_dict(d) assert "Missing keys" in str(e.value) with pytest.raises(InvalidNotificationError) as e: Notification.from_dict(None) assert "Can only run 'from_dict' on a dict" in str(e.value)
def test_to_dict(): note = Notification(actor, verb_inf, note_object, source, level=level_name) d = note.to_dict() assert d["actor"] == actor_d assert d["verb"] == verb_id assert d["object"] == object_d assert d["source"] == source assert isinstance(d["expires"], int) and d["expires"] == note.expires assert isinstance(d["created"], int) and d["created"] == note.created assert d["target"] == [] assert d["context"] == {} assert d["level"] == level_id assert d["external_key"] is None assert d["users"] == []
def test_user_view(): note = Notification(actor, verb_inf, note_object, source, level=level_id) v = note.user_view() assert v["actor"] == actor_d assert v["verb"] == verb_past assert v["object"] == object_d assert v["source"] == source assert isinstance(v["expires"], int) and v["expires"] == note.expires assert isinstance(v["created"], int) and v["created"] == note.created assert v["target"] == [] assert v["context"] == {} assert v["level"] == level_name assert "external_key" not in v assert "users" not in v
def test_deserialization(): note = Notification(actor, verb_inf, note_object, source, level=level_id, target=target, external_key=external_key, context=context, users=users) serial = note.serialize() note2 = Notification.deserialize(serial) assert note2.id == note.id assert note2.actor == note.actor assert note2.verb.id == note.verb.id assert note2.object == note.object assert note2.source == note.source assert note2.level.id == note.level.id assert note2.target == note.target assert note2.external_key == note.external_key assert note2.context == note.context assert note2.users == note.users
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 test_serialization(): note = Notification(actor, verb_inf, note_object, source, level=level_id) serial = note.serialize() json_serial = json.loads(serial) assert "i" in json_serial assert_is_uuid(json_serial['i']) assert "a" in json_serial and json_serial['a'] == str(actor) assert "v" in json_serial and json_serial['v'] == verb_id assert "o" in json_serial and json_serial['o'] == str(note_object) assert "s" in json_serial and json_serial['s'] == source assert "l" in json_serial and json_serial['l'] == level_id assert "c" in json_serial and json_serial['c'] == note.created assert "e" in json_serial and json_serial['e'] == note.expires assert "n" in json_serial and json_serial['n'] == {} assert "x" in json_serial and json_serial['x'] == None assert "t" in json_serial and json_serial['t'] == [] assert "u" in json_serial and json_serial['u'] == []
def test_note_new_diff_levels(): assert_args = { "actor": actor, "verb_inf": verb_inf, "object": note_object, "source": source } for name in ['alert', 'warning', 'request', 'error']: note = Notification(actor, verb_inf, note_object, source, level=name) test_args = assert_args.copy() test_args['level_name'] = name assert_note_ok(note, **test_args) for id_ in ['1', '2', '3', '4']: note = Notification(actor, verb_inf, note_object, source, level=id_) test_args = assert_args.copy() test_args['level_id'] = id_ assert_note_ok(note, **test_args)
def test_serialization_all_kwargs(): note = Notification(actor, verb_inf, note_object, source, level=level_id, target=target, external_key=external_key, context=context, users=users) serial = note.serialize() json_serial = json.loads(serial) assert "i" in json_serial assert_is_uuid(json_serial['i']) assert "a" in json_serial and json_serial['a'] == str(actor) assert "v" in json_serial and json_serial['v'] == verb_id assert "o" in json_serial and json_serial['o'] == str(note_object) assert "s" in json_serial and json_serial['s'] == source assert "l" in json_serial and json_serial['l'] == level_id assert "c" in json_serial and json_serial['c'] == note.created assert "e" in json_serial and json_serial['e'] == note.expires assert "n" in json_serial and json_serial['n'] == context assert "x" in json_serial and json_serial['x'] == external_key assert "t" in json_serial and json_serial['t'] == [str(target[0])] assert "u" in json_serial and json_serial['u'] == [str(users[0])]
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 add_notification(): """ Adds a new notification for other users to see. Form data requires the following: * `actor` - a user or org id. * `actor_type` - either 'user' or 'group' * `type` - one of the type keywords (see below, TBD (as of 10/8)) * `target` - optional, a user or org id. - always receives this notification * `object` - object of the notice. For invitations, the group to be invited to. For narratives, the narrative UPA. * `level` - alert, error, warning, or request. * `content` - optional, content of the notification, otherwise it'll be autogenerated from the info above. * `global` - true or false. If true, gets added to the global notification feed and everyone gets a copy. * `expires` - int, optional time to expire a notifications. * `source` - string, the source service behind the notification. Used in some logic to determine which feeds receive the notification This also requires a service token as an Authorization header. """ token = get_auth_token(request) try: validate_service_token(token) except InvalidTokenError: if cfg.debug: if not is_feeds_admin(token): raise InvalidTokenError( 'Auth token must be either a Service token ' 'or from a user with the FEEDS_ADMIN role!') else: raise log(__name__, request.get_data()) params = parse_notification_params(json.loads(request.get_data())) # create a Notification from params. new_note = Notification(params.get('actor'), params.get('verb'), params.get('object'), params.get('source'), level=params.get('level'), target=params.get('target', []), context=params.get('context'), expires=params.get('expires'), external_key=params.get('external_key'), users=params.get('users', [])) # pass it to the NotificationManager to dole out to its audience feeds. manager = NotificationManager() manager.add_notification(new_note) # on success, return the notification id and info. return (flask.jsonify({'id': new_note.id}), 200)
def test_deserialize_bad(): with pytest.raises(InvalidNotificationError) as e: Notification.deserialize(None) assert "Can't deserialize an input of 'None'" in str(e.value) with pytest.raises(InvalidNotificationError) as e: Notification.deserialize(json.dumps({'a': actor.to_dict()})) assert "Missing keys" in str(e.value) with pytest.raises(InvalidNotificationError) as e: Notification.deserialize("foo") assert "Can only deserialize a JSON string" in str(e.value)
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_from_dict(): act_id = str(uuid.uuid4()) verb = [verb_id, str(verb_id), verb_inf, verb_past] level = [level_id, level_name, str(level_id)] d = { "actor": actor_d, "object": object_d, "source": source, "expires": 1234567890111, "created": 1234567890000, "target": [target_d], "context": context, "external_key": external_key, "id": act_id, "users": [user_d] } # just being lazy and putting in the real Entity objects d_cmp = { "actor": actor, "object": note_object, "source": source, "expires": 1234567890111, "created": 1234567890000, "target": target, "context": context, "external_key": external_key, "id": act_id, "users": users } for v in verb: for l in level: note_d = d.copy() note_d_cmp = d_cmp.copy() note_d.update({'level': l, 'verb': v}) note_d_cmp.update({'level': l, 'verb': v}) note = Notification.from_dict(note_d) assert_note_ok(note, **note_d_cmp)
def test_note_new_bad_actor(): # TODO: Should only fail on validate - shouldn't do a lookup whenever a new note is made. # also, shouldn't be None. with pytest.raises(AssertionError) as e: Notification(None, verb_inf, note_object, source) assert "actor must not be None" in str(e.value)
def test_note_new_bad_object(): # TODO: Also test object validation itself later. with pytest.raises(AssertionError) as e: Notification(actor, verb_inf, None, source) assert 'note_object must not be None' in str(e.value)
def test_note_new_context(): note = Notification(actor, verb_inf, note_object, source, context=context) assert_note_ok(note, actor=actor, verb_inf=verb_inf, object=note_object, source=source, context=context)
def test_note_new_users(): note = Notification(actor, verb_inf, note_object, source, users=users) assert_note_ok(note, actor=actor, verb_inf=verb_inf, object=note_object, source=source, users=users)
def test_note_new_target(): note = Notification(actor, verb_inf, note_object, source, target=target) assert_note_ok(note, actor=actor, verb_inf=verb_inf, object=note_object, source=source, target=target)
def test_note_new_bad_source(): # TODO: Validate sources as being real. with pytest.raises(AssertionError) as e: Notification(actor, verb_inf, note_object, None) assert 'source must not be None' in str(e.value)
def test_note_new_ok_no_kwargs(): note = Notification(actor, verb_inf, note_object, source) assert_note_ok(note, actor=actor, verb_inf=verb_inf, object=note_object, source=source)
def test_note_new_bad_users(): bad_users = [{}, "foo", 123, False] for bad in bad_users: with pytest.raises(AssertionError) as e: Notification(actor, verb_inf, note_object, source, users=bad) assert "users must be either a list or None" in str(e.value)
def test_note_new_bad_context(): bad_context = [[], "foo", 123, False] for bad in bad_context: with pytest.raises(AssertionError) as e: Notification(actor, verb_inf, note_object, source, context=bad) assert "context must be either a dict or None" in str(e.value)
def test_default_lifespan(): note = Notification(actor, verb_inf, note_object, source) lifespan = int(cfg.get('feeds', 'lifespan')) assert note.expires - note.created == lifespan * 24 * 60 * 60 * 1000
def test_note_new_external_key(): note = Notification(actor, verb_inf, note_object, source, external_key=external_key) assert_note_ok(note, actor=actor, verb_inf=verb_inf, object=note_object, source=source, external_key=external_key)