From fcd20d3496fdf8da67ea881f22ae5654b132098d Mon Sep 17 00:00:00 2001 From: bloodbare Date: Thu, 16 Jul 2015 13:27:50 +0200 Subject: [PATCH] [fc] Repository: plone.app.iterate Branch: refs/heads/master Date: 2015-07-15T16:07:28+02:00 Author: vangheem (vangheem) Commit: https://github.com/plone/plone.app.iterate/commit/642f4652eefc59dd9179645cf8ebea9194c6ad74 merge plone.app.stagingbehavior into plone.app.iterate without the behavior implementation Files changed: A plone/app/iterate/dexterity/__init__.py A plone/app/iterate/dexterity/configure.zcml A plone/app/iterate/dexterity/copier.py A plone/app/iterate/dexterity/interfaces.py A plone/app/iterate/dexterity/policy.py A plone/app/iterate/dexterity/relation.py A plone/app/iterate/dexterity/utils.py A plone/app/iterate/tests/dexterity.rst A plone/app/iterate/tests/test_annotations.py M CHANGES.rst M plone/app/iterate/__init__.py M plone/app/iterate/archiver.py M plone/app/iterate/browser/cancel.pt M plone/app/iterate/browser/checkin.pt M plone/app/iterate/browser/control.py M plone/app/iterate/browser/diff.py M plone/app/iterate/browser/info.py M plone/app/iterate/browser/info_baseline.pt M plone/app/iterate/configure.zcml M plone/app/iterate/interfaces.py M plone/app/iterate/policy.py M plone/app/iterate/relation.py M plone/app/iterate/testing.py M plone/app/iterate/tests/test_doctests.py M plone/app/iterate/tests/test_iterate.py M plone/app/iterate/util.py M setup.py Repository: plone.app.iterate Branch: refs/heads/master Date: 2015-07-15T18:09:39+02:00 Author: vangheem (vangheem) Commit: https://github.com/plone/plone.app.iterate/commit/1e12b189b1f4410bc2875af39d77092e0c58a1e5 add more tests Files changed: A plone/app/iterate/tests/test_interfaces.py M plone/app/iterate/browser/info.py M plone/app/iterate/browser/info_baseline.pt M plone/app/iterate/dexterity/policy.py M plone/app/iterate/permissions.py M plone/app/iterate/policy.py M plone/app/iterate/testing.py M plone/app/iterate/tests/dexterity.rst M plone/app/iterate/tests/test_annotations.py M plone/app/iterate/util.py Repository: plone.app.iterate Branch: refs/heads/master Date: 2015-07-16T13:27:50+02:00 Author: Ramon Navarro Bosch (bloodbare) Commit: https://github.com/plone/plone.app.iterate/commit/5e27ac902eb8fdd334a9c754fa7c83f23ca9c17b Merge pull request #15 from plone/dexterity-support Provide dexterity support in plone.app.iterate Files changed: A plone/app/iterate/dexterity/__init__.py A plone/app/iterate/dexterity/configure.zcml A plone/app/iterate/dexterity/copier.py A plone/app/iterate/dexterity/interfaces.py A plone/app/iterate/dexterity/policy.py A plone/app/iterate/dexterity/relation.py A plone/app/iterate/dexterity/utils.py A plone/app/iterate/tests/dexterity.rst A plone/app/iterate/tests/test_annotations.py A plone/app/iterate/tests/test_interfaces.py M CHANGES.rst M plone/app/iterate/__init__.py M plone/app/iterate/archiver.py M plone/app/iterate/browser/cancel.pt M plone/app/iterate/browser/checkin.pt M plone/app/iterate/browser/control.py M plone/app/iterate/browser/diff.py M plone/app/iterate/browser/info.py M plone/app/iterate/browser/info_baseline.pt M plone/app/iterate/configure.zcml M plone/app/iterate/interfaces.py M plone/app/iterate/permissions.py M plone/app/iterate/policy.py M plone/app/iterate/relation.py M plone/app/iterate/testing.py M plone/app/iterate/tests/test_doctests.py M plone/app/iterate/tests/test_iterate.py M plone/app/iterate/util.py M setup.py --- last_commit.txt | 4058 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 4035 insertions(+), 23 deletions(-) diff --git a/last_commit.txt b/last_commit.txt index 8fd68b57ab..5305b9682f 100644 --- a/last_commit.txt +++ b/last_commit.txt @@ -1,44 +1,4056 @@ -Repository: plonetheme.barceloneta +Repository: plone.app.iterate Branch: refs/heads/master -Date: 2015-07-16T12:21:07+02:00 +Date: 2015-07-15T16:07:28+02:00 Author: vangheem (vangheem) -Commit: https://github.com/plone/plonetheme.barceloneta/commit/a48d4703cbb3bee47766c8b0929e13d9dfb4a8f1 +Commit: https://github.com/plone/plone.app.iterate/commit/642f4652eefc59dd9179645cf8ebea9194c6ad74 -do not use absolute path to reference index.html +merge plone.app.stagingbehavior into plone.app.iterate without the + behavior implementation Files changed: +A plone/app/iterate/dexterity/__init__.py +A plone/app/iterate/dexterity/configure.zcml +A plone/app/iterate/dexterity/copier.py +A plone/app/iterate/dexterity/interfaces.py +A plone/app/iterate/dexterity/policy.py +A plone/app/iterate/dexterity/relation.py +A plone/app/iterate/dexterity/utils.py +A plone/app/iterate/tests/dexterity.rst +A plone/app/iterate/tests/test_annotations.py M CHANGES.rst -M plonetheme/barceloneta/theme/rules.xml +M plone/app/iterate/__init__.py +M plone/app/iterate/archiver.py +M plone/app/iterate/browser/cancel.pt +M plone/app/iterate/browser/checkin.pt +M plone/app/iterate/browser/control.py +M plone/app/iterate/browser/diff.py +M plone/app/iterate/browser/info.py +M plone/app/iterate/browser/info_baseline.pt +M plone/app/iterate/configure.zcml +M plone/app/iterate/interfaces.py +M plone/app/iterate/policy.py +M plone/app/iterate/relation.py +M plone/app/iterate/testing.py +M plone/app/iterate/tests/test_doctests.py +M plone/app/iterate/tests/test_iterate.py +M plone/app/iterate/util.py +M setup.py diff --git a/CHANGES.rst b/CHANGES.rst -index 2c8923e..f6c5308 100644 +index 15c8459..3c05579 100644 --- a/CHANGES.rst +++ b/CHANGES.rst -@@ -4,7 +4,9 @@ Changelog - 1.6.8 (unreleased) +@@ -1,9 +1,13 @@ + Changelog + ========= + +-3.0.2 (unreleased) ++3.1.0 (unreleased) ------------------ --- Nothing changed yet. -+- do not use absolute prefix to reference index.html to copying themes -+ does not reference original theme file ++- merge plone.app.stagingbehavior into plone.app.iterate without the ++ behavior implementation. This is for Plone 5 iterate support + [vangheem] ++ + - Don't remove aquisition on object for getToolByName call + [tomgross] + +diff --git a/plone/app/iterate/__init__.py b/plone/app/iterate/__init__.py +index bcc6582..4b7c8c5 100644 +--- a/plone/app/iterate/__init__.py ++++ b/plone/app/iterate/__init__.py +@@ -22,7 +22,25 @@ + """ + """ + ++import logging + from zope.i18nmessageid import MessageFactory ++from plone.app.iterate import permissions # noqa ++ + PloneMessageFactory = MessageFactory('plone') ++logger = logging.getLogger('plone.app.iterate') ++ ++ ++try: ++ import plone.app.relationfield # noqa ++except ImportError: ++ logger.warn('Dexterity support for iterate is not available. ' ++ 'You must install plone.app.relationfield') ++ + +-from plone.app.iterate import permissions ++try: ++ import plone.app.stagingbehavior # noqa ++ logger.error('plone.app.stagingbehavior should NOT be installed with this version ' ++ 'of plone.app.iterate. You may experience problems running this configuration. ' ++ 'plone.app.iterate now has dexterity suport built-in.') ++except ImportError: ++ pass +\ No newline at end of file +diff --git a/plone/app/iterate/archiver.py b/plone/app/iterate/archiver.py +index 3dff719..bf8c13b 100644 +--- a/plone/app/iterate/archiver.py ++++ b/plone/app/iterate/archiver.py +@@ -30,30 +30,30 @@ + + import interfaces + +-class ContentArchiver( object ): ++class ContentArchiver(object): + +- implements( interfaces.IObjectArchiver ) +- adapts( interfaces.IIterateAware ) ++ implements(interfaces.IObjectArchiver) ++ adapts(interfaces.IIterateAware) + +- def __init__( self, context ): ++ def __init__(self, context): + self.context = context + self.repository = getToolByName(context, 'portal_repository') + +- def save( self, checkin_message ): +- self.repository.save( self.context, checkin_message ) ++ def save(self, checkin_message): ++ self.repository.save(self.context, checkin_message) + +- def isVersionable( self ): +- if not self.repository.isVersionable( self.context ): ++ def isVersionable(self): ++ if not self.repository.isVersionable(self.context): + return False + return True + +- def isVersioned( self ): ++ def isVersioned(self): + archivist = getToolByName(self.context, 'portal_archivist') +- version_count = len( archivist.queryHistory( self.context ) ) +- return bool( version_count ) ++ version_count = len(archivist.queryHistory(self.context)) ++ return bool(version_count) + +- def isModified( self ): ++ def isModified(self): + try: +- return not self.repository.isUpToDate( self.context ) ++ return not self.repository.isUpToDate(self.context) + except: + return False +diff --git a/plone/app/iterate/browser/cancel.pt b/plone/app/iterate/browser/cancel.pt +index e7f68b2..a1af209 100644 +--- a/plone/app/iterate/browser/cancel.pt ++++ b/plone/app/iterate/browser/cancel.pt +@@ -1,7 +1,15 @@ +- +- +-
+- ++ ++ ++ ++ ++ ++
+
+@@ -38,6 +46,9 @@ +
+ + +- ++ ++ ++ + +- ++ ++ +\ No newline at end of file +diff --git a/plone/app/iterate/browser/checkin.pt b/plone/app/iterate/browser/checkin.pt +index af03ac4..ebfa2ed 100644 +--- a/plone/app/iterate/browser/checkin.pt ++++ b/plone/app/iterate/browser/checkin.pt +@@ -1,6 +1,15 @@ +- ++ ++ + +-
++ ++ ++
+ +
+ +
++
++
++
+ +-
+- +- ++ ++ +\ No newline at end of file +diff --git a/plone/app/iterate/browser/control.py b/plone/app/iterate/browser/control.py +index ec3efd1..2d5a391 100644 +--- a/plone/app/iterate/browser/control.py ++++ b/plone/app/iterate/browser/control.py +@@ -20,16 +20,14 @@ + # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ################################################################## + +-from plone.memoize.view import memoize +- + from AccessControl import getSecurityManager + from Acquisition import aq_inner +-from Products.Five.browser import BrowserView +-from Products.Archetypes.interfaces import IReferenceable + import Products.CMFCore.permissions +- ++from Products.Five.browser import BrowserView + from plone.app.iterate import interfaces +-from plone.app.iterate.relation import WorkingCopyRelation ++from plone.app.iterate.interfaces import ICheckinCheckoutPolicy ++from plone.app.iterate.interfaces import IWorkingCopy ++from plone.memoize.view import memoize + + + class Control(BrowserView): +@@ -38,12 +36,6 @@ class Control(BrowserView): + This is a public view, referenced in action condition expressions. + """ + +- def get_original(self, context): +- if IReferenceable.providedBy(context): +- refs = context.getRefs(WorkingCopyRelation.relationship) +- if refs: +- return refs[0] +- + def checkin_allowed(self): + """Check if a checkin is allowed + """ +@@ -57,12 +49,15 @@ def checkin_allowed(self): + if not archiver.isVersionable(): + return False + +- original = self.get_original(context) ++ if not IWorkingCopy.providedBy(context): ++ return False ++ ++ policy = ICheckinCheckoutPolicy(context) ++ original = policy.getBaseline() + if original is None: + return False + +- if not checkPermission( +- Products.CMFCore.permissions.ModifyPortalContent, original): ++ if not checkPermission(Products.CMFCore.permissions.ModifyPortalContent, original): + return False + + return True +@@ -75,19 +70,17 @@ def checkout_allowed(self): + if not interfaces.IIterateAware.providedBy(context): + return False + +- if not IReferenceable.providedBy(context): +- return False +- + archiver = interfaces.IObjectArchiver(context) + if not archiver.isVersionable(): + return False + +- # check if there is an existing checkout +- if len(context.getBRefs(WorkingCopyRelation.relationship)) > 0: ++ policy = ICheckinCheckoutPolicy(context) ++ ++ if policy.getWorkingCopy() is not None: + return False + + # check if its is a checkout +- if len(context.getRefs(WorkingCopyRelation.relationship)) > 0: ++ if policy.getBaseline() is not None: + return False + + return True +@@ -97,4 +90,6 @@ def cancel_allowed(self): + """Check to see if the user can cancel the checkout on the + given working copy + """ +- return self.get_original(aq_inner(self.context)) is not None ++ policy = ICheckinCheckoutPolicy(self.context) ++ original = policy.getBaseline() ++ return original is not None +diff --git a/plone/app/iterate/browser/diff.py b/plone/app/iterate/browser/diff.py +index 7d0b712..792ee51 100644 +--- a/plone/app/iterate/browser/diff.py ++++ b/plone/app/iterate/browser/diff.py +@@ -6,30 +6,26 @@ + from Products.Five.browser import BrowserView + + from plone.app.iterate.interfaces import IWorkingCopy, IBaseline +-from plone.app.iterate.relation import WorkingCopyRelation ++from plone.app.iterate.interfaces import ICheckinCheckoutPolicy + +-class DiffView( BrowserView ): + +- def __init__( self, context, request ): +- self.context = context +- self.request = request +- if IBaseline.providedBy( self.context ): +- self.baseline = context +- self.working_copy = context.getBackReferences( WorkingCopyRelation.relationship )[0] +- elif IWorkingCopy.providedBy( self.context ): +- self.working_copy = context +- self.baseline = context.getReferences( WorkingCopyRelation.relationship )[0] ++class DiffView(BrowserView): ++ ++ def __call__(self): ++ policy = ICheckinCheckoutPolicy(self.context) ++ if IBaseline.providedBy(self.context): ++ self.baseline = self.context ++ self.working_copy = policy.getWorkingCopy() ++ elif IWorkingCopy.providedBy(self.context): ++ self.working_copy = self.context ++ self.baseline = policy.getBaseline() + else: + raise AttributeError("Invalid Context") ++ return self.index() + +- def diffs( self ): ++ def diffs(self): + diff = getToolByName(self.context, 'portal_diff') +- return diff.createChangeSet( self.baseline, +- self.working_copy, +- id1="Baseline", +- id2="Working Copy" ) +- +- +- +- +- ++ return diff.createChangeSet(self.baseline, ++ self.working_copy, ++ id1="Baseline", ++ id2="Working Copy") +diff --git a/plone/app/iterate/browser/info.py b/plone/app/iterate/browser/info.py +index 8578340..0c1012f 100644 +--- a/plone/app/iterate/browser/info.py ++++ b/plone/app/iterate/browser/info.py +@@ -2,67 +2,65 @@ + $Id: base.py 1808 2007-02-06 11:39:11Z hazmat $ + """ + +-from zope.interface import implements +- +-from zope.viewlet.interfaces import IViewlet +- +-from DateTime import DateTime + from AccessControl import getSecurityManager +- +-from Products.Five.browser import BrowserView +-from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile ++from DateTime import DateTime + from Products.CMFCore.permissions import ModifyPortalContent + from Products.CMFCore.utils import getToolByName +- +-from plone.app.iterate.permissions import CheckoutPermission +-from plone.app.iterate.util import get_storage ++from Products.CMFPlone.log import logger ++from Products.Five.browser import BrowserView ++from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile ++from plone.app.iterate.interfaces import ICheckinCheckoutPolicy + from plone.app.iterate.interfaces import keys, IBaseline +- +-from plone.app.iterate.relation import WorkingCopyRelation +- ++from plone.app.iterate.permissions import CheckoutPermission + from plone.memoize.instance import memoize +-from Products.CMFPlone.log import logger ++from zope.interface import implements ++from zope.viewlet.interfaces import IViewlet ++ + +-class BaseInfoViewlet( BrowserView ): ++class BaseInfoViewlet(BrowserView): + +- implements( IViewlet ) ++ implements(IViewlet) + +- def __init__( self, context, request, view, manager ): +- super( BaseInfoViewlet, self ).__init__( context, request ) ++ def __init__(self, context, request, view, manager): ++ super(BaseInfoViewlet, self).__init__(context, request) + self.__parent__ = view + self.view = view + self.manager = manager + +- def update( self ): ++ def update(self): + pass + +- def render( self ): ++ def render(self): + raise NotImplementedError + ++ @property ++ @memoize ++ def policy(self): ++ return ICheckinCheckoutPolicy(self.context) ++ + @memoize +- def created( self ): +- time = self.properties.get( keys.checkout_time, DateTime() ) ++ def created(self): ++ time = self.properties.get(keys.checkout_time, DateTime()) + util = getToolByName(self.context, 'translation_service') + return util.ulocalized_time(time, context=self.context, domain='plonelocales') + + @memoize +- def creator( self ): +- user_id = self.properties.get( keys.checkout_user ) ++ def creator(self): ++ user_id = self.properties.get(keys.checkout_user) + membership = getToolByName(self.context, 'portal_membership') + if not user_id: + return membership.getAuthenticatedMember() +- return membership.getMemberById( user_id ) ++ return membership.getMemberById(user_id) + + @memoize +- def creator_url( self ): ++ def creator_url(self): + creator = self.creator() + if creator is not None: + portal_url = getToolByName(self.context, 'portal_url') +- return "%s/author/%s" % ( portal_url(), creator.getId() ) +- ++ return "%s/author/%s" % (portal_url(), creator.getId()) + + @memoize +- def creator_name( self ): ++ def creator_name(self): + creator = self.creator() + if creator is not None: + return creator.getProperty('fullname') or creator.getId() +@@ -70,26 +68,27 @@ def creator_name( self ): + # the user and log this. + name = self.properties.get(keys.checkout_user) + if IBaseline.providedBy(self.context): +- warning_tpl = "%s is a baseline of a plone.app.iterate checkout by an unknown user id '%s'" ++ warning_tpl = "%s is a baseline of a plone.app.iterate checkout by an unknown user id '%s'" # noqa + else: + # IWorkingCopy.providedBy(self.context) +- warning_tpl = "%s is a working copy of a plone.app.iterate checkout by an unknown user id '%s'" ++ warning_tpl = "%s is a working copy of a plone.app.iterate checkout by an unknown user id '%s'" # noqa + logger.warning(warning_tpl, self.context, name) + return name + + @property + @memoize +- def properties( self ): +- wc_ref = self._getReference() +- if wc_ref is not None: +- return get_storage( wc_ref ) ++ def properties(self): ++ ref = self._getReference() ++ if ref: ++ return self.policy.getProperties(ref) + else: + return {} + +- def _getReference( self ): ++ def _getReference(self): + raise NotImplemented + +-class BaselineInfoViewlet( BaseInfoViewlet ): ++ ++class BaselineInfoViewlet(BaseInfoViewlet): + + index = ViewPageTemplateFile('info_baseline.pt') + +@@ -105,21 +104,14 @@ def render(self): + return "" + + @memoize +- def working_copy( self ): +- refs = self.context.getBRefs( WorkingCopyRelation.relationship ) +- if len( refs ) > 0: +- return refs[0] +- else: +- return None ++ def working_copy(self): ++ return self.policy.getWorkingCopy() + +- def _getReference( self ): +- refs = self.context.getBackReferenceImpl( WorkingCopyRelation.relationship ) +- if len( refs ) > 0: +- return refs[0] +- else: +- return None ++ def _getReference(self): ++ return self.working_copy() + +-class CheckoutInfoViewlet( BaseInfoViewlet ): ++ ++class CheckoutInfoViewlet(BaseInfoViewlet): + + index = ViewPageTemplateFile('info_checkout.pt') + +@@ -134,17 +126,8 @@ def render(self): + return "" + + @memoize +- def baseline( self ): +- refs = self.context.getReferences( WorkingCopyRelation.relationship ) +- if len( refs ) > 0: +- return refs[0] +- else: +- return None +- +- def _getReference( self ): +- refs = self.context.getReferenceImpl( WorkingCopyRelation.relationship ) +- if len( refs ) > 0: +- return refs[0] +- else: +- return None ++ def baseline(self): ++ return self.policy.getBaseline() + ++ def _getReference(self): ++ return self.baseline() +diff --git a/plone/app/iterate/browser/info_baseline.pt b/plone/app/iterate/browser/info_baseline.pt +index ad85a7d..6b7d6ef 100644 +--- a/plone/app/iterate/browser/info_baseline.pt ++++ b/plone/app/iterate/browser/info_baseline.pt +@@ -1,10 +1,12 @@ +
++ tal:define="working_copy view/working_copy; ++ isAnon context/@@plone_portal_state/anonymous;" ++ i18n:domain="plone" ++ tal:condition="python: not isAnon"> + + Warning + +- This item is being edited by ++ This item is being edited beingy + + + +@@ -77,4 +78,6 @@ + title="iterate : Check out content" + /> + ++ ++ + +diff --git a/plone/app/iterate/dexterity/__init__.py b/plone/app/iterate/dexterity/__init__.py +new file mode 100644 +index 0000000..f10e292 +--- /dev/null ++++ b/plone/app/iterate/dexterity/__init__.py +@@ -0,0 +1,2 @@ ++ ++ITERATE_RELATION_NAME = 'iterate-working-copy' +diff --git a/plone/app/iterate/dexterity/configure.zcml b/plone/app/iterate/dexterity/configure.zcml +new file mode 100644 +index 0000000..7351a01 +--- /dev/null ++++ b/plone/app/iterate/dexterity/configure.zcml +@@ -0,0 +1,26 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +diff --git a/plone/app/iterate/dexterity/copier.py b/plone/app/iterate/dexterity/copier.py +new file mode 100644 +index 0000000..50a1f45 +--- /dev/null ++++ b/plone/app/iterate/dexterity/copier.py +@@ -0,0 +1,172 @@ ++from Acquisition import aq_inner, aq_parent ++from Products.CMFCore.utils import getToolByName ++from Products.DCWorkflow.DCWorkflow import DCWorkflowDefinition ++from ZODB.PersistentMapping import PersistentMapping ++from plone.app.iterate import copier ++from plone.app.iterate import interfaces ++from plone.app.iterate.event import AfterCheckinEvent ++from plone.app.iterate.dexterity import ITERATE_RELATION_NAME ++from plone.app.iterate.dexterity.relation import StagingRelationValue ++from plone.dexterity.utils import iterSchemata ++from z3c.relationfield import event ++from zc.relation.interfaces import ICatalog ++from zope import component ++from zope.annotation.interfaces import IAnnotations ++from zope.event import notify ++from zope.interface import implements ++from zope.schema import getFieldsInOrder ++ ++ ++try: ++ from zope.intid.interfaces import IIntIds ++except: ++ from zope.app.intid.interfaces import IIntIds ++ ++ ++class ContentCopier(copier.ContentCopier): ++ implements(interfaces.IObjectCopier) ++ ++ def copyTo(self, container): ++ context = aq_inner(self.context) ++ wc = self._copyBaseline(container) ++ # get id of objects ++ intids = component.getUtility(IIntIds) ++ wc_id = intids.getId(wc) ++ # create a relation ++ relation = StagingRelationValue(wc_id) ++ event._setRelation(context, ITERATE_RELATION_NAME, relation) ++ # ++ self._handleReferences(self.context, wc, 'checkout', relation) ++ return wc, relation ++ ++ def merge(self): ++ baseline = self._getBaseline() ++ ++ # delete the working copy reference to the baseline ++ wc_ref = self._deleteWorkingCopyRelation() ++ ++ # reassemble references on the new baseline ++ self._handleReferences(baseline, self.context, "checkin", wc_ref) ++ ++ # move the working copy to the baseline container, deleting the baseline ++ new_baseline = self._replaceBaseline(baseline) ++ ++ # patch the working copy with baseline info not preserved during checkout ++ self._reassembleWorkingCopy(new_baseline, baseline) ++ ++ return new_baseline ++ ++ def _replaceBaseline(self, baseline): ++ wc_id = self.context.getId() ++ wc_container = aq_parent(self.context) ++ ++ # copy all field values from the working copy to the baseline ++ for schema in iterSchemata(baseline): ++ for name, field in getFieldsInOrder(schema): ++ # Skip read-only fields ++ if field.readonly: ++ continue ++ if field.__name__ == 'id': ++ continue ++ try: ++ value = field.get(schema(self.context)) ++ except: ++ value = None ++ ++ # TODO: We need a way to identify the DCFieldProperty ++ # fields and use the appropriate set_name/get_name ++ if name == 'effective': ++ baseline.effective_date = self.context.effective() ++ elif name == 'expires': ++ baseline.expiration_date = self.context.expires() ++ elif name == 'subjects': ++ baseline.setSubject(self.context.Subject()) ++ else: ++ field.set(baseline, value) ++ ++ baseline.reindexObject() ++ ++ # copy annotations ++ wc_annotations = IAnnotations(self.context) ++ baseline_annotations = IAnnotations(baseline) ++ ++ baseline_annotations.clear() ++ baseline_annotations.update(wc_annotations) ++ ++ # delete the working copy ++ wc_container._delObject(wc_id) ++ ++ return baseline ++ ++ def _reassembleWorkingCopy(self, new_baseline, baseline): ++ # reattach the source's workflow history, try avoid a dangling ref ++ try: ++ new_baseline.workflow_history = PersistentMapping(baseline.workflow_history.items()) ++ except AttributeError: ++ # No workflow apparently. Oh well. ++ pass ++ ++ # reset wf state security directly ++ workflow_tool = getToolByName(self.context, 'portal_workflow') ++ wfs = workflow_tool.getWorkflowsFor(self.context) ++ for wf in wfs: ++ if not isinstance(wf, DCWorkflowDefinition): ++ continue ++ wf.updateRoleMappingsFor(new_baseline) ++ return new_baseline ++ ++ def _handleReferences(self, baseline, wc, mode, wc_ref): ++ pass ++ ++ def _deleteWorkingCopyRelation(self): ++ # delete the wc reference keeping a reference to it for its annotations ++ relation = self._get_relation_to_baseline() ++ relation.broken(relation.to_path) ++ return relation ++ ++ def _get_relation_to_baseline(self): ++ context = aq_inner(self.context) ++ # get id ++ intids = component.getUtility(IIntIds) ++ id = intids.getId(context) ++ # ask catalog ++ catalog = component.getUtility(ICatalog) ++ relations = list(catalog.findRelations({'to_id': id})) ++ relations = filter(lambda r: r.from_attribute == ITERATE_RELATION_NAME, ++ relations) ++ # do we have a baseline in our relations? ++ if relations and not len(relations) == 1: ++ raise interfaces.CheckinException("Baseline count mismatch") ++ ++ if not relations or not relations[0]: ++ raise interfaces.CheckinException("Baseline has disappeared") ++ return relations[0] ++ ++ def _getBaseline(self): ++ intids = component.getUtility(IIntIds) ++ relation = self._get_relation_to_baseline() ++ if relation: ++ baseline = intids.getObject(relation.from_id) ++ ++ if not baseline: ++ raise interfaces.CheckinException("Baseline has disappeared") ++ return baseline ++ ++ def checkin(self, checkin_message): ++ # get the baseline for this working copy, raise if not found ++ baseline = self._getBaseline() ++ # get a hold of the relation object ++ relation = self._get_relation_to_baseline() ++ # publish the event for subscribers, early because contexts are about to be manipulated ++ notify(event.CheckinEvent(self.context, ++ baseline, ++ relation, ++ checkin_message ++ )) ++ # merge the object back to the baseline with a copier ++ copier = component.queryAdapter(self.context, ++ interfaces.IObjectCopier) ++ new_baseline = copier.merge() ++ # don't need to unlock the lock disappears with old baseline deletion ++ notify(AfterCheckinEvent(new_baseline, checkin_message)) ++ return new_baseline +diff --git a/plone/app/iterate/dexterity/interfaces.py b/plone/app/iterate/dexterity/interfaces.py +new file mode 100644 +index 0000000..0798744 +--- /dev/null ++++ b/plone/app/iterate/dexterity/interfaces.py +@@ -0,0 +1,11 @@ ++from plone.app.iterate.interfaces import IIterateAware ++from zope.interface import Attribute ++from z3c.relationfield.interfaces import IRelationValue ++ ++ ++class IStagingRelationValue(IRelationValue): ++ iterate_properties = Attribute('Iterate information') ++ ++ ++class IDexterityIterateAware(IIterateAware): ++ pass +\ No newline at end of file +diff --git a/plone/app/iterate/dexterity/policy.py b/plone/app/iterate/dexterity/policy.py +new file mode 100644 +index 0000000..89080de +--- /dev/null ++++ b/plone/app/iterate/dexterity/policy.py +@@ -0,0 +1,60 @@ ++from plone.app import iterate ++from plone.app.iterate.dexterity.utils import get_baseline ++from plone.app.iterate.dexterity.utils import get_relations ++from plone.app.iterate.dexterity.utils import get_working_copy ++from plone.app.iterate.dexterity.utils import get_checkout_relation ++from zope import component ++from zope.event import notify ++from zope.interface import implements ++ ++ ++class CheckinCheckoutPolicyAdapter(iterate.policy.CheckinCheckoutPolicyAdapter): ++ """ ++ Dexterity Checkin Checkout Policy ++ """ ++ implements(iterate.interfaces.ICheckinCheckoutPolicy) ++ ++ def _get_relation_to_baseline(self): ++ # do we have a baseline in our relations? ++ relations = get_relations(self.context) ++ ++ if relations and not len(relations) == 1: ++ raise iterate.interfaces.CheckinException("Baseline count mismatch") ++ ++ if not relations or not relations[0]: ++ raise iterate.interfaces.CheckinException("Baseline has disappeared") ++ ++ return relations[0] ++ ++ def _getBaseline(self): ++ baseline = get_baseline(self.context) ++ if not baseline: ++ raise iterate.interfaces.CheckinException("Baseline has disappeared") ++ return baseline ++ ++ def checkin(self, checkin_message): ++ # get the baseline for this working copy, raise if not found ++ baseline = self._getBaseline() ++ # get a hold of the relation object ++ relation = self._get_relation_to_baseline() ++ # publish the event for subscribers, early because contexts are about to be manipulated ++ notify(iterate.event.CheckinEvent(self.context, ++ baseline, ++ relation, ++ checkin_message)) ++ # merge the object back to the baseline with a copier ++ copier = component.queryAdapter(self.context, ++ iterate.interfaces.IObjectCopier) ++ new_baseline = copier.merge() ++ # don't need to unlock the lock disappears with old baseline deletion ++ notify(iterate.event.AfterCheckinEvent(new_baseline, checkin_message)) ++ return new_baseline ++ ++ def getBaseline(self): ++ return get_baseline(self.context) ++ ++ def getWorkingCopy(self): ++ return get_working_copy(self.context) ++ ++ def getProperties(self, obj): ++ return get_checkout_relation(obj).iterate_properties +\ No newline at end of file +diff --git a/plone/app/iterate/dexterity/relation.py b/plone/app/iterate/dexterity/relation.py +new file mode 100644 +index 0000000..7267d10 +--- /dev/null ++++ b/plone/app/iterate/dexterity/relation.py +@@ -0,0 +1,41 @@ ++from Products.CMFCore.interfaces import ISiteRoot ++from Products.CMFCore.utils import getToolByName ++from persistent.dict import PersistentDict ++from plone.app.iterate.dexterity.interfaces import IStagingRelationValue ++from z3c.relationfield import relation ++from zc.relation.interfaces import ICatalog ++from zope.annotation.interfaces import IAttributeAnnotatable ++from zope.component import getUtility ++from zope.interface import implements ++ ++ ++try: ++ from zope.intid.interfaces import IIntIds ++except ImportError: ++ from zope.app.intid.interfaces import IIntIds ++ ++ ++class StagingRelationValue(relation.RelationValue): ++ implements(IStagingRelationValue, IAttributeAnnotatable) ++ ++ @classmethod ++ def get_relations_of(cls, obj, from_attribute=None): ++ """ a list of relations to or from the passed object ++ """ ++ catalog = getUtility(ICatalog) ++ intids = getUtility(IIntIds) ++ obj_id = intids.getId(obj) ++ items = list(catalog.findRelations({'from_id': obj_id})) ++ items += list(catalog.findRelations({'to_id': obj_id})) ++ if from_attribute: ++ condition = lambda r: r.from_attribute == from_attribute and not r.is_broken() ++ items = filter(condition, items) ++ return items ++ ++ def __init__(self, to_id): ++ super(StagingRelationValue, self).__init__(to_id) ++ self.iterate_properties = PersistentDict() ++ # remember the creator ++ portal = getUtility(ISiteRoot) ++ mstool = getToolByName(portal, 'portal_membership') ++ self.creator = mstool.getAuthenticatedMember().getId() +diff --git a/plone/app/iterate/dexterity/utils.py b/plone/app/iterate/dexterity/utils.py +new file mode 100644 +index 0000000..bb2fbc3 +--- /dev/null ++++ b/plone/app/iterate/dexterity/utils.py +@@ -0,0 +1,51 @@ ++from Acquisition import aq_inner, aq_base ++from plone.app.iterate.dexterity import ITERATE_RELATION_NAME ++from zc.relation.interfaces import ICatalog ++from zope import component ++ ++ ++try: ++ from zope.intid.interfaces import IIntIds ++except: ++ from zope.app.intid.interfaces import IIntIds ++ ++ ++def get_relations(context): ++ context = aq_inner(context) ++ # get id ++ intids = component.getUtility(IIntIds) ++ id = intids.queryId(aq_base(context)) ++ if not id: ++ # for objects without intid or ++ # objects being deleted in the current transaction return empty list ++ return [] ++ # ask catalog ++ catalog = component.getUtility(ICatalog) ++ relations = list(catalog.findRelations({'to_id': id})) ++ relations += list(catalog.findRelations({'from_id': id})) ++ relations = filter(lambda r: r.from_attribute == ITERATE_RELATION_NAME, relations) ++ return relations ++ ++ ++def get_checkout_relation(context): ++ relations = get_relations(context) ++ if len(relations) > 0: ++ return relations[0] ++ else: ++ return None ++ ++ ++def get_baseline(context): ++ relation = get_checkout_relation(context) ++ if relation and relation.from_id: ++ intids = component.getUtility(IIntIds) ++ return intids.getObject(relation.from_id) ++ return None ++ ++ ++def get_working_copy(context): ++ relation = get_checkout_relation(context) ++ if relation and relation.to_id: ++ intids = component.getUtility(IIntIds) ++ return intids.getObject(relation.to_id) ++ return None +diff --git a/plone/app/iterate/interfaces.py b/plone/app/iterate/interfaces.py +index 81e78d0..627bae4 100644 +--- a/plone/app/iterate/interfaces.py ++++ b/plone/app/iterate/interfaces.py +@@ -33,30 +33,30 @@ + from Products.Archetypes.interfaces import IReference + + ################################ +-## Marker interface ++# Marker interface + +-class IIterateAware( Interface ): ++class IIterateAware(Interface): + """An object that can be used for check-in/check-out operations. + """ + + ################################# +-## Lock types ++# Lock types + +-ITERATE_LOCK = LockType( u'iterate.lock', stealable=False, user_unlockable=False, timeout=MAX_TIMEOUT) ++ITERATE_LOCK = LockType(u'iterate.lock', stealable=False, user_unlockable=False, timeout=MAX_TIMEOUT) # noqa + + ################################# +-## Exceptions ++# Exceptions + +-class CociException( Exception ): ++class CociException(Exception): + pass + +-class CheckinException( CociException ): ++class CheckinException(CociException): + pass + +-class CheckoutException( CociException ): ++class CheckoutException(CociException): + pass + +-class ConflictError( CheckinException ): ++class ConflictError(CheckinException): + pass + + +@@ -64,17 +64,16 @@ class ConflictError( CheckinException ): + # Annotation Key + annotation_key = "ore.iterate" + +-class keys( object ): ++class keys(object): + # various common keys + checkout_user = "checkout_user" + checkout_time = "checkout_time" + + +- + ################################# +-## Event Interfaces ++# Event Interfaces + +-class ICheckinEvent( IObjectEvent ): ++class ICheckinEvent(IObjectEvent): + """ a working copy is being checked in, event.object is the working copy, this + message is sent before any mutation/merge has been done on the objects + """ +@@ -83,26 +82,26 @@ class ICheckinEvent( IObjectEvent ): + relation = Attribute("The Working Copy Archetypes Relation Object") + checkin_message = Attribute("checkin message") + +-class IAfterCheckinEvent( IObjectEvent ): ++class IAfterCheckinEvent(IObjectEvent): + """ sent out after an object is checked in """ + + checkin_message = Attribute("checkin message") + +-class IBeforeCheckoutEvent( IObjectEvent ): ++class IBeforeCheckoutEvent(IObjectEvent): + """ sent out before a working copy is created """ + +-class ICheckoutEvent( IObjectEvent ): ++class ICheckoutEvent(IObjectEvent): + """ an object is being checked out, event.object is the baseline """ + + working_copy = Attribute("The object's working copy") + relation = Attribute("The Working Copy Archetypes Relation Object") + +-class ICancelCheckoutEvent( IObjectEvent ): ++class ICancelCheckoutEvent(IObjectEvent): + """ a working copy is being cancelled """ + + baseline = Attribute("The working copy's baseline") + +-class IWorkingCopyDeletedEvent( IObjectEvent ): ++class IWorkingCopyDeletedEvent(IObjectEvent): + """ a working copy is being deleted, this gets called multiple times at different + states. so on cancel checkout and checkin operations, its mostly designed to + broadcast an event when the user deletes a working copy using the standard +@@ -115,27 +114,27 @@ class IWorkingCopyDeletedEvent( IObjectEvent ): + ################################# + # Content Marker Interfaces + +-class IIterateManagedContent ( Interface ): ++class IIterateManagedContent(Interface): + """Any content managed by iterate - normally a sub-interface is + applied as a marker to an instance. + """ + +-class IWorkingCopy( IIterateManagedContent ): ++class IWorkingCopy(IIterateManagedContent): + """A working copy/check-out + """ + +-class IBaseline( IIterateManagedContent ): ++class IBaseline(IIterateManagedContent): + """A baseline + """ + +-class IWorkingCopyRelation( IReference ): ++class IWorkingCopyRelation(IReference): + """A relationship to a working copy + """ + + ################################# +-## Working copy container locator ++# Working copy container locator + +-class IWCContainerLocator( Interface ): ++class IWCContainerLocator(Interface): + """A named adapter capable of discovering containers where working + copies can be created. + """ +@@ -149,80 +148,80 @@ def __call__(): + """ + + ################################# +-## Interfaces ++# Interfaces + +-class ICheckinCheckoutTool( Interface ): ++class ICheckinCheckoutTool(Interface): + +- def allowCheckin( content ): ++ def allowCheckin(content): + """ + denotes whether a checkin operation can be performed on the content. + """ + +- def allowCheckout( content ): ++ def allowCheckout(content): + """ + denotes whether a checkout operation can be performed on the content. + """ + +- def allowCancelCheckout( content ): ++ def allowCancelCheckout(content): + """ + denotes whether a cancel checkout operation can be performed on the content. + """ + +- def checkin( content, checkin_messsage ): ++ def checkin(content, checkin_messsage): + """ + check the working copy in, this will merge the working copy with the baseline + + """ + +- def checkout( container, content ): ++ def checkout(container, content): + """ + """ + +- def cancelCheckout( content ): ++ def cancelCheckout(content): + """ + """ + + +-class IObjectCopier( Interface ): ++class IObjectCopier(Interface): + """ copies and merges the object state + """ + +- def copyTo( container ): ++ def copyTo(container): + """ copy the context to the given container, must also create an AT relation + using the WorkingCopyRelation.relation name between the source and the copy. + returns the copy. + """ + +- def merge( ): ++ def merge(): + """ merge/replace the source with the copy, context is the copy. + """ + +-class IObjectArchiver( Interface ): ++class IObjectArchiver(Interface): + """ iterate needs minimal versioning support + """ + +- def save( checkin_message ): ++ def save(checkin_message): + """ save a new version of the object + """ + +- def isVersioned( self ): ++ def isVersioned(self): + """ is this content already versioned + """ + +- def isVersionable( self ): ++ def isVersionable(self): + """ is versionable check. + """ + +- def isModified( self ): ++ def isModified(self): + """ is the resource current state, different than its last saved state. + """ + +-class ICheckinCheckoutPolicy( Interface ): ++class ICheckinCheckoutPolicy(Interface): + """ + Checkin / Checkout Policy + """ + +- def checkin( checkin_message ): ++ def checkin(checkin_message): + """ + checkin the context, if the target has been deleted then raises a checkin exception. + +@@ -231,7 +230,7 @@ def checkin( checkin_message ): + # + """ + +- def checkout( container ): ++ def checkout(container): + """ + checkout the content object into the container, iff another object with + the same id exists the id is amended, the working copy object is returned. +@@ -241,44 +240,52 @@ def checkout( container ): + raises a CheckoutError if the object is already checked out. + """ + +- def cancelCheckout( ): ++ def cancelCheckout(): + """ + coxtent is a checkout (working copy), this method will go ahead and delete + the working copy. + """ + +- def getWorkingCopies( ): ++ def getWorkingCopies(): ++ """ ++ """ ++ ++ def getBaseline(): ++ """ ++ """ ++ ++ def getWorkingCopy(): + """ + """ + +-## def merge( content ): +-## """ +-## if there are known conflicts between the checkout and the checkedin version, +-## using the merge method signals that conflicts have been resolved in the working +-## copy. +-## """ ++# def merge( content ): ++# """ ++# if there are known conflicts between the checkout and the checkedin version, ++# using the merge method signals that conflicts have been resolved in the working ++# copy. ++# """ + + + ################################# + +-class ICheckinCheckoutReference( Interface ): ++class ICheckinCheckoutReference(Interface): + # a reference processor + +- def checkout( baseline, wc, references, storage ): ++ def checkout(baseline, wc, references, storage): + """ + handle processing of the given references from the baseline + into the working copy, storage is an annotation for bookkeeping + information. + """ + +- def checkoutBackReferences( baseline, wc, references, storage ): ++ def checkoutBackReferences(baseline, wc, references, storage): + """ + """ + +- def checkin( baseline, wc, references, storage ): ++ def checkin(baseline, wc, references, storage): + """ + """ + +- def checkinBackReferences( baseline, wc, references, storage ): ++ def checkinBackReferences(baseline, wc, references, storage): + """ + """ +diff --git a/plone/app/iterate/policy.py b/plone/app/iterate/policy.py +index bcd3c68..66c2ad0 100644 +--- a/plone/app/iterate/policy.py ++++ b/plone/app/iterate/policy.py +@@ -24,101 +24,116 @@ + + """ + ++from Acquisition import aq_inner, aq_parent ++from Products.Archetypes.interfaces import IReferenceable ++import event ++import interfaces ++from plone.app.iterate.util import get_storage ++from relation import WorkingCopyRelation + from zope import component + from zope.event import notify + from zope.interface import implements + +-from Acquisition import Implicit, aq_base, aq_inner, aq_parent + +-import interfaces +-import event +-import lock +- +-from relation import WorkingCopyRelation +- +-class CheckinCheckoutPolicyAdapter( object ): ++class CheckinCheckoutPolicyAdapter(object): + """ + Default Checkin Checkout Policy For Content + + on checkout context is the baseline + +- on checkin context is the working copy ++ on checkin context is the working copy. ++ ++ This default Policy works with Archetypes. ++ ++ dexterity folder has dexterity compatible one + """ + +- implements( interfaces.ICheckinCheckoutPolicy ) +- component.adapts( interfaces.IIterateAware ) ++ implements(interfaces.ICheckinCheckoutPolicy) ++ component.adapts(interfaces.IIterateAware) + + # used when creating baseline version for first time + default_base_message = "Created Baseline" + +- def __init__( self, context ): ++ def __init__(self, context): + self.context = context + +- def checkout( self, container ): ++ def checkout(self, container): + # see interface +- notify( event.BeforeCheckoutEvent( self.context ) ) ++ notify(event.BeforeCheckoutEvent(self.context)) + + # use the object copier to checkout the content to the container +- copier = component.queryAdapter( self.context, interfaces.IObjectCopier ) +- working_copy, relation = copier.copyTo( container ) ++ copier = component.queryAdapter(self.context, interfaces.IObjectCopier) ++ working_copy, relation = copier.copyTo(container) + + # publish the event for any subscribers +- notify( event.CheckoutEvent( self.context, working_copy, relation ) ) ++ notify(event.CheckoutEvent(self.context, working_copy, relation)) + + # finally return the working copy + return working_copy + +- def checkin( self, checkin_message ): ++ def checkin(self, checkin_message): + # see interface + + # get the baseline for this working copy, raise if not found + baseline = self._getBaseline() + + # get a hold of the relation object +- wc_ref = self.context.getReferenceImpl( WorkingCopyRelation.relationship )[ 0] ++ wc_ref = self.context.getReferenceImpl(WorkingCopyRelation.relationship)[0] + + # publish the event for subscribers, early because contexts are about to be manipulated +- notify( event.CheckinEvent( self.context, baseline, wc_ref, checkin_message ) ) ++ notify(event.CheckinEvent(self.context, baseline, wc_ref, checkin_message)) + + # merge the object back to the baseline with a copier + + # XXX by gotcha + # bug we should or use a getAdapter call or test if copier is None +- copier = component.queryAdapter( self.context, interfaces.IObjectCopier ) ++ copier = component.queryAdapter(self.context, interfaces.IObjectCopier) + new_baseline = copier.merge() + + # don't need to unlock the lock disappears with old baseline deletion +- notify( event.AfterCheckinEvent( new_baseline, checkin_message ) ) ++ notify(event.AfterCheckinEvent(new_baseline, checkin_message)) + + return new_baseline + +- def cancelCheckout( self ): ++ def cancelCheckout(self): + # see interface + + # get the baseline + baseline = self._getBaseline() + + # publish an event +- notify( event.CancelCheckoutEvent( self.context, baseline ) ) ++ notify(event.CancelCheckoutEvent(self.context, baseline)) + + # delete the working copy +- wc_container = aq_parent( aq_inner( self.context ) ) +- wc_container.manage_delObjects( [ self.context.getId() ] ) ++ wc_container = aq_parent(aq_inner(self.context)) ++ wc_container.manage_delObjects([self.context.getId()]) + + return baseline + + ################################# +- ## Checkin Support Methods ++ # Checkin Support Methods + +- def _getBaseline( self ): ++ def _getBaseline(self): + # follow the working copy's reference back to the baseline +- refs = self.context.getRefs( WorkingCopyRelation.relationship ) ++ refs = self.context.getReferences(WorkingCopyRelation.relationship) + + if not len(refs) == 1: +- raise interfaces.CheckinException( "Baseline count mismatch" ) ++ raise interfaces.CheckinException("Baseline count mismatch") + + if not refs or refs[0] is None: +- raise interfaces.CheckinException( "Baseline has disappeared" ) ++ raise interfaces.CheckinException("Baseline has disappeared") + + baseline = refs[0] + return baseline ++ ++ def getBaseline(self): ++ if IReferenceable.providedBy(self.context): ++ refs = self.context.getReferences(WorkingCopyRelation.relationship) ++ if refs: ++ return refs[0] ++ ++ def getWorkingCopy(self): ++ return self.context.getBRefs(WorkingCopyRelation.relationship) ++ ++ def getProperties(self, obj): ++ return get_storage(self, obj) +\ No newline at end of file +diff --git a/plone/app/iterate/relation.py b/plone/app/iterate/relation.py +index 1cf2bb1..21b6fc6 100644 +--- a/plone/app/iterate/relation.py ++++ b/plone/app/iterate/relation.py +@@ -35,7 +35,7 @@ + from interfaces import IIterateAware + + +-class WorkingCopyRelation( Reference ): ++class WorkingCopyRelation(Reference): + """ + Source Object is Working Copy + +@@ -43,10 +43,10 @@ class WorkingCopyRelation( Reference ): + """ + relationship = "Working Copy Relation" + +- implements( IWorkingCopyRelation, IAttributeAnnotatable ) ++ implements(IWorkingCopyRelation, IAttributeAnnotatable) + + +-class CheckinCheckoutReferenceAdapter ( object ): ++class CheckinCheckoutReferenceAdapter(object): + """ + default adapter for references. + +@@ -65,47 +65,46 @@ class CheckinCheckoutReferenceAdapter ( object ): + + """ + +- implements( ICheckinCheckoutReference ) +- adapts( IIterateAware ) ++ implements(ICheckinCheckoutReference) ++ adapts(IIterateAware) + + storage_key = "coci.references" + +- def __init__(self, context ): ++ def __init__(self, context): + self.context = context + +- def checkout( self, baseline, wc, refs, storage ): ++ def checkout(self, baseline, wc, refs, storage): + for ref in refs: +- wc.addReference( ref.targetUID, ref.relationship, referenceClass=ref.__class__ ) ++ wc.addReference(ref.targetUID, ref.relationship, referenceClass=ref.__class__) + +- def checkin( self, *args ): ++ def checkin(self, *args): + pass + + checkoutBackReferences = checkinBackReferences = checkin + + +- +-class NoCopyReferenceAdapter( object ): ++class NoCopyReferenceAdapter(object): + """ + an adapter for references that does not copy them to the wc on checkout. + + additionally custom reference state is kept when the wc is checked in. + """ + +- implements( ICheckinCheckoutReference ) ++ implements(ICheckinCheckoutReference) + + def __init__(self, context): + self.context = context + +- def checkin( self, baseline, wc, refs, storage ): ++ def checkin(self, baseline, wc, refs, storage): + # move the references from the baseline to the wc + + # one note, on checkin the wc uid is not yet changed to match that of the baseline + ref_ids = [r.getId() for r in refs] + +- baseline_ref_container = getattr( baseline, atconf.REFERENCE_ANNOTATION ) +- clipboard = baseline_ref_container.manage_cutObjects( ref_ids ) ++ baseline_ref_container = getattr(baseline, atconf.REFERENCE_ANNOTATION) ++ clipboard = baseline_ref_container.manage_cutObjects(ref_ids) + +- wc_ref_container = getattr( wc, atconf.REFERENCE_ANNOTATION ) ++ wc_ref_container = getattr(wc, atconf.REFERENCE_ANNOTATION) + + # references aren't globally addable w/ associated perm which default copysupport + # wants to check, temporarily monkey around the issue. +diff --git a/plone/app/iterate/testing.py b/plone/app/iterate/testing.py +index c14b947..a562686 100644 +--- a/plone/app/iterate/testing.py ++++ b/plone/app/iterate/testing.py +@@ -1,4 +1,5 @@ + # -*- coding: utf-8 -*- ++from plone.app.contenttypes.testing import PloneAppContenttypes + from plone.app.testing import PLONE_FIXTURE + from plone.app.testing import PloneSandboxLayer + from plone.app.testing import applyProfile +@@ -107,3 +108,23 @@ def setUpPloneSite(self, portal): + PLONEAPPITERATE_FUNCTIONAL_TESTING = FunctionalTesting( + bases=(PLONEAPPITERATE_FIXTURE,), + name="PloneAppIterateLayer:Functional") ++ ++ ++class DexPloneAppIterateLayer(PloneAppContenttypes): ++ def setUpZope(self, app, configurationContext): ++ import plone.app.iterate ++ self.loadZCML(package=plone.app.iterate) ++ z2.installProduct(app, 'plone.app.iterate') ++ ++ def setUpPloneSite(self, portal): ++ applyProfile(portal, 'plone.app.iterate:plone.app.iterate') ++ ++ ++PLONEAPPITERATEDEX_FIXTURE = DexPloneAppIterateLayer() ++PLONEAPPITERATEDEX_INTEGRATION_TESTING = IntegrationTesting( ++ bases=(PLONEAPPITERATEDEX_FIXTURE,), ++ name="DexPloneAppIterateLayer:Integration") ++ ++PLONEAPPITERATEDEX_FUNCTIONAL_TESTING = FunctionalTesting( ++ bases=(PLONEAPPITERATEDEX_FIXTURE,), ++ name="DexPloneAppIterateLayer:Functional") +diff --git a/plone/app/iterate/tests/dexterity.rst b/plone/app/iterate/tests/dexterity.rst +new file mode 100644 +index 0000000..6579768 +--- /dev/null ++++ b/plone/app/iterate/tests/dexterity.rst +@@ -0,0 +1,59 @@ ++Staging behavior regression tests ++================================= ++ ++Tests for bugs that would distract from usage examples in stagingbehavior.txt ++ ++If we access the site as an admin TTW:: ++ ++ >>> from plone.testing.z2 import Browser ++ >>> browser = Browser(layer["app"]) ++ >>> browser.handleErrors = False ++ >>> portal = layer["portal"] ++ >>> portal_url = "http://nohost/plone" ++ >>> from plone.app.testing.interfaces import SITE_OWNER_NAME, SITE_OWNER_PASSWORD ++ >>> browser.addHeader("Authorization", "Basic %s:%s" % (SITE_OWNER_NAME, SITE_OWNER_PASSWORD)) ++ ++KeyError with aquisition wrapper ++========================================= ++ ++When an item provides IBaseline (has been checked in at least once) and it is accessed through an ++Aquisition wrapper you get a KeyError from zope.intid, originating from five.intid. ++ ++ >>> browser.open(portal_url + "/folder_factories") ++ >>> browser.getControl("Folder").click() ++ >>> browser.getControl("Add").click() ++ >>> import pdb; pdb.set_trace() ++ >>> browser.getControl(name="form.widgets.IDublinCore.title").value = "My Folder" ++ >>> browser.getControl(name="form.buttons.save").click() ++ >>> import pdb; pdb.set_trace() ++ >>> browser.url ++ 'http://nohost/plone/my-folder/view' ++ ++ >>> browser.open("http://nohost/plone/my-folder/folder_factories") ++ >>> browser.getControl("Page").click() ++ >>> browser.getControl("Add").click() ++ >>> browser.getControl(name="form.widgets.IDublinCore.title").value = "My Sub-object" ++ >>> browser.getControl(name="form.buttons.save").click() ++ >>> browser.url ++ 'http://nohost/plone/my-folder/my-sub-object/view' ++ ++Checkout ++ ++ >>> browser.getLink("Check out").click() ++ >>> browser.contents ++ '...This is a working copy of...My Sub-object..., made by...admin... on...' ++ ++Checkin ++ ++ >>> browser.getLink("Check in").click() ++ >>> browser.contents ++ '...Check in...' ++ >>> browser.getControl(name="form.button.Checkin").click() ++ >>> browser.url ++ 'http://nohost/plone/my-folder/my-sub-object' ++ ++Test can view through Aquisition wrapper (repeating test_folder is deliberate here) ++ ++ >>> browser.open("http://nohost/plone/my-folder/my-sub-object") ++ >>> browser.contents ++ '...My Sub-object...' +diff --git a/plone/app/iterate/tests/test_annotations.py b/plone/app/iterate/tests/test_annotations.py +new file mode 100644 +index 0000000..81c272f +--- /dev/null ++++ b/plone/app/iterate/tests/test_annotations.py +@@ -0,0 +1,79 @@ ++# -*- coding: utf-8 -*- ++ ++import unittest ++ ++from zope.annotation.interfaces import IAnnotatable ++from zope.annotation.interfaces import IAnnotations ++ ++from zope.component import getAdapters ++ ++from plone.app.iterate.interfaces import ICheckinCheckoutPolicy ++from plone.app.iterate.interfaces import IWCContainerLocator ++ ++from plone.app.testing import TEST_USER_ID ++from plone.app.testing import setRoles ++ ++from plone.app.iterate.testing import PLONEAPPITERATEDEX_INTEGRATION_TESTING ++ ++ ++class AnnotationsTestCase(unittest.TestCase): ++ ++ layer = PLONEAPPITERATEDEX_INTEGRATION_TESTING ++ ++ def setUp(self): ++ self.portal = self.layer['portal'] ++ setRoles(self.portal, TEST_USER_ID, ['Manager']) ++ self.portal.invokeFactory('Document', 's1') ++ self.s1 = self.portal['s1'] ++ ++ def test_object_annotatable(self): ++ self.assertTrue(IAnnotatable.providedBy(self.s1)) ++ ++ def test_annotation_saved_on_checkin(self): ++ # First we get and save a custom annotation to the existing object ++ obj_annotations = IAnnotations(self.s1) ++ self.assertEqual(obj_annotations, {}) ++ ++ obj_annotations['key1'] = u'value1' ++ obj_annotations = IAnnotations(self.s1) ++ self.assertEqual(obj_annotations, {'key1': u'value1'}) ++ ++ # Now, let's get a working copy for it. ++ locators = getAdapters((self.s1,), IWCContainerLocator) ++ location = u'plone.app.iterate.parent' ++ locator = [c[1] for c in locators if c[0] == location][0] ++ ++ policy = ICheckinCheckoutPolicy(self.s1) ++ ++ wc = policy.checkout(locator()) ++ ++ # Annotations should be the same ++ new_annotations = IAnnotations(wc) ++ self.assertEqual(new_annotations['key1'], u'value1') ++ ++ # Now, let's modify the existing one, and create a new one ++ new_annotations['key1'] = u'value2' ++ new_annotations['key2'] = u'value1' ++ ++ # Check that annotations were stored correctly and original ones were ++ # not overriten ++ new_annotations = IAnnotations(wc) ++ self.assertEqual(new_annotations['key1'], u'value2') ++ self.assertEqual(new_annotations['key2'], u'value1') ++ ++ obj_annotations = IAnnotations(self.s1) ++ self.assertEqual(obj_annotations['key1'], u'value1') ++ self.assertFalse('key2' in obj_annotations) ++ ++ # Now, we do a checkin ++ policy = ICheckinCheckoutPolicy(wc) ++ policy.checkin(u'Commit message') ++ ++ # And finally check that the old object has the same annotations as ++ # its working copy ++ ++ obj_annotations = IAnnotations(self.s1) ++ self.assertTrue('key1' in obj_annotations) ++ self.assertTrue('key2' in obj_annotations) ++ self.assertEqual(obj_annotations['key1'], u'value2') ++ self.assertEqual(obj_annotations['key2'], u'value1') +diff --git a/plone/app/iterate/tests/test_doctests.py b/plone/app/iterate/tests/test_doctests.py +index 2e2a393..ce053be 100644 +--- a/plone/app/iterate/tests/test_doctests.py ++++ b/plone/app/iterate/tests/test_doctests.py +@@ -3,6 +3,7 @@ + from unittest import TestSuite + + from plone.app.iterate.testing import PLONEAPPITERATE_FUNCTIONAL_TESTING ++from plone.app.iterate.testing import PLONEAPPITERATEDEX_FUNCTIONAL_TESTING + from plone.testing import layered + + +@@ -17,4 +18,12 @@ def test_suite(): + ), + layer=PLONEAPPITERATE_FUNCTIONAL_TESTING) + ) ++ suite.addTest(layered( ++ doctest.DocFileSuite( ++ 'dexterity.rst', ++ optionflags=OPTIONFLAGS, ++ package="plone.app.iterate.tests", ++ ), ++ layer=PLONEAPPITERATEDEX_FUNCTIONAL_TESTING) ++ ) + return suite +diff --git a/plone/app/iterate/tests/test_iterate.py b/plone/app/iterate/tests/test_iterate.py +index b5cb43b..e30534e 100644 +--- a/plone/app/iterate/tests/test_iterate.py ++++ b/plone/app/iterate/tests/test_iterate.py +@@ -29,9 +29,7 @@ + + from plone.app.iterate.interfaces import ICheckinCheckoutPolicy + from plone.app.iterate.testing import PLONEAPPITERATE_INTEGRATION_TESTING +-from plone.app.iterate.testing import PLONEAPPITERATE_FUNCTIONAL_TESTING + +-from plone.app.testing import SITE_OWNER_NAME + from plone.app.testing import TEST_USER_ID + from plone.app.testing import TEST_USER_NAME + from plone.app.testing import login +diff --git a/plone/app/iterate/util.py b/plone/app/iterate/util.py +index 6a83207..edebb5d 100644 +--- a/plone/app/iterate/util.py ++++ b/plone/app/iterate/util.py +@@ -25,12 +25,13 @@ + from interfaces import annotation_key + from Products.CMFCore.utils import getToolByName + +-def get_storage( context ): +- annotations = IAnnotations( context ) +- if not annotations.has_key( annotation_key ): +- annotations[ annotation_key ] = PersistentDict() ++def get_storage(context): ++ annotations = IAnnotations(context) ++ if annotation_key not in annotations: ++ annotations[annotation_key] = PersistentDict() + return annotations[annotation_key] + ++ + def upgrade_by_reinstall(context): + qi = getToolByName(context, 'portal_quickinstaller') + qi.reinstallProducts(['plone.app.iterate']) +diff --git a/setup.py b/setup.py +index d82dd1d..12dc11c 100644 +--- a/setup.py ++++ b/setup.py +@@ -1,13 +1,11 @@ + from setuptools import setup, find_packages + +-version = '3.0.2.dev0' ++version = '3.1.0.dev0' + + setup(name='plone.app.iterate', + version=version, + description="check-out/check-in staging for Plone", +- long_description=\ +- open("README.rst").read() + "\n" + \ +- open("CHANGES.rst").read(), ++ long_description=open("README.rst").read() + "\n" + open("CHANGES.rst").read(), + classifiers=[ + "Environment :: Web Environment", + "Framework :: Plone", +@@ -17,20 +15,21 @@ + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 2.7", +- ], ++ ], + keywords='', + author='Plone Foundation', + author_email='plone-developers@lists.sourceforge.net', + url='http://pypi.python.org/pypi/plone.app.iterate', + license='GPL version 2', + packages=find_packages(exclude=['ez_setup']), +- namespace_packages = ['plone', 'plone.app'], ++ namespace_packages=['plone', 'plone.app'], + include_package_data=True, + zip_safe=False, + extras_require=dict( +- test=[ +- 'plone.app.testing', +- ] ++ test=[ ++ 'plone.app.testing', ++ 'plone.app.contenttypes' ++ ] + ), + install_requires=[ + 'setuptools', +@@ -55,7 +54,7 @@ + 'ZODB3', + 'Zope2', + ], +- entry_points = ''' ++ entry_points=''' + [z3c.autoinclude.plugin] + target = plone + ''', + + +Repository: plone.app.iterate + + +Branch: refs/heads/master +Date: 2015-07-15T18:09:39+02:00 +Author: vangheem (vangheem) +Commit: https://github.com/plone/plone.app.iterate/commit/1e12b189b1f4410bc2875af39d77092e0c58a1e5 + +add more tests + +Files changed: +A plone/app/iterate/tests/test_interfaces.py +M plone/app/iterate/browser/info.py +M plone/app/iterate/browser/info_baseline.pt +M plone/app/iterate/dexterity/policy.py +M plone/app/iterate/permissions.py +M plone/app/iterate/policy.py +M plone/app/iterate/testing.py +M plone/app/iterate/tests/dexterity.rst +M plone/app/iterate/tests/test_annotations.py +M plone/app/iterate/util.py + +diff --git a/plone/app/iterate/browser/info.py b/plone/app/iterate/browser/info.py +index 0c1012f..f8e7833 100644 +--- a/plone/app/iterate/browser/info.py ++++ b/plone/app/iterate/browser/info.py +@@ -80,7 +80,7 @@ def creator_name(self): + def properties(self): + ref = self._getReference() + if ref: +- return self.policy.getProperties(ref) ++ return self.policy.getProperties(ref, default={}) + else: + return {} + +diff --git a/plone/app/iterate/browser/info_baseline.pt b/plone/app/iterate/browser/info_baseline.pt +index 6b7d6ef..e7e2799 100644 +--- a/plone/app/iterate/browser/info_baseline.pt ++++ b/plone/app/iterate/browser/info_baseline.pt +@@ -6,7 +6,7 @@ + + Warning + +- This item is being edited beingy ++ This item is being edited by + >> browser.open(portal_url + "/folder_factories") + >>> browser.getControl("Folder").click() + >>> browser.getControl("Add").click() +- >>> import pdb; pdb.set_trace() + >>> browser.getControl(name="form.widgets.IDublinCore.title").value = "My Folder" + >>> browser.getControl(name="form.buttons.save").click() +- >>> import pdb; pdb.set_trace() + >>> browser.url + 'http://nohost/plone/my-folder/view' + +diff --git a/plone/app/iterate/tests/test_annotations.py b/plone/app/iterate/tests/test_annotations.py +index 81c272f..cab1f79 100644 +--- a/plone/app/iterate/tests/test_annotations.py ++++ b/plone/app/iterate/tests/test_annotations.py +@@ -2,18 +2,14 @@ + + import unittest + +-from zope.annotation.interfaces import IAnnotatable +-from zope.annotation.interfaces import IAnnotations +- +-from zope.component import getAdapters +- + from plone.app.iterate.interfaces import ICheckinCheckoutPolicy + from plone.app.iterate.interfaces import IWCContainerLocator +- ++from plone.app.iterate.testing import PLONEAPPITERATEDEX_INTEGRATION_TESTING + from plone.app.testing import TEST_USER_ID + from plone.app.testing import setRoles +- +-from plone.app.iterate.testing import PLONEAPPITERATEDEX_INTEGRATION_TESTING ++from zope.annotation.interfaces import IAnnotatable ++from zope.annotation.interfaces import IAnnotations ++from zope.component import getAdapters + + + class AnnotationsTestCase(unittest.TestCase): +diff --git a/plone/app/iterate/tests/test_interfaces.py b/plone/app/iterate/tests/test_interfaces.py +new file mode 100644 +index 0000000..e180081 +--- /dev/null ++++ b/plone/app/iterate/tests/test_interfaces.py +@@ -0,0 +1,94 @@ ++from plone.app.iterate.interfaces import IBaseline ++from plone.app.iterate.interfaces import ICheckinCheckoutPolicy ++from plone.app.iterate.interfaces import IIterateAware ++from plone.app.iterate.interfaces import IWorkingCopy ++from plone.app.iterate.testing import PLONEAPPITERATEDEX_INTEGRATION_TESTING ++from plone.app.testing import TEST_USER_ID ++from plone.app.testing import TEST_USER_NAME ++from plone.app.testing import login ++from plone.app.testing import logout ++from plone.app.testing import setRoles ++from plone.dexterity.utils import createContentInContainer ++from unittest2 import TestCase ++ ++ ++class TestObjectsProvideCorrectInterfaces(TestCase): ++ """Since p.a.iterate replaces the baseline on checkin with the working copy ++ but p.a.stagingbehavior just copies the values, the provided interfaces ++ may be wrong after checkin. ++ ++ For making sure that provided interfaces are correct in every state we ++ test it here. ++ ++ See: https://dev.plone.org/ticket/13163 ++ """ ++ ++ layer = PLONEAPPITERATEDEX_INTEGRATION_TESTING ++ ++ def setUp(self): ++ super(TestObjectsProvideCorrectInterfaces, self).setUp() ++ ++ self.portal = self.layer['portal'] ++ setRoles(self.portal, TEST_USER_ID, ['Manager']) ++ login(self.portal, TEST_USER_NAME) ++ ++ # create a folder where everything of this test suite should happen ++ self.assertNotIn('test-folder', self.portal.objectIds()) ++ self.folder = self.portal.get( ++ self.portal.invokeFactory('Folder', 'test-folder')) ++ ++ self.obj = createContentInContainer(self.folder, 'Document') ++ ++ def tearDown(self): ++ self.portal.manage_delObjects([self.folder.id]) ++ logout() ++ setRoles(self.portal, TEST_USER_ID, ['Member']) ++ super(TestObjectsProvideCorrectInterfaces, self).tearDown() ++ ++ def do_checkout(self): ++ policy = ICheckinCheckoutPolicy(self.obj) ++ working_copy = policy.checkout(self.folder) ++ return working_copy ++ ++ def do_cancel(self, working_copy): ++ policy = ICheckinCheckoutPolicy(working_copy) ++ policy.cancelCheckout() ++ ++ def do_checkin(self, working_copy): ++ policy = ICheckinCheckoutPolicy(working_copy) ++ policy.checkin('') ++ ++ def test_before_checkout(self): ++ self.assertTrue(self.obj) ++ self.assertTrue(IIterateAware.providedBy(self.obj)) ++ self.assertFalse(IBaseline.providedBy(self.obj)) ++ self.assertFalse(IWorkingCopy.providedBy(self.obj)) ++ ++ def test_after_checkout(self): ++ working_copy = self.do_checkout() ++ self.assertTrue(working_copy) ++ self.assertTrue(IIterateAware.providedBy(working_copy)) ++ self.assertFalse(IBaseline.providedBy(working_copy)) ++ self.assertTrue(IWorkingCopy.providedBy(working_copy)) ++ ++ self.assertTrue(IIterateAware.providedBy(self.obj)) ++ self.assertTrue(IBaseline.providedBy(self.obj)) ++ self.assertFalse(IWorkingCopy.providedBy(self.obj)) ++ ++ def test_after_cancel_checkout(self): ++ working_copy = self.do_checkout() ++ self.assertTrue(working_copy) ++ ++ self.do_cancel(working_copy) ++ self.assertTrue(IIterateAware.providedBy(self.obj)) ++ self.assertFalse(IBaseline.providedBy(self.obj)) ++ self.assertFalse(IWorkingCopy.providedBy(self.obj)) ++ ++ def test_after_checkin(self): ++ working_copy = self.do_checkout() ++ self.assertTrue(working_copy) ++ ++ self.do_checkin(working_copy) ++ self.assertTrue(IIterateAware.providedBy(self.obj)) ++ self.assertFalse(IBaseline.providedBy(self.obj)) ++ self.assertFalse(IWorkingCopy.providedBy(self.obj)) +diff --git a/plone/app/iterate/util.py b/plone/app/iterate/util.py +index edebb5d..f1a2908 100644 +--- a/plone/app/iterate/util.py ++++ b/plone/app/iterate/util.py +@@ -25,9 +25,11 @@ + from interfaces import annotation_key + from Products.CMFCore.utils import getToolByName + +-def get_storage(context): ++def get_storage(context, default=None): + annotations = IAnnotations(context) + if annotation_key not in annotations: ++ if default is not None: ++ return default + annotations[annotation_key] = PersistentDict() + return annotations[annotation_key] + + + +Repository: plone.app.iterate + + +Branch: refs/heads/master +Date: 2015-07-16T13:27:50+02:00 +Author: Ramon Navarro Bosch (bloodbare) +Commit: https://github.com/plone/plone.app.iterate/commit/5e27ac902eb8fdd334a9c754fa7c83f23ca9c17b + +Merge pull request #15 from plone/dexterity-support + +Provide dexterity support in plone.app.iterate + +Files changed: +A plone/app/iterate/dexterity/__init__.py +A plone/app/iterate/dexterity/configure.zcml +A plone/app/iterate/dexterity/copier.py +A plone/app/iterate/dexterity/interfaces.py +A plone/app/iterate/dexterity/policy.py +A plone/app/iterate/dexterity/relation.py +A plone/app/iterate/dexterity/utils.py +A plone/app/iterate/tests/dexterity.rst +A plone/app/iterate/tests/test_annotations.py +A plone/app/iterate/tests/test_interfaces.py +M CHANGES.rst +M plone/app/iterate/__init__.py +M plone/app/iterate/archiver.py +M plone/app/iterate/browser/cancel.pt +M plone/app/iterate/browser/checkin.pt +M plone/app/iterate/browser/control.py +M plone/app/iterate/browser/diff.py +M plone/app/iterate/browser/info.py +M plone/app/iterate/browser/info_baseline.pt +M plone/app/iterate/configure.zcml +M plone/app/iterate/interfaces.py +M plone/app/iterate/permissions.py +M plone/app/iterate/policy.py +M plone/app/iterate/relation.py +M plone/app/iterate/testing.py +M plone/app/iterate/tests/test_doctests.py +M plone/app/iterate/tests/test_iterate.py +M plone/app/iterate/util.py +M setup.py + +diff --git a/CHANGES.rst b/CHANGES.rst +index 15c8459..3c05579 100644 +--- a/CHANGES.rst ++++ b/CHANGES.rst +@@ -1,9 +1,13 @@ + Changelog + ========= + +-3.0.2 (unreleased) ++3.1.0 (unreleased) + ------------------ + ++- merge plone.app.stagingbehavior into plone.app.iterate without the ++ behavior implementation. This is for Plone 5 iterate support ++ [vangheem] ++ + - Don't remove aquisition on object for getToolByName call + [tomgross] + +diff --git a/plone/app/iterate/__init__.py b/plone/app/iterate/__init__.py +index bcc6582..4b7c8c5 100644 +--- a/plone/app/iterate/__init__.py ++++ b/plone/app/iterate/__init__.py +@@ -22,7 +22,25 @@ + """ + """ + ++import logging + from zope.i18nmessageid import MessageFactory ++from plone.app.iterate import permissions # noqa ++ + PloneMessageFactory = MessageFactory('plone') ++logger = logging.getLogger('plone.app.iterate') ++ ++ ++try: ++ import plone.app.relationfield # noqa ++except ImportError: ++ logger.warn('Dexterity support for iterate is not available. ' ++ 'You must install plone.app.relationfield') ++ + +-from plone.app.iterate import permissions ++try: ++ import plone.app.stagingbehavior # noqa ++ logger.error('plone.app.stagingbehavior should NOT be installed with this version ' ++ 'of plone.app.iterate. You may experience problems running this configuration. ' ++ 'plone.app.iterate now has dexterity suport built-in.') ++except ImportError: ++ pass +\ No newline at end of file +diff --git a/plone/app/iterate/archiver.py b/plone/app/iterate/archiver.py +index 3dff719..bf8c13b 100644 +--- a/plone/app/iterate/archiver.py ++++ b/plone/app/iterate/archiver.py +@@ -30,30 +30,30 @@ + + import interfaces + +-class ContentArchiver( object ): ++class ContentArchiver(object): + +- implements( interfaces.IObjectArchiver ) +- adapts( interfaces.IIterateAware ) ++ implements(interfaces.IObjectArchiver) ++ adapts(interfaces.IIterateAware) + +- def __init__( self, context ): ++ def __init__(self, context): + self.context = context + self.repository = getToolByName(context, 'portal_repository') + +- def save( self, checkin_message ): +- self.repository.save( self.context, checkin_message ) ++ def save(self, checkin_message): ++ self.repository.save(self.context, checkin_message) + +- def isVersionable( self ): +- if not self.repository.isVersionable( self.context ): ++ def isVersionable(self): ++ if not self.repository.isVersionable(self.context): + return False + return True + +- def isVersioned( self ): ++ def isVersioned(self): + archivist = getToolByName(self.context, 'portal_archivist') +- version_count = len( archivist.queryHistory( self.context ) ) +- return bool( version_count ) ++ version_count = len(archivist.queryHistory(self.context)) ++ return bool(version_count) + +- def isModified( self ): ++ def isModified(self): + try: +- return not self.repository.isUpToDate( self.context ) ++ return not self.repository.isUpToDate(self.context) + except: + return False +diff --git a/plone/app/iterate/browser/cancel.pt b/plone/app/iterate/browser/cancel.pt +index e7f68b2..a1af209 100644 +--- a/plone/app/iterate/browser/cancel.pt ++++ b/plone/app/iterate/browser/cancel.pt +@@ -1,7 +1,15 @@ +- +- +-
+- ++ ++ ++ ++ ++ ++
+
+@@ -38,6 +46,9 @@ +
+ + +-
++ ++ ++ + +- ++ ++ +\ No newline at end of file +diff --git a/plone/app/iterate/browser/checkin.pt b/plone/app/iterate/browser/checkin.pt +index af03ac4..ebfa2ed 100644 +--- a/plone/app/iterate/browser/checkin.pt ++++ b/plone/app/iterate/browser/checkin.pt +@@ -1,6 +1,15 @@ +- ++ ++ + +-
++ ++ ++
+ +
+ +
++
++
++
+ +-
+- +- ++ ++ +\ No newline at end of file +diff --git a/plone/app/iterate/browser/control.py b/plone/app/iterate/browser/control.py +index ec3efd1..2d5a391 100644 +--- a/plone/app/iterate/browser/control.py ++++ b/plone/app/iterate/browser/control.py +@@ -20,16 +20,14 @@ + # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ################################################################## + +-from plone.memoize.view import memoize +- + from AccessControl import getSecurityManager + from Acquisition import aq_inner +-from Products.Five.browser import BrowserView +-from Products.Archetypes.interfaces import IReferenceable + import Products.CMFCore.permissions +- ++from Products.Five.browser import BrowserView + from plone.app.iterate import interfaces +-from plone.app.iterate.relation import WorkingCopyRelation ++from plone.app.iterate.interfaces import ICheckinCheckoutPolicy ++from plone.app.iterate.interfaces import IWorkingCopy ++from plone.memoize.view import memoize + + + class Control(BrowserView): +@@ -38,12 +36,6 @@ class Control(BrowserView): + This is a public view, referenced in action condition expressions. + """ + +- def get_original(self, context): +- if IReferenceable.providedBy(context): +- refs = context.getRefs(WorkingCopyRelation.relationship) +- if refs: +- return refs[0] +- + def checkin_allowed(self): + """Check if a checkin is allowed + """ +@@ -57,12 +49,15 @@ def checkin_allowed(self): + if not archiver.isVersionable(): + return False + +- original = self.get_original(context) ++ if not IWorkingCopy.providedBy(context): ++ return False ++ ++ policy = ICheckinCheckoutPolicy(context) ++ original = policy.getBaseline() + if original is None: + return False + +- if not checkPermission( +- Products.CMFCore.permissions.ModifyPortalContent, original): ++ if not checkPermission(Products.CMFCore.permissions.ModifyPortalContent, original): + return False + + return True +@@ -75,19 +70,17 @@ def checkout_allowed(self): + if not interfaces.IIterateAware.providedBy(context): + return False + +- if not IReferenceable.providedBy(context): +- return False +- + archiver = interfaces.IObjectArchiver(context) + if not archiver.isVersionable(): + return False + +- # check if there is an existing checkout +- if len(context.getBRefs(WorkingCopyRelation.relationship)) > 0: ++ policy = ICheckinCheckoutPolicy(context) ++ ++ if policy.getWorkingCopy() is not None: + return False + + # check if its is a checkout +- if len(context.getRefs(WorkingCopyRelation.relationship)) > 0: ++ if policy.getBaseline() is not None: + return False + + return True +@@ -97,4 +90,6 @@ def cancel_allowed(self): + """Check to see if the user can cancel the checkout on the + given working copy + """ +- return self.get_original(aq_inner(self.context)) is not None ++ policy = ICheckinCheckoutPolicy(self.context) ++ original = policy.getBaseline() ++ return original is not None +diff --git a/plone/app/iterate/browser/diff.py b/plone/app/iterate/browser/diff.py +index 7d0b712..792ee51 100644 +--- a/plone/app/iterate/browser/diff.py ++++ b/plone/app/iterate/browser/diff.py +@@ -6,30 +6,26 @@ + from Products.Five.browser import BrowserView + + from plone.app.iterate.interfaces import IWorkingCopy, IBaseline +-from plone.app.iterate.relation import WorkingCopyRelation ++from plone.app.iterate.interfaces import ICheckinCheckoutPolicy + +-class DiffView( BrowserView ): + +- def __init__( self, context, request ): +- self.context = context +- self.request = request +- if IBaseline.providedBy( self.context ): +- self.baseline = context +- self.working_copy = context.getBackReferences( WorkingCopyRelation.relationship )[0] +- elif IWorkingCopy.providedBy( self.context ): +- self.working_copy = context +- self.baseline = context.getReferences( WorkingCopyRelation.relationship )[0] ++class DiffView(BrowserView): ++ ++ def __call__(self): ++ policy = ICheckinCheckoutPolicy(self.context) ++ if IBaseline.providedBy(self.context): ++ self.baseline = self.context ++ self.working_copy = policy.getWorkingCopy() ++ elif IWorkingCopy.providedBy(self.context): ++ self.working_copy = self.context ++ self.baseline = policy.getBaseline() + else: + raise AttributeError("Invalid Context") ++ return self.index() + +- def diffs( self ): ++ def diffs(self): + diff = getToolByName(self.context, 'portal_diff') +- return diff.createChangeSet( self.baseline, +- self.working_copy, +- id1="Baseline", +- id2="Working Copy" ) +- +- +- +- +- ++ return diff.createChangeSet(self.baseline, ++ self.working_copy, ++ id1="Baseline", ++ id2="Working Copy") +diff --git a/plone/app/iterate/browser/info.py b/plone/app/iterate/browser/info.py +index 8578340..f8e7833 100644 +--- a/plone/app/iterate/browser/info.py ++++ b/plone/app/iterate/browser/info.py +@@ -2,67 +2,65 @@ + $Id: base.py 1808 2007-02-06 11:39:11Z hazmat $ + """ + +-from zope.interface import implements +- +-from zope.viewlet.interfaces import IViewlet +- +-from DateTime import DateTime + from AccessControl import getSecurityManager +- +-from Products.Five.browser import BrowserView +-from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile ++from DateTime import DateTime + from Products.CMFCore.permissions import ModifyPortalContent + from Products.CMFCore.utils import getToolByName +- +-from plone.app.iterate.permissions import CheckoutPermission +-from plone.app.iterate.util import get_storage ++from Products.CMFPlone.log import logger ++from Products.Five.browser import BrowserView ++from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile ++from plone.app.iterate.interfaces import ICheckinCheckoutPolicy + from plone.app.iterate.interfaces import keys, IBaseline +- +-from plone.app.iterate.relation import WorkingCopyRelation +- ++from plone.app.iterate.permissions import CheckoutPermission + from plone.memoize.instance import memoize +-from Products.CMFPlone.log import logger ++from zope.interface import implements ++from zope.viewlet.interfaces import IViewlet ++ + +-class BaseInfoViewlet( BrowserView ): ++class BaseInfoViewlet(BrowserView): + +- implements( IViewlet ) ++ implements(IViewlet) + +- def __init__( self, context, request, view, manager ): +- super( BaseInfoViewlet, self ).__init__( context, request ) ++ def __init__(self, context, request, view, manager): ++ super(BaseInfoViewlet, self).__init__(context, request) + self.__parent__ = view + self.view = view + self.manager = manager + +- def update( self ): ++ def update(self): + pass + +- def render( self ): ++ def render(self): + raise NotImplementedError + ++ @property ++ @memoize ++ def policy(self): ++ return ICheckinCheckoutPolicy(self.context) ++ + @memoize +- def created( self ): +- time = self.properties.get( keys.checkout_time, DateTime() ) ++ def created(self): ++ time = self.properties.get(keys.checkout_time, DateTime()) + util = getToolByName(self.context, 'translation_service') + return util.ulocalized_time(time, context=self.context, domain='plonelocales') + + @memoize +- def creator( self ): +- user_id = self.properties.get( keys.checkout_user ) ++ def creator(self): ++ user_id = self.properties.get(keys.checkout_user) + membership = getToolByName(self.context, 'portal_membership') + if not user_id: + return membership.getAuthenticatedMember() +- return membership.getMemberById( user_id ) ++ return membership.getMemberById(user_id) + + @memoize +- def creator_url( self ): ++ def creator_url(self): + creator = self.creator() + if creator is not None: + portal_url = getToolByName(self.context, 'portal_url') +- return "%s/author/%s" % ( portal_url(), creator.getId() ) +- ++ return "%s/author/%s" % (portal_url(), creator.getId()) + + @memoize +- def creator_name( self ): ++ def creator_name(self): + creator = self.creator() + if creator is not None: + return creator.getProperty('fullname') or creator.getId() +@@ -70,26 +68,27 @@ def creator_name( self ): + # the user and log this. + name = self.properties.get(keys.checkout_user) + if IBaseline.providedBy(self.context): +- warning_tpl = "%s is a baseline of a plone.app.iterate checkout by an unknown user id '%s'" ++ warning_tpl = "%s is a baseline of a plone.app.iterate checkout by an unknown user id '%s'" # noqa + else: + # IWorkingCopy.providedBy(self.context) +- warning_tpl = "%s is a working copy of a plone.app.iterate checkout by an unknown user id '%s'" ++ warning_tpl = "%s is a working copy of a plone.app.iterate checkout by an unknown user id '%s'" # noqa + logger.warning(warning_tpl, self.context, name) + return name + + @property + @memoize +- def properties( self ): +- wc_ref = self._getReference() +- if wc_ref is not None: +- return get_storage( wc_ref ) ++ def properties(self): ++ ref = self._getReference() ++ if ref: ++ return self.policy.getProperties(ref, default={}) + else: + return {} + +- def _getReference( self ): ++ def _getReference(self): + raise NotImplemented + +-class BaselineInfoViewlet( BaseInfoViewlet ): ++ ++class BaselineInfoViewlet(BaseInfoViewlet): + + index = ViewPageTemplateFile('info_baseline.pt') + +@@ -105,21 +104,14 @@ def render(self): + return "" + + @memoize +- def working_copy( self ): +- refs = self.context.getBRefs( WorkingCopyRelation.relationship ) +- if len( refs ) > 0: +- return refs[0] +- else: +- return None ++ def working_copy(self): ++ return self.policy.getWorkingCopy() + +- def _getReference( self ): +- refs = self.context.getBackReferenceImpl( WorkingCopyRelation.relationship ) +- if len( refs ) > 0: +- return refs[0] +- else: +- return None ++ def _getReference(self): ++ return self.working_copy() + +-class CheckoutInfoViewlet( BaseInfoViewlet ): ++ ++class CheckoutInfoViewlet(BaseInfoViewlet): + + index = ViewPageTemplateFile('info_checkout.pt') + +@@ -134,17 +126,8 @@ def render(self): + return "" + + @memoize +- def baseline( self ): +- refs = self.context.getReferences( WorkingCopyRelation.relationship ) +- if len( refs ) > 0: +- return refs[0] +- else: +- return None +- +- def _getReference( self ): +- refs = self.context.getReferenceImpl( WorkingCopyRelation.relationship ) +- if len( refs ) > 0: +- return refs[0] +- else: +- return None ++ def baseline(self): ++ return self.policy.getBaseline() + ++ def _getReference(self): ++ return self.baseline() +diff --git a/plone/app/iterate/browser/info_baseline.pt b/plone/app/iterate/browser/info_baseline.pt +index ad85a7d..e7e2799 100644 +--- a/plone/app/iterate/browser/info_baseline.pt ++++ b/plone/app/iterate/browser/info_baseline.pt +@@ -1,6 +1,8 @@ +
++ tal:define="working_copy view/working_copy; ++ isAnon context/@@plone_portal_state/anonymous;" ++ i18n:domain="plone" ++ tal:condition="python: not isAnon"> + + Warning + +diff --git a/plone/app/iterate/configure.zcml b/plone/app/iterate/configure.zcml +index c1fa981..f4b5074 100644 +--- a/plone/app/iterate/configure.zcml ++++ b/plone/app/iterate/configure.zcml +@@ -2,6 +2,7 @@ + xmlns="http://namespaces.zope.org/zope" + xmlns:five="http://namespaces.zope.org/five" + xmlns:genericsetup="http://namespaces.zope.org/genericsetup" ++ xmlns:zcml="http://namespaces.zope.org/zcml" + i18n_domain="plone"> + + +@@ -77,4 +78,6 @@ + title="iterate : Check out content" + /> + ++ ++ + +diff --git a/plone/app/iterate/dexterity/__init__.py b/plone/app/iterate/dexterity/__init__.py +new file mode 100644 +index 0000000..f10e292 +--- /dev/null ++++ b/plone/app/iterate/dexterity/__init__.py +@@ -0,0 +1,2 @@ ++ ++ITERATE_RELATION_NAME = 'iterate-working-copy' +diff --git a/plone/app/iterate/dexterity/configure.zcml b/plone/app/iterate/dexterity/configure.zcml +new file mode 100644 +index 0000000..7351a01 +--- /dev/null ++++ b/plone/app/iterate/dexterity/configure.zcml +@@ -0,0 +1,26 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +diff --git a/plone/app/iterate/dexterity/copier.py b/plone/app/iterate/dexterity/copier.py +new file mode 100644 +index 0000000..50a1f45 +--- /dev/null ++++ b/plone/app/iterate/dexterity/copier.py +@@ -0,0 +1,172 @@ ++from Acquisition import aq_inner, aq_parent ++from Products.CMFCore.utils import getToolByName ++from Products.DCWorkflow.DCWorkflow import DCWorkflowDefinition ++from ZODB.PersistentMapping import PersistentMapping ++from plone.app.iterate import copier ++from plone.app.iterate import interfaces ++from plone.app.iterate.event import AfterCheckinEvent ++from plone.app.iterate.dexterity import ITERATE_RELATION_NAME ++from plone.app.iterate.dexterity.relation import StagingRelationValue ++from plone.dexterity.utils import iterSchemata ++from z3c.relationfield import event ++from zc.relation.interfaces import ICatalog ++from zope import component ++from zope.annotation.interfaces import IAnnotations ++from zope.event import notify ++from zope.interface import implements ++from zope.schema import getFieldsInOrder ++ ++ ++try: ++ from zope.intid.interfaces import IIntIds ++except: ++ from zope.app.intid.interfaces import IIntIds ++ ++ ++class ContentCopier(copier.ContentCopier): ++ implements(interfaces.IObjectCopier) ++ ++ def copyTo(self, container): ++ context = aq_inner(self.context) ++ wc = self._copyBaseline(container) ++ # get id of objects ++ intids = component.getUtility(IIntIds) ++ wc_id = intids.getId(wc) ++ # create a relation ++ relation = StagingRelationValue(wc_id) ++ event._setRelation(context, ITERATE_RELATION_NAME, relation) ++ # ++ self._handleReferences(self.context, wc, 'checkout', relation) ++ return wc, relation ++ ++ def merge(self): ++ baseline = self._getBaseline() ++ ++ # delete the working copy reference to the baseline ++ wc_ref = self._deleteWorkingCopyRelation() ++ ++ # reassemble references on the new baseline ++ self._handleReferences(baseline, self.context, "checkin", wc_ref) ++ ++ # move the working copy to the baseline container, deleting the baseline ++ new_baseline = self._replaceBaseline(baseline) ++ ++ # patch the working copy with baseline info not preserved during checkout ++ self._reassembleWorkingCopy(new_baseline, baseline) ++ ++ return new_baseline ++ ++ def _replaceBaseline(self, baseline): ++ wc_id = self.context.getId() ++ wc_container = aq_parent(self.context) ++ ++ # copy all field values from the working copy to the baseline ++ for schema in iterSchemata(baseline): ++ for name, field in getFieldsInOrder(schema): ++ # Skip read-only fields ++ if field.readonly: ++ continue ++ if field.__name__ == 'id': ++ continue ++ try: ++ value = field.get(schema(self.context)) ++ except: ++ value = None ++ ++ # TODO: We need a way to identify the DCFieldProperty ++ # fields and use the appropriate set_name/get_name ++ if name == 'effective': ++ baseline.effective_date = self.context.effective() ++ elif name == 'expires': ++ baseline.expiration_date = self.context.expires() ++ elif name == 'subjects': ++ baseline.setSubject(self.context.Subject()) ++ else: ++ field.set(baseline, value) ++ ++ baseline.reindexObject() ++ ++ # copy annotations ++ wc_annotations = IAnnotations(self.context) ++ baseline_annotations = IAnnotations(baseline) ++ ++ baseline_annotations.clear() ++ baseline_annotations.update(wc_annotations) ++ ++ # delete the working copy ++ wc_container._delObject(wc_id) ++ ++ return baseline ++ ++ def _reassembleWorkingCopy(self, new_baseline, baseline): ++ # reattach the source's workflow history, try avoid a dangling ref ++ try: ++ new_baseline.workflow_history = PersistentMapping(baseline.workflow_history.items()) ++ except AttributeError: ++ # No workflow apparently. Oh well. ++ pass ++ ++ # reset wf state security directly ++ workflow_tool = getToolByName(self.context, 'portal_workflow') ++ wfs = workflow_tool.getWorkflowsFor(self.context) ++ for wf in wfs: ++ if not isinstance(wf, DCWorkflowDefinition): ++ continue ++ wf.updateRoleMappingsFor(new_baseline) ++ return new_baseline ++ ++ def _handleReferences(self, baseline, wc, mode, wc_ref): ++ pass ++ ++ def _deleteWorkingCopyRelation(self): ++ # delete the wc reference keeping a reference to it for its annotations ++ relation = self._get_relation_to_baseline() ++ relation.broken(relation.to_path) ++ return relation ++ ++ def _get_relation_to_baseline(self): ++ context = aq_inner(self.context) ++ # get id ++ intids = component.getUtility(IIntIds) ++ id = intids.getId(context) ++ # ask catalog ++ catalog = component.getUtility(ICatalog) ++ relations = list(catalog.findRelations({'to_id': id})) ++ relations = filter(lambda r: r.from_attribute == ITERATE_RELATION_NAME, ++ relations) ++ # do we have a baseline in our relations? ++ if relations and not len(relations) == 1: ++ raise interfaces.CheckinException("Baseline count mismatch") ++ ++ if not relations or not relations[0]: ++ raise interfaces.CheckinException("Baseline has disappeared") ++ return relations[0] ++ ++ def _getBaseline(self): ++ intids = component.getUtility(IIntIds) ++ relation = self._get_relation_to_baseline() ++ if relation: ++ baseline = intids.getObject(relation.from_id) ++ ++ if not baseline: ++ raise interfaces.CheckinException("Baseline has disappeared") ++ return baseline ++ ++ def checkin(self, checkin_message): ++ # get the baseline for this working copy, raise if not found ++ baseline = self._getBaseline() ++ # get a hold of the relation object ++ relation = self._get_relation_to_baseline() ++ # publish the event for subscribers, early because contexts are about to be manipulated ++ notify(event.CheckinEvent(self.context, ++ baseline, ++ relation, ++ checkin_message ++ )) ++ # merge the object back to the baseline with a copier ++ copier = component.queryAdapter(self.context, ++ interfaces.IObjectCopier) ++ new_baseline = copier.merge() ++ # don't need to unlock the lock disappears with old baseline deletion ++ notify(AfterCheckinEvent(new_baseline, checkin_message)) ++ return new_baseline +diff --git a/plone/app/iterate/dexterity/interfaces.py b/plone/app/iterate/dexterity/interfaces.py +new file mode 100644 +index 0000000..0798744 +--- /dev/null ++++ b/plone/app/iterate/dexterity/interfaces.py +@@ -0,0 +1,11 @@ ++from plone.app.iterate.interfaces import IIterateAware ++from zope.interface import Attribute ++from z3c.relationfield.interfaces import IRelationValue ++ ++ ++class IStagingRelationValue(IRelationValue): ++ iterate_properties = Attribute('Iterate information') ++ ++ ++class IDexterityIterateAware(IIterateAware): ++ pass +\ No newline at end of file +diff --git a/plone/app/iterate/dexterity/policy.py b/plone/app/iterate/dexterity/policy.py +new file mode 100644 +index 0000000..fa8b191 +--- /dev/null ++++ b/plone/app/iterate/dexterity/policy.py +@@ -0,0 +1,63 @@ ++from plone.app import iterate ++from plone.app.iterate.dexterity.utils import get_baseline ++from plone.app.iterate.dexterity.utils import get_relations ++from plone.app.iterate.dexterity.utils import get_working_copy ++from plone.app.iterate.dexterity.utils import get_checkout_relation ++from zope import component ++from zope.event import notify ++from zope.interface import implements ++ ++ ++class CheckinCheckoutPolicyAdapter(iterate.policy.CheckinCheckoutPolicyAdapter): ++ """ ++ Dexterity Checkin Checkout Policy ++ """ ++ implements(iterate.interfaces.ICheckinCheckoutPolicy) ++ ++ def _get_relation_to_baseline(self): ++ # do we have a baseline in our relations? ++ relations = get_relations(self.context) ++ ++ if relations and not len(relations) == 1: ++ raise iterate.interfaces.CheckinException("Baseline count mismatch") ++ ++ if not relations or not relations[0]: ++ raise iterate.interfaces.CheckinException("Baseline has disappeared") ++ ++ return relations[0] ++ ++ def _getBaseline(self): ++ baseline = get_baseline(self.context) ++ if not baseline: ++ raise iterate.interfaces.CheckinException("Baseline has disappeared") ++ return baseline ++ ++ def checkin(self, checkin_message): ++ # get the baseline for this working copy, raise if not found ++ baseline = self._getBaseline() ++ # get a hold of the relation object ++ relation = self._get_relation_to_baseline() ++ # publish the event for subscribers, early because contexts are about to be manipulated ++ notify(iterate.event.CheckinEvent(self.context, ++ baseline, ++ relation, ++ checkin_message)) ++ # merge the object back to the baseline with a copier ++ copier = component.queryAdapter(self.context, ++ iterate.interfaces.IObjectCopier) ++ new_baseline = copier.merge() ++ # don't need to unlock the lock disappears with old baseline deletion ++ notify(iterate.event.AfterCheckinEvent(new_baseline, checkin_message)) ++ return new_baseline ++ ++ def getBaseline(self): ++ return get_baseline(self.context) ++ ++ def getWorkingCopy(self): ++ return get_working_copy(self.context) ++ ++ def getProperties(self, obj, default=None): ++ try: ++ return get_checkout_relation(obj).iterate_properties ++ except AttributeError: ++ return default +\ No newline at end of file +diff --git a/plone/app/iterate/dexterity/relation.py b/plone/app/iterate/dexterity/relation.py +new file mode 100644 +index 0000000..7267d10 +--- /dev/null ++++ b/plone/app/iterate/dexterity/relation.py +@@ -0,0 +1,41 @@ ++from Products.CMFCore.interfaces import ISiteRoot ++from Products.CMFCore.utils import getToolByName ++from persistent.dict import PersistentDict ++from plone.app.iterate.dexterity.interfaces import IStagingRelationValue ++from z3c.relationfield import relation ++from zc.relation.interfaces import ICatalog ++from zope.annotation.interfaces import IAttributeAnnotatable ++from zope.component import getUtility ++from zope.interface import implements ++ ++ ++try: ++ from zope.intid.interfaces import IIntIds ++except ImportError: ++ from zope.app.intid.interfaces import IIntIds ++ ++ ++class StagingRelationValue(relation.RelationValue): ++ implements(IStagingRelationValue, IAttributeAnnotatable) ++ ++ @classmethod ++ def get_relations_of(cls, obj, from_attribute=None): ++ """ a list of relations to or from the passed object ++ """ ++ catalog = getUtility(ICatalog) ++ intids = getUtility(IIntIds) ++ obj_id = intids.getId(obj) ++ items = list(catalog.findRelations({'from_id': obj_id})) ++ items += list(catalog.findRelations({'to_id': obj_id})) ++ if from_attribute: ++ condition = lambda r: r.from_attribute == from_attribute and not r.is_broken() ++ items = filter(condition, items) ++ return items ++ ++ def __init__(self, to_id): ++ super(StagingRelationValue, self).__init__(to_id) ++ self.iterate_properties = PersistentDict() ++ # remember the creator ++ portal = getUtility(ISiteRoot) ++ mstool = getToolByName(portal, 'portal_membership') ++ self.creator = mstool.getAuthenticatedMember().getId() +diff --git a/plone/app/iterate/dexterity/utils.py b/plone/app/iterate/dexterity/utils.py +new file mode 100644 +index 0000000..bb2fbc3 +--- /dev/null ++++ b/plone/app/iterate/dexterity/utils.py +@@ -0,0 +1,51 @@ ++from Acquisition import aq_inner, aq_base ++from plone.app.iterate.dexterity import ITERATE_RELATION_NAME ++from zc.relation.interfaces import ICatalog ++from zope import component ++ ++ ++try: ++ from zope.intid.interfaces import IIntIds ++except: ++ from zope.app.intid.interfaces import IIntIds ++ ++ ++def get_relations(context): ++ context = aq_inner(context) ++ # get id ++ intids = component.getUtility(IIntIds) ++ id = intids.queryId(aq_base(context)) ++ if not id: ++ # for objects without intid or ++ # objects being deleted in the current transaction return empty list ++ return [] ++ # ask catalog ++ catalog = component.getUtility(ICatalog) ++ relations = list(catalog.findRelations({'to_id': id})) ++ relations += list(catalog.findRelations({'from_id': id})) ++ relations = filter(lambda r: r.from_attribute == ITERATE_RELATION_NAME, relations) ++ return relations ++ ++ ++def get_checkout_relation(context): ++ relations = get_relations(context) ++ if len(relations) > 0: ++ return relations[0] ++ else: ++ return None ++ ++ ++def get_baseline(context): ++ relation = get_checkout_relation(context) ++ if relation and relation.from_id: ++ intids = component.getUtility(IIntIds) ++ return intids.getObject(relation.from_id) ++ return None ++ ++ ++def get_working_copy(context): ++ relation = get_checkout_relation(context) ++ if relation and relation.to_id: ++ intids = component.getUtility(IIntIds) ++ return intids.getObject(relation.to_id) ++ return None +diff --git a/plone/app/iterate/interfaces.py b/plone/app/iterate/interfaces.py +index 81e78d0..627bae4 100644 +--- a/plone/app/iterate/interfaces.py ++++ b/plone/app/iterate/interfaces.py +@@ -33,30 +33,30 @@ + from Products.Archetypes.interfaces import IReference + + ################################ +-## Marker interface ++# Marker interface + +-class IIterateAware( Interface ): ++class IIterateAware(Interface): + """An object that can be used for check-in/check-out operations. + """ + + ################################# +-## Lock types ++# Lock types + +-ITERATE_LOCK = LockType( u'iterate.lock', stealable=False, user_unlockable=False, timeout=MAX_TIMEOUT) ++ITERATE_LOCK = LockType(u'iterate.lock', stealable=False, user_unlockable=False, timeout=MAX_TIMEOUT) # noqa + + ################################# +-## Exceptions ++# Exceptions + +-class CociException( Exception ): ++class CociException(Exception): + pass + +-class CheckinException( CociException ): ++class CheckinException(CociException): + pass + +-class CheckoutException( CociException ): ++class CheckoutException(CociException): + pass + +-class ConflictError( CheckinException ): ++class ConflictError(CheckinException): + pass + + +@@ -64,17 +64,16 @@ class ConflictError( CheckinException ): + # Annotation Key + annotation_key = "ore.iterate" + +-class keys( object ): ++class keys(object): + # various common keys + checkout_user = "checkout_user" + checkout_time = "checkout_time" + + +- + ################################# +-## Event Interfaces ++# Event Interfaces + +-class ICheckinEvent( IObjectEvent ): ++class ICheckinEvent(IObjectEvent): + """ a working copy is being checked in, event.object is the working copy, this + message is sent before any mutation/merge has been done on the objects + """ +@@ -83,26 +82,26 @@ class ICheckinEvent( IObjectEvent ): + relation = Attribute("The Working Copy Archetypes Relation Object") + checkin_message = Attribute("checkin message") + +-class IAfterCheckinEvent( IObjectEvent ): ++class IAfterCheckinEvent(IObjectEvent): + """ sent out after an object is checked in """ + + checkin_message = Attribute("checkin message") + +-class IBeforeCheckoutEvent( IObjectEvent ): ++class IBeforeCheckoutEvent(IObjectEvent): + """ sent out before a working copy is created """ + +-class ICheckoutEvent( IObjectEvent ): ++class ICheckoutEvent(IObjectEvent): + """ an object is being checked out, event.object is the baseline """ + + working_copy = Attribute("The object's working copy") + relation = Attribute("The Working Copy Archetypes Relation Object") + +-class ICancelCheckoutEvent( IObjectEvent ): ++class ICancelCheckoutEvent(IObjectEvent): + """ a working copy is being cancelled """ + + baseline = Attribute("The working copy's baseline") + +-class IWorkingCopyDeletedEvent( IObjectEvent ): ++class IWorkingCopyDeletedEvent(IObjectEvent): + """ a working copy is being deleted, this gets called multiple times at different + states. so on cancel checkout and checkin operations, its mostly designed to + broadcast an event when the user deletes a working copy using the standard +@@ -115,27 +114,27 @@ class IWorkingCopyDeletedEvent( IObjectEvent ): + ################################# + # Content Marker Interfaces + +-class IIterateManagedContent ( Interface ): ++class IIterateManagedContent(Interface): + """Any content managed by iterate - normally a sub-interface is + applied as a marker to an instance. + """ + +-class IWorkingCopy( IIterateManagedContent ): ++class IWorkingCopy(IIterateManagedContent): + """A working copy/check-out + """ + +-class IBaseline( IIterateManagedContent ): ++class IBaseline(IIterateManagedContent): + """A baseline + """ + +-class IWorkingCopyRelation( IReference ): ++class IWorkingCopyRelation(IReference): + """A relationship to a working copy + """ + + ################################# +-## Working copy container locator ++# Working copy container locator + +-class IWCContainerLocator( Interface ): ++class IWCContainerLocator(Interface): + """A named adapter capable of discovering containers where working + copies can be created. + """ +@@ -149,80 +148,80 @@ def __call__(): + """ + + ################################# +-## Interfaces ++# Interfaces + +-class ICheckinCheckoutTool( Interface ): ++class ICheckinCheckoutTool(Interface): + +- def allowCheckin( content ): ++ def allowCheckin(content): + """ + denotes whether a checkin operation can be performed on the content. + """ + +- def allowCheckout( content ): ++ def allowCheckout(content): + """ + denotes whether a checkout operation can be performed on the content. + """ + +- def allowCancelCheckout( content ): ++ def allowCancelCheckout(content): + """ + denotes whether a cancel checkout operation can be performed on the content. + """ + +- def checkin( content, checkin_messsage ): ++ def checkin(content, checkin_messsage): + """ + check the working copy in, this will merge the working copy with the baseline + + """ + +- def checkout( container, content ): ++ def checkout(container, content): + """ + """ + +- def cancelCheckout( content ): ++ def cancelCheckout(content): + """ + """ + + +-class IObjectCopier( Interface ): ++class IObjectCopier(Interface): + """ copies and merges the object state + """ + +- def copyTo( container ): ++ def copyTo(container): + """ copy the context to the given container, must also create an AT relation + using the WorkingCopyRelation.relation name between the source and the copy. + returns the copy. + """ + +- def merge( ): ++ def merge(): + """ merge/replace the source with the copy, context is the copy. + """ + +-class IObjectArchiver( Interface ): ++class IObjectArchiver(Interface): + """ iterate needs minimal versioning support + """ + +- def save( checkin_message ): ++ def save(checkin_message): + """ save a new version of the object + """ + +- def isVersioned( self ): ++ def isVersioned(self): + """ is this content already versioned + """ + +- def isVersionable( self ): ++ def isVersionable(self): + """ is versionable check. + """ + +- def isModified( self ): ++ def isModified(self): + """ is the resource current state, different than its last saved state. + """ + +-class ICheckinCheckoutPolicy( Interface ): ++class ICheckinCheckoutPolicy(Interface): + """ + Checkin / Checkout Policy + """ + +- def checkin( checkin_message ): ++ def checkin(checkin_message): + """ + checkin the context, if the target has been deleted then raises a checkin exception. + +@@ -231,7 +230,7 @@ def checkin( checkin_message ): + # + """ + +- def checkout( container ): ++ def checkout(container): + """ + checkout the content object into the container, iff another object with + the same id exists the id is amended, the working copy object is returned. +@@ -241,44 +240,52 @@ def checkout( container ): + raises a CheckoutError if the object is already checked out. + """ + +- def cancelCheckout( ): ++ def cancelCheckout(): + """ + coxtent is a checkout (working copy), this method will go ahead and delete + the working copy. + """ + +- def getWorkingCopies( ): ++ def getWorkingCopies(): ++ """ ++ """ ++ ++ def getBaseline(): ++ """ ++ """ ++ ++ def getWorkingCopy(): + """ + """ + +-## def merge( content ): +-## """ +-## if there are known conflicts between the checkout and the checkedin version, +-## using the merge method signals that conflicts have been resolved in the working +-## copy. +-## """ ++# def merge( content ): ++# """ ++# if there are known conflicts between the checkout and the checkedin version, ++# using the merge method signals that conflicts have been resolved in the working ++# copy. ++# """ + + + ################################# + +-class ICheckinCheckoutReference( Interface ): ++class ICheckinCheckoutReference(Interface): + # a reference processor + +- def checkout( baseline, wc, references, storage ): ++ def checkout(baseline, wc, references, storage): + """ + handle processing of the given references from the baseline + into the working copy, storage is an annotation for bookkeeping + information. + """ + +- def checkoutBackReferences( baseline, wc, references, storage ): ++ def checkoutBackReferences(baseline, wc, references, storage): + """ + """ + +- def checkin( baseline, wc, references, storage ): ++ def checkin(baseline, wc, references, storage): + """ + """ + +- def checkinBackReferences( baseline, wc, references, storage ): ++ def checkinBackReferences(baseline, wc, references, storage): + """ + """ +diff --git a/plone/app/iterate/permissions.py b/plone/app/iterate/permissions.py +index 9d72f33..6abebad 100644 +--- a/plone/app/iterate/permissions.py ++++ b/plone/app/iterate/permissions.py +@@ -22,7 +22,7 @@ + + from Products.CMFCore.permissions import setDefaultRoles + +-CheckinPermission = "iterate : Check in content" ++CheckinPermission = "iterate : Check in content" + CheckoutPermission = "iterate : Check out content" + + DEFAULT_ROLES = ('Manager', 'Owner', 'Site Administrator', 'Editor') +diff --git a/plone/app/iterate/policy.py b/plone/app/iterate/policy.py +index bcd3c68..336f53e 100644 +--- a/plone/app/iterate/policy.py ++++ b/plone/app/iterate/policy.py +@@ -24,101 +24,119 @@ + + """ + ++from Acquisition import aq_inner, aq_parent ++from Products.Archetypes.interfaces import IReferenceable ++import event ++import interfaces ++from plone.app.iterate.util import get_storage ++from relation import WorkingCopyRelation + from zope import component + from zope.event import notify + from zope.interface import implements + +-from Acquisition import Implicit, aq_base, aq_inner, aq_parent + +-import interfaces +-import event +-import lock +- +-from relation import WorkingCopyRelation +- +-class CheckinCheckoutPolicyAdapter( object ): ++class CheckinCheckoutPolicyAdapter(object): + """ + Default Checkin Checkout Policy For Content + + on checkout context is the baseline + +- on checkin context is the working copy ++ on checkin context is the working copy. ++ ++ This default Policy works with Archetypes. ++ ++ dexterity folder has dexterity compatible one + """ + +- implements( interfaces.ICheckinCheckoutPolicy ) +- component.adapts( interfaces.IIterateAware ) ++ implements(interfaces.ICheckinCheckoutPolicy) ++ component.adapts(interfaces.IIterateAware) + + # used when creating baseline version for first time + default_base_message = "Created Baseline" + +- def __init__( self, context ): ++ def __init__(self, context): + self.context = context + +- def checkout( self, container ): ++ def checkout(self, container): + # see interface +- notify( event.BeforeCheckoutEvent( self.context ) ) ++ notify(event.BeforeCheckoutEvent(self.context)) + + # use the object copier to checkout the content to the container +- copier = component.queryAdapter( self.context, interfaces.IObjectCopier ) +- working_copy, relation = copier.copyTo( container ) ++ copier = component.queryAdapter(self.context, interfaces.IObjectCopier) ++ working_copy, relation = copier.copyTo(container) + + # publish the event for any subscribers +- notify( event.CheckoutEvent( self.context, working_copy, relation ) ) ++ notify(event.CheckoutEvent(self.context, working_copy, relation)) + + # finally return the working copy + return working_copy + +- def checkin( self, checkin_message ): ++ def checkin(self, checkin_message): + # see interface + + # get the baseline for this working copy, raise if not found + baseline = self._getBaseline() + + # get a hold of the relation object +- wc_ref = self.context.getReferenceImpl( WorkingCopyRelation.relationship )[ 0] ++ wc_ref = self.context.getReferenceImpl(WorkingCopyRelation.relationship)[0] + + # publish the event for subscribers, early because contexts are about to be manipulated +- notify( event.CheckinEvent( self.context, baseline, wc_ref, checkin_message ) ) ++ notify(event.CheckinEvent(self.context, baseline, wc_ref, checkin_message)) + + # merge the object back to the baseline with a copier + + # XXX by gotcha + # bug we should or use a getAdapter call or test if copier is None +- copier = component.queryAdapter( self.context, interfaces.IObjectCopier ) ++ copier = component.queryAdapter(self.context, interfaces.IObjectCopier) + new_baseline = copier.merge() + + # don't need to unlock the lock disappears with old baseline deletion +- notify( event.AfterCheckinEvent( new_baseline, checkin_message ) ) ++ notify(event.AfterCheckinEvent(new_baseline, checkin_message)) + + return new_baseline + +- def cancelCheckout( self ): ++ def cancelCheckout(self): + # see interface + + # get the baseline + baseline = self._getBaseline() + + # publish an event +- notify( event.CancelCheckoutEvent( self.context, baseline ) ) ++ notify(event.CancelCheckoutEvent(self.context, baseline)) + + # delete the working copy +- wc_container = aq_parent( aq_inner( self.context ) ) +- wc_container.manage_delObjects( [ self.context.getId() ] ) ++ wc_container = aq_parent(aq_inner(self.context)) ++ wc_container.manage_delObjects([self.context.getId()]) + + return baseline + + ################################# +- ## Checkin Support Methods ++ # Checkin Support Methods + +- def _getBaseline( self ): ++ def _getBaseline(self): + # follow the working copy's reference back to the baseline +- refs = self.context.getRefs( WorkingCopyRelation.relationship ) ++ refs = self.context.getReferences(WorkingCopyRelation.relationship) + + if not len(refs) == 1: +- raise interfaces.CheckinException( "Baseline count mismatch" ) ++ raise interfaces.CheckinException("Baseline count mismatch") + + if not refs or refs[0] is None: +- raise interfaces.CheckinException( "Baseline has disappeared" ) ++ raise interfaces.CheckinException("Baseline has disappeared") + + baseline = refs[0] + return baseline ++ ++ def getBaseline(self): ++ if IReferenceable.providedBy(self.context): ++ refs = self.context.getReferences(WorkingCopyRelation.relationship) ++ if refs: ++ return refs[0] ++ ++ def getWorkingCopy(self): ++ if IReferenceable.providedBy(self.context): ++ refs = self.context.getBRefs(WorkingCopyRelation.relationship) ++ if refs: ++ return refs[0] ++ ++ def getProperties(self, obj, default=None): ++ return get_storage(obj, default=default) +\ No newline at end of file +diff --git a/plone/app/iterate/relation.py b/plone/app/iterate/relation.py +index 1cf2bb1..21b6fc6 100644 +--- a/plone/app/iterate/relation.py ++++ b/plone/app/iterate/relation.py +@@ -35,7 +35,7 @@ + from interfaces import IIterateAware + + +-class WorkingCopyRelation( Reference ): ++class WorkingCopyRelation(Reference): + """ + Source Object is Working Copy + +@@ -43,10 +43,10 @@ class WorkingCopyRelation( Reference ): + """ + relationship = "Working Copy Relation" + +- implements( IWorkingCopyRelation, IAttributeAnnotatable ) ++ implements(IWorkingCopyRelation, IAttributeAnnotatable) + + +-class CheckinCheckoutReferenceAdapter ( object ): ++class CheckinCheckoutReferenceAdapter(object): + """ + default adapter for references. + +@@ -65,47 +65,46 @@ class CheckinCheckoutReferenceAdapter ( object ): + + """ + +- implements( ICheckinCheckoutReference ) +- adapts( IIterateAware ) ++ implements(ICheckinCheckoutReference) ++ adapts(IIterateAware) + + storage_key = "coci.references" + +- def __init__(self, context ): ++ def __init__(self, context): + self.context = context + +- def checkout( self, baseline, wc, refs, storage ): ++ def checkout(self, baseline, wc, refs, storage): + for ref in refs: +- wc.addReference( ref.targetUID, ref.relationship, referenceClass=ref.__class__ ) ++ wc.addReference(ref.targetUID, ref.relationship, referenceClass=ref.__class__) + +- def checkin( self, *args ): ++ def checkin(self, *args): + pass + + checkoutBackReferences = checkinBackReferences = checkin + + +- +-class NoCopyReferenceAdapter( object ): ++class NoCopyReferenceAdapter(object): + """ + an adapter for references that does not copy them to the wc on checkout. + + additionally custom reference state is kept when the wc is checked in. + """ + +- implements( ICheckinCheckoutReference ) ++ implements(ICheckinCheckoutReference) + + def __init__(self, context): + self.context = context + +- def checkin( self, baseline, wc, refs, storage ): ++ def checkin(self, baseline, wc, refs, storage): + # move the references from the baseline to the wc + + # one note, on checkin the wc uid is not yet changed to match that of the baseline + ref_ids = [r.getId() for r in refs] + +- baseline_ref_container = getattr( baseline, atconf.REFERENCE_ANNOTATION ) +- clipboard = baseline_ref_container.manage_cutObjects( ref_ids ) ++ baseline_ref_container = getattr(baseline, atconf.REFERENCE_ANNOTATION) ++ clipboard = baseline_ref_container.manage_cutObjects(ref_ids) + +- wc_ref_container = getattr( wc, atconf.REFERENCE_ANNOTATION ) ++ wc_ref_container = getattr(wc, atconf.REFERENCE_ANNOTATION) + + # references aren't globally addable w/ associated perm which default copysupport + # wants to check, temporarily monkey around the issue. +diff --git a/plone/app/iterate/testing.py b/plone/app/iterate/testing.py +index c14b947..7668d38 100644 +--- a/plone/app/iterate/testing.py ++++ b/plone/app/iterate/testing.py +@@ -1,4 +1,5 @@ + # -*- coding: utf-8 -*- ++from plone.app.contenttypes.testing import PloneAppContenttypes + from plone.app.testing import PLONE_FIXTURE + from plone.app.testing import PloneSandboxLayer + from plone.app.testing import applyProfile +@@ -107,3 +108,25 @@ def setUpPloneSite(self, portal): + PLONEAPPITERATE_FUNCTIONAL_TESTING = FunctionalTesting( + bases=(PLONEAPPITERATE_FIXTURE,), + name="PloneAppIterateLayer:Functional") ++ ++ ++class DexPloneAppIterateLayer(PloneAppContenttypes): ++ def setUpZope(self, app, configurationContext): ++ super(DexPloneAppIterateLayer, self).setUpZope(app, configurationContext) ++ import plone.app.iterate ++ self.loadZCML(package=plone.app.iterate) ++ z2.installProduct(app, 'plone.app.iterate') ++ ++ def setUpPloneSite(self, portal): ++ super(DexPloneAppIterateLayer, self).setUpPloneSite(portal) ++ applyProfile(portal, 'plone.app.iterate:plone.app.iterate') ++ ++ ++PLONEAPPITERATEDEX_FIXTURE = DexPloneAppIterateLayer() ++PLONEAPPITERATEDEX_INTEGRATION_TESTING = IntegrationTesting( ++ bases=(PLONEAPPITERATEDEX_FIXTURE,), ++ name="DexPloneAppIterateLayer:Integration") ++ ++PLONEAPPITERATEDEX_FUNCTIONAL_TESTING = FunctionalTesting( ++ bases=(PLONEAPPITERATEDEX_FIXTURE,), ++ name="DexPloneAppIterateLayer:Functional") +diff --git a/plone/app/iterate/tests/dexterity.rst b/plone/app/iterate/tests/dexterity.rst +new file mode 100644 +index 0000000..6ba2186 +--- /dev/null ++++ b/plone/app/iterate/tests/dexterity.rst +@@ -0,0 +1,57 @@ ++Staging behavior regression tests ++================================= ++ ++Tests for bugs that would distract from usage examples in stagingbehavior.txt ++ ++If we access the site as an admin TTW:: ++ ++ >>> from plone.testing.z2 import Browser ++ >>> browser = Browser(layer["app"]) ++ >>> browser.handleErrors = False ++ >>> portal = layer["portal"] ++ >>> portal_url = "http://nohost/plone" ++ >>> from plone.app.testing.interfaces import SITE_OWNER_NAME, SITE_OWNER_PASSWORD ++ >>> browser.addHeader("Authorization", "Basic %s:%s" % (SITE_OWNER_NAME, SITE_OWNER_PASSWORD)) ++ ++KeyError with aquisition wrapper ++========================================= ++ ++When an item provides IBaseline (has been checked in at least once) and it is accessed through an ++Aquisition wrapper you get a KeyError from zope.intid, originating from five.intid. ++ ++ >>> browser.open(portal_url + "/folder_factories") ++ >>> browser.getControl("Folder").click() ++ >>> browser.getControl("Add").click() ++ >>> browser.getControl(name="form.widgets.IDublinCore.title").value = "My Folder" ++ >>> browser.getControl(name="form.buttons.save").click() ++ >>> browser.url ++ 'http://nohost/plone/my-folder/view' ++ ++ >>> browser.open("http://nohost/plone/my-folder/folder_factories") ++ >>> browser.getControl("Page").click() ++ >>> browser.getControl("Add").click() ++ >>> browser.getControl(name="form.widgets.IDublinCore.title").value = "My Sub-object" ++ >>> browser.getControl(name="form.buttons.save").click() ++ >>> browser.url ++ 'http://nohost/plone/my-folder/my-sub-object/view' ++ ++Checkout ++ ++ >>> browser.getLink("Check out").click() ++ >>> browser.contents ++ '...This is a working copy of...My Sub-object..., made by...admin... on...' ++ ++Checkin ++ ++ >>> browser.getLink("Check in").click() ++ >>> browser.contents ++ '...Check in...' ++ >>> browser.getControl(name="form.button.Checkin").click() ++ >>> browser.url ++ 'http://nohost/plone/my-folder/my-sub-object' ++ ++Test can view through Aquisition wrapper (repeating test_folder is deliberate here) ++ ++ >>> browser.open("http://nohost/plone/my-folder/my-sub-object") ++ >>> browser.contents ++ '...My Sub-object...' +diff --git a/plone/app/iterate/tests/test_annotations.py b/plone/app/iterate/tests/test_annotations.py +new file mode 100644 +index 0000000..cab1f79 +--- /dev/null ++++ b/plone/app/iterate/tests/test_annotations.py +@@ -0,0 +1,75 @@ ++# -*- coding: utf-8 -*- ++ ++import unittest ++ ++from plone.app.iterate.interfaces import ICheckinCheckoutPolicy ++from plone.app.iterate.interfaces import IWCContainerLocator ++from plone.app.iterate.testing import PLONEAPPITERATEDEX_INTEGRATION_TESTING ++from plone.app.testing import TEST_USER_ID ++from plone.app.testing import setRoles ++from zope.annotation.interfaces import IAnnotatable ++from zope.annotation.interfaces import IAnnotations ++from zope.component import getAdapters ++ ++ ++class AnnotationsTestCase(unittest.TestCase): ++ ++ layer = PLONEAPPITERATEDEX_INTEGRATION_TESTING ++ ++ def setUp(self): ++ self.portal = self.layer['portal'] ++ setRoles(self.portal, TEST_USER_ID, ['Manager']) ++ self.portal.invokeFactory('Document', 's1') ++ self.s1 = self.portal['s1'] ++ ++ def test_object_annotatable(self): ++ self.assertTrue(IAnnotatable.providedBy(self.s1)) ++ ++ def test_annotation_saved_on_checkin(self): ++ # First we get and save a custom annotation to the existing object ++ obj_annotations = IAnnotations(self.s1) ++ self.assertEqual(obj_annotations, {}) ++ ++ obj_annotations['key1'] = u'value1' ++ obj_annotations = IAnnotations(self.s1) ++ self.assertEqual(obj_annotations, {'key1': u'value1'}) ++ ++ # Now, let's get a working copy for it. ++ locators = getAdapters((self.s1,), IWCContainerLocator) ++ location = u'plone.app.iterate.parent' ++ locator = [c[1] for c in locators if c[0] == location][0] ++ ++ policy = ICheckinCheckoutPolicy(self.s1) ++ ++ wc = policy.checkout(locator()) ++ ++ # Annotations should be the same ++ new_annotations = IAnnotations(wc) ++ self.assertEqual(new_annotations['key1'], u'value1') ++ ++ # Now, let's modify the existing one, and create a new one ++ new_annotations['key1'] = u'value2' ++ new_annotations['key2'] = u'value1' ++ ++ # Check that annotations were stored correctly and original ones were ++ # not overriten ++ new_annotations = IAnnotations(wc) ++ self.assertEqual(new_annotations['key1'], u'value2') ++ self.assertEqual(new_annotations['key2'], u'value1') ++ ++ obj_annotations = IAnnotations(self.s1) ++ self.assertEqual(obj_annotations['key1'], u'value1') ++ self.assertFalse('key2' in obj_annotations) ++ ++ # Now, we do a checkin ++ policy = ICheckinCheckoutPolicy(wc) ++ policy.checkin(u'Commit message') ++ ++ # And finally check that the old object has the same annotations as ++ # its working copy ++ ++ obj_annotations = IAnnotations(self.s1) ++ self.assertTrue('key1' in obj_annotations) ++ self.assertTrue('key2' in obj_annotations) ++ self.assertEqual(obj_annotations['key1'], u'value2') ++ self.assertEqual(obj_annotations['key2'], u'value1') +diff --git a/plone/app/iterate/tests/test_doctests.py b/plone/app/iterate/tests/test_doctests.py +index 2e2a393..ce053be 100644 +--- a/plone/app/iterate/tests/test_doctests.py ++++ b/plone/app/iterate/tests/test_doctests.py +@@ -3,6 +3,7 @@ + from unittest import TestSuite + + from plone.app.iterate.testing import PLONEAPPITERATE_FUNCTIONAL_TESTING ++from plone.app.iterate.testing import PLONEAPPITERATEDEX_FUNCTIONAL_TESTING + from plone.testing import layered + + +@@ -17,4 +18,12 @@ def test_suite(): + ), + layer=PLONEAPPITERATE_FUNCTIONAL_TESTING) + ) ++ suite.addTest(layered( ++ doctest.DocFileSuite( ++ 'dexterity.rst', ++ optionflags=OPTIONFLAGS, ++ package="plone.app.iterate.tests", ++ ), ++ layer=PLONEAPPITERATEDEX_FUNCTIONAL_TESTING) ++ ) + return suite +diff --git a/plone/app/iterate/tests/test_interfaces.py b/plone/app/iterate/tests/test_interfaces.py +new file mode 100644 +index 0000000..e180081 +--- /dev/null ++++ b/plone/app/iterate/tests/test_interfaces.py +@@ -0,0 +1,94 @@ ++from plone.app.iterate.interfaces import IBaseline ++from plone.app.iterate.interfaces import ICheckinCheckoutPolicy ++from plone.app.iterate.interfaces import IIterateAware ++from plone.app.iterate.interfaces import IWorkingCopy ++from plone.app.iterate.testing import PLONEAPPITERATEDEX_INTEGRATION_TESTING ++from plone.app.testing import TEST_USER_ID ++from plone.app.testing import TEST_USER_NAME ++from plone.app.testing import login ++from plone.app.testing import logout ++from plone.app.testing import setRoles ++from plone.dexterity.utils import createContentInContainer ++from unittest2 import TestCase ++ ++ ++class TestObjectsProvideCorrectInterfaces(TestCase): ++ """Since p.a.iterate replaces the baseline on checkin with the working copy ++ but p.a.stagingbehavior just copies the values, the provided interfaces ++ may be wrong after checkin. ++ ++ For making sure that provided interfaces are correct in every state we ++ test it here. ++ ++ See: https://dev.plone.org/ticket/13163 ++ """ ++ ++ layer = PLONEAPPITERATEDEX_INTEGRATION_TESTING ++ ++ def setUp(self): ++ super(TestObjectsProvideCorrectInterfaces, self).setUp() ++ ++ self.portal = self.layer['portal'] ++ setRoles(self.portal, TEST_USER_ID, ['Manager']) ++ login(self.portal, TEST_USER_NAME) ++ ++ # create a folder where everything of this test suite should happen ++ self.assertNotIn('test-folder', self.portal.objectIds()) ++ self.folder = self.portal.get( ++ self.portal.invokeFactory('Folder', 'test-folder')) ++ ++ self.obj = createContentInContainer(self.folder, 'Document') ++ ++ def tearDown(self): ++ self.portal.manage_delObjects([self.folder.id]) ++ logout() ++ setRoles(self.portal, TEST_USER_ID, ['Member']) ++ super(TestObjectsProvideCorrectInterfaces, self).tearDown() ++ ++ def do_checkout(self): ++ policy = ICheckinCheckoutPolicy(self.obj) ++ working_copy = policy.checkout(self.folder) ++ return working_copy ++ ++ def do_cancel(self, working_copy): ++ policy = ICheckinCheckoutPolicy(working_copy) ++ policy.cancelCheckout() ++ ++ def do_checkin(self, working_copy): ++ policy = ICheckinCheckoutPolicy(working_copy) ++ policy.checkin('') ++ ++ def test_before_checkout(self): ++ self.assertTrue(self.obj) ++ self.assertTrue(IIterateAware.providedBy(self.obj)) ++ self.assertFalse(IBaseline.providedBy(self.obj)) ++ self.assertFalse(IWorkingCopy.providedBy(self.obj)) ++ ++ def test_after_checkout(self): ++ working_copy = self.do_checkout() ++ self.assertTrue(working_copy) ++ self.assertTrue(IIterateAware.providedBy(working_copy)) ++ self.assertFalse(IBaseline.providedBy(working_copy)) ++ self.assertTrue(IWorkingCopy.providedBy(working_copy)) ++ ++ self.assertTrue(IIterateAware.providedBy(self.obj)) ++ self.assertTrue(IBaseline.providedBy(self.obj)) ++ self.assertFalse(IWorkingCopy.providedBy(self.obj)) ++ ++ def test_after_cancel_checkout(self): ++ working_copy = self.do_checkout() ++ self.assertTrue(working_copy) ++ ++ self.do_cancel(working_copy) ++ self.assertTrue(IIterateAware.providedBy(self.obj)) ++ self.assertFalse(IBaseline.providedBy(self.obj)) ++ self.assertFalse(IWorkingCopy.providedBy(self.obj)) ++ ++ def test_after_checkin(self): ++ working_copy = self.do_checkout() ++ self.assertTrue(working_copy) ++ ++ self.do_checkin(working_copy) ++ self.assertTrue(IIterateAware.providedBy(self.obj)) ++ self.assertFalse(IBaseline.providedBy(self.obj)) ++ self.assertFalse(IWorkingCopy.providedBy(self.obj)) +diff --git a/plone/app/iterate/tests/test_iterate.py b/plone/app/iterate/tests/test_iterate.py +index b5cb43b..e30534e 100644 +--- a/plone/app/iterate/tests/test_iterate.py ++++ b/plone/app/iterate/tests/test_iterate.py +@@ -29,9 +29,7 @@ + + from plone.app.iterate.interfaces import ICheckinCheckoutPolicy + from plone.app.iterate.testing import PLONEAPPITERATE_INTEGRATION_TESTING +-from plone.app.iterate.testing import PLONEAPPITERATE_FUNCTIONAL_TESTING + +-from plone.app.testing import SITE_OWNER_NAME + from plone.app.testing import TEST_USER_ID + from plone.app.testing import TEST_USER_NAME + from plone.app.testing import login +diff --git a/plone/app/iterate/util.py b/plone/app/iterate/util.py +index 6a83207..f1a2908 100644 +--- a/plone/app/iterate/util.py ++++ b/plone/app/iterate/util.py +@@ -25,12 +25,15 @@ + from interfaces import annotation_key + from Products.CMFCore.utils import getToolByName +-def get_storage( context ): +- annotations = IAnnotations( context ) +- if not annotations.has_key( annotation_key ): +- annotations[ annotation_key ] = PersistentDict() ++def get_storage(context, default=None): ++ annotations = IAnnotations(context) ++ if annotation_key not in annotations: ++ if default is not None: ++ return default ++ annotations[annotation_key] = PersistentDict() + return annotations[annotation_key] - 1.6.7 (2015-06-05) -diff --git a/plonetheme/barceloneta/theme/rules.xml b/plonetheme/barceloneta/theme/rules.xml -index c484982..3a9af46 100644 ---- a/plonetheme/barceloneta/theme/rules.xml -+++ b/plonetheme/barceloneta/theme/rules.xml -@@ -5,7 +5,7 @@ - xmlns:xsl="http://www.w3.org/1999/XSL/Transform" - xmlns:xi="http://www.w3.org/2001/XInclude"> ++ + def upgrade_by_reinstall(context): + qi = getToolByName(context, 'portal_quickinstaller') + qi.reinstallProducts(['plone.app.iterate']) +diff --git a/setup.py b/setup.py +index d82dd1d..12dc11c 100644 +--- a/setup.py ++++ b/setup.py +@@ -1,13 +1,11 @@ + from setuptools import setup, find_packages -- -+ - +-version = '3.0.2.dev0' ++version = '3.1.0.dev0' - + setup(name='plone.app.iterate', + version=version, + description="check-out/check-in staging for Plone", +- long_description=\ +- open("README.rst").read() + "\n" + \ +- open("CHANGES.rst").read(), ++ long_description=open("README.rst").read() + "\n" + open("CHANGES.rst").read(), + classifiers=[ + "Environment :: Web Environment", + "Framework :: Plone", +@@ -17,20 +15,21 @@ + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 2.7", +- ], ++ ], + keywords='', + author='Plone Foundation', + author_email='plone-developers@lists.sourceforge.net', + url='http://pypi.python.org/pypi/plone.app.iterate', + license='GPL version 2', + packages=find_packages(exclude=['ez_setup']), +- namespace_packages = ['plone', 'plone.app'], ++ namespace_packages=['plone', 'plone.app'], + include_package_data=True, + zip_safe=False, + extras_require=dict( +- test=[ +- 'plone.app.testing', +- ] ++ test=[ ++ 'plone.app.testing', ++ 'plone.app.contenttypes' ++ ] + ), + install_requires=[ + 'setuptools', +@@ -55,7 +54,7 @@ + 'ZODB3', + 'Zope2', + ], +- entry_points = ''' ++ entry_points=''' + [z3c.autoinclude.plugin] + target = plone + ''',