===============================================================
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)
  9

  >>> 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)
  6
  >>> 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.query import QueryConcept
  >>> component.provideAdapter(QueryConcept)
  >>> query = concepts['query']

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.

  >>> from loops.setup import addAndConfigureObject
  >>> 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()

