[z3-checkins] r5535 - in z3/pentecost: . branch tag trunk

philikon at codespeak.net philikon at codespeak.net
Mon Jul 12 15:58:22 MEST 2004


Author: philikon
Date: Mon Jul 12 15:58:21 2004
New Revision: 5535

Added:
   z3/pentecost/
   z3/pentecost/branch/
   z3/pentecost/tag/
   z3/pentecost/trunk/
   z3/pentecost/trunk/README.txt
   z3/pentecost/trunk/TODO.txt
   z3/pentecost/trunk/__init__.py
   z3/pentecost/trunk/btreecontainer.zcml
   z3/pentecost/trunk/configure.zcml
   z3/pentecost/trunk/interfaces.py
   z3/pentecost/trunk/pentecost-configure.zcml
   z3/pentecost/trunk/pentecost.py
   z3/pentecost/trunk/tests.py
   z3/pentecost/trunk/traverse.py
   z3/pentecost/trunk/vocabulary.py
Log:
Checked in a little piece of software that I hacked together
last weekend.  Pentecost can be used to i18n-ize regular content
components by using masquerading containers.  Any IContainer
implementation will do.  I would be interested in feedback.


Added: z3/pentecost/trunk/README.txt
==============================================================================
--- (empty file)
+++ z3/pentecost/trunk/README.txt	Mon Jul 12 15:58:21 2004
@@ -0,0 +1,61 @@
+==========
+README.txt
+==========
+
+``And they were all filled with the Holy Ghost, and began to speak
+with other tongues, as the Spirit gave them utterance.''
+
+							Acts 2:4
+
+
+Overview
+--------
+
+With Zope 3, it has become almost (almost!) trivial to make a web
+application i18n-aware.  The ``utilities/i18nextract.py`` script
+extracts i18n message strings from Python code, Page Templates and
+ZCML files.  If the message strings are only half decently tagged, the
+message catalog template can be handed to a translator and, voila!,
+you have a translated application.
+
+Yet, content often needs to be made available in different languages,
+too.  Thanks to a simple language negotiator API, content objects that
+need to provide data in different languages can be created with
+limited, but still existant effort.  This effort turns into an
+inconvenience when content components that provide the functionality
+you need already exist but lack i18n-awareness.  You would now have to
+reimplement them.
+
+This is where Pentecost comes in.  Instead of making content
+components i18n-aware, they are wrapped in a Pentecost object, which
+is really a container that can contain an instance of your content
+component for every language.  To the outside, a Pentecost object
+tries to behave like your content component and always serves the
+version of your content component that the user prefers according to
+his/her language settings.
+
+Pentecost is to be designed to be as modular as possible.  It works
+with
+
+  * any type of content component
+
+  * any container implementation (IContainer)
+
+That means, Pentecost can theoretically work with containers that
+store their data in different places than the ZODB, such as SQL.  To
+make Pentecost work with your container, only ZCML configuration is
+needed, no Python code.
+
+Pentecost comes with a default container configuration using Zope's
+BTreeContainer in ``btreecontainer.zcml``.  To keep Pentecost itself
+as independent as possible, it is not loaded by default.  A good place
+to load it is in the ZCML slug that is placed in the
+``package-includes`` directory of your Zope instance.
+
+
+Copyright
+---------
+
+Pentecost is copyrighted (c) 2004 by Philipp "philiKON" von
+Weitershausen.  It is freely distributable under the terms of the Zope
+Public License Version 2.1 (ZPL), as distributed with Zope 3.

Added: z3/pentecost/trunk/TODO.txt
==============================================================================
--- (empty file)
+++ z3/pentecost/trunk/TODO.txt	Mon Jul 12 15:58:21 2004
@@ -0,0 +1,18 @@
+====
+TODO
+====
+
+- what about annotations, metadata, etc.?
+
+- functional tests
+
+- better contents, adding, etc. views  --> browser subpackage
+
+  * default/fallback language should be settable
+
+  * allow a more comfortable way of adding languages (e.g. choose a
+    language from a list or something similar)
+
+  => need a language vocabulary?
+
+- browser:icon... get it from the current language object

Added: z3/pentecost/trunk/__init__.py
==============================================================================
--- (empty file)
+++ z3/pentecost/trunk/__init__.py	Mon Jul 12 15:58:21 2004
@@ -0,0 +1 @@
+# make this directory a package

Added: z3/pentecost/trunk/btreecontainer.zcml
==============================================================================
--- (empty file)
+++ z3/pentecost/trunk/btreecontainer.zcml	Mon Jul 12 15:58:21 2004
@@ -0,0 +1,50 @@
+<configure 
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:browser="http://namespaces.zope.org/browser"
+    i18n_domain="pentecost"
+    >
+
+  <!-- Pentecost works with any IContainer;
+       This default implementation uses BTreeContainer -->
+
+  <!-- BTreeContainer doesn't have security declarations, so we
+       provide some here; when using Pentecost with your own
+       container, make sure it has the proper declarations, including
+       the one for IPentecost -->
+
+  <content class="zope.app.container.btree.BTreeContainer">
+    <require
+        permission="zope.View"
+        interface="zope.app.container.interfaces.IReadContainer" 
+        />
+    <require
+        permission="zope.ManageContent"
+        interface="zope.app.container.interfaces.IWriteContainer"
+        />
+
+    <require
+        permission="zope.View"
+        interface="pentecost.interfaces.IPentecost"
+        />
+    <require
+        permission="zope.ManageContent"
+        set_schema="pentecost.interfaces.IPentecost"
+        />
+  </content>
+
+  <browser:addMenuItem
+      title="Pentecost"
+      class="zope.app.container.btree.BTreeContainer"
+      view="AddPentecost.html"
+      permission="zope.ManageContent"
+      />
+
+  <browser:addform
+      schema="pentecost.interfaces.IPentecostAdd"
+      content_factory="zope.app.container.btree.BTreeContainer"
+      label="Add Pentecost object"
+      name="AddPentecost.html"
+      permission="zope.ManageContent" 
+      />
+
+</configure>

Added: z3/pentecost/trunk/configure.zcml
==============================================================================
--- (empty file)
+++ z3/pentecost/trunk/configure.zcml	Mon Jul 12 15:58:21 2004
@@ -0,0 +1,64 @@
+<configure 
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:browser="http://namespaces.zope.org/browser"
+    xmlns:i18n="http://namespaces.zope.org/i18n"
+    i18n_domain="pentecost"
+    >
+
+  <adapter
+      for="zope.app.container.interfaces.IContainer"
+      provides="pentecost.interfaces.IPentecostAdd"
+      factory="pentecost.pentecost.ContainerAddingAdapter"
+      />
+
+  <vocabulary 
+      name="Add Menu Content Types"
+      factory="pentecost.vocabulary.AddMenuContentTypesVocabulary"
+      />
+
+  <content class="pentecost.pentecost.PentecostInterface">
+    <!-- for InterfaceClass, these security declarations are built
+         into zope.security; we have to do it through ZCML here -->
+    <allow interface="zope.interface.interfaces.IInterface" />
+    <allow attributes="__str__ _implied subscribe" />
+  </content>
+
+  <view
+      for="pentecost.interfaces.IPentecostContainer"
+      type="zope.publisher.interfaces.browser.IBrowserRequest"
+      provides="zope.publisher.interfaces.browser.IBrowserPublisher"
+      factory="pentecost.traverse.PentecostTraverser"
+      permission="zope.Public"
+      />
+
+  <view
+      for="pentecost.interfaces.IPentecostContainer"
+      type="zope.publisher.interfaces.browser.IBrowserRequest"
+      name="view"
+      provides="zope.app.traversing.interfaces.ITraversable"
+      factory="pentecost.traverse.PentecostViewTraverser" 
+      />
+
+
+  <!-- TODO: provide different views for contents, adding etc. -->
+
+  <browser:page
+      for="pentecost.interfaces.IPentecostContainer"
+      name="contents.html"
+      class="zope.app.container.browser.contents.Contents"
+      attribute="contents"
+      menu="zmi_views" title="Contents"
+      permission="zope.ManageContent"
+      />
+
+  <configure package="zope.app.preview">
+    <browser:page
+        for="pentecost.interfaces.IPentecostContainer"
+        name="preview.html"
+        template="preview.pt"
+        menu="zmi_views" title="Preview"
+        permission="zope.ManageContent"
+        />
+  </configure>
+
+</configure>

Added: z3/pentecost/trunk/interfaces.py
==============================================================================
--- (empty file)
+++ z3/pentecost/trunk/interfaces.py	Mon Jul 12 15:58:21 2004
@@ -0,0 +1,52 @@
+##############################################################################
+#
+# Copyright (c) 2004 Philipp "philiKON" von Weitershausen
+#
+# This software is distributed under the terms of the Zope Public
+# License (ZPL) v2.1.
+#
+##############################################################################
+"""Interfaces
+
+$Id$
+"""
+from zope.interface import Interface
+from zope.schema import Choice
+from zope.i18nmessageid import MessageIDFactory
+from zope.app.container.interfaces import IContentContainer
+
+_ = MessageIDFactory('pentecost')
+
+class IPentecost(Interface):
+    """Describes Pentecost's dependency on a specific content type"""
+
+    # Instead of this custom vocabulary 'Add Menu Content Types',
+    # which constructs a list of content types from the browser
+    # container add menu, you can also use the regular 'Content Types'
+    # vocabulary.  With this one, however, the term's titles for the
+    # add form are the interface's dotted names which isn't as nice as
+    # the titles of the add menu entries.
+    # ('zope.app.file.interfaces.IFile' vs. 'File')
+
+    content_type = Choice(
+	title=_(u"Content type"),
+	description=_(u"The content type of subobjects"),
+	required=True,
+	vocabulary="Add Menu Content Types",
+	)
+
+class IPentecostContainer(IPentecost, IContentContainer):
+    """Pentecost container
+
+    Marker interface to identify pentecost containers so that we can
+    register our own traversal adapters for them.
+    """
+
+class IPentecostAdd(IPentecost):
+    """Pentecost adding
+
+    Describes the data necessary for adding a pentecost object.  We
+    define another schema here on purpose because we want the form
+    machinery to adapt newly added containers so that we can make
+    Pentecost containers out of them during adaption.
+    """

Added: z3/pentecost/trunk/pentecost-configure.zcml
==============================================================================
--- (empty file)
+++ z3/pentecost/trunk/pentecost-configure.zcml	Mon Jul 12 15:58:21 2004
@@ -0,0 +1,4 @@
+<configure>
+  <include package="pentecost" />
+  <include package="pentecost" file="btreecontainer.zcml" />
+</configure>

Added: z3/pentecost/trunk/pentecost.py
==============================================================================
--- (empty file)
+++ z3/pentecost/trunk/pentecost.py	Mon Jul 12 15:58:21 2004
@@ -0,0 +1,160 @@
+##############################################################################
+#
+# Copyright (c) 2004 Philipp "philiKON" von Weitershausen
+#
+# This software is distributed under the terms of the Zope Public
+# License (ZPL) v2.1.
+#
+##############################################################################
+"""Core machinery
+
+The components in here allow Pentecost to be as transparent as it is.
+It works with any IContainer and any content type.
+
+$Id$
+"""
+from zope.interface import implements, directlyProvides
+from zope.interface.interface import InterfaceClass
+from zope.security.proxy import trustedRemoveSecurityProxy
+
+from zope.app import zapi
+from zope.app.container.interfaces import IContainer
+from zope.app.container.constraints import ItemTypePrecondition
+from zope.app.content.interfaces import IContentType
+from zope.app.component.interface import provideInterface
+
+from interfaces import IPentecostAdd, IPentecostContainer
+
+class PentecostInterface(InterfaceClass):
+    """Pickable interface for pentecost objects
+
+    Pentecost creates interfaces on-the-fly, thus the pickling
+    machinery cannot rely on finding it there again and wants to
+    pickle it.  However, interfaces are not pickable (yet), so we do
+    some tricks to recreate on-the-fly interfaces upon demand.  Plus,
+    we can hide some functionality here.
+
+    For example PentecostInterfaces always extend
+    ``IPentecostContainer``:
+
+    >>> from zope.interface import Interface
+    >>> IFoo = PentecostInterface('IFoo', Interface)
+    >>> IFoo.extends(IPentecostContainer)
+    True
+
+    By implementing a method of the pickling protocol, they avoid pickling:
+
+    >>> reduced = IFoo.__reduce__()[1]
+    >>> reduced[0]
+    'IFoo'
+    >>> reduced[1] is Interface
+    True
+
+    Finally, to play nice, they are hasheable:
+
+    >>> hash(IFoo) == hash('IFoo')
+    True
+
+    XXX test precondition on __setitem__
+    """
+
+    def __init__(self, name, content_type):
+        self.content_type = content_type
+
+        class Base(IPentecostContainer):
+            def __setitem__(name, object):
+                """Add an item"""
+            __setitem__.precondition = ItemTypePrecondition(content_type)
+
+	super(PentecostInterface, self).__init__(name, (Base,))
+
+    def __reduce__(self):
+        """Tell the pickling machiner not to pickle a PentecostInterface but
+        to recreate instances with whatever we return here"""
+        # That we return content_type here implies that it needs to be
+	# located in a module like a regular interface, a risk that
+	# we're willing to take for now
+	return (PentecostInterface, (self.getName(), self.content_type))
+
+    def __hash__(self):
+	return hash(self.__name__)
+
+class ContainerAddingAdapter(object):
+    """Adapter for young, innocent IContainers that are to be transvestited
+    to pentecost objects.
+
+    First, we need a content type that we want our pentecost object to
+    contain and an object implementing it:
+
+    >>> from zope.interface import implements, providedBy, Interface
+    >>> class ISampleType(Interface):
+    ...     pass
+
+    >>> class SampleObject(object):
+    ...     implements(ISampleType)
+    >>> sample = SampleObject()
+
+    Let's create a container then.  This poor bastard doesn't know yet
+    what's gonna hit him... Muahaha!
+
+    >>> from zope.app.container.sample import SampleContainer
+    >>> container = SampleContainer()
+
+    See, this container does not have a 2nd bottom or anything.  It's
+    a regular container:
+
+    >>> IPentecostContainer.providedBy(container)
+    False
+    >>> from zope.app.container.constraints import checkObject
+    >>> checkObject(container, "some_obj", object())
+
+    As you can see, no constraint on it yet. Now to begin the delicate
+    operation... We adapt the container and set the content type.
+
+    >>> adapter = ContainerAddingAdapter(container)
+    >>> adapter.content_type = ISampleType
+
+    It should now have a constraint that only allows us to add
+    ISampleTypes to it:
+
+    >>> checkObject(container, "sample", sample)
+
+    the simple object from before fails to be added:
+
+    >>> from zope.app.container.interfaces import InvalidItemType
+    >>> try:
+    ...     checkObject(container, "some_obj", object())
+    ... except InvalidItemType:
+    ...     print 'Failed'
+    ... else:
+    ...     print 'Should have failed'
+    Failed
+    """
+    implements(IPentecostAdd)
+    __used_for__ = IContainer
+
+    def __init__(self, context):
+        self.context = context
+
+    def getContentType(self):
+        return self.context.content_type
+
+    def setContentType(self, content_type):
+	if IPentecostContainer.providedBy(self.context):
+	    # XXX shall we raise an error here?
+	    return
+
+	self.context.content_type = content_type
+
+	# create a custom interface based on the content type for the
+	# pentecost object and make it a content type
+	# TODO: maybe do something about unique names
+	name = "Pentecost"
+        iface = PentecostInterface(name, content_type)
+        directlyProvides(iface, IContentType)
+
+	# directlyProvides freaks out about a security proxy
+	context = trustedRemoveSecurityProxy(self.context)
+        directlyProvides(context, iface)
+
+    content_type = property(getContentType, setContentType)

Added: z3/pentecost/trunk/tests.py
==============================================================================
--- (empty file)
+++ z3/pentecost/trunk/tests.py	Mon Jul 12 15:58:21 2004
@@ -0,0 +1,23 @@
+##############################################################################
+#
+# Copyright (c) 2004 Philipp "philiKON" von Weitershausen
+#
+# This software is distributed under the terms of the Zope Public
+# License (ZPL) v2.1.
+#
+##############################################################################
+"""Unit tests
+
+$Id$
+"""
+import unittest
+from zope.testing.doctestunit import DocTestSuite
+
+def test_suite():
+    return unittest.TestSuite((
+	DocTestSuite('pentecost.pentecost'),
+	DocTestSuite('pentecost.vocabulary'),
+        ))
+
+if __name__ == '__main__':
+    unittest.main()

