Skip to content

Commit

Permalink
Implement tile book-keeping. See tests.py for some examples of how to…
Browse files Browse the repository at this point in the history
… use it.

svn path=/plone.app.tiles/trunk/; revision=32549
  • Loading branch information
optilude committed Dec 20, 2009
1 parent 636069d commit 61605b6
Show file tree
Hide file tree
Showing 8 changed files with 233 additions and 22 deletions.
4 changes: 0 additions & 4 deletions docs/TODO.txt
@@ -1,7 +1,3 @@
plone.app.tiles todo
====================

[ ] Book-keeping for which tiles have been persisted in annotations, to allow
a "delete all" or selection of tile ids to delete. This may be done using
events.

74 changes: 74 additions & 0 deletions plone/app/tiles/bookkeeping.py
@@ -0,0 +1,74 @@
"""Event handlers to keep a list of tiles
"""

from BTrees.OOBTree import OOBTree

from zope.component import adapts
from zope.interface import implements
from zope.component import adapter

from zope.annotation.interfaces import IAnnotatable, IAnnotations

from zope.lifecycleevent.interfaces import IObjectAddedEvent
from zope.lifecycleevent.interfaces import IObjectRemovedEvent

from plone.tiles.interfaces import ITile

from plone.app.tiles.interfaces import ITileBookkeeping

ANNOTATIONS_KEY = "plone.app.tiles.bookkeeping"

@adapter(ITile, IObjectAddedEvent)
def recordTileAdded(tile, event):
"""Inform the ITileBookkeeping adapter for the tile's new parent that
it has just been added.
"""

bookkeeping = ITileBookkeeping(event.newParent, None)
if bookkeeping is not None:
bookkeeping.added(tile.__name__, tile.id)

@adapter(ITile, IObjectRemovedEvent)
def recordTileRemoved(tile, event):
"""Inform the ITileBookkeeping adapter for the tile's old parent that
it has just been removed.
"""

bookkeeping = ITileBookkeeping(event.oldParent, None)
if bookkeeping is not None:
bookkeeping.removed(tile.id)

class AnnotationsTileBookkeeping(object):
"""Default adapter for tile bookkeeping.
This stores a btree in annotations on the content object, with tile ids
as keys and tile types as values.
"""

implements(ITileBookkeeping)
adapts(IAnnotatable)

def __init__(self, context):
self.context = context
self.annotations = IAnnotations(context)

def added(self, tileType, tileId):
tree = self.annotations.setdefault(ANNOTATIONS_KEY, OOBTree())
tree[tileId] = tileType

def removed(self, tileId):
tree = self.annotations.get(ANNOTATIONS_KEY, {})
if tileId in tree:
del tree[tileId]
return True
return False

def typeOf(self, tileId):
tree = self.annotations.get(ANNOTATIONS_KEY, {})
return tree.get(tileId, None)

def enumerate(self, tileType=None):
tree = self.annotations.setdefault(ANNOTATIONS_KEY, {})
for tileId, tileTypeStored in tree.iteritems():
if tileType is None or tileTypeStored == tileType:
yield (tileId, tileTypeStored,)
3 changes: 2 additions & 1 deletion plone/app/tiles/browser/add.py
@@ -1,7 +1,7 @@
from z3c.form import form, button
from plone.z3cform import layout

from zope.lifecycleevent import ObjectCreatedEvent
from zope.lifecycleevent import ObjectCreatedEvent, ObjectAddedEvent
from zope.event import notify

from zope.traversing.browser.absoluteurl import absoluteURL
Expand Down Expand Up @@ -61,6 +61,7 @@ def handleAdd(self, action):
tileURL = absoluteURL(tile, self.request)

notify(ObjectCreatedEvent(tile))
notify(ObjectAddedEvent(tile, self.context, self.tileId))

IStatusMessage(self.request).addStatusMessage(
_(u"Tile created at ${url}", mapping={'url': tileURL}), type=u'info'
Expand Down
7 changes: 2 additions & 5 deletions plone/app/tiles/browser/delete.py
@@ -1,6 +1,8 @@
from zope.component import getAllUtilitiesRegisteredFor
from zope.component import getUtility

from zope.lifecycleevent import ObjectRemovedEvent

from AccessControl import Unauthorized
from Products.Five.browser import BrowserView

Expand All @@ -12,11 +14,6 @@

from plone.memoize.view import memoize

try:
from zope.lifecycleevent import ObjectRemovedEvent
except ImportError:
from zope.app.container.contained import ObjectRemovedEvent

class TileDelete(BrowserView):
"""Delete a given tile
"""
Expand Down
6 changes: 6 additions & 0 deletions plone/app/tiles/configure.zcml
Expand Up @@ -5,8 +5,14 @@
<include package="plone.tiles" />
<include package="plone.tiles" file="meta.zcml"/>

<!-- Views -->
<include package=".browser" />

<!-- Bookkeeping -->
<subscriber handler=".bookkeeping.recordTileAdded" />
<subscriber handler=".bookkeeping.recordTileRemoved" />
<adapter factory=".bookkeeping.AnnotationsTileBookkeeping" />

<!-- TODO: Remove once we stop doing TTW testing -->
<include file="demo.zcml" />

Expand Down
30 changes: 30 additions & 0 deletions plone/app/tiles/interfaces.py
@@ -1,3 +1,4 @@
from zope.interface import Interface
from zope.publisher.interfaces.browser import IBrowserView

class ITileAddView(IBrowserView):
Expand All @@ -15,3 +16,32 @@ class ITileEditView(IBrowserView):
this interface. Per-tile type overrides can be created by registering
named adapters matching the tile name.
"""

class ITileBookkeeping(Interface):
"""Tile bookkeeping information.
Adapt a content object to this interface to obtain information about tiles
associated with that content item. The default implementation stores this
information in annotations, and maintains it using event handlers for
``IObjectAddedEvent`` and ``IObjectRemovedEvent`` for ``ITile``.
"""

def added(tileType, tileId):
"""Record that a tile of the given type (a string) and id (also a
string) was added.
"""

def removed(tileType, tileId):
"""Record that a tile with the given id (a string) was removed.
"""

def typeOf(tileId):
"""Given a tile id, return its type (a strong), or None if the tile
cannot be found.
"""

def enumerate(tileType=None):
"""Obtain an iterator for all tiles which have been recorded. The
iterator returns tuples of strings ``(tileId, tileType)``. If
``tileType`` is given, return only tiles of that type.
"""
129 changes: 118 additions & 11 deletions plone/app/tiles/tests.py
Expand Up @@ -9,9 +9,15 @@

import plone.app.tiles

from zope.event import notify
from zope.lifecycleevent import ObjectAddedEvent, ObjectRemovedEvent

from zope.annotation.interfaces import IAnnotations

from plone.tiles.interfaces import ITileDataManager
from plone.tiles.data import ANNOTATIONS_KEY_PREFIX

from plone.app.tiles.interfaces import ITileBookkeeping
from plone.app.tiles.demo import TransientTile

ptc.setupPloneSite()
Expand All @@ -33,6 +39,78 @@ class FunctionalTest(ptc.FunctionalTestCase):
def afterSetUp(self):
self.browser = Browser()
self.browser.handleErrors = False

def test_restrictedTraverse(self):

# The easiest way to look up a tile in Zope 2 is to use traversal:

traversed = self.portal.restrictedTraverse('@@plone.app.tiles.demo.transient/tile1')
self.failUnless(isinstance(traversed, TransientTile))
self.assertEquals('plone.app.tiles.demo.transient', traversed.__name__)
self.assertEquals('tile1', traversed.id)

def test_forensic_bookkeeping(self):

# The ITileBookkeeping adapter provides a means of listing the tiles
# in a given context and cleaning up leftover data

# Create a persistent tile with some data
tile1 = self.folder.restrictedTraverse('@@plone.app.tiles.demo.persistent/tile1')
ITileDataManager(tile1).set({'message': u"First message", 'counter': 1})
notify(ObjectAddedEvent(tile1, self.folder, 'tile1'))

# Also create two other tiles, for control purposes
tile2 = self.folder.restrictedTraverse('@@plone.app.tiles.demo.persistent/tile2')
ITileDataManager(tile2).set({'message': u"Second message", 'counter': 2})
notify(ObjectAddedEvent(tile2, self.folder, 'tile2'))

tile3 = self.folder.restrictedTraverse('@@plone.app.tiles.demo.transient/tile3')
ITileDataManager(tile3).set({'message': u"Third message"})
notify(ObjectAddedEvent(tile3, self.folder, 'tile3'))

# And some other tiles in another context
tile2a = self.portal.restrictedTraverse('@@plone.app.tiles.demo.persistent/tile2')
ITileDataManager(tile2a).set({'message': u"Second message", 'counter': 2})
notify(ObjectAddedEvent(tile2a, self.portal, 'tile2'))

tile4a = self.portal.restrictedTraverse('@@plone.app.tiles.demo.persistent/tile4')
ITileDataManager(tile4a).set({'message': u"Fourth message", 'counter': 4})
notify(ObjectAddedEvent(tile4a, self.portal, 'tile4'))

# Find the tiles again using ITileBookkeeping
bookkeeping = ITileBookkeeping(self.folder)

self.assertEquals([('tile1', 'plone.app.tiles.demo.persistent'), ('tile2', 'plone.app.tiles.demo.persistent'), ('tile3', 'plone.app.tiles.demo.transient')],
sorted(list(bookkeeping.enumerate())))
self.assertEquals([('tile1', 'plone.app.tiles.demo.persistent'), ('tile2', 'plone.app.tiles.demo.persistent')],
sorted(list(bookkeeping.enumerate('plone.app.tiles.demo.persistent'))))
self.assertEquals('plone.app.tiles.demo.persistent', bookkeeping.typeOf('tile1'))
self.assertEquals('plone.app.tiles.demo.persistent', bookkeeping.typeOf('tile2'))
self.assertEquals('plone.app.tiles.demo.transient', bookkeeping.typeOf('tile3'))

# Let's say we found 'tile1' in the enumeration list and we realised
# this tile was "lost" (e.g. no longer part of any valid page or site
# layout). If we wanted to clean up its data, we could now do this:

lostTileId = 'tile1'

lostTileType = bookkeeping.typeOf(lostTileId)
lostTileInstance = self.folder.restrictedTraverse('@@%s/%s' % (lostTileType, lostTileId,))
ITileDataManager(lostTileInstance).delete()
notify(ObjectRemovedEvent(lostTileInstance, self.folder, lostTileId))

# Verify that the tile we wanted to remove is gone
self.assertEquals([('tile2', 'plone.app.tiles.demo.persistent'), ('tile3', 'plone.app.tiles.demo.transient')],
sorted(list(bookkeeping.enumerate())))
self.assertEquals([('tile2', 'plone.app.tiles.demo.persistent')],
sorted(list(bookkeeping.enumerate('plone.app.tiles.demo.persistent'))))
self.assertEquals(None, bookkeeping.typeOf('tile1'))
self.assertEquals('plone.app.tiles.demo.persistent', bookkeeping.typeOf('tile2'))
self.assertEquals('plone.app.tiles.demo.transient', bookkeeping.typeOf('tile3'))

# Our other tiles are untouched
bookkeeping = ITileBookkeeping(self.portal)
self.assertEquals([('tile2', 'plone.app.tiles.demo.persistent'), ('tile4', 'plone.app.tiles.demo.persistent')], sorted(list(bookkeeping.enumerate())))

def test_transient_lifecycle(self):
# Log in
Expand All @@ -52,7 +130,13 @@ def test_transient_lifecycle(self):
'/@@edit-tile/plone.app.tiles.demo.transient/tile1?message=Test+message&' +
'tiledata=%7B%22action%22%3A%20%22save%22%2C%20%22url%22%3A%20%22http%3A//nohost/plone/Members/test_user_1_/%40%40plone.app.tiles.demo.transient/tile1%3Fmessage%3DTest%2Bmessage%22%2C%20%22type%22%3A%20%22plone.app.tiles.demo.transient%22%2C%20%22id%22%3A%20%22tile1%22%7D',
self.browser.url)


# Check bookkeeping information
bookkeeping = ITileBookkeeping(self.folder)
self.assertEquals([('tile1', 'plone.app.tiles.demo.transient')], list(bookkeeping.enumerate()))
self.assertEquals([('tile1', 'plone.app.tiles.demo.transient')], list(bookkeeping.enumerate('plone.app.tiles.demo.transient')))
self.assertEquals('plone.app.tiles.demo.transient', bookkeeping.typeOf('tile1'))

# View the tile
self.browser.open(self.folder.absolute_url() + '/@@plone.app.tiles.demo.transient/tile1?message=Test+message')
self.assertEquals("<html><body><b>Transient tile Test message</b></body></html>", self.browser.contents)
Expand All @@ -67,12 +151,18 @@ def test_transient_lifecycle(self):
'/@@edit-tile/plone.app.tiles.demo.transient/tile1?message=New+message&' +
'tiledata=%7B%22action%22%3A%20%22save%22%2C%20%22url%22%3A%20%22http%3A//nohost/plone/Members/test_user_1_/%40%40plone.app.tiles.demo.transient/tile1%3Fmessage%3DNew%2Bmessage%22%2C%20%22type%22%3A%20%22plone.app.tiles.demo.transient%22%2C%20%22id%22%3A%20%22tile1%22%7D',
self.browser.url)


# Check bookkeeping information
bookkeeping = ITileBookkeeping(self.folder)
self.assertEquals([('tile1', 'plone.app.tiles.demo.transient')], list(bookkeeping.enumerate()))
self.assertEquals([('tile1', 'plone.app.tiles.demo.transient')], list(bookkeeping.enumerate('plone.app.tiles.demo.transient')))
self.assertEquals('plone.app.tiles.demo.transient', bookkeeping.typeOf('tile1'))

# View the tile
self.browser.open(self.folder.absolute_url() + '/@@plone.app.tiles.demo.transient/tile1?message=New+message')
self.assertEquals("<html><body><b>Transient tile New message</b></body></html>", self.browser.contents)

# Remove the tile (really a no-op for transient tiles)
# Remove the tile
self.browser.open(self.folder.absolute_url() + '/@@delete-tile')
self.browser.getControl(name='id').value = 'tile1'
self.browser.getControl(name='type').value = ['plone.app.tiles.demo.transient']
Expand All @@ -81,6 +171,12 @@ def test_transient_lifecycle(self):
self.assertEquals('tile1', self.browser.getControl(name='deleted.id').value)
self.assertEquals('plone.app.tiles.demo.transient', self.browser.getControl(name='deleted.type').value)

# Check bookkeeping information
bookkeeping = ITileBookkeeping(self.folder)
self.assertEquals([], list(bookkeeping.enumerate()))
self.assertEquals([], list(bookkeeping.enumerate('plone.app.tiles.demo.transient')))
self.assertEquals(None, bookkeeping.typeOf('tile1'))

# Return to the content object
self.browser.getControl(label='OK').click()
self.assertEquals(self.folder.absolute_url() + '/view', self.browser.url)
Expand Down Expand Up @@ -115,6 +211,12 @@ def test_persistent_lifecycle(self):
self.assertEquals('Test message', folderAnnotations[annotationsKey]['message'])
self.assertEquals(1, folderAnnotations[annotationsKey]['counter'])

# Check bookkeeping information
bookkeeping = ITileBookkeeping(self.folder)
self.assertEquals([('tile2', 'plone.app.tiles.demo.persistent')], list(bookkeeping.enumerate()))
self.assertEquals([('tile2', 'plone.app.tiles.demo.persistent')], list(bookkeeping.enumerate('plone.app.tiles.demo.persistent')))
self.assertEquals('plone.app.tiles.demo.persistent', bookkeeping.typeOf('tile2'))

# View the tile
self.browser.open(self.folder.absolute_url() + '/@@plone.app.tiles.demo.persistent/tile2')
self.assertEquals("<html><body><b>Persistent tile Test message #1</b></body></html>", self.browser.contents)
Expand All @@ -133,7 +235,13 @@ def test_persistent_lifecycle(self):
# Verify annotations
self.assertEquals('New message', folderAnnotations[annotationsKey]['message'])
self.assertEquals(1, folderAnnotations[annotationsKey]['counter'])


# Check bookkeeping information
bookkeeping = ITileBookkeeping(self.folder)
self.assertEquals([('tile2', 'plone.app.tiles.demo.persistent')], list(bookkeeping.enumerate()))
self.assertEquals([('tile2', 'plone.app.tiles.demo.persistent')], list(bookkeeping.enumerate('plone.app.tiles.demo.persistent')))
self.assertEquals('plone.app.tiles.demo.persistent', bookkeeping.typeOf('tile2'))

# View the tile
self.browser.open(self.folder.absolute_url() + '/@@plone.app.tiles.demo.persistent/tile2')
self.assertEquals("<html><body><b>Persistent tile New message #1</b></body></html>", self.browser.contents)
Expand All @@ -150,16 +258,15 @@ def test_persistent_lifecycle(self):
# Verify annotations
self.assertEquals(None, folderAnnotations.get(annotationsKey))

# Check bookkeeping information
bookkeeping = ITileBookkeeping(self.folder)
self.assertEquals([], list(bookkeeping.enumerate()))
self.assertEquals([], list(bookkeeping.enumerate('plone.app.tiles.demo.persistent')))
self.assertEquals(None, bookkeeping.typeOf('tile2'))

# Return to the content object
self.browser.getControl(label='OK').click()
self.assertEquals(self.folder.absolute_url() + '/view', self.browser.url)

def test_restrictedTraverse(self):

traversed = self.portal.restrictedTraverse('@@plone.app.tiles.demo.transient/tile1')
self.failUnless(isinstance(traversed, TransientTile))
self.assertEquals('plone.app.tiles.demo.transient', traversed.__name__)
self.assertEquals('tile1', traversed.id)

def test_suite():
return unittest.defaultTestLoader.loadTestsFromName(__name__)
2 changes: 1 addition & 1 deletion setup.py
Expand Up @@ -30,7 +30,7 @@
'plone.memoize',
'z3c.form',
'zope.traversing',
'zope.lifecycleevent',
'zope.lifecycleevent >= 3.5.2',
'zope.event',
'zope.component',
'zope.event',
Expand Down

0 comments on commit 61605b6

Please sign in to comment.