===============================================================
loops - Linked Objects for Organization and Processing Services
===============================================================

  ($Id$)

Note: This packages depends on cybertools.organize.

Let's do some basic setup

  >>> from zope.app.testing.setup import placefulSetUp, placefulTearDown
  >>> site = placefulSetUp(True)
  >>> from zope import component, interface

and set up a simple loops site with a concept manager and some concepts
(with all the type machinery, what in real life is done via standard
ZCML setup):

  >>> from loops.organize.setup import SetupManager
  >>> component.provideAdapter(SetupManager, name='organize')
  >>> from loops.tests.setup import TestSite
  >>> t = TestSite(site)
  >>> concepts, resources, views = t.setup()

  >>> from loops import util
  >>> loopsRoot = site['loops']
  >>> loopsId = util.getUidForObject(loopsRoot)

  >>> from loops.organize.tests import setupUtilitiesAndAdapters
  >>> setupData = setupUtilitiesAndAdapters(loopsRoot)

  >>> type = concepts['type']
  >>> person = concepts['person']

  >>> from loops.concept import Concept
  >>> johnC = concepts['john'] = Concept(u'John')
  >>> johnC.conceptType = person


Organizations: Persons (and Users), Institutions, Addresses...
==============================================================

The classes used in this package are just adapters to IConcept.

  >>> from loops.organize.interfaces import IPerson

  >>> john = IPerson(johnC)
  >>> john.title
  u'John'
  >>> john.firstName = u'John'
  >>> johnC._firstName
  u'John'
  >>> john.lastName is None
  True
  >>> john.someOtherAttribute
  Traceback (most recent call last):
  ...
  AttributeError: ... someOtherAttribute

We can use the age calculations from the base Person class:

  >>> from datetime import date
  >>> john.birthDate = date(1980, 3, 26)
  >>> john.ageAt(date(2006, 5, 12))
  26
  >>> john.age >= 26
  True

A person can be associated with a user of the system by setting the
userId attribute; this will also register the person concept in the
corresponding principal annotations so that there is a fast way to find
the person(s) belonging to a user/principal.

For testing, we first have to provide the needed utilities and settings
(in real life this is all done during Zope startup):

  >>> auth = setupData.auth
  >>> principalAnnotations = setupData.principalAnnotations

  >>> principal = auth.definePrincipal('users.john', u'John', login='john')
  >>> john.userId = 'users.john'

  >>> annotations = principalAnnotations.getAnnotationsById('users.john')
  >>> from loops.organize.party import ANNOTATION_KEY
  >>> annotations[ANNOTATION_KEY][loopsId] == johnC
  True

Change a userId assignment:

  >>> principal = auth.definePrincipal('users.johnny', u'Johnny', login='johnny')
  >>> john.userId = 'users.johnny'

  >>> annotations = principalAnnotations.getAnnotationsById('users.johnny')
  >>> annotations[ANNOTATION_KEY][loopsId] == johnC
  True
  >>> annotations = principalAnnotations.getAnnotationsById('users.john')
  >>> annotations[ANNOTATION_KEY][loopsId] is None
  True

  >>> john.userId = None
  >>> annotations = principalAnnotations.getAnnotationsById('users.johnny')
  >>> annotations[ANNOTATION_KEY][loopsId] is None
  True

Deleting a person with a userId assigned removes the corresponding
principal annotation:

  >>> from zope.app.container.interfaces import IObjectRemovedEvent
  >>> from zope.app.container.contained import ObjectRemovedEvent
  >>> from zope.event import notify
  >>> from zope.interface import Interface
  >>> from loops.interfaces import IConcept
  >>> from loops.organize.party import removePersonReferenceFromPrincipal
  >>> from zope.app.testing import ztapi
  >>> ztapi.subscribe([IConcept, IObjectRemovedEvent], None,
  ...                 removePersonReferenceFromPrincipal)

  >>> john.userId = 'users.john'
  >>> annotations = principalAnnotations.getAnnotationsById('users.john')
  >>> annotations[ANNOTATION_KEY][loopsId] == johnC
  True

  >>> del concepts['john']
  >>> annotations = principalAnnotations.getAnnotationsById('users.john')
  >>> annotations[ANNOTATION_KEY][loopsId] is None
  True

