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] + 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 ''',