
git-svn-id: svn://svn.cy55.de/Zope3/src/loops/trunk@2935 fd906abe-77d9-0310-91a1-e0d9ade77398
471 lines
15 KiB
Text
Executable file
471 lines
15 KiB
Text
Executable file
===============================================================
|
|
loops - Linked Objects for Organization and Processing Services
|
|
===============================================================
|
|
|
|
This file documents and tests a bunch of facilities that support the
|
|
loops framework, mostly provided by sub-packages of the cybertools
|
|
package.
|
|
|
|
($Id$)
|
|
|
|
Let's first do some basic imports
|
|
|
|
>>> from zope import interface, component
|
|
>>> from zope.interface import Interface
|
|
>>> from zope.publisher.browser import TestRequest
|
|
|
|
>>> from zope.app.testing.setup import placefulSetUp, placefulTearDown
|
|
>>> site = placefulSetUp(True)
|
|
|
|
>>> from loops.tests.setup import TestSite
|
|
>>> t = TestSite(site)
|
|
>>> concepts, resources, views = t.setup()
|
|
|
|
>>> concepts['hasType'].title
|
|
u'has Type'
|
|
|
|
>>> loopsRoot = site['loops']
|
|
|
|
We also add some example concepts,
|
|
|
|
>>> from loops.concept import Concept
|
|
>>> cc1 = Concept(u'Zope')
|
|
>>> concepts['cc1'] = cc1
|
|
>>> cc1.title
|
|
u'Zope'
|
|
|
|
>>> cc2 = Concept(u'Zope 3')
|
|
>>> concepts['cc2'] = cc2
|
|
>>> cc2.title
|
|
u'Zope 3'
|
|
|
|
resources,
|
|
|
|
>>> from loops.resource import Resource
|
|
>>> file1 = resources['file1'] = Resource(u'A file')
|
|
>>> file1.resourceType = concepts['file']
|
|
|
|
(note: the use of Document is deprecated)
|
|
|
|
>>> from loops.resource import Document
|
|
>>> doc1 = Document(u'Zope Info')
|
|
>>> resources['doc1'] = doc1
|
|
>>> doc1.title
|
|
u'Zope Info'
|
|
|
|
>>> img1 = resources['img1'] = Resource(u'An Image')
|
|
>>> img1.resourceType = concepts['file']
|
|
|
|
and nodes (in view space):
|
|
|
|
>>> from loops.view import Node
|
|
>>> m1 = Node(u'Home')
|
|
>>> views['m1'] = m1
|
|
>>> m1.nodeType = 'menu'
|
|
|
|
>>> m1p1 = Node(u'Page')
|
|
>>> m1['p1'] = m1p1
|
|
>>> m1p1.nodeType = 'page'
|
|
|
|
|
|
Type management with typology
|
|
=============================
|
|
|
|
The type of an object may be determined by adapting it to the IType
|
|
interface. The loops framework provides an adapter (LoopsType) for this
|
|
purpose:
|
|
|
|
>>> from cybertools.typology.interfaces import IType
|
|
>>> from loops.type import ConceptType
|
|
>>> component.provideAdapter(ConceptType)
|
|
>>> cc1_type = IType(cc1)
|
|
|
|
As we have not yet associated a type with one of our content objects we get
|
|
|
|
>>> cc1_type.typeProvider is None
|
|
True
|
|
>>> cc1_type.title
|
|
u'Unknown Type'
|
|
>>> cc1_type.token
|
|
'.unknown'
|
|
|
|
During setup we created two special concepts: one ('hasType') as the predicate
|
|
signifying a type relation, and the other ('type') as the one and only type
|
|
concept:
|
|
|
|
>>> typeObject = concepts['type']
|
|
|
|
Assigning a type to a concept is a core functionality of concepts as
|
|
concept types are themselves concepts; and we assign the type object to
|
|
itself (as it in fact is of type 'type'):
|
|
|
|
>>> typeObject.conceptType = typeObject
|
|
|
|
So let's check the type of the type object:
|
|
|
|
>>> type_type = IType(typeObject)
|
|
>>> type_type.typeProvider is typeObject
|
|
True
|
|
>>> type_type.title
|
|
u'Type'
|
|
>>> type_type.token
|
|
'.loops/concepts/type'
|
|
>>> type_type.tokenForSearch
|
|
'loops:concept:type'
|
|
>>> type_type.qualifiers
|
|
('concept', 'system')
|
|
|
|
Now we register another type ('topic') and assign it to cc1:
|
|
|
|
>>> concepts['topic'] = Concept(u'Topic')
|
|
>>> topic = concepts['topic']
|
|
>>> topic.conceptType = typeObject
|
|
>>> cc1.conceptType = topic
|
|
|
|
Note: as these kind of usually short-living adapters makes heavy use of
|
|
lazy properties, one should always get a new adapter:
|
|
|
|
>>> topic_type = IType(topic)
|
|
>>> cc1_type = IType(cc1)
|
|
>>> topic_type == type_type
|
|
True
|
|
>>> cc1_type == topic_type
|
|
False
|
|
>>> cc1_type.typeProvider == topic
|
|
True
|
|
>>> topic_type.qualifiers
|
|
('concept', 'system')
|
|
>>> topic_type.defaultContainer
|
|
<loops.concept.ConceptManager object ...>
|
|
>>> topic_type.factory
|
|
<class 'loops.concept.Concept'>
|
|
|
|
Now let's have a look at resources.
|
|
|
|
>>> from loops.interfaces import IResource
|
|
>>> from loops.type import LoopsType
|
|
>>> component.provideAdapter(LoopsType, (IResource,), IType)
|
|
>>> file1_type = IType(file1)
|
|
>>> file1_type.title
|
|
u'File'
|
|
>>> file1_type.token
|
|
'.loops/concepts/file'
|
|
>>> file1_type.tokenForSearch
|
|
'loops:resource:file'
|
|
>>> file1_type.qualifiers
|
|
('resource',)
|
|
>>> file1_type.defaultContainer
|
|
<loops.resource.ResourceManager object ...>
|
|
>>> file1_type.factory
|
|
<class 'loops.resource.Resource'>
|
|
|
|
(The use of Document is deprecated!)
|
|
|
|
>>> from loops.interfaces import IResource, IDocument
|
|
>>> from loops.type import ResourceType
|
|
>>> component.provideAdapter(ResourceType, (IDocument,), IType)
|
|
|
|
>>> doc1_type = IType(doc1)
|
|
>>> doc1_type.title
|
|
u'Document'
|
|
>>> doc1_type.token
|
|
'loops.resource.Document'
|
|
>>> doc1_type.tokenForSearch
|
|
'loops:resource:document'
|
|
>>> doc1_type.qualifiers
|
|
('resource',)
|
|
>>> doc1_type.defaultContainer
|
|
<loops.resource.ResourceManager object ...>
|
|
>>> doc1_type.factory
|
|
<class 'loops.resource.Document'>
|
|
|
|
>>> img1_type = IType(img1)
|
|
>>> img1_type.token
|
|
'.loops/concepts/file'
|
|
>>> img1_type.tokenForSearch
|
|
'loops:resource:file'
|
|
>>> img1_type.title
|
|
u'File'
|
|
|
|
Using the type machinery we can also specify options that may be used
|
|
for controlling e.g. storage for external files.
|
|
|
|
>>> from loops.interfaces import IFile
|
|
>>> from loops.resource import FileAdapter
|
|
>>> component.provideAdapter(FileAdapter, provides=IFile)
|
|
|
|
>>> extfile = concepts['extfile'] = Concept(u'External File')
|
|
>>> ef1 = resources['ef1'] = Resource(u'Extfile #1')
|
|
>>> ef1.resourceType = extfile
|
|
>>> ef1_type = IType(ef1)
|
|
>>> IType(ef1).options
|
|
[]
|
|
|
|
>>> from loops.type import TypeConcept
|
|
>>> extfile_ad = TypeConcept(extfile)
|
|
>>> extfile_ad.options = ['dummy', 'storage:varsubdir',
|
|
... 'storage_parameters:extfiles']
|
|
>>> IType(ef1).options
|
|
['dummy', 'storage:varsubdir', 'storage_parameters:extfiles']
|
|
>>> IType(ef1).optionsDict
|
|
{'default': ['dummy'], 'storage_parameters': 'extfiles', 'storage': 'varsubdir'}
|
|
|
|
Can we find out somehow which types are available? This is the time to look
|
|
for a type manager. This could be a utility; but in the loops package it
|
|
is again an adapter, now for the loops root object. Nevertheless one can
|
|
get a type manager from all loops objects, always with the same context:
|
|
|
|
>>> from cybertools.typology.interfaces import ITypeManager
|
|
>>> from loops.interfaces import ILoopsObject
|
|
>>> from loops.type import LoopsTypeManager
|
|
>>> component.provideAdapter(LoopsTypeManager)
|
|
>>> typeManager = ITypeManager(loopsRoot)
|
|
>>> typeManager.context == ITypeManager(cc1).context == loopsRoot
|
|
True
|
|
|
|
>>> types = typeManager.types
|
|
>>> typeTokens = sorted(t.token for t in types)
|
|
>>> len(typeTokens)
|
|
8
|
|
|
|
>>> typeManager.getType('.loops/concepts/topic') == cc1_type
|
|
True
|
|
|
|
The listTypes() method allows us to select types that fulfill a certain
|
|
condition:
|
|
|
|
>>> types = typeManager.listTypes(include=('concept',))
|
|
>>> typeTokens = sorted(t.token for t in types)
|
|
>>> len(typeTokens)
|
|
5
|
|
>>> types = typeManager.listTypes(exclude=('concept',))
|
|
>>> typeTokens = sorted(t.token for t in types)
|
|
>>> len(typeTokens)
|
|
3
|
|
|
|
Type-based interfaces and adapters
|
|
----------------------------------
|
|
|
|
A type has an optional typeInterface attribute that objects of this type
|
|
will be adaptable to. The default for this is None:
|
|
|
|
>>> cc1_type.typeInterface is None
|
|
True
|
|
|
|
For concept objects that provide types (type providers) the value of
|
|
the typeInterface attribute is the ITypeConcept interface:
|
|
|
|
>>> from loops.interfaces import ITypeConcept
|
|
>>> topic_type.typeInterface is ITypeConcept
|
|
True
|
|
|
|
We now want to have a topic (i.e. a concept that has topic as its
|
|
conceptType and thus topic_type as its type) to be adaptable to ITopic.
|
|
This is done by assigning this interface to topic_type.typeProvider,
|
|
i.e. the 'topic' concept, via an adapter:
|
|
|
|
>>> class ITopic(Interface): pass
|
|
>>> from zope.interface import implements
|
|
>>> class Topic(object):
|
|
... implements(ITopic)
|
|
... def __init__(self, context): pass
|
|
>>> from loops.interfaces import IConcept
|
|
>>> component.provideAdapter(Topic, (IConcept,), ITopic)
|
|
|
|
>>> ITypeConcept(topic).typeInterface = ITopic
|
|
>>> cc1.conceptType = topic
|
|
|
|
>>> cc1_type = IType(cc1)
|
|
>>> cc1Adapter = cc1_type.typeInterface(cc1)
|
|
>>> ITopic.providedBy(cc1Adapter)
|
|
True
|
|
|
|
There is a shortcut to getting the typeInterface adapter for an object.
|
|
|
|
>>> from loops.common import adapted
|
|
>>> cc1Adapter = adapted(cc1)
|
|
>>> ITopic.providedBy(cc1Adapter)
|
|
True
|
|
|
|
|
|
Simple access to type information with BaseView
|
|
-----------------------------------------------
|
|
|
|
loops browser views are typically based on a common parent class, BaseView.
|
|
BaseView provides simple access to a lot of information often needed for
|
|
browser views; among others also some important informations about the
|
|
context object's type:
|
|
|
|
>>> from loops.browser.common import BaseView
|
|
>>> view = BaseView(cc1, TestRequest())
|
|
>>> view.typeTitle
|
|
u'Topic'
|
|
>>> view.typeInterface
|
|
<InterfaceClass ...ITopic>
|
|
>>> view.typeAdapter
|
|
<Topic object ...>
|
|
|
|
|
|
Concepts as Queries
|
|
===================
|
|
|
|
We first have to set up the query type, i.e. a type concept associated
|
|
with the IQueryConcept interface. The query type concept itself has already
|
|
been provided by the setup, but we have to register a corresponding adapter.
|
|
|
|
>>> from loops.expert.concept import IQueryConcept, QueryConcept
|
|
>>> component.provideAdapter(QueryConcept)
|
|
>>> from loops.setup import addAndConfigureObject
|
|
>>> query = addAndConfigureObject(concepts, Concept,
|
|
... 'query', conceptType=typeObject, typeInterface=IQueryConcept)
|
|
|
|
Next we need a concept of this type:
|
|
|
|
>>> simpleQuery = concepts['simpleQuery'] = Concept(u'Simple query')
|
|
>>> simpleQuery.conceptType = query
|
|
>>> sq_type = IType(simpleQuery)
|
|
>>> sq_adapter = sq_type.typeInterface(simpleQuery)
|
|
>>> sq_adapter.viewName = 'simpleview.html'
|
|
>>> simpleQuery._viewName
|
|
'simpleview.html'
|
|
|
|
This viewName attribute of the query will be automatically used by
|
|
a concept view when asked for the view that should be used for rendering
|
|
the concept...
|
|
|
|
>>> from loops.browser.concept import ConceptView
|
|
>>> from zope.publisher.browser import TestRequest
|
|
>>> sq_baseView = ConceptView(simpleQuery, TestRequest())
|
|
>>> sq_view = sq_baseView.view
|
|
|
|
...but only when the view exists, i.e. there is a class registered as a
|
|
view/multi-adapter with this name:
|
|
|
|
>>> sq_view is sq_baseView
|
|
True
|
|
|
|
>>> class SimpleView(object):
|
|
... def __init__(self, context, request): pass
|
|
>>> from zope.publisher.interfaces.browser import IBrowserRequest
|
|
>>> component.provideAdapter(SimpleView, (IConcept, IBrowserRequest), Interface,
|
|
... name='simpleview.html')
|
|
>>> sq_baseView = ConceptView(simpleQuery, TestRequest())
|
|
>>> sq_view = sq_baseView.view
|
|
>>> sq_view is sq_baseView
|
|
False
|
|
>>> sq_view.__class__
|
|
<class 'SimpleView'>
|
|
|
|
|
|
Controlling Presentation Using View Properties
|
|
==============================================
|
|
|
|
>>> from zope.annotation.interfaces import IAttributeAnnotatable, IAnnotations
|
|
>>> from zope.annotation.attribute import AttributeAnnotations
|
|
>>> from loops.interfaces import INode
|
|
|
|
First we have to make sure we can use attribute annotations with our nodes,
|
|
and we also have to register an IViewConfigurator adapter for them:
|
|
|
|
>>> component.provideAdapter(AttributeAnnotations, (INode,), IAnnotations)
|
|
|
|
>>> from cybertools.browser.configurator import IViewConfigurator
|
|
>>> from loops.browser.node import NodeViewConfigurator
|
|
>>> from zope.publisher.interfaces.browser import IBrowserRequest
|
|
>>> component.provideAdapter(NodeViewConfigurator, (INode, IBrowserRequest),
|
|
... IViewConfigurator)
|
|
|
|
Now we are ready to set up a view on our page node:
|
|
|
|
>>> from loops.browser.node import NodeView
|
|
>>> request = TestRequest()
|
|
>>> view = NodeView(m1p1, request)
|
|
|
|
The elements responsible for presentation are controlled by a controller
|
|
object; note that we have to provide a named template 'loops.node_macros' that
|
|
is used to retrieve a macro used by NodeView.
|
|
|
|
As the display of the standard macros is controlled by permissions we have
|
|
to install a checker first.
|
|
|
|
>>> from cybertools.browser.controller import Controller
|
|
>>> from loops.browser.util import node_macros
|
|
>>> from loops.browser.common import BaseView
|
|
>>> component.provideAdapter(node_macros, (BaseView,), name='loops.node_macros')
|
|
>>> controller = Controller(view, request)
|
|
>>> getattr(controller, 'skinName', None) is None
|
|
True
|
|
|
|
There is no `skinName` setting in the controller as we did not set any.
|
|
The configurator (IViewConfigurator adapter, see above) takes the
|
|
view properties from the attribute annotations. We set these properties
|
|
using an adapter to the config schema; the configurator will only use
|
|
settings on menu nodes (possibly above the node to be viewed in the
|
|
browser).
|
|
|
|
>>> from loops.interfaces import IViewConfiguratorSchema
|
|
>>> from loops.browser.node import ViewPropertiesConfigurator
|
|
>>> component.provideAdapter(ViewPropertiesConfigurator, (INode,),
|
|
... IViewConfiguratorSchema)
|
|
|
|
>>> pageConfigurator = IViewConfiguratorSchema(m1)
|
|
>>> pageConfigurator.skinName = 'SuperSkin'
|
|
|
|
>>> controller = Controller(view, request)
|
|
>>> controller.skinName.value
|
|
'SuperSkin'
|
|
|
|
|
|
Folders
|
|
=======
|
|
|
|
We may provide a concept type called a folder - there is no special
|
|
functionality about it but it may be used for building a pseudo hierarchy
|
|
using nested folders. This may make it easier for users to map the
|
|
structures of their documents in the filesystem to the loops concept
|
|
map.
|
|
|
|
>>> tFolder = addAndConfigureObject(concepts, Concept, 'folder',
|
|
... title=u'Folder', conceptType=typeObject)
|
|
|
|
Usually we want to create folders only in objects of a certain type,
|
|
e.g. in a domain. So we activate the folder creation action by providing
|
|
the domain type with a corresponding option.
|
|
|
|
>>> tDomain = concepts['domain']
|
|
>>> taDomain = adapted(tDomain)
|
|
>>> taDomain.options = ['action.portlet:createFolder']
|
|
|
|
Importing the FolderView will register this action.
|
|
|
|
>>> from loops.browser.folder import FolderView
|
|
|
|
If we now create a domain and set up a view on it it will provide the
|
|
folder creation action.
|
|
|
|
>>> general = addAndConfigureObject(concepts, Concept, 'general',
|
|
... title=u'General', conceptType=tDomain)
|
|
|
|
>>> from loops.browser.concept import ConceptView
|
|
>>> view = ConceptView(general, TestRequest())
|
|
>>> sorted(a.name for a in view.getActions('portlet'))
|
|
['createFolder']
|
|
|
|
Let's now create a folder.
|
|
|
|
>>> f01 = addAndConfigureObject(concepts, Concept, 'f01',
|
|
... title=u'Test Folder', conceptType=tFolder)
|
|
|
|
A folder should be associated with a FolderView that provides two actions
|
|
for editing the folder and for creating a new subfolder.
|
|
|
|
>>> view = FolderView(f01, TestRequest())
|
|
>>> sorted(a.name for a in view.getActions('portlet'))
|
|
['createFolder', 'editFolder']
|
|
|
|
|
|
Fin de partie
|
|
=============
|
|
|
|
>>> placefulTearDown()
|
|
|