# -*- coding: utf-8 -*- ############################################################################## # # Copyright (c) 2002-2003 Nexedi SARL and Contributors. All Rights Reserved. # Jean-Paul Smets-Solanes <jp@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. # ############################################################################## from struct import unpack from copy import copy import warnings import types import thread, threading from Products.ERP5Type.Globals import InitializeClass, DTMLFile from AccessControl import ClassSecurityInfo from AccessControl.Permission import pname, Permission from AccessControl.PermissionRole import rolesForPermissionOn from AccessControl.SecurityManagement import getSecurityManager from AccessControl.ZopeGuards import guarded_getattr from Acquisition import aq_base, aq_inner, aq_acquire, aq_chain from DateTime import DateTime import OFS.History from OFS.SimpleItem import SimpleItem from OFS.PropertyManager import PropertyManager from persistent.TimeStamp import TimeStamp from zExceptions import NotFound, Unauthorized from ZopePatch import ERP5PropertyManager from Products.CMFCore.PortalContent import PortalContent from Products.CMFCore.Expression import Expression from Products.CMFCore.utils import getToolByName, _setCacheHeaders, _ViewEmulator from Products.CMFCore.WorkflowCore import ObjectDeleted, ObjectMoved from Products.CMFCore.CMFCatalogAware import CMFCatalogAware from Products.DCWorkflow.Transitions import TRIGGER_WORKFLOW_METHOD, TRIGGER_USER_ACTION from Products.ERP5Type import _dtmldir from Products.ERP5Type import PropertySheet from Products.ERP5Type import interfaces from Products.ERP5Type import Permissions from Products.ERP5Type.patches.CMFCoreSkinnable import SKINDATA, skinResolve from Products.ERP5Type.Utils import UpperCase from Products.ERP5Type.Utils import convertToUpperCase, convertToMixedCase from Products.ERP5Type.Utils import createExpressionContext, simple_decorator from Products.ERP5Type.Accessor.Accessor import Accessor from Products.ERP5Type.Accessor.Constant import PropertyGetter as ConstantGetter from Products.ERP5Type.Accessor.TypeDefinition import list_types from Products.ERP5Type.Accessor import Base as BaseAccessor from Products.ERP5Type.mixin.property_translatable import PropertyTranslatableBuiltInDictMixIn from Products.ERP5Type.XMLExportImport import Base_asXML from Products.ERP5Type.Cache import CachingMethod, clearCache, getReadOnlyTransactionCache from Accessor import WorkflowState from Products.ERP5Type.Log import log as unrestrictedLog from Products.ERP5Type.TransactionalVariable import getTransactionalVariable from Products.ERP5Type.Accessor.TypeDefinition import type_definition from CopySupport import CopyContainer, CopyError,\ tryMethodCallWithTemporaryPermission from Errors import DeferredCatalogError, UnsupportedWorkflowMethod from Products.CMFActivity.ActiveObject import ActiveObject from Products.ERP5Type.Accessor.Accessor import Accessor as Method from Products.ERP5Type.Accessor.TypeDefinition import asDate from Products.ERP5Type.Message import Message from Products.ERP5Type.ConsistencyMessage import ConsistencyMessage from Products.ERP5Type.UnrestrictedMethod import UnrestrictedMethod from zope.interface import classImplementsOnly, implementedBy from string import join import sys, re from cStringIO import StringIO from socket import gethostname, gethostbyaddr import random import inspect from pprint import pformat import zope.interface from ZODB.POSException import ConflictError from zLOG import LOG, INFO, ERROR, WARNING _MARKER = [] global registered_workflow_method_set wildcard_interaction_method_id_match = re.compile(r'[[.?*+{(\\]').search workflow_method_registry = [] # XXX A set() would be better but would require a hash in WorkflowMethod class def resetRegisteredWorkflowMethod(portal_type=None): """ TODO: unwrap workflow methos which were standard methods initially """ for method in workflow_method_registry: method.reset(portal_type=portal_type) class WorkflowMethod(Method): def __init__(self, method, id=None, reindex=1): """ method - a callable object or a method id - the workflow transition id. This is useful to emulate "old" CMF behaviour but is somehow inconsistent with the new registration based approach implemented here. We store id as _transition_id and use it to register the transition for each portal type and each workflow for which it is applicable. """ self._m = method if id is None: self._transition_id = method.__name__ else: self._transition_id = id # Only publishable methods can be published as interactions # A pure private method (ex. _doNothing) can not be published # This is intentional to prevent methods such as submit, share to # be called from a URL. If someone can show that this way # is wrong (ex. for remote operation of a site), let us know. if not method.__name__.startswith('_'): self.__name__ = method.__name__ for func_id in ['func_code', 'func_defaults', 'func_dict', 'func_doc', 'func_globals', 'func_name']: setattr(self, func_id, getattr(method, func_id, None)) self._invoke_once = {} self._invoke_always = {} # Store in a dict all workflow IDs which require to # invoke wrapWorkflowMethod at every call # during the same transaction def getTransitionId(self): return self._transition_id def __call__(self, instance, *args, **kw): """ Invoke the wrapped method, and deal with the results. """ if getattr(self, '__name__', None) in ('getPhysicalPath', 'getId'): # To prevent infinite recursion, 2 methods must have special treatment # this is clearly not the best way to implement this but it is # already better than what we had. I (JPS) would prefer to use # critical sections in this part of the code and a # thread variable which tells in which semantic context the code # should ne executed. - XXX return self._m(instance, *args, **kw) # New implementation does not use any longer wrapWorkflowMethod # but directly calls the workflow methods try: wf = getattr(instance.getPortalObject(), 'portal_workflow') except AttributeError: # XXX instance is unwrapped(no acquisition) # XXX I must think that what is a correct behavior.(Yusei) return self._m(instance, *args, **kw) # Build a list of transitions which may need to be invoked instance_path = instance.getPhysicalPath() portal_type = instance.portal_type transactional_variable = getTransactionalVariable() invoke_once_dict = self._invoke_once.get(portal_type, {}) valid_invoke_once_item_list = [] # Only keep those transitions which were never invoked once_transition_dict = {} for wf_id, transition_list in invoke_once_dict.iteritems(): valid_transition_list = [] for transition_id in transition_list: once_transition_key = ('Products.ERP5Type.Base.WorkflowMethod.__call__', wf_id, transition_id, instance_path) once_transition_dict[(wf_id, transition_id)] = once_transition_key if once_transition_key not in transactional_variable: valid_transition_list.append(transition_id) if valid_transition_list: valid_invoke_once_item_list.append((wf_id, valid_transition_list)) candidate_transition_item_list = valid_invoke_once_item_list + \ self._invoke_always.get(portal_type, {}).items() #LOG('candidate_transition_item_list %s' % self.__name__, 0, str(candidate_transition_item_list)) # Try to return immediately if there are no transition to invoke if not candidate_transition_item_list: return apply(self.__dict__['_m'], (instance,) + args, kw) # Prepare a list of transitions which should be invoked. # This list is based on the results of isWorkflowMethodSupported. # An interaction is ignored if the guard prevents execution. # Otherwise, an exception is raised if the workflow transition does not # exist from the current state, or if the guard rejects it. valid_transition_item_list = [] for wf_id, transition_list in candidate_transition_item_list: candidate_workflow = wf[wf_id] valid_list = [] for transition_id in transition_list: if candidate_workflow.isWorkflowMethodSupported(instance, transition_id): valid_list.append(transition_id) once_transition_key = once_transition_dict.get((wf_id, transition_id)) if once_transition_key: # a run-once transition, prevent it from running again in # the same transaction transactional_variable[once_transition_key] = 1 elif candidate_workflow.__class__.__name__ == 'DCWorkflowDefinition': raise UnsupportedWorkflowMethod(instance, wf_id, transition_id) # XXX Keep the log for projects that needs to comment out # the previous line. LOG("WorkflowMethod.__call__", ERROR, "Transition %s/%s on %r is ignored. Current state is %r." % (wf_id, transition_id, instance, candidate_workflow._getWorkflowStateOf(instance, id_only=1))) if valid_list: valid_transition_item_list.append((wf_id, valid_list)) #LOG('valid_transition_item_list %s' % self.__name__, 0, str(valid_transition_item_list)) # Call whatever must be called before changing states for wf_id, transition_list in valid_transition_item_list: wf[wf_id].notifyBefore(instance, transition_list, args=args, kw=kw) # Compute expected result result = apply(self.__dict__['_m'], (instance,) + args, kw) # Change the state of statefull workflows for wf_id, transition_list in valid_transition_item_list: try: wf[wf_id].notifyWorkflowMethod(instance, transition_list, args=args, kw=kw) except ObjectDeleted: # Re-raise with a different result. raise ObjectDeleted(result) except ObjectMoved, ex: # Re-raise with a different result. raise ObjectMoved(ex.getNewObject(), result) # Call whatever must be called after changing states for wf_id, transition_list in valid_transition_item_list: wf[wf_id].notifySuccess(instance, transition_list, result, args=args, kw=kw) # Return result finally return result # Interactions should not be disabled during normal operation. Only in very # rare and specific cases like data migration. That's why it is implemented # with temporary monkey-patching, instead of slowing down __call__ with yet # another condition. _do_interaction = __call__ _no_interaction_lock = threading.Lock() _no_interaction_log = None _no_interaction_thread_id = None def _no_interaction(self, *args, **kw): if WorkflowMethod._no_interaction_thread_id != thread.get_ident(): return self._do_interaction(*args, **kw) log = "skip interactions for %r" % args[0] if WorkflowMethod._no_interaction_log != log: WorkflowMethod._no_interaction_log = log LOG("WorkflowMethod", INFO, log) return self.__dict__['_m'](*args, **kw) @staticmethod @simple_decorator def disable(func): def wrapper(*args, **kw): thread_id = thread.get_ident() if WorkflowMethod._no_interaction_thread_id == thread_id: return func(*args, **kw) WorkflowMethod._no_interaction_lock.acquire() try: WorkflowMethod._no_interaction_thread_id = thread_id WorkflowMethod.__call__ = WorkflowMethod.__dict__['_no_interaction'] return func(*args, **kw) finally: WorkflowMethod.__call__ = WorkflowMethod.__dict__['_do_interaction'] WorkflowMethod._no_interaction_thread_id = None WorkflowMethod._no_interaction_lock.release() return wrapper @staticmethod def disabled(): return WorkflowMethod._no_interaction_lock.locked() def registerTransitionAlways(self, portal_type, workflow_id, transition_id): """ Transitions registered as always will be invoked always """ transition_list = self._invoke_always.setdefault(portal_type, {}).setdefault(workflow_id, []) if transition_id not in transition_list: transition_list.append(transition_id) self.register() def registerTransitionOncePerTransaction(self, portal_type, workflow_id, transition_id): """ Transitions registered as one per transactions will be invoked only once per transaction """ transition_list = self._invoke_once.setdefault(portal_type, {}).setdefault(workflow_id, []) if transition_id not in transition_list: transition_list.append(transition_id) self.register() def register(self): """ Registers the method so that _aq_reset may later reset it """ workflow_method_registry.append(self) def reset(self, portal_type=None): """ Reset the list of registered interactions or transitions """ if portal_type: self._invoke_once[portal_type] = {} self._invoke_always[portal_type] = {} else: self._invoke_once = {} self._invoke_always = {} def _aq_reset(): warnings.warn("_aq_reset is deprecated in favor of "\ "portal_types.resetDynamicDocumentsOnceAtTransactionBoundary, "\ "calling this method affects greatly performances", DeprecationWarning, stacklevel=2) # Callers expect to re-generates accessors right now, so call # resetDynamicDocuments to maintain backward-compatibility from Products.ERP5.ERP5Site import getSite getSite().portal_types.resetDynamicDocuments() class PropertyHolder(object): isRADContent = 1 WORKFLOW_METHOD_MARKER = ('Base._doNothing',) RESERVED_PROPERTY_SET = set(('_constraints', '_properties', '_categories', '__implements__', 'property_sheets', '__ac_permissions__', '_erp5_properties')) def __init__(self, name='PropertyHolder'): self.__name__ = name self.security = ClassSecurityInfo() # We create a new security info object self.workflow_method_registry = {} self._categories = [] self._properties = [] self._constraints = [] self.constraints = [] def _getPropertyHolderItemList(self): return [x for x in self.__dict__.items() if x[0] not in PropertyHolder.RESERVED_PROPERTY_SET] def registerWorkflowMethod(self, id, wf_id, tr_id, once_per_transaction=0): portal_type = self.portal_type workflow_method = getattr(self, id, None) if workflow_method is None: # XXX: We should pass 'tr_id' as second parameter. workflow_method = WorkflowMethod(Base._doNothing) setattr(self, id, workflow_method) if once_per_transaction: workflow_method.registerTransitionOncePerTransaction(portal_type, wf_id, tr_id) else: workflow_method.registerTransitionAlways(portal_type, wf_id, tr_id) def declareProtected(self, permission, accessor_name): """ It is possible to gain 30% of accessor RAM footprint by postponing security declaration. WARNING: we optimize size by not setting security if it is the same as the default. This may be a bit dangerous if classes use another default security. """ if permission not in (Permissions.AccessContentsInformation, Permissions.ModifyPortalContent): self.security.declareProtected(permission, accessor_name) # Inspection methods def getAccessorMethodItemList(self): """ Return a list of tuple (id, method) for every accessor """ accessor_method_item_list = [] accessor_method_item_list_append = accessor_method_item_list.append for x, y in self._getPropertyHolderItemList(): if isinstance(y, tuple): if y is PropertyHolder.WORKFLOW_METHOD_MARKER or x == '__ac_permissions__': continue if len(y) == 0: continue if not issubclass(y[0], Accessor): continue elif not isinstance(y, Accessor): continue accessor_method_item_list_append((x, y)) return accessor_method_item_list def getAccessorMethodIdList(self): """ Return the list of accessor IDs """ return [ x[0] for x in self.getAccessorMethodItemList() ] def getWorkflowMethodItemList(self): """ Return a list of tuple (id, method) for every workflow method """ return [x for x in self._getPropertyHolderItemList() if isinstance(x[1], WorkflowMethod) or (isinstance(x[1], types.TupleType) and x[1] is PropertyHolder.WORKFLOW_METHOD_MARKER)] def getWorkflowMethodIdList(self): """ Return the list of workflow method IDs """ return [x[0] for x in self.getWorkflowMethodItemList()] def _getClassDict(self, klass, inherited=1, local=1): """ Return a dict for every property of a class """ result = {} if inherited: for parent in reversed(klass.mro()): result.update(parent.__dict__) if local: result.update(klass.__dict__) return result def _getClassItemList(self, klass, inherited=1, local=1): """ Return a list of tuple (id, method) for every property of a class """ return self._getClassDict(klass, inherited=inherited, local=local).items() def getClassMethodItemList(self, klass, inherited=1, local=1): """ Return a list of tuple (id, method, module) for every class method """ return [x for x in self._getClassItemList(klass, inherited=inherited, local=local) if callable(x[1])] def getClassMethodIdList(self, klass, inherited=1, local=1): """ Return the list of class method IDs """ return [x[0] for x in self.getClassMethodItemList(klass, inherited=inherited, local=local)] def getClassPropertyItemList(self, klass, inherited=1, local=1): """ Return a list of tuple (id, method) for every class method """ return [x for x in self._getClassItemList(klass, inherited=inherited, local=local) if not callable(x[1])] def getClassPropertyIdList(self, klass, inherited=1, local=1): """ Return the list of class method IDs """ return [x[0] for x in self.getClassPropertyItemList(klass, inherited=inherited, local=local)] def getClassPropertyList(klass): ps_list = getattr(klass, 'property_sheets', ()) ps_list = tuple(ps_list) for super_klass in klass.__bases__: if getattr(super_klass, 'isRADContent', 0): ps_list = ps_list + tuple([p for p in getClassPropertyList(super_klass) if p not in ps_list]) return ps_list def initializePortalTypeDynamicWorkflowMethods(ptype_klass, portal_workflow): """We should now make sure workflow methods are defined and also make sure simulation state is defined.""" # aq_inner is required to prevent extra name lookups from happening # infinitely. For instance, if a workflow is missing, and the acquisition # wrapper contains an object with _aq_dynamic defined, the workflow id # is looked up with _aq_dynamic, thus causes infinite recursions. portal_workflow = aq_inner(portal_workflow) portal_type = ptype_klass.__name__ dc_workflow_dict = dict() interaction_workflow_dict = dict() for wf in portal_workflow.getWorkflowsFor(portal_type): wf_id = wf.id wf_type = wf.__class__.__name__ if wf_type == "DCWorkflowDefinition": # Create state var accessor # and generate methods that support the translation of workflow states state_var = wf.variables.getStateVar() for method_id, getter in ( ('get%s' % UpperCase(state_var), WorkflowState.Getter), ('get%sTitle' % UpperCase(state_var), WorkflowState.TitleGetter), ('getTranslated%s' % UpperCase(state_var), WorkflowState.TranslatedGetter), ('getTranslated%sTitle' % UpperCase(state_var), WorkflowState.TranslatedTitleGetter), ('serialize%s' % UpperCase(state_var), WorkflowState.SerializeGetter), ): if not hasattr(ptype_klass, method_id): method = getter(method_id, wf_id) # Attach to portal_type ptype_klass.registerAccessor(method, Permissions.AccessContentsInformation) storage = dc_workflow_dict transitions = wf.transitions elif wf_type == "InteractionWorkflowDefinition": storage = interaction_workflow_dict transitions = wf.interactions else: continue # extract Trigger transitions from workflow definitions for later transition_id_set = set(transitions.objectIds()) trigger_dict = dict() for tr_id in transition_id_set: tdef = transitions.get(tr_id, None) if tdef.trigger_type == TRIGGER_WORKFLOW_METHOD: trigger_dict[tr_id] = tdef storage[wf_id] = (transition_id_set, trigger_dict) for wf_id, v in dc_workflow_dict.iteritems(): transition_id_set, trigger_dict = v for tr_id, tdef in trigger_dict.iteritems(): method_id = convertToMixedCase(tr_id) method = getattr(ptype_klass, method_id, _MARKER) if method is _MARKER: ptype_klass.security.declareProtected(Permissions.AccessContentsInformation, method_id) ptype_klass.registerWorkflowMethod(method_id, wf_id, tr_id) continue # Wrap method if not callable(method): LOG('initializePortalTypeDynamicWorkflowMethods', 100, 'WARNING! Can not initialize %s on %s' % \ (method_id, portal_type)) continue if not isinstance(method, WorkflowMethod): method = WorkflowMethod(method) setattr(ptype_klass, method_id, method) else: # We must be sure that we # are going to register class defined # workflow methods to the appropriate transition transition_id = method.getTransitionId() if transition_id in transition_id_set: method.registerTransitionAlways(portal_type, wf_id, transition_id) method.registerTransitionAlways(portal_type, wf_id, tr_id) if not interaction_workflow_dict: return # all methods in mro of portal type class: that contains all # workflow methods and accessors you could possibly ever need class_method_id_list = ptype_klass.getClassMethodIdList(ptype_klass) interaction_queue = [] # XXX This part is (more or less...) a copy and paste for wf_id, v in interaction_workflow_dict.iteritems(): transition_id_set, trigger_dict = v for tr_id, tdef in trigger_dict.iteritems(): # Check portal type filter if (tdef.portal_type_filter is not None and \ portal_type not in tdef.portal_type_filter): continue # Check portal type group filter if tdef.portal_type_group_filter is not None: getPortalGroupedTypeSet = portal_workflow.getPortalObject()._getPortalGroupedTypeSet if not any(portal_type in getPortalGroupedTypeSet(portal_type_group) for portal_type_group in tdef.portal_type_group_filter): continue for imethod_id in tdef.method_id: if wildcard_interaction_method_id_match(imethod_id): # Interactions workflows can use regexp based wildcard methods # XXX What happens if exception ? method_id_matcher = re.compile(imethod_id).match # queue transitions using regexps for later examination interaction_queue.append((wf_id, tr_id, transition_id_set, tdef.once_per_transaction, method_id_matcher)) # XXX - class stuff is missing here method_id_list = filter(method_id_matcher, class_method_id_list) else: # Single method # XXX What if the method does not exist ? # It's not consistent with regexp based filters. method_id_list = [imethod_id] for method_id in method_id_list: method = getattr(ptype_klass, method_id, _MARKER) if method is _MARKER: # set a default security, if this method is not already # protected. if method_id not in ptype_klass.security.names: ptype_klass.security.declareProtected( Permissions.AccessContentsInformation, method_id) ptype_klass.registerWorkflowMethod(method_id, wf_id, tr_id, tdef.once_per_transaction) continue # Wrap method if not callable(method): LOG('initializePortalTypeDynamicWorkflowMethods', 100, 'WARNING! Can not initialize %s on %s' % \ (method_id, portal_type)) continue if not isinstance(method, WorkflowMethod): method = WorkflowMethod(method) setattr(ptype_klass, method_id, method) else: # We must be sure that we # are going to register class defined # workflow methods to the appropriate transition transition_id = method.getTransitionId() if transition_id in transition_id_set: method.registerTransitionAlways(portal_type, wf_id, transition_id) if tdef.once_per_transaction: method.registerTransitionOncePerTransaction(portal_type, wf_id, tr_id) else: method.registerTransitionAlways(portal_type, wf_id, tr_id) if not interaction_queue: return # the only methods that could have appeared since last check are # workflow methods # TODO we could just queue the ids of methods that are attached to the # portal type class in the previous loop, to improve performance new_method_set = set(ptype_klass.getWorkflowMethodIdList()) added_method_set = new_method_set.difference(class_method_id_list) # We need to run this part twice in order to handle interactions of interactions # ex. an interaction workflow creates a workflow method which matches # the regexp of another interaction workflow for wf_id, tr_id, transition_id_set, once, method_id_matcher in interaction_queue: for method_id in filter(method_id_matcher, added_method_set): # method must already exist and be a workflow method method = getattr(ptype_klass, method_id) transition_id = method.getTransitionId() if transition_id in transition_id_set: method.registerTransitionAlways(portal_type, wf_id, transition_id) if once: method.registerTransitionOncePerTransaction(portal_type, wf_id, tr_id) else: method.registerTransitionAlways(portal_type, wf_id, tr_id) class Base( CopyContainer, PortalContent, ActiveObject, OFS.History.Historical, ERP5PropertyManager, PropertyTranslatableBuiltInDictMixIn ): """ This is the base class for all ERP5 Zope objects. It defines object attributes which are necessary to implement relations and data synchronisation id -- the standard object id rid -- the standard object id in the master ODB the object was subsribed from uid -- a global object id which is unique sid -- the id of the subscribtion/syncrhonisation object which this object was generated from sync_status -- The status of this document in the synchronization process (NONE, SENT, ACKNOWLEDGED, SYNCHRONIZED) could work as a workflow but CPU expensive """ meta_type = 'ERP5 Base Object' portal_type = 'Base Object' #_local_properties = () # no need since getattr isRADContent = 1 # isPortalContent = ConstantGetter('isPortalContent', value=True) isCapacity = ConstantGetter('isCapacity', value=False) isCategory = ConstantGetter('isCategory', value=False) isBaseCategory = ConstantGetter('isBaseCategory', value=False) isInventoryMovement = ConstantGetter('isInventoryMovement', value=False) isDelivery = ConstantGetter('isDelivery', value=False) isInventory = ConstantGetter('isInventory', value=False) # If set to 0, reindexing will not happen (useful for optimization) isIndexable = ConstantGetter('isIndexable', value=True) isPredicate = ConstantGetter('isPredicate', value=False) isTemplate = ConstantGetter('isTemplate', value=False) isDocument = ConstantGetter('isDocument', value=False) isTempDocument = ConstantGetter('isTempDocument', value=False) # Dynamic method acquisition system (code generation) aq_method_generated = set() aq_method_generating = [] aq_portal_type = {} aq_related_generated = 0 # Declarative security - in ERP5 we use AccessContentsInformation to # define the right of accessing content properties as opposed # to view which is the right to view the object with a form security = ClassSecurityInfo() security.declareObjectProtected(Permissions.AccessContentsInformation) # Declarative properties property_sheets = ( PropertySheet.Base, ) # Declarative interfaces zope.interface.implements(interfaces.ICategoryAccessProvider, interfaces.IValueAccessProvider, ) # We want to use a default property view manage_main = manage_propertiesForm = DTMLFile( 'properties', _dtmldir ) manage_main._setName('manage_main') manage_options = ( PropertyManager.manage_options + SimpleItem.manage_options + OFS.History.Historical.manage_options + CMFCatalogAware.manage_options ) # Place for all is... method security.declareProtected(Permissions.AccessContentsInformation, 'isMovement') def isMovement(self): return 0 security.declareProtected( Permissions.ModifyPortalContent, 'setTitle' ) def setTitle(self, value): """ sets the title. (and then reindexObject)""" self._setTitle(value) self.reindexObject() security.declareProtected( Permissions.ModifyPortalContent, 'setDescription' ) def setDescription(self, value): """ Set the description and reindex. Overrides CMFCore/PortalFolder.py implementation, which does not reindex and is guarded by inappropriate security check. """ self._setDescription(value) self.reindexObject() security.declarePublic('provides') def provides(cls, interface_name): """ Check if the current class provides a particular interface from ERP5Type's interfaces registry """ interface = getattr(interfaces, interface_name, None) if interface is not None: return interface.implementedBy(cls) return False provides = classmethod(CachingMethod(provides, 'Base.provides', cache_factory='erp5_ui_long')) def _aq_key(self): return (self.portal_type, self.__class__) def _propertyMap(self, local_properties=False): """ Method overload - properties are now defined on the ptype """ property_list = [] # Get all the accessor holders for this portal type if not local_properties: property_list += self.__class__.getAccessorHolderPropertyList() property_list += getattr(self, '_local_properties', []) return tuple(property_list) def manage_historyCompare(self, rev1, rev2, REQUEST, historyComparisonResults=''): return Base.inheritedAttribute('manage_historyCompare')( self, rev1, rev2, REQUEST, historyComparisonResults=OFS.History.html_diff( pformat(rev1.__dict__), pformat(rev2.__dict__))) def _aq_dynamic(self, id): # ahah! disabled, thanks to portal type classes return None # Constructor def __init__(self, id, uid=None, rid=None, sid=None, **kw): self.id = id if uid is not None : self.uid = uid # Else it will be generated when we need it self.sid = sid # XXX This is necessary to override getId which is also defined in SimpleItem. security.declareProtected( Permissions.AccessContentsInformation, 'getId' ) getId = BaseAccessor.Getter('getId', 'id', 'string') # Debug def getOid(self): """ Return ODB oid """ return self._p_oid def getOidRepr(self): """ Return ODB oid, in an 'human' readable form. """ from ZODB.utils import oid_repr return oid_repr(self._p_oid) def getSerial(self): """Return ODB Serial.""" return self._p_serial def getHistorySerial(self): """Return ODB Serial, in the same format used for history keys""" return '.'.join([str(x) for x in unpack('>HHHH', self._p_serial)]) # Utils def _getCategoryTool(self): return aq_inner(self.getPortalObject().portal_categories) def _getTypesTool(self): return aq_inner(self.getPortalObject().portal_types) def _doNothing(self, *args, **kw): # A method which does nothing (and can be used to build WorkflowMethods which trigger worklow transitions) pass # Generic accessor def _getDefaultAcquiredProperty(self, key, default_value, null_value, acquisition_object_id=None, base_category=None, portal_type=None, copy_value=0, mask_value=0, accessor_id=None, depends=None, storage_id=None, alt_accessor_id=None, is_list_type=0, is_tales_type=0, checked_permission=None): """ This method implements programmable acquisition of values in ERP5. The principle is that some object attributes should be looked up, copied or synchronized from the values of another object which relates to the first thereof. The parameters which define value acquisition are: base_category -- a single base category or a list of base categories to look up related objects portal_type -- a single portal type or a list of portal types to filter the list of related objects copy_value -- if set to 1, the looked up value will be copied as an attribute of self mask_value -- if set to 1, the value of the attribute of self has priority on the looked up value accessor_id -- the id of the accessor to call on the related filtered objects depends -- a list of parameters to propagate in the look up process acquisition_object_id -- List of object Ids where look up properties before looking up on acquired objects The purpose of copy_value / mask_value is to solve issues related to relations and synchronisation of data. copy_value determines if a value should be copied as an attribute of self. Copying a value is useful for example when we do invoices and want to remember the price at a given point of time. mask_value allows to give priority to the value holded by self, rather than to the lookup through related objects. This is for example useful for invoices (again) for which we want the value not to change in time. Another case is the case of independent modules on multiple Zope. If for example a sales opportunity modules runs on a Zope No1 and an Organisation module runs on a Zope No 2. We want to enter peoples's names on the Zope No1. They will be entered as strings and stored as such in attributes. When such opportunities are synchronized on the Zope No 2, we want to be able to augment content locally by adding some category information (in order to say for example that M. Lawno is client/person/23) and eventually want M. Lawno to be displayed as "James Lawno". So, we want to give priority to the looked up attribute rather than to the attribute. However, we may want Zope No 1 to still display "James Lawno" as "M. Lawno". This means that we do not want to synchronize back this attribute. Other case : we add relation after entering information... Other case : we want to change the phone number of a related object without going to edit the related object """ # Push context to prevent loop tv = getTransactionalVariable() if isinstance(portal_type, list): portal_type = tuple(portal_type) elif portal_type is None: portal_type = () acquisition_key = ('_getDefaultAcquiredProperty', self.getPath(), key, acquisition_object_id, base_category, portal_type, copy_value, mask_value, accessor_id, depends, storage_id, alt_accessor_id, is_list_type, is_tales_type, checked_permission) if acquisition_key in tv: return null_value[0] tv[acquisition_key] = 1 try: if storage_id is None: storage_id=key #LOG("Get Acquired Property storage_id",0,str(storage_id)) # Test presence of attribute without acquisition # if present, get it in its context, thus we keep acquisition if # returned value is an object d = getattr(aq_base(self), storage_id, _MARKER) if d is not _MARKER: value = getattr(self, storage_id, None) else: value = None local_value = value # If we hold an attribute and mask_value is set, return the attribute if mask_value and value not in null_value: # Pop context if is_tales_type: expression = Expression(value) econtext = createExpressionContext(self) return expression(econtext) else: if is_list_type: # We must provide the first element of the acquired list if value in null_value: result = None else: if isinstance(value, (list, tuple)): if len(value) is 0: result = None else: result = value[0] else: result = value else: # Value is a simple type result = value return result #Look at acquisition object before call acquisition if acquisition_object_id is not None: value = None if isinstance(acquisition_object_id, str): acquisition_object_id = tuple(acquisition_object_id) for object_id in acquisition_object_id: try: value = self[object_id] if value not in null_value: break except (KeyError, AttributeError): pass if copy_value: if getattr(self, storage_id, None) is None: # Copy the value if it does not already exist as an attribute of self # Like in the case of orders / invoices setattr(self, storage_id, value) if is_list_type: # We must provide the first element of the acquired list if value in null_value: result = None else: if isinstance(value, (list, tuple)): if len(value) is 0: result = None else: result = value[0] else: result = value else: # Value is a simple type result = value else: result = None if result not in null_value: return result # Retrieve the list of related objects #LOG("Get Acquired Property self",0,str(self)) #LOG("Get Acquired Property portal_type",0,str(portal_type)) #LOG("Get Acquired Property base_category",0,str(base_category)) #super_list = self._getValueList(base_category, portal_type=portal_type) # We only do a single jump super_list = self._getAcquiredValueList(base_category, portal_type=portal_type, checked_permission=checked_permission) # Full acquisition super_list = [o for o in super_list if o.getPhysicalPath() != self.getPhysicalPath()] # Make sure we do not create stupid loop here #LOG("Get Acquired Property super_list",0,str(super_list)) #LOG("Get Acquired Property accessor_id",0,str(accessor_id)) if len(super_list) > 0: super = super_list[0] # Retrieve the value if accessor_id is None: value = super.getProperty(key) else: method = getattr(super, accessor_id) value = method() # We should add depends here XXXXXX # There is also a strong risk here of infinite loop if copy_value: if getattr(self, storage_id, None) is None: # Copy the value if it does not already exist as an attribute of self # Like in the case of orders / invoices setattr(self, storage_id, value) if is_list_type: # We must provide the first element of the acquired list if value in null_value: result = None else: if isinstance(value, (list, tuple)): if len(value) is 0: result = None else: result = value[0] else: result = value else: # Value is a simple type result = value else: result = None if result not in null_value: return result elif local_value not in null_value: # Nothing has been found by looking up # through acquisition documents, fallback by returning # at least local_value return local_value else: #LOG("alt_accessor_id",0,str(alt_accessor_id)) if alt_accessor_id is not None: for id in alt_accessor_id: #LOG("method",0,str(id)) method = getattr(self, id, None) if callable(method): try: result = method(checked_permission=checked_permission) except TypeError: result = method() if result not in null_value: if is_list_type: if isinstance(result, (list, tuple)): # We must provide the first element of the alternate result if len(result) > 0: return result[0] else: return result else: # Result is a simple type return result if copy_value: return getattr(self,storage_id, default_value) else: # Return the default value defined at the class level XXXXXXXXXXXXXXX return default_value finally: # Pop the acquisition context. try: del tv[acquisition_key] except KeyError: pass def _getAcquiredPropertyList(self, key, default_value, null_value, base_category, portal_type=None, copy_value=0, mask_value=0, append_value=0, accessor_id=None, depends=None, storage_id=None, alt_accessor_id=None, acquisition_object_id=None, is_list_type=0, is_tales_type=0, checked_permission=None): """ Default accessor. Implements the default attribute accessor. portal_type copy_value depends """ # Push context to prevent loop tv = getTransactionalVariable() if isinstance(portal_type, list): portal_type = tuple(portal_type) elif portal_type is None: portal_type = () acquisition_key = ('_getAcquiredPropertyList', self.getPath(), key, base_category, portal_type, copy_value, mask_value, accessor_id, depends, storage_id, alt_accessor_id, acquisition_object_id, is_list_type, is_tales_type, checked_permission) if acquisition_key in tv: return null_value tv[acquisition_key] = 1 try: if storage_id is None: storage_id=key value = getattr(self, storage_id, None) if mask_value and value is not None: if is_tales_type: expression = Expression(value) econtext = createExpressionContext(self) return expression(econtext) else: return value super_list = [] if acquisition_object_id is not None: if isinstance(acquisition_object_id, str): acquisition_object_id = tuple(acquisition_object_id) for object_id in acquisition_object_id: try: acquisition_object = self[object_id] super_list.append(acquisition_object) except (KeyError, AttributeError): pass super_list.extend(self._getAcquiredValueList( base_category, portal_type=portal_type, checked_permission=checked_permission)) # Full acquisition super_list = [o for o in super_list if o.getPhysicalPath() != self.getPhysicalPath()] # Make sure we do not create stupid loop here if len(super_list) > 0: value = [] for super in super_list: if accessor_id is None: if is_list_type: result = super.getPropertyList(key) if isinstance(result, (list, tuple)): value += result else: value += [result] else: value += [super.getProperty(key)] else: method = getattr(super, accessor_id) if is_list_type: result = method() # We should add depends here if isinstance(result, (list, tuple)): value += result else: value += [result] else: value += [method()] # We should add depends here if copy_value: if not hasattr(self, storage_id): setattr(self, storage_id, value) return value else: # ????? if copy_value: return getattr(self,storage_id, default_value) else: return default_value finally: # Pop the acquisition context. try: del tv[acquisition_key] except KeyError: pass security.declareProtected( Permissions.AccessContentsInformation, 'getProperty' ) def getProperty(self, key, d=_MARKER, **kw): """getProperty is the generic accessor to all properties and categories defined on this object. If an accessor exists for this property, the accessor will be called, default value will be passed to the accessor as first positional argument. """ accessor_name = 'get' + UpperCase(key) aq_self = aq_base(self) if getattr(aq_self, accessor_name, None) is not None: method = getattr(self, accessor_name) if d is not _MARKER: try: # here method is a method defined on the class, we don't know if the # method supports default argument or not, so we'll try and if the # method doesn't accepts it, we ignore default argument. return method(d, **kw) except TypeError: pass return method(**kw) # Try a mono valued accessor if it is available # and return it as a list if accessor_name.endswith('List'): mono_valued_accessor_name = accessor_name[:-4] method = getattr(self.__class__, mono_valued_accessor_name, None) if method is not None: # We have a monovalued property if d is _MARKER: result = method(self, **kw) else: try: result = method(self, d, **kw) except TypeError: result = method(self, **kw) if not isinstance(result, (list, tuple)): result = [result] return result if d is not _MARKER: return ERP5PropertyManager.getProperty(self, key, d=d, local_properties=True, **kw) return ERP5PropertyManager.getProperty(self, key, local_properties=True, **kw) security.declareProtected( Permissions.AccessContentsInformation, 'getPropertyList' ) def getPropertyList(self, key, d=None): """Same as getProperty, but for list properties. """ return self.getProperty('%s_list' % key, d=d) security.declareProtected( Permissions.ModifyPortalContent, 'setPropertyList' ) def setPropertyList(self, key, value, **kw): """Same as setProperty, but for list properties. """ return self.setProperty('%s_list' % key, value, **kw) security.declareProtected( Permissions.ModifyPortalContent, 'setProperty' ) def setProperty(self, key, value, type='string', **kw): """ Previous Name: setValue New Name: we use the naming convention of /usr/lib/zope/lib/python/OFS/PropertySheets.py TODO: check possible conflicts Generic accessor. Calls the real accessor """ self._setProperty(key, value, type=type, **kw) self.reindexObject() def _setProperty(self, key, value, type=None, **kw): """ Previous Name: _setValue Generic accessor. Calls the real accessor **kw allows to call setProperty as a generic setter (ex. setProperty(value_uid, portal_type=)) """ if type is not None: # Speed if type in list_types: # Patch for OFS PropertyManager key += '_list' accessor_name = '_set' + UpperCase(key) aq_self = aq_base(self) # We must use aq_self # since we will change the value on self # rather than through implicit aquisition if getattr(aq_self, accessor_name, None) is not None: method = getattr(self, accessor_name) return method(value, **kw) public_accessor_name = 'set' + UpperCase(key) if getattr(aq_self, public_accessor_name, None) is not None: method = getattr(self, public_accessor_name) return method(value, **kw) # Try a mono valued setter if it is available # and call it if accessor_name.endswith('List'): mono_valued_accessor_name = accessor_name[:-4] mono_valued_public_accessor_name = public_accessor_name[:-4] method = None if hasattr(self, mono_valued_accessor_name): method = getattr(self, mono_valued_accessor_name) elif hasattr(self, mono_valued_public_accessor_name): method = getattr(self, mono_valued_public_accessor_name) if method is not None: if isinstance(value, (list, tuple)): value_len = len(value) if value_len == 1: mono_value = value[0] return method(mono_value, **kw) raise TypeError, \ "A mono valued property must be set with a list of len 1" # Finaly use standard PropertyManager #LOG("Changing attr: ",0, key) # If we are here, this means we do not use a property that # comes from an ERP5 PropertySheet, we should use the # PropertyManager if ERP5PropertyManager.hasProperty(self,key, local_properties=True): ERP5PropertyManager._updateProperty(self, key, value, local_properties=True) else: ERP5PropertyManager._setProperty(self, key, value, type=type) # This should not be there, because this ignore all checks made by # the PropertyManager. If there is problems, please complain to # seb@nexedi.com #except: # # This should be removed if we want strict property checking # setattr(self, key, value) return (self,) def _setPropValue(self, key, value, **kw): self._wrapperCheck(value) if isinstance(value, list): value = tuple(value) accessor_name = '_set' + UpperCase(key) aq_self = aq_base(self) # We must use aq_self # since we will change the value on self # rather than through implicit aquisition if hasattr(aq_self, accessor_name): method = getattr(self, accessor_name) method(value, **kw) return public_accessor_name = 'set' + UpperCase(key) if hasattr(aq_self, public_accessor_name): method = getattr(self, public_accessor_name) method(value, **kw) return # Finaly use standard PropertyManager #LOG("Changing attr: ",0, key) #try: ERP5PropertyManager._setPropValue(self, key, value) #except ConflictError: # raise # This should not be there, because this ignore all checks made by # the PropertyManager. If there is problems, please complain to # seb@nexedi.com #except: # # This should be removed if we want strict property checking # setattr(self, key, value) security.declareProtected(Permissions.AccessContentsInformation, 'hasProperty') def hasProperty(self, key): """ Previous Name: hasValue Generic accessor. Calls the real accessor and returns 0 if it fails. The idea of hasProperty is to call the tester methods. It will return True only if a property was defined on the object. (either by calling a Tester accessor or by checking if a local property was added). It will return False if the property is not part of the list of valid properties (ie. the list of properties defined in PropertySheets) or if the property has never been updated. NOTE - One possible issue in the current implementation is that a property which is set to its default value will be considered as not being defined. Ex. self.hasProperty('first_name') on a Person object returns False if the first name was never defined and even if self.getProperty('first_name') returns '' """ method = getattr(self, 'has' + UpperCase(key), _MARKER) if method is _MARKER: # Check in local properties (which obviously were defined at some point) return key in self.propertyIds() try: return method() except ConflictError: raise except: return 0 security.declareProtected(Permissions.AccessContentsInformation, 'hasCategory') def hasCategory(self, key): """ Previous Name: hasValue Generic accessor. Calls the real accessor and returns 0 if it fails """ return key in self.getCategoryList() # Accessors are not workflow methods by default # Ping provides a dummy method to trigger automatic methods # XXX : maybe an empty edit is enough (self.edit()) def ping(self): pass ping = WorkflowMethod(ping) # Object attributes update method def _edit(self, REQUEST=None, force_update=0, reindex_object=0, keep_existing=0, activate_kw=None, edit_order=[], restricted=0, **kw): """ Generic edit Method for all ERP5 object The purpose of this method is to update attributed, eventually do some kind of type checking according to the property sheet and index the object. Each time attributes of an object are updated, they should be updated through this generic edit method Modification date is supported by edit_workflow in ERP5 There is no need to change it here. keep_existing -- if set to 1 or True, only those properties for which hasProperty is False will be updated. """ if not kw: return key_list = kw.keys() modified_property_dict = self._v_modified_property_dict = {} modified_object_dict = {} unordered_key_list = [k for k in key_list if k not in edit_order] ordered_key_list = [k for k in edit_order if k in key_list] restricted_method_set = set() default_permission_set = set(('Access contents information', 'Modify portal content')) if restricted: # retrieve list of accessors which doesn't use default permissions for ancestor in self.__class__.mro(): for permissions in getattr(ancestor, '__ac_permissions__', ()): if permissions[0] not in default_permission_set: for method in permissions[1]: if method.startswith('set'): restricted_method_set.add(method) getProperty = self.getProperty hasProperty = self.hasProperty _setProperty = self._setProperty def setChangedPropertyList(key_list): not_modified_list = [] for key in key_list: # We only change if the value is different # This may be very long... if force_update: old_value = None else: try: old_value = getProperty(key, evaluate=0) except TypeError: old_value = getProperty(key) if old_value == kw[key]: not_modified_list.append(key) continue # We keep in a thread var the previous values # this can be useful for interaction workflow to implement lookups # XXX If iteraction workflow script is triggered by edit and calls # edit itself, this is useless as the dict will be overwritten # If the keep_existing flag is set to 1, we do not update properties which are defined if keep_existing and hasProperty(key): continue if restricted: accessor_name = 'set' + UpperCase(key) if accessor_name in restricted_method_set: # will raise Unauthorized when not allowed guarded_getattr(self, accessor_name) modified_property_dict[key] = old_value if key != 'id': modified_object_list = _setProperty(key, kw[key]) # BBB: if the setter does not return anything, assume # that self has been modified. if modified_object_list is None: modified_object_list = (self,) for o in modified_object_list: # XXX using id is not quite nice, but getUID causes a # problem at the bootstrap of an ERP5 site. Therefore, # objects themselves cannot be used as keys. modified_object_dict[id(o)] = o else: self.setId(kw['id'], reindex=reindex_object) return not_modified_list unmodified_key_list = setChangedPropertyList(unordered_key_list) setChangedPropertyList(unmodified_key_list) # edit_order MUST be enforced, and done at the complete end setChangedPropertyList(ordered_key_list) if reindex_object: for o in modified_object_dict.itervalues(): o.reindexObject(activate_kw=activate_kw) security.declareProtected( Permissions.ModifyPortalContent, 'setId' ) def setId(self, id, reindex = 1): """ changes id of an object by calling the Zope machine """ tryMethodCallWithTemporaryPermission(self, 'Copy or Move', self.aq_inner.aq_parent.manage_renameObject, (self.id, id), {}, CopyError) # Do not flush any more, because it generates locks security.declareProtected( Permissions.ModifyPortalContent, 'updateRelatedContent' ) def updateRelatedContent(self, previous_category_url, new_category_url): """ updateRelatedContent is implemented by portal_categories """ self._getCategoryTool().updateRelatedContent(self, previous_category_url, new_category_url) security.declareProtected(Permissions.ModifyPortalContent, 'edit') def edit(self, REQUEST=None, force_update=0, reindex_object=1, **kw): """ Generic edit Method for all ERP5 object """ return self._edit(REQUEST=REQUEST, force_update=force_update, reindex_object=reindex_object, restricted=1, **kw) # XXX Is this useful ? (Romain) # Probably not. Even if it should speed up portal_type initialization and # save some memory because edit_workflow is used in many places, I (jm) # think it's negligible compared to the performance loss on all # classes/types that are not bound to edit_workflow. edit = WorkflowMethod(edit) # Accessing object property through ERP5ish interface security.declareProtected( Permissions.View, 'getPropertyIdList' ) def getPropertyIdList(self): return self.propertyIds() security.declareProtected( Permissions.View, 'getPropertyValueList' ) def getPropertyValueList(self): return self.propertyValues() security.declareProtected( Permissions.View, 'getPropertyItemList' ) def getPropertyItemList(self): return self.propertyItems() security.declareProtected( Permissions.View, 'getPropertyMap' ) def getPropertyMap(self): return self.propertyMap() # ERP5 content properties interface security.declareProtected( Permissions.View, 'getContentPropertyIdList' ) def getContentPropertyIdList(self): """ Return content properties of the current instance. Content properties are filtered out in getPropertyIdList so that rendering in ZMI is compatible with Zope standard properties """ result = set() for parent_class in self.__class__.mro(): for property in getattr(parent_class, '_properties', []): if property['type'] == 'content': result.add(property['id']) return list(result) security.declareProtected( Permissions.View, 'getStandardPropertyIdList' ) def getStandardPropertyIdList(self): """ Return standard properties of the current instance. Unlike getPropertyIdList, properties are not converted or rewritten here. """ result = set() for parent_class in self.__class__.mro(): for property in getattr(parent_class, '_properties', []): if property['type'] != 'content': result.add(property['id']) return list(result) # Catalog Related security.declareProtected( Permissions.AccessContentsInformation, 'getObject' ) def getObject(self, relative_url = None, REQUEST=None): """ Returns self - useful for ListBox when we do not know if the getObject is called on a brain object or on the actual object """ return self def getDocumentInstance(self): """ Returns self Returns instance if category through document_instance relation """ return self security.declareProtected(Permissions.ManagePortal, 'getMountedObject') def getMountedObject(self): """ If self is a mount-point, return the mounted object in its own storage """ from Products.ZODBMountPoint.MountedObject import getMountPoint mount_point = getMountPoint(self) if mount_point is not None: connection = self._p_jar assert mount_point._getMountedConnection(connection) is connection return mount_point._traverseToMountedRoot(connection.root(), None) def asSQLExpression(self, strict_membership=0, table='category', base_category = None): """ Any document can be used as a Category. It can therefore serve in a Predicate and must be rendered as an sql expression. This can be useful to create reporting trees based on the ZSQLCatalog whenever documents are used rather than categories TODO: - strict_membership is not implemented """ if isinstance(base_category, str): base_category = self.portal_categories[base_category] if base_category is None: sql_text = '(%s.category_uid = %s)' % \ (table, self.getUid()) else: sql_text = '(%s.category_uid = %s AND %s.base_category_uid = %s)' % \ (table, self.getUid(), table, base_category.getBaseCategoryUid()) return sql_text security.declareProtected( Permissions.AccessContentsInformation, 'getParentSQLExpression' ) def getParentSQLExpression(self, table = 'catalog', strict_membership = 0): """ Builds an SQL expression to search children and subclidren """ return "%s.parent_uid = %s" % (table, self.getUid()) security.declareProtected( Permissions.AccessContentsInformation, 'getParentUid' ) def getParentUid(self): """ Returns the UID of the parent of the current object. Used for the implementation of the ZSQLCatalog based listing of objects. """ return self.aq_inner.aq_parent.getUid() security.declareProtected( Permissions.AccessContentsInformation, 'getParentTitleOrId' ) def getParentTitleOrId(self): """ Returns the title or the id of the parent """ return self.aq_inner.aq_parent.getTitleOrId() security.declareProtected( Permissions.AccessContentsInformation, 'getParentRelativeUrl' ) def getParentRelativeUrl(self): """ Returns the title or the id of the parent """ return self.aq_inner.aq_parent.getRelativeUrl() security.declareProtected( Permissions.AccessContentsInformation, 'getParentId' ) def getParentId(self): """ Returns the id of the parent """ return self.aq_inner.aq_parent.getId() security.declareProtected( Permissions.AccessContentsInformation, 'getParentTitle' ) def getParentTitle(self): """ Returns the title or of the parent """ return self.aq_inner.aq_parent.getTitle() security.declareProtected( Permissions.AccessContentsInformation, 'getParentValue' ) def getParentValue(self): """ Returns the parent of the current object. """ return self.aq_inner.aq_parent security.declareProtected( Permissions.AccessContentsInformation, 'getParent' ) def getParent(self): """Returns the parent of the current object (whereas it should return the relative_url of the parent for consistency with CMFCategory. This method still uses this behaviour, because some part of the code still uses getParent instead of getParentValue. This may change in the future. """ warnings.warn("getParent implementation still returns the parent object, "\ "which is inconsistant with CMFCategory API. "\ "Use getParentValue instead", FutureWarning) return self.getParentValue() # Compatibility security.declareProtected( Permissions.AccessContentsInformation, 'getUid' ) def getUid(self): """ Returns the UID of the object. Eventually reindexes the object in order to make sure there is a UID (useful for import / export). WARNING : must be updated for circular references issues """ uid = getattr(aq_base(self), 'uid', None) if uid is None: self.uid = self.getPortalObject().portal_catalog.newUid() uid = getattr(aq_base(self), 'uid', None) if uid is None: raise DeferredCatalogError('Could neither access uid nor generate it', self) return uid security.declareProtected(Permissions.AccessContentsInformation, 'getLogicalPath') def getLogicalPath(self, REQUEST=None, **kw) : """ Returns the absolute path of an object, using titles when available """ pathlist = self.getPhysicalPath() objectlist = [self.getPhysicalRoot()] for element in pathlist[1:] : objectlist.append(objectlist[-1][element]) return '/' + join([object.getTitle() for object in objectlist[1:]], '/') security.declareProtected(Permissions.AccessContentsInformation, 'getCompactLogicalPath') def getCompactLogicalPath(self, REQUEST=None) : """ Returns a compact representation of the absolute path of an object using compact titles when available """ pathlist = self.getPhysicalPath() objectlist = [self.getPhysicalRoot()] for element in pathlist[1:] : objectlist.append(objectlist[-1][element]) return '/' + join([object.getCompactTitle() for object in objectlist[1:]], '/') security.declareProtected(Permissions.AccessContentsInformation, 'getUrl') def getUrl(self, REQUEST=None): """ Returns the absolute path of an object """ return join(self.getPhysicalPath(),'/') # Old name - for compatibility security.declareProtected(Permissions.AccessContentsInformation, 'getPath') getPath = getUrl security.declareProtected(Permissions.AccessContentsInformation, 'getRelativeUrl') def getRelativeUrl(self): """ Returns the url of an object relative to the portal site. """ return self.getPortalObject().portal_url.getRelativeUrl(self) security.declareProtected(Permissions.AccessContentsInformation, 'getAbsoluteUrl') def getAbsoluteUrl(self): """ Returns the absolute url of an object. """ return self.absolute_url() security.declarePublic('getPortalObject') def getPortalObject(self): """ Returns the portal object """ return self.aq_inner.aq_parent.getPortalObject() security.declareProtected(Permissions.AccessContentsInformation, 'getWorkflowIds') def getWorkflowIds(self): """ Returns the list of workflows """ return self.portal_workflow.getWorkflowIds() # Object Database Management security.declareProtected( Permissions.ManagePortal, 'upgrade' ) def upgrade(self, REQUEST=None): """ Upgrade an object and do whatever necessary to make sure it is compatible with the latest version of a class """ pass # For Debugging security.declareProtected( Permissions.ManagePortal, 'showDict' ) def showDict(self): """ Returns the dictionnary of the object Only for debugging """ d = copy(self.__dict__) klass = self.__class__ d['__class__'] = '%s.%s' % (klass.__module__, klass.__name__) return d security.declareProtected( Permissions.ManagePortal, 'showPermissions' ) def showPermissions(self, all=1): """ Return the tuple of permissions Only for debugging """ permission_list = [] for permission in self.ac_inherited_permissions(all=all): name, value = permission[:2] role_list = Permission(name, value, self).getRoles(default=[]) permission_list.append((name, role_list)) return tuple(permission_list) security.declareProtected( Permissions.AccessContentsInformation, 'getViewPermissionOwner' ) def getViewPermissionOwner(self): """Returns the user ID of the user with 'Owner' local role on this document, if the Owner role has View permission. If there is more than one Owner local role, the result is undefined. """ if 'Owner' in rolesForPermissionOn(Permissions.View, self): owner_list = self.users_with_local_role('Owner') if owner_list: return owner_list[0] # Private accessors for the implementation of relations based on # categories def _setValue(self, id, target, spec=(), filter=None, portal_type=(), keep_default=1, checked_permission=None): getRelativeUrl = self.getPortalObject().portal_url.getRelativeUrl def cleanupCategory(path): # prevent duplicating base categories and storing "portal_categories/" for start_string in ("%s/" % id, "portal_categories/"): if path.startswith(start_string): path = path[len(start_string):] return path if target is None : path = target elif isinstance(target, str): # We have been provided a string path = target elif isinstance(target, (tuple, list, set, frozenset)): # We have been provided a list or tuple path_list = [] for target_item in target: if isinstance(target_item, str): path = target_item else: path = getRelativeUrl(target_item) path_list.append(cleanupCategory(path)) path = path_list else: # We have been provided an object path = cleanupCategory(getRelativeUrl(target)) self._setCategoryMembership(id, path, spec=spec, filter=filter, portal_type=portal_type, base=0, keep_default=keep_default, checked_permission=checked_permission) security.declareProtected( Permissions.ModifyPortalContent, '_setValueList' ) _setValueList = _setValue security.declareProtected( Permissions.ModifyPortalContent, 'setValue' ) def setValue(self, id, target, spec=(), filter=None, portal_type=(), keep_default=1, checked_permission=None): self._setValue(id, target, spec=spec, filter=filter, portal_type=portal_type, keep_default=keep_default, checked_permission=checked_permission) self.reindexObject() security.declareProtected( Permissions.ModifyPortalContent, 'setValueList' ) setValueList = setValue def _setDefaultValue(self, id, target, spec=(), filter=None, portal_type=(), checked_permission=None): start_string = "%s/" % id start_string_len = len(start_string) if target is None : path = target elif isinstance(target, str): # We have been provided a string path = target if path.startswith(start_string): path = path[start_string_len:] # Prevent duplicating base category else: # We have been provided an object # Find the object path = target.getRelativeUrl() if path.startswith(start_string): path = path[start_string_len:] # Prevent duplicating base category self._setDefaultCategoryMembership(id, path, spec=spec, filter=filter, portal_type=portal_type, base=0, checked_permission=checked_permission) security.declareProtected(Permissions.ModifyPortalContent, 'setDefaultValue' ) def setDefaultValue(self, id, target, spec=(), filter=None, portal_type=()): self._setDefaultValue(id, target, spec=spec, filter=filter, portal_type=portal_type, checked_permission=None) self.reindexObject() def _getDefaultValue(self, id, spec=(), filter=None, portal_type=(), checked_permission=None): path = self._getDefaultCategoryMembership(id, spec=spec, filter=filter, portal_type=portal_type,base=1, checked_permission=checked_permission) if path is None: return None else: return self._getCategoryTool().resolveCategory(path) security.declareProtected(Permissions.AccessContentsInformation, 'getDefaultValue') getDefaultValue = _getDefaultValue def _getValueList(self, id, spec=(), filter=None, portal_type=(), checked_permission=None): ref_list = [] for path in self._getCategoryMembershipList(id, spec=spec, filter=filter, portal_type=portal_type, base=1, checked_permission=checked_permission): # LOG('_getValueList',0,str(path)) try: value = self._getCategoryTool().resolveCategory(path) if value is not None: ref_list.append(value) except ConflictError: raise except: LOG("ERP5Type WARNING",0,"category %s has no object value" % path, error=sys.exc_info()) return ref_list security.declareProtected(Permissions.AccessContentsInformation, 'getValueList') getValueList = _getValueList def _getDefaultAcquiredValue(self, id, spec=(), filter=None, portal_type=(), evaluate=1, checked_permission=None, **kw): path = self._getDefaultAcquiredCategoryMembership(id, spec=spec, filter=filter, portal_type=portal_type, base=1, checked_permission=checked_permission, **kw) if path is None: return None else: return self._getCategoryTool().resolveCategory(path) security.declareProtected(Permissions.AccessContentsInformation, 'getDefaultAcquiredValue') getDefaultAcquiredValue = _getDefaultAcquiredValue def _getAcquiredValueList(self, id, spec=(), filter=None, **kw): ref_list = [] for path in self._getAcquiredCategoryMembershipList(id, base=1, spec=spec, filter=filter, **kw): category = self._getCategoryTool().resolveCategory(path) if category is not None: ref_list.append(category) return ref_list security.declareProtected(Permissions.AccessContentsInformation, 'getAcquiredValueList') getAcquiredValueList = _getAcquiredValueList def _getDefaultRelatedValue(self, id, spec=(), filter=None, portal_type=(), strict_membership=0, strict="deprecated", checked_permission=None): # backward compatibility to keep strict keyword working if strict != "deprecated" : strict_membership = strict value_list = self._getRelatedValueList( id, spec=spec, filter=filter, portal_type=portal_type, strict_membership=strict_membership, checked_permission=checked_permission) try: return value_list[0] except IndexError: return None security.declareProtected(Permissions.AccessContentsInformation, 'getDefaultRelatedValue') getDefaultRelatedValue = _getDefaultRelatedValue def _getRelatedValueList(self, *args, **kw): # backward compatibility to keep strict keyword working if 'strict' in kw: kw['strict_membership'] = kw.pop('strict') return self._getCategoryTool().getRelatedValueList(self, *args, **kw) security.declareProtected(Permissions.AccessContentsInformation, 'getRelatedValueList') getRelatedValueList = _getRelatedValueList def _getDefaultRelatedProperty(self, id, property_name, spec=(), filter=None, portal_type=(), strict_membership=0, checked_permission=None): property_list = self._getCategoryTool().getRelatedPropertyList(self, id, property_name=property_name, spec=spec, filter=filter, portal_type=portal_type, strict_membership=strict_membership, checked_permission=checked_permission) try: return property_list[0] except IndexError: return None security.declareProtected(Permissions.AccessContentsInformation, 'getDefaultRelatedProperty') getDefaultRelatedProperty = _getDefaultRelatedProperty def _getRelatedPropertyList(self, id, property_name, spec=(), filter=None, portal_type=(), strict_membership=0, checked_permission=None): return self._getCategoryTool().getRelatedPropertyList(self, id, property_name=property_name, spec=spec, filter=filter, portal_type=portal_type, strict_membership=strict_membership, checked_permission=checked_permission) security.declareProtected( Permissions.AccessContentsInformation, 'getRelatedPropertyList' ) getRelatedPropertyList = _getRelatedPropertyList security.declareProtected(Permissions.AccessContentsInformation, 'getValueUidList') def getValueUidList(self, id, spec=(), filter=None, portal_type=(), checked_permission=None): uid_list = [] for o in self._getValueList(id, spec=spec, filter=filter, portal_type=portal_type, checked_permission=checked_permission): uid_list.append(o.getUid()) return uid_list security.declareProtected( Permissions.View, 'getValueUids' ) getValueUids = getValueUidList # DEPRECATED def _setValueUidList(self, id, uids, spec=(), filter=None, portal_type=(), keep_default=1, checked_permission=None): # We must do an ordered list so we can not use the previous method # self._setValue(id, self.portal_catalog.getObjectList(uids), spec=spec) references = map(self.getPortalObject().portal_catalog.getObject, (uids,) if isinstance(uids, (int, long)) else uids) self._setValue(id, references, spec=spec, filter=filter, portal_type=portal_type, keep_default=keep_default, checked_permission=checked_permission) security.declareProtected( Permissions.ModifyPortalContent, '_setValueUidList' ) _setValueUids = _setValueUidList # DEPRECATED security.declareProtected( Permissions.ModifyPortalContent, 'setValueUidList' ) def setValueUidList(self, id, uids, spec=(), filter=None, portal_type=(), keep_default=1, checked_permission=None): self._setValueUids(id, uids, spec=spec, filter=filter, portal_type=portal_type, keep_default=keep_default, checked_permission=checked_permission) self.reindexObject() security.declareProtected( Permissions.ModifyPortalContent, 'setValueUidList' ) setValueUids = setValueUidList # DEPRECATED def _setDefaultValueUid(self, id, uid, spec=(), filter=None, portal_type=(), checked_permission=None): # We must do an ordered list so we can not use the previous method # self._setValue(id, self.portal_catalog.getObjectList(uids), spec=spec) references = self.portal_catalog.getObject(uid) self._setDefaultValue(id, references, spec=spec, filter=filter, portal_type=portal_type, checked_permission=checked_permission) security.declareProtected( Permissions.ModifyPortalContent, 'setDefaultValueUid' ) def setDefaultValueUid(self, id, uid, spec=(), filter=None, portal_type=(), checked_permission=None): self._setDefaultValueUid(id, uid, spec=spec, filter=filter, portal_type=portal_type, checked_permission=checked_permission) self.reindexObject() # Private accessors for the implementation of categories def _setCategoryMembership(self, *args, **kw): self._getCategoryTool()._setCategoryMembership(self, *args, **kw) #self.activate().edit() # Do nothing except call workflow method # XXX This is a problem - it is used to circumvent a lack of edit security.declareProtected( Permissions.ModifyPortalContent, 'setCategoryMembership' ) def setCategoryMembership(self, *args, **kw): self._setCategoryMembership(*args, **kw) self.reindexObject() def _setDefaultCategoryMembership(self, category, node_list, spec=(), filter=None, portal_type=(), base=0, checked_permission=None): self._getCategoryTool().setDefaultCategoryMembership(self, category, node_list, spec=spec, filter=filter, portal_type=portal_type, base=base, checked_permission=checked_permission) security.declareProtected( Permissions.ModifyPortalContent, 'setDefaultCategoryMembership' ) def setDefaultCategoryMembership(self, category, node_list, spec=(), filter=None, portal_type=(), base=0, checked_permission=None): self._setDefaultCategoryMembership(category, node_list, spec=spec, filter=filter, portal_type=portal_type, base=base, checked_permission=checked_permission) self.reindexObject() def _getCategoryMembershipList(self, category, spec=(), filter=None, portal_type=(), base=0, keep_default=1, checked_permission=None, **kw): """ This returns the list of categories for an object """ return self._getCategoryTool().getCategoryMembershipList(self, category, spec=spec, filter=filter, portal_type=portal_type, base=base, keep_default=keep_default, checked_permission=checked_permission, **kw) security.declareProtected( Permissions.AccessContentsInformation, 'getCategoryMembershipList' ) getCategoryMembershipList = _getCategoryMembershipList def _getAcquiredCategoryMembershipList(self, category, spec=(), filter=None, portal_type=(), base=0, keep_default=1, checked_permission=None, **kw): """ Returns the list of acquired categories """ return self._getCategoryTool().getAcquiredCategoryMembershipList(self, category, spec=spec, filter=filter, portal_type=portal_type, base=base, keep_default=keep_default, checked_permission=checked_permission, **kw ) security.declareProtected( Permissions.AccessContentsInformation, 'getAcquiredCategoryMembershipList' ) getAcquiredCategoryMembershipList = _getAcquiredCategoryMembershipList def _getCategoryMembershipItemList(self, category, spec=(), filter=None, portal_type=(), base=0, checked_permission=None): membership_list = self._getCategoryMembershipList(category, spec=spec, filter=filter, portal_type=portal_type, base=base, checked_permission=checked_permission) return [(x, x) for x in membership_list] def _getAcquiredCategoryMembershipItemList(self, category, spec=(), filter=None, portal_type=(), base=0, method_id=None, sort_id='default', checked_permission=None): # Standard behaviour - should be OK # sort_id should be None for not sort - default behaviour in other methods if method_id is None and sort_id in (None, 'default'): membership_list = self._getAcquiredCategoryMembershipList(category, spec = spec, filter=filter, portal_type=portal_type, base=base, checked_permission=checked_permission) if sort_id == 'default': membership_list.sort() return [(x, x) for x in membership_list] # Advanced behaviour XXX This is new and needs to be checked membership_list = self._getAcquiredCategoryMembershipList(category, spec = spec, filter=filter, portal_type=portal_type, base=1, checked_permission=checked_permission) result = [] for path in membership_list: value = self._getCategoryTool().resolveCategory(path) if value is not None: result += [value] result.sort(key=lambda x: getattr(x,sort_id)()) if method_id is None: return [(x, x) for x in membership_list] return [(x,getattr(x, method_id)()) for x in membership_list] def _getDefaultCategoryMembership(self, category, spec=(), filter=None, portal_type=(), base=0, default=None, checked_permission=None, **kw): membership = self._getCategoryMembershipList(category, spec=spec, filter=filter, portal_type=portal_type, base=base, checked_permission=checked_permission, **kw) if len(membership) > 0: return membership[0] else: return default def _getDefaultAcquiredCategoryMembership(self, category, spec=(), filter=None, portal_type=(), base=0, default=None, checked_permission=None, **kw): membership = self._getAcquiredCategoryMembershipList(category, spec=spec, filter=filter, portal_type=portal_type, base=base, checked_permission=checked_permission, **kw) if len(membership) > 0: return membership[0] else: return default security.declareProtected(Permissions.AccessContentsInformation, 'getDefaultAcquiredCategoryMembership') getDefaultAcquiredCategoryMembership = _getDefaultAcquiredCategoryMembership security.declareProtected(Permissions.AccessContentsInformation, 'getCategoryList') def getCategoryList(self): """ Returns the list of local categories """ return self._getCategoryTool().getCategoryList(self) def _getCategoryList(self): return self._getCategoryTool()._getCategoryList(self) security.declareProtected(Permissions.AccessContentsInformation, 'getAcquiredCategoryList') def getAcquiredCategoryList(self): """ Returns the list of acquired categories """ return self._getCategoryTool().getAcquiredCategoryList(self) security.declareProtected( Permissions.ModifyPortalContent, 'setCategoryList' ) def setCategoryList(self, path_list): self.portal_categories.setCategoryList(self, path_list) def _setCategoryList(self, path_list): self.portal_categories._setCategoryList(self, path_list) security.declareProtected(Permissions.AccessContentsInformation, 'getBaseCategoryList') def getBaseCategoryList(self): """ Lists the base_category ids which apply to this instance """ return self._getCategoryTool().getBaseCategoryList(context=self) security.declareProtected( Permissions.View, 'getBaseCategoryIds' ) getBaseCategoryIds = getBaseCategoryList security.declareProtected(Permissions.AccessContentsInformation, 'getBaseCategoryValueList') def getBaseCategoryValueList(self): return self._getCategoryTool().getBaseCategoryValues(context=self) security.declareProtected( Permissions.View, 'getBaseCategoryValues' ) getBaseCategoryValues = getBaseCategoryValueList # Category testing security.declareProtected( Permissions.AccessContentsInformation, 'isMemberOf' ) def isMemberOf(self, category, **kw): """ Tests if an object if member of a given category """ return self._getCategoryTool().isMemberOf(self, category, **kw) security.declareProtected( Permissions.AccessContentsInformation, 'isAcquiredMemberOf' ) def isAcquiredMemberOf(self, category): """ Tests if an object if member of a given category """ return self._getCategoryTool().isAcquiredMemberOf(self, category) # Aliases security.declareProtected(Permissions.AccessContentsInformation, 'getTitleOrId') def getTitleOrId(self): """ Returns the title or the id if the id is empty """ title = self.getTitle() if title is not None: title = str(title) if title == '' or title is None: return self.getId() else: return title return self.getId() security.declareProtected(Permissions.AccessContentsInformation, 'Title' ) Title = getTitleOrId # Why ??? # CMF Compatibility security.declareProtected(Permissions.AccessContentsInformation, 'title_or_id' ) title_or_id = getTitleOrId security.declareProtected(Permissions.AccessContentsInformation, 'getTitleAndId') def getTitleAndId(self): """ Returns the title and the id in parenthesis """ return self.title_and_id() security.declareProtected(Permissions.AccessContentsInformation, 'getTranslatedShortTitleOrId') def getTranslatedShortTitleOrId(self): """ Returns the translated short title or the id if the id is empty """ title = self.getTranslatedShortTitle() if title is not None: title = str(title) if title == '' or title is None: return self.getId() else: return title return self.getId() security.declareProtected(Permissions.AccessContentsInformation, 'getTranslatedTitleOrId') def getTranslatedTitleOrId(self): """ Returns the translated title or the id if the id is empty """ title = self.getTranslatedTitle() if title is not None: title = str(title) if title == '' or title is None: return self.getId() else: return title return self.getId() security.declarePublic('getIdTranslationDict') def getIdTranslationDict(self): """Returns the mapping which is used to translate IDs. """ property_dict = { 'Address': dict(default_address='Default Address'), 'Telephone': dict(default_telephone='Default Telephone', mobile_telephone='Mobile Telephone',), 'Fax': dict(default_fax='Default Fax'), 'Email': dict(default_email='Default Email', alternate_email='Alternate Email'), 'Career': dict(default_career='Default Career'), 'Payment Condition': dict(default_payment_condition= 'Default Payment Condition'), 'Annotation Line': dict( work_time_annotation_line='Work Time Annotation Line', social_insurance_annotation_line='Social Insurance Annotation Line', overtime_annotation_line='Overtime Annotation Line'), 'Image': dict(default_image='Default Image'), 'Internal Supply Line': dict(internal_supply_line= 'Default Internal Supply Line'), 'Purchase Supply Line': dict(purchase_supply_line= 'Default Purchase Supply Line'), 'Sale Supply Line': dict(sale_supply_line= 'Default Sale Supply Line'), 'Accounting Transaction Line': dict(bank='Bank', payable='Payable', receivable='Receivable'), 'Purchase Invoice Transaction Line': dict(expense='Expense', payable='Payable', refundable_vat='Refundable VAT'), 'Sale Invoice Transaction Line': dict(income='Income', collected_vat='Collected VAT', receivable='Receivable'), } method = self._getTypeBasedMethod('getIdTranslationDict') if method is not None: user_dict = method() for k in user_dict.keys(): if property_dict.get(k, None) is not None: property_dict[k].update(user_dict[k]) else: property_dict[k] = user_dict[k] return property_dict security.declareProtected(Permissions.AccessContentsInformation, 'getTranslatedId') def getTranslatedId(self): """Returns the translated ID, if the ID of the current document has a special meaning, otherwise returns None. """ global_translation_dict = self.getIdTranslationDict() ptype_translation_dict = global_translation_dict.get( self.portal_type, None) if ptype_translation_dict is not None: id_ = self.getId() if id_ in ptype_translation_dict: return str(Message('erp5_ui', ptype_translation_dict[id_])) security.declareProtected(Permissions.AccessContentsInformation, 'getCompactTitle') def getCompactTitle(self): """ Returns the first non-null value from the following: - "getCompactTitle" type based method - short title - reference - title - id """ method = self._getTypeBasedMethod('getCompactTitle') if method is not None: r = method() if r: return r if self.hasShortTitle(): r = self.getShortTitle() if r: return r return (self.getProperty('reference') or getattr(self, '_baseGetTitle', str)() or self.getId()) security.declareProtected(Permissions.AccessContentsInformation, 'getCompactTranslatedTitle') def getCompactTranslatedTitle(self): """ Returns the first non-null value from the following: - "getCompactTranslatedTitle" type based method - "getCompactTitle" type based method - translated short title - short title - reference - translated title - title - id """ method = self._getTypeBasedMethod('getCompactTranslatedTitle') if method is not None: r = method() if r: return r method = self._getTypeBasedMethod('getCompactTitle') if method is not None: r = method() if r: return r if self.hasShortTitle(): r = self.getTranslatedShortTitle() if r: return r r = self.getShortTitle() if r: return r return (self.getProperty('reference') or # No need to test existence since all Base instances have this method # Also useful whenever title is calculated self._baseGetTranslatedTitle() or self.getId()) # This method allows to sort objects in list is a more reasonable way security.declareProtected(Permissions.AccessContentsInformation, 'getIntId') def getIntId(self): try: id_string = self.getId() return int(id_string) except (ValueError, TypeError): try: return int(id_string, 16) except (ValueError, TypeError): return None def _renderDefaultView(self, view, **kw): ti = self.getTypeInfo() if ti is None: raise NotFound('Cannot find default %s for %r' % (view, self.getPath())) method = ti.getDefaultViewFor(self, view) if getattr(aq_base(method), 'isDocTemp', 0): return method(self, self.REQUEST, self.REQUEST['RESPONSE'], **kw) else: return method(**kw) security.declareProtected(Permissions.View, 'view') def view(self): """Returns the default view even if index_html is overridden""" result = self._renderDefaultView('view') # call caching policy manager. _setCacheHeaders(_ViewEmulator().__of__(self), {}) return result # Default views - the default security in CMFCore # is View - however, security was not defined on # __call__ - to be consistent, between view and # __call__ we have to define permission here to View security.declareProtected(Permissions.View, '__call__') __call__ = view # This special value informs ZPublisher to use __call__. We define it here # since Products.CMFCore.PortalContent.PortalContent stopped defining it on # CMF 2.x. They use aliases and Zope3 style views now and make pretty sure # not to let zpublisher reach this value. index_html = None # By the Way, Products.ERP5.Document.File and .Image define their own # index_html to make sure this value here is not used so that they're # downloadable by their naked URL. security.declareProtected(Permissions.View, 'list') def list(self, reset=0): """Returns the default list even if folder_contents is overridden""" return self._renderDefaultView('list', reset=reset) # Proxy methods for security reasons security.declareProtected(Permissions.AccessContentsInformation, 'getOwnerInfo') def getOwnerInfo(self): """ this returns the Owner Info """ return self.owner_info() # Missing attributes security.declareProtected(Permissions.AccessContentsInformation, 'getPortalType') def getPortalType(self): """ This returns the portal_type """ return self.portal_type security.declareProtected(Permissions.AccessContentsInformation, 'getTranslatedPortalType') def getTranslatedPortalType(self): """ This returns the translated portal_type """ localizer = self.getPortalObject().Localizer return localizer.erp5_ui.gettext(self.portal_type).encode('utf8') security.declareProtected(Permissions.AccessContentsInformation, 'getMetaType') def getMetaType(self): """ This returns the Meta Type """ return self.meta_type # def _recursiveApply(self,f): # """ # """ # error_list = [] # for o in self.objectValues(): # try: # error_list += f(o) # error_list += o.recursiveApply(f) # except: # LOG('ERP5Type.Base',0,"error in recursiveApply : %s, %s on %s" # % (str(sys.exc_type),str(sys.exc_value),o.getPath())) # # return error_list # # def recursiveApply(self,f): # """ # This allows to apply a function, f, on the current object # and all subobjects. # # This function can be created inside a python script on the # zope management interface, then we just have to call recursiveApply. # """ # return self._recursiveApply(f) # Content consistency implementation def _checkConsistency(self, fixit=False): """ Check the constitency of objects. Private method. """ return [] def _fixConsistency(self): """ Fix the constitency of objects. Private method. """ return self._checkConsistency(fixit=True) security.declareProtected(Permissions.AccessContentsInformation, 'checkConsistency') def checkConsistency(self, fixit=False, filter=None, **kw): """ Check the constitency of objects. For example we can check if every Organisation has at least one Address. This method looks the constraints defined inside the propertySheets then check each of them Here, we try to check consistency without security, because consistency should not depend on the user. But if the user does not have enough permission, the detail of the error should be hidden. """ def getUnauthorizedErrorMessage(constraint): return ConsistencyMessage(constraint, object_relative_url=self.getRelativeUrl(), message='There is something wrong.') error_list = UnrestrictedMethod(self._checkConsistency)(fixit=fixit) if len(error_list) > 0: try: self._checkConsistency() except Unauthorized: error_list = [getUnauthorizedErrorMessage(self)] # We are looking inside all instances in constraints, then we check # the consistency for all of them for constraint_instance in self._filteredConstraintList(filter): if fixit: extra_error_list = UnrestrictedMethod( constraint_instance.fixConsistency)(self, **kw) else: extra_error_list = UnrestrictedMethod( constraint_instance.checkConsistency)(self, **kw) if len(extra_error_list) > 0: try: if not fixit: extra_error_list = constraint_instance.checkConsistency(self, **kw) else: constraint_instance.checkConsistency(self, **kw) except Unauthorized: error_list.append(getUnauthorizedErrorMessage(constraint_instance)) else: error_list += extra_error_list if fixit and len(error_list) > 0: self.reindexObject() return error_list security.declareProtected(Permissions.ManagePortal, 'fixConsistency') def fixConsistency(self, filter=None, **kw): """ Fix the constitency of objects. """ return self.checkConsistency(fixit=True, filter=filter, **kw) def _filteredConstraintList(self, filt): """ Returns a list of constraints filtered by filt argument. """ # currently only 'id' and 'reference', 'constraint_type' are supported. constraints = self.constraints if filt is not None: if 'id' in filt: id_list = filt.get('id', None) if not isinstance(id_list, (list, tuple)): id_list = [id_list] constraints = filter(lambda x:x.id in id_list, constraints) # New ZODB based constraint uses reference for identity if 'reference' in filt: reference_list = filt.get('reference', None) if not isinstance(reference_list, (list, tuple)): reference_list = [reference_list] constraints = filter(lambda x:x.getProperty('reference') in \ reference_list, constraints) if 'constraint_type' in filt: constraint_type_list = filt.get('constraint_type', None) if not isinstance(constraint_type_list, (list, tuple)): constraint_type_list = [constraint_type_list] constraints = filter(lambda x:x.__of__(self).getProperty('constraint_type') in \ constraint_type_list, constraints) return constraints # Context related methods security.declarePublic('asContext') def asContext(self, context=None, REQUEST=None, **kw): """ The purpose of asContext is to allow users overloading easily the properties and categories of an existing persistent object. (Use the same data and create a different portal type instance) Pay attention, to use asContext to create a temp object is wrong usage. ex : joe_person = person_module.bob_person.asContext(first_name='Joe') """ if context is None: pt = self._getTypesTool() portal_type = self.getPortalType() type_info = pt.getTypeInfo(portal_type) if type_info is None: raise ValueError('No such content type: %s' % portal_type) context = type_info.constructInstance( container=self.getParentValue(), id=self.getId(), temp_object=True, notify_workflow=False, is_indexable=False) # Pass all internal data to new instance. Do not copy, but # pass the same data. This is on purpose. context.__dict__.update(self.__dict__) # Copy REQUEST properties to self if REQUEST is not None: # Avoid copying a SESSION object, because it is newly created # implicitly when not present, thus it may induce conflict errors. # As ERP5 does not use Zope sessions, it is better to skip SESSION. for k in REQUEST.keys(): if k != 'SESSION': setattr(context, k, REQUEST[k]) # Set the original document kw['_original'] = self # Define local properties context.__dict__.update(kw) return context else: return context.asContext(REQUEST=REQUEST, **kw) security.declarePublic('getOriginalDocument') def getOriginalDocument(self, context=None, REQUEST=None, **kw): """ This method returns: * the original document for an asContext() result document. * self for a real document. * None for a temporary document. """ if not self.isTempObject(): return self else: original = getattr(self, '_original', None) if original is not None: return aq_inner(original) else: return None security.declarePublic('isTempObject') def isTempObject(self): """Return true if self is an instance of a temporary document class. """ isTempDocument = getattr(self.__class__, 'isTempDocument', None) if isTempDocument is not None: return isTempDocument() else: return False security.declareProtected(Permissions.AccessContentsInformation, 'isDeleted') def isDeleted(self): """Test if the context is in 'deleted' state""" for wf in self.getPortalObject().portal_workflow.getWorkflowsFor(self): state = wf._getWorkflowStateOf(self) if state is not None and state.getId() == 'deleted': return True return False security.declareProtected(Permissions.AccessContentsInformation, 'getRelationCountForDeletion') def getRelationCountForDeletion(self): """Count number of related objects preventing deletion""" portal = self.getPortalObject() getRelatedValueList = portal.portal_categories.getRelatedValueList ignore_list = [x.getPhysicalPath() for x in ( portal.portal_simulation, portal.portal_trash, self)] related_list = [(related.getPhysicalPath(), related) for o in self.getIndexableChildValueList() for related in getRelatedValueList(o)] related_list.sort() ignored = None related_count = 0 for related_path, related in related_list: if ignored is None or related_path[:len(ignored)] != ignored: for ignored in ignore_list: if related_path[:len(ignored)] == ignored: break else: if related.isDeleted(): ignored = related_path else: ignored = None related_count += 1 return related_count # Workflow Related Method security.declarePublic('getWorkflowStateItemList') def getWorkflowStateItemList(self): """ Returns a list of tuples {id:workflow_id, state:workflow_state} """ result = [] for wf in self.portal_workflow.getWorkflowsFor(self): result += [(wf.id, wf._getWorkflowStateOf(self, id_only=1))] return result security.declarePublic('getWorkflowInfo') def getWorkflowInfo(self, name='state', wf_id=None): """ Returns a list of tuples {id:workflow_id, state:workflow_state} """ portal_workflow = self.portal_workflow return portal_workflow.getInfoFor(self, name, wf_id=wf_id) # Hide Acquisition to prevent loops (ex. in cells) # Another approach is to use XMLObject everywhere # DIRTY TRICK XXX # def objectValues(self, *args, **kw): # return [] # # def contentValues(self, *args, **kw): # return [] # # def objectIds(self, *args, **kw): # return [] # # def contentIds(self, *args, **kw): # return [] security.declarePublic('immediateReindexObject') def immediateReindexObject(self, *args, **kw): """ Reindexes an object - also useful for testing """ root_indexable = int(getattr(self.getPortalObject(),'isIndexable',1)) if self.isIndexable and root_indexable: #LOG("immediateReindexObject",0,self.getRelativeUrl()) PortalContent.reindexObject(self, *args, **kw) else: pass #LOG("No reindex now",0,self.getRelativeUrl()) security.declarePublic('recursiveImmediateReindexObject') recursiveImmediateReindexObject = immediateReindexObject security.declarePublic('reindexObject') def reindexObject(self, *args, **kw): """ Reindexes an object args / kw required since we must follow API """ self._reindexObject(*args, **kw) def _reindexObject(self, activate_kw=None, **kw): # When the activity supports group methods, portal_catalog/catalogObjectList is called instead of # immediateReindexObject. # Do not check if root is indexable, it is done into catalogObjectList, # so we will save time if self.isIndexable: if activate_kw is None: activate_kw = {} reindex_kw = self.getDefaultReindexParameterDict() if reindex_kw is not None: reindex_kw = reindex_kw.copy() reindex_activate_kw = reindex_kw.pop('activate_kw', None) or {} reindex_activate_kw.update(activate_kw) reindex_kw.update(kw) kw = reindex_kw activate_kw = reindex_activate_kw group_id_list = [] if kw.get("group_id", "") not in ('', None): group_id_list.append(kw.get("group_id", "")) if kw.get("sql_catalog_id", "") not in ('', None): group_id_list.append(kw.get("sql_catalog_id", "")) group_id = ' '.join(group_id_list) self.activate(group_method_id='portal_catalog/catalogObjectList', alternate_method_id='alternateReindexObject', group_id=group_id, serialization_tag=self.getRootDocumentPath(), **activate_kw).immediateReindexObject(**kw) security.declarePublic('recursiveReindexObject') recursiveReindexObject = reindexObject def getRootDocumentPath(self): # Return the path of its root document, or itself if no root document. self_path_list = self.getPhysicalPath() portal_depth = len(self.getPortalObject().getPhysicalPath()) return '/'.join(self_path_list[:portal_depth+2]) security.declareProtected( Permissions.AccessContentsInformation, 'getIndexableChildValueList' ) def getIndexableChildValueList(self): """ Get indexable childen recursively. """ if self.isIndexable: return [self] return [] security.declareProtected(Permissions.ModifyPortalContent, 'reindexObjectSecurity') def reindexObjectSecurity(self, *args, **kw): """ Reindex security-related indexes on the object (and its descendants). """ # In ERP5, simply reindex all objects. #LOG('reindexObjectSecurity', 0, 'self = %r, self.getPath() = %r' % (self, self.getPath())) self.reindexObject(*args, **kw) security.declareProtected( Permissions.AccessContentsInformation, 'asXML' ) def asXML(self, root=None): """ Generate an xml text corresponding to the content of this object """ return Base_asXML(self, root=root) # Optimized Menu System security.declarePublic('allowedContentTypes') def allowedContentTypes( self ): """ List portal_types which can be added in this folder / object. """ return [] security.declarePublic('getVisibleAllowedContentTypeList') def getVisibleAllowedContentTypeList(self): """ List portal_types which can be added in this folder / object. """ return [] security.declareProtected(Permissions.AccessContentsInformation, 'getRedirectParameterDictAfterAdd') def getRedirectParameterDictAfterAdd(self, container, **kw): """Return a dict of parameters to specify where the user is redirected to after a new object is added in the UI.""" method = self._getTypeBasedMethod('getRedirectParameterDictAfterAdd', 'Base_getRedirectParameterDictAfterAdd') if method is not None: return method(container, **kw) # XXX this should not happen, unless the Business Template is broken. return dict(redirect_url=container.absolute_url(), selection_index=None, selection_name=None) security.declareProtected(Permissions.ModifyPortalContent, 'setGuid') def setGuid(self): """ This generate a global and unique id It will be defined like this : full dns name + portal_name + uid + random the guid should be defined only one time for each object """ if self.getGuid() is None: guid = '' # Set the dns name guid += gethostbyaddr(gethostname())[0] guid += '_' + self.portal_url.getPortalPath() guid += '_' + str(self.uid) guid += '_' + str(random.randrange(1,2147483600)) setattr(self, 'guid', guid) security.declareProtected(Permissions.AccessContentsInformation, 'getGuid') def getGuid(self): """ Get the global and unique id """ return getattr(aq_base(self), 'guid', None) security.declareProtected(Permissions.AccessContentsInformation, 'getTypeBasedMethod') def getTypeBasedMethod(self, *args, **kw): return self._getTypeBasedMethod(*args, **kw) # Type Casting def _getTypeBasedMethod(self, method_id, fallback_script_id=None, script_id=None,**kw): """ Looks up for a zodb script wich ends with what is given as method_id and starts with the name of the portal type or meta type. For example, method_id can be "asPredicate" and we will on a sale packing list line: SalePackingListLine_asPredicate DeliveryLine_asPredicate fallback_script_id : the script to use if nothing is found """ # script_id should not be used any more, keep compatibility if script_id is not None: LOG('ERP5Type/Base.getTypeBaseMethod',0, 'DEPRECATED script_id parameter is used') fallback_script_id=script_id # use a transactional variable to cache results within the same # transaction portal_type = self.getPortalType() tv = getTransactionalVariable() type_base_cache = tv.setdefault('Base.type_based_cache', {}) cache_key = (portal_type, method_id) try: script = type_base_cache[cache_key] except KeyError: script_name_end = '_' + method_id for base_class in self.__class__.mro(): if issubclass(base_class, Base): script_id = base_class.__name__.replace(' ','') + script_name_end script = getattr(self, script_id, None) if script is not None: type_base_cache[cache_key] = aq_inner(script) return script type_base_cache[cache_key] = None if script is not None: return script.__of__(self) if fallback_script_id is not None: return getattr(self, fallback_script_id) security.declareProtected(Permissions.AccessContentsInformation, 'skinSuper') def skinSuper(self, skin, id): if id[:1] != '_' and id[:3] != 'aq_': skin_info = SKINDATA.get(thread.get_ident()) if skin_info is not None: object = skinResolve(self.getPortalObject(), (skin_info[0], skin), id) if object is not None: return object.__of__(self) raise AttributeError(id) def _getAcquireLocalRoles(self): """This method returns the value of acquire_local_roles of the object's portal_type. - True means local roles are acquired, which is the standard behavior of Zope objects. - False means that the role acquisition chain is cut. The code to support this is on the user class, see ERP5Security.ERP5UserFactory.ERP5User """ def cached_getAcquireLocalRoles(portal_type): ti = self._getTypesTool().getTypeInfo(portal_type) return ti is None or ti.getTypeAcquireLocalRole() cached_getAcquireLocalRoles = CachingMethod(cached_getAcquireLocalRoles, id='Base__getAcquireLocalRoles', cache_factory='erp5_content_short') return cached_getAcquireLocalRoles(portal_type=self.getPortalType()) security.declareProtected(Permissions.AccessContentsInformation, 'get_local_permissions') def get_local_permissions(self): """ This works like get_local_roles. It allows to get all permissions defined locally """ local_permission_list = () for permission in self.possible_permissions(): permission_role = getattr(self,pname(permission),None) if permission_role is not None: local_permission_list += ((permission,permission_role),) return local_permission_list security.declareProtected(Permissions.ManagePortal, 'manage_setLocalPermissions') def manage_setLocalPermissions(self,permission,local_permission_list=None): """ This works like manage_setLocalRoles. It allows to set all permissions defined locally """ permission_name = pname(permission) if local_permission_list is None: if hasattr(self,permission_name): delattr(self,permission_name) else: if isinstance(local_permission_list, str): local_permission_list = (local_permission_list,) setattr(self,permission_name,tuple(local_permission_list)) ### Content accessor methods security.declareProtected(Permissions.View, 'getSearchableText') def getSearchableText(self, md=None): """ Used by the catalog for basic full text indexing. """ searchable_text_list = [] portal_type = self.portal_types.getTypeInfo(self) if portal_type is None: # it can be a temp object or a tool (i.e. Activity Tool) for which we have no portal_type definition # so use definition of 'Base Type' for searchable methods & properties portal_type = self.portal_types.getTypeInfo('Base Type') searchable_text_method_id_list = [] # generated from properties methods and add explicitly defined method_ids as well for searchable_text_property_id in portal_type.getSearchableTextPropertyIdList(): if self.hasProperty(searchable_text_property_id): method_id = convertToUpperCase(searchable_text_property_id) searchable_text_method_id_list.extend(['get%s' %method_id]) searchable_text_method_id_list.extend(portal_type.getSearchableTextMethodIdList()) for method_id in searchable_text_method_id_list: # XXX: how to exclude exclude acquisition (not working) #if getattr(aq_base(self), method_id, None) is not None: # method = getattr(self, method_id, None) # should we do it as ZSQLCatalog should care for calling this method on proper context? method = getattr(self, method_id, None) if method is not None: method_value = method() if method_value is not None: if isinstance(method_value, (list, tuple)): searchable_text_list.extend(method_value) else: searchable_text_list.append(method_value) searchable_text = ' '.join([str(x) for x in searchable_text_list]) return searchable_text.strip() # Compatibility with CMF Catalog / CPS sites SearchableText = getSearchableText security.declareProtected(Permissions.View, 'newError') def newError(self, **kw): """ Create a new Error object """ from Products.ERP5Type.Error import Error return Error(**kw) security.declarePublic('log') def log(self, description, content='', level=INFO): """Put a log message """ warnings.warn("The usage of Base.log is deprecated.\n" "Please use Products.ERP5Type.Log.log instead.", DeprecationWarning) unrestrictedLog(description, content = content, level = level) # Dublin Core Emulation for CMF interoperatibility # CMF Dublin Core Compatibility def Subject(self): return self.getSubjectList() def Description(self): return self.getDescription('') def EffectiveDate(self): return self.getEffectiveDate('None') def ExpirationDate(self): return self.getExpirationDate('None') def Contributors(self): return self.getContributorList() def Format(self): return self.getFormat('') def Language(self): return self.getLanguage('') def Rights(self): return self.getRight('') # Creation and modification date support through workflow security.declareProtected(Permissions.AccessContentsInformation, 'getCreationDate') def getCreationDate(self): """ Returns the creation date of the document based on workflow information """ # Check if edit_workflow defined portal_workflow = getToolByName(self.getPortalObject(), 'portal_workflow') wf = portal_workflow.getWorkflowById('edit_workflow') wf_list = list(portal_workflow.getWorkflowsFor(self)) if wf is not None: wf_list = [wf] + wf_list for wf in wf_list: try: history = wf.getInfoFor(self, 'history', None) except KeyError: history = None if history is not None: if len(history): # Then get the first line of edit_workflow return history[0].get('time', None) if getattr(aq_base(self), 'CreationDate', None) is not None: return asDate(self.CreationDate()) return None # JPS-XXX - try to find a way to return a creation date instead of None security.declareProtected(Permissions.AccessContentsInformation, 'getModificationDate') def getModificationDate(self): """ Returns the modification date of the document based on workflow information NOTE: this method is not generic enough. Suggestion: define a modification_date variable on the workflow which is an alias to time. XXX: Should we return the ZODB date if it's after the last history entry ? """ try: history_list = aq_base(self).workflow_history except AttributeError: pass else: max_date = None for history in history_list.itervalues(): try: date = history[-1]['time'] except (IndexError, KeyError): continue if date > max_date: max_date = date if max_date: # Return a copy of history time, to prevent modification return DateTime(max_date) if self._p_serial: return DateTime(TimeStamp(self._p_serial).timeTime()) # Layout management security.declareProtected(Permissions.AccessContentsInformation, 'getApplicableLayout') def getApplicableLayout(self): """ The applicable layout of a standard document in the content layout. However, if we are displaying a Web Section as its default document, we should use the container layout. """ try: # Default documents should be displayed in the layout of the container if self.REQUEST.get('is_web_section_default_document', None): return self.REQUEST.get('current_web_section').getContainerLayout() # ERP5 Modules should be displayed as containers # XXX - this shows that what is probably needed is a more sophisticated # mapping system between contents and layouts. if self.getParentValue().meta_type == 'ERP5 Site': return self.getContainerLayout() return self.getContentLayout() or self.getContainerLayout() except AttributeError: return None security.declarePublic('isWebMode') def isWebMode(self): """ return True if we are in web mode """ if self.getApplicableLayout() is None: return False if getattr(self.REQUEST, 'ignore_layout', 0): return False return True security.declarePublic('isEditableWebMode') def isEditableWebMode(self): """ return True if we are in editable mode """ return getattr(self.REQUEST, 'editable_mode', 0) security.declarePublic('isEditableMode') isEditableMode = isEditableWebMode # for backwards compatability security.declareProtected(Permissions.ChangeLocalRoles, 'updateLocalRolesOnSecurityGroups') def updateLocalRolesOnSecurityGroups(self, **kw): """Assign Local Roles to Groups on self, based on Portal Type Role Definitions and "ERP5 Role Definition" objects contained inside self. """ self._getTypesTool().getTypeInfo(self) \ .updateLocalRolesOnDocument(self, **kw) security.declareProtected(Permissions.ModifyPortalContent, 'assignRoleToSecurityGroup') def assignRoleToSecurityGroup(self, **kw): """DEPRECATED. This is basically the same as `updateLocalRolesOnSecurityGroups`, but with a different permission. """ warnings.warn('assignRoleToSecurityGroup is a deprecated alias to ' 'updateLocalRolesOnSecurityGroups. Please note that the ' 'permission changed to "Change Local Roles".', DeprecationWarning) self.updateLocalRolesOnSecurityGroups(**kw) security.declareProtected(Permissions.ManagePortal, 'updateRoleMappingsFor') def updateRoleMappingsFor(self, wf_id, **kw): """ Update security policy according to workflow settings given by wf_id There's no check that the document is actually chained to the workflow, it's caller responsability to perform this check. """ workflow = self.portal_workflow.getWorkflowById(wf_id) if workflow is not None: changed = workflow.updateRoleMappingsFor(self) if changed: self.reindexObjectSecurity(activate_kw={'priority':4}) # Template Management security.declareProtected(Permissions.View, 'getDocumentTemplateList') def getDocumentTemplateList(self) : """ Returns an empty list of allowed templates (this is not a folder) """ return [] security.declareProtected(Permissions.ModifyPortalContent,'makeTemplate') def makeTemplate(self): """ Make document behave as a template. A template is no longer indexable TODO: - make template read only, acquired local roles, etc. - stronger security model - prevent from changing templates or invoking workflows """ parent = self.getParentValue() if parent.getPortalType() != "Preference" and not parent.isTemplate: raise ValueError, "Template documents can not be created outside Preferences" self.isTemplate = ConstantGetter('isTemplate', value=True) # XXX reset security here security.declareProtected(Permissions.ModifyPortalContent,'makeTemplateInstance') def makeTemplateInstance(self): """ Make document behave as standard document (indexable) """ if self.getParentValue().getPortalType() == "Preference": raise ValueError, "Template instances can not be created within Preferences" # We remove attributes from the instance # We do this rather than self.isIndexable = 0 because we want to # go back to previous situation (class based definition) if self.__dict__.has_key('isIndexable'): delattr(self, 'isIndexable') if self.__dict__.has_key('isTemplate'): delattr(self, 'isTemplate') # Add to catalog self.reindexObject() # ZODB Transaction Management security.declarePublic('serialize') def serialize(self): """Make the transaction accessing to this object atomic """ self._p_changed = 1 # Helpers def getQuantityPrecisionFromResource(self, resource, d=2): """ Provides a quick access to precision without accessing the resource value in ZODB. Here resource is the relative_url of the resource, such as the result of self.getResource(). """ def cached_getQuantityPrecisionFromResource(resource): if resource: resource_value = self.portal_categories.resolveCategory(resource) if resource_value is not None: return resource_value.getQuantityPrecision() return None cached_getQuantityPrecisionFromResource = CachingMethod( cached_getQuantityPrecisionFromResource, id='Base_getQuantityPrecisionFromResource', cache_factory='erp5_content_short') precision = cached_getQuantityPrecisionFromResource(resource) if precision is None: precision = d return precision security.declareProtected(Permissions.ModifyPortalContent, 'setDefaultReindexParameters' ) def setDefaultReindexParameters(self, **kw): warnings.warn('setDefaultReindexParameters is deprecated in favour of ' 'setDefaultReindexParameterDict.', DeprecationWarning) self.setDefaultReindexParameterDict(kw) security.declareProtected(Permissions.ModifyPortalContent, 'setDefaultReindexParameterDict' ) def setDefaultReindexParameterDict(self, kw): # This method sets the default keyword parameters to reindex. This is useful # when you need to specify special parameters implicitly (e.g. to reindexObject). tv = getTransactionalVariable() key = ('default_reindex_parameter', id(aq_base(self))) tv[key] = kw security.declareProtected(Permissions.AccessContentsInformation, 'getDefaultReindexParameterDict') def getDefaultReindexParameterDict(self, inherit_placeless=True): # This method returns default reindex parameters to self. # The result can be either a dict object or None. tv = getTransactionalVariable() if inherit_placeless: placeless = tv.get(('default_reindex_parameter', )) if placeless is not None: placeless = placeless.copy() else: placeless = None local = tv.get(('default_reindex_parameter', id(aq_base(self)))) if local is None: result = placeless else: if placeless is None: result = local.copy() else: # local defaults takes precedence over placeless defaults. result = {} result.update(placeless) result.update(local) return result security.declareProtected(Permissions.AccessContentsInformation, 'isItem') def isItem(self): return self.portal_type in self.getPortalItemTypeList() security.declareProtected(Permissions.DeletePortalContent, 'migratePortalType') def migratePortalType(self, portal_type): """ Recreate document by recomputing inputted parameters with help of contribution tool. Use an Unrestricted method to edit related relations on other objects. """ if self.getPortalType() == portal_type: raise TypeError, 'Can not migrate a document to same portal_type' if not portal_type: raise TypeError, 'Missing portal_type value' # Reingestion requested with portal_type. input_kw = {} input_kw['portal_type'] = portal_type for property_id in self.propertyIds(): if property_id not in ('portal_type', 'uid', 'id',) \ and self.hasProperty(property_id): input_kw[property_id] = self.getProperty(property_id) if getattr(self, 'hasUrlString', None) is not None and self.hasUrlString(): # try to reingest from url if data and/or filename is missing. if not 'data' in input_kw or not 'filename' in input_kw: # URL is not stored on document # pass required properties for portal_contributions.newContent input_kw['url'] = self.asURL() # Use meta transition to jump from one state to another # without existing transitions. from Products.ERP5.InteractionWorkflow import InteractionWorkflowDefinition portal = self.getPortalObject() workflow_tool = portal.portal_workflow worflow_variable_list = [] for workflow in workflow_tool.getWorkflowsFor(self): if not isinstance(workflow, InteractionWorkflowDefinition): worflow_variable_list.append(self.getProperty(workflow.state_var)) # then restart ingestion with new portal_type # XXX Contribution Tool accept only document which are containing # at least the couple data and filename or one url portal_contributions = portal.portal_contributions new_document = portal_contributions.newContent(**input_kw) # Meta transitions for state in worflow_variable_list: if workflow_tool._isJumpToStatePossibleFor(new_document, state): workflow_tool._jumpToStateFor(new_document, state) # Update relations UnrestrictedMethod(self.updateRelatedContent)(self.getRelativeUrl(), new_document.getRelativeUrl()) # Delete actual content self.getParentValue()._delObject(self.getId()) return new_document InitializeClass(Base) from Products.CMFCore.interfaces import IContentish # suppress CMFCore event machinery from trying to reindex us through events # by removing Products.CMFCore.interfaces.IContentish interface. # We reindex ourselves in manage_afterAdd thank you very much. def removeIContentishInterface(cls): classImplementsOnly(cls, implementedBy(cls) - IContentish) removeIContentishInterface(Base) class TempBase(Base): """ If we need Base services (categories, edit, etc) in temporary objects we shoud used TempBase """ isIndexable = ConstantGetter('isIndexable', value=False) isTempDocument = ConstantGetter('isTempDocument', value=True) # Declarative security security = ClassSecurityInfo() def reindexObject(self, *args, **kw): pass def recursiveReindexObject(self, *args, **kw): pass def activate(self, *args, **kw): return self def setUid(self, value): self.uid = value # Required for Listbox so that no casting happens when we use TempBase to create new objects def setTitle(self, value): """ Required so that getProperty('title') will work on tempBase objects The dynamic acquisition work very well for a lot of properties, but not for title. For example, if we do setProperty('organisation_url'), then even if organisation_url is not in a propertySheet, the method getOrganisationUrl will be generated. But this does not work for title, because I(seb)'m almost sure there is somewhere a method '_setTitle' or 'setTitle' with no method getTitle on Base. That why setProperty('title') and getProperty('title') does not work. """ self.title = value def getTitle(self): """ Returns the title of this document """ return getattr(aq_base(self), 'title', None) security.declarePublic('setProperty') security.declarePublic('getProperty') security.declarePublic('edit') # Persistence.Persistent is one of the superclasses of TempBase, and on Zope2.8 # its __class_init__ method is InitializeClass. This is not the case on # Zope2.12 which requires us to call InitializeClass manually, otherwise # allow_class(TempBase) in ERP5Type/Document/__init__.py will trample our # ClassSecurityInfo with one that doesn't declare our public methods InitializeClass(TempBase)