477 lines
15 KiB
Text
477 lines
15 KiB
Text
===============================================================
|
|
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.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=u'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
|
|
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'
|
|
>>> john.email = 'john@loopz.org'
|
|
|
|
>>> 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)
|
|
<...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
|
|
|
|
|
|
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=u'Eddie',
|
|
... conceptType=person)
|
|
>>> eddie = adapted(eddieC)
|
|
>>> eddie.userId = 'persons.eddie'
|
|
|
|
>>> pbAuth.setPassword('eddie', 'secret')
|
|
>>> pbAuth.authenticateCredentials(dict(login='eddie', password='secret'))
|
|
PrincipalInfo(u'persons.eddie')
|
|
|
|
|
|
Security
|
|
========
|
|
|
|
Automatic security settings on persons
|
|
--------------------------------------
|
|
|
|
>>> from zope.traversing.api import getName
|
|
>>> list(sorted(getName(c) for c in concepts['person'].getChildren()))
|
|
[u'jim', 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.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.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())
|
|
>>> 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=u'Event',
|
|
... conceptType=type, typeInterface=ITask)
|
|
>>> event01 = addAndConfigureObject(concepts, Concept, 'event01',
|
|
... title=u'Event #1', conceptType=event,
|
|
... )
|
|
|
|
>>> from loops.organize.browser.event import Events
|
|
>>> events = addAndConfigureObject(concepts, Concept, 'events', title=u'Events',
|
|
... conceptType=concepts['query'])
|
|
>>> listing = Events(events, TestRequest())
|
|
>>> listing.getActions('portlet')
|
|
[<loops.browser.action.DialogAction ...>]
|
|
|
|
>>> from loops.config.base import QueryOptions
|
|
>>> component.provideAdapter(QueryOptions)
|
|
|
|
>>> list(listing.events())
|
|
[<loops.browser.concept.ConceptRelationView ...>]
|
|
|
|
|
|
Send Email to Members
|
|
=====================
|
|
|
|
>>> menu.target = event01
|
|
|
|
>>> from loops.organize.browser.party import SendEmailForm
|
|
>>> form = SendEmailForm(menu, TestRequest())
|
|
>>> form.members
|
|
[{'object': <...Person...>, 'email': 'john@loopz.org', 'title': u'John'},
|
|
{'object': <...Person...>, 'email': u'tommy@sawyer.com', 'title': u'Tom Sawyer'}]
|
|
>>> form.subject
|
|
u"loops Notification from '$site'"
|
|
>>> form.mailBody
|
|
u'\n\nEvent #1\nhttp://127.0.0.1/loops/views/menu/.97\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=u'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()
|