=============================================================== 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 u'.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 >>> topic_type.factory 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 u'.loops/concepts/file' >>> file1_type.tokenForSearch 'loops:resource:file' >>> file1_type.qualifiers ('resource',) >>> file1_type.defaultContainer >>> file1_type.factory (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 >>> doc1_type.factory >>> img1_type = IType(img1) >>> img1_type.token u'.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 >>> view.typeAdapter 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__ 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()