If we try to assign a userId of a principal that already has a person
concept assigned we should get an error:

  >>> johnC = concepts['john'] = Concept(u'John')
  >>> johnC.conceptType = person
  >>> john = IPerson(johnC)
  >>> john.userId = 'users.john'

  >>> marthaC = concepts['martha'] = Concept(u'Martha')
  >>> marthaC.conceptType = person
  >>> martha = IPerson(marthaC)

  >>> martha.userId = 'users.john'
  Traceback (most recent call last):
  ...
  ValueError: ...


Member Registrations
====================

The member registration needs the whole pluggable authentication stuff
with a principal folder:

  >>> from zope.app.appsetup.bootstrap import ensureUtility
  >>> from zope.app.authentication.authentication import PluggableAuthentication
  >>> from zope.app.security.interfaces import IAuthentication
  >>> ensureUtility(site, IAuthentication, '', PluggableAuthentication,
  ...               copy_to_zlog=False, asObject=True)
  <...PluggableAuthentication...>
  >>> pau = component.getUtility(IAuthentication, context=site)

  >>> from zope.app.authentication.principalfolder import PrincipalFolder
  >>> from zope.app.authentication.interfaces import IAuthenticatorPlugin
  >>> pFolder = PrincipalFolder('loops.')
  >>> pau['loops'] = pFolder
  >>> pau.authenticatorPlugins = ('loops',)

In addition, we have to create at least one node in the view space
and register an IMemberRegistrationManager adapter for the loops root object:

  >>> from loops.view import Node
  >>> menu = views['menu'] = Node('Home')
  >>> menu.nodeType = 'menu'

  >>> from loops.organize.member import MemberRegistrationManager
  >>> component.provideAdapter(MemberRegistrationManager)

Now we can enter the registration info for a new member (after having made
sure that a principal object can be served by a corresponding factory):

  >>> from zope.app.authentication.principalfolder import FoundPrincipalFactory
  >>> component.provideAdapter(FoundPrincipalFactory)

  >>> data = {'loginName': u'newuser',
  ...         'password': u'quack',
  ...         'passwordConfirm': u'quack',
  ...         'lastName': u'Sawyer',
  ...         'firstName': u'Tom',
  ...         'email': u'tommy@sawyer.com',
  ...         'action': 'update',}

and register it.

  >>> from zope.publisher.browser import TestRequest
  >>> request = TestRequest(form=data)
  >>> from loops.organize.browser.member import MemberRegistration
  >>> regView = MemberRegistration(menu, request)
  >>> regView.update()
  False

  >>> from loops.common import adapted
  >>> person = concepts['person.newuser']
  >>> pa = adapted(person)
  >>> pa.lastName, pa.userId
  (u'Sawyer', u'loops.newuser')

Now we can also retrieve it from the authentication utility:

  >>> pau.getPrincipal('loops.newuser').title
  u'Tom Sawyer'

Change Password
---------------

  >>> data = {'oldPassword': u'tiger',
  ...         'password': u'lion',
  ...         'passwordConfirm': u'lion',
  ...         'action': 'update'}

  >>> request = TestRequest(form=data)

We need a principal for testing the login stuff:

  >>> from zope.app.authentication.principalfolder import InternalPrincipal
  >>> principal = InternalPrincipal('scott', 'tiger', 'Scotty')
  >>> request.setPrincipal(principal)

  >>> from loops.organize.browser.member import PasswordChange
  >>> pwcView = PasswordChange(menu, request)
  >>> pwcView.update()
  False


Security
========

Automatic security settings on persons
--------------------------------------

  >>> from zope.traversing.api import getName
  >>> list(sorted(getName(c) for c in concepts['person'].getChildren()))
  [u'john', u'martha', u'person.newuser']

Person objects that have a user assigned to them receive this user
(principal) as their owner.

  >>> from zope.app.securitypolicy.interfaces import IPrincipalRoleMap
  >>> IPrincipalRoleMap(concepts['john']).getPrincipalsAndRoles()
  [('loops.Owner', 'users.john', PermissionSetting: Allow)]
  >>> IPrincipalRoleMap(concepts['person.newuser']).getPrincipalsAndRoles()
  [('loops.Owner', u'loops.newuser', PermissionSetting: Allow)]

The person ``martha`` hasn't got a user id, so there is no role assigned
to it.

  >>> IPrincipalRoleMap(concepts['martha']).getPrincipalsAndRoles()
  []

Only the owner (and a few other privileged people) should be able
to edit a person object.

We also need an interaction with a participation based on the principal
whose permissions we want to check.

  >>> from zope.app.authentication.principalfolder import Principal
  >>> pJohn = Principal('users.john', 'xxx', u'John')

  >>> from loops.tests.auth import login
  >>> login(pJohn)

We also want to grant some global permissions and roles, i.e. on the site or
loops root level.

  >>> rolePermissions = setupData.rolePermissions
  >>> rolePermissions.grantPermissionToRole('zope.View', 'zope.Member')

  >>> principalRoles = setupData.principalRoles
  >>> principalRoles.assignRoleToPrincipal('zope.Member', 'users.john')

Now we are ready to look for the real stuff - what John is allowed to do.

  >>> from zope.security import canAccess, canWrite, checkPermission
  >>> john = concepts['john']

  >>> canAccess(john, 'title')
  True

Person objects that have an owner may be modified by this owner.

  >>> canWrite(john, 'title')
  True

So let's try with another user with another role setting.

  >>> rolePermissions.grantPermissionToRole('zope.ManageContent', 'loops.Staff')
  >>> principalRoles.assignRoleToPrincipal('loops.Staff', 'users.martha')
  >>> principalRoles.assignRoleToPrincipal('zope.Member', 'users.martha')

  >>> pMartha = Principal('users.martha', 'xxx', u'Martha')
  >>> login(pMartha)

  >>> canAccess(john, 'title')
  True
  >>> canWrite(john, 'title')
  False

If we clear the userId attribute from a person object others may be allowed
again to edit it...

  >>> adapted(john).userId = ''
  >>> canWrite(john, 'title')
  True

... but John no more...

  >>> login(pJohn)
  >>> canWrite(john, 'title')
  False


Tasks and Events
================

Task view with edit action
--------------------------

  >>> from loops.setup import addAndConfigureObject
  >>> from loops.organize.interfaces import ITask
  >>> task = addAndConfigureObject(concepts, Concept, 'task', title=u'Task',
  ...                              conceptType=type, typeInterface=ITask)

  >>> from loops.organize.task import Task
  >>> component.provideAdapter(Task)

  >>> task01 = addAndConfigureObject(concepts, Concept, 'task01',
  ...                                title=u'Task #1', conceptType=task)

  >>> from loops.organize.browser.task import TaskView
  >>> view = TaskView(task01, TestRequest())
  >>> view.getActions('portlet')
  [<loops.browser.action.DialogAction ...>]

Events listing
--------------

  >>> event = addAndConfigureObject(concepts, Concept, 'event', title=u'Event',
  ...                               conceptType=type, typeInterface=ITask)
  >>> event01 = addAndConfigureObject(concepts, Concept, 'event01',
  ...                                 title=u'Event #1', conceptType=event,
  ...                           )

  >>> from loops.organize.browser.event import Events
  >>> listing = Events(johnC, TestRequest())
  >>> listing.getActions('portlet')
  [<loops.browser.action.DialogAction ...>]

  >>> list(listing.events())
  [<loops.browser.concept.ConceptRelationView ...>]


Fin de partie
=============

  >>> placefulTearDown()
