===============================================================
loops - Linked Objects for Organization and Processing Services
===============================================================
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.expert.testsetup 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
>>> from loops.setup import addAndConfigureObject
>>> johnC = addAndConfigureObject(concepts, Concept, 'john', title='John',
... 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
'John'
>>> john.firstName = 'John'
>>> johnC._firstName
'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', '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', '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('John')
>>> johnC.conceptType = person
>>> john = IPerson(johnC)
>>> john.userId = 'users.john'
>>> john.email = 'john@loopz.org'
>>> marthaC = concepts['martha'] = Concept('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.pluggableauth import PluggableAuthentication
>>> from zope.authentication.interfaces import IAuthentication
>>> ensureUtility(site, IAuthentication, '', PluggableAuthentication,
... copy_to_zlog=False)
<...PluggableAuthentication...>
>>> pau = component.getUtility(IAuthentication, context=site)
>>> from zope.pluggableauth.plugins.principalfolder import PrincipalFolder
>>> from zope.pluggableauth.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': 'newuser',
... 'password': 'quack',
... 'passwordConfirm': 'quack',
... 'lastName': 'Sawyer',
... 'firstName': 'Tom',
... 'email': 'tommy@sawyer.com',
... 'form.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
('Sawyer', 'loops.newuser')
Now we can also retrieve it from the authentication utility:
>>> pau.getPrincipal('loops.newuser').title
'Tom Sawyer'
Change Password
---------------
>>> data = {'oldPassword': 'tiger',
... 'password': 'lion',
... 'passwordConfirm': '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
Reset Password
--------------
Invalidates the user account by generating a new password. A mail ist sent to
the email address of the person with a link for re-activating the account
and enter a new password.
>>> data = {'loginName': 'dummy',
... 'action': 'update'}
>>> request = TestRequest(form=data)
>>> from loops.organize.browser.member import PasswordReset
>>> pwrView = PasswordReset(menu, request)
>>> pwrView.update()
True
Pure Person-based Authentication
================================
The person-based authenticator provides authentication without having to
store a persistent (internal) principal object.
>>> from loops.organize.auth import PersonBasedAuthenticator
>>> pbAuth = PersonBasedAuthenticator('persons.')
>>> pau['persons'] = pbAuth
>>> pau.authenticatorPlugins = ('loops', 'persons',)
>>> eddieC = addAndConfigureObject(concepts, Concept, 'eddie', title='Eddie',
... conceptType=person)
>>> eddie = adapted(eddieC)
>>> eddie.userId = 'persons.eddie'
>>> pbAuth.setPassword('eddie', 'secret')
>>> pbAuth.authenticateCredentials(dict(login='eddie', password='secret'))
PrincipalInfo('persons.eddie')
Security
========
Automatic security settings on persons
--------------------------------------
>>> from zope.traversing.api import getName
>>> list(sorted(getName(c) for c in concepts['person'].getChildren()))
['jim', 'john', 'martha', 'person.newuser']
Person objects that have a user assigned to them receive this user
(principal) as their owner.
>>> from zope.securitypolicy.interfaces import IPrincipalRoleMap
>>> IPrincipalRoleMap(concepts['john']).getPrincipalsAndRoles()
[('loops.Person', 'users.john', PermissionSetting: Allow)]
>>> IPrincipalRoleMap(concepts['person.newuser']).getPrincipalsAndRoles()
[('loops.Person', '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', '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.
(Changed in 2013-01-14: Owner not set automatically)
>>> canWrite(john, 'title')
False
was: 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', '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.organize.interfaces import ITask
>>> task = addAndConfigureObject(concepts, Concept, 'task', title='Task',
... conceptType=type, typeInterface=ITask)
>>> from loops.organize.task import Task
>>> component.provideAdapter(Task)
>>> task01 = addAndConfigureObject(concepts, Concept, 'task01',
... title='Task #1', conceptType=task)
>>> from loops.organize.browser.task import TaskView
>>> view = TaskView(task01, TestRequest())
>>> list(view.getActions('portlet'))
[...]
OK, the action is not provided automatically any more by the TaskView
but has to be entered as a type option.
>>> adapted(task).options = ['action.portlet:editTask']
>>> view = TaskView(task01, TestRequest())
>>> list(view.getActions('portlet'))
[<loops.browser.action.DialogAction ...>]
Events listing
--------------
>>> event = addAndConfigureObject(concepts, Concept, 'event', title='Event',
... conceptType=type, typeInterface=ITask)
>>> event01 = addAndConfigureObject(concepts, Concept, 'event01',
... title='Event #1', conceptType=event,
... )
>>> from loops.organize.browser.event import Events
>>> events = addAndConfigureObject(concepts, Concept, 'events', title='Events',
... conceptType=concepts['query'])
>>> listing = Events(events, TestRequest())
>>> listing.getActions('portlet')
[<loops.browser.action.DialogAction ...>]
>>> from loops.config.base import QueryOptions
>>> from cybertools.meta.interfaces import IOptions
>>> component.provideAdapter(QueryOptions, provides=IOptions)
>>> list(listing.events())
[<loops.browser.concept.ConceptRelationView ...>]
Creation of follow-up event
---------------------------
>>> from loops.organize.browser.event import CreateFollowUpEvent
Send Email to Members
=====================
>>> menu.target = event01
>>> from loops.organize.browser.party import SendEmailForm
>>> form = SendEmailForm(menu, TestRequest())
>>> form.members
[{'title': 'John', ...}, {'title': 'Tom Sawyer', ...}]
[{'object': <...Person...>, 'email': 'john@loopz.org', 'title': 'John'},
{'object': <...Person...>, 'email': 'tommy@sawyer.com', 'title': 'Tom Sawyer'}]
>>> form.subject
"loops Notification from '$site'"
>>> form.mailBody
'\n\nEvent #1\nhttp://127.0.0.1/loops/views/menu/.118\n\n'
Show Presence of Other Users
============================
>>> from loops.organize.presence import Presence
>>> component.provideUtility(Presence())
Roles of Persons
================
When persons are assigned to a parent (e.g. an instutution or a project)
this assignment may be characterized by a certain role. This role may
be specified by using a special predicate that is associated with a
predicate interface that allows to specify the role.
(Note that the security-relevant assignment of persons is managed via
other special predicates: 'ismember', 'ismaster'. The 'hasrole'
predicate described here is intended for situations where the roles
may be chosen from an arbitrary list.)
>>> from loops.organize.interfaces import IHasRole
>>> predicate = concepts['predicate']
>>> hasRole = addAndConfigureObject(concepts, Concept, 'hasrole',
... title='has Role',
... conceptType=predicate, predicateInterface=IHasRole)
Let's now assign john to task01 and have a look at the relation created.
>>> task01.assignChild(john, hasRole)
>>> relation = task01.getChildRelations([hasRole])[0]
The role may be accessed by getting a relation adapter
>>> from loops.predicate import adaptedRelation
>>> adRelation = adaptedRelation(relation)
>>> adRelation.role is None
True
>>> adRelation.role = 'member'
>>> relation._role
'member'
>>> adRelation = adaptedRelation(relation)
>>> adRelation.role
'member'
Calendar
========
>>> from loops.organize.browser.event import CalendarInfo
>>> calendar = CalendarInfo(menu, TestRequest(cal_year=2009, cal_month=2))
>>> mc = calendar.monthCalendar
>>> mc
[[0, 0, 0, 0, 0, 0, 1],
[2, 3, 4, 5, 6, 7, 8],
[9, 10, 11, 12, 13, 14, 15],
[16, 17, 18, 19, 20, 21, 22],
[23, 24, 25, 26, 27, 28, 0]]
>>> calendar.getWeekNumber(mc[0])
5
>>> calendar.isToday(18)
False
Fin de partie
=============
>>> placefulTearDown()