From 7a1e32eb59dd48a4074533faea1b460d743b05d1 Mon Sep 17 00:00:00 2001
From: Tristan Cavelier <tristan.cavelier@nexedi.com>
Date: Fri, 11 Dec 2015 10:10:39 +0000
Subject: [PATCH] erp5_forge: add tools for business template diff generation

---
 .../Base_formatDiffObjectListToHTML.xml       | 119 +++++++++++++++
 .../Base_formatDiffObjectListToText.xml       |  81 ++++++++++
 ...Base_getBusinessTemplateDiffObjectList.xml | 107 ++++++++++++++
 ...tInstalledBusinessTemplateModification.xml |  96 ++++++++++++
 ...ase_reportUpgraderBusinessTemplateDiff.xml |  25 ++--
 ...Template_getDetailedDiffWithZODBAsHTML.xml |  67 +++++++++
 ...nessTemplate_getDiffObjectListFromZODB.xml |  81 ++++++++++
 ...BusinessTemplate_getDiffWithZODBAsHTML.xml |  74 ++++++++++
 ...BusinessTemplate_getDiffWithZODBAsText.xml |  81 +---------
 .../erp5_toolbox/Folder_resolvePath.xml       | 139 ++++++++++++++++++
 10 files changed, 782 insertions(+), 88 deletions(-)
 create mode 100644 bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/Base_formatDiffObjectListToHTML.xml
 create mode 100644 bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/Base_formatDiffObjectListToText.xml
 create mode 100644 bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/Base_getBusinessTemplateDiffObjectList.xml
 create mode 100644 bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/Base_reportInstalledBusinessTemplateModification.xml
 create mode 100644 bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/BusinessTemplate_getDetailedDiffWithZODBAsHTML.xml
 create mode 100644 bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/BusinessTemplate_getDiffObjectListFromZODB.xml
 create mode 100644 bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/BusinessTemplate_getDiffWithZODBAsHTML.xml
 create mode 100644 bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/Folder_resolvePath.xml

