From e489380e50647d91a08b35f3ed24341e6212d520 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=A9rome=20Perrin?= <jerome@nexedi.com>
Date: Sun, 22 Jan 2023 06:48:37 +0100
Subject: [PATCH] BusinessTemplate: make portal type role export stable with >
 10 roles

This sort was based on the id of the role definition but this did not
work when the type definition had more than 10 documents, because:

    >>> sorted(['1', '2', '10'])
    ['1', '10', '2']

This switches to an approach of converting the id to integer, with a
fallback in case the id generator is customized.
---
 .../test.erp5.testBusinessTemplate.py         | 55 +++++++++++++++++++
 product/ERP5/Document/BusinessTemplate.py     | 10 +++-
 2 files changed, 64 insertions(+), 1 deletion(-)

diff --git a/bt5/erp5_core_test/TestTemplateItem/portal_components/test.erp5.testBusinessTemplate.py b/bt5/erp5_core_test/TestTemplateItem/portal_components/test.erp5.testBusinessTemplate.py
index 36f4dee213..b3dacce472 100644
--- a/bt5/erp5_core_test/TestTemplateItem/portal_components/test.erp5.testBusinessTemplate.py
+++ b/bt5/erp5_core_test/TestTemplateItem/portal_components/test.erp5.testBusinessTemplate.py
@@ -159,6 +159,8 @@ class BusinessTemplateMixin(ERP5TypeTestCase, LogInterceptor):
 
     if 'Geek Object' in self.getTypeTool().objectIds():
       self.getTypeTool().manage_delObjects(['Geek Object', 'Geek Module'])
+    if 'Dummy Type' in self.getTypeTool().objectIds():
+      self.getTypeTool().manage_delObjects(['Dummy Type'])
     if 'geek_module' in self.getPortal().objectIds():
       self.getPortal().manage_delObjects(['geek_module'])
     if 'geek_workflow' in self.getWorkflowTool().objectIds():
@@ -6555,6 +6557,59 @@ class TestBusinessTemplate(BusinessTemplateMixin):
     self.assertNotEqual(None,
         self.portal.portal_actions.getActionInfo('object_view/test_global_action'))
 
+  def test_role_information_stable_export(self):
+    dummy_type = self.portal.portal_types.newContent(
+      portal_type='Base Type',
+      id='Dummy Type',
+      type_class='Folder'
+    )
+
+    for i in range(20):
+      dummy_type.newContent(
+        portal_type='Role Information',
+        title='Dummy Role Definition',
+        description='Role %s' % i,
+        role_name_list=('Assignee', ),
+        role_base_category_list=('group',),
+        role_category_list=('group/g1',))
+
+    bt = self.portal.portal_templates.newContent(
+      portal_type='Business Template',
+      title='test_bt_%s' % self.id(),
+      template_portal_type_id_list=('Dummy Type',),
+      template_portal_type_role_list=('Dummy Type', ))
+    self.tic()
+    bt.build()
+    self.tic()
+    export_dir = tempfile.mkdtemp()
+    self.addCleanup(shutil.rmtree, export_dir)
+    bt.export(path=export_dir, local=True)
+
+    with open(os.path.join(export_dir, 'PortalTypeRolesTemplateItem', 'Dummy%20Type.xml')) as f:
+      role_content = f.read()
+    self.tic()
+    new_bt = self.portal.portal_templates.download(url='file://%s' % export_dir)
+
+    self.portal.portal_types.manage_delObjects(['Dummy Type'])
+    self.tic()
+    new_bt.install()
+
+    dummy_type = self.portal.portal_types['Dummy Type']
+    self.assertEqual(
+      [rd.getRoleNameList() for rd in dummy_type.contentValues(portal_type='Role Information')],
+      [['Assignee']] * 20
+    )
+
+    # rebuild and check the output is same
+    bt.build()
+    self.tic()
+    export_dir = tempfile.mkdtemp()
+    self.addCleanup(shutil.rmtree, export_dir)
+    bt.export(path=export_dir, local=True)
+
+    with open(os.path.join(export_dir, 'PortalTypeRolesTemplateItem', 'Dummy%20Type.xml')) as f:
+      self.assertEqual(f.read(), role_content)
+
   def test_indexation_of_updated_path_item(self):
     """Tests indexation on updated paths item.
     They should keep their uid and still be available to catalog
diff --git a/product/ERP5/Document/BusinessTemplate.py b/product/ERP5/Document/BusinessTemplate.py
index ec047cb55a..7ce227aaa9 100644
--- a/product/ERP5/Document/BusinessTemplate.py
+++ b/product/ERP5/Document/BusinessTemplate.py
@@ -3462,7 +3462,15 @@ class PortalTypeRolesTemplateItem(BaseTemplateItem):
           type_role_dict[k] = v
         if 'id' in type_role_dict:
           type_role_list.append(type_role_dict)
-      type_role_list.sort(key=lambda x: (x.get('title'), x['object_id'],))
+
+      def sort_key(o):
+        try:
+          int_id = int(o['object_id'])
+        except ValueError:
+          # When the container does not use numerical ids
+          int_id = obj[o['object_id']].getCreationDate()
+        return (o.get('title'), int_id)
+      type_role_list.sort(key=sort_key)
 
   # Function to generate XML Code Manually
   def generateXml(self, path=None):
-- 
2.30.9