def test_update_raw_node_relationship(self): group1, group2 = Group.objects.create( name='group1'), Group.objects.create(name='group2') node1, node2 = get_node_for_object(group1), get_node_for_object(group2) # Create a custom relationship between group1 and group2 results, _ = db.cypher_query( 'MATCH (a), (b) WHERE ID(a) = %d AND ID(b) = %d ' 'CREATE (a)-[r1:RELATION]->(b), (b)-[r2:RELATION]->(a) ' 'RETURN r1, r2' % (node1.id, node2.id)) results = list(flatten(results)) self.assertEqual(len(results), 2) self.assertTrue(all([r.type == 'RELATION' for r in results])) node1.__update_raw_node__() # Make sure custom relationship is deleted results, _ = db.cypher_query( 'MATCH (n)-[r]->() WHERE ID(n) = %d RETURN r' % node1.id) self.assertEqual(len(results), 0) # The other relationship should still be intact results, _ = db.cypher_query( 'MATCH (n)-[r]->() WHERE ID(n) = %d RETURN r' % node2.id) results = list(flatten(results)) self.assertEqual(len(results), 1)
def post_save_handler(sender, instance, **kwargs): """ Keep the graph model in sync with the model. """ if settings.ENABLED is True: if not get_model_string(instance._meta.model) in settings.IGNORE_MODELS: get_node_for_object(instance).sync(max_depth=settings.MAX_CONNECTION_DEPTH, update_existing=True)
def post_save_handler(sender, instance, **kwargs): """ Sync the node instance after it has been saved. """ if settings.ENABLED: get_node_for_object(instance, bind=False).sync( max_depth=settings.MAX_CONNECTION_DEPTH, update_existing=True)
def test_multiple_permissions_to_check_requires_staff(self): groups = Group.objects.bulk_create( [Group(name=name) for name in ['group1', 'group2', 'group3']]) access_rule = AccessRule.objects.create( ctype_source=utils.get_content_type(User), ctype_target=utils.get_content_type(Group), requires_staff=True, relation_types=[{ 'GROUPS': None }]) perms = Permission.objects.filter( content_type__app_label='auth', codename__in=['add_group', 'delete_group']) access_rule.permissions.add(*perms) self.user1.user_permissions.add(*perms) self.user1.groups.add(*groups) self.user1.is_staff = True get_node_for_object(self.user1).sync( ) # Sync node in order to write `is_staff` property objects = utils.get_objects_for_user( self.user1, ['auth.add_group', 'auth.delete_group']) self.assertEqual(set(groups), set(objects)) self.user2.user_permissions.add(*perms) self.user2.groups.add(*groups) self.assertFalse(self.user2.is_staff) objects = utils.get_objects_for_user( self.user2, ['auth.add_group', 'auth.delete_group']) self.assertEqual(set(), set(objects))
def test_ignored_model_sync(self): group = Group.objects.create(name='group') get_node_for_object(group).sync() klass = get_node_class_for_model(Group) try: klass.nodes.get(pk=group.pk) self.fail( 'Did not fail when trying to look up an ignored node class.') except klass.DoesNotExist as e: self.assertEqual(str(e), "{'pk': %d}" % group.pk)
def test_save_existing_node_is_updated(self): group = Group.objects.create(name='a group') node1 = get_node_for_object(group) self.assertEqual(group.name, node1.name) group.name = 'still the same group' group.save() node2 = get_node_for_object(group) self.assertEqual(group.name, node2.name) self.assertEqual(node1.id, node2.id)
def test_related_ignore_model_sync(self): user = User.objects.create_user(username='******', password='******') group = Group.objects.create(name='group') user.groups.add(group) get_node_for_object(user).sync() klass = get_node_class_for_model(Group) try: klass.nodes.get(pk=group.pk) self.fail( 'Did not fail when trying to look up an ignored node class.') except klass.DoesNotExist as e: self.assertEqual(str(e), "{'pk': %d}" % group.pk)
def test_nonexistent_source_node(self): user = User.objects.create_user(username='******') node = get_node_for_object(user).sync() node.delete() objects = utils.get_objects_for_user(user, ['testapp.add_book']) self.assertEqual(set(objects), set(Book.objects.none()))
def create_nodes(): book = BookFixture(Book).create_one() node = get_node_for_object(book) assert isinstance(node, StructuredNode) klass = get_node_class_for_model(Book) assert len(klass.nodes.all()) == 1
def test_sync_existing_node_is_updated(self): group = Group.objects.create(name='a group') node = get_node_for_object(group) self.assertEqual(group.name, node.name) group.name = 'another name' group.save() node.sync() self.assertEqual(group.name, node.name)
def m2m_changed_handler(sender, instance, action, reverse, model, pk_set, **kwargs): """ Update relations for the node when m2m relationships are added or deleted. """ if settings.ENABLED: if action not in ('post_add', 'post_remove', 'post_clear'): return if action == 'post_add': get_nodeset_for_queryset(model.objects.filter(pk__in=pk_set), sync=True, max_depth=settings.MAX_CONNECTION_DEPTH) elif action == 'post_remove': get_nodeset_for_queryset(model.objects.filter(pk__in=pk_set), sync=True, max_depth=settings.MAX_CONNECTION_DEPTH) elif action == 'post_clear': get_node_for_object(instance, bind=False).sync( max_depth=settings.MAX_CONNECTION_DEPTH, update_existing=True)
def test_sync_related_branch(self): queryset = Store.objects.filter( pk__in=map(lambda n: n.pk, StoreFixture(Store).create(count=2, commit=True))) store_nodeset = get_nodeset_for_queryset(queryset, sync=True, max_depth=1) for store in store_nodeset: store_obj = store.get_object() if store_obj.bestseller: self.assertEqual(store.bestseller.get(), get_node_for_object(store_obj.bestseller)) self.assertEqual(len(store.books.all()), store_obj.books.count()) for book in store.books.all(): book_obj = book.get_object() self.assertTrue(store in book.store_set.all()) self.assertEqual(book.publisher.get(), get_node_for_object(book_obj.publisher)) self.assertEqual(len(book.store_set.all()), book_obj.store_set.count()) self.assertEqual(len(book.bestseller_stores.all()), book_obj.bestseller_stores.count()) self.assertEqual(len(book.authors.all()), book_obj.authors.count()) # We have currently some issues with recursive max_depth. See #7 # FIXME: workaround - Sync nodeset for related queryset. get_nodeset_for_queryset(book_obj.authors.all(), sync=True, max_depth=1) for author in book.authors.all(): author_obj = author.get_object() self.assertTrue(book in author.book_set.all()) user = author.user.get() self.assertEqual( user, get_node_for_object(author_obj.user).sync()) self.assertEqual(author, user.author.get())
def test_flush_nodes_context_manager(self): with flush_nodes(): book = BookFixture(Book).create_one() node = get_node_for_object(book) self.assertIsInstance(node, StructuredNode) klass = get_node_class_for_model(Book) self.assertEqual(len(klass.nodes.all()), 1) self.assertEqual(len(klass.nodes.all()), 0)
def recursive_connect(self, prop, relation, max_depth, instance=None): """ Recursively connect a related branch. :param prop: For example a ``ZeroOrMore`` instance. :param relation: ``RelationShipDefinition`` instance :param instance: Optional django model instance. :param max_depth: Maximum depth of recursive connections to be made. :returns: None """ from chemtrails.neoutils import get_node_for_object def back_connect(n, depth): if n._recursion_depth >= depth: return n._recursion_depth += 1 for p, r in n.defined_properties(aliases=False, properties=False).items(): n.recursive_connect(getattr(n, p), r, max_depth=n._recursion_depth - 1) # We require a model instance to look for filter values. instance = instance or self.get_object(self.pk) if not instance or not hasattr(instance, prop.name): return klass = relation.definition['node_class'] source = getattr(instance, prop.name) if isinstance(source, models.Model): node = klass.nodes.get_or_none(pk=source.pk) if not node: node = get_node_for_object(source).sync(update_existing=True) prop.connect(node) back_connect(node, max_depth) elif isinstance(source, Manager): nodeset = klass.nodes.filter( pk__in=list(source.values_list('pk', flat=True))) for node in nodeset: prop.connect(node) back_connect(node, max_depth)
def get_statement(self, instance=None): if not isinstance(instance, self._meta.model): return '' # TODO: This should be DRY'ed up! fake_model = instance.ctype_source.model_class()(pk=0) manager = get_node_for_object(fake_model, bind=False).paths if instance.direction is not None: manager.direction = instance.direction error_message = _( 'Unable to validate cypher statement.\nError was: "%(error)s".') query = None for n, rule_definition in enumerate(instance.relation_types_obj): relation_type, target_props = zip(*rule_definition.items()) relation_type, target_props = relation_type[0], target_props[0] source_props = {} if n == 0 and instance.requires_staff: source_props.update({'is_staff': True}) try: manager = manager.add(relation_type, source_props=source_props, target_props=target_props) except (ValueError, AttributeError) as e: return error_message % {'error': e} if manager.statement: query = manager.get_path() try: if query: return query # FIXME: Bug in libcypher-parser-python # https://github.com/inonit/libcypher-parser-python/issues/1 # validate_cypher(query, raise_exception=True, exc_class=ValidationError) except ValidationError as e: return error_message % {'error': e} return query
def test_update_raw_node_property(self): group = Group.objects.create(name='group') node = get_node_for_object(group) # Set a custom attribute on the node and make sure it's saved on the node result, _ = list( flatten( db.cypher_query('MATCH (n) WHERE ID(n) = %d ' 'SET n.foo = "bar", n.baz = "qux" RETURN n' % node.id))) self.assertTrue(all(i in result.properties for i in ('foo', 'baz'))) self.assertEqual(result.properties['foo'], 'bar') self.assertEqual(result.properties['baz'], 'qux') self.assertFalse(all(hasattr(node, i) for i in ('foo', 'baz'))) node.__update_raw_node__() # Make sure the custom attribute has been deleted result, _ = list( flatten( db.cypher_query('MATCH (n) WHERE ID(n) = %d RETURN n' % node.id))) self.assertFalse(all(i in result.properties for i in ('foo', 'baz')))
def test_get_node_for_object(self): store = StoreFixture(Store).create_one(commit=True) store_node = get_node_for_object(store) self.assertIsInstance(store_node, get_node_class_for_model(Store))
def test_recursive_connect(self): post_save.disconnect( post_save_handler, dispatch_uid='chemtrails.signals.handlers.post_save_handler') m2m_changed.disconnect( m2m_changed_handler, dispatch_uid='chemtrails.signals.handlers.m2m_changed_handler') try: book = BookFixture(Book, generate_m2m={ 'authors': (1, 1) }).create_one() for depth in range(3): db.cypher_query( 'MATCH (n)-[r]-() WHERE n.type = "ModelNode" DELETE r' ) # Delete all relationships book_node = get_node_for_object(book).save() book_node.recursive_connect(depth) if depth == 0: # Max depth 0 means that no recursion should occur, and no connections # can be made, because the connected objects might not exist. for prop in book_node.defined_properties( aliases=False, properties=False).keys(): relation = getattr(book_node, prop) try: self.assertEqual(len(relation.all()), 0) except CardinalityViolation: # Will raise CardinalityViolation for nodes which has a single # required relationship continue elif depth == 1: self.assertEqual( 0, len( get_node_class_for_model(Book).nodes.has( store_set=True))) self.assertEqual( 0, len( get_node_class_for_model(Store).nodes.has( books=True))) self.assertEqual( 0, len( get_node_class_for_model(Book).nodes.has( bestseller_stores=True))) self.assertEqual( 0, len( get_node_class_for_model(Store).nodes.has( bestseller=True))) self.assertEqual( 1, len( get_node_class_for_model(Book).nodes.has( publisher=True))) self.assertEqual( 1, len( get_node_class_for_model(Publisher).nodes.has( book_set=True))) self.assertEqual( 1, len( get_node_class_for_model(Book).nodes.has( authors=True))) self.assertEqual( 1, len( get_node_class_for_model(Author).nodes.has( book_set=True))) self.assertEqual( 0, len( get_node_class_for_model(Author).nodes.has( user=True))) self.assertEqual( 0, len( get_node_class_for_model(User).nodes.has( author=True))) self.assertEqual( 1, len( get_node_class_for_model(Book).nodes.has( tags=True))) self.assertEqual( 0, len( get_node_class_for_model(Tag).nodes.has( content_type=True))) elif depth == 2: self.assertEqual( 1, len( get_node_class_for_model(Author).nodes.has( user=True))) self.assertEqual( 1, len( get_node_class_for_model(User).nodes.has( author=True))) self.assertEqual( 1, len( get_node_class_for_model(Tag).nodes.has( content_type=True))) self.assertEqual( 1, len( get_node_class_for_model(ContentType).nodes.has( content_type_set_for_tag=True))) finally: post_save.connect( post_save_handler, dispatch_uid='chemtrails.signals.handlers.post_save_handler') m2m_changed.connect( m2m_changed_handler, dispatch_uid='chemtrails.signals.handlers.m2m_changed_handler')
def get_users_with_perms(obj, permissions, with_superusers=False, with_group_users=True): """ Returns a queryset of all ``User`` objects which there can be calculated a path from the given ``obj``. :param obj: model instance. :param permissions: Single permission string, or sequence of permissions strings that user requires to have. :param with_superusers: Default: ``False``. If set to ``True`` result would include all superusers. :param with_group_users: Default: ``True``. If set to ``False`` result would **not** include users which has only group permissions for given ``obj``. :raises MixedContentTypeError: If computed content type for ``permissions`` and/or ``obj`` clashes. :returns: Queryset containing ``User`` objects which has ``permissions`` for ``obj``. """ ctype, codenames = check_permissions_app_label(permissions) if ctype is None: ctype = get_content_type(obj) if codenames: # Make sure permissions are valid. _codenames = set( ctype.permission_set.filter( codename__in=codenames).values_list('codename', flat=True)) if not codenames == _codenames: message = ngettext_lazy( 'Calculated content type from permission "%s" does not match %r.' % (next(iter(codenames)), ctype), 'One or more permissions "%s" from calculated content type does not match %r.' % (', '.join(sorted(codenames)), ctype), len(codenames)) raise MixedContentTypeError(message) elif not ctype == get_content_type(obj): raise MixedContentTypeError( 'Calculated content type %r does not match %r.' % (ctype, get_content_type(obj))) queryset = _get_queryset(User) # If there is no node in the graph for ``obj``, return empty queryset. target_node = get_node_class_for_model(obj).nodes.get_or_none( **{'pk': obj.pk}) if not codenames or not target_node: if with_superusers is True: return queryset.filter(is_superuser=True) return queryset.none() ctype_source = get_content_type(User) # We need a fake source content type model to use as origin. fake_model = ctype_source.model_class()() source_node = get_node_for_object(fake_model, bind=False) queries = [] for access_rule in get_access_rules(ctype_source, ctype, codenames): manager = source_node.paths if access_rule.direction is not None: manager.direction = access_rule.direction for n, rule_definition in enumerate(access_rule.relation_types_obj): relation_type, target_props = zip(*rule_definition.items()) relation_type, target_props = relation_type[0], target_props[0] source_props = {} target_props = target_props or {} if n == 0 and access_rule.requires_staff: source_props.update({'is_staff': True}) # Make sure the last object in the query is matched to ``obj``. if n == len(access_rule.relation_types_obj) - 1: target_props['pk'] = target_node.pk # FIXME: Workaround for https://github.com/inonit/django-chemtrails/issues/46 # If using "{source}.<attr>" filters, ignore them! target_props = { key: value for key, value in target_props.items() if isinstance(value, str) and not value.startswith('{source}.') } manager = manager.add(relation_type, source_props=source_props, target_props=target_props) if manager.statement: queries.append(manager.get_path()) q_values = Q() if with_superusers is True: q_values |= Q(is_superuser=True) start_node_class = get_node_class_for_model(queryset.model) end_node_class = get_node_class_for_model(obj) for query in queries: # FIXME: https://github.com/inonit/libcypher-parser-python/issues/1 # validate_cypher(query, raise_exception=True) result, _ = db.cypher_query(query) if result: values = set() for item in flatten(result): if not isinstance(item, Path): # pragma: no cover continue elif (start_node_class.__label__ not in item.start.labels or end_node_class.__label__ not in item.end.labels): continue try: start, end = (start_node_class.inflate(item.start), end_node_class.inflate(item.end)) if isinstance(start, start_node_class) and end == target_node: # Make sure the user object has correct permissions instance = start.get_object() global_perms = set( get_perms(instance, obj) if with_group_users else get_user_perms(instance, obj)) if all((code in global_perms for code in codenames)): values.add(item.start.properties['pk']) except (KeyError, InflateError, ObjectDoesNotExist): continue q_values |= Q(pk__in=values) if not q_values: return queryset.none() return queryset.filter(q_values)
def setUp(self): self.node = get_node_for_object(BookFixture(Book).create_one())
def test_path_manager_build_query_with_invalid_relationship(self): node = get_node_for_object(BookFixture(Book).create_one()) self.assertRaisesMessage( AttributeError, '<BookNode: {id}> has no relation type FOOBAR'.format(id=node.id), node.paths.add, 'FOOBAR')