diff --git a/common.py b/common.py
index fccb711..3229b09 100644
--- a/common.py
+++ b/common.py
@@ -58,29 +58,23 @@ def adapted(obj, langInfo=None):
# helper functions for specifying automatic attribute handling
-def adapterAttributes(*args):
+def collectAttributeNames(lst, name):
attrs = []
- for arg in args:
+ for arg in lst:
if isinstance(arg, basestring):
attrs.append(arg)
elif isinstance(arg, type):
- attrs.extend(list(arg._adapterAttributes))
+ attrs.extend(list(getattr(arg, name)))
else:
raise ValueError("Argument must be string or class, '%s' is '%s'." %
(arg, type(arg)))
return tuple(attrs)
+def adapterAttributes(*args):
+ return collectAttributeNames(args, '_adapterAttributes')
+
def contextAttributes(*args):
- attrs = []
- for arg in args:
- if isinstance(arg, basestring):
- attrs.append(arg)
- elif isinstance(arg, type):
- attrs.extend(list(arg._contextAttributes))
- else:
- raise ValueError("Argument must be string or class, '%s' is '%s'." %
- (arg, type(arg)))
- return attrs
+ return collectAttributeNames(args, '_contextAttributes')
# type interface adapters
@@ -133,7 +127,7 @@ class ResourceAdapterBase(AdapterBase):
implements(IStorageInfo)
adapts(IResource)
- _adapterAttributes = ('storageName', 'storageParams', ) + AdapterBase._adapterAttributes
+ _adapterAttributes = adapterAttributes('storageName', 'storageParams', AdapterBase)
_contextAttributes = list(IResourceAdapter)
storageName = None
diff --git a/configure.zcml b/configure.zcml
index 8910dcc..f237c72 100644
--- a/configure.zcml
+++ b/configure.zcml
@@ -331,6 +331,7 @@
-->
+
-
-
diff --git a/organize/configure.zcml b/organize/configure.zcml
index 8a68ee2..e54ca82 100644
--- a/organize/configure.zcml
+++ b/organize/configure.zcml
@@ -43,7 +43,6 @@
-
@@ -70,5 +69,8 @@
+
+
+
diff --git a/organize/personal/README.txt b/organize/personal/README.txt
index e076c97..e4e2dfe 100644
--- a/organize/personal/README.txt
+++ b/organize/personal/README.txt
@@ -38,55 +38,9 @@ In order to be able to login and store favorites and other personal data
we have to prepare our environment. We need some basic adapter registrations,
and a pluggable authentication utility with a principal folder.
- >>> from loops.organize.tests import setupUtilitiesAndAdapters
- >>> setupData = setupUtilitiesAndAdapters(loopsRoot)
-
- >>> 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('users.')
- >>> pau['users'] = pFolder
- >>> pau.authenticatorPlugins = ('users',)
-
-So we can now register a user ...
-
- >>> from zope.app.authentication.principalfolder import InternalPrincipal
- >>> pFolder['john'] = InternalPrincipal('john', 'xx', u'John')
- >>> from zope.app.authentication.principalfolder import FoundPrincipalFactory
- >>> component.provideAdapter(FoundPrincipalFactory)
-
-... and create a corresponding person.
-
- >>> from loops.concept import Concept
- >>> johnC = concepts['john'] = Concept(u'John')
- >>> person = concepts['person']
- >>> johnC.conceptType = person
- >>> from loops.common import adapted
- >>> adapted(johnC).userId = 'users.john'
-
-Finally, we log in as the newly created user.
-
- >>> from zope.app.authentication.principalfolder import Principal
- >>> pJohn = Principal('users.john', 'xxx', u'John')
-
- >>> from loops.tests.auth import login
- >>> login(pJohn)
-
-One step is still missing: As we are now working with a real principal
-the security checks e.g. in views are active. So we have to provide
-our user with the necessary permissions.
-
- >>> grantPermission = setupData.rolePermissions.grantPermissionToRole
- >>> assignRole = setupData.principalRoles.assignRoleToPrincipal
- >>> grantPermission('zope.View', 'zope.Member')
- >>> assignRole('zope.Member', 'users.john')
+ >>> from loops.organize.tests import setupObjectsForTesting
+ >>> setupData = setupObjectsForTesting(site, concepts)
+ >>> johnC = setupData.johnC
Working with the favorites storage
----------------------------------
@@ -116,7 +70,7 @@ The adapter provides convenience methods for accessing the favorites storage.
So we are now ready to query the favorites.
- >>> favs = favorites.query(userName=johnCId)
+ >>> favs = list(favorites.query(userName=johnCId))
>>> favs
[]
@@ -149,7 +103,7 @@ Let's now trigger the saving of a favorite.
>>> view.add()
- >>> len(favorites.query(userName=johnCId))
+ >>> len(list(favorites.query(userName=johnCId)))
2
>>> d002Id = util.getUidForObject(resources['d001.txt'])
@@ -157,5 +111,11 @@ Let's now trigger the saving of a favorite.
>>> view = FavoriteView(home, request)
>>> view.remove()
- >>> len(favorites.query(userName=johnCId))
+ >>> len(list(favorites.query(userName=johnCId)))
1
+
+
+Fin de partie
+=============
+
+ >>> placefulTearDown()
diff --git a/process/README.txt b/organize/process/README.txt
similarity index 95%
rename from process/README.txt
rename to organize/process/README.txt
index dd1bf66..340912f 100644
--- a/process/README.txt
+++ b/organize/process/README.txt
@@ -31,7 +31,7 @@ ZCML setup):
>>> from loops.interfaces import ILoops
>>> from loops.setup import ISetupManager
- >>> from loops.process.setup import SetupManager
+ >>> from loops.organize.process.setup import SetupManager
>>> component.provideAdapter(SetupManager, (ILoops,), ISetupManager,
... name='process')
@@ -53,7 +53,7 @@ Manage processes
The classes used in this package are just adapters to IConcept.
- >>> from loops.process.definition import Process
+ >>> from loops.organize.process.definition import Process
>>> from cybertools.process.interfaces import IProcess
>>> component.provideAdapter(Process, (IConcept,), IProcess)
diff --git a/process/__init__.py b/organize/process/__init__.py
similarity index 100%
rename from process/__init__.py
rename to organize/process/__init__.py
diff --git a/process/configure.zcml b/organize/process/configure.zcml
similarity index 75%
rename from process/configure.zcml
rename to organize/process/configure.zcml
index a406305..71c2583 100644
--- a/process/configure.zcml
+++ b/organize/process/configure.zcml
@@ -8,11 +8,10 @@
-
-
-
+
-
diff --git a/process/definition.py b/organize/process/definition.py
similarity index 100%
rename from process/definition.py
rename to organize/process/definition.py
diff --git a/process/interfaces.py b/organize/process/interfaces.py
similarity index 100%
rename from process/interfaces.py
rename to organize/process/interfaces.py
diff --git a/process/setup.py b/organize/process/setup.py
similarity index 100%
rename from process/setup.py
rename to organize/process/setup.py
diff --git a/process/tests.py b/organize/process/tests.py
similarity index 100%
rename from process/tests.py
rename to organize/process/tests.py
diff --git a/stateful/README.txt b/organize/stateful/README.txt
similarity index 94%
rename from stateful/README.txt
rename to organize/stateful/README.txt
index 5eaca2a..384eefd 100644
--- a/stateful/README.txt
+++ b/organize/stateful/README.txt
@@ -32,7 +32,7 @@ making an object statful we'll use an adapter.
>>> component.provideUtility(simplePublishing, IStatesDefinition,
... name='loops.simple_publishing')
- >>> from loops.stateful.base import SimplePublishable
+ >>> from loops.organize.stateful.base import SimplePublishable
>>> component.provideAdapter(SimplePublishable, name='loops.simple_publishing')
We may now take a document and adapt it to IStateful so that we may
@@ -57,3 +57,9 @@ not just kept in the adapter.
>>> statefulDoc01.state
'published'
+
+
+Fin de partie
+=============
+
+ >>> placefulTearDown()
diff --git a/stateful/__init__.py b/organize/stateful/__init__.py
similarity index 100%
rename from stateful/__init__.py
rename to organize/stateful/__init__.py
diff --git a/stateful/base.py b/organize/stateful/base.py
similarity index 100%
rename from stateful/base.py
rename to organize/stateful/base.py
diff --git a/stateful/configure.zcml b/organize/stateful/configure.zcml
similarity index 81%
rename from stateful/configure.zcml
rename to organize/stateful/configure.zcml
index d541a00..37923c9 100644
--- a/stateful/configure.zcml
+++ b/organize/stateful/configure.zcml
@@ -10,9 +10,9 @@
name="loops.simple_publishing" />
-
+
>> 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.organize.tracking.setup import SetupManager
+ >>> component.provideAdapter(SetupManager, name='organize.tracking')
+
+ >>> from loops.tests.setup import TestSite
+ >>> t = TestSite(site)
+ >>> concepts, resources, views = t.setup()
+
+
+Tracking Changes and Object Access
+==================================
+
+ >>> loopsRoot = concepts.getLoopsRoot()
+ >>> records = loopsRoot.getRecordManager()
+ >>> changes = records['changes']
+
+User management setup
+---------------------
+
+In order to be able to login and store personal data
+we have to prepare our environment. We need some basic adapter registrations,
+and a pluggable authentication utility with a principal folder.
+
+ >>> from loops.organize.tests import setupObjectsForTesting
+ >>> setupData = setupObjectsForTesting(site, concepts)
+ >>> johnC = setupData.johnC
+
+Recording changes to objects
+----------------------------
+
+ >>> from loops.organize.tracking.change import recordModification
+ >>> component.provideHandler(recordModification)
+
+ >>> tTask = concepts['task']
+ >>> from loops.concept import Concept
+ >>> from loops.setup import addAndConfigureObject
+ >>> t01 = addAndConfigureObject(concepts, Concept, 't01', conceptType=tTask,
+ ... title='Develop change tracking')
+
+ >>> len(changes)
+ 1
+
+
+Fin de partie
+=============
+
+ >>> placefulTearDown()
diff --git a/organize/tracking/__init__.py b/organize/tracking/__init__.py
new file mode 100644
index 0000000..38314f3
--- /dev/null
+++ b/organize/tracking/__init__.py
@@ -0,0 +1,3 @@
+"""
+$Id$
+"""
diff --git a/organize/tracking/change.py b/organize/tracking/change.py
new file mode 100644
index 0000000..5a22ff7
--- /dev/null
+++ b/organize/tracking/change.py
@@ -0,0 +1,91 @@
+#
+# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+"""
+Recording changes to loops objects.
+
+$Id$
+"""
+
+from zope.app.container.interfaces import IObjectAddedEvent, IObjectRemovedEvent
+from zope.app.container.interfaces import IObjectMovedEvent
+from zope.cachedescriptors.property import Lazy
+from zope.component import adapter
+from zope.lifecycleevent.interfaces import IObjectModifiedEvent, IObjectCreatedEvent
+
+from cybertools.tracking.btree import Track, getTimeStamp
+from loops.concept import ConceptManager
+from loops.resource import ResourceManager
+from loops.interfaces import IAssignmentEvent, IDeassignmentEvent
+from loops.interfaces import ILoopsObject
+from loops.organize.party import getPersonForUser
+from loops.security.common import getCurrentPrincipal
+from loops import util
+
+
+class ChangeManager(object):
+
+ context = None
+
+ def __init__(self, context):
+ if isinstance(context, (ConceptManager, ResourceManager)):
+ return
+ self.context = context
+
+ @Lazy
+ def valid(self):
+ return not (self.context is None or
+ self.storage is None or
+ self.person is None)
+
+ @Lazy
+ def loopsRoot(self):
+ return self.context.getLoopsRoot()
+
+ @Lazy
+ def storage(self):
+ records = self.loopsRoot.getRecordManager()
+ if records is not None:
+ return records.get('changes')
+ return None
+
+ @Lazy
+ def person(self):
+ principal = getCurrentPrincipal()
+ if principal is not None:
+ return getPersonForUser(self.context, principal=principal)
+ return None
+
+ def recordModification(self, event=None):
+ if not self.valid:
+ return
+ uid = util.getUidForObject(self.context)
+ personUid = util.getUidForObject(self.person)
+ last = self.storage.getLastUserTrack(uid, 0, personUid)
+ if last is None or last.metadata['timeStamp'] < getTimeStamp() - 5:
+ self.storage.saveUserTrack(uid, 0, personUid, dict(action='modify'))
+
+
+class ChangeRecord(Track):
+
+ typeName = 'ChangeRecord'
+
+
+@adapter(ILoopsObject, IObjectModifiedEvent)
+def recordModification(obj, event):
+ ChangeManager(obj).recordModification(event)
diff --git a/organize/tracking/configure.zcml b/organize/tracking/configure.zcml
new file mode 100644
index 0000000..c192bec
--- /dev/null
+++ b/organize/tracking/configure.zcml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/organize/tracking/setup.py b/organize/tracking/setup.py
new file mode 100644
index 0000000..6f4fbdf
--- /dev/null
+++ b/organize/tracking/setup.py
@@ -0,0 +1,38 @@
+#
+# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+"""
+Automatic setup of a loops site for the organize.tracking package.
+
+$Id$
+"""
+
+from zope.component import adapts
+from zope.interface import implements, Interface
+
+from cybertools.tracking.btree import TrackingStorage
+from loops.organize.tracking.change import ChangeRecord
+from loops.setup import SetupManager as BaseSetupManager
+
+
+class SetupManager(BaseSetupManager):
+
+ def setup(self):
+ records = self.context.getRecordManager()
+ changes = self.addObject(records, TrackingStorage, 'changes',
+ trackFactory=ChangeRecord)
diff --git a/organize/tracking/tests.py b/organize/tracking/tests.py
new file mode 100755
index 0000000..310c225
--- /dev/null
+++ b/organize/tracking/tests.py
@@ -0,0 +1,22 @@
+# $Id$
+
+import unittest, doctest
+from zope.testing.doctestunit import DocFileSuite
+
+
+class Test(unittest.TestCase):
+ "Basic tests for the loops.organize.tracking package."
+
+ def testBasics(self):
+ pass
+
+
+def test_suite():
+ flags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
+ return unittest.TestSuite((
+ unittest.makeSuite(Test),
+ DocFileSuite('README.txt', optionflags=flags),
+ ))
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
diff --git a/organize/util.py b/organize/util.py
index 6189896..d304342 100644
--- a/organize/util.py
+++ b/organize/util.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2006 Helmut Merz helmutm@cy55.de
+# Copyright (c) 2007 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -82,3 +82,11 @@ def getInternalPrincipal(id, context=None):
if next is not None:
return next.getPrincipal(pau.prefix + id)
raise PrincipalLookupError(id)
+
+
+def getTrackingStorage(obj, name):
+ records = obj.getLoopsRoot().getRecordManager()
+ if records is not None:
+ return records.get(name)
+ return None
+