Ejemplo n.º 1
0
    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)
Ejemplo n.º 2
0
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)
Ejemplo n.º 3
0
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)
Ejemplo n.º 4
0
    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))
Ejemplo n.º 5
0
    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)
Ejemplo n.º 6
0
    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)
Ejemplo n.º 7
0
    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)
Ejemplo n.º 8
0
    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()))
Ejemplo n.º 9
0
        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
Ejemplo n.º 10
0
    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)
Ejemplo n.º 11
0
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)
Ejemplo n.º 12
0
    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())
Ejemplo n.º 13
0
    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)
Ejemplo n.º 14
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)
Ejemplo n.º 15
0
    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
Ejemplo n.º 16
0
    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')))
Ejemplo n.º 17
0
 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))
Ejemplo n.º 18
0
    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')
Ejemplo n.º 19
0
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)
Ejemplo n.º 20
0
 def setUp(self):
     self.node = get_node_for_object(BookFixture(Book).create_one())
Ejemplo n.º 21
0
 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')