diff --git a/last_commit.txt b/last_commit.txt index dd5880d97f..c36615f501 100644 --- a/last_commit.txt +++ b/last_commit.txt @@ -1,1908 +1,231 @@ -Repository: plone.app.content +Repository: plone.app.collection Branch: refs/heads/master -Date: 2016-04-08T10:48:17+02:00 -Author: Johannes Raggam (thet) -Commit: https://github.com/plone/plone.app.content/commit/2af6fafa9130e6afd466a421d2c86ab4e48dfcce +Date: 2016-04-09T01:24:13+02:00 +Author: Maurits van Rees (mauritsvanrees) +Commit: https://github.com/plone/plone.app.collection/commit/2b7f142590f04c36818e174a544cfe8387f684fb -More INavigationRoot fixes via get_top_site_from_url -Folder contents: Acquire the top most visible portal object to operate on. -Fixes some issues in INavigationRoot or ISite based subsites and virtual hosting environments pointing to subsites. -Fixes include: show correct breadcrumb paths, paste to correct location. +Added uninstall profile. + +The Collection type is removed when you uninstall this package. Files changed: +A plone/app/collection/profiles/uninstall/types.xml M CHANGES.rst -M plone/app/content/browser/contents/__init__.py -M plone/app/content/browser/contents/paste.py -M plone/app/content/browser/contents/properties.py -M plone/app/content/browser/contents/tags.py +M plone/app/collection/configure.zcml +M plone/app/collection/profiles/default/types.xml diff --git a/CHANGES.rst b/CHANGES.rst -index 88cf599..62115af 100644 +index 80c9317..0155996 100644 --- a/CHANGES.rst +++ b/CHANGES.rst -@@ -10,16 +10,20 @@ Incompatibilities: +@@ -10,7 +10,8 @@ Incompatibilities: New: --- Add ``Creator``, ``Description``, ``end``, ``start`` and ``location`` to the available columns and context attributes for folder_contents. -+- Show attributes from ``_unsafe_metadata`` if user has "Modify Portal Content" permissions. - [thet] - -- Show attributes from ``_unsafe_metadata`` if user has "Modify Portal Content" permissions. -+- Add ``Creator``, ``Description``, ``end``, ``start`` and ``location`` to the available columns and context attributes for folder_contents. - [thet] - - Fixes: - --- Remove ``portal_type`` from available columns and use ``Type`` instead, which is meant to be read by humans. -- ``portal_type`` is now available on the attributes object. -+- Folder contents: Acquire the top most visible portal object to operate on. -+ Fixes some issues in INavigationRoot or ISite based subsites and virtual hosting environments pointing to subsites. -+ Fixes include: show correct breadcrumb paths, paste to correct location. -+ [thet] -+ -+ Fixes folder contents path and breadcrumbs settings to show correct paths and render the toolbar correctly in navigation root subsites and virtual hosting environments pointing to subsites. - [thet] - - - Added most notably `portal_type`, `review_state` and `Subject` but also `exclude_from_nav`, `is_folderish`, `last_comment_date`, `meta_type` and `total_comments` to ``BaseVocabularyView`` ``translate_ignored`` list. -@@ -27,14 +31,15 @@ Fixes: - Fixes https://github.com/plone/plone.app.content/issues/77 - [thet] - -+- Remove ``portal_type`` from available columns and use ``Type`` instead, which is meant to be read by humans. -+ ``portal_type`` is now available on the attributes object. -+ [thet] -+ - - Vocabulary permissions are considered View permission by default, if not - stated different in PERMISSION global. Renamed _permissions to PERMISSIONS, - Deprecated BBB name in place. Also minor code-style changes - [jensens, thet] - --- Fix folder contents path and breadcrumbs settings to show correct paths and render the toolbar correctly in navigation root subsites and virtual hosting environments pointing to subsites. -- [thet] -- - - Fix test isolation problem and remove an unnecessary test dependency on ``plone.app.widgets``. - [thet] - -diff --git a/plone/app/content/browser/contents/__init__.py b/plone/app/content/browser/contents/__init__.py -index 72aa372..9594054 100644 ---- a/plone/app/content/browser/contents/__init__.py -+++ b/plone/app/content/browser/contents/__init__.py -@@ -68,6 +68,10 @@ class ContentsBaseAction(BrowserView): - failure_msg = _('Failure') - required_obj_permission = None - -+ @property -+ def site(self): -+ return get_top_site_from_url(self.context, self.request) -+ - def objectTitle(self, obj): - context = aq_inner(obj) - title = utils.pretty_title_or_id(context, context) -@@ -100,11 +104,10 @@ def finish(self): - def __call__(self): - self.protect() - self.errors = [] -- site = getSite() - context = aq_inner(self.context) - selection = self.get_selection() - -- self.dest = site.restrictedTraverse( -+ self.dest = self.site.restrictedTraverse( - str(self.request.form['folder'].lstrip('/'))) - - self.catalog = getToolByName(context, 'portal_catalog') -diff --git a/plone/app/content/browser/contents/paste.py b/plone/app/content/browser/contents/paste.py -index 0f5206d..c6952f4 100644 ---- a/plone/app/content/browser/contents/paste.py -+++ b/plone/app/content/browser/contents/paste.py -@@ -4,7 +4,6 @@ - from plone.app.content.interfaces import IStructureAction - from Products.CMFPlone import PloneMessageFactory as _ - from ZODB.POSException import ConflictError --from zope.component.hooks import getSite - from zope.i18n import translate - from zope.interface import implementer - -@@ -35,9 +34,8 @@ class PasteActionView(ContentsBaseAction): - def __call__(self): - self.protect() - self.errors = [] -- site = getSite() - -- self.dest = site.restrictedTraverse( -+ self.dest = self.site.restrictedTraverse( - str(self.request.form['folder'].lstrip('/'))) - - try: -diff --git a/plone/app/content/browser/contents/properties.py b/plone/app/content/browser/contents/properties.py -index 2ccf800..ce5d9a3 100644 ---- a/plone/app/content/browser/contents/properties.py -+++ b/plone/app/content/browser/contents/properties.py -@@ -21,9 +21,7 @@ def __init__(self, context, request): - self.request = request - - def get_options(self): -- site = getSite() -- base_url = site.absolute_url() -- base_vocabulary = '%s/@@getVocabulary?name=' % base_url -+ base_vocabulary = '%s/@@getVocabulary?name=' % getSite().absolute_url() - return { - 'title': translate(_('Properties'), context=self.request), - 'id': 'properties', -@@ -34,7 +32,7 @@ def get_options(self): - 'template': self.template( - vocabulary_url='%splone.app.vocabularies.Users' % ( - base_vocabulary) -- ) -+ ) - } - } - -diff --git a/plone/app/content/browser/contents/tags.py b/plone/app/content/browser/contents/tags.py -index d582f13..e3acb67 100644 ---- a/plone/app/content/browser/contents/tags.py -+++ b/plone/app/content/browser/contents/tags.py -@@ -19,9 +19,7 @@ def __init__(self, context, request): - self.request = request - - def get_options(self): -- site = getSite() -- base_url = site.absolute_url() -- base_vocabulary = '%s/@@getVocabulary?name=' % base_url -+ base_vocabulary = '%s/@@getVocabulary?name=' % getSite().absolute_url() - return { - 'title': translate(_('Tags'), context=self.request), - 'id': 'tags', -@@ -31,7 +29,7 @@ def get_options(self): - 'template': self.template( - vocabulary_url='%splone.app.vocabularies.Keywords' % ( - base_vocabulary) -- ) -+ ) - } - } - - - -Repository: plone.app.content - - -Branch: refs/heads/master -Date: 2016-04-08T11:36:15+02:00 -Author: Johannes Raggam (thet) -Commit: https://github.com/plone/plone.app.content/commit/5c094eab7ac420cd0502f8307f49edfdcd20871c - -FC: handle "disallowed subobject type" -Folder contents: When pasting, handle "Disallowed subobject type" ValueError and present a helpful error message. -Fixes: plone/mockup#657 - -Files changed: -M CHANGES.rst -M plone/app/content/browser/contents/paste.py - -diff --git a/CHANGES.rst b/CHANGES.rst -index 62115af..7de58ae 100644 ---- a/CHANGES.rst -+++ b/CHANGES.rst -@@ -18,9 +18,14 @@ New: +-- *add item here* ++- Added uninstall profile. The Collection type is removed when you ++ uninstall this package. [maurits] Fixes: -+- Folder contents: When pasting, handle "Disallowed subobject type" ValueError and present a helpful error message. -+ Fixes: plone/mockup#657 -+ [thet] -+ - - Folder contents: Acquire the top most visible portal object to operate on. - Fixes some issues in INavigationRoot or ISite based subsites and virtual hosting environments pointing to subsites. - Fixes include: show correct breadcrumb paths, paste to correct location. -+ Fixes: #86 - [thet] - - Fixes folder contents path and breadcrumbs settings to show correct paths and render the toolbar correctly in navigation root subsites and virtual hosting environments pointing to subsites. -diff --git a/plone/app/content/browser/contents/paste.py b/plone/app/content/browser/contents/paste.py -index c6952f4..09dae98 100644 ---- a/plone/app/content/browser/contents/paste.py -+++ b/plone/app/content/browser/contents/paste.py -@@ -49,5 +49,13 @@ def __call__(self): - self.errors.append( - _(u'You are not authorized to paste ${title} here.', - mapping={u'title': self.objectTitle(self.dest)})) -+ except ValueError as e: -+ if 'Disallowed subobject type: ' in e.message: -+ msg_parts = e.message.split(':') -+ self.errors.append( -+ _(u'Disallowed subobject type: ${type}', -+ mapping={u'type': msg_parts[1].strip()})) -+ else: -+ raise e - - return self.message() - - -Repository: plone.app.content - - -Branch: refs/heads/master -Date: 2016-04-08T14:31:48+02:00 -Author: Johannes Raggam (thet) -Commit: https://github.com/plone/plone.app.content/commit/1b0f671e60e203643c2d0409077d0a0d5af2709e - -move doctest to rst and use syntax highlighting - -Files changed: -A plone/app/content/basecontent.rst -M plone/app/content/tests/test_basecontent.py -D plone/app/content/basecontent.txt - -diff --git a/plone/app/content/basecontent.rst b/plone/app/content/basecontent.rst +diff --git a/plone/app/collection/configure.zcml b/plone/app/collection/configure.zcml +index d2bda5d..7f8e3b6 100644 +--- a/plone/app/collection/configure.zcml ++++ b/plone/app/collection/configure.zcml +@@ -9,7 +9,7 @@ + + + +- ++ + + ++ ++ + + + +- + +diff --git a/plone/app/collection/profiles/uninstall/types.xml b/plone/app/collection/profiles/uninstall/types.xml new file mode 100644 -index 0000000..4007109 +index 0000000..a1ea1c6 --- /dev/null -+++ b/plone/app/content/basecontent.rst -@@ -0,0 +1,152 @@ -+============= -+Basic content -+============= -+ -+plone.app.content provides some helper base classes for content. Here are -+some simple examples of using them. -+.. code-block:: python -+ -+ >>> from plone.app.content import container, item -+ -+Let us define two fictional types, a folderish one and a non-folderish one. -+We should define factories for these types as well. Factories can be -+referenced from CMF FTI's, and also from directives. -+With an appropriate add view (e.g. using formlib's AddForm) this will be -+available from Plone's "add item" menu. Factories are registered as named -+utilities. -+ -+Note that we need to define a portal_type to keep CMF happy. -+.. code-block:: python -+ -+ >>> from zope.interface import implements, Interface -+ >>> from zope import schema -+ >>> from zope.component.factory import Factory -+ -+First, a container: -+.. code-block:: python -+ -+ >>> class IMyContainer(Interface): -+ ... title = schema.TextLine(title=u"My title") -+ ... description = schema.TextLine(title=u"My other title") -+ -+ >>> class MyContainer(container.Container): -+ ... implements(IMyContainer) -+ ... portal_type = "My container" -+ ... title = u"" -+ ... description = u"" -+ -+ >>> containerFactory = Factory(MyContainer) -+ -+Then, an item: -+.. code-block:: python -+ -+ >>> class IMyType(Interface): -+ ... title = schema.TextLine(title=u"My title") -+ ... description = schema.TextLine(title=u"My other title") -+ -+ >>> class MyType(item.Item): -+ ... implements(IMyType) -+ ... portal_type = "My type" -+ ... title = u"" -+ ... description = u"" -+ -+ >>> itemFactory = Factory(MyType) -+ -+We can now create the items. -+.. code-block:: python -+ -+ >>> container = containerFactory("my-container") -+ >>> container.id -+ 'my-container' -+ >>> container.title = "A sample container" -+ -+We need to add it to an object manager for acquisition to do its magic. -+.. code-block:: python -+ -+ >>> newid = self.portal._setObject(container.id, container) -+ >>> container = getattr(self.portal, newid) -+ -+We will add the item directly to the container later. -+.. code-block:: python -+ -+ >>> item = itemFactory("my-item") -+ >>> item.id -+ 'my-item' -+ >>> item.title = "A non-folderish item" -+ -+Note that both the container type and the item type are contentish. This is -+important, because CMF provides event handlers that automatically index -+objects that are IContentish. -+.. code-block:: python -+ -+ >>> from Products.CMFCore.interfaces import IContentish -+ >>> IContentish.providedBy(container) -+ True -+ >>> IContentish.providedBy(item) -+ True -+ -+Only the container is folderish, though: -+.. code-block:: python -+ -+ >>> from Products.CMFCore.interfaces import IFolderish -+ >>> bool(container.isPrincipiaFolderish) -+ True -+ >>> IFolderish.providedBy(container) -+ True -+ >>> bool(item.isPrincipiaFolderish) -+ False -+ >>> IFolderish.providedBy(item) -+ False -+ -+We can use the more natural Zope3-style container API, or the traditional -+ObjectManager one. -+.. code-block:: python -+ -+ >>> container['my-item'] = item -+ >>> 'my-item' in container -+ True -+ >>> 'my-item' in container.objectIds() -+ True -+ >>> del container['my-item'] -+ >>> 'my-item' in container -+ False -+ >>> container._setObject('my-item', item) -+ 'my-item' -+ >>> 'my-item' in container -+ True -+ -+Both pieces of content should have been cataloged. -+.. code-block:: python -+ -+ >>> container = self.portal['my-container'] -+ >>> item = container['my-item'] -+ -+ >>> from Products.CMFCore.utils import getToolByName -+ >>> catalog = getToolByName(self.portal, 'portal_catalog') -+ >>> [b.Title for b in catalog(getId = 'my-container')] -+ ['A sample container'] -+ >>> [b.Title for b in catalog(getId = 'my-item')] -+ ['A non-folderish item'] -+ -+If we modify an object and trigger a modified event, it should be updated. -+.. code-block:: python -+ -+ >>> from zope.lifecycleevent import ObjectModifiedEvent -+ >>> from zope.event import notify -+ -+ >>> container.title = "Updated title" -+ >>> item.title = "Also updated title" -+ -+ >>> [b.Title for b in catalog(getId = 'my-container')] -+ ['A sample container'] -+ >>> [b.Title for b in catalog(getId = 'my-item')] -+ ['A non-folderish item'] -+ -+ -+ >>> notify(ObjectModifiedEvent(container)) -+ >>> notify(ObjectModifiedEvent(item)) -+ -+ >>> [b.Title for b in catalog(getId = 'my-container')] -+ ['Updated title'] -+ >>> [b.Title for b in catalog(getId = 'my-item')] -+ ['Also updated title'] -diff --git a/plone/app/content/basecontent.txt b/plone/app/content/basecontent.txt -deleted file mode 100644 -index 03f618c..0000000 ---- a/plone/app/content/basecontent.txt -+++ /dev/null -@@ -1,140 +0,0 @@ --============= --Basic content --============= -- --plone.app.content provides some helper base classes for content. Here are --some simple examples of using them. -- -- >>> from plone.app.content import container, item -- --Let us define two fictional types, a folderish one and a non-folderish one. --We should define factories for these types as well. Factories can be --referenced from CMF FTI's, and also from directives. --With an appropriate add view (e.g. using formlib's AddForm) this will be --available from Plone's "add item" menu. Factories are registered as named --utilities. -- --Note that we need to define a portal_type to keep CMF happy. -- -- >>> from zope.interface import implements, Interface -- >>> from zope import schema -- >>> from zope.component.factory import Factory -- --First, a container: -- -- >>> class IMyContainer(Interface): -- ... title = schema.TextLine(title=u"My title") -- ... description = schema.TextLine(title=u"My other title") -- -- >>> class MyContainer(container.Container): -- ... implements(IMyContainer) -- ... portal_type = "My container" -- ... title = u"" -- ... description = u"" -- -- >>> containerFactory = Factory(MyContainer) -- --Then, an item: -- -- >>> class IMyType(Interface): -- ... title = schema.TextLine(title=u"My title") -- ... description = schema.TextLine(title=u"My other title") -- -- >>> class MyType(item.Item): -- ... implements(IMyType) -- ... portal_type = "My type" -- ... title = u"" -- ... description = u"" -- -- >>> itemFactory = Factory(MyType) -- --We can now create the items. -- -- >>> container = containerFactory("my-container") -- >>> container.id -- 'my-container' -- >>> container.title = "A sample container" -- --We need to add it to an object manager for acquisition to do its magic. -- -- >>> newid = self.portal._setObject(container.id, container) -- >>> container = getattr(self.portal, newid) -- --We will add the item directly to the container later. -- -- >>> item = itemFactory("my-item") -- >>> item.id -- 'my-item' -- >>> item.title = "A non-folderish item" -- --Note that both the container type and the item type are contentish. This is --important, because CMF provides event handlers that automatically index --objects that are IContentish. -- -- >>> from Products.CMFCore.interfaces import IContentish -- >>> IContentish.providedBy(container) -- True -- >>> IContentish.providedBy(item) -- True -- --Only the container is folderish, though: -- -- >>> from Products.CMFCore.interfaces import IFolderish -- >>> bool(container.isPrincipiaFolderish) -- True -- >>> IFolderish.providedBy(container) -- True -- >>> bool(item.isPrincipiaFolderish) -- False -- >>> IFolderish.providedBy(item) -- False -- --We can use the more natural Zope3-style container API, or the traditional --ObjectManager one. -- -- >>> container['my-item'] = item -- >>> 'my-item' in container -- True -- >>> 'my-item' in container.objectIds() -- True -- >>> del container['my-item'] -- >>> 'my-item' in container -- False -- >>> container._setObject('my-item', item) -- 'my-item' -- >>> 'my-item' in container -- True -- --Both pieces of content should have been cataloged. -- -- >>> container = self.portal['my-container'] -- >>> item = container['my-item'] -- -- >>> from Products.CMFCore.utils import getToolByName -- >>> catalog = getToolByName(self.portal, 'portal_catalog') -- >>> [b.Title for b in catalog(getId = 'my-container')] -- ['A sample container'] -- >>> [b.Title for b in catalog(getId = 'my-item')] -- ['A non-folderish item'] -- --If we modify an object and trigger a modified event, it should be updated. -- -- >>> from zope.lifecycleevent import ObjectModifiedEvent -- >>> from zope.event import notify -- -- >>> container.title = "Updated title" -- >>> item.title = "Also updated title" -- -- >>> [b.Title for b in catalog(getId = 'my-container')] -- ['A sample container'] -- >>> [b.Title for b in catalog(getId = 'my-item')] -- ['A non-folderish item'] -- -- -- >>> notify(ObjectModifiedEvent(container)) -- >>> notify(ObjectModifiedEvent(item)) -- -- >>> [b.Title for b in catalog(getId = 'my-container')] -- ['Updated title'] -- >>> [b.Title for b in catalog(getId = 'my-item')] -- ['Also updated title'] -diff --git a/plone/app/content/tests/test_basecontent.py b/plone/app/content/tests/test_basecontent.py -index c8ac5ab..5b9a0ea 100644 ---- a/plone/app/content/tests/test_basecontent.py -+++ b/plone/app/content/tests/test_basecontent.py -@@ -8,7 +8,7 @@ - def test_suite(): - return unittest.TestSuite(( - ztc.ZopeDocFileSuite( -- 'basecontent.txt', -+ 'basecontent.rst', - package='plone.app.content', - test_class=ContentFunctionalTestCase, - optionflags=(doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE)), ++++ b/plone/app/collection/profiles/uninstall/types.xml +@@ -0,0 +1,4 @@ ++ ++ ++ ++ -Repository: plone.app.content +Repository: plone.app.collection Branch: refs/heads/master -Date: 2016-04-10T00:37:05+02:00 -Author: Johannes Raggam (thet) -Commit: https://github.com/plone/plone.app.content/commit/6c23c61890a5473588418ccd3331b5c979f2b36a +Date: 2016-04-09T01:26:15+02:00 +Author: Maurits van Rees (mauritsvanrees) +Commit: https://github.com/plone/plone.app.collection/commit/9a285b009abbbb8ce6771f76a2b715af4448959c -add unittest for get_top_site_from_url +Bump version for feature. Files changed: -A plone/app/content/tests/test_contents.py M CHANGES.rst -M plone/app/content/browser/contents/__init__.py +M setup.py diff --git a/CHANGES.rst b/CHANGES.rst -index 7de58ae..47c1b77 100644 +index 0155996..19f7fe3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst -@@ -28,9 +28,6 @@ Fixes: - Fixes: #86 - [thet] +@@ -1,7 +1,7 @@ + Changelog + ========= -- Fixes folder contents path and breadcrumbs settings to show correct paths and render the toolbar correctly in navigation root subsites and virtual hosting environments pointing to subsites. -- [thet] -- - - Added most notably `portal_type`, `review_state` and `Subject` but also `exclude_from_nav`, `is_folderish`, `last_comment_date`, `meta_type` and `total_comments` to ``BaseVocabularyView`` ``translate_ignored`` list. - Some of them are necessary for frontend logic and others cannot be translated. - Fixes https://github.com/plone/plone.app.content/issues/77 -diff --git a/plone/app/content/browser/contents/__init__.py b/plone/app/content/browser/contents/__init__.py -index 9594054..cb74782 100644 ---- a/plone/app/content/browser/contents/__init__.py -+++ b/plone/app/content/browser/contents/__init__.py -@@ -39,7 +39,14 @@ - - - def get_top_site_from_url(context, request): -- """Find the top-most site, which is in the url path. -+ """Find the top-most site, which is still in the url path. -+ -+ If the current context is within a subsite within a PloneSiteRoot and no -+ virtual hosting is in place, the PloneSiteRoot is returned. -+ When at the same context but in a virtual hosting environment with the -+ virtual host root pointing to the subsites, it returns the subsite instead -+ of the PloneSiteRoot. -+ - For this given content structure: - - /Plone/Subsite -diff --git a/plone/app/content/tests/test_contents.py b/plone/app/content/tests/test_contents.py -new file mode 100644 -index 0000000..1bf594d ---- /dev/null -+++ b/plone/app/content/tests/test_contents.py -@@ -0,0 +1,78 @@ -+# -*- coding: utf-8 -*- -+import unittest -+ -+ -+class ContentsUnitTests(unittest.TestCase): -+ -+ def test_get_top_site_from_url(self): -+ """Unit test for ``get_top_site_from_url`` with context and request -+ mocks. -+ -+ Test content structure: -+ /approot/PloneSite/folder/SubSite/folder -+ PloneSite and SubSite implement ISite -+ """ -+ from plone.app.content.browser.contents import get_top_site_from_url -+ from zope.component.interfaces import ISite -+ from zope.interface import alsoProvides -+ from urlparse import urlparse -+ -+ class MockContext(object): -+ vh_url = 'http://nohost' -+ vh_root = '' -+ -+ def __init__(self, physical_path): -+ self.physical_path = physical_path -+ if self.physical_path.split('/')[-1] in ('PloneSite', 'SubSite'): # noqa -+ alsoProvides(self, ISite) -+ -+ @property -+ def id(self): -+ return self.physical_path.split('/')[-1] -+ -+ def absolute_url(self): -+ return self.vh_url + self.physical_path[len(self.vh_root):] or '/' # noqa -+ -+ def restrictedTraverse(self, path): -+ return MockContext(self.vh_root + path) -+ -+ class MockRequest(object): -+ vh_url = 'http://nohost' -+ vh_root = '' -+ -+ def physicalPathFromURL(self, url): -+ # Return the physical path from a URL. -+ # The outer right '/' is not part of the path. -+ path = self.vh_root + urlparse(url).path.rstrip('/') -+ return path.split('/') -+ -+ # NO VIRTUAL HOSTING -+ -+ req = MockRequest() -+ -+ # Case 1: -+ ctx = MockContext('/approot/PloneSite') -+ self.assertEqual(get_top_site_from_url(ctx, req).id, 'PloneSite') -+ -+ # Case 2 -+ ctx = MockContext('/approot/PloneSite/folder') -+ self.assertEqual(get_top_site_from_url(ctx, req).id, 'PloneSite') -+ -+ # Case 3: -+ ctx = MockContext('/approot/PloneSite/folder/SubSite/folder') -+ self.assertEqual(get_top_site_from_url(ctx, req).id, 'PloneSite') -+ -+ # VIRTUAL HOSTING ON SUBSITE -+ -+ req = MockRequest() -+ req.vh_root = '/approot/PloneSite/folder/SubSite' -+ -+ # Case 4: -+ ctx = MockContext('/approot/PloneSite/folder/SubSite') -+ ctx.vh_root = '/approot/PloneSite/folder/SubSite' -+ self.assertEqual(get_top_site_from_url(ctx, req).id, 'SubSite') -+ -+ # Case 5: -+ ctx = MockContext('/approot/PloneSite/folder/SubSite/folder') -+ ctx.vh_root = '/approot/PloneSite/folder/SubSite' -+ self.assertEqual(get_top_site_from_url(ctx, req).id, 'SubSite') - - -Repository: plone.app.content - - -Branch: refs/heads/master -Date: 2016-04-10T02:10:07+02:00 -Author: Johannes Raggam (thet) -Commit: https://github.com/plone/plone.app.content/commit/dd11bf0dbf49632bcb56265dd33c26e04c110a6e - -add tests for _unsafe_metadata - -Files changed: -M plone/app/content/browser/vocabulary.py -M plone/app/content/tests/test_widgets.py - -diff --git a/plone/app/content/browser/vocabulary.py b/plone/app/content/browser/vocabulary.py -index 37b4104..fdf32fd 100644 ---- a/plone/app/content/browser/vocabulary.py -+++ b/plone/app/content/browser/vocabulary.py -@@ -31,6 +31,7 @@ - MAX_BATCH_SIZE = 500 # prevent overloading server - - DEFAULT_PERMISSION = 'View' -+DEFAULT_PERMISSION_SECURE = 'Modify portal content' - PERMISSIONS = { - 'plone.app.vocabularies.Catalog': 'View', - 'plone.app.vocabularies.Keywords': 'Modify portal content', -@@ -193,7 +194,7 @@ def __call__(self): - if attributes: - base_path = getNavigationRoot(context) - sm = getSecurityManager() -- can_edit = sm.checkPermission('Modify portal content', context) -+ can_edit = sm.checkPermission(DEFAULT_PERMISSION_SECURE, context) - for vocab_item in results: - if not results_are_brains: - vocab_item = vocab_item.value -diff --git a/plone/app/content/tests/test_widgets.py b/plone/app/content/tests/test_widgets.py -index 48befae..6541d37 100644 ---- a/plone/app/content/tests/test_widgets.py -+++ b/plone/app/content/tests/test_widgets.py -@@ -146,7 +146,69 @@ def testVocabularyCatalogResults(self): - }) - data = json.loads(view()) - self.assertEquals(len(data['results']), 1) -- self.portal.manage_delObjects(['page']) -+ -+ def testVocabularyCatalogUnsafeMetadataAllowed(self): -+ """Users with permission "Modify portal content" are allowed to see -+ ``_unsafe_metadata``. -+ """ -+ self.portal.invokeFactory('Document', id="page", title="page") -+ self.portal.page.reindexObject() -+ view = VocabularyView(self.portal, self.request) -+ query = { -+ 'criteria': [ -+ { -+ 'i': 'path', -+ 'o': 'plone.app.querystring.operation.string.path', -+ 'v': '/plone/page' -+ } -+ ] -+ } -+ self.request.form.update({ -+ 'name': 'plone.app.vocabularies.Catalog', -+ 'query': json.dumps(query), -+ 'attributes': [ -+ 'id', -+ 'commentors', -+ 'Creator', -+ 'listCreators', -+ ] -+ }) -+ data = json.loads(view()) -+ self.assertEquals(len(data['results'][0].keys()), 4) -+ -+ def testVocabularyCatalogUnsafeMetadataDisallowed(self): -+ """Users without permission "Modify portal content" are not allowed to -+ see ``_unsafe_metadata``. -+ """ -+ self.portal.invokeFactory('Document', id="page", title="page") -+ self.portal.page.reindexObject() -+ # Downgrade permissions -+ setRoles(self.portal, TEST_USER_ID, []) -+ view = VocabularyView(self.portal, self.request) -+ query = { -+ 'criteria': [ -+ { -+ 'i': 'path', -+ 'o': 'plone.app.querystring.operation.string.path', -+ 'v': '/plone/page' -+ } -+ ] -+ } -+ self.request.form.update({ -+ 'name': 'plone.app.vocabularies.Catalog', -+ 'query': json.dumps(query), -+ 'attributes': [ -+ 'id', -+ 'commentors', -+ 'Creator', -+ 'listCreators', -+ ] -+ }) -+ data = json.loads(view()) -+ # Only one result key should be returned, as ``commentors``, -+ # ``Creator`` and ``listCreators`` is considered unsafe and thus -+ # skipped. -+ self.assertEquals(len(data['results'][0].keys()), 1) - - def testVocabularyBatching(self): - amount = 30 - - -Repository: plone.app.content - - -Branch: refs/heads/master -Date: 2016-04-11T11:33:33+02:00 -Author: Johannes Raggam (thet) -Commit: https://github.com/plone/plone.app.content/commit/ae99abbcf8d1b8a9315e6498a65f01dc3d5d884b - -Add test for bbb529a3 - translating result values from the ``@@getVocabulary`` view - -Files changed: -M plone/app/content/tests/test_widgets.py -M setup.py - -diff --git a/plone/app/content/tests/test_widgets.py b/plone/app/content/tests/test_widgets.py -index 6541d37..096e1ef 100644 ---- a/plone/app/content/tests/test_widgets.py -+++ b/plone/app/content/tests/test_widgets.py -@@ -25,6 +25,7 @@ - from zope.publisher.browser import TestRequest - - import json -+import mock - import os - import transaction - -@@ -516,6 +517,45 @@ def testQueryStringConfiguration(self): - # just test one so we know it's working... - self.assertEqual(data['indexes']['sortable_title']['sortable'], True) - -+ @mock.patch('zope.i18n.negotiate', new=lambda ctx: 'de') -+ def testUntranslatableMetadata(self): -+ """Test translation of ``@@getVocabulary`` view results. -+ From the standard metadata columns, only ``Type`` is translated. -+ """ -+ # Language is set via language negotiaton patch. -+ -+ self.portal.invokeFactory('Document', id="page", title="page") -+ self.portal.page.reindexObject() -+ view = VocabularyView(self.portal, self.request) -+ query = { -+ 'criteria': [ -+ { -+ 'i': 'path', -+ 'o': 'plone.app.querystring.operation.string.path', -+ 'v': '/plone/page' -+ } -+ ] -+ } -+ self.request.form.update({ -+ 'name': 'plone.app.vocabularies.Catalog', -+ 'query': json.dumps(query), -+ 'attributes': [ -+ 'id', -+ 'portal_type', -+ 'Type', -+ ] -+ }) -+ -+ # data['results'] should return one item, which represents the document -+ # created before. -+ data = json.loads(view()) -+ -+ # Type is translated -+ self.assertEqual(data['results'][0]['Type'], u'Seite') -+ -+ # portal_type is never translated -+ self.assertEqual(data['results'][0]['portal_type'], u'Document') -+ - - class FunctionalBrowserTest(unittest.TestCase): +-1.1.7 (unreleased) ++1.2.0 (unreleased) + ------------------ + Incompatibilities: diff --git a/setup.py b/setup.py -index 0c3e735..7970ec7 100644 +index df18723..a79df48 100644 --- a/setup.py +++ b/setup.py -@@ -32,6 +32,7 @@ - test=[ - 'plone.app.contenttypes', - 'plone.app.testing', -+ 'mock' - ] - ), - install_requires=[ - - -Repository: plone.app.content - - -Branch: refs/heads/master -Date: 2016-04-11T15:27:52+02:00 -Author: Johannes Raggam (thet) -Commit: https://github.com/plone/plone.app.content/commit/88b484d7bcafdba033da92e196dd16ef59fb792b - -use warning status if errors are present - -Files changed: -M plone/app/content/browser/contents/__init__.py - -diff --git a/plone/app/content/browser/contents/__init__.py b/plone/app/content/browser/contents/__init__.py -index cb74782..faa89aa 100644 ---- a/plone/app/content/browser/contents/__init__.py -+++ b/plone/app/content/browser/contents/__init__.py -@@ -163,7 +163,7 @@ def message(self, missing=[]): - ) - - return self.json({ -- 'status': 'success', -+ 'status': 'warning' if self.errors else 'success', - 'msg': translated_msg - }) - - - -Repository: plone.app.content - - -Branch: refs/heads/master -Date: 2016-04-11T15:28:16+02:00 -Author: Johannes Raggam (thet) -Commit: https://github.com/plone/plone.app.content/commit/b7a9a14bea3467a88ab2f876123a2877dd3e5ebc - -make the error look a bit nicer. has to be translated! - -Files changed: -M plone/app/content/browser/contents/paste.py - -diff --git a/plone/app/content/browser/contents/paste.py b/plone/app/content/browser/contents/paste.py -index 09dae98..54776b0 100644 ---- a/plone/app/content/browser/contents/paste.py -+++ b/plone/app/content/browser/contents/paste.py -@@ -53,7 +53,7 @@ def __call__(self): - if 'Disallowed subobject type: ' in e.message: - msg_parts = e.message.split(':') - self.errors.append( -- _(u'Disallowed subobject type: ${type}', -+ _(u'Disallowed subobject type "${type}"', - mapping={u'type': msg_parts[1].strip()})) - else: - raise e - - -Repository: plone.app.content - - -Branch: refs/heads/master -Date: 2016-04-11T15:28:32+02:00 -Author: Johannes Raggam (thet) -Commit: https://github.com/plone/plone.app.content/commit/2203918affba3da6d9ab5a63486f42cf43e33b36 - -tests for folder contents pasting - -Files changed: -M plone/app/content/tests/test_contents.py - -diff --git a/plone/app/content/tests/test_contents.py b/plone/app/content/tests/test_contents.py -index 1bf594d..97dac7c 100644 ---- a/plone/app/content/tests/test_contents.py -+++ b/plone/app/content/tests/test_contents.py -@@ -1,4 +1,13 @@ - # -*- coding: utf-8 -*- -+from plone.app.content.testing import PLONE_APP_CONTENT_DX_INTEGRATION_TESTING -+from plone.app.testing import login -+from plone.app.testing import setRoles -+from plone.app.testing import TEST_USER_ID -+from plone.app.testing import TEST_USER_NAME -+from plone.dexterity.fti import DexterityFTI -+ -+import json -+import mock - import unittest +@@ -1,6 +1,6 @@ + from setuptools import setup, find_packages +-version = '1.1.7.dev0' ++version = '1.2.0.dev0' -@@ -76,3 +85,71 @@ def physicalPathFromURL(self, url): - ctx = MockContext('/approot/PloneSite/folder/SubSite/folder') - ctx.vh_root = '/approot/PloneSite/folder/SubSite' - self.assertEqual(get_top_site_from_url(ctx, req).id, 'SubSite') -+ -+ -+class ContentsPasteTests(unittest.TestCase): -+ layer = PLONE_APP_CONTENT_DX_INTEGRATION_TESTING -+ -+ def setUp(self): -+ self.portal = self.layer['portal'] -+ self.request = self.layer['request'] -+ -+ # TYPE 1 -+ type1_fti = DexterityFTI('type1') -+ type1_fti.klass = 'plone.dexterity.content.Container' -+ type1_fti.filter_content_types = True -+ type1_fti.allowed_content_types = ['type1'] -+ type1_fti.behaviors = ( -+ 'Products.CMFPlone.interfaces.constrains.ISelectableConstrainTypes', # noqa -+ 'plone.app.dexterity.behaviors.metadata.IBasic' -+ ) -+ self.portal.portal_types._setObject('type1', type1_fti) -+ self.type1_fti = type1_fti -+ -+ login(self.portal, TEST_USER_NAME) -+ setRoles(self.portal, TEST_USER_ID, ['Manager']) -+ -+ self.portal.invokeFactory('type1', id='it1', title='Item 1') -+ -+ @mock.patch('plone.app.content.browser.contents.ContentsBaseAction.protect', lambda x: True) # noqa -+ def test_paste_success(self): -+ """Copy content item and paste in portal root. -+ """ -+ # # setup copying via @@fc-copy -+ # from plone.uuid.interfaces import IUUID -+ # self.request['selection'] = [IUUID(self.portal.it1)] -+ # self.portal.restrictedTraverse('@@fc-copy')() -+ -+ self.request['__cp'] = self.portal.manage_copyObjects(['it1']) -+ self.request.form['folder'] = '/' -+ res = self.portal.restrictedTraverse('@@fc-paste')() -+ -+ res = json.loads(res) -+ self.assertEqual(res['status'], 'success') -+ self.assertEqual(len(self.portal.contentIds()), 2) -+ -+ @mock.patch('plone.app.content.browser.contents.ContentsBaseAction.protect', lambda x: True) # noqa -+ def test_paste_success_paste_in_itself(self): -+ """Copy content item and paste in itself. Because we can. -+ """ -+ self.request['__cp'] = self.portal.manage_copyObjects(['it1']) -+ self.request.form['folder'] = '/it1' -+ res = self.portal.it1.restrictedTraverse('@@fc-paste')() -+ -+ res = json.loads(res) -+ self.assertEqual(res['status'], 'success') -+ self.assertEqual(len(self.portal.it1.contentIds()), 1) -+ -+ @mock.patch('plone.app.content.browser.contents.ContentsBaseAction.protect', lambda x: True) # noqa -+ def test_paste_fail_constraint(self): -+ """Fail pasting content item in itself when folder constraints don't -+ allow to. -+ """ -+ self.type1_fti.allowed_content_types = [] # set folder constraints -+ self.request['__cp'] = self.portal.manage_copyObjects(['it1']) -+ self.request.form['folder'] = '/it1' -+ res = self.portal.it1.restrictedTraverse('@@fc-paste')() -+ -+ res = json.loads(res) -+ self.assertEqual(res['status'], 'warning') -+ self.assertEqual(len(self.portal.it1.contentIds()), 0) + setup(name='plone.app.collection', + version=version, -Repository: plone.app.content +Repository: plone.app.collection Branch: refs/heads/master -Date: 2016-04-11T16:51:25+02:00 +Date: 2016-04-11T16:56:07+02:00 Author: Jens W. Klein (jensens) -Commit: https://github.com/plone/plone.app.content/commit/fcaa054c67c11dd9809bc63329e1e8f74af5e718 +Commit: https://github.com/plone/plone.app.collection/commit/0252e3c51989d1470b39806b2c461b3bc1261a3b -Merge pull request #87 from plone/thet-fcfixes +Merge pull request #34 from plone/uninstall-profile -More folder contents fixes +Add uninstall profile Files changed: -A plone/app/content/basecontent.rst -A plone/app/content/tests/test_contents.py +A plone/app/collection/profiles/uninstall/types.xml M CHANGES.rst -M plone/app/content/browser/contents/__init__.py -M plone/app/content/browser/contents/paste.py -M plone/app/content/browser/contents/properties.py -M plone/app/content/browser/contents/tags.py -M plone/app/content/browser/vocabulary.py -M plone/app/content/tests/test_basecontent.py -M plone/app/content/tests/test_widgets.py +M plone/app/collection/configure.zcml +M plone/app/collection/profiles/default/types.xml M setup.py -D plone/app/content/basecontent.txt diff --git a/CHANGES.rst b/CHANGES.rst -index 88cf599..47c1b77 100644 +index 80c9317..19f7fe3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst -@@ -10,16 +10,22 @@ Incompatibilities: - - New: - --- Add ``Creator``, ``Description``, ``end``, ``start`` and ``location`` to the available columns and context attributes for folder_contents. -+- Show attributes from ``_unsafe_metadata`` if user has "Modify Portal Content" permissions. - [thet] - -- Show attributes from ``_unsafe_metadata`` if user has "Modify Portal Content" permissions. -+- Add ``Creator``, ``Description``, ``end``, ``start`` and ``location`` to the available columns and context attributes for folder_contents. - [thet] - - Fixes: - --- Remove ``portal_type`` from available columns and use ``Type`` instead, which is meant to be read by humans. -- ``portal_type`` is now available on the attributes object. -+- Folder contents: When pasting, handle "Disallowed subobject type" ValueError and present a helpful error message. -+ Fixes: plone/mockup#657 -+ [thet] -+ -+- Folder contents: Acquire the top most visible portal object to operate on. -+ Fixes some issues in INavigationRoot or ISite based subsites and virtual hosting environments pointing to subsites. -+ Fixes include: show correct breadcrumb paths, paste to correct location. -+ Fixes: #86 - [thet] - - - Added most notably `portal_type`, `review_state` and `Subject` but also `exclude_from_nav`, `is_folderish`, `last_comment_date`, `meta_type` and `total_comments` to ``BaseVocabularyView`` ``translate_ignored`` list. -@@ -27,14 +33,15 @@ Fixes: - Fixes https://github.com/plone/plone.app.content/issues/77 - [thet] - -+- Remove ``portal_type`` from available columns and use ``Type`` instead, which is meant to be read by humans. -+ ``portal_type`` is now available on the attributes object. -+ [thet] -+ - - Vocabulary permissions are considered View permission by default, if not - stated different in PERMISSION global. Renamed _permissions to PERMISSIONS, - Deprecated BBB name in place. Also minor code-style changes - [jensens, thet] - --- Fix folder contents path and breadcrumbs settings to show correct paths and render the toolbar correctly in navigation root subsites and virtual hosting environments pointing to subsites. -- [thet] -- - - Fix test isolation problem and remove an unnecessary test dependency on ``plone.app.widgets``. - [thet] - -diff --git a/plone/app/content/basecontent.rst b/plone/app/content/basecontent.rst -new file mode 100644 -index 0000000..4007109 ---- /dev/null -+++ b/plone/app/content/basecontent.rst -@@ -0,0 +1,152 @@ -+============= -+Basic content -+============= -+ -+plone.app.content provides some helper base classes for content. Here are -+some simple examples of using them. -+.. code-block:: python -+ -+ >>> from plone.app.content import container, item -+ -+Let us define two fictional types, a folderish one and a non-folderish one. -+We should define factories for these types as well. Factories can be -+referenced from CMF FTI's, and also from directives. -+With an appropriate add view (e.g. using formlib's AddForm) this will be -+available from Plone's "add item" menu. Factories are registered as named -+utilities. -+ -+Note that we need to define a portal_type to keep CMF happy. -+.. code-block:: python -+ -+ >>> from zope.interface import implements, Interface -+ >>> from zope import schema -+ >>> from zope.component.factory import Factory -+ -+First, a container: -+.. code-block:: python -+ -+ >>> class IMyContainer(Interface): -+ ... title = schema.TextLine(title=u"My title") -+ ... description = schema.TextLine(title=u"My other title") -+ -+ >>> class MyContainer(container.Container): -+ ... implements(IMyContainer) -+ ... portal_type = "My container" -+ ... title = u"" -+ ... description = u"" -+ -+ >>> containerFactory = Factory(MyContainer) -+ -+Then, an item: -+.. code-block:: python -+ -+ >>> class IMyType(Interface): -+ ... title = schema.TextLine(title=u"My title") -+ ... description = schema.TextLine(title=u"My other title") -+ -+ >>> class MyType(item.Item): -+ ... implements(IMyType) -+ ... portal_type = "My type" -+ ... title = u"" -+ ... description = u"" -+ -+ >>> itemFactory = Factory(MyType) -+ -+We can now create the items. -+.. code-block:: python -+ -+ >>> container = containerFactory("my-container") -+ >>> container.id -+ 'my-container' -+ >>> container.title = "A sample container" -+ -+We need to add it to an object manager for acquisition to do its magic. -+.. code-block:: python -+ -+ >>> newid = self.portal._setObject(container.id, container) -+ >>> container = getattr(self.portal, newid) -+ -+We will add the item directly to the container later. -+.. code-block:: python -+ -+ >>> item = itemFactory("my-item") -+ >>> item.id -+ 'my-item' -+ >>> item.title = "A non-folderish item" -+ -+Note that both the container type and the item type are contentish. This is -+important, because CMF provides event handlers that automatically index -+objects that are IContentish. -+.. code-block:: python -+ -+ >>> from Products.CMFCore.interfaces import IContentish -+ >>> IContentish.providedBy(container) -+ True -+ >>> IContentish.providedBy(item) -+ True -+ -+Only the container is folderish, though: -+.. code-block:: python -+ -+ >>> from Products.CMFCore.interfaces import IFolderish -+ >>> bool(container.isPrincipiaFolderish) -+ True -+ >>> IFolderish.providedBy(container) -+ True -+ >>> bool(item.isPrincipiaFolderish) -+ False -+ >>> IFolderish.providedBy(item) -+ False -+ -+We can use the more natural Zope3-style container API, or the traditional -+ObjectManager one. -+.. code-block:: python -+ -+ >>> container['my-item'] = item -+ >>> 'my-item' in container -+ True -+ >>> 'my-item' in container.objectIds() -+ True -+ >>> del container['my-item'] -+ >>> 'my-item' in container -+ False -+ >>> container._setObject('my-item', item) -+ 'my-item' -+ >>> 'my-item' in container -+ True -+ -+Both pieces of content should have been cataloged. -+.. code-block:: python -+ -+ >>> container = self.portal['my-container'] -+ >>> item = container['my-item'] -+ -+ >>> from Products.CMFCore.utils import getToolByName -+ >>> catalog = getToolByName(self.portal, 'portal_catalog') -+ >>> [b.Title for b in catalog(getId = 'my-container')] -+ ['A sample container'] -+ >>> [b.Title for b in catalog(getId = 'my-item')] -+ ['A non-folderish item'] -+ -+If we modify an object and trigger a modified event, it should be updated. -+.. code-block:: python -+ -+ >>> from zope.lifecycleevent import ObjectModifiedEvent -+ >>> from zope.event import notify -+ -+ >>> container.title = "Updated title" -+ >>> item.title = "Also updated title" -+ -+ >>> [b.Title for b in catalog(getId = 'my-container')] -+ ['A sample container'] -+ >>> [b.Title for b in catalog(getId = 'my-item')] -+ ['A non-folderish item'] -+ -+ -+ >>> notify(ObjectModifiedEvent(container)) -+ >>> notify(ObjectModifiedEvent(item)) -+ -+ >>> [b.Title for b in catalog(getId = 'my-container')] -+ ['Updated title'] -+ >>> [b.Title for b in catalog(getId = 'my-item')] -+ ['Also updated title'] -diff --git a/plone/app/content/basecontent.txt b/plone/app/content/basecontent.txt -deleted file mode 100644 -index 03f618c..0000000 ---- a/plone/app/content/basecontent.txt -+++ /dev/null -@@ -1,140 +0,0 @@ --============= --Basic content --============= -- --plone.app.content provides some helper base classes for content. Here are --some simple examples of using them. -- -- >>> from plone.app.content import container, item -- --Let us define two fictional types, a folderish one and a non-folderish one. --We should define factories for these types as well. Factories can be --referenced from CMF FTI's, and also from directives. --With an appropriate add view (e.g. using formlib's AddForm) this will be --available from Plone's "add item" menu. Factories are registered as named --utilities. -- --Note that we need to define a portal_type to keep CMF happy. -- -- >>> from zope.interface import implements, Interface -- >>> from zope import schema -- >>> from zope.component.factory import Factory -- --First, a container: -- -- >>> class IMyContainer(Interface): -- ... title = schema.TextLine(title=u"My title") -- ... description = schema.TextLine(title=u"My other title") -- -- >>> class MyContainer(container.Container): -- ... implements(IMyContainer) -- ... portal_type = "My container" -- ... title = u"" -- ... description = u"" -- -- >>> containerFactory = Factory(MyContainer) -- --Then, an item: -- -- >>> class IMyType(Interface): -- ... title = schema.TextLine(title=u"My title") -- ... description = schema.TextLine(title=u"My other title") -- -- >>> class MyType(item.Item): -- ... implements(IMyType) -- ... portal_type = "My type" -- ... title = u"" -- ... description = u"" -- -- >>> itemFactory = Factory(MyType) -- --We can now create the items. -- -- >>> container = containerFactory("my-container") -- >>> container.id -- 'my-container' -- >>> container.title = "A sample container" -- --We need to add it to an object manager for acquisition to do its magic. -- -- >>> newid = self.portal._setObject(container.id, container) -- >>> container = getattr(self.portal, newid) -- --We will add the item directly to the container later. -- -- >>> item = itemFactory("my-item") -- >>> item.id -- 'my-item' -- >>> item.title = "A non-folderish item" -- --Note that both the container type and the item type are contentish. This is --important, because CMF provides event handlers that automatically index --objects that are IContentish. -- -- >>> from Products.CMFCore.interfaces import IContentish -- >>> IContentish.providedBy(container) -- True -- >>> IContentish.providedBy(item) -- True -- --Only the container is folderish, though: -- -- >>> from Products.CMFCore.interfaces import IFolderish -- >>> bool(container.isPrincipiaFolderish) -- True -- >>> IFolderish.providedBy(container) -- True -- >>> bool(item.isPrincipiaFolderish) -- False -- >>> IFolderish.providedBy(item) -- False -- --We can use the more natural Zope3-style container API, or the traditional --ObjectManager one. -- -- >>> container['my-item'] = item -- >>> 'my-item' in container -- True -- >>> 'my-item' in container.objectIds() -- True -- >>> del container['my-item'] -- >>> 'my-item' in container -- False -- >>> container._setObject('my-item', item) -- 'my-item' -- >>> 'my-item' in container -- True -- --Both pieces of content should have been cataloged. -- -- >>> container = self.portal['my-container'] -- >>> item = container['my-item'] -- -- >>> from Products.CMFCore.utils import getToolByName -- >>> catalog = getToolByName(self.portal, 'portal_catalog') -- >>> [b.Title for b in catalog(getId = 'my-container')] -- ['A sample container'] -- >>> [b.Title for b in catalog(getId = 'my-item')] -- ['A non-folderish item'] -- --If we modify an object and trigger a modified event, it should be updated. -- -- >>> from zope.lifecycleevent import ObjectModifiedEvent -- >>> from zope.event import notify -- -- >>> container.title = "Updated title" -- >>> item.title = "Also updated title" -- -- >>> [b.Title for b in catalog(getId = 'my-container')] -- ['A sample container'] -- >>> [b.Title for b in catalog(getId = 'my-item')] -- ['A non-folderish item'] -- -- -- >>> notify(ObjectModifiedEvent(container)) -- >>> notify(ObjectModifiedEvent(item)) -- -- >>> [b.Title for b in catalog(getId = 'my-container')] -- ['Updated title'] -- >>> [b.Title for b in catalog(getId = 'my-item')] -- ['Also updated title'] -diff --git a/plone/app/content/browser/contents/__init__.py b/plone/app/content/browser/contents/__init__.py -index 72aa372..faa89aa 100644 ---- a/plone/app/content/browser/contents/__init__.py -+++ b/plone/app/content/browser/contents/__init__.py -@@ -39,7 +39,14 @@ - - - def get_top_site_from_url(context, request): -- """Find the top-most site, which is in the url path. -+ """Find the top-most site, which is still in the url path. -+ -+ If the current context is within a subsite within a PloneSiteRoot and no -+ virtual hosting is in place, the PloneSiteRoot is returned. -+ When at the same context but in a virtual hosting environment with the -+ virtual host root pointing to the subsites, it returns the subsite instead -+ of the PloneSiteRoot. -+ - For this given content structure: - - /Plone/Subsite -@@ -68,6 +75,10 @@ class ContentsBaseAction(BrowserView): - failure_msg = _('Failure') - required_obj_permission = None - -+ @property -+ def site(self): -+ return get_top_site_from_url(self.context, self.request) -+ - def objectTitle(self, obj): - context = aq_inner(obj) - title = utils.pretty_title_or_id(context, context) -@@ -100,11 +111,10 @@ def finish(self): - def __call__(self): - self.protect() - self.errors = [] -- site = getSite() - context = aq_inner(self.context) - selection = self.get_selection() - -- self.dest = site.restrictedTraverse( -+ self.dest = self.site.restrictedTraverse( - str(self.request.form['folder'].lstrip('/'))) - - self.catalog = getToolByName(context, 'portal_catalog') -@@ -153,7 +163,7 @@ def message(self, missing=[]): - ) +@@ -1,7 +1,7 @@ + Changelog + ========= - return self.json({ -- 'status': 'success', -+ 'status': 'warning' if self.errors else 'success', - 'msg': translated_msg - }) +-1.1.7 (unreleased) ++1.2.0 (unreleased) + ------------------ -diff --git a/plone/app/content/browser/contents/paste.py b/plone/app/content/browser/contents/paste.py -index 0f5206d..54776b0 100644 ---- a/plone/app/content/browser/contents/paste.py -+++ b/plone/app/content/browser/contents/paste.py -@@ -4,7 +4,6 @@ - from plone.app.content.interfaces import IStructureAction - from Products.CMFPlone import PloneMessageFactory as _ - from ZODB.POSException import ConflictError --from zope.component.hooks import getSite - from zope.i18n import translate - from zope.interface import implementer + Incompatibilities: +@@ -10,7 +10,8 @@ Incompatibilities: -@@ -35,9 +34,8 @@ class PasteActionView(ContentsBaseAction): - def __call__(self): - self.protect() - self.errors = [] -- site = getSite() - -- self.dest = site.restrictedTraverse( -+ self.dest = self.site.restrictedTraverse( - str(self.request.form['folder'].lstrip('/'))) - - try: -@@ -51,5 +49,13 @@ def __call__(self): - self.errors.append( - _(u'You are not authorized to paste ${title} here.', - mapping={u'title': self.objectTitle(self.dest)})) -+ except ValueError as e: -+ if 'Disallowed subobject type: ' in e.message: -+ msg_parts = e.message.split(':') -+ self.errors.append( -+ _(u'Disallowed subobject type "${type}"', -+ mapping={u'type': msg_parts[1].strip()})) -+ else: -+ raise e - - return self.message() -diff --git a/plone/app/content/browser/contents/properties.py b/plone/app/content/browser/contents/properties.py -index 2ccf800..ce5d9a3 100644 ---- a/plone/app/content/browser/contents/properties.py -+++ b/plone/app/content/browser/contents/properties.py -@@ -21,9 +21,7 @@ def __init__(self, context, request): - self.request = request - - def get_options(self): -- site = getSite() -- base_url = site.absolute_url() -- base_vocabulary = '%s/@@getVocabulary?name=' % base_url -+ base_vocabulary = '%s/@@getVocabulary?name=' % getSite().absolute_url() - return { - 'title': translate(_('Properties'), context=self.request), - 'id': 'properties', -@@ -34,7 +32,7 @@ def get_options(self): - 'template': self.template( - vocabulary_url='%splone.app.vocabularies.Users' % ( - base_vocabulary) -- ) -+ ) - } - } - -diff --git a/plone/app/content/browser/contents/tags.py b/plone/app/content/browser/contents/tags.py -index d582f13..e3acb67 100644 ---- a/plone/app/content/browser/contents/tags.py -+++ b/plone/app/content/browser/contents/tags.py -@@ -19,9 +19,7 @@ def __init__(self, context, request): - self.request = request + New: - def get_options(self): -- site = getSite() -- base_url = site.absolute_url() -- base_vocabulary = '%s/@@getVocabulary?name=' % base_url -+ base_vocabulary = '%s/@@getVocabulary?name=' % getSite().absolute_url() - return { - 'title': translate(_('Tags'), context=self.request), - 'id': 'tags', -@@ -31,7 +29,7 @@ def get_options(self): - 'template': self.template( - vocabulary_url='%splone.app.vocabularies.Keywords' % ( - base_vocabulary) -- ) -+ ) - } - } +-- *add item here* ++- Added uninstall profile. The Collection type is removed when you ++ uninstall this package. [maurits] -diff --git a/plone/app/content/browser/vocabulary.py b/plone/app/content/browser/vocabulary.py -index 37b4104..fdf32fd 100644 ---- a/plone/app/content/browser/vocabulary.py -+++ b/plone/app/content/browser/vocabulary.py -@@ -31,6 +31,7 @@ - MAX_BATCH_SIZE = 500 # prevent overloading server + Fixes: - DEFAULT_PERMISSION = 'View' -+DEFAULT_PERMISSION_SECURE = 'Modify portal content' - PERMISSIONS = { - 'plone.app.vocabularies.Catalog': 'View', - 'plone.app.vocabularies.Keywords': 'Modify portal content', -@@ -193,7 +194,7 @@ def __call__(self): - if attributes: - base_path = getNavigationRoot(context) - sm = getSecurityManager() -- can_edit = sm.checkPermission('Modify portal content', context) -+ can_edit = sm.checkPermission(DEFAULT_PERMISSION_SECURE, context) - for vocab_item in results: - if not results_are_brains: - vocab_item = vocab_item.value -diff --git a/plone/app/content/tests/test_basecontent.py b/plone/app/content/tests/test_basecontent.py -index c8ac5ab..5b9a0ea 100644 ---- a/plone/app/content/tests/test_basecontent.py -+++ b/plone/app/content/tests/test_basecontent.py -@@ -8,7 +8,7 @@ - def test_suite(): - return unittest.TestSuite(( - ztc.ZopeDocFileSuite( -- 'basecontent.txt', -+ 'basecontent.rst', - package='plone.app.content', - test_class=ContentFunctionalTestCase, - optionflags=(doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE)), -diff --git a/plone/app/content/tests/test_contents.py b/plone/app/content/tests/test_contents.py +diff --git a/plone/app/collection/configure.zcml b/plone/app/collection/configure.zcml +index d2bda5d..7f8e3b6 100644 +--- a/plone/app/collection/configure.zcml ++++ b/plone/app/collection/configure.zcml +@@ -9,7 +9,7 @@ + + + +- ++ + + ++ ++ + + + +- + +diff --git a/plone/app/collection/profiles/uninstall/types.xml b/plone/app/collection/profiles/uninstall/types.xml new file mode 100644 -index 0000000..97dac7c +index 0000000..a1ea1c6 --- /dev/null -+++ b/plone/app/content/tests/test_contents.py -@@ -0,0 +1,155 @@ -+# -*- coding: utf-8 -*- -+from plone.app.content.testing import PLONE_APP_CONTENT_DX_INTEGRATION_TESTING -+from plone.app.testing import login -+from plone.app.testing import setRoles -+from plone.app.testing import TEST_USER_ID -+from plone.app.testing import TEST_USER_NAME -+from plone.dexterity.fti import DexterityFTI -+ -+import json -+import mock -+import unittest -+ -+ -+class ContentsUnitTests(unittest.TestCase): -+ -+ def test_get_top_site_from_url(self): -+ """Unit test for ``get_top_site_from_url`` with context and request -+ mocks. -+ -+ Test content structure: -+ /approot/PloneSite/folder/SubSite/folder -+ PloneSite and SubSite implement ISite -+ """ -+ from plone.app.content.browser.contents import get_top_site_from_url -+ from zope.component.interfaces import ISite -+ from zope.interface import alsoProvides -+ from urlparse import urlparse -+ -+ class MockContext(object): -+ vh_url = 'http://nohost' -+ vh_root = '' -+ -+ def __init__(self, physical_path): -+ self.physical_path = physical_path -+ if self.physical_path.split('/')[-1] in ('PloneSite', 'SubSite'): # noqa -+ alsoProvides(self, ISite) -+ -+ @property -+ def id(self): -+ return self.physical_path.split('/')[-1] -+ -+ def absolute_url(self): -+ return self.vh_url + self.physical_path[len(self.vh_root):] or '/' # noqa -+ -+ def restrictedTraverse(self, path): -+ return MockContext(self.vh_root + path) -+ -+ class MockRequest(object): -+ vh_url = 'http://nohost' -+ vh_root = '' -+ -+ def physicalPathFromURL(self, url): -+ # Return the physical path from a URL. -+ # The outer right '/' is not part of the path. -+ path = self.vh_root + urlparse(url).path.rstrip('/') -+ return path.split('/') -+ -+ # NO VIRTUAL HOSTING -+ -+ req = MockRequest() -+ -+ # Case 1: -+ ctx = MockContext('/approot/PloneSite') -+ self.assertEqual(get_top_site_from_url(ctx, req).id, 'PloneSite') -+ -+ # Case 2 -+ ctx = MockContext('/approot/PloneSite/folder') -+ self.assertEqual(get_top_site_from_url(ctx, req).id, 'PloneSite') -+ -+ # Case 3: -+ ctx = MockContext('/approot/PloneSite/folder/SubSite/folder') -+ self.assertEqual(get_top_site_from_url(ctx, req).id, 'PloneSite') -+ -+ # VIRTUAL HOSTING ON SUBSITE -+ -+ req = MockRequest() -+ req.vh_root = '/approot/PloneSite/folder/SubSite' -+ -+ # Case 4: -+ ctx = MockContext('/approot/PloneSite/folder/SubSite') -+ ctx.vh_root = '/approot/PloneSite/folder/SubSite' -+ self.assertEqual(get_top_site_from_url(ctx, req).id, 'SubSite') -+ -+ # Case 5: -+ ctx = MockContext('/approot/PloneSite/folder/SubSite/folder') -+ ctx.vh_root = '/approot/PloneSite/folder/SubSite' -+ self.assertEqual(get_top_site_from_url(ctx, req).id, 'SubSite') -+ -+ -+class ContentsPasteTests(unittest.TestCase): -+ layer = PLONE_APP_CONTENT_DX_INTEGRATION_TESTING -+ -+ def setUp(self): -+ self.portal = self.layer['portal'] -+ self.request = self.layer['request'] -+ -+ # TYPE 1 -+ type1_fti = DexterityFTI('type1') -+ type1_fti.klass = 'plone.dexterity.content.Container' -+ type1_fti.filter_content_types = True -+ type1_fti.allowed_content_types = ['type1'] -+ type1_fti.behaviors = ( -+ 'Products.CMFPlone.interfaces.constrains.ISelectableConstrainTypes', # noqa -+ 'plone.app.dexterity.behaviors.metadata.IBasic' -+ ) -+ self.portal.portal_types._setObject('type1', type1_fti) -+ self.type1_fti = type1_fti -+ -+ login(self.portal, TEST_USER_NAME) -+ setRoles(self.portal, TEST_USER_ID, ['Manager']) -+ -+ self.portal.invokeFactory('type1', id='it1', title='Item 1') -+ -+ @mock.patch('plone.app.content.browser.contents.ContentsBaseAction.protect', lambda x: True) # noqa -+ def test_paste_success(self): -+ """Copy content item and paste in portal root. -+ """ -+ # # setup copying via @@fc-copy -+ # from plone.uuid.interfaces import IUUID -+ # self.request['selection'] = [IUUID(self.portal.it1)] -+ # self.portal.restrictedTraverse('@@fc-copy')() -+ -+ self.request['__cp'] = self.portal.manage_copyObjects(['it1']) -+ self.request.form['folder'] = '/' -+ res = self.portal.restrictedTraverse('@@fc-paste')() -+ -+ res = json.loads(res) -+ self.assertEqual(res['status'], 'success') -+ self.assertEqual(len(self.portal.contentIds()), 2) -+ -+ @mock.patch('plone.app.content.browser.contents.ContentsBaseAction.protect', lambda x: True) # noqa -+ def test_paste_success_paste_in_itself(self): -+ """Copy content item and paste in itself. Because we can. -+ """ -+ self.request['__cp'] = self.portal.manage_copyObjects(['it1']) -+ self.request.form['folder'] = '/it1' -+ res = self.portal.it1.restrictedTraverse('@@fc-paste')() -+ -+ res = json.loads(res) -+ self.assertEqual(res['status'], 'success') -+ self.assertEqual(len(self.portal.it1.contentIds()), 1) -+ -+ @mock.patch('plone.app.content.browser.contents.ContentsBaseAction.protect', lambda x: True) # noqa -+ def test_paste_fail_constraint(self): -+ """Fail pasting content item in itself when folder constraints don't -+ allow to. -+ """ -+ self.type1_fti.allowed_content_types = [] # set folder constraints -+ self.request['__cp'] = self.portal.manage_copyObjects(['it1']) -+ self.request.form['folder'] = '/it1' -+ res = self.portal.it1.restrictedTraverse('@@fc-paste')() -+ -+ res = json.loads(res) -+ self.assertEqual(res['status'], 'warning') -+ self.assertEqual(len(self.portal.it1.contentIds()), 0) -diff --git a/plone/app/content/tests/test_widgets.py b/plone/app/content/tests/test_widgets.py -index 48befae..096e1ef 100644 ---- a/plone/app/content/tests/test_widgets.py -+++ b/plone/app/content/tests/test_widgets.py -@@ -25,6 +25,7 @@ - from zope.publisher.browser import TestRequest - - import json -+import mock - import os - import transaction - -@@ -146,7 +147,69 @@ def testVocabularyCatalogResults(self): - }) - data = json.loads(view()) - self.assertEquals(len(data['results']), 1) -- self.portal.manage_delObjects(['page']) -+ -+ def testVocabularyCatalogUnsafeMetadataAllowed(self): -+ """Users with permission "Modify portal content" are allowed to see -+ ``_unsafe_metadata``. -+ """ -+ self.portal.invokeFactory('Document', id="page", title="page") -+ self.portal.page.reindexObject() -+ view = VocabularyView(self.portal, self.request) -+ query = { -+ 'criteria': [ -+ { -+ 'i': 'path', -+ 'o': 'plone.app.querystring.operation.string.path', -+ 'v': '/plone/page' -+ } -+ ] -+ } -+ self.request.form.update({ -+ 'name': 'plone.app.vocabularies.Catalog', -+ 'query': json.dumps(query), -+ 'attributes': [ -+ 'id', -+ 'commentors', -+ 'Creator', -+ 'listCreators', -+ ] -+ }) -+ data = json.loads(view()) -+ self.assertEquals(len(data['results'][0].keys()), 4) -+ -+ def testVocabularyCatalogUnsafeMetadataDisallowed(self): -+ """Users without permission "Modify portal content" are not allowed to -+ see ``_unsafe_metadata``. -+ """ -+ self.portal.invokeFactory('Document', id="page", title="page") -+ self.portal.page.reindexObject() -+ # Downgrade permissions -+ setRoles(self.portal, TEST_USER_ID, []) -+ view = VocabularyView(self.portal, self.request) -+ query = { -+ 'criteria': [ -+ { -+ 'i': 'path', -+ 'o': 'plone.app.querystring.operation.string.path', -+ 'v': '/plone/page' -+ } -+ ] -+ } -+ self.request.form.update({ -+ 'name': 'plone.app.vocabularies.Catalog', -+ 'query': json.dumps(query), -+ 'attributes': [ -+ 'id', -+ 'commentors', -+ 'Creator', -+ 'listCreators', -+ ] -+ }) -+ data = json.loads(view()) -+ # Only one result key should be returned, as ``commentors``, -+ # ``Creator`` and ``listCreators`` is considered unsafe and thus -+ # skipped. -+ self.assertEquals(len(data['results'][0].keys()), 1) - - def testVocabularyBatching(self): - amount = 30 -@@ -454,6 +517,45 @@ def testQueryStringConfiguration(self): - # just test one so we know it's working... - self.assertEqual(data['indexes']['sortable_title']['sortable'], True) - -+ @mock.patch('zope.i18n.negotiate', new=lambda ctx: 'de') -+ def testUntranslatableMetadata(self): -+ """Test translation of ``@@getVocabulary`` view results. -+ From the standard metadata columns, only ``Type`` is translated. -+ """ -+ # Language is set via language negotiaton patch. -+ -+ self.portal.invokeFactory('Document', id="page", title="page") -+ self.portal.page.reindexObject() -+ view = VocabularyView(self.portal, self.request) -+ query = { -+ 'criteria': [ -+ { -+ 'i': 'path', -+ 'o': 'plone.app.querystring.operation.string.path', -+ 'v': '/plone/page' -+ } -+ ] -+ } -+ self.request.form.update({ -+ 'name': 'plone.app.vocabularies.Catalog', -+ 'query': json.dumps(query), -+ 'attributes': [ -+ 'id', -+ 'portal_type', -+ 'Type', -+ ] -+ }) -+ -+ # data['results'] should return one item, which represents the document -+ # created before. -+ data = json.loads(view()) -+ -+ # Type is translated -+ self.assertEqual(data['results'][0]['Type'], u'Seite') -+ -+ # portal_type is never translated -+ self.assertEqual(data['results'][0]['portal_type'], u'Document') -+ - - class FunctionalBrowserTest(unittest.TestCase): - ++++ b/plone/app/collection/profiles/uninstall/types.xml +@@ -0,0 +1,4 @@ ++ ++ ++ ++ diff --git a/setup.py b/setup.py -index 0c3e735..7970ec7 100644 +index df18723..a79df48 100644 --- a/setup.py +++ b/setup.py -@@ -32,6 +32,7 @@ - test=[ - 'plone.app.contenttypes', - 'plone.app.testing', -+ 'mock' - ] - ), - install_requires=[ +@@ -1,6 +1,6 @@ + from setuptools import setup, find_packages + +-version = '1.1.7.dev0' ++version = '1.2.0.dev0' + + setup(name='plone.app.collection', + version=version,