diff --git a/bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/Base_formatDiffObjectListToHTML.xml b/bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/Base_formatDiffObjectListToHTML.xml
new file mode 100644
index 0000000000..735959021d
--- /dev/null
+++ b/bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/Base_formatDiffObjectListToHTML.xml
@@ -0,0 +1,119 @@
+<?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>_body</string> </key>
+            <value> <string encoding="cdata"><![CDATA[
+
+from Products.ERP5Type.DiffUtils import DiffFile\n
+from Products.PythonScripts.standard import html_quote\n
+\n
+def sortDiffObjectList(diff_object_list):\n
+  return sorted(diff_object_list, key=lambda x: (x.object_state, x.object_class, x.object_id))\n
+\n
+url_prefix = html_quote(context.getPortalObject().absolute_url())\n
+\n
+link_configuration = {}\n
+for key in ["Skin", "Workflow"]:\n
+  link_configuration[key] = \'<a href="\' + url_prefix + \'/%(object_id)s/manage_main">%(object_id)s</a>\' # ZMI\n
+link_configuration["PortalTypeWorkflowChain"] = \'<a href="\' + url_prefix + \'/portal_workflow/manage_main">%s</a>\' # ZMI\n
+for key in ["Path", "Category", "PortalType", "Module", "PropertySheet"]:\n
+  link_configuration[key] = \'<a href="\' + url_prefix + \'/%(object_id)s">%(object_id)s</a>\' # ERP5\n
+for key in ["PortalTypePropertySheet", "PortalTypeBaseCategory",\n
+            "PortalTypeBaseCategory", "PortalTypeAllowedContentType"]:\n
+  link_configuration[key] = \'<a href="\' + url_prefix + \'/portal_types/%(object_id)s">%(object_id)s</a>\' # ERP5\n
+link_configuration["Action"] = \'<a href="\' + url_prefix + \'/portal_types/%(object_id)s/../BaseType_viewAction">%(object_id)s</a>\' # ERP5\n
+\n
+print("<div>")\n
+for diff_object in sortDiffObjectList(diff_object_list):\n
+  if getattr(diff_object, "error", None) is not None:\n
+    print("<p>")\n
+    print("Error")\n
+    print("(%s) -" % html_quote(diff_object.object_class))\n
+    if diff_object.object_class in link_configuration:\n
+      print(link_configuration[diff_object.object_class] % {"object_id": html_quote(diff_object.object_id)})\n
+    else:\n
+      print(html_quote(diff_object.object_id))\n
+    print("</p>")\n
+    if detailed:\n
+      print("<p>")\n
+      print(html_quote(diff_object.error))\n
+      print("</p>")\n
+  else:\n
+    print("<p>")\n
+    print(html_quote(diff_object.object_state))\n
+    print("(%s) -" % html_quote(diff_object.object_class))\n
+    if diff_object.object_class in link_configuration:\n
+      print(link_configuration[diff_object.object_class] % {"object_id": html_quote(diff_object.object_id)})\n
+    else:\n
+      print(html_quote(diff_object.object_id))\n
+    print("</p>")\n
+    if detailed and getattr(diff_object, "data", None) is not None:\n
+      print("<div>")\n
+      print(DiffFile(diff_object.data).toHTML())\n
+      print("</div>")\n
+print("</div>")\n
+return printed\n
+
+
+]]></string> </value>
+        </item>
+        <item>
+            <key> <string>_params</string> </key>
+            <value> <string>diff_object_list, detailed=True</string> </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>Base_formatDiffObjectListToHTML</string> </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/Base_formatDiffObjectListToText.xml b/bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/Base_formatDiffObjectListToText.xml
new file mode 100644
index 0000000000..ca7bcea4e3
--- /dev/null
+++ b/bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/Base_formatDiffObjectListToText.xml
@@ -0,0 +1,81 @@
+<?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>_body</string> </key>
+            <value> <string>def sortDiffObjectList(diff_object_list):\n
+  return sorted(diff_object_list, key=lambda x: (x.object_state, x.object_class, x.object_id))\n
+\n
+for diff_object in sortDiffObjectList(diff_object_list):\n
+  print("%s (%s) - %s" % (diff_object.object_state, diff_object.object_class, diff_object.object_id))\n
+  if getattr(diff_object, "error", None) is not None:\n
+    if detailed:\n
+      print("  %s" % diff_object.error)\n
+    print("")\n
+  else:\n
+    if detailed and getattr(diff_object, "data", None) is not None:\n
+      print("%s" % diff_object.data.lstrip())\n
+    print("")\n
+\n
+return printed\n
+</string> </value>
+        </item>
+        <item>
+            <key> <string>_params</string> </key>
+            <value> <string>diff_object_list, detailed=True</string> </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>Base_formatDiffObjectListToText</string> </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/Base_getBusinessTemplateDiffObjectList.xml b/bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/Base_getBusinessTemplateDiffObjectList.xml
new file mode 100644
index 0000000000..1ca0c8a736
--- /dev/null
+++ b/bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/Base_getBusinessTemplateDiffObjectList.xml
@@ -0,0 +1,107 @@
+<?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>_body</string> </key>
+            <value> <string>"""\n
+Return a list of diff objects between two business templates.\n
+The building state of the business templates should be "built".\n
+\n
+Arguments:\n
+\n
+- ``bt1``: The first business template object to compare\n
+- ``bt2``: The second business template object to compare\n
+"""\n
+from Products.ERP5Type.Document import newTempBase\n
+from ZODB.POSException import ConflictError\n
+\n
+template_tool = context.getPortalObject().portal_templates\n
+\n
+assert bt1.getBuildingState() == "built"\n
+assert bt2.getBuildingState() == "built"\n
+\n
+modified_object_list = bt2.preinstall(check_dependencies=0, compare_to=bt1)\n
+keys = modified_object_list.keys()\n
+#keys.sort() # XXX don\'t care ?\n
+bt1_id = bt1.getId()\n
+bt2_id = bt2.getId()\n
+i = 0\n
+object_list = []\n
+for object_id in keys:\n
+  object_state, object_class = modified_object_list[object_id]\n
+  line = newTempBase(template_tool, \'tmp_install_%s\' % str(i)) # template_tool or context?\n
+  line.edit(object_id=object_id, object_state=object_state, object_class=object_class, bt1=bt1_id, bt2=bt2_id)\n
+  line.setUid(\'new_%s\' % object_id)\n
+  if detailed and object_state == "Modified":\n
+    try:\n
+      line.edit(data=bt2.diffObject(line, compare_with=bt1_id))\n
+    except ConflictError:\n
+      raise\n
+    except Exception as e:\n
+      if raise_on_diff_error:\n
+        raise\n
+      line.edit(error=repr(e))\n
+  object_list.append(line)\n
+  i += 1\n
+return object_list\n
+</string> </value>
+        </item>
+        <item>
+            <key> <string>_params</string> </key>
+            <value> <string>bt1, bt2, detailed=False, raise_on_diff_error=False</string> </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>Base_getBusinessTemplateDiffObjectList</string> </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/Base_reportInstalledBusinessTemplateModification.xml b/bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/Base_reportInstalledBusinessTemplateModification.xml
new file mode 100644
index 0000000000..78c446e64d
--- /dev/null
+++ b/bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/Base_reportInstalledBusinessTemplateModification.xml
@@ -0,0 +1,96 @@
+<?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>_body</string> </key>
+            <value> <string># It should be ran in an alarm thank to a property sheet constraint.\n
+from ZODB.POSException import ConflictError\n
+\n
+if fixit:\n
+  return ["Cannot fix automatically, please do it manually. (Or deactivate the constraint to force upgrade.)"]\n
+\n
+portal = context.getPortalObject()\n
+\n
+black_list = getattr(context, "Base_getInstalledBusinessTemplateBlackListForModificationList", lambda: ())()\n
+bt_list = [\n
+  x.getObject()\n
+  for x in portal.portal_catalog(\n
+    portal_type="Business Template",\n
+    installation_state="installed",\n
+  )\n
+  if x.getInstallationState() == "installed" and x.getTitle() not in black_list and x.getId() not in black_list\n
+]\n
+\n
+diff_list = []\n
+for bt in bt_list:\n
+  try:\n
+    diff = bt.BusinessTemplate_getDiffWithZODBAsText()\n
+    if diff:\n
+      diff_list += ["===== %s =====" % bt.getTitle()] + diff.splitlines()\n
+  except ConflictError:\n
+    raise\n
+  except Exception as e:\n
+    diff_list += ["===== %s =====" % bt.getTitle(), repr(e)]\n
+\n
+return diff_list\n
+</string> </value>
+        </item>
+        <item>
+            <key> <string>_params</string> </key>
+            <value> <string>fixit=False, **kw</string> </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>Base_reportInstalledBusinessTemplateModification</string> </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/Base_reportUpgraderBusinessTemplateDiff.xml b/bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/Base_reportUpgraderBusinessTemplateDiff.xml
index c1f7cd613b..c7cd71a7a7 100644
--- a/bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/Base_reportUpgraderBusinessTemplateDiff.xml
+++ b/bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/Base_reportUpgraderBusinessTemplateDiff.xml
@@ -50,9 +50,8 @@
         </item>
         <item>
             <key> <string>_body</string> </key>
-            <value> <string># This script comes with Base_gitDiffWithZODBAsText.\n
-# It should be ran in an alarm thank to a property sheet constraint.\n
-# Where does this script should be commited?\n
+            <value> <string># It should be ran in an alarm thank to a property sheet constraint.\n
+from ZODB.POSException import ConflictError\n
 from Products.ZSQLCatalog.SQLCatalog import SimpleQuery\n
 \n
 if fixit:\n
@@ -77,20 +76,16 @@ bt_list = [\n
   if bt.getInstallationState() == "installed"\n
 ]\n
 \n
-#bt_list = [\n
-#  x.getObject()\n
-#  for x in portal.portal_catalog(\n
-#    portal_type="Business Template",\n
-#    installation_state="installed",\n
-#  )\n
-#  if x.getInstallationState() == "installed"\n
-#]\n
-\n
 diff_list = []\n
 for bt in bt_list:\n
-  diff = bt.BusinessTemplate_getDiffWithZODBAsText()\n
-  if diff:\n
-    diff_list += ["===== %s =====" % bt.getTitle()] + diff.splitlines()\n
+  try:\n
+    diff = bt.BusinessTemplate_getDiffWithZODBAsText()\n
+    if diff:\n
+      diff_list += ["===== %s =====" % bt.getTitle()] + diff.splitlines()\n
+  except ConflictError:\n
+    raise\n
+  except Exception as e:\n
+    diff_list += ["===== %s =====" % bt.getTitle(), repr(e)]\n
 \n
 return diff_list\n
 </string> </value>
diff --git a/bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/BusinessTemplate_getDetailedDiffWithZODBAsHTML.xml b/bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/BusinessTemplate_getDetailedDiffWithZODBAsHTML.xml
new file mode 100644
index 0000000000..55d70d4cdf
--- /dev/null
+++ b/bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/BusinessTemplate_getDetailedDiffWithZODBAsHTML.xml
@@ -0,0 +1,67 @@
+<?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>_body</string> </key>
+            <value> <string>return context.BusinessTemplate_getDiffWithZODBAsHTML(REQUEST=REQUEST, detailed=True)\n
+</string> </value>
+        </item>
+        <item>
+            <key> <string>_params</string> </key>
+            <value> <string>REQUEST=None</string> </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>BusinessTemplate_getDetailedDiffWithZODBAsHTML</string> </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/BusinessTemplate_getDiffObjectListFromZODB.xml b/bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/BusinessTemplate_getDiffObjectListFromZODB.xml
new file mode 100644
index 0000000000..636d884810
--- /dev/null
+++ b/bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/BusinessTemplate_getDiffObjectListFromZODB.xml
@@ -0,0 +1,81 @@
+<?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>_body</string> </key>
+            <value> <string>if REQUEST is not None:\n
+  raise ValueError("This script cannot be called from the web")\n
+\n
+import string\n
+import random\n
+\n
+installed_bt_for_diff = context.Base_createCloneDocument(clone=1, batch_mode=1)\n
+\n
+random_str = \'\'.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(5))\n
+installed_bt_for_diff.setId("installed_bt_for_diff_%s" % random_str)\n
+installed_bt_for_diff.build()\n
+diff_object_list = context.Base_getBusinessTemplateDiffObjectList(context, installed_bt_for_diff, detailed=detailed)\n
+# XXX replace context.getPortalObject().portal_templates by something like context.getParentObject\n
+context.getPortalObject().portal_templates.manage_delObjects(ids=[installed_bt_for_diff.getId()])\n
+return diff_object_list\n
+</string> </value>
+        </item>
+        <item>
+            <key> <string>_params</string> </key>
+            <value> <string>REQUEST=None, detailed=False</string> </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>BusinessTemplate_getDiffObjectListFromZODB</string> </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/BusinessTemplate_getDiffWithZODBAsHTML.xml b/bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/BusinessTemplate_getDiffWithZODBAsHTML.xml
new file mode 100644
index 0000000000..fa871ec778
--- /dev/null
+++ b/bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/BusinessTemplate_getDiffWithZODBAsHTML.xml
@@ -0,0 +1,74 @@
+<?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>_body</string> </key>
+            <value> <string>def respond(v):\n
+  if REQUEST is None:\n
+    return v\n
+  REQUEST.RESPONSE.setHeader("Content-Type", "text/html")\n
+  REQUEST.RESPONSE.write(v)\n
+  raise ValueError("Abort Transaction")\n
+\n
+return respond(context.Base_formatDiffObjectListToHTML(context.BusinessTemplate_getDiffObjectListFromZODB(detailed=detailed)))\n
+</string> </value>
+        </item>
+        <item>
+            <key> <string>_params</string> </key>
+            <value> <string>REQUEST=None, detailed=False</string> </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>BusinessTemplate_getDiffWithZODBAsHTML</string> </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/BusinessTemplate_getDiffWithZODBAsText.xml b/bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/BusinessTemplate_getDiffWithZODBAsText.xml
index 2a0fe6037b..8c8581275d 100644
--- a/bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/BusinessTemplate_getDiffWithZODBAsText.xml
+++ b/bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/BusinessTemplate_getDiffWithZODBAsText.xml
@@ -50,83 +50,18 @@
         </item>
         <item>
             <key> <string>_body</string> </key>
-            <value> <string encoding="cdata"><![CDATA[
-
-# This script comes with Base_reportUpgraderBunisessTemplateDiff.\n
-# It can be ran individualy, of course.\n
-# Where does this script should be commited? erp5_core?\n
-# This diff script clones and builds it\'s own bt for comparision,\n
-# check with other script why this is not already done. Like on\n
-# TemplateTool_getDetailedDiff in erp5_core portal skin.\n
-from Products.ERP5Type.Document import newTempBase\n
-from ZODB.POSException import ConflictError\n
+            <value> <string>def respond(v):\n
+  if REQUEST is None:\n
+    return v\n
+  REQUEST.RESPONSE.write(v)\n
+  raise ValueError("Abort Transaction")\n
 \n
-template_tool = context.getPortalObject().portal_templates\n
-\n
-assert context.getBuildingState() == "built", "%s != \'built\'" % repr(context.getBuildingState())\n
-\n
-def getDiffObjectList(business_template, installed_bt_for_diff):\n
-  # business_template is assumed built and installed\n
-  modified_object_list = business_template.preinstall(check_dependencies=0, compare_to=installed_bt_for_diff)\n
-  keys = modified_object_list.keys()\n
-  #keys.sort() # XXX don\'t care ?\n
-  bt_id = business_template.getId()\n
-  i = 0\n
-  object_list = []\n
-  for object_id in keys:\n
-    object_state, object_class = modified_object_list[object_id]\n
-    line = newTempBase(template_tool, \'tmp_install_%s\' % (str(i))) # template_tool or context?\n
-    line.edit(object_id=object_id, object_state=object_state, object_class=object_class, bt1=bt_id, bt2=bt_id)\n
-    line.setUid(\'new_%s\' % object_id)\n
-    object_list.append(line)\n
-    i += 1\n
-  return object_list\n
-\n
-def getSortedDiffObjectList(business_template, installed_bt_for_diff):\n
-  return sorted(\n
-    sorted(\n
-      getDiffObjectList(business_template, installed_bt_for_diff),\n
-      key=lambda x: x.object_id\n
-    ),\n
-    key=lambda x: x.object_state\n
-  )\n
-\n
-\n
-try:\n
-  installed_bt_for_diff = context.Base_createCloneDocument(clone=1, batch_mode=1)\n
-except ConflictError:\n
-  raise\n
-except Exception as e:\n
-  print(e)\n
-  return printed #.split("\\n")\n
-\n
-name = "installed_bt_for_diff"\n
-try:\n
-  installed_bt_for_diff.setId(name)\n
-  installed_bt_for_diff.build()\n
-  for diff_object in getSortedDiffObjectList(context, installed_bt_for_diff):\n
-    print("%s (%s) -> %s" % (diff_object.object_state, diff_object.object_class, diff_object.object_id))\n
-    # Just uncomment these lines below to append diff to the text\n
-    #if diff_object.object_state != "Modified":\n
-    #  continue\n
-    #print(context.diffObject(diff_object, compare_with=name).lstrip())\n
-    #print("")\n
-except ConflictError:\n
-  raise\n
-except Exception as e:\n
-  print(e)\n
-finally:\n
-  template_tool.manage_delObjects(ids=[installed_bt_for_diff.getId()])\n
-\n
-#return "<pre>" + printed.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\\"", "&quot;") + "</pre>"\n
-return printed #.split("\\n")\n
-
-
-]]></string> </value>
+return respond(context.Base_formatDiffObjectListToText(context.BusinessTemplate_getDiffObjectListFromZODB(detailed=detailed)))\n
+</string> </value>
         </item>
         <item>
             <key> <string>_params</string> </key>
-            <value> <string></string> </value>
+            <value> <string>REQUEST=None, detailed=False</string> </value>
         </item>
         <item>
             <key> <string>id</string> </key>
diff --git a/bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/Folder_resolvePath.xml b/bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/Folder_resolvePath.xml
new file mode 100644
index 0000000000..d2b1ff2713
--- /dev/null
+++ b/bt5/erp5_forge/SkinTemplateItem/portal_skins/erp5_toolbox/Folder_resolvePath.xml
@@ -0,0 +1,139 @@
+<?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>_body</string> </key>
+            <value> <string encoding="cdata"><![CDATA[
+
+"""Usage example:\n
+\n
+- in a url ``<your erp5 url>/portal_skins/Folder_resolvePath?path=**``\n
+- in a script ``context.Folder_resolvePath("**")``\n
+    - for a business template path list ``portal.Folder_resolvePath(bt.getTemplatePathList())``\n
+\n
+Arguments:\n
+\n
+- ``path`` can be a string or a list (if ``path_list`` is not defined).\n
+- ``path_list`` must be a list (if ``path`` is not defined).\n
+- ``traverse`` if True, return object list instead of path list.\n
+- ``globbing`` if False, handle "*" and "**" as normal id.\n
+"""\n
+if path is None:\n
+  if path_list is None:\n
+    raise TypeError("`path` or `path_list` argument should be defined")\n
+elif isinstance(path, (list, tuple)):\n
+  path_list = path\n
+else:\n
+  path_list = [path]\n
+\n
+context_is_portal = context.getPortalObject() == context\n
+contextTraverse = context.restrictedTraverse\n
+\n
+resolved_list = []\n
+append = resolved_list.append\n
+\n
+if globbing:\n
+  for path in path_list:\n
+    if path == "*" or (context_is_portal and path == "**"): # acts like _resolvePath in Products.ERP5.Document.BusinessTemplate.PathTemplateItem\n
+      for sub_path, sub_obj in context.ZopeFind(context, search_sub=0):\n
+        if traverse:\n
+          append(sub_obj)\n
+        else:\n
+          append(sub_path)\n
+    elif path == "**":\n
+      for sub_path, sub_obj in context.ZopeFind(context, search_sub=1):\n
+        if traverse:\n
+          append(sub_obj)\n
+        else:\n
+          append(sub_path)\n
+    elif path.endswith("/**"):\n
+      parent_path = path[:-3]\n
+      obj = contextTraverse(parent_path)\n
+      for sub_path, sub_obj in obj.ZopeFind(obj, search_sub=1):\n
+        if traverse:\n
+          append(sub_obj)\n
+        else:\n
+          append(parent_path + "/" + sub_path)\n
+    elif path.endswith("/*"):\n
+      parent_path = path[:-2]\n
+      obj = contextTraverse(parent_path)\n
+      for sub_path, sub_obj in obj.ZopeFind(obj, search_sub=0):\n
+        if traverse:\n
+          append(sub_obj)\n
+        else:\n
+          append(parent_path + "/" + sub_path)\n
+    else:\n
+      if traverse:\n
+        append(contextTraverse(path))\n
+      else:\n
+        append(path)\n
+else:\n
+  for path in path_list:\n
+    if traverse:\n
+      append(contextTraverse(path))\n
+    else:\n
+      append(path)\n
+return resolved_list\n
+
+
+]]></string> </value>
+        </item>
+        <item>
+            <key> <string>_params</string> </key>
+            <value> <string>path=None, path_list=None, traverse=False, globbing=True</string> </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>Folder_resolvePath</string> </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+</ZopeData>
-- 
2.30.9