Added: z3/pentecost/trunk/traverse.py
==============================================================================
--- (empty file)
+++ z3/pentecost/trunk/traverse.py	Mon Jul 12 15:58:21 2004
@@ -0,0 +1,67 @@
+##############################################################################
+#
+# Copyright (c) 2004 Philipp "philiKON" von Weitershausen
+#
+# This software is distributed under the terms of the Zope Public
+# License (ZPL) v2.1.
+#
+##############################################################################
+"""Browser traversal adapters
+
+These make it possible to simply open the URL of a pentecost object in
+a browser and get whatever language back the browser prefers.
+
+$Id$
+"""
+from zope.interface import implements
+from zope.i18n.negotiator import Negotiator
+from zope.exceptions import NotFoundError
+from zope.publisher.interfaces.browser import IBrowserPublisher
+
+from zope.app import zapi
+from zope.app.container.traversal import ContainerTraverser
+from zope.app.traversing.interfaces import ITraversable
+
+from interfaces import IPentecostContainer
+
+class PentecostTraverser(ContainerTraverser, object):
+    """A traverser that knows how to look up objects by name in a container."""
+
+    implements(IBrowserPublisher)
+    __used_for__ = IPentecostContainer
+
+    def browserDefault(self, request):
+        """See zope.publisher.browser.interfaces.IBrowserPublisher"""
+        negotiator = Negotiator()
+        language = negotiator.getLanguage(self.context.keys(), self.request)
+        if language in self.context:
+            subobj = self.context.get(language)
+            return self.context, (language,)
+
+        return super(PentecostTraverser, self).browserDefault(request)
+    
+class PentecostViewTraverser(object):
+    """Handle views for Pentecost containers"""
+    implements(ITraversable)
+    __used_for__ = IPentecostContainer
+
+    def __init__(self, context, request):
+        self.context = context
+        self.request = request
+
+    def traverse(self, name, ignored):
+        # find our own views first
+        view = zapi.queryView(self.context, name, self.request)
+        if view is not None:
+            return view
+
+	# maybe the language subobject has a view...
+        negotiator = Negotiator()
+        language = negotiator.getLanguage(self.context.keys(), self.request)
+        subobj = self.context.get(language, None)
+        view = zapi.queryView(subobj, name, self.request)
+
+	# or neither, then report not found
+        if view is None:
+            raise NotFoundError(self.context, name)
+        return view

Added: z3/pentecost/trunk/vocabulary.py
==============================================================================
--- (empty file)
+++ z3/pentecost/trunk/vocabulary.py	Mon Jul 12 15:58:21 2004
@@ -0,0 +1,88 @@
+##############################################################################
+#
+# Copyright (c) 2004 Philipp "philiKON" von Weitershausen
+#
+# This software is distributed under the terms of the Zope Public
+# License (ZPL) v2.1.
+#
+##############################################################################
+"""Vocabulary
+
+$Id$
+"""
+from zope.interface.interface import Specification
+from zope.component.interfaces import IFactory
+from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary
+
+from zope.app import zapi
+from zope.app.content.interfaces import IContentType
+from zope.app.container.constraints import checkFactory
+
+def queryContentType(ifaces):
+    """Out of an iterable of interfaces, find the one interface that is a
+    content type
+
+    Imagine we have a couple of interfaces, one of them is a content
+    type:
+
+    >>> from zope.interface import Interface, directlyProvides
+    >>> class IFoo(Interface):
+    ...     pass
+    >>> class IBar(Interface):
+    ...     pass
+    >>> directlyProvides(IBar, IContentType)
+    >>> class ISubbar(IBar):
+    ...     pass
+
+    Now we throw two of them in a list:
+
+    >>> ifaces = [IFoo, ISubbar]
+
+    We expect queryContentType to tell us that IBar is the content
+    type:
+
+    >>> queryContentType(ifaces) is IBar
+    True
+    """
+    spec = Specification(ifaces)
+    for iface in spec.__iro__:
+        if IContentType.providedBy(iface):
+            return iface
+    return None
+
+class AddMenuContentTypesVocabulary(SimpleVocabulary):
+    """Vocabulary of content type interfaces based on the browser add menu
+
+    TODO: doctest
+    """
+
+    def __init__(self, context):
+        terms = []
+        menu_service = zapi.getService("BrowserMenu")
+        menu_id = 'zope.app.container.add'
+        # there can be several factories for one content type; we
+        # really care about the content type, not the factory; we just
+        # conveniently use the factory id as a token
+        seen_types = {}
+        for item in menu_service.getAllMenuItems(menu_id, context):
+            if item.extra:
+                token = item.extra.get('factory')
+                if token:
+                    factory = zapi.getUtility(IFactory, token)
+                    content_type = queryContentType(factory.getInterfaces())
+                    # don't continue if the add entry is not for a content type
+                    if not content_type:
+                        continue
+                    # check if this type of object can actually added here
+                    if not checkFactory(context, None, factory):
+                        continue
+                    if content_type in seen_types:
+                        # XXX this is not very i18n friendly
+                        seen_types[content_type][1] += ", " + item.title
+                    else:
+                        seen_types[content_type] = [token, item.title]
+
+        terms = [SimpleTerm(content_type, token, title)
+                 for content_type, (token, title)
+                 in seen_types.iteritems()]
+        super(AddMenuContentTypesVocabulary, self).__init__(terms)


More information about the z3-checkins mailing list