跳转到主要内容

用于定义和查询对象之间复杂关系的工具

项目描述

简介

定义和查询对象之间复杂关系的工具。此产品基于并需要zc.relationship和five.intid。

致谢

作者:Alec Mitchell <apm13@columbia.edu>

基于Zope Corporation的Gary Poster提供的索引和关系容器,以及Whit Morriss在five.intid中将Zope intid实用程序移植到Zope 2。此软件包包括zc.relationship中的container.txt的略微修改版本,版权属于Zope Corporation,并按照ZPL分发。此软件包主要受到该产品doctests中思想的影响。

此工作部分得到了The Daily Reel (http://www.thedailyreel.com)的赞助。

变更日志

2.0 - 2011-10-06

  • 更新以与Zope 2.13兼容并需要。 [hannosch]

1.0rc4 - 未发布

  • 由于我们使用命名空间包,依赖于setuptools。 [maurits]

  • 依赖于zc.relationship 1.1.1或更高版本。 [maurits]

  • 依赖于zc.relationship 1.1.1以支持ZODB 3.7和3.8。 [alecm]

1.0rc3 - 2008-12-06

  • 将版本锁定为zc.relationship的1.0版本以最大程度地提高当前的后向兼容性。 [alecm]

1.0rc2 - 未发布

  • 1.0rc2从未发布。

1.0rc1 - 2008-11-26

1.0b5 - 2008-05-10

  • 根据pyflakes的报告删除了未使用的导入语句。 [tomster]

  • 不要假设IntId已经存在。 [alecm]

  • 准备带有固定依赖项和新适配器关系代理的发布版本。 [alecm]

  • 添加了对使用适配器获取的代理对象关系的支持。 [massimo]

  • 当目标可能是一个内容项(例如,一个文件夹,其空文件夹是false)时,使用“not target”是不安全的。新代码现在与zc.relationship中使用的相同模式相同。 [optilude]

  • 更新版本和zc.relationship依赖项。 [alecm]

  • 删除了collective.testing依赖项。 [alecm]

  • 在测试期间使用savepoint而不是完整提交。 [alecm]

  • 我们不再保证在检索时关系对象本身被包装,而只是它们可以根据需要包装。 [alecm]

  • 为了获取工作流/模板表达式的getPhysicalRoot,我们需要隐式获取。 [alecm]

  • 删除getPhysicalPath方法,因为它们不再需要,即使在某些来源/目标缺失的情况下,也要使关系对象的字符串表示显示合理的内容。 [alecm]

  • 早期贡献者。 [optilude, ramon, wichert]

  • 初始实现。 [alecm]

详细文档

概述

这是一个基于Zope 3的zc.relationship产品的产品。它试图允许从Zope 2使用该包的功能,以及一些从该包的基本关系Index中派生出的简单附加功能。

这里提供的关系容器与zc.relationship中的非常相似。它用于存储和查询实现或可适配简单IRelationship接口的对象,但也支持更复杂的关系。这些额外功能是在IRelationship接口的几个扩展中定义的。以下将描述这些接口。

IRelationship 定义了一种基本关系,仅由 来源目标 组成。这些是构成关系的一系列对象。在默认实现中,这些都必须是来自 ZODB(或更一般地,可以使用可用的 IIntId 工具生成 intid 的对象(参考 zope.intidfive.intid)的持久对象。

IComplexRelationship 增加了一个关系谓词来指示涉及的关系类型。这个谓词从一个称为 relation 的属性中检索,默认实现中应该是一个不可变的 Unicode 字符串(因此可以使用 zope.i18n.Message),

IContextAwareRelationship 增加了一个应用关系的环境。这个环境由一个名为 getContext 的方法提供,在默认实现中,应该返回与 IRelationship 所需相同类型的对象(例如,来自 ZODB 的持久对象)。例如:仅存在于特定部门或项目 _环境_ 中的层次关系。

IStatefulRelationship 增加了一个关系状态来指示特定关系的状态,如果该关系是随时间或用户操作而变化的关系。这个状态从一个称为 state 的属性中检索,应该是一个不可变的 Unicode 字符串(见上文)。例如:需要涉及的目标对象明确批准的关系,它将从未批准的 state 开始,然后在目标对象表示其批准时过渡到批准状态。此外,state 可能代表特定关系的不同阶段,例如 陌生人熟人朋友挚友

这些附加接口完全是可选的,并且可能将通过适应到所需接口来查找。因此,关系对象本身不必直接提供这些属性或方法,尽管这也是可能的。只需 来源目标 就可以创建一个可查询的关系。

这种额外的丰富性可以使用默认 zc.relationship 容器支持的查询后过滤器来获得。然而,这种方式过滤效率较低,允许直接索引和查询这些潜在的常见属性(尤其是在这样做只会导致存储需求略有增加时)。

使用此包

该包提供的基本功能在 container.txt 中演示和测试,它基本上在 Zope 2 环境中复制了来自 zc.relationship 的容器测试。本节演示了一些基本用法,以及上面描述的附加接口提供的功能。

首先,您需要一个包含一些内容并且默认具有 IIntId 工具的网站。这是测试设置为我们创建的,它提供了一个 app 和由 five.intid 包提供的 IIntId 工具。此外,我们需要创建一个关系容器来使用。

>>> from plone.relations import tests
>>> tests.setUp(app)
>>> import transaction
>>> from plone.relations import interfaces
>>> from plone.relations.container import Z2RelationshipContainer
>>> container = Z2RelationshipContainer()
>>> from zope.interface.verify import verifyObject
>>> verifyObject(interfaces.IComplexRelationshipContainer, container)
True
>>> app._setOb('references', container)
>>> container.__name__ = 'references'
>>> container.__parent__ = app
>>> container = app['references']

这通常被注册为一个提供 IComplexRelationshipContainer 接口的有名本地实用工具,但我们将直接使用它。现在我们创建一些关系,使用提供的 Relationship 类,该类实现 IRelationship 并具有内置的到 IComplexRelationship 的适配器。为了正确说明关系的潜在复杂性,我们将使用来自 1974 年电影《Chinatown》的一些角色和环境。

>>> from plone.relations.tests import ChinatownSetUp
>>> ChinatownSetUp(app) #creates our characters and contexts
>>> from plone.relations.relationships import Z2Relationship as Relationship
>>> rel1 = Relationship((app['noah'],), (app['evelyn'],), relation='parent')
>>> verifyObject(interfaces.IRelationship, rel1)
True
>>> interfaces.IComplexRelationship(rel1).relation
'parent'
>>> container.add(rel1)
>>> rel2 = Relationship((app['hollis'],), (app['noah'],), relation='business-partner')
>>> container.add(rel2)

请注意,对于IRelationship对象有一个默认适配器,它使用关系上的简单属性提供IComplexRelationship。

然后我们通过直接应用接口并添加属性(这不是一种很好的做法)来添加一个带有状态的关系。

>>> rel3 = Relationship((app['hollis'],), (app['evelyn'],), relation='intimate')
>>> rel3.state = 'married'
>>> from plone.relations.interfaces import IStatefulRelationship
>>> from zope.interface import alsoProvides
>>> alsoProvides(rel3, IStatefulRelationship)
>>> container.add(rel3)

我们目前有一个简单的树形结构。

 noah <---(business-partner)---
  | (parent)                   |
  v                            |
evelyn <-(intimate:married)- hollis

现在我们可以对这个简单数据集进行查询,例如找到源或目标为另一个对象的那些对象。

>>> list(container.findTargets(source=app['hollis']))
[<Demo noah>, <Demo evelyn>]
>>> list(container.findTargets(source=app['hollis'], relation='intimate'))
[<Demo evelyn>]
>>> list(container.findTargets(source=app['hollis'], relation='intimate', state='married'))
[<Demo evelyn>]
>>> list(container.findTargets(source=app['hollis'], relation='intimate', state='divorced'))
[]
>>> list(container.findTargets(source=app['evelyn'], relation='parent'))
[]
>>> list(container.findTargets(source=app['noah'], relation='parent'))
[<Demo evelyn>]
>>> list(container.findSources(target=app['evelyn']))
[<Demo noah>, <Demo hollis>]
>>> list(container.findSources(target=app['evelyn'], relation='parent'))
[<Demo noah>]
>>> list(container.findSources(target=app['evelyn'], relation='intimate'))
[<Demo hollis>]

传递性

我们还可以生成一个关系列表,并通过指定查询的最大深度(可选的最小深度)来 transitively查看关系链。特别是findRelationships方法将寻找匹配指定参数的关系链。让我们看看hollis和evelyn是如何连接的

>>> list(container.findRelationships(source=app['hollis'],
...                                  target=app['evelyn'], maxDepth=2))
[(<Relationship 'intimate' from (<Demo hollis>,) to (<Demo evelyn>,)>,), (<Relationship 'business-partner' from (<Demo hollis>,) to (<Demo noah>,)>, <Relationship 'parent' from (<Demo noah>,) to (<Demo evelyn>,)>)]

Hollis是evelyn的丈夫,也是她父亲的合作伙伴。

修改关系

上述方法还允许我们直接访问现有的关系,这在我们需要更改它们时特别有帮助。在这种情况下,hollis已经被谋杀;因此,evelyn现在是他遗孀。我们通过关系上的状态更改来表示这一点,请注意,在直接将其应用于它之后,我们必须重新索引关系,如果我们使用适配器来提供状态,那么在设置属性时,它应该已经为我们处理了这一点。

>>> relations = container.findRelationships(target=app['evelyn'], relation='intimate')
>>> relations = list(relations)
>>> relations
[(<Relationship 'intimate' from (<Demo hollis>,) to (<Demo evelyn>,)>,)]
>>> marriage = relations[0][0]
>>> marriage.state = 'widowed'
>>> container.reindex(marriage) # an adapter could handle this, as
...                             # we'll see later with context

我们已经更改了婚姻的状态,让我们确保我们仍然可以像以前一样找到它,同时也使用我们的新状态。

>>> list(container.findTargets(source=app['hollis'], relation='intimate'))
[<Demo evelyn>]
>>> list(container.findTargets(source=app['hollis'], relation='intimate', state='widowed'))
[<Demo evelyn>]
>>> list(container.findTargets(source=app['hollis'], relation='intimate', state='happy'))
[]

现在让我们添加一些更多的关系,包括一个带有未知关系的。以下是新的关系树

        noah <----(business-partner)---
         | (parent)                    |
         v                             |
       evelyn <-(intimate:widowed)- hollis
         /\
(client)/  \ (??)
       v    v
    jake    katherine

以及相关的代码

>>> rel4 = Relationship((app['evelyn'],), (app['jake'],), relation='client')
>>> rel5 = Relationship((app['evelyn'],), (app['katherine'],))
>>> container.add(rel4)
>>> container.add(rel5)
>>> sorted([repr(r) for r in container.findTargets(source=app['evelyn'])])
['<Demo jake>', '<Demo katherine>']
>>> list(container.findTargets(source=app['evelyn'], relation=None))
[<Demo katherine>]
>>> list(container.findTargets(source=app['noah'], relation=None))
[]

请注意,我们可以使用None作为查询参数来找到具有空参数的条目。

上下文

现在我们将使用简单适配器将上下文应用于现有关系,在现实生活中,这些额外的数据可能通过关系上的注释来存储,但在这里我们直接存储。

>>> class ContextAdapter(object):
...     def __init__(self, relationship):
...         self.relationship = relationship
...     def getContext(self):
...         return getattr(self.relationship, '_context', None)
...     def setContext(self, context):
...         self.relationship._context = context
...         #reindex ourself in the container
...         if self.relationship.__parent__ is not None:
...             self.relationship.__parent__.reindex(self.relationship)
>>> from zope.component import provideAdapter
>>> provideAdapter(ContextAdapter, (interfaces.IRelationship,), interfaces.IContextAwareRelationship)

目前evelyn和jake之间的客户关系没有告诉我们太多,因为客户关系可能有多种不同的上下文。在这种情况下,jake是一名私人调查员,上下文是hollis谋杀案的调查。这个调查对象可能包括与调查相关的笔记或其他相关数据。我们将它作为上下文应用于关系。

>>> list(container.findSources(target=app['jake'], relation='client',
...                            context=app['investigation']))
[]
>>> relationships = list(container.findRelationships(source=app['evelyn'],
...                                                  target=app['jake']))
>>> relationships
[(<Relationship 'client' from (<Demo evelyn>,) to (<Demo jake>,)>,)]
>>> evelyn_jake = relationships[0][0]
>>> interfaces.IContextAwareRelationship(evelyn_jake).setContext(
...                                                   app['investigation'])
>>> list(container.findSources(target=app['jake'], relation='client',
...                            context=app['investigation']))
[<Demo evelyn>]
>>> list(container.findSources(target=app['jake'], context=None))
[]
>>> list(container.findSources(target=app['katherine'], context=None))
[<Demo evelyn>]

随着时间的推移,一些额外的关系发展起来。在调查期间,jake和katherine有过一段恋情。此外,jake开始怀疑hollis的商业伙伴和岳父noah。

>>> rel6 = Relationship((app['jake'],), (app['evelyn'],), 'intimate')
>>> rel6.state = 'fling'
>>> interfaces.IContextAwareRelationship(rel6).setContext(app['investigation'])
>>> rel7 = Relationship((app['jake'],), (app['noah'],), 'nemesis')
>>> interfaces.IContextAwareRelationship(rel7).setContext(app['investigation'])
>>> container.add(rel6)
>>> container.add(rel7)

多关系链和循环

我们有一个相当复杂的关系图,但当我们了解到katherine是evelyn的姐妹时,现有的关系变得更为清晰。

>>> murky = list(container.findRelationships(source=app['evelyn'],
...                                          target=app['katherine']))
>>> evelyn_katherine = murky[0][0]
>>> interfaces.IComplexRelationship(evelyn_katherine).relation = 'sibling'

以下是当前关系树的ASCII形式

        (nemesis)---->noah <-----(business-partner)--
 [investigation]|      | (parent)                    |
                |      v                             |
(intimate:fling)|--> evelyn <-(intimate:widowed)- hollis
[investigation] |      /\
                |(client)\
           [investigation]\ (sibling)
                |   /      \
                |  v        v
                jake       katherine

这种复杂性将允许我们探索关系查询机制如何解决多个关系路径。

>>> list(container.findTargets(source=app['jake'], context=app['investigation']))
[<Demo evelyn>, <Demo noah>]
>>> list(container.findRelationships(context=app['investigation']))
[(<Relationship 'client' from (<Demo evelyn>,) to (<Demo jake>,)>,), (<Relationship 'intimate' from (<Demo jake>,) to (<Demo evelyn>,)>,), (<Relationship 'nemesis' from (<Demo jake>,) to (<Demo noah>,)>,)]

上面的第一个findTargets示例显示了在调查的上下文中jake的目标所有人。然后我们有一个在调查上下文中适用所有关系的映射。

在电影结尾,我们发现这些角色之间存在着一些相当可疑的联系。《诺亚》(Noah)是霍利斯(hollis)的杀手,并且与他女儿《伊夫琳》(Evelyn)有不恰当的亲密关系,这导致了他们的女儿《凯瑟琳》(Katherine)。我们在下面添加了这些关系(注意一个人可以使用多个来源或目标来表示与《诺亚》和《伊夫琳》的单个关系,这些来源是《凯瑟琳》的父母的来源)

 noah-(intimate[the past])->evelyn
    |\                     /
    | \                   /
    |  \                 /
    |   \  (parents)    /
    |    -->katherine<--
(murderer)
    |
  hollis

和代码

>>> rel8 = Relationship((app['noah'],), (app['evelyn'],), 'intimate')
>>> interfaces.IContextAwareRelationship(rel8).setContext(app['the past'])
>>> container.add(rel8)
>>> rel9 = Relationship((app['noah'],), (app['hollis'],), 'murderer')
>>> container.add(rel9)
>>> rel10 = Relationship((app['evelyn'], app['noah']), (app['katherine'],),
...                      'parent')
>>> container.add(rel10)

此时,关系树过于复杂且充满循环,无法使用ASCII艺术清晰绘制。然而,我们的关系容器检查它并不麻烦

>>> list(container.findSources(target=app['katherine'], relation='parent', maxDepth=None))
[<Demo evelyn>, <Demo noah>]
>>> list(container.findRelationships(source=app['noah'],
...                                  target=app['katherine'],
...                                  relation='parent', maxDepth=None))
[(<Relationship 'parent' from (<Demo evelyn>, <Demo noah>) to (<Demo katherine>,)>,), (<Relationship 'parent' from (<Demo noah>,) to (<Demo evelyn>,)>, <Relationship 'parent' from (<Demo evelyn>, <Demo noah>) to (<Demo katherine>,)>)]

这与我们之前尝试的查询相同,当时我们不清楚《凯瑟琳》和《诺亚》之间的关系。现在我们可以看到,《诺亚》既是她的父亲也是她的祖父(真恶心!)。

从《伊夫琳》探索指向《凯瑟琳》的关系会产生一个非常疯狂的图像,即使我们限制自己最多只有2层关系(我们需要玩一些技巧来确保结果以可重复的顺序返回,以便通过此测试)

>>> relations = container.findRelationships(target=app['katherine'],
...                                         maxDepth=2)
>>> res = [repr(r) for r in relations]
>>> res.sort(key=lambda x:(len(x), x)) # sort by length
>>> print '\n'.join(res)
(<Relationship 'sibling' from (<Demo evelyn>,) to (<Demo katherine>,)>,)
(<Relationship 'parent' from (<Demo evelyn>, <Demo noah>) to (<Demo katherine>,)>,)
(<Relationship 'parent' from (<Demo noah>,) to (<Demo evelyn>,)>, <Relationship 'sibling' from (<Demo evelyn>,) to (<Demo katherine>,)>)
(<Relationship 'intimate' from (<Demo jake>,) to (<Demo evelyn>,)>, <Relationship 'sibling' from (<Demo evelyn>,) to (<Demo katherine>,)>)
(<Relationship 'intimate' from (<Demo noah>,) to (<Demo evelyn>,)>, <Relationship 'sibling' from (<Demo evelyn>,) to (<Demo katherine>,)>)
(<Relationship 'intimate' from (<Demo hollis>,) to (<Demo evelyn>,)>, <Relationship 'sibling' from (<Demo evelyn>,) to (<Demo katherine>,)>)
(<Relationship 'nemesis' from (<Demo jake>,) to (<Demo noah>,)>, <Relationship 'parent' from (<Demo evelyn>, <Demo noah>) to (<Demo katherine>,)>)
(<Relationship 'parent' from (<Demo noah>,) to (<Demo evelyn>,)>, <Relationship 'parent' from (<Demo evelyn>, <Demo noah>) to (<Demo katherine>,)>)
(<Relationship 'intimate' from (<Demo jake>,) to (<Demo evelyn>,)>, <Relationship 'parent' from (<Demo evelyn>, <Demo noah>) to (<Demo katherine>,)>)
(<Relationship 'intimate' from (<Demo noah>,) to (<Demo evelyn>,)>, <Relationship 'parent' from (<Demo evelyn>, <Demo noah>) to (<Demo katherine>,)>)
(<Relationship 'intimate' from (<Demo hollis>,) to (<Demo evelyn>,)>, <Relationship 'parent' from (<Demo evelyn>, <Demo noah>) to (<Demo katherine>,)>)
(<Relationship 'business-partner' from (<Demo hollis>,) to (<Demo noah>,)>, <Relationship 'parent' from (<Demo evelyn>, <Demo noah>) to (<Demo katherine>,)>)

关系如下

伊夫琳 |-(兄弟姐妹)-> 凯瑟琳 伊夫琳+诺亚 |-(父母)-> 凯瑟琳 诺亚 |-(父母)-> 伊夫琳 |-(兄弟姐妹)-> 凯瑟琳 贾克 |-(亲密关系)-> 伊夫琳 |-(兄弟姐妹)-> 凯瑟琳 诺亚 |-(亲密关系)-> 伊夫琳 |-(兄弟姐妹)-> 凯瑟琳 霍利斯 |-(亲密关系)-> 伊夫琳 |-(兄弟姐妹)-> 凯瑟琳 贾克 |-(宿敌)-> 诺亚 |-(父母)-> 凯瑟琳 诺亚 |-(父母)-> 伊夫琳 |-(父母)-> 凯瑟琳 贾克 |-(亲密关系)-> 伊夫琳 |-(父母)-> 凯瑟琳 诺亚 |-(亲密关系)-> 伊夫琳 |-(父母)-> 凯瑟琳 霍利斯 |-(亲密关系)-> 伊夫琳 |-(父母)-> 凯瑟琳 霍利斯 |-(商业伙伴)-> 诺亚 |-(父母)-> 凯瑟琳

重要的是要注意,当发现循环时,并没有什么爆炸。在这种情况下,结果只是一个实现ICircularRelationshipPath的特殊元组。我们可以通过查看《伊夫琳》和她自己之间最简单的循环来看到这一点

>>> list(container.findRelationships(source=app['evelyn'],
...                                  target=app['evelyn'], maxDepth=2))
[cycle(<Relationship 'client' from (<Demo evelyn>,) to (<Demo jake>,)>, <Relationship 'intimate' from (<Demo jake>,) to (<Demo evelyn>,)>)]

收购废话

Zope 2几乎要求每个对象都支持收购才能运行(这是安全和遍历所必需的)。以下我们将执行一些健全性检查,以确保涉及的对象以符合Zope 2期望的方式包装

>>> list(container.findSources(target=app['katherine']))[0].aq_chain
[<Demo evelyn>, <Application at >, <ZPublisher.BaseRequest.RequestContainer object at ...>]
>>> list(container.findTargets(source=app['hollis'],
...                            relation='business-partner'))[0].aq_chain
[<Demo noah>, <Application at >, <ZPublisher.BaseRequest.RequestContainer object at ...>]
>>> list(list(container.findRelationships(source=app['evelyn'],
...                      target=app['katherine']))[0][0].targets)[0].aq_chain
[<Demo katherine>, <Application at >, <ZPublisher.BaseRequest.RequestContainer object at ...>]
>>> list(list(container.findRelationships(source=app['evelyn'],
...                      target=app['katherine']))[0][0].sources)[0].aq_chain
[<Demo evelyn>, <Application at >, <ZPublisher.BaseRequest.RequestContainer object at ...>]

正如你所看到的,即使从搜索返回,目标和来源也保留着它们的原始包装。关系没有被包装,尽管在需要安全检查时,可以使用某些可用的上下文显式包装。返回的关系的《来源》和《目标》属性也将保留其原始包装,即使在幽灵化之后也是如此

>>> evelyn = list(container.findSources(target=app['katherine']))[0]
>>> noah = list(container.findTargets(source=app['hollis'],
...                                   relation='business-partner'))[0]
>>> rel = list(container.findRelationships(source=app['evelyn'],
...                          target=app['katherine']))[0][0]
>>> sp = transaction.savepoint()
>>> evelyn._p_deactivate()
>>> noah._p_deactivate()
>>> for _rel in container.values():
...    _rel._p_deactivate()
...    _rel.targets._p_deactivate()
...    _rel.sources._p_deactivate()
>>> container._p_deactivate()
>>> list(rel.targets)[0].aq_chain
[<Demo katherine>, <Application at >, <ZPublisher.BaseRequest.RequestContainer object at ...>]
>>> list(container.findSources(target=app['katherine']))[0].aq_chain
[<Demo evelyn>, <Application at >, <ZPublisher.BaseRequest.RequestContainer object at ...>]
>>> list(container.findTargets(source=app['hollis'],
...                            relation='business-partner'))[0].aq_chain
[<Demo noah>, <Application at >, <ZPublisher.BaseRequest.RequestContainer object at ...>]
>>> list(list(container.findRelationships(source=app['evelyn'],
...                      target=app['katherine']))[0][0].targets)[0].aq_chain
[<Demo katherine>, <Application at >, <ZPublisher.BaseRequest.RequestContainer object at ...>]

除了《来源》和《目标》上的包装外,所有包装都被保留,因此,出于这个原因,大多数情况下不应直接依赖它们(至少不是需要安全检查或收购的代码)。

当我们创建一个指向显式重新包装的对象的关系时会发生什么

>>> rel = Relationship((app['katherine'],),(app['jake'].__of__(container),))
>>> container.add(rel)
>>> list(rel.targets)[0].aq_chain
[<Demo jake>, <Application at >, <ZPublisher.BaseRequest.RequestContainer object at ...>]
>>> list(container.findTargets(source=app['katherine'],
...                            relation=None))[0].aq_chain
[<Demo jake>, <Application at >, <ZPublisher.BaseRequest.RequestContainer object at ...>]

通过搜索检索返回的对象仅由其原始容器包装,而不论它在关系中使用时是如何包装的。当我们检索关系时,将恢复《来源》和《目标》的原始包装。

>>> sp = transaction.savepoint()
>>> rel._p_deactivate()
>>> rel.sources._p_deactivate()
>>> rel.targets._p_deactivate()
>>> list(list(container.findRelationships(source=app['katherine'],
...                                relation=None))[0][0].targets)[0].aq_chain
[<Demo jake>, <Application at >, <ZPublisher.BaseRequest.RequestContainer object at ...>]
>>> tests.tearDown()

项目详情


下载文件

下载适用于您的平台的文件。如果您不确定选择哪个,请了解有关安装包的更多信息。

源代码分发

plone.relations-2.0.zip (50.1 kB 查看哈希值)

上传时间 源代码

由以下组织支持