Commit 0db17646 authored by Ayush Tiwari's avatar Ayush Tiwari Committed by Ayush Tiwari

Business Manager: Introduction of Business Manager class and changes in...

Business Manager: Introduction of Business Manager class and changes in template tool according to it
parent 16a9f0c0
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (c) 2017 Nexedi SARL and Contributors. All Rights Reserved.
# Ayush-Tiwari <ayush.tiwari@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################
import gc
import os
import posixpath
import transaction
import imghdr
import tarfile
import time
import hashlib
import fnmatch
import re
import threading
import pprint
from copy import deepcopy
from collections import defaultdict
from cStringIO import StringIO
from OFS.Image import Pdata
from lxml.etree import parse
from urllib import quote, unquote
from OFS import SimpleItem, XMLExportImport
from datetime import datetime
from itertools import chain
from operator import attrgetter
from persistent.list import PersistentList
from AccessControl import ClassSecurityInfo, Unauthorized, getSecurityManager
from Acquisition import Implicit, aq_base, aq_inner, aq_parent
from zLOG import LOG, INFO, WARNING
from Products.ERP5Type.XMLObject import XMLObject
from Products.ERP5Type.Core.Folder import Folder
from Products.CMFCore.utils import getToolByName
from Products.PythonScripts.PythonScript import PythonScript
from Products.ERP5Type.dynamic.lazy_class import ERP5BaseBroken
from Products.ERP5Type.Globals import Persistent, PersistentMapping
from Products.ERP5Type import Permissions, PropertySheet, interfaces
from Products.ERP5Type.Globals import InitializeClass
from Products.ERP5Type.TransactionalVariable import getTransactionalVariable
from Products.ERP5Type.patches.ppml import importXML
from Products.ERP5Type.Accessor.Constant import PropertyGetter as ConstantGetter
customImporters = {
XMLExportImport.magic: importXML,
}
CACHE_DATABASE_PATH = None
try:
if int(os.getenv('ERP5_BT5_CACHE', 0)):
from App.config import getConfiguration
import gdbm
instancehome = getConfiguration().instancehome
CACHE_DATABASE_PATH = os.path.join(instancehome, 'bt5cache.db')
except TypeError:
pass
cache_database = threading.local()
_MARKER = []
SEPARATELY_EXPORTED_PROPERTY_DICT = {
# For objects whose class name is 'class_name', the 'property_name'
# attribute is removed from the XML export, and the value is exported in a
# separate file, with extension specified by 'extension'.
# 'extension' must be None for auto-detection.
#
# class_name: (extension, unicode_data, property_name),
"Document Component": ("py", 0, "text_content"),
"DTMLDocument": (None, 0, "raw"),
"DTMLMethod": (None, 0, "raw"),
"Extension Component": ("py", 0, "text_content"),
"File": (None, 0, "data"),
"Image": (None, 0, "data"),
"OOoTemplate": ("oot", 1, "_text"),
"PDF": ("pdf", 0, "data"),
"PDFForm": ("pdf", 0, "data"),
"Python Script": ("py", 0, "_body"),
"PythonScript": ("py", 0, "_body"),
"Spreadsheet": (None, 0, "data"),
"SQL": ("sql", 0, "src"),
"SQL Method": ("sql", 0, "src"),
"Test Component": ("py", 0, "text_content"),
"Test Page": (None, 0, "text_content"),
"Web Page": (None, 0, "text_content"),
"Web Script": (None, 0, "text_content"),
"Web Style": (None, 0, "text_content"),
"ZopePageTemplate": ("zpt", 1, "_text"),
}
def _delObjectWithoutHook(obj, id):
"""OFS.ObjectManager._delObject without calling manage_beforeDelete."""
ob = obj._getOb(id)
if obj._objects:
obj._objects = tuple([i for i in obj._objects if i['id'] != id])
obj._delOb(id)
try:
ob._v__object_deleted__ = 1
except:
pass
def _recursiveRemoveUid(obj):
"""Recusivly set uid to None, to prevent (un)indexing.
This is used to prevent unindexing real objects when we delete subobjects on
a copy of this object.
"""
if getattr(aq_base(obj), 'uid', _MARKER) is not _MARKER:
obj.uid = None
# Make all objects and sub-object un-indexable
# XXX: Should be moved into another function or change the name and desc
# of this function
obj.isIndexable = ConstantGetter('isIndexable', value=False)
for subobj in obj.objectValues():
_recursiveRemoveUid(subobj)
# New BusinessItem addition function
def manage_addBusinessItem(self, item_path='', item_sign=1, item_layer=0, *args, **kw):
# Create BusinessItem object container
c = BusinessItem(item_path, item_sign, item_layer)
return c
# New BusinessPropertyItem addition function
def manage_addBusinessPropertyItem(self, item_path='', item_sign=1, item_layer=0, *args, **kw):
# Create BusinessPathItem object container
c = BusinessPropertyItem(item_path, item_sign, item_layer)
return c
# New BusinessPatchItem addition function
def manage_addBusinessPatchItem(self, item_path='', item_sign=1, item_layer=0, *args, **kw):
# Create BusinessPatchItem object container
c = BusinessPatchItem(item_path, item_sign, item_layer)
return c
class BusinessManager(Folder):
"""Business Manager is responsible for saving objects and properties in
an ERP5Site. Everything will be saved just via path"""
meta_type = 'ERP5 Business Manager'
portal_type = 'Business Manager'
add_permission = Permissions.AddPortalContent
template_format_version = 3
# Factory Type Information
factory_type_information = \
{ 'id' : portal_type
, 'meta_type' : meta_type
, 'icon' : 'file_icon.gif'
, 'product' : 'ERP5Type'
, 'factory' : ''
, 'type_class' : 'BusinessManager'
, 'immediate_view' : 'BusinessManager_view'
, 'allow_discussion' : 1
, 'allowed_content_types': ('Business Item',
'Business Property Item',
)
, 'filter_content_types' : 1
}
# Declarative security
security = ClassSecurityInfo()
security.declareObjectProtected(Permissions.AccessContentsInformation)
# Declarative properties
property_sheets = (
PropertySheet.Base,
PropertySheet.XMLObject,
PropertySheet.SimpleItem,
PropertySheet.CategoryCore,
PropertySheet.Version,
PropertySheet.BusinessManager,
)
def getShortRevision(self):
return None
def getVersion(self):
# Override the getter because currently we don't migrate the version, and
# this is used to find missing dependency list
return '5.4.7'
security.declareProtected(Permissions.AccessContentsInformation,
'getBuildingState')
def getBuildingState(self, default=None, id_only=1):
"""
Returns the current state in building
"""
portal_workflow = getToolByName(self, 'portal_workflow')
wf = portal_workflow.getWorkflowById(
'business_manager_building_workflow')
return wf._getWorkflowStateOf(self, id_only=id_only )
security.declareProtected(Permissions.AccessContentsInformation,
'getInstallationState')
def getInstallationState(self, default=None, id_only=1):
"""
Returns the current state in installation
"""
portal = self.getPortalObject()
portal_workflow = portal.portal_workflow
wf = portal_workflow.getWorkflowById(
'business_manager_installation_workflow')
return wf._getWorkflowStateOf(self, id_only=id_only )
def changeBuildingStatetoModified(self):
"""
Change building_state to 'modified'. This is needed specifically as we want
to update the building state even if we change any sub-objects(Business Item
or Business Property Item) of Business Manager.
"""
portal_workflow = self.getPortalObject().portal_workflow
wf = portal_workflow._getOb('business_manager_building_workflow')
wf._executeMetaTransition(self, 'modified')
def applytoERP5(self, DB):
"""Apply the flattened/reduced Business Manager to the DB"""
portal = self.getPortalObject()
pass
def _clean(self):
"""
Clean built information.
Remove all the built sub-objects from Business Item or Business Property
Item.
"""
for item in self.objectValues():
if item.getPortalType() == 'Business Item':
# Delete the sub-object
id_list = [l for l in item.objectIds()]
if id_list:
item.manage_delObjects(ids=id_list)
elif item.getPortalType() == 'Business Property Item':
# Add empty property item_property_name, value and type
# XXX: Shouldn't we check if the properties exist and then just delete
# them ?
item.setProperty('item_property_name', '')
item.setProperty('item_property_value', '')
item.setProperty('item_property_type', '')
security.declareProtected(Permissions.ManagePortal, 'preinstall')
def preinstall(self, check_dependencies=1, **kw):
"""
Preinstall for Business Manager comapres the installation state and returns
the changes in a manner which can keep up compatibilty with the view we use
in Business Template installation.
This function calls `portal_templates.updateInstallationState` to get the
change_list.
We don't care of check_dependencies here as this all would be taken care by
`udpateInstallationState` iteself.
"""
change_list = self.aq_parent.compareInstallationState([self])
modified_object_list = {}
for path in change_list:
type_name = 'Business Item'
if '#' in path[0]:
type_name = 'Business Property Item'
modified_object_list[path[0]] = [path[1], type_name]
return modified_object_list
security.declareProtected(Permissions.ManagePortal, 'clean')
clean = _clean
def _setTitle(self, value):
"""
Override required due to bootstrap
"""
self.title = value
def getPathItemList(self):
return self.objectValues()
# XXX: Change into property
security.declareProtected(Permissions.ManagePortal, 'getTemplateFormatVersion')
def getTemplateFormatVersion(self):
return self.template_format_version
# XXX: Change into property
def _setTemplateFormatVersion(self, value):
self.template_format_version = int(value)
def propertyMap(self):
prop_map = super(BusinessManager, self).propertyMap()
final_prop_map = prop_map+self._properties
return final_prop_map
def export(self, path=None, **kw):
"""
Export the object as zexp file
"""
if not self.getBuildingState() == 'built':
raise ValueError, 'Manager not built properly'
f = StringIO()
self._p_jar.exportFile(self._p_oid, f)
# XXX: Improve naming
name = self.getTitle()
name = posixpath.join(path, name)
# XXX required due to overuse of os.path
name = name.replace('\\', '/').replace(':', '/')
name = quote(name + '.zexp')
obj_path = name.replace('/', os.sep)
f.seek(0)
obj = f.read()
object_path = os.path.join(path, obj_path)
path = os.path.dirname(object_path)
os.path.exists(path) or os.makedirs(path)
f = open(object_path, 'wb')
try:
f.write(obj)
finally:
f.close()
security.declareProtected(Permissions.ManagePortal, 'importFile')
def importFile(self, path, connection=None):
"""
Import Business Manager object and all attribute to current BM itself
"""
if not connection:
connection = self.aq_parent._p_jar
file = open(path, 'rb')
imported_manager = connection.importFile(file)
self.title = imported_manager.title
for obj in imported_manager.objectValues():
delattr(obj, '__ac_local_roles__')
# XXX: Donot merge this, needed just for migrated erp5_core
try:
self._setObject(obj.id, aq_base(obj))
except Exception:
pass
obj.isIndexable = ConstantGetter('isIndexable', value=False)
def __add__(self, other):
"""
Adds the Business Item objects for the given Business Manager objects
"""
# XXX: Still to define
return self
__radd__ = __add__
def __sub__(self, other):
"""
Override subtract to find difference b/w the values in different cases.
"""
# Create the sha list for all path item list available in current object
sha_list = [item.sha for item in self._path_item_list]
# Reverse the sign of Business Item objects for the old Business Manager
# Trying comparing/subtracting ZODB with old installed object
for path_item in other._path_item_list:
if path_item.sha in sha_list:
self._path_item_list = [item for item
in self._path_item_list
if item.sha != path_item.sha]
else:
path_item.sign = -1
self._path_item_list.append(path_item)
return self
__rsub__ = __sub__
security.declareProtected(Permissions.ManagePortal, 'storeTemplateData')
def storeTemplateData(self, isBuild=False, **kw):
"""
Store data for objects in the ERP5.
Create Business Item sub-objects after resolving the paths. Also, add
layers to all Business Item objects
"""
portal = self.getPortalObject()
LOG('Business Manager', INFO, 'Storing Manager Data')
to_delete_id_list = []
for item in self.objectValues():
# Only try to resolve the Business Item objects
if item.getPortalType() != 'Business Item':
continue
item_path = item.getProperty('item_path')
# Resolve the path and update sub-objects lists
path_list = self._resolvePath(portal, [], item_path.split('/'))
if len(path_list) == 1 and path_list[0] == item_path:
continue
else:
item_sign = item.getProperty('item_sign')
item_layer = item.getProperty('item_layer')
# Create new Business Item objects with path in path_list and sign and
# layer same as that of the path used for resolving to new paths.
for path in path_list:
path_item = self.newContent(portal_type='Business Item')
path_item.edit(
item_path=path,
item_sign=item_sign,
item_layer=item_layer
)
# Add Id of BusinessItem to be deleted as we do already have resolved
# path and new sub-objects based on resolved paths
to_delete_id_list.append(item.getId())
# Now, delete the original Business Item(s) sub-object as we do have
# Business Item created from resolved paths
self.manage_delObjects(ids=to_delete_id_list)
def _resolvePath(self, folder, relative_url_list, id_list):
"""
For Business Manager, we expect to resolve the path incase we face
paths which expect to include sub-objects.
For example: 'portal_catalog/erp5_mysql_innodb/**' should only consider
the sub-objects of the object mentioned, and create separate BusinessItem
objects for all of them.
This method calls itself recursively.
The folder is the current object which contains sub-objects.
The list of ids are path components. If the list is empty,
the current folder is valid.
"""
if len(id_list) == 0:
return ['/'.join(relative_url_list)]
id = id_list[0]
if re.search('[\*\?\[\]]', id) is None:
# If the id has no meta character, do not have to check all objects.
obj = folder._getOb(id, None)
if obj is None:
raise AttributeError, "Could not resolve '%s' during business template processing." % id
return self._resolvePath(obj, relative_url_list + [id], id_list[1:])
path_list = []
for object_id in fnmatch.filter(folder.objectIds(), id):
if object_id != "":
path_list.extend(self._resolvePath(
folder._getOb(object_id),
relative_url_list + [object_id], id_list[1:]))
return path_list
def getPathList(self):
path_list = []
for item in self.objectValues():
path_list.append(item.getProperty('item_path'))
return path_list
def getPathItemDict(self):
path_item_dict = {}
# TODO: Handle error for BM with multiple items at same path
for item in self.objectValues():
path_item_dict[item.getProperty('item_path')] = item
return path_item_dict
def getBusinessItemByPath(self, path):
path_item_dict = self.getPathItemDict()
try:
return path_item_dict[path]
except KeyError:
return
def build(self, no_action=False, **kw):
"""Creates new values for business item from the values from
OFS Database"""
LOG('Business Manager', INFO, 'Building Business Manager')
removable_sub_object_path_list = kw.get('removable_sub_object_path', [])
removable_property_dict = kw.get('removable_property', {})
# Now, we need to put a check here for returning whih objects should be
# updated during rebuild of BM
checkNeeded = kw.get('checkNeeded', False)
# Build all paths if there is no check required(i.e, its a new build action)
if not checkNeeded:
if not no_action:
self.storeTemplateData(isBuild=True, **kw)
for path_item in self.objectValues():
kwargs = {}
item_path = path_item.getProperty('item_path')
kwargs['removable_property_list'] = removable_property_dict.get(item_path, [])
kwargs['remove_sub_objects'] = item_path in removable_sub_object_path_list
path_item.build(self, **kwargs)
return self
else:
item_path_list = kw.get('item_path_list', [])
if item_path_list:
for path in item_path_list:
item = self.getBusinessItemByPath(path)
item.build(self)
return self
def flattenBusinessManager(self):
"""
Flattening a reduced Business Manager with two path p1 and p2 where p1 <> p2:
flatten([(p1, s1, l1, v1), (p2, s2, l2, v2)]) = [(p1, s1, 0, v1), (p2, s2, 0, v2)]
A reduced Business Manager BT is said to be flattened if and only if:
flatten(BT) = BT
"""
pass
def reduceBusinessManager(self):
"""
Reduction is a function that takes a Business Manager as input and returns
a smaller Business Manager by taking out values with lower priority layers.
After taking out BusinessItem(s) with lower priority layer, we also go
through arithmetic in case there are multiple number of BI at the higher layer
Two path on different layer are reduced as a single path with the highest layer:
If l1 > l2,
reduce([(p, s, l1, (a, b, c)), (p, s, l2, (d, e))]) = [(p, s, l1, merge(a, b, c))]
A Business Manager BT is said to be reduced if and only if:
reduce(BT) = BT
"""
path_list = self.getPathList()
reduced_path_item_list = []
# We separate the path list in the ones which are repeated and the ones
# which are unique for the installation
seen_path_list = set()
unique_path_list = [x for x
in path_list
if x not in seen_path_list
and not seen_path_list.add(x)]
# Create an extra dict for values on path which are repeated in the path list
seen_path_dict = {path: [] for path in seen_path_list}
for item in self.objectValues():
if item.getProperty('item_path') in seen_path_list:
# In case the path is repeated keep the path_item in a separate dict
# for further arithmetic
seen_path_dict[item.getProperty('item_path')].append(item)
else:
# If the path is unique, add them in the list of reduced Business Item
reduced_path_item_list.append(item)
# Reduce the values and get the merged result out of it
for path, path_item_list in seen_path_dict.items():
# Create separate list of list items with highest priority
higest_priority_layer = max(path_item_list, key=attrgetter('item_layer')).item_layer
prioritized_path_item = [path_item for path_item
in path_item_list
if path_item.item_layer == higest_priority_layer]
# Separate the positive and negative sign path_item
if len(prioritized_path_item) > 1:
path_item_list_add = [item for item
in prioritized_path_item
if item.getProperty('item_sign') > 0]
path_item_list_subtract = [item for item
in prioritized_path_item
if item.getProperty('item_sign') < 0]
if path_item_list_add:
combined_added_path_item = reduce(lambda x, y: x+y, path_item_list_add)
if combined_added_path_item.getPortalType() == 'Business Item':
added_value = combined_added_path_item.objectValues()[0]
elif combined_added_path_item.getPortalType() == 'Business Property Item':
added_value = combined_added_path_item.getProperty('item_property_value')
reduced_path_item_list.append(combined_added_path_item)
if path_item_list_subtract:
combined_subtracted_path_item = reduce(lambda x, y: x+y, path_item_list_subtract)
if combined_subtracted_path_item.getPortalType() == 'Business Item':
added_value = combined_subtracted_path_item.objectValues()[0]
elif combined_subtracted_path_item.getPortalType() == 'Business Property Item':
added_value = combined_subtracted_path_item.getProperty('item_property_value')
reduced_path_item_list.append(combined_subtracted_path_item)
# XXX: For now, we just care about added items and append them in reduced
# path list.
#if added_value != subtracted_value:
# Append the arithmetically combined path_item objects in the final
# reduced list after removing the intersection
# added_value, subtracted_value = \
# self._simplifyValueIntersection(added_value, subtracted_value)
# combined_added_path_item.value = added_value
# combined_subtracted_path_item.value = subtracted_value
# Append the path_item to the final reduced path_item_list after
# doing required arithmetic on it. Make sure to first append
# subtracted item because while installation, we need to first
# uninstall the old object and then install new object at same path
# reduced_path_item_list.append(combined_subtracted_path_item)
# reduced_path_item_list.append(combined_added_path_item)
else:
reduced_path_item_list.append(prioritized_path_item[0])
id_list = [l for l in self.objectIds()]
for l in id_list:
try:
self._delObject(l)
except Exception:
# XXX: REMOVE/RECHEK BEFORE MERGE
# The reason for doing this horrible workaround is to delete object
# for 'portal_memcached/persistent_memcached_plugin' as it is not getting
# deleted in 1st attempt with failure :
# AttributeError: 'NoneType' object has no attribute 'getUid'
self._delObject(l)
for item in reduced_path_item_list:
item.isIndexable = ConstantGetter('isIndexable', value=False)
new_id = self.generateNewId()
self._setObject(new_id, aq_base(item),
suppress_events=True)
def _simplifyValueIntersection(self, added_value, subtracted_value):
"""
Returns values for the Business Item having same path and layer after
removing the intersection of the values
Parameters:
added_value - Value for the Business Item having sign = +1
subtracted_value - Value for Busienss Item having sign = -1
"""
built_in_number_type = (int, long, float, complex)
built_in_container_type = (tuple, list, dict, set)
built_in_type_list = built_in_number_type + built_in_container_type
# For ERP5 objects, we should return the added and subtracted values as it is
if type(added_value).__name__ not in built_in_type_list and \
type(subtracted_value).__name__ not in built_in_type_list:
return added_value, subtracted_value
# For all the values of container type, we remove the intersection
added_value = [x for x in added_value if x not in subtracted_value]
subtracted_value = [x for x in subtracted_value if x not in added_value]
return added_value, subtracted_value
def mergeBusinessPatchItem(self, item1, item2):
"""
Merge two BusinessPatchItem at same layer and same sign.
XXX: This function shouldn't be used at reduction, rather than during
updating the installation_state, because we need to know all the states to
be able to find out which patch needs to be used.
Cases:
1. Shouldn't matter if both have same new_value as we give preference to
the final result and try to remove conflict as much as possible.
2. If both have different old_value and different new_value, merge should
return both of them and let the updateInstallationState process decide
which one to apply and if needed, raise a conflict
"""
pass
class BusinessItem(XMLObject):
"""Saves the path and values for objects, properties, etc, the
attributes for a path configuration being:
- item_path (similar to an xpath expression)
Examples of path :
portal_type/Person
portal_type/Person#title
portal_type/Person#property_sheet?ancestor=DublinCore
portal_type/Person#property_sheet?position=2
- item_sign (+1/-1)
- item_layer (0, 1, 2, 3, etc.)
- item_value (a set of pickable value in python)"""
add_permission = Permissions.AddPortalContent
# Declarative security
security = ClassSecurityInfo()
security.declareObjectProtected(Permissions.AccessContentsInformation)
constructors = (manage_addBusinessItem,)
portal_type = 'Business Item'
meta_type = 'Business Item'
icon = None
isProperty = False
isIndexable = False
def _edit(self, item_path='', item_sign=1, item_layer=0, *args, **kw):
"""
Overriden function so that we can update attributes for BusinessItem objects
"""
return super(BusinessItem, self)._edit(item_path=item_path,
item_sign=item_sign,
item_layer=item_layer,
**kw)
def build(self, context, **kw):
"""
Extract value for the given path from the OFS
Three different situations to extract value:
1. For paths which point directly to an object in OFS
2. For paths which point to multiple objects inside a folder
3. For paths which point to property of an object in OFS : In this case,
we can have URL delimiters like ?, #, = in the path
"""
LOG('Business Manager', INFO, 'Building Business Item')
# Remove the old sub-objects if exisiting before building
id_list = [l for l in self.objectIds()]
if id_list:
self.manage_delObjects(ids=id_list)
p = context.getPortalObject()
path = self.getProperty('item_path')
obj = p.unrestrictedTraverse(path)
obj = obj._getCopy(context)
# We should remove the extra properties of object so that there
# shouldn't be redundancy of the proeprties
removable_property_list = kw.get('removable_property_list', [])
# We should also add extra parameter to remove sub-objects by removing
# `_tree` for any erp5 object. This way we can have control over adding
# sub-objects as new Business Item objects
remove_sub_objects = kw.get('remove_sub_objects', False)
if remove_sub_objects:
removable_property_list.append('_tree')
keep_workflow_history = False
# For portal_components object, we need validation_history
if self.getProperty('item_path').startswith('portal_components'):
keep_workflow_history = True
obj = self.removeProperties(obj,
1,
properties=removable_property_list,
keep_workflow_history=keep_workflow_history,
)
obj = obj.__of__(context)
# XXX: '_recursiveRemoveUid' is not working as expected
_recursiveRemoveUid(obj)
obj = aq_base(obj)
obj.isIndexable = ConstantGetter('isIndexable', value=False)
new_id = self.generateNewId()
self._setObject(new_id, obj, suppress_events=True)
def _resolvePath(self, folder, relative_url_list, id_list):
"""
We go through 3 types of paths:
1. General path we find in erp5 for objects
Ex: portal_type/Person
In this case, we import/export the object on the path
2. Path where we consider saving sub-objects also, in that case we create
new BusinessItem for those objects
Ex: portal_catalog/erp5_mysql_innodb/**
This should create BI for the catalog methods sub-objects present in the
erp5_catalog.
This method calls itself recursively.
The folder is the current object which contains sub-objects.
The list of ids are path components. If the list is empty,
the current folder is valid.
"""
if len(id_list) == 0:
return ['/'.join(relative_url_list)]
id = id_list[0]
if re.search('[\*\?\[\]]', id) is None:
# If the id has no meta character, do not have to check all objects.
obj = folder._getOb(id, None)
if obj is None:
raise AttributeError, "Could not resolve '%s' during BusinessItem processing." % id
return self._resolvePath(obj, relative_url_list + [id], id_list[1:])
path_list = []
for object_id in fnmatch.filter(folder.objectIds(), id):
if object_id != "":
path_list.extend(self._resolvePath(
folder._getOb(object_id),
relative_url_list + [object_id], id_list[1:]))
return path_list
def install(self, context, *args):
"""
Set the value to the defined path.
"""
# In case the path denotes property, we create separate object for
# ObjectTemplateItem and handle the installation there.
try:
portal = context.getPortalObject()
except AttributeError:
# This is a possibility during bootstrap where the context is not an
# erp5 object but a dynamic class
if args:
portal = args[0]
else:
raise AttributeError('No portal object found')
path = self.getProperty('item_path')
path_list = path.split('/')
container_path = path_list[:-1]
object_id = path_list[-1]
try:
container = self.unrestrictedResolveValue(portal, container_path)
except KeyError:
# parent object can be set to nothing, in this case just go on
container_url = '/'.join(container_path)
old_obj = container._getOb(object_id, None)
# delete the old object before installing a new object
if old_obj:
container._delObject(object_id)
# Create a new object only if sign is +1
# If sign is +1, set the new object on the container
if int(self.getProperty('item_sign')) == 1:
# install object
obj = self.objectValues()[0]
obj = obj._getCopy(container)
# Update the type_provider_list if needed
# XXX: Find a way to fiter and do this check only for tool, so that we
# don't lose on perfomance in checking this everytime
if interfaces.ITypeProvider.providedBy(obj):
type_container_id = obj.id
types_tool = portal.portal_types
if type_container_id not in types_tool.type_provider_list:
types_tool.type_provider_list = tuple(types_tool.type_provider_list) + \
(type_container_id,)
# Before making `obj` a sub-object of `container`, we should the acquired
# roles on obj
obj.isIndexable = ConstantGetter('isIndexable', value=False)
delattr(obj, '__ac_local_roles__')
container._setObject(object_id, obj, suppress_events=True)
obj = container._getOb(object_id)
skin_tool = portal.portal_skins
if obj.aq_parent.meta_type == 'CMF Skins Tool':
registerSkinFolder(skin_tool, obj)
def unrestrictedResolveValue(self, context=None, path='', default=_MARKER,
restricted=0):
"""
Get the value without checking the security.
This method does not acquire the parent.
"""
if isinstance(path, basestring):
stack = path.split('/')
else:
stack = list(path)
stack.reverse()
if stack:
if context is None:
portal = aq_inner(self.getPortalObject())
container = portal
else:
container = context
if restricted:
validate = getSecurityManager().validate
while stack:
key = stack.pop()
try:
value = container[key]
except KeyError:
LOG('BusinessManager', WARNING,
'Could not access object %s' % (path,))
if default is _MARKER:
raise
return default
if restricted:
try:
if not validate(container, container, key, value):
raise Unauthorized('unauthorized access to element %s' % key)
except Unauthorized:
LOG('BusinessManager', WARNING,
'access to %s is forbidden' % (path,))
if default is _MARKER:
raise
return default
container = value
return value
else:
return context
def __add__(self, other):
"""
Add the values from the path when the path is same for 2 objects
"""
if self.getProperty('item_path') != other.getProperty('item_path'):
raise ValueError, "BusinessItem are incommensurable, have different path"
elif self.getProperty('item_sign') != other.getProperty('item_sign'):
raise ValueError, "BusinessItem are incommensurable, have different sign"
else:
new_value = self._mergeValue(value_list=[self.objectValues()[0], other.objectValues()[0]])
# Remove old objects if it exists
old_id_list = [l for l in self.objectIds()]
if old_id_list:
self.manage_delObjects(ids=old_id_list)
# Add the new sub-object
self._setObject(new_value.id, new_value)
return self
__radd__ = __add__
def _mergeValue(self, value_list):
"""
Merge value in value list
merge(a, b, c) : A monotonic commutative function that depends on the
type of a, b and c:
if a, b and c are sets, merge = union
if a, b and c are lists, merge = ordered concatenation
if a, b and c are objects, merge = the object created the last
else merge = MAX
"""
builtin_number_type = (int, long, float, complex)
# Now, consider the type of both values
if all(isinstance(x, builtin_number_type) for x in value_list):
merged_value = max(value_list)
elif all(isinstance(x, set) for x in value_list):
merged_value = set(chain.from_iterable(value_list))
elif all(isinstance(x, list) for x in value_list):
merged_value = list(set(chain.from_iterable(value_list)))
elif all(isinstance(x, tuple) for x in value_list):
merged_value = tuple(set(chain.from_iterable(value_list)))
else:
# In all other case, check if the values are objects and then take the
# objects created last.
# XXX: Should we go with creation date or modification_date ??
# TODO:
# 1. Add check that the values are ERP5 objects
# 2. In case 2 maximum values are created at same time, prefer one with
# higher priority layer
merged_value = max([max(value, key=attrgetter('creation_date'))
for value in value_list],
key=attrgetter('creation_date'))
return merged_value
def removeProperties(self,
obj,
export,
properties=[],
keep_workflow_history=False,
keep_workflow_history_last_history_only=False):
"""
Remove unneeded properties for export
"""
obj._p_activate()
klass = obj.__class__
classname = klass.__name__
attr_set = {'_dav_writelocks', '_filepath', '_owner', '_related_index',
'last_id', 'uid',
'__ac_local_roles__', '__ac_local_roles_group_id_dict__'}
if properties:
for prop in properties:
if prop.endswith('_list'):
prop = prop[:-5]
attr_set.add(prop)
if export:
if keep_workflow_history_last_history_only:
self._removeAllButLastWorkflowHistory(obj)
elif not keep_workflow_history:
attr_set.add('workflow_history')
# PythonScript covers both Zope Python scripts
# and ERP5 Python Scripts
if isinstance(obj, PythonScript):
attr_set.update(('func_code', 'func_defaults', '_code',
'_lazy_compilation', 'Python_magic'))
for attr in 'errors', 'warnings', '_proxy_roles':
if not obj.__dict__.get(attr, 1):
delattr(obj, attr)
elif classname in ('File', 'Image'):
attr_set.update(('_EtagSupport__etag', 'size'))
elif classname == 'SQL' and klass.__module__ == 'Products.ZSQLMethods.SQL':
attr_set.update(('_arg', 'template'))
elif interfaces.IIdGenerator.providedBy(obj):
attr_set.update(('last_max_id_dict', 'last_id_dict'))
elif classname == 'Types Tool' and klass.__module__ == 'erp5.portal_type':
attr_set.add('type_provider_list')
for attr in obj.__dict__.keys():
if attr in attr_set or attr.startswith('_cache_cookie_'):
try:
delattr(obj, attr)
except AttributeError:
# XXX: Continue in cases where we want to delete some properties which
# are not in attribute list
# Raise an error
continue
if classname == 'PDFForm':
if not obj.getProperty('business_template_include_content', 1):
obj.deletePdfContent()
return obj
def getBusinessPath(self):
return self.getProperty('item_path')
def getBusinessPathSign(self):
return self.getProperty('item_sign', 1)
def getBusinessPathLayer(self):
return self.getProperty('item_layer', 1)
def getParentBusinessManager(self):
return self.aq_parent
class BusinessPropertyItem(XMLObject):
"""Class to deal with path(s) which refer to property of an ERP5 object.
Used to store property name, type and value for a given object and property"""
add_permission = Permissions.AddPortalContent
# Declarative security
security = ClassSecurityInfo()
security.declareObjectProtected(Permissions.AccessContentsInformation)
portal_type = 'Business Property Item'
meta_type = 'Business Property Item'
icon = None
isIndexable = False
isProperty = True
constructors = (manage_addBusinessPropertyItem,)
def _edit(self, item_path='', item_sign=1, item_layer=0, *args, **kw):
"""
Overriden function so that we can update attributes for BusinessItem objects
"""
return super(BusinessPropertyItem, self)._edit(item_path=item_path,
item_sign=item_sign,
item_layer=item_layer,
**kw)
def build(self, context, **kw):
p = context.getPortalObject()
path = self.getProperty('item_path')
relative_url, property_id = path.split('#')
obj = p.unrestrictedTraverse(relative_url)
property_value = obj.getProperty(property_id)
property_type = obj.getPropertyType(property_id)
self.setProperty('item_property_name', property_id)
self.setProperty('item_property_type', property_type)
self.setProperty('item_property_value', property_value)
def install(self, context, *args):
# Get portal object
try:
portal = context.getPortalObject()
except AttributeError:
# This is a possibility during bootstrap where the context is not an
# erp5 object but a dynamic class
if args:
portal = args[0]
else:
raise AttributeError('No portal object found')
path = self.getProperty('item_path')
relative_url, property_id = path.split('#')
obj = portal.unrestrictedTraverse(relative_url)
property_name = self.getProperty('item_property_name')
property_type = self.getProperty('item_property_type')
property_value = self.getProperty('item_property_value')
# First remove the property from the existing path and keep the default
# empty, and update only if the sign is +1
obj._delPropValue(property_name)
# Remove the '_list' from the end of property_name. This is required because
# of the way _setProperty is defined where if the type is list_type, it
# explicitly adds '_list' at the end of property_name before trying to call
# the accessor
if property_name.endswith('_list'):
property_name = property_name[:-5]
# XXX: Explicit addition of property_type for 'type_workflow' property on
# 'ERP5Type' objects. This is required as we have created an explicit
# property outside of property sheet for ERP5Type objects, but at the same
# time, the object(s) also have property sheet for them. Since, there is no
# functions like _setProperty, hasProperty, etc to combine both of them
# we have to end up using explicit mention of 'list' property_value here
# 1. Other option can be to override hasProperty or _setProperty for ERP5Type
# object and write it so that it can handle both _properties attribute as
# well as accessor holders generated from property sheet(s).
# 2. After that we can use hasProperty here for objects to check their
# property_type and then just use the generic '_setProperty' function.
# XXX: For now, we just put this explicitly/hard-code
if property_name == 'type_workflow':
property_type = 'multiple selection'
if int(self.getProperty('item_sign')) == 1:
obj._setProperty(property_name, property_value, property_type)
def getBusinessPath(self):
return self.getProperty('item_path')
def getBusinessPathSign(self):
return self.getProperty('item_sign', 1)
def getBusinessPathLayer(self):
return self.getProperty('item_layer', 1)
def getParentBusinessManager(self):
return self.aq_parent
def getBusinessItemPropertyName(self):
return self.getProperty('item_property_name')
def getBusinessItemPropertyType(self):
return self.getProperty('item_property_type')
def getBusinessItemPropertyValue(self):
return self.getProperty('item_property_value')
def __add__(self, other):
"""
Add the values from the path when the path is same for 2 objects
"""
if self.getProperty('item_path') != other.getProperty('item_path'):
raise ValueError, "BusinessItem are incommensurable, have different path"
elif self.getProperty('item_sign') != other.getProperty('item_sign'):
raise ValueError, "BusinessItem are incommensurable, have different sign"
else:
self.setProperty('item_property_value', self._mergeValue(value_list=[
self.getProperty('item_property_value'),
other.getProperty('item_property_value')]
))
return self
__radd__ = __add__
def _mergeValue(self, value_list):
"""
Merge value in value list
merge(a, b, c) : A monotonic commutative function that depends on the
type of a, b and c:
if a, b and c are sets, merge = union
if a, b and c are lists, merge = ordered concatenation
if a, b and c are objects, merge = the object created the last
else merge = MAX
"""
builtin_number_type = (int, long, float, complex)
# Now, consider the type of both values
if all(isinstance(x, builtin_number_type) for x in value_list):
merged_value = max(value_list)
elif all(isinstance(x, set) for x in value_list):
merged_value = set(chain.from_iterable(value_list))
elif all(isinstance(x, list) for x in value_list):
merged_value = list(set(chain.from_iterable(value_list)))
elif all(isinstance(x, tuple) for x in value_list):
merged_value = tuple(set(chain.from_iterable(value_list)))
else:
# In all other case, check if the values are objects and then take the
# objects created last.
# XXX: Should we go with creation date or modification_date ??
# TODO:
# 1. Add check that the values are ERP5 objects
# 2. In case 2 maximum values are created at same time, prefer one with
# higher priority layer
merged_value = max([max(value, key=attrgetter('creation_date'))
for value in value_list],
key=attrgetter('creation_date'))
return merged_value
class BusinessPatchItem(XMLObject):
"""
Business Item for saving patch and diff. This will help us to create a diff or
patch between the old value and current value.
item_sign -- +1 or -1
item_path -- extended OFS path (equivalent to deepdiff path)
ex. a/b/c#x/y/z:int/w
item_layer -- layer
old -- old value
new -- new value
dependency_list -- a list of bt5 identifiers useful to rebuild the
BusinesTemplatePatchItem instance
preserved_list -- a list of bt5 identifiers useful to rebuild the
BusinesTemplatePatchItem instance
(XXX: We don't use `preserved_list` for now)
Business Patch Item can be both PathItem or PropertyItem, but both of them are
quite distinguishable, so we prefer not to use them as base class for it.
"""
# CMF Type definition
portal_type = 'Business Patch Item'
meta_type = 'Business Patch Item'
# Declarative security
security = ClassSecurityInfo()
security.declareObjectProtected(Permissions.AccessContentsInformation)
icon = None
isIndexable = False
isProperty = False
constructors = (manage_addBusinessPatchItem,)
allowed_types = ('Business Item', 'Business Property Item',)
def _edit(self, **kw):
"""
Override _edit to create Business Item and BusinessPropertyItem for old and
new value
"""
dependency_list = kw.get('dependency_list', self.getProperty('dependency_list'))
# Raise error if no dependency_list, this way we ensure there are no useless
# patch_item objects
if not dependency_list:
raise ValueError('Please add dependency to the Business Patch Item')
super(BusinessPatchItem, self)._edit(**kw)
def build(self, context, **kw):
"""
Build should update the old and new value
"""
portal = self.getPortalObject()
portal_templates = portal.portal_templates
item_path = self.getProperty('item_path')
item_layer = self.getProperty('item_layer')
item_sign = self.getProperty('item_sign')
# Remove old objects if it exists
old_id_list = [l for l in self.objectIds()]
if old_id_list:
self.manage_delObjects(ids=old_id_list)
# Get the dependency Business Manager
dependency_list = self.getProperty('dependency_list')
if dependency_list:
dependency_title = dependency_list[0]
dependency_bm = portal_templates.getInstalledBusinessManager(dependency_title)
if not dependency_bm:
raise ValueError('Missing Installed Business Manager for dependecy_list \
which is required to build')
# Use item_path to determine if we need to create Business Item or
# Business Property Item for storing old and new values
if '#' in item_path:
# Create new_prop_item and build it from ZODB
new_item = self.newContent(portal_type='Business Property Item',
item_path=item_path,
item_layer=item_layer,
item_sign=item_sign,
id='new_item')
new_item.build(self)
else:
# Create new_item and build it from ZODB
new_item = self.newContent(portal_type='Business Item',
item_path=item_path,
item_layer=item_layer,
item_sign=item_sign,
id='new_item')
new_item.build(self)
updated_id = 'old_item'
# Copy old item/property item from the item at similar path in dependency_bm
dependency_item = dependency_bm.getBusinessItemByPath(item_path)
# Raise if there is no item exisiting in dependency Business Manager
if not dependency_item:
raise ValueError('No %s exist at path %s in installed version of %s'
% ( new_item.getPortalType(),
item_path,
dependency_title,))
cp_data = dependency_bm.manage_copyObjects([dependency_item.getId()])
new_id = self.manage_pasteObjects(cp_data)[0]['new_id']
self.manage_renameObject(id=new_id, new_id=updated_id)
# Get the copied object and update the properties
old_item = self._getOb(updated_id)
old_item.setProperty('item_layer', item_layer)
old_item.setProperty('item_sign', item_sign)
# Commit the transaction
transaction.commit()
def getOldValue(self):
"""
Returns old value for the BusinessPatchItem
"""
old_item = self._getOb('old_item')[0]
if old_item.getPortalType() == 'Business Item':
return old_item.objectValues()
else:
return old_item.getProperty('item_property_value')
def getNewValue(self, build=False):
"""
Returns new value for the given BusinessPatchItem
"""
old_item = self._getOb('new_item')
if old_item.getPortalType() == 'Business Item':
return old_item.objectValues()[0]
else:
return old_item.getProperty('item_property_value')
def getDiff(self, patch_format='deepdiff'):
"""
Use diff tool to find the diff between two values
XXX: For now we display the json format of the patched diff
"""
patch = self.getPatchObject(patch_format)
if patch_format == 'deepdiff':
return patch.asDeepDiffPatch().json
else:
# For json-patch object
return patch.asJSONPatch().to_string()
def getPatchObject(self, patch_format='deepdiff'):
portal = self.getPortalObject()
diff_tool = portal.portal_diff
old_item = self._getOb('old_item')
new_item = self._getOb('new_item')
if (old_item.getPortalType() == new_item.getPortalType() == 'Business Item'):
old = old_item.objectValues()[0]
new = new_item.objectValues()[0]
else:
old = old_item.getProperty('item_property_value')
new = new_item.getProperty('item_property_value')
patch = diff_tool.diffPortalObject(old=old,
new=new,
patch_format=patch_format
)
return patch
def install(self, context, *args):
"""
Install will call the apply function which puts the new value at the
path mentioned while checking if the old value exists.
"""
# Installation is basically running installation on the new_value
self.new_value.install(context)
def applyPatch(self):
"""
Apply the new value by removing the old value. Also, show conflict in case
the value at old_installation_state is not same as the old value.
"""
pass
def registerSkinFolder(skin_tool, skin_folder):
request = skin_tool.REQUEST
# XXX: Getting parameter from request instead of dialog is bad
# XXX: This is even non consistent with rest of parameters selected by user
# (like update_translation or update_catalog)
register_skin_selection = request.get('your_register_skin_selection', 1)
reorder_skin_selection = request.get('your_reorder_skin_selection', 1)
skin_layer_list = request.get('your_skin_layer_list',
skin_tool.getSkinSelections())
skin_folder_id = skin_folder.getId()
try:
skin_selection_list = skin_folder.getProperty(
'business_template_registered_skin_selections',
skin_tool.getSkinSelections()
)
except AttributeError:
skin_selection_list = skin_tool.getSkinSelections()
if isinstance(skin_selection_list, basestring):
skin_selection_list = skin_selection_list.split()
def skin_sort_key(skin_folder_id):
obj = skin_tool._getOb(skin_folder_id, None)
if obj is None:
return 0, skin_folder_id
return -obj.getProperty('business_template_skin_layer_priority',
obj.meta_type == 'Filesystem Directory View' and -1 or 0), skin_folder_id
for skin_name in skin_selection_list:
if (skin_name not in skin_tool.getSkinSelections()) and \
register_skin_selection:
createSkinSelection(skin_tool, skin_name)
# add newly created skins to list of skins we care for
skin_layer_list.append(skin_name)
selection = skin_tool.getSkinPath(skin_name) or ''
selection_list = selection.split(',')
if (skin_folder_id not in selection_list):
selection_list.insert(0, skin_folder_id)
if reorder_skin_selection:
# Sort by skin priority and ID
selection_list.sort(key=skin_sort_key)
if (skin_name in skin_layer_list):
skin_tool.manage_skinLayers(skinpath=selection_list,
skinname=skin_name, add_skin=1)
skin_tool.getPortalObject().changeSkin(None)
def createSkinSelection(skin_tool, skin_name):
# This skin selection does not exist, so we create a new one.
# We'll initialize it with all skin folders, unless:
# - they explictly define a list of
# "business_template_registered_skin_selections", and we
# are not in this list.
# - they are not registered in the default skin selection
skin_path = ''
for skin_folder in skin_tool.objectValues():
if skin_name in skin_folder.getProperty(
'business_template_registered_skin_selections',
(skin_name, )):
if skin_folder.getId() in \
skin_tool.getSkinPath(skin_tool.getDefaultSkin()):
if skin_path:
skin_path = '%s,%s' % (skin_path, skin_folder.getId())
else:
skin_path= skin_folder.getId()
# add newly created skins to list of skins we care for
skin_tool.addSkinSelection(skin_name, skin_path)
skin_tool.getPortalObject().changeSkin(None)
def deleteSkinSelection(skin_tool, skin_name):
# Do not delete default skin
if skin_tool.getDefaultSkin() != skin_name:
for skin_folder in skin_tool.objectValues():
try:
if skin_name in skin_folder.getProperty(
'business_template_registered_skin_selections', ()):
break
except AttributeError:
pass
else:
skin_tool.manage_skinLayers(chosen=[skin_name], del_skin=1)
skin_tool.getPortalObject().changeSkin(None)
def unregisterSkinFolderId(skin_tool, skin_folder_id, skin_selection_list):
for skin_selection in skin_selection_list:
selection = skin_tool.getSkinPath(skin_selection)
selection = selection.split(',')
if (skin_folder_id in selection):
selection.remove(skin_folder_id)
skin_tool.manage_skinLayers(skinpath=tuple(selection),
skinname=skin_selection, add_skin=1)
deleteSkinSelection(skin_tool, skin_selection)
skin_tool.getPortalObject().changeSkin(None)
......@@ -33,17 +33,22 @@ from App.config import getConfiguration
import os
import shutil
import sys
import hashlib
import pprint
import transaction
from Acquisition import Implicit, Explicit
from AccessControl import ClassSecurityInfo
from AccessControl.SecurityInfo import ModuleSecurityInfo
from Products.CMFActivity.ActiveResult import ActiveResult
from Products.PythonScripts.PythonScript import PythonScript
from Products.ERP5Type.Globals import InitializeClass, DTMLFile, PersistentMapping
from Products.ERP5Type.DiffUtils import DiffFile
from Products.ERP5Type.Tool.BaseTool import BaseTool
from Products.ERP5Type.Cache import transactional_cached
from Products.ERP5Type import Permissions
from Products.ERP5Type import Permissions, interfaces
from Products.ERP5.Document.BusinessTemplate import BusinessTemplateMissingDependency
from Products.ERP5Type.Accessor.Constant import PropertyGetter as ConstantGetter
from Products.ERP5.genbt5list import generateInformation
from Acquisition import aq_base
from tempfile import mkstemp, mkdtemp
......@@ -60,6 +65,8 @@ from base64 import b64encode, b64decode
from Products.ERP5Type.Message import translateString
from zLOG import LOG, INFO, WARNING
from base64 import decodestring
from difflib import unified_diff
from operator import attrgetter
import subprocess
import time
......@@ -101,7 +108,10 @@ class TemplateTool (BaseTool):
title = 'Template Tool'
meta_type = 'ERP5 Template Tool'
portal_type = 'Template Tool'
allowed_types = ('ERP5 Business Template', )
allowed_types = (
'ERP5 Business Template',
'ERP5 Business Manager',
)
# This stores information on repositories.
repository_dict = {}
......@@ -128,7 +138,14 @@ class TemplateTool (BaseTool):
# potential danger because business templates may exchange catalog
# methods, so the database could be broken temporarily.
last_bt = last_time = None
for bt in self.objectValues(portal_type='Business Template'):
for bt in self.objectValues(portal_type=['Business Template',
'Business Manager']):
if bt.getPortalType() == 'Business Manager':
if bt.getInstallationState() == 'installed' and bt.title == title:
return bt
else:
continue
return None
if bt.getTitle() == title or title in bt.getProvisionList():
state = bt.getInstallationState()
if state == 'installed':
......@@ -149,6 +166,22 @@ class TemplateTool (BaseTool):
last_time = t
return last_bt
security.declareProtected(Permissions.AccessContentsInformation,
'getInstalledBusinessManager')
def getInstalledBusinessManager(self, title, strict=False, **kw):
"""Returns an installed version of business manager of given title.
Returns None if business manager is not installed or has been uninstalled.
"""
last_bm = None
for bm in self.objectValues(portal_type='Business Manager'):
if bm.getTitle() == title:
state = bm.getInstallationState()
if state == 'installed':
return bm
return last_bm
security.declareProtected(Permissions.AccessContentsInformation,
'getInstalledBusinessTemplatesList')
def getInstalledBusinessTemplatesList(self):
......@@ -161,7 +194,8 @@ class TemplateTool (BaseTool):
"""Get the list of installed business templates.
"""
installed_bts = []
for bt in self.contentValues(portal_type='Business Template'):
for bt in self.contentValues(portal_type=['Business Template',
'Business Manager']):
if bt.getInstallationState() == 'installed':
bt5 = bt
if only_title:
......@@ -306,6 +340,7 @@ class TemplateTool (BaseTool):
self.deleteContent(id)
self._importObjectFromFile(StringIO(export_string), id=id)
security.declareProtected( Permissions.ManagePortal, 'manage_download' )
def manage_download(self, url, id=None, REQUEST=None):
"""The management interface for download.
......@@ -321,9 +356,14 @@ class TemplateTool (BaseTool):
REQUEST.RESPONSE.redirect("%s?portal_status_message=%s"
% (ret_url, psm))
def _download_local(self, path, bt_id):
def _download_local(self, path, bt_id, format_version=1):
"""Download Business Template from local directory or file
"""
if format_version == 3:
bm = self.newContent(bt_id, 'Business Manager')
bm.importFile(path)
return bm
bt = self.newContent(bt_id, 'Business Template')
bt.importFile(path)
return bt
......@@ -361,12 +401,17 @@ class TemplateTool (BaseTool):
# come from the management interface.
if REQUEST is not None:
return self.manage_download(url, id=id, REQUEST=REQUEST)
if id is None:
id = self.generateNewId()
urltype, path = splittype(url)
if WIN and urltype and '\\' in path:
urltype, name = splittype(url)
# Create a zexp path which would be used for Business Manager files
zexp_path = name + '/' + name.split('/')[-1] + '.zexp'
# Better to expand path as we now use ~software_release for the software
# folder
zexp_path = os.path.expanduser(zexp_path)
if WIN and urltype and '\\' in name:
urltype = None
path = url
if urltype and urltype != 'file':
......@@ -377,9 +422,28 @@ class TemplateTool (BaseTool):
del bt.uid
return self[self._setObject(id, bt)]
bt = self._download_url(url, id)
elif os.path.exists(zexp_path):
# If the path exists, we create a Business Manager object after
# downloading it from zexp path
bt = self._download_local(os.path.normpath(zexp_path), id, format_version=3)
else:
path = os.path.normpath(os.path.expanduser(path))
bt = self._download_local(path, id)
template_version_path_list = [
name+'/bt/template_format_version',
]
for path in template_version_path_list:
try:
file = open(os.path.normpath(path))
except IOError:
continue
try:
format_version = int(file.read())
file.close()
except UnboundLocalError:
# In case none of the above paths do have template_format_version
format_version = 1
# XXX: Download only needed in case the file is in directory
bt = self._download_local(os.path.expanduser(os.path.normpath(name)), id, format_version)
bt.build(no_action=True)
return bt
......@@ -534,6 +598,413 @@ class TemplateTool (BaseTool):
self.activate(activity='SQLQueue').\
importAndReExportBusinessTemplateFromPath(template_path)
security.declareProtected( 'Import/Export objects', 'migrateBTToBM')
def migrateBTToBM(self, template_path, isReduced=False, REQUEST=None, **kw):
"""
Migrate business template repository to Business Manager repo.
Business Manager completely rely only on BusinessItem and to show
the difference between both of them
So, the steps should be:
1. Install the business template which is going to be migrated
2. Create a new Business Manager with random id and title
3. Add the path, build and export the template
4. Remove the business template from the directory and add the business
manager there instead
5. Change the ID and title of the business manager
6. Export the business manager to the directory, leaving anything in
the installed erp5 unchanged
"""
import_template = self.download(url=template_path)
if import_template.getPortalType() == 'Business Manager':
LOG(import_template.getTitle(),0,'Already migrated')
return
export_dir = mkdtemp()
removable_property = {}
removable_sub_object_path = []
installed_bt_list = self.getInstalledBusinessTemplatesList()
installed_bt_title_list = [bt.title for bt in installed_bt_list]
is_installed = False
if import_template.getTitle() not in installed_bt_title_list:
# Install the business template
import_template.install(**kw)
is_installed = True
# Make list of object paths which needs to be added in the bm5
# This can be decided by looping over every type of items we do have in
# bt5 and checking if there have been any changes being made to it via this
# bt5 installation or not.
# For ex:
# CatalogTempalteMethodItem, CatalogResultsKeyItem, etc. do make some
# changes in erp5_mysql_innodb(by adding properties, by adding sub-objects),
# so we need to add portal_catalog/erp5_mysql_innodb everytime we find
# a bt5 making changes in any of these items.
portal_path = self.getPortalObject()
template_path_list = []
property_path_list = []
# For modules, we don't need to create path for the module
module_list = import_template.getTemplateModuleIdList()
for path in module_list:
template_path_list.append(path)
# For portal_types, we have to add path and subobjects
portal_type_id_list = import_template.getTemplatePortalTypeIdList()
portal_type_path_list = []
portal_type_workflow_chain_path_list = []
for id in portal_type_id_list:
portal_type_path_list.append('portal_types/'+id)
# Add type_worklow list separately in path
portal_type_workflow_chain_path_list.append('portal_types/'+id+'#type_workflow_list')
# Remove type_workflow_list from the objects, so that we don't end up in
# conflict
portal_type_path = 'portal_types/' + id
removable_property[portal_type_path] = ['type_workflow_list']
template_path_list.extend(portal_type_path_list)
template_path_list.extend(portal_type_workflow_chain_path_list)
# For categories, we create path for category objects as well as the subcategories
category_list = import_template.getTemplateBaseCategoryList()
category_path_list = []
for base_category in category_list:
category_path_list.append('portal_categories/'+base_category)
#category_path_list.append('portal_categories/'+base_category+'/**')
template_path_list.extend(category_path_list)
# Adding tools
template_tool_id_list = import_template.getTemplateToolIdList()
tool_id_list = []
for tool_id in template_tool_id_list:
tool_id_list.append(tool_id)
template_path_list.extend(tool_id_list)
# Adding business template skin selection property on the portal_tempaltes
template_skin_selection_list = import_template.getTemplateRegisteredSkinSelectionList()
selection_list = []
for selection in template_skin_selection_list:
skin, selection = selection.split(' | ')
selection_list.append('portal_skins/%s#business_template_registered_skin_selections'%skin)
# For portal_skins, we export the folder
portal_skin_list = import_template.getTemplateSkinIdList()
portal_skin_path_list = []
for skin in portal_skin_list:
portal_skin_path_list.append('portal_skins/'+skin)
#portal_skin_path_list.append('portal_skins/'+skin+'/**')
template_path_list.extend(portal_skin_path_list)
# For workflow chains,
# We have 2 objects in the Business Template design where we deal with
# workflow objects, we deal with the installation separately:
# 1. Workflow_id : We export the whole workflow objects in this case
# 2. Portal Workflow chain: It is already being exported via portal_types
# XXX: CHECK For 2, keeping in mind the migration of workflow would be merged
# before this part where we make workflow_list as property of portal_type
workflow_id_list = import_template.getTemplateWorkflowIdList()
workflow_path_list = []
for workflow in workflow_id_list:
workflow_path_list.append('portal_workflow/' + workflow)
#workflow_path_list.append('portal_workflow/' + workflow + '/**')
template_path_list.extend(workflow_path_list)
# For tests in portal components add them with portal_components head
test_id_list = import_template.getTemplateTestIdList()
test_path_list = []
for path in test_id_list:
test_path_list.append('portal_components/' + path)
template_path_list.extend(test_path_list)
# For documents in portal components add them with portal_components head
document_id_list = import_template.getTemplateDocumentIdList()
document_path_list = []
for path in document_id_list:
document_path_list.append('portal_components/' + path)
template_path_list.extend(document_path_list)
# For extensions in portal components add them with portal_components head
extension_id_list = import_template.getTemplateExtensionIdList()
extension_path_list = []
for path in extension_id_list:
extension_path_list.append('portal_components/' + path)
template_path_list.extend(extension_path_list)
# For paths, we add them directly to the path list
path_list = import_template.getTemplatePathList()
for path in path_list:
template_path_list.append(path)
# Catalog methods would be added as sub objects
catalog_method_item_list = import_template.getTemplateCatalogMethodIdList()
catalog_method_path_list = []
for method in catalog_method_item_list:
catalog_method_path_list.append('portal_catalog/' + method)
template_path_list.extend(catalog_method_path_list)
# For catalog objects, we check if there is any catalog object, and then
# add catalog object also in the path if there is
template_catalog_datetime_key = import_template.getTemplateCatalogDatetimeKeyList()
template_catalog_full_text_key = import_template.getTemplateCatalogFullTextKeyList()
template_catalog_keyword_key = import_template.getTemplateCatalogKeywordKeyList()
template_catalog_local_role_key = import_template.getTemplateCatalogLocalRoleKeyList()
template_catalog_multivalue_key = import_template.getTemplateCatalogMultivalueKeyList()
template_catalog_related_key = import_template.getTemplateCatalogRelatedKeyList()
template_catalog_request_key = import_template.getTemplateCatalogRequestKeyList()
template_catalog_result_key = import_template.getTemplateCatalogResultKeyList()
template_catalog_result_table = import_template.getTemplateCatalogResultTableList()
template_catalog_role_key = import_template.getTemplateCatalogRoleKeyList()
template_catalog_scriptable_key = import_template.getTemplateCatalogScriptableKeyList()
template_catalog_search_key = import_template.getTemplateCatalogSearchKeyList()
template_catalog_security_uid_column = import_template.getTemplateCatalogSecurityUidColumnList()
template_catalog_topic_key = import_template.getTemplateCatalogTopicKeyList()
catalog_property_list = [
template_catalog_datetime_key,
template_catalog_full_text_key,
template_catalog_keyword_key,
template_catalog_local_role_key,
template_catalog_multivalue_key,
template_catalog_related_key,
template_catalog_request_key,
template_catalog_result_key,
template_catalog_result_table,
template_catalog_role_key,
template_catalog_scriptable_key,
template_catalog_search_key,
template_catalog_security_uid_column,
template_catalog_topic_key,
]
is_property_added = any(catalog_property_list)
properties_removed = [
'sql_catalog_datetime_search_keys_list',
'sql_catalog_full_text_search_keys_list',
'sql_catalog_keyword_search_keys_list',
'sql_catalog_local_role_keys_list',
'sql_catalog_multivalue_keys_list',
'sql_catalog_related_keys_list',
'sql_catalog_request_keys_list',
'sql_search_result_keys_list',
'sql_search_tables_list',
'sql_catalog_role_keys_list',
'sql_catalog_scriptable_keys_list',
'sql_catalog_search_keys_list',
'sql_catalog_security_uid_columns_list',
'sql_catalog_topic_search_keys_list',
]
if is_property_added:
if catalog_method_path_list:
catalog_path = catalog_method_path_list[0].rsplit('/', 1)[0]
else:
catalog_path = 'portal_catalog/erp5_mysql_innodb'
removable_sub_object_path.append(catalog_path)
removable_property[catalog_path] = properties_removed
for prop in properties_removed:
property_path_list.append('%s#%s' % (catalog_path, prop))
# Add these catalog items in the object_property instead of adding
# dummy path item for them
if import_template.getTitle() == 'erp5_mysql_innodb_catalog':
template_path_list.append('portal_catalog/erp5_mysql_innodb')
# Add portal_property_sheets
property_sheet_id_list = import_template.getTemplatePropertySheetIdList()
property_sheet_path_list = []
for property_sheet in property_sheet_id_list:
property_sheet_path_list.append('portal_property_sheets/' + property_sheet)
#property_sheet_path_list.append('portal_property_sheets/' + property_sheet + '/**')
template_path_list.extend(property_sheet_path_list)
# Create new objects for business manager
migrated_bm = self.newContent(
portal_type='Business Manager',
title=import_template.getTitle()
)
template_path_list.extend(property_path_list)
template_path_list.extend(selection_list)
template_path_list = self.cleanTemplatePathList(template_path_list)
# XXX: Add layer=1 and sign=1 for default for all paths
template_path_list = [l + ' | 1 | 1' for l in template_path_list]
def reduceDependencyList(bt, template_path_list):
"""
Used for recursive udpation of layer for dependency in a BT
"""
dependency_list = bt.getDependencyList()
# XXX: Do not return template_path_list of the new BM incase there is no
# dependency_list, instead look for the latest updated version of
# new_template_path_list
if not dependency_list:
return template_path_list
else:
# Copy of the initial template list to be used to update the layer
new_template_path_list = list(template_path_list)
for item in dependency_list:
dependency = item.split(' ', 1)
if len(dependency) > 1:
version = dependency[1]
if version:
version = version[1:-1]
base_bt = self.getLastestBTOnRepos(dependency[0], version)
else:
try:
base_bt = self.getLastestBTOnRepos(dependency[0])
except BusinessTemplateIsMeta:
bt_list = self.getProviderList(dependency[0])
# We explicilty use the Business Template which is used the most
# while dealing with provision list
repository_list = self.getRepositoryList()
if dependency[0] == 'erp5_full_text_catalog':
base_bt = [repository_list[1], 'erp5_full_text_mroonga_catalog']
if dependency[0] == 'erp5_view_style':
base_bt = [repository_list[0], 'erp5_xhtml_style']
if dependency[0] == 'erp5_catalog':
base_bt = [repository_list[0], 'erp5_mysql_innodb_catalog']
# XXX: Create path for the BT(s) here
# Download the base_bt
base_bt_path = os.path.join(base_bt[0], base_bt[1])
base_bt = self.download(base_bt_path)
# Check for the item list and if the BT is Business Manager,
# if BM, then compare and update layer and if not run migration and
# then do it again
if base_bt.getPortalType() != 'Business Manager':
# If the base_bt is not Business Manager, run the migration on the
# base_bt
base_bt = self.migrateBTToBM(base_bt_path, isReduced=True)
# Check for item path which also exists in base_bt
base_path_list = base_bt.getPathList()
copy_of_template_path_list = new_template_path_list[:]
# Loop through all the paths in the new_template_path_list and
# check for their existence in base_path_list
for idx, path in enumerate(new_template_path_list):
path_list = path.split(' | ')
item_path = path_list[0]
item_layer = path_list[2]
if item_path in base_path_list:
# TODO: Increase the layer of the path item by +1 and save it
# back at updated_template_path_list
item_layer = int(item_layer) + 1
updated_path = item_path + ' | 1 | ' + str(item_layer)
copy_of_template_path_list[idx] = updated_path
new_template_path_list = copy_of_template_path_list
if base_bt.getPortalType() != 'Business Manager':
# Recursively reduce the base Business Templatem no need to do
# this for Business Manager(s) as it had already been migrated
# with taking care of layer
reduceDependencyList(base_bt, new_template_path_list)
return new_template_path_list
# Take care about the the dependency_list also and then update the layer
# accordingly for the path(s) that already exists in the dependencies.
template_path_list = reduceDependencyList(import_template, template_path_list)
# Create new sub-objects instead based on template_path_list
for path in template_path_list:
path_list = path.split(' | ')
# Create Business Property Item for objects with property in path
if '#' in path_list[0]:
migrated_bm.newContent(
portal_type='Business Property Item',
item_path=path_list[0],
item_sign=path_list[1],
item_layer=path_list[2],
)
else:
migrated_bm.newContent(
portal_type='Business Item',
item_path=path_list[0],
item_sign=path_list[1],
item_layer=path_list[2],
)
kw['removable_property'] = removable_property
kw['removable_sub_object_path'] = removable_sub_object_path
migrated_bm.build(**kw)
# Commit transaction to generate all oids before exporting
transaction.commit()
# Export the newly built business manager to the export directory
migrated_bm.export(path=export_dir, local=True)
if is_installed:
import_template.uninstall()
if isReduced:
return migrated_bm
def cleanTemplatePathList(self, path_list):
"""
Remove redundant paths and sub-objects' path if the object path already
exist.
"""
# Split path into list
a2 = [l.split('/') for l in path_list]
# Create new list for paths with **
a3 = [l for l in a2 if l[-1] in ('**', '*')]
# Create new list for paths without **
a4 = [l for l in a2 if l[-1] not in ('**', '*')]
# Remove ** from paths in a3
reserved_id = ('portal_transforms', 'portal_ids')
a3 = [l[:-1] for l in a3 if l[0] not in reserved_id] + [l for l in a3 if l[0] in reserved_id]
# Create new final path list
a2 = a3+a4
# Join the path list
a2 = [('/').join(l) for l in a2]
# Remove the redundant paths
seen = set()
seen_add = seen.add
# XXX: What about redundant signs with different layers
# Maybe we will end up reducing them
a2 = [x for x in a2 if not (x in seen or seen_add(x))]
return a2
security.declareProtected( 'Import/Export objects', 'migrateBTListToBM')
def migrateBTToBMRequest(self, bt_title_list, REQUEST=None, **kw):
"""
Run migration for BT5 one by one in a given repository. This will be done
via activities.
"""
if REQUEST is None:
REQUEST = getattr(self, 'REQUEST', None)
if len(bt_title_list) == 0 and REQUEST:
ret_url = self.absolute_url()
REQUEST.RESPONSE.redirect("%s?portal_status_message=%s"
% (ret_url, 'No BT title was given'))
repository_list = self.getRepositoryList()
for title in bt_title_list:
title = title.rstrip('\n')
title = title.rstrip('\r')
for repository in repository_list:
if title in os.listdir(repository):
template_path = os.path.join(repository, title)
else:
continue
if os.path.isfile(template_path):
LOG(title, 0, 'is file, so it is skipped')
else:
if not os.path.exists((os.path.join(template_path, 'bt'))):
LOG(title, 0, 'has no bt sub-folder, so it is skipped')
else:
self.migrateBTToBM(template_path)
security.declareProtected(Permissions.ManagePortal, 'getFilteredDiff')
def getFilteredDiff(self, diff):
"""
......@@ -676,6 +1147,212 @@ class TemplateTool (BaseTool):
finally:
f.close()
#XXX: Hardcoding 'erp5_core_proxy_field_legacy' BP in the list
bp_dict_1 ={
'copyright_list': ['Copyright (c) 2001-2017 Nexedi SA'],
'dependency_list': [],
'description': '',
'force_install': 0,
'id': 'erp5_core_proxy_field_legacy',
'license': 'GPL',
'revision': '',
'test_dependency_list': [],
'provision_list': [],
'title': 'erp5_core_proxy_field_legacy',
'version': '5.4.7'}
bp_dict_2 ={
'copyright_list': ['Copyright (c) 2001-2017 Nexedi SA'],
'dependency_list': ['erp5_base',],
'description': '',
'force_install': 0,
'id': 'erp5_pdm',
'license': 'GPL',
'revision': '',
'test_dependency_list': [],
'provision_list': [],
'title': 'erp5_pdm',
'version': '5.4.7'}
bp_dict_3 ={
'copyright_list': ['Copyright (c) 2001-2017 Nexedi SA'],
'dependency_list': ['erp5_ui_test',],
'description': '',
'force_install': 0,
'id': 'erp5_performance_test',
'license': 'GPL',
'revision': '',
'test_dependency_list': [],
'provision_list': [],
'title': 'erp5_performance_test',
'version': '5.4.7'}
bp_dict_4 ={
'copyright_list': ['Copyright (c) 2001-2017 Nexedi SA'],
'dependency_list': ['erp5_ui_test_core',],
'description': '',
'force_install': 0,
'id': 'erp5_ui_test',
'license': 'GPL',
'revision': '',
'test_dependency_list': [],
'provision_list': [],
'title': 'erp5_ui_test',
'version': '5.4.7'}
bp_dict_5 ={
'copyright_list': ['Copyright (c) 2001-2017 Nexedi SA'],
'dependency_list': ['erp5_core', 'erp5_xhtml_style',],
'description': '',
'force_install': 0,
'id': 'erp5_ui_test_core',
'license': 'GPL',
'revision': '',
'test_dependency_list': [],
'provision_list': [],
'title': 'erp5_ui_test_core',
'version': '5.4.7'}
bp_dict_6 ={
'copyright_list': ['Copyright (c) 2001-2017 Nexedi SA'],
'dependency_list': ['erp5_core (>= 1.0rc12)',
'erp5_full_text_catalog',
'erp5_core_proxy_field_legacy',],
'description': '',
'force_install': 0,
'id': 'erp5_base',
'license': 'GPL',
'revision': '',
'test_dependency_list': [],
'provision_list': [],
'title': 'erp5_base',
'version': '5.4.7'}
bp_dict_7 ={
'copyright_list': ['Copyright (c) 2001-2017 Nexedi SA'],
'dependency_list': [],
'description': '',
'force_install': 0,
'id': 'erp5_full_text_mroonga_catalog',
'license': 'GPL',
'revision': '',
'test_dependency_list': [],
'provision_list': ['erp5_full_text_catalog'],
'title': 'erp5_full_text_mroonga_catalog',
'version': '5.4.7'}
if repository.endswith('/bt5'):
property_dict_list.append(bp_dict_1)
property_dict_list.append(bp_dict_2)
property_dict_list.append(bp_dict_3)
#property_dict_list.append(bp_dict_4)
property_dict_list.append(bp_dict_5)
property_dict_list.append(bp_dict_6)
property_dict_list.append(bp_dict_7)
bm_dict_1 ={
'copyright_list': ['Copyright (c) 2001-2017 Nexedi SA'],
'dependency_list': [],
'description': '',
'force_install': 0,
'id': 'erp5_mysql_innodb_catalog',
'license': 'GPL',
'revision': '',
'test_dependency_list': [],
'provision_list': ['erp5_catalog'],
'title': 'erp5_mysql_innodb_catalog',
'version': '5.4.7'}
bm_dict_2 ={
'copyright_list': ['Copyright (c) 2001-2017 Nexedi SA'],
'dependency_list': ['erp5_core'],
'description': '',
'force_install': 0,
'id': 'erp5_xhtml_style',
'license': 'GPL',
'revision': '',
'test_dependency_list': [],
'provision_list': ['erp5_view_style'],
'title': 'erp5_xhtml_style',
'version': '5.4.7'}
bm_dict_3 ={
'copyright_list': ['Copyright (c) 2001-2017 Nexedi SA'],
'dependency_list': [],
'description': '',
'force_install': 0,
'id': 'erp5_mysql_ndb_catalog',
'license': 'GPL',
'revision': '',
'test_dependency_list': [],
'provision_list': ['erp5_catalog'],
'title': 'erp5_mysql_ndb_catalog',
'version': '5.4.7'}
bm_dict_4 ={
'copyright_list': ['Copyright (c) 2001-2017 Nexedi SA'],
'dependency_list': ['erp5_view_style',],
'description': '',
'force_install': 0,
'id': 'erp5_jquery',
'license': 'GPL',
'revision': '',
'test_dependency_list': [],
'provision_list': [],
'title': 'erp5_jquery',
'version': '5.4.7'}
bm_dict_5 ={
'copyright_list': ['Copyright (c) 2001-2017 Nexedi SA'],
'dependency_list': [],
'description': '',
'force_install': 0,
'id': 'erp5_property_sheets',
'license': 'GPL',
'revision': '',
'test_dependency_list': [],
'provision_list': [],
'title': 'erp5_property_sheets',
'version': '5.4.7'}
bm_dict_6 ={
'copyright_list': ['Copyright (c) 2001-2017 Nexedi SA'],
'dependency_list': ['erp5_catalog (>= 1.1)',
'erp5_core_proxy_field_legacy',
'erp5_property_sheets'],
'description': '',
'force_install': 0,
'id': 'erp5_core',
'license': 'GPL',
'revision': '',
'test_dependency_list': ['erp5_full_text_mroonga_catalog',
'erp5_base'],
'provision_list': ['erp5_auto_logout',],
'title': 'erp5_core',
'version': '5.4.7'}
bm_dict_7 ={
'copyright_list': ['Copyright (c) 2001-2017 Nexedi SA'],
'dependency_list': [],
'description': '',
'force_install': 0,
'id': 'erp5_business_package',
'license': 'GPL',
'revision': '',
'test_dependency_list': [],
'provision_list': [],
'title': 'erp5_business_package',
'version': '5.4.7'}
if repository.endswith('/bootstrap'):
property_dict_list.append(bm_dict_1)
#property_dict_list.append(bm_dict_2)
property_dict_list.append(bm_dict_3)
property_dict_list.append(bm_dict_4)
property_dict_list.append(bm_dict_5)
property_dict_list.append(bm_dict_6)
#property_dict_list.append(bm_dict_7)
self.repository_dict[repository] = tuple(property_dict_list)
if REQUEST is not None:
......@@ -1141,10 +1818,8 @@ class TemplateTool (BaseTool):
template_title_list,
with_test_dependency_list=False):
available_bt5_list = self.getRepositoryBusinessTemplateList()
template_title_list = set(template_title_list)
installed_bt5_title_list = self.getInstalledBusinessTemplateTitleList()
bt5_set = set()
for available_bt5 in available_bt5_list:
if available_bt5.title in template_title_list:
......@@ -1341,6 +2016,9 @@ class TemplateTool (BaseTool):
if update_catalog is CATALOG_UPDATABLE and install_kw != {}:
update_catalog = imported_bt5.isCatalogUpdatable()
if imported_bt5.getPortalType() == 'Business Manager':
self.updateInstallationState([imported_bt5])
else:
imported_bt5.install(object_to_update=install_kw,
update_catalog=update_catalog)
......@@ -1357,6 +2035,598 @@ class TemplateTool (BaseTool):
return imported_bt5
security.declareProtected(Permissions.ManagePortal,
'installBusinessManager')
def installBusinessManager(self, bm):
"""
Run installation on flattened Business Manager
"""
# Run install on separate Business Item one by one
for path_item in bm._path_item_list:
path_item.install(self)
bm.setStatus('installed')
def updateHash(self, item):
"""
Function to update hash of Business Item or Business Property Item
"""
# Check for isProperty attribute
if item.isProperty:
value = item.getProperty('item_property_value')
else:
value_list = item.objectValues()
if value_list:
value = value_list[0]
else:
value = ''
if value:
item.setProperty('item_sha', self.calculateComparableHash(
value,
item.isProperty,
))
def rebuildBusinessManager(self, bm):
"""
Compare the sub-objects in the Business Manager to the previous built
state to give user powet to decide on which item to rebuild.
"""
checkNeeded = True
changed_path_list = []
if bm.getBuildingState() not in ['built', 'modified']:
# In case the building_state is not built, we build the BM without
# comparing anything
checkNeeded = False
return checkNeeded, changed_path_list
portal = self.getPortalObject()
for item in bm.objectValues():
# Check for the change compared to old building state, i.e, if there is
# some change made at ZODB state(it also count changes made due to
# change while installation of other BM)
path = item.getProperty('item_path')
try:
if item.isProperty:
# Get the value for Business Property Item
value = item.getProperty('item_property_value')
# Get the value at ZODB
relative_url, property_id = path.split('#')
obj = portal.restrictedTraverse(relative_url)
property_value = obj.getProperty(property_id)
# If the value at ZODB for the property is none, raise KeyError
# This is important to have compatibility between the way we check
# path as well as property. Otherwise, if we install a new property,
# we are always be getting an Error that there is change made at
# ZODB for this property
if not property_value:
raise KeyError
obj = property_value
else:
# Get the value of the Business Path Item
value_list = item.objectValues()
if value_list:
value = value_list[0]
else:
# If there is no value, it means the path_item is new, thus no
# need to comapre hash and check anything
changed_path_list.append((path, 'New'))
continue
# Get the object at ZODB
obj = portal.restrictedTraverse(path)
# Calculate hash for value at ZODB
obj_sha = self.calculateComparableHash(obj, item.isProperty)
# Update hash for value at property_value
self.updateHash(item)
item_sha = item.getProperty('item_sha')
# Compare the hash with the item hash
if obj_sha != item_sha:
changed_path_list.append((path, 'Changed'))
else:
changed_path_list.append((path, 'Unchanged'))
# KeyError is raised in case the value/object has been deleted at ZODB
except KeyError:
changed_path_list.append((path, 'Deleted'))
return checkNeeded, changed_path_list
security.declareProtected(Permissions.ManagePortal,
'updateInstallationState')
def compareInstallationState(self, bm_list):
"""
Run installation after comparing combined Business Manager status
Steps:
1. Create combinedBM for the bm_list
2. Get the old combinedBM by checking the 'installed' status for it or
by checking timestamp (?? which is better)
CombinedBM: Collection of all Business item(s) whether installed or
uninstalled
3. Build BM from the filesystem
4. Compare the combinedBM state to the last combinedBM state
5. Compare the installation state to the OFS state
6. If conflict while comaprison at 3, raise the error
7. In all other case, install the BM List
"""
# Create old installation state from Installed Business Manager
installed_bm_list = self.getInstalledBusinessManagerList()
combined_installed_path_item = [item for bm
in installed_bm_list
for item in bm.objectValues()]
# Create BM for old installation state and update its path item list
old_installation_state = self.newContent(
portal_type='Business Manager',
title='Old Installation State',
temp_object=True,
)
for item in combined_installed_path_item:
item.isIndexable = ConstantGetter('isIndexable', value=False)
# Better to use new ids so that we don't end up in conflicts
new_id = old_installation_state.generateNewId()
old_installation_state._setObject(new_id, aq_base(item),
suppress_events=True)
forbidden_bm_title_list = ['Old Installation State',]
for bm in bm_list:
forbidden_bm_title_list.append(bm.title)
new_installed_bm_list = [l for l
in self.getInstalledBusinessManagerList()
if l.title not in forbidden_bm_title_list]
new_installed_bm_list.extend(bm_list)
combined_new_path_item = [item for bm
in new_installed_bm_list
for item in bm.objectValues()]
# Create BM for new installation state and update its path item list
new_installation_state = self.newContent(
portal_type='Business Manager',
title='New Installation State',
temp_object=True,
)
for item in combined_new_path_item:
item.isIndexable = ConstantGetter('isIndexable', value=False)
new_id = new_installation_state.generateNewId()
new_installation_state._setObject(new_id, aq_base(item),
suppress_events=True)
# Create installation process, which have the changes to be made in the
# OFS during installation. Importantly, it should also be a Business Manager
installation_process = self.newContent(
portal_type='Business Manager',
title='Installation Process',
temp_object=True,
)
# Reduce both old and new Installation State
old_installation_state.reduceBusinessManager()
new_installation_state.reduceBusinessManager()
# Get path list for old and new states
old_state_path_list = old_installation_state.getPathList()
new_state_path_list = new_installation_state.getPathList()
to_install_path_item_list = []
# Get the path which has been removed in new installation_state
removed_path_list = [path for path
in old_state_path_list
if path not in new_state_path_list]
# Add the removed path with negative sign in the to_install_path_item_list
for path in removed_path_list:
old_item = old_installation_state.getBusinessItemByPath(path)
old_item.setProperty('item_sign', '-1')
to_install_path_item_list.append(old_item)
# Reduce old_installation_state again as changes as new sub-objects maybe
# added to the old_installation_state
old_installation_state.reduceBusinessManager()
# XXX: At this point, we expect all the Business Manager objects as 'reduced',
# thus all the BusinessItem sub-objects should have single value
# Update hashes of item in old state before installation
for item in old_installation_state.objectValues():
# In case of Business Patch Item we need to update hash of both new and
# old value
if item.getPortalType() == 'Business Patch Item':
new_val = item._getOb('new_item')
old_val = item._getOb('old_item')
self.updateHash(new_val)
self.updateHash(old_val)
else:
self.updateHash(item)
# Path Item List for installation_process should be the difference between
# old and new installation state
for item in new_installation_state.objectValues():
# If the path has been removed, then add it with sign = -1
old_item = old_installation_state.getBusinessItemByPath(item.getProperty('item_path'))
# In case of Business Patch Item we need to update hash of both new and
# old value
if item.getPortalType() == 'Business Patch Item':
new_val = item._getOb('new_item')
old_val = item._getOb('old_item')
self.updateHash(new_val)
self.updateHash(old_val)
else:
self.updateHash(item)
if old_item:
to_be_installed_item = item
if old_item.getPortalType() == 'Business Patch Item':
# In case of Business Patch Item, we just need to compare the hash
# of old_item
old_item = old_item._getOb('old_item')
item = item._getOb('old_item')
# If the old_item exists, we match the hashes and if it differs, then
# add the new item
if old_item.getProperty('item_sha') != item.getProperty('item_sha'):
to_install_path_item_list.append(to_be_installed_item)
else:
to_install_path_item_list.append(item)
for item in to_install_path_item_list:
item.isIndexable = ConstantGetter('isIndexable', value=False)
new_id = new_installation_state.generateNewId()
installation_process._setObject(new_id, aq_base(item),
suppress_events=True)
change_list = self.compareOldStateToOFS(installation_process, old_installation_state)
if change_list:
change_list = [(l[0].item_path, l[1]) for l in change_list]
return change_list
def updateInstallationState(self, bm_list, force=1):
"""
First compare installation state and then install the final value
"""
change_list = self.compareInstallationState(bm_list)
if force:
to_install_path_list = [l[0] for l in change_list]
to_install_path_list = self.sortPathList(to_install_path_list)
# Install the path items with bm_list as context
self.installBusinessItemList(bm_list, to_install_path_list)
installMultipleBusinessManager = updateInstallationState
def installBusinessItemList(self, manager_list, item_path_list):
"""
Install Business Item/Business Property Item from the current Installation
Process given the change_list which carries the list of paths to be
installed
"""
LOG('INFO', 0, '%s' % [item_path_list])
# Create BM for new installation state and update its path item list
new_installation_state = self.newContent(
portal_type='Business Manager',
title='Final Installation State',
temp_object=True,
)
combined_new_path_item_list = [item for bm
in manager_list
for item in bm.objectValues()]
for item in combined_new_path_item_list:
item.isIndexable = ConstantGetter('isIndexable', value=False)
new_id = new_installation_state.generateNewId()
new_installation_state._setObject(new_id, aq_base(item),
suppress_events=True)
for path in item_path_list:
item = new_installation_state.getBusinessItemByPath(path)
if item is None:
raise ValueError("Couldn't find path in current Installation State")
item.install(new_installation_state)
# Update workflow history of the installed Business Manager(s)
# Get the 'business_manager_installation_workflow' as it is already
# bootstrapped and installed
portal_workflow = self.getPortalObject().portal_workflow
wf = portal_workflow._getOb('business_manager_installation_workflow')
# Change the installation state for all the BM(s) in manager_list.
for manager in manager_list:
wf._executeMetaTransition(manager, 'installed')
def calculateComparableHash(self, object, isProperty=False):
"""
Remove some attributes before comparing hashses
and return hash of the comparable object dict, in case the object is
an erp5 object.
Use shallow copy of the dict of the object at ZODB after removing
attributes which changes at small updation, like workflow_history,
uid, volatile attributes(which starts with _v)
# XXX: Comparable hash shouldn't be used for BusinessPatchItem as whole.
We can compare the old_value and new_value, but there shouldn't be hash
for the Patch Item.
"""
if isProperty:
obj_dict = object
# Have compatibilty between tuples and list while comparing as we face
# this situation a lot especially for list type properties
if isinstance(obj_dict, list):
obj_dict = tuple(obj_dict)
else:
klass = object.__class__
classname = klass.__name__
obj_dict = object.__dict__.copy()
# If the dict is empty, do calculate hash of None as it stays same on
# one platform and in any case its impossiblt to move live python
# objects from one seeion to another
if not bool(obj_dict):
return hash(None)
attr_set = {'_dav_writelocks', '_filepath', '_owner', '_related_index',
'last_id', 'uid', '_mt_index', '_count', '_tree',
'__ac_local_roles__', '__ac_local_roles_group_id_dict__',
'workflow_history', 'subject_set_uid_dict', 'security_uid_dict',
'filter_dict', '_max_uid'}
attr_set.update(('isIndexable',))
if classname in ('File', 'Image'):
attr_set.update(('_EtagSupport__etag', 'size'))
elif classname == 'Types Tool' and klass.__module__ == 'erp5.portal_type':
attr_set.add('type_provider_list')
for attr in object.__dict__.keys():
if attr in attr_set or attr.startswith('_cache_cookie_') or attr.startswith('_v'):
try:
del obj_dict[attr]
except AttributeError:
# XXX: Continue in cases where we want to delete some properties which
# are not in attribute list
# Raise an error
continue
# Special case for configuration instance attributes
if attr in ['_config', '_config_metadata']:
import collections
# Order the dictionary so that comparison can be correct
obj_dict[attr] = collections.OrderedDict(sorted(obj_dict[attr].items()))
if 'valid_tags' in obj_dict[attr]:
try:
obj_dict[attr]['valid_tags'] = collections.OrderedDict(sorted(obj_dict[attr]['valid_tags'].items()))
except AttributeError:
# This can occur in case the valid_tag object is PersistentList
pass
if 'data' in obj_dict:
try:
obj_dict['data'] = obj_dict.get('data').__dict__
except AttributeError:
pass
obj_sha = hash(pprint.pformat(obj_dict))
return obj_sha
def sortPathList(self, path_list):
"""
Custom sort for path_list according to the priorities of paths
"""
def comparePath(path):
split_path_list = path.split('/')
# Paths with property item should have the least priority as they should
# be installed after installing the object only
if '#' in path:
return 11
if len(split_path_list) == 2 and split_path_list[0] in ('portal_types', 'portal_categories'):
return 1
# portal_transforms objects needs portal_components installed first so
# as to register the modules
if len(split_path_list) == 2 and split_path_list[0] == 'portal_transforms':
return 12
if len(split_path_list) > 2:
return 10
if len(split_path_list) == 1:
return 2
return 5
return sorted(path_list, key=comparePath)
def compareOldStateToOFS(self, installation_process, old_state):
# Get the paths about which we are concerned about
to_update_path_list = installation_process.getPathList()
portal = self.getPortalObject()
# List to store what changes will be done to which path. Here we compare
# with all the states (old version, new version and state of object at ZODB)
change_list = []
to_update_path_list = self.sortPathList(to_update_path_list)
for path in to_update_path_list:
try:
# Better to check for status of BusinessPatchItem separately as it
# can contain both BusinessItem as well as BusinessPropertyItem
new_item = installation_process.getBusinessItemByPath(path)
if new_item.getPortalType() == 'Business Patch Item':
patch_item = new_item
# If the value is in ZODB, then compare it to the old_value
if '#' in str(path):
isProperty = True
relative_url, property_id = path.split('#')
obj = portal.restrictedTraverse(relative_url)
property_value = obj.getProperty(property_id)
if not property_value:
raise KeyError
property_type = obj.getPropertyType(property_id)
obj = property_value
else:
# If the path is path on an object and not of a property
isProperty = False
obj = portal.restictedTraverse(path)
obj_sha = self.calculateComparableHash(obj, isProperty)
# Get the sha of new_item from the BusinessPatchItem object
new_item_sha = patch_item._getOb('new_item').getProperty('item_sha')
old_item_sha = patch_item._getOb('old_item').getProperty('item_sha')
if new_item_sha == obj_sha:
# If the new_item in the patch is same as the one at ZODB, do
# nothing
continue
elif old_item_sha == obj_sha:
change_list.append((patch_item._getOb('new_item'), 'Adding'))
else:
change_list.append((patch_item._getOb('new_item'), 'Removing'))
if '#' in str(path):
isProperty = True
relative_url, property_id = path.split('#')
obj = portal.restrictedTraverse(relative_url)
property_value = obj.getProperty(property_id)
# If the value at ZODB for the property is none, raise KeyError
# This is important to have compatibility between the way we check
# path as well as property. Otherwise, if we install a new property,
# we are always be getting an Error that there is change made at
# ZODB for this property
if not property_value:
raise KeyError
property_type = obj.getPropertyType(property_id)
obj = property_value
else:
isProperty = False
# XXX: Hardcoding because of problem with 'resource' trying to access
# the resource via acqusition. Should be removed completely before
# merging (DONT PUSH THIS)
if path == 'portal_categories/resource':
path_list = path.split('/')
container_path = path_list[:-1]
object_id = path_list[-1]
container = portal.restrictedTraverse(container_path)
obj = container._getOb(object_id)
else:
obj = portal.restrictedTraverse(path)
obj_sha = self.calculateComparableHash(obj, isProperty)
# Get item at old state
old_item = old_state.getBusinessItemByPath(path)
# Check if there is an object at old state at this path
if old_item:
# Compare hash with ZODB
if old_item.getProperty('item_sha') == obj_sha:
# No change at ZODB on old item, so get the new item
new_item = installation_process.getBusinessItemByPath(path)
# Compare new item hash with ZODB
if new_item.getProperty('item_sha') == obj_sha:
if int(new_item.getProperty('item_sign')) == -1:
# If the sign is negative, remove the value from the path
change_list.append((new_item, 'Removing'))
else:
# If same hash, and +1 sign, do nothing
continue
else:
# Install the new_item
change_list.append((new_item, 'Adding'))
else:
# Change at ZODB, so get the new item
new_item = installation_process.getBusinessItemByPath(path)
# Compare new item hash with ZODB
if new_item.getProperty('item_sha') == obj_sha:
# If same hash, do nothing
continue
else:
# Trying to update change at ZODB
change_list.append((new_item, 'Updating'))
else:
# Object created at ZODB by the user
# Compare with the new_item
new_item = installation_process.getBusinessItemByPath(path)
if new_item.getProperty('item_sha') == obj_sha:
# If same hash, do nothing
continue
else:
# Trying to update change at ZODB
change_list.append((new_item, 'Updating'))
except (AttributeError, KeyError) as e:
# Get item at old state
old_item = old_state.getBusinessItemByPath(path)
# Check if there is an object at old state at this path
if old_item:
# This means that the user had removed the object at this path
# Check what the sign is for the new_item
new_item = installation_process.getBusinessItemByPath(path)
# Check sign of new_item
if int(new_item.getProperty('item_sign')) == 1:
# Object at ZODB has been removed by the user
change_list.append((new_item, 'Adding'))
else:
# If there is no item at old state, install the new_item
new_item = installation_process.getBusinessItemByPath(path)
# XXX: Hack for not trying to install the sub-objects from zexp,
# This should rather be implemented while exporting the object,
# where we shouldn't export sub-objects in the zexp
if not isProperty:
try:
value = new_item.objectValues()[0]
except IndexError:
continue
# Installing a new item
change_list.append((new_item, 'Adding'))
return change_list
def getInstalledBusinessManagerList(self):
bm_list = self.objectValues(portal_type='Business Manager')
installed_bm_list = [bm for bm in bm_list
if bm.getInstallationState() == 'installed']
return installed_bm_list
def getInstalledBusinessManagerTitleList(self):
installed_bm_list = self.getInstalledBusinessManagerList()
if not len(installed_bm_list):
return []
installed_bm_title_list = [bm.title for bm in installed_bm_list]
return installed_bm_title_list
security.declareProtected(Permissions.ManagePortal,
'getBusinessTemplateUrl')
def getBusinessTemplateUrl(self, base_url_list, bt5_title):
......
......@@ -54,10 +54,12 @@ from Tool import CategoryTool, SimulationTool, RuleTool, IdTool, TemplateTool,\
CertificateAuthorityTool, InotifyTool, TaskDistributionTool,\
DiffTool
import ERP5Site
from Document import PythonScript, SQLMethod
from Document import PythonScript, BusinessManager
object_classes = ( ERP5Site.ERP5Site,
PythonScript.PythonScriptThroughZMI,
SQLMethod.SQLMethod,
BusinessManager.BusinessItem,
BusinessManager.BusinessPropertyItem,
BusinessManager.BusinessPatchItem,
)
portal_tools = ( CategoryTool.CategoryTool,
SimulationTool.SimulationTool,
......
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