From e3d3e6ce71956fe2e2e7a90b8636395dc75acb02 Mon Sep 17 00:00:00 2001
From: Tatuya Kamada <tatuya@nexedi.com>
Date: Tue, 29 Aug 2017 11:22:00 +0200
Subject: [PATCH] BusinessTemplate: Reset portal_components on the fly while
 installing components

Before:

  self.portal_components.reset(force=True,
                               reset_portal_type_at_transaction_boundary=True)

After:

  self.portal_components.reset(force=True)

If the reset_portal_type_at_transaction_boundary is True, it does reset
component only once at the end of transaction.
(In detail, it reset component document module on the fly, and reset
 the component portal_types in the end of the transaction)

However, it is possible that those components are required in the middle of
the transaction while installing the business templates. For exmaple:
 - A method of a component is triggered while installing
 - A document component is required in a different business template,
   and those buisiness templates are installed inside a single transaction
   by upgrader.

Thus reset here on the fly.
---
 product/ERP5/Document/BusinessTemplate.py  |  11 +-
 product/ERP5/tests/testBusinessTemplate.py | 235 +++++++++++++++++++++
 2 files changed, 244 insertions(+), 2 deletions(-)

diff --git a/product/ERP5/Document/BusinessTemplate.py b/product/ERP5/Document/BusinessTemplate.py
index 3632ffa5a2..34478d47a7 100644
--- a/product/ERP5/Document/BusinessTemplate.py
+++ b/product/ERP5/Document/BusinessTemplate.py
@@ -4238,8 +4238,15 @@ class DocumentTemplateItem(FilesystemToZodbTemplateItem):
     """
     if self._is_already_migrated(self._objects.keys()):
       ObjectTemplateItem.install(self, context, **kw)
-      self.portal_components.reset(force=True,
-                                   reset_portal_type_at_transaction_boundary=True)
+      # Reset component on the fly, because it is possible that those
+      # components are required in the middle of the transaction. For example:
+      # - A method in a component is called while installing.
+      # - A document component is used in a different business template,
+      #   and those business templates are installed in a single transaction
+      #   by upgrader.
+      # This reset is called at most 3 times in one business template
+      # installation. (for Document, Test, Extension)
+      self.portal_components.reset(force=True)
     else:
       FilesystemDocumentTemplateItem.install(self, context, **kw)
 
diff --git a/product/ERP5/tests/testBusinessTemplate.py b/product/ERP5/tests/testBusinessTemplate.py
index 8ee842ecc6..4912a73573 100644
--- a/product/ERP5/tests/testBusinessTemplate.py
+++ b/product/ERP5/tests/testBusinessTemplate.py
@@ -7323,6 +7323,241 @@ class TestBusinessTemplate(BusinessTemplateMixin):
       self.assertEqual(True, method("aa/bb"))
       self.assertEqual(True, method("aa/bb/cc"))
 
+  def stepCreateDocumentComponentWhichTriggersAnOperationWhenSubDocumentIsAdded(
+        self, sequence=None, **kw):
+    from textwrap import dedent
+    # The source code of the component
+    document_data = dedent(
+    """
+    from Products.ERP5Type.Tool.BaseTool import BaseTool
+    from Products.ERP5Type.Core.Folder import Folder
+
+    class MyTool(BaseTool):
+      ''' my tool '''
+      id = 'portal_mytools'
+      meta_type = 'ERP5 MyTool'
+      portal_type = 'My Tool'
+
+      def _setOb(self, id, object):
+        '''
+          override
+        '''
+        Folder._setOb(self, id, object)
+    """)
+
+    component_id_prefix = DocumentComponent._getIdPrefix()
+    component_portal_type = DocumentComponent.portal_type
+    tool_type = 'My Tool'
+    tool_class = 'MyTool'
+    document_id = component_id_prefix + '.erp5.' + tool_class
+    component = self.portal.portal_components.newContent(
+      id=document_id,
+      version='erp5',
+      reference=tool_class,
+      text_content=document_data,
+      portal_type=component_portal_type)
+    component.validate()
+    sequence.edit(document_title=tool_class,
+                  document_id=document_id,
+                  document_data=document_data,
+                  tool_id='portal_mytools',
+                  tool_type=tool_type,
+                  tool_class=tool_class,
+                  type_allowed_content_type_list =('Python Script', ),
+                  template_property='template_document_id_list')
+    self.portal.portal_components.reset(force=True)
+
+  def stepCreateMyToolPortalType(self, sequence=None, **kw):
+    pt = self.getTypeTool()
+    kwdict = dict(type_class=sequence['tool_class'])
+    allowed_type_list = sequence['type_allowed_content_type_list']
+    if allowed_type_list:
+      kwdict['type_allowed_content_type_list'] = allowed_type_list
+    object_type = pt.newContent(sequence['tool_type'], 'Base Type', **kwdict)
+    self.assertTrue(object_type is not None)
+    sequence.edit(object_ptype_id=object_type.getId())
+
+  def stepAddMyToolToERP5Site(self, sequence=None, **kw):
+    from Products.ERP5 import ERP5Site
+    tool_id = sequence['tool_id']
+    ERP5Site.addERP5Tool(self.portal, tool_id, sequence['tool_type'])
+    sequence.edit(parent_document=self.portal[tool_id])
+
+  def stepSetSubdocumentAsMyScript(self, sequence=None, **kw):
+    sequence.edit(subdocument_id='my_script',
+                  subdocument_portal_type='Python Script')
+
+  def stepCreateSubDocument(self, sequence=None, **kw):
+    parent_document = sequence['parent_document']
+    self.assertTrue(parent_document != None)
+    subdocument_portal_type = sequence['subdocument_portal_type']
+    sub_document = parent_document.newContent(sequence['subdocument_id'],
+                                              portal_type=subdocument_portal_type,
+                                              body='# zzz')
+    sequence.edit(template_path_list=[sub_document.getRelativeUrl()])
+
+  def stepRemoveDocumentComponent(self, sequence=None, **kw):
+    document_id = sequence['document_id']
+    self.portal.portal_components.manage_delObjects(document_id)
+    self.assertEqual(getattr(self.portal.portal_components, document_id, None),
+                     None)
+
+  def stepRemoveSubDocument(self, sequence=None, **kw):
+    subdocument_id = sequence['subdocument_id']
+    parent_document = sequence['parent_document']
+    parent_document.manage_delObjects([subdocument_id])
+    self.assertEqual(getattr(parent_document, subdocument_id, None),
+                     None)
+
+  def stepAddDocumentComponentToBusinessTemplate(self, sequence=None, **kw):
+    sequence['current_bt'].setProperty(sequence['template_property'],
+                                       sequence['document_id'])
+
+  def stepAddTestComponentToBusinessTemplate(self, sequence=None, **kw):
+    sequence['current_bt'].setProperty('template_test_id_list',
+                                      ['test.erp5.testActivityTool'])
+
+  def stepAddMyToolPortalTypeToBusinessTemplate(self, sequence=None, **kw):
+    bt = sequence.get('current_bt', None)
+    self.assertTrue(bt is not None)
+    ptype_ids = []
+    ptype_ids.append(sequence.get('object_ptype_id', ''))
+    bt.edit(template_portal_type_id_list=ptype_ids)
+
+  def stepAddSubDocumentPathToBusinessTemplate(self, sequence=None, **kw):
+    bt = sequence.get('current_bt', None)
+    self.assertTrue(bt is not None)
+    bt.edit(template_path_list=sequence['template_path_list'])
+
+  def stepCheckDocumentComponentIsInstalled(self, sequence=None, **kw):
+    my_tool = getattr(self.portal.portal_components, sequence['document_id'],
+                      None)
+    self.assertNotEqual(my_tool, None)
+    self.assertEqual(my_tool.getPortalType(), 'Document Component')
+
+  def stepCheckParentDocumentIsExist(self, sequence=None, **kw):
+    parent_document = sequence['parent_document']
+    self.assertNotEqual(parent_document, None)
+    self.assertEqual(parent_document.getPortalType(), sequence['tool_type'])
+
+  def stepCheckSubDocumentIsInstalled(self, sequence=None, **kw):
+    parent_document = sequence['parent_document']
+    subdocument_id = sequence['subdocument_id']
+    subdocument = getattr(parent_document, subdocument_id, None)
+    self.assertNotEqual(subdocument, None)
+
+  def stepModifySubDocument(self, sequence=None, **kw):
+    parent_document = sequence['parent_document']
+    subdocument_id = sequence['subdocument_id']
+    parent_document[subdocument_id].edit(body='# yyy')
+
+  def stepRevertSubDocument(self, sequence=None, **kw):
+    parent_document = sequence['parent_document']
+    subdocument_id = sequence['subdocument_id']
+    parent_document[subdocument_id].edit(body='# zzz')
+
+  def stepCheckSubDocumentIsReverted(self, sequence=None, **kw):
+    parent_document = sequence['parent_document']
+    subdocument_id = sequence['subdocument_id']
+    body = parent_document[subdocument_id].getBody().rstrip()
+    self.assertEqual(body, '# zzz')
+
+  def stepCheckSubDocumentIsModified(self, sequence=None, **kw):
+    parent_document = sequence['parent_document']
+    subdocument_id = sequence['subdocument_id']
+    body = parent_document[subdocument_id].getBody().rstrip()
+    self.assertEqual(body, '# yyy')
+
+  def stepCreateDifferentBusinessTemplateForInstall(self, sequence=None, **kw):
+    pt = self.getTemplateTool()
+    template = pt.newContent(portal_type='Business Template')
+    self.assertTrue(template.getBuildingState() == 'draft')
+    self.assertTrue(template.getInstallationState() == 'not_installed')
+    template.edit(title='different template',
+                  version='1.0',
+                  description='bt for unit_test')
+    # set current_bt and import_bt for install
+    sequence.edit(current_bt=template)
+    sequence.edit(import_bt=template)
+
+  def stepSimulateToCreateNewRequest(self, sequence=None, **kw):
+    """
+     Remove the caches in _module_cache_set to simulate to create new REQUEST
+
+     Why we need this is because:
+     - ZODB Components relies on this cache to prevend gc of the component,
+     - Since Unit Test environment patches get_request() and publish(),
+       and everything run in a single thread, the REQUEST is not recreated
+       even if the request (transaction) is finished.
+     This removal imitates the behavior in a real ZOPE environment
+    """
+    from Products.ERP5Type.Globals import get_request
+    request_obj = get_request()
+    module_cache_set = getattr(request_obj, '_module_cache_set', None)
+    # delete the reference (decrement the reference count)
+    module_cache_set.clear()
+
+  def stepAddExtensionComponentToBusinessTemplate(self, sequence=None, **kw):
+    sequence['current_bt'].setProperty('template_extension_id_list',
+                                      ['extension.erp5.InventoryBrain'])
+
+
+  def testUpdateDocumentWhichIsInsideZODBDocumentFolder(self):
+    """
+      Test a case when the subobject of a ZODBComponent is updated as a path,
+      and at the same time, the component is triggered while the installation.
+    """
+    sequence_list = SequenceList()
+    sequence_string = """
+      CreateDocumentComponentWhichTriggersAnOperationWhenSubDocumentIsAdded
+      CreateMyToolPortalType
+      AddMyToolToERP5Site
+      CreateNewBusinessTemplate
+      UseExportBusinessTemplate
+      AddDocumentComponentToBusinessTemplate
+      AddMyToolPortalTypeToBusinessTemplate
+      FillPortalTypesFields
+      Tic
+      BuildBusinessTemplate
+      SaveBusinessTemplate
+      Tic
+      ImportBusinessTemplate
+      UseImportBusinessTemplate
+      InstallWithoutForceBusinessTemplate
+      Tic
+      SetSubdocumentAsMyScript
+      CreateSubDocument
+      Tic
+      CreateDifferentBusinessTemplateForInstall
+      AddSubDocumentPathToBusinessTemplate
+      AddTestComponentToBusinessTemplate
+      BuildBusinessTemplate
+      SaveBusinessTemplate
+      InstallWithoutForceBusinessTemplate
+      Tic
+      ModifySubDocument
+      SimulateToCreateNewRequest
+      Tic
+      ImportBusinessTemplate
+      UseImportBusinessTemplate
+      BuildBusinessTemplate
+      Tic
+      InstallWithoutForceBusinessTemplate
+      Tic
+      RevertSubDocument
+      SimulateToCreateNewRequest
+      Tic
+      ImportBusinessTemplate
+      UseImportBusinessTemplate
+      AddExtensionComponentToBusinessTemplate
+      BuildBusinessTemplate
+      InstallWithoutForceBusinessTemplate
+      Tic
+      """
+    sequence_list.addSequenceString(sequence_string)
+    sequence_list.play(self)
+
+
 from Products.ERP5Type.Core.DocumentComponent import DocumentComponent
 
 class TestDocumentTemplateItem(BusinessTemplateMixin):
-- 
2.30.9