Commits

Daniel Holth committed 215b4a7

initial commit

  • Participants

Comments (0)

Files changed (19)

+syntax:glob
+.*
+*~
+*.db
+*.egg-info/*
+*.pyc

LinguaPlus/__init__.py

+# Mark translations as outdated when the canonical version is edited.
+
+import zope.i18nmessageid
+MessageFactory = zope.i18nmessageid.MessageFactory('LinguaPlus')

LinguaPlus/browser/__init__.py

Empty file added.

LinguaPlus/browser/configure.zcml

+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:browser="http://namespaces.zope.org/browser"
+    i18n_domain="LinguaPlus">
+
+    <!-- Allow side-by-side editing of a working copy and its parent's
+    canonical translation. -->
+    <browser:page
+        name="linguaplone-edit-view"
+        for="plone.app.iterate.interfaces.IWorkingCopy"
+        class=".sidebyside.SideBySideView"
+        permission="zope2.View"
+        />
+
+    <!-- Shortcut isEnabled() -> False for other content. -->
+    <browser:page
+      name="linguaplone-edit-view"
+      for="*"
+      class=".sidebyside.NoSideBySideView"
+      permission="zope2.View"
+      />
+ 
+</configure>

LinguaPlus/browser/sidebyside.py

+def __init__():
+
+    global SideBySideView, NoSideBySideView
+
+    from AccessControl import ClassSecurityInfo
+    from plone.app.iterate.interfaces import IWorkingCopy
+    from plone.app.iterate.relation import WorkingCopyRelation
+    from Products.Five.browser import BrowserView
+    from Products.CMFCore import permissions
+
+    class SideBySideView(BrowserView):
+        """Provide some helper methods to be used inside the (form controller
+        based) side-by-side editing view."""
+        security = ClassSecurityInfo()
+
+        security.declareProtected(permissions.View, 'getCanonical')
+        def getCanonical(self):
+            """Returns the canonical translation."""
+            ret = self.context
+            refs = ret.getTranslationReferences()
+            if len(refs):
+                ret = self._getReferenceObject(uid=refs[0].targetUID)
+            return ret
+
+        security.declareProtected(permissions.View, 'getBaselineCopy')
+        def getBaselineCopy(self):
+            """Return the original copy, or self.context if this is not a working copy."""
+            if IWorkingCopy.providedBy(self.context):
+                return self.context.getReferences(WorkingCopyRelation.relationship)[0]
+            return self.context
+
+        security.declareProtected(permissions.View, 'isEnabled')
+        def isEnabled(self):
+            """Should side-by-side tab be visible for this content?"""
+            return self.context != self.getBaselineCopy()
+
+    class NoSideBySideView(SideBySideView):
+        def isEnabled(self): return False
+
+__init__()

LinguaPlus/configure.zcml

+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:browser="http://namespaces.zope.org/browser"    
+    xmlns:cmf="http://namespaces.zope.org/cmf"
+    xmlns:five="http://namespaces.zope.org/five"
+    xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
+    xmlns:plone="http://namespaces.plone.org/plone"
+    i18n_domain="LinguaPlus">
+
+    <include package="plone.browserlayer" />
+    <include package="plone.app.contentrules" />
+    <include package=".browser" />
+
+    <cmf:registerDirectory name="LinguaPlus"/>
+
+    <!-- Support side-by-side editing for working copies: -->
+    <genericsetup:registerProfile
+      name="default"
+      title="LinguaPlus"
+      directory="profiles/default"
+      description="plone.app.iterate support and other enhancements for LinguaPlone"
+      provides="Products.GenericSetup.interfaces.EXTENSION"
+      />
+
+    <!-- Ability to trigger on plone.app.iterate events -->
+
+    <interface
+      interface="plone.app.iterate.interfaces.ICheckinEvent"
+      type="plone.contentrules.rule.interfaces.IRuleEventType"
+      name="A working copy will be checked in."
+      />
+
+    <interface
+      interface="plone.app.iterate.interfaces.IAfterCheckinEvent"
+      type="plone.contentrules.rule.interfaces.IRuleEventType"
+      name="A working copy has been checked in."
+      />
+
+    <interface
+      interface="plone.app.iterate.interfaces.IBeforeCheckoutEvent"
+      type="plone.contentrules.rule.interfaces.IRuleEventType"
+      name="An object will be checked out."
+      />
+
+    <interface
+      interface="plone.app.iterate.interfaces.ICheckoutEvent"
+      type="plone.contentrules.rule.interfaces.IRuleEventType"
+      name="An object has been checked out."
+      />
+
+    <!-- Subscribers for above -->
+
+    <subscriber
+      for="plone.app.iterate.interfaces.ICheckinEvent"
+      handler=".handlers.checkin_action"
+      />
+
+    <subscriber
+      for="plone.app.iterate.interfaces.IAfterCheckinEvent"
+      handler=".handlers.aftercheckin_action"
+      />
+
+    <subscriber
+      for="plone.app.iterate.interfaces.IBeforeCheckoutEvent"
+      handler=".handlers.beforecheckout_action"
+      />
+
+    <subscriber
+      for="plone.app.iterate.interfaces.ICheckoutEvent"
+      handler=".handlers.checkout_action"
+      />
+
+    <!-- Outdate translations action -->
+
+    <adapter factory=".outdate.OutdateActionExecutor" />
+
+    <browser:page
+      for="plone.app.contentrules.browser.interfaces.IRuleActionAdding"
+      name="LinguaPlus.Outdate"
+      class=".outdate.OutdateAddForm"
+      permission="plone.app.contentrules.ManageContentRules"
+      />
+
+    <plone:ruleAction
+      name="LinguaPlus.Outdate"
+      title="Outdate translations"
+      description="Outdate all translations of an item"
+      for="*"
+      event="zope.component.interfaces.IObjectEvent"
+      addview="LinguaPlus.Outdate"
+      editview="edit"
+      schema=".outdate.IOutdateAction"
+      factory=".outdate.OutdateAction"
+      />
+
+  </configure>

LinguaPlus/handlers.py

+"""
+Handle plone.app.iterate events triggering content rules.
+"""
+
+def __init__():
+
+    global checkin_action, aftercheckin_action, beforecheckout_action
+    global checkout_action, execute_rules
+
+    # execute_rules(event) runs rules defined for the parent.
+    # See also execute(context, event) to run rules defined for the
+    # context (is execute() preferable?)
+    # Both functions bubble up the acquisition chain. 
+    try:
+        from plone.app.contentrules.handlers import execute_rules
+    except ImportError:
+        from Acquisition import aq_inner, aq_parent
+        from plone.app.contentrules.handlers import execute, is_portal_factory
+        # copied from plone.app.iterate 2.0:
+        def execute_rules(event):
+            """ When an action is invoked on an object,
+            execute rules assigned to its parent.
+            Base action executor handler """
+
+            if is_portal_factory(event.object):
+                return
+
+            execute(aq_parent(aq_inner(event.object)), event)
+
+
+    def checkin_action(event):
+        """Handle plone.app.iterate.interfaces.ICheckinEvent"""
+        execute_rules(event)
+
+    def aftercheckin_action(event):
+        """Handle plone.app.iterate.interfaces.IAfterCheckinEvent"""
+        execute_rules(event)
+
+    def beforecheckout_action(event):
+        """Handle plone.app.iterate.interfaces.IBeforeCheckoutEvent"""
+        execute_rules(event)
+
+    def checkout_action(event):
+        """Handle plone.app.iterate.interfaces.ICheckoutEvent"""
+        execute_rules(event)
+
+__init__()

LinguaPlus/outdate.py

+from Acquisition import aq_inner
+from OFS.SimpleItem import SimpleItem
+from plone.app.contentrules.browser.formhelper import NullAddForm
+from plone.contentrules.rule.interfaces import IExecutable, IRuleElementData
+from slc.outdated import Outdated
+from zope.component import adapts
+from zope.component.interfaces import IObjectEvent
+from zope.interface import implements, Interface
+
+from LinguaPlus import MessageFactory as _
+
+class Outdater(object):
+    """Wrapper to expose the outdated property referencing a context.
+    """
+    outdated = Outdated()
+    def __init__(self, context):
+        self.context = context
+
+class IOutdateAction(Interface):
+    """Describe the 0 configuration parameters for outdating.
+    """
+
+class OutdateAction(SimpleItem):
+    """Persistent (nothing to save though)
+    """
+    implements(IOutdateAction, IRuleElementData)
+
+    element = 'LinguaPlus.Outdate'
+
+    @property
+    def summary(self):
+        return _(u"Mark translations as outdated.")
+
+class OutdateActionExecutor(object):
+    """Outdate all translations of canonical content. No effect if the
+    content is not canonical.
+    """
+    implements(IExecutable)
+    adapts(Interface, IOutdateAction, IObjectEvent)
+
+    def __init__(self, context, element, event):
+        """
+        context: Object on which the rule is defined e.g. the site root
+        element: OutdateAction (contains any configuration for the action)
+        event: IObjectEvent that triggered the action
+        """
+        self.context = context
+        self.element = element
+        self.event = event
+
+    def __call__(self):
+        obj = self.event.object
+
+        if not obj.isCanonical():
+            return True
+        
+        translations = obj.getTranslations()
+        for lang in translations:
+            document, state = translations[lang]
+            # don't outdate self:
+            if document == obj:
+                # reindex self.event.object?
+                continue
+            outdater = Outdater(aq_inner(document))
+            outdater.outdated = True
+            # extra reindexObject() per http://plone.org/documentation/manual/
+            #   plone-community-developer-documentation/content/manipulating:
+            outdater.context.reindexObject()
+            outdater.context.reindexObject(idxs=["outdated"])
+
+        return True
+
+class OutdateAddForm(NullAddForm):
+    """Degenerate add form for 'outdate translations' action.
+    """
+    def create(self):
+        return OutdateAction()
+

LinguaPlus/profiles/default/actions.xml

+<?xml version="1.0"?>
+<object name="portal_actions" meta_type="Plone Actions Tool"
+   xmlns:i18n="http://xml.zope.org/namespaces/i18n">
+ <object name="object" meta_type="CMF Action Category">
+  <object name="side-by-side" meta_type="CMF Action" i18n:domain="LinguaPlus">
+   <property name="title" i18n:translate="">Side by side</property>
+   <property name="description" i18n:translate="">Edit content alongside canonical translation</property>
+   <property name="url_expr">string:${object_url}/translate_iter</property>
+   <property name="icon_expr"></property>
+   <property name="available_expr">object/@@linguaplone-edit-view/isEnabled</property>
+   <property name="permissions">
+    <element value="Modify portal content"/>
+   </property>
+   <property name="visible">True</property>
+  </object>
+ </object>
+</object>

LinguaPlus/profiles/default/skins.xml

+<?xml version="1.0"?>
+<object name="portal_skins">
+ <object name="LinguaPlus" meta_type="Filesystem Directory View"
+     directory="LinguaPlus:skins/LinguaPlus"/>
+
+ <skin-path name="*">
+  <layer name="LinguaPlus" insert-after="LinguaPlone" />
+ </skin-path>
+</object>

LinguaPlus/skins/LinguaPlus/translate_iter.cpt

+
+<tal:block metal:define-macro="master"
+           xmlns:tal="http://xml.zope.org/namespaces/tal"
+           xmlns:metal="http://xml.zope.org/namespaces/metal"
+           xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+           define="view context/@@at_base_edit_view;
+                   portal context/@@plone_portal_state/portal;
+                   dummy python:view.isTemporaryObject() and request.set('disable_border', True);
+                   lifecycle context/@@at_lifecycle_view;
+                   lock_info context/@@plone_lock_info;
+                   dummy lifecycle/begin_edit;
+                   errors options/state/getErrors | nothing;
+                   schematas here/Schemata;
+                   allow_tabbing python: not view.isMultiPageSchema();
+                   fieldsets python:[key for key in schematas.keys() if (schematas[key].editableFields(here, visible_only=True))];
+                   default_fieldset python:(not schematas or 'default' in schematas) and 'default' or fieldsets[0];
+                   fieldset request/fieldset|options/fieldset|default_fieldset;
+                   fields python:[f for key in fieldsets for f in schematas[key].editableFields(here)];
+                   dummy python:here.at_isEditable(fields);
+                   portal_type python:here.getPortalTypeName().lower().replace(' ', '_');
+                   type_name here/getPortalTypeName|here/archetype_name;
+                   base_macros here/edit_macros/macros;
+                   edit_template python:'%s_edit' % portal_type;
+                   edit_macros python:path('here/%s/macros | nothing' % edit_template);
+                   header_macro edit_macros/header | header_macro | base_macros/header;
+                   typedescription_macro edit_macros/typedescription | typedescription_macro | base_macros/typedescription;
+                   body_macro edit_macros/body | body_macro | base_macros/body;
+                   footer_macro edit_macros/footer | footer_macro | base_macros/footer;
+                   isLocked isLocked | lock_info/is_locked_for_current_user;
+                   css python:here.getUniqueWidgetAttr(fields, 'helper_css');
+                   js python:here.getUniqueWidgetAttr(fields, 'helper_js');
+                   baseline here/@@linguaplone-edit-view/getBaselineCopy;
+                   other python:baseline.getCanonical();
+                   ptool context/portal_properties;
+                   lprops ptool/linguaplone_properties|nothing;
+                   hide_column_two lprops/hide_right_column_on_translate_form|python:False;">
+
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xml:lang="en"
+      lang="en"
+      xmlns:tal="http://xml.zope.org/namespaces/tal"
+      xmlns:metal="http://xml.zope.org/namespaces/metal"
+      xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+      metal:use-macro="here/main_template/macros/master"
+      i18n:domain="plone">
+
+  <metal:head fill-slot="top_slot">
+    <tal:block define="macro edit_macros/topslot | nothing"
+                    condition="macro">
+      <metal:block use-macro="macro" />
+    </tal:block>
+  </metal:head>
+
+  <metal:javascript_head fill-slot="javascript_head_slot">
+    <tal:block tal:condition="hide_column_two">
+      <tal:hide tal:define="global sr python:False;" />
+    </tal:block>
+    <tal:block define="macro here/archetypes_custom_js/macros/javascript_head | nothing"
+               condition="macro">
+      <metal:block use-macro="macro" />
+    </tal:block>
+    <tal:js condition="js"
+            repeat="item js">
+      <script type="text/javascript"
+              charset="iso-8859-1"
+              tal:condition="python:exists('portal/%s' % item)"
+              tal:attributes="src string:$portal_url/$item">
+      </script>
+    </tal:js>
+    <tal:block define="macro edit_macros/javascript_head | nothing"
+                    condition="macro">
+      <metal:block use-macro="macro" />
+    </tal:block>
+  </metal:javascript_head>
+
+  <metal:css fill-slot="style_slot">
+    <tal:css condition="css"
+             repeat="item css">
+      <style type="text/css"
+             media="all"
+             tal:condition="python:exists('portal/%s' % item)"
+             tal:content="structure string:&lt;!-- @import url($portal_url/$item); --&gt;">
+      </style>
+    </tal:css>
+    <tal:block define="macro edit_macros/css | nothing"
+                    condition="macro">
+      <metal:block use-macro="macro" />
+    </tal:block>
+  </metal:css>
+    
+  <body>
+
+    <metal:fill fill-slot="main">
+      <metal:main define-macro="main"
+          tal:define="lang_name nocall:here/portal_languages/getNameForLanguageCode">
+        <metal:use_header use-macro="header_macro" />
+        <metal:use_typedescription use-macro="typedescription_macro" />
+
+        <metal:use_body use-macro="body_macro">
+            <metal:block metal:fill-slot="widgets">
+                <tal:tabbed tal:condition="allow_tabbing | nothing">
+                  <fieldset tal:repeat="fieldset fieldsets"
+                            tal:attributes="id string:fieldset-${fieldset}">
+                    <legend id=""
+                            tal:content="python: view.getTranslatedSchemaLabel(fieldset)"
+                            tal:attributes="id string:fieldsetlegend-${fieldset}" />
+                    <table style="width: 100%; border: 1px solid #fefefe">
+                        <tr tal:condition="python:fieldset=='default'">
+                            <td colspan="2">
+                                <p class="documentDescription"
+                                   i18n:translate="description_translating_from_to"
+                                   tal:define="langs here/getUntranslatedLanguages">
+                                    Translating from
+                                    <span i18n:name="from">
+                                        <select name="lp_translating_from"
+                                                tal:define="lp_translating_from request/lp_translating_from | baseline/getCanonicalLanguage">
+                                            <option selected="selected"
+                                                    tal:define="code baseline/getCanonicalLanguage"
+                                                    tal:content="python:lang_name(code)"
+                                                    tal:attributes="selected python:lp_translating_from == code;
+                                                                    value code">Language</option>
+                                            <option tal:repeat="lang langs"
+                                                    tal:content="python:lang[1]"
+                                                    tal:attributes="selected python:lp_translating_from == lang[0];
+                                                                    value python:lang[0]">Language</option>
+                                        </select>
+                                    </span>
+                                    to
+                                    <span i18n:name="to">
+                                        <select name="lp_translating_to"
+                                                tal:define="lp_translating_to request/lp_translating_to | here/getLanguage">
+                                            <option selected="selected"
+                                                    tal:define="code here/getLanguage"
+                                                    tal:content="python:lang_name(code)"
+                                                    tal:attributes="selected python:lp_translating_to == code;
+                                                                    value code">Language</option>
+                                            <option tal:repeat="lang langs"
+                                                    tal:content="python:lang[1]"
+                                                    tal:attributes="selected python:lp_translating_to == lang[0];
+                                                                    value python:lang[0]">Language</option>
+                                        </select>
+                                    </span>
+                                </p>
+                            </td>
+                        </tr>
+                        <tr tal:repeat="field python:schematas[fieldset].editableFields(here, visible_only=True)"
+                            tal:attributes="id string:archetypes-fieldname-${field/getName}">
+                            <tal:block define="fieldname field/getName">
+                                <td class="canonicalLanguage" style="width: 50%"
+                                     tal:define="otherfield python:other.Schemata()[fieldset][fieldname];
+                                                 otherwidget python:otherfield.widget;
+                                                 textformat python:otherfield.getContentType(other);
+                                                 textareafields python:('TextField','LinesField');
+                                                 renderableMimeTypes here/mimetypesToRenderInTranslationForm;
+                                                 renderablefield python:textformat in renderableMimeTypes">
+                                        <!-- This is the canonical content -->
+
+                                    <div tal:define="target_language other/Language">
+                                        <label tal:content="python:otherwidget.Label(here, target_language=target_language)">
+                                            Field
+                                        </label>
+                                        <div class="discreet"
+                                             tal:content="python:otherwidget.Description(here, target_language=target_language)">
+                                            Description
+                                        </div>
+                                    </div>
+
+                                    <div>
+                                        <div tal:condition="renderablefield"
+                                             style="height:35em; overflow:scroll"
+                                             content="structure python:otherfield.getAccessor(other)()">
+                                            <metal:fieldMacro use-macro="python:other.widget(otherfield.getName(), mode='view')"/>
+                                            <!-- if a renderable field, use the accessor and have scrollbars-->
+                                        </div>
+
+                                        <div tal:condition="not: renderablefield">
+                                            <metal:fieldMacro use-macro="python:other.widget(otherfield.getName(), mode='view')"/>
+                                        </div>
+                                    </div>
+                                </td>
+                                <td class="targetLanguage" style="width: 50%"
+                                    tal:define="read_only python:field.isLanguageIndependent(context)">
+                                    <tal:block condition="python:not read_only">
+                                      <metal:fieldMacro use-macro="python:here.widget(fieldname, mode='edit')" />
+                                    </tal:block>
+                                    <tal:block condition="python:read_only">
+                                      <metal:fieldMacro use-macro="python:here.widget(fieldname)" />
+                                    </tal:block>
+                                    
+                                </td>
+                            </tal:block>
+                        </tr>
+                    </table>
+                  </fieldset>
+                </tal:tabbed>
+                <tal:nottabbed tal:condition="not: allow_tabbing | nothing">
+                    <tal:fields repeat="field python:schematas[fieldset].editableFields(here, visible_only=True)">
+                      <metal:fieldMacro use-macro="python:here.widget(field.getName(), mode='edit')" />
+                    </tal:fields>
+                </tal:nottabbed>
+            </metal:block>
+
+        </metal:use_body>
+
+        <metal:use_footer use-macro="footer_macro" />
+      </metal:main>
+    </metal:fill>
+
+  </body>
+
+</html>
+
+</tal:block>

LinguaPlus/skins/LinguaPlus/translate_iter.cpt.metadata

+[default]
+title = Edit
+
+[validators]
+validators = validate_atct
+
+[actions]
+action.success = traverse_to:string:content_edit
+action.success..cancel = traverse_to:string:go_back
+action.failure = traverse_to_action:string:edit
+

LinguaPlus/tests.py

+"""
+Automated tests.
+
+This is mostly copy & paste from
+http://plone.org/documentation/kb/creating-content-rule-conditions-and-actions/tutorial-all-pages
+
+Daniel Holth <daniel.holth@exac.com>
+"""
+
+import unittest
+import LinguaPlus
+
+from zope.component import getUtility
+
+from plone.app.iterate.interfaces import ICheckinCheckoutPolicy
+from Products.PloneTestCase import PloneTestCase as ptc
+from Products.PloneTestCase.layer import PloneSite
+from Products.Five import fiveconfigure, zcml
+from plone.contentrules.rule.interfaces import IRuleEventType
+import Products.LinguaPlone
+import plone.app.iterate
+from zope.component.interfaces import IObjectEvent
+from zope.interface import implements
+from LinguaPlus.outdate import OutdateActionExecutor, OutdateAction,\
+    Outdater
+from plone.app.iterate.event import BeforeCheckoutEvent, CheckoutEvent,\
+    CheckinEvent, AfterCheckinEvent
+
+ptc.setupPloneSite()
+
+class DummyEvent(object):
+    implements(IObjectEvent)
+    
+    def __init__(self, obj):
+        self.object = obj
+
+class SisypheanTest(ptc.PloneTestCase):
+    class layer(PloneSite):
+        @classmethod
+        def setUp(cls):
+            fiveconfigure.debug_mode = True
+            zcml.load_config('configure.zcml',
+                             Products.LinguaPlone)
+            zcml.load_config('configure.zcml',
+                             plone.app.iterate)
+            zcml.load_config('configure.zcml',
+                             LinguaPlus)
+            fiveconfigure.debug_mode = False
+
+        @classmethod
+        def tearDown(cls):
+            pass
+
+    def afterSetUp(self):
+        self.setRoles(('Manager',))        
+        self.portal.portal_setup.runAllImportStepsFromProfile(
+            'profile-plone.app.iterate:plone.app.iterate')
+        
+    def testRegistered(self):
+        # Why bother asserting the ZCML does what it says it does? 
+        element = getUtility(IRuleEventType, name='A working copy will be checked in.')
+        assert element
+        
+    def testExecute(self):
+        """Assert OutdateAction marks translations as outdated."""
+        self.folder.invokeFactory('Document', 'd1')
+        self.folder.d1.setSubject(['Bar'])
+        es = self.folder.d1.addTranslation('es')
+
+        self.assertFalse(Outdater(self.folder.d1).outdated)
+        self.assertFalse(Outdater(es).outdated)
+        
+        executor = OutdateActionExecutor(self.portal, OutdateAction(), DummyEvent(self.folder.d1))        
+        self.assertTrue(executor())
+                
+        # canonical is not marked as outdated
+        self.assertFalse(Outdater(self.folder.d1).outdated)
+        # but the translation is marked as outdated
+        self.assertTrue(Outdater(es).outdated)
+        
+    def testEvents(self):
+        """Assert our new checkin/checkout listeners are listening."""
+        events = []
+        def appender(event):
+            events.append(event)
+        execute_rules = LinguaPlus.handlers.execute_rules
+        try:
+            LinguaPlus.handlers.execute_rules = appender     
+            self.folder.invokeFactory('Document', 'd2')
+            self.folder.d2.setSubject(['Bar'])
+            working_copy = ICheckinCheckoutPolicy(self.folder.d2).checkout(self.folder)
+            ICheckinCheckoutPolicy(working_copy).checkin("Automated Test")
+            assert len(events) == 4, events
+            b, co, ci, aci = events
+            assert isinstance(b, BeforeCheckoutEvent)
+            assert isinstance(co, CheckoutEvent)
+            assert isinstance(ci, CheckinEvent)
+            assert isinstance(aci, AfterCheckinEvent)
+        finally:
+            LinguaPlus.handlers.execute_rules = execute_rules
+        
+
+def test_suite():
+    return unittest.TestSuite([
+            unittest.makeSuite(SisypheanTest)
+        ])
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')
+
+LinguaPlus
+==========
+
+Overview
+--------
+
+LinguaPlus provides several amenities designed to improve the LinguaPlone
+workflow. By automatically applying 'mark outdated' (from slc.outdated)
+to translations when the canonical version is changed,  translators can
+keep track of which translated items need to be updated. Side-by-side
+editing for plone.app.iterate working copies allows the translator
+to reference the canonical version while making changes in a working
+copy. When the content has been updated an administrator can check it
+back in and remove the outdated flag.
+
+Provide content rule triggers for `plone.app.iterate`.
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This package allows you to trigger content rules based on
+plone.app.iterate's four events: "An object will been checked
+out (IBeforeCheckoutEvent)", "An object has been checked out
+(ICheckoutEvent)", "A working copy will be checked in (ICheckinEvent)",
+"A working copy has been checked in (IAfterCheckinEvent)".
+
+Mark translations as outdated when the canonical version is changed.
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This package provides a content action that can be used to mark all
+translations of a piece of content as outdated when they are edited,
+using `slc.outdated` to provide the underlying 'mark as outdated' feature.
+
+Per-language collections of outdated content can serve as a to-do list
+for translators. Once the content has been updated, manually 'toggle
+outdated' on the newly retranslated content.
+
+Edit working copies side-by-side with the canonical version.
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This package provides a 'side-by-side' view to edit plone.app.iterate
+working copies alongside the baseline's canonical revision, just like
+LinguaPlone's edit view but for working copies.
+
+Changelog for LinguaPlus
+
+- 0.3
+    - First public version
+
+- 0.2
+    - First usable version
+
+- 0.1 Unreleased
+
+    - Initial package structure.
+      [zopeskel]
+
+		    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+			    NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+		     END OF TERMS AND CONDITIONS
+  LinguaPlus is written by Daniel Holth <daniel.holth@exac.com>.
+  Copyright Exactech, Inc. (Gainesville, FL), 2011
+
+  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.
+[nosetests]
+match=^test
+nocapture=1
+from setuptools import setup, find_packages
+import os
+
+version = '0.3'
+
+setup(name='LinguaPlus',
+      version=version,
+      description="Content actions and plone.app.iterate support for LinguaPlone.",
+      long_description=open("README.txt").read() + "\n" +
+                       open(os.path.join("docs", "HISTORY.txt")).read(),
+      # Get more strings from http://www.python.org/pypi?%3Aaction=list_classifiers
+      classifiers=[
+        "Framework :: Plone",
+        "Programming Language :: Python",
+        "Topic :: Software Development :: Libraries :: Python Modules",
+        ],
+      keywords='',
+      author='Daniel Holth',
+      author_email='daniel.holth@exac.com',
+      url='http://python.org/pypi/LinguaPlus',
+      license='GPL',
+      packages=find_packages(),
+      include_package_data=True,
+      zip_safe=False,
+      install_requires=[
+          'setuptools',
+          'slc.outdated',
+          'Products.LinguaPlone',
+          'plone.app.iterate',
+          # -*- Extra requirements: -*-
+      ],
+      test_suite='nose.collector',
+      entry_points="""
+      # -*- Entry points: -*-
+      """,
+      )