loops/organize
2011-09-29 22:27:06 +02:00
..
browser work in progress: restrict mail recipients to users who have access to the target object; future extension: select recipients by groups 2011-07-24 14:39:45 +02:00
comment use loops-specific print.css, hiding all parts not needed in printed output 2010-11-15 16:15:59 +00:00
job work in progress: loops site synchronization: export (and subsequent import) of changes 2010-02-14 09:51:49 +00:00
personal filtering functionality basically working, including search and standard concept listings 2011-02-07 16:53:08 +00:00
process work in progress: make loops package work with BlueBream 1.0 2011-09-29 19:21:12 +02:00
stateful make (old) state query (loops.organize.stateful) work again correctly with new action stuff in loops.expert 2011-08-31 13:48:28 +02:00
tracking - allow editing/creation of work items by others (creator, admin), 2011-08-03 16:29:51 +02:00
work provide new query for retrieving work items independently of task or user 2011-08-24 15:17:04 +02:00
__init__.py include setup upon loops site creation in the organize and knowledge packages 2006-06-11 14:37:18 +00:00
auth.py provide password retrieval for person-based authenticator 2009-09-29 14:56:52 +00:00
configure.zcml make loops package work with BlueBream 1.0: system running 2011-09-29 22:27:06 +02:00
interfaces.py work in progress: make loops package work with BlueBream 1.0 2011-09-29 19:21:12 +02:00
member.py extend/improve form/processing for registering users, clean up options 2009-04-11 10:07:14 +00:00
memberinfo.py merged Dojo 1.0 branch 2008-02-10 09:56:42 +00:00
party.py allow for adoption of relations to a predicate interface; 2011-09-23 09:39:32 +02:00
presence.py new view list_children.html; action options for queries; send mail feature basically working; presence with link to person 2009-10-12 06:05:14 +00:00
README.txt make loops package work with BlueBream 1.0: system running 2011-09-29 22:27:06 +02:00
schema.py avoid error when userId field already removed from schema fields for form generation 2010-06-18 07:02:04 +00:00
setup.py provide predicateInterface field for IPredicate; set up example 'IAllocated' 2008-07-31 07:21:35 +00:00
task.py work in progress: task indexing: add index for start date, include work item texts in text index entry 2009-04-20 06:43:08 +00:00
tests.py make loops package work with BlueBream 1.0: system running 2011-09-29 22:27:06 +02:00
util.py work in progress: make loops package work with BlueBream 1.0 2011-09-29 19:21:12 +02:00

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