Commit e0e16129 authored by Jérome Perrin's avatar Jérome Perrin

CMFCategory: do not store self membership of categories

CategoryTool.getCategoryList dynamically returns categories as member of
themselves, but there were cases (for example after changing id) where
the membership happens to be saved in `categories` attribute and later
caused problems after clones or renames.
To prevent these problems, CategoryTool._setCategoryList detects the
membership to self and does not save it as part of .categories

Remove no longer needed after clone script.

Remove tests which no longer makes sense now that setCategoryList does
not add membership to self.
parent b6f82d25
...@@ -1189,11 +1189,19 @@ class CategoryTool(BaseTool): ...@@ -1189,11 +1189,19 @@ class CategoryTool(BaseTool):
security.declareProtected( Permissions.ModifyPortalContent, '_setCategoryList' ) security.declareProtected( Permissions.ModifyPortalContent, '_setCategoryList' )
def _setCategoryList(self, context, value): def _setCategoryList(self, context, value):
old = set(getattr(aq_base(context), 'categories', ())) old = set(getattr(aq_base(context), 'categories', ()))
relative_url = context.getRelativeUrl()
# Pure category are member of itself, but we don't store this membership
# (because of issues when the category is renamed/moved or cloned)
if getattr(context, 'isCategory', 0) and relative_url in value:
# note that we don't want to cast as set at this point to keep ordering (and duplicates).
value = [x for x in tuple(value) if x != relative_url]
context.categories = value = tuple(value) context.categories = value = tuple(value)
if context.isTempDocument(): if context.isTempDocument():
return return
value = set(value) value = set(value)
relative_url = context.getRelativeUrl()
for edit, value in ("remove", old - value), ("add", value - old): for edit, value in ("remove", old - value), ("add", value - old):
for path in value: for path in value:
base = self.getBaseCategoryId(path) base = self.getBaseCategoryId(path)
......
...@@ -29,6 +29,7 @@ ...@@ -29,6 +29,7 @@
from collections import deque from collections import deque
import unittest import unittest
from Acquisition import aq_base
from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
from Products.CMFCategory.Category import NBSP_UTF8 from Products.CMFCategory.Category import NBSP_UTF8
from Testing.ZopeTestCase.PortalTestCase import PortalTestCase from Testing.ZopeTestCase.PortalTestCase import PortalTestCase
...@@ -659,52 +660,24 @@ class TestCMFCategory(ERP5TypeTestCase): ...@@ -659,52 +660,24 @@ class TestCMFCategory(ERP5TypeTestCase):
""" """
europe = self.portal.portal_categories.resolveCategory('region/europe') europe = self.portal.portal_categories.resolveCategory('region/europe')
self.assertTrue('region/europe' in europe.getCategoryList()) self.assertIn('region/europe', europe.getCategoryList())
self.assertTrue(europe.isMemberOf('region/europe')) self.assertTrue(europe.isMemberOf('region/europe'))
def test_CategoryCloneIsNotMemberOfCopiedCategory(self): # this membership is not saved in .categories
# This is a bug reproduction test for problems happening when membership to self.assertNotIn('region/europe', getattr(aq_base(europe), 'categories', ()))
# self was saved in .categories.
europe_west = self.portal.portal_categories['region']['europe']['west']
france = europe_west.france
# Initially, categories' getCategoryList dynamically returns the
# "membership to self" eventhough it's not part of .categories, but after a
# category ID is changed, interaction update it's .categories to set the
# categories.
france.setCategoryList(france.getCategoryList())
cb_data = europe_west.manage_copyObjects(['france'])
destination = self.portal.portal_categories.region
copy_info, = destination.manage_pasteObjects(cb_data)
clone = destination[copy_info['new_id']]
self.assertEqual([clone.getRelativeUrl()], clone.getCategoryList())
def test_CategoryMovedIsNotMemberOfCopiedCategory(self):
# This is a bug reproduction test for problems happening when membership to
# self was saved in .categories.
original_container = self.portal.portal_categories['region']['europe']['west']
new_category = original_container.newContent(id=self.id())
# This sets membership to self in .categories
new_category.setCategoryList(new_category.getCategoryList())
self.commit()
cb_data = original_container.manage_cutObjects([new_category.getId()]) # even if we set explicitly
destination = self.portal.portal_categories.region europe.setCategoryList(europe.getCategoryList())
copy_info, = destination.manage_pasteObjects(cb_data) self.assertNotIn('region/europe', getattr(aq_base(europe), 'categories', ()))
clone = destination[copy_info['new_id']]
self.assertEqual([clone.getRelativeUrl()], clone.getCategoryList())
def test_19_getCategoryList(self): # or if we set other categories
""" europe.setCategoryList(['subordination/person_module'])
check that getCategoryList called on a category does not append self again self.assertItemsEqual(
and again ['region/europe', 'subordination/person_module'],
""" europe.getCategoryList())
region_value = self.portal.portal_categories.resolveCategory('region/%s' % self.region1) self.assertEqual(
category_list = region_value.getCategoryList() ('subordination/person_module',),
region_value.setCategoryList(category_list) getattr(aq_base(europe), 'categories', ()))
self.assertEqual(category_list, region_value.getCategoryList())
def test_19_CategoryMemberValueList(self): def test_19_CategoryMemberValueList(self):
"""Test strict_membership parameter to Category Member Value List """ """Test strict_membership parameter to Category Member Value List """
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>Script_magic</string> </key>
<value> <int>3</int> </value>
</item>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Category_afterClone</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment