Пример #1
0
class Project(Node, Movement, XMLMatrix):
    """
    Project is a class which describes a typical project in consulting firm.
    A project has a client, an invoiced client. A project has also a start
    date and a stop date. It is composed of several tasks.

    Each task has a person to perform it, a certain amount of time, a date,
    a place, a description. For each person and each task, there is dedicated
    time rate.
    """

    meta_type = 'ERP5 Project'
    portal_type = 'Project'
    add_permission = Permissions.AddPortalContent
    # XXX to index start_date and stop_date in delivery table:
    isDelivery = ConstantGetter('isDelivery', value=True)
    isAccountable = ConstantGetter('isAccountable', value=False)

    zope.interface.implements(interfaces.INode, interfaces.IMovement)

    # Declarative security
    security = ClassSecurityInfo()
    security.declareObjectProtected(Permissions.AccessContentsInformation)

    # Default Properties
    property_sheets = (
        PropertySheet.Base,
        PropertySheet.DublinCore,
        PropertySheet.XMLObject,
        PropertySheet.CategoryCore,
        PropertySheet.Arrow,
        PropertySheet.Task,
        PropertySheet.Reference,
    )
Пример #2
0
    def generatePortalTypeAccessors(cls, site, portal_type_category_list):
        category_tool = getattr(site, 'portal_categories', None)
        for category_id in portal_type_category_list:
            # we need to generate only categories defined on portal type
            CategoryProperty.applyDefinitionOnAccessorHolder(
                cls, category_id, category_tool)

        portal_workflow = getattr(site, 'portal_workflow', None)
        if portal_workflow is None:
            if not getattr(site, '_v_bootstrapping', False):
                LOG(
                    "ERP5Type.Dynamic", WARNING,
                    "Could not generate workflow methods for %s" %
                    cls.__name__)
        else:
            initializePortalTypeDynamicWorkflowMethods(cls, portal_workflow)

        # portal type group methods, isNodeType, isResourceType...
        from Products.ERP5Type.ERP5Type import ERP5TypeInformation
        # XXX possible optimization:
        # generate all methods on Base accessor holder, with all methods
        # returning False, and redefine on portal types only those returning True,
        # aka only those for the group they belong to
        for group in ERP5TypeInformation.defined_group_list:
            value = cls.__name__ in site._getPortalGroupedTypeSet(group)
            accessor_name = 'is' + UpperCase(group) + 'Type'
            method = ConstantGetter(accessor_name, group, value)
            cls.registerAccessor(method, Permissions.AccessContentsInformation)

        from Products.ERP5Type.Cache import initializePortalCachingProperties
        initializePortalCachingProperties(site)
Пример #3
0
class Acknowledgement(EmailDocumentProxyMixin, Event):
    """
    goal :

    Acts as a proxy to the message in the case of
    - private email
    - message displayed to the user ?

    We need this proxy because the user might not have the right to access
    to the original message, and we don't wish to duplicate the content of
    the original message (which can use attachements).

    Use Case:

      - A Site Notification is created in order to notify to all people of a
      company. Then every time an user will acknowledge the notification,
      a new Acknowledgement is created.
  """

    meta_type = 'ERP5 Acknowledgement'
    portal_type = 'Acknowledgement'
    add_permission = Permissions.AddPortalContent
    isDelivery = ConstantGetter('isDelivery', value=True)

    # Declarative security
    security = ClassSecurityInfo()
    security.declareObjectProtected(Permissions.AccessContentsInformation)

    # Declarative properties
    property_sheets = (PropertySheet.Base, PropertySheet.XMLObject,
                       PropertySheet.CategoryCore, PropertySheet.Document,
                       PropertySheet.DublinCore, PropertySheet.Task,
                       PropertySheet.Url, PropertySheet.Arrow,
                       PropertySheet.Event, PropertySheet.Delivery,
                       PropertySheet.DocumentProxy)
Пример #4
0
class TempXMLMatrix(XMLMatrix):
    """
    Temporary XMLMatrix.

    If we need Base services (categories, edit, etc) in temporary objects
    we shoud used TempBase
  """
    isIndexable = ConstantGetter('isIndexable', value=False)

    def newCellContent(self, id):
        """
       Creates a new content in a cell.
    """
        new_temp_object = TempBase(id)
        self._setObject(id, new_temp_object)
        return self.get(id)

    def reindexObject(self, *args, **kw):
        pass

    def unindexObject(self, *args, **kw):
        pass

    def activate(self):
        return self
Пример #5
0
class InventoryCell(DeliveryCell):
    """
      An InventoryCell allows to define specific inventory
      for each variation of a resource in an inventory line.
    """

    meta_type = 'ERP5 Inventory Cell'
    portal_type = 'Inventory Cell'
    add_permission = Permissions.AddPortalContent
    isInventoryMovement = ConstantGetter('isInventoryMovement', value=True)

    # Declarative security
    security = ClassSecurityInfo()
    security.declareObjectProtected(Permissions.AccessContentsInformation)

    # Declarative properties
    property_sheets = ( PropertySheet.Base
                      , PropertySheet.CategoryCore
                      , PropertySheet.Amount
                      , PropertySheet.InventoryMovement
                      , PropertySheet.Task
                      , PropertySheet.Movement
                      , PropertySheet.Price
                      , PropertySheet.Predicate
                      , PropertySheet.MappedValue
                      , PropertySheet.ItemAggregation
                      )
    
    security.declareProtected(Permissions.AccessContentsInformation, 'getTotalInventory')
    def getTotalInventory(self):
      """
        Returns the inventory, as cells are not supposed to contain more cells.
      """
      return self.getInventory()

    security.declareProtected(Permissions.AccessContentsInformation, 'getQuantity')
    def getQuantity(self):
      """
        Computes a quantity which allows to reach inventory
      """
      if not self.hasCellContent():
        # First check if quantity already exists
        quantity = self._baseGetQuantity()
        if quantity not in (0.0, 0, None):
          return quantity
        # Make sure inventory is defined somewhere (here or parent)
        if getattr(aq_base(self), 'inventory', None) is None:
          return 0.0 # No inventory defined, so no quantity
        return self.getInventory()
      else:
        return None
      
    # Inventory cataloging
    security.declareProtected(Permissions.AccessContentsInformation, 'getConvertedInventory')
    def getConvertedInventory(self):
      """
        provides a default inventory value - None since
        no inventory was defined.
      """
      return self.getInventory() # XXX quantity unit is missing
Пример #6
0
class Capacity(XMLObject):
    """
        Mix-in
    """
    meta_type = 'ERP5 Capacity'

    isCapacity = ConstantGetter('isCapacity', value=True)

    # Declarative security
    security = ClassSecurityInfo()
    security.declareObjectProtected(Permissions.AccessContentsInformation)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'asCapacityItemList')

    def asCapacityItemList(self):
        """
        Returns an association list of points and capacity values
      """
        pass

    security.declarePublic('reindexObject')

    def reindexObject(self):
        """
        Overload reindexing in order to forward any modifications of capacity
        to simulation tool
      """
        self.getParentValue().updateCapacity()
        XMLObject.reindexObject(self)
Пример #7
0
class BalanceTransactionLine(AccountingTransactionLine, InventoryLine):
    """A balance transaction line inherits price handling from accounting
  transaction line, and indexing from inventory line.
  """

    meta_type = 'ERP5 Balance Transaction Line'
    portal_type = 'Balance Transaction Line'
    add_permission = Permissions.AddPortalContent
    isInventoryMovement = ConstantGetter('isInventoryMovement', value=True)

    # Declarative security
    security = ClassSecurityInfo()
    security.declareObjectProtected(Permissions.AccessContentsInformation)

    reindexObject = InventoryLine.reindexObject
    recursiveReindexObject = InventoryLine.recursiveReindexObject
    immediateReindexObject = InventoryLine.immediateReindexObject
Пример #8
0
class Ticket(Project):
    """
    A Ticket allows to track a sales process involving
    multilple Person and Organisations. It is a placeholder for
    documents, events, etc.

    Tickets behave both a movements and as projects:

    - as movements because they relate to an amount
      of resource exchanged between multiple parties

    - as a project because it acts as a reporting
      node for other movements (ex. accounting,
      task reports)

    Ticket are a good example of documents which may require
    synchronisation process accross multiple sites and
    for which acquisition properties such as source_title
    may be useful to provide a simple way to synchronise
    data with relations.
    """

    meta_type = 'ERP5 Ticket'
    portal_type = 'Ticket'
    add_permission = Permissions.AddPortalContent
    isDelivery = ConstantGetter('isDelivery', value=True)

    # Declarative security
    security = ClassSecurityInfo()
    security.declareObjectProtected(Permissions.AccessContentsInformation)

    # Declarative properties
    property_sheets = (PropertySheet.Base, PropertySheet.XMLObject,
                       PropertySheet.CategoryCore, PropertySheet.DublinCore,
                       PropertySheet.Arrow, PropertySheet.Price,
                       PropertySheet.Movement, PropertySheet.Amount,
                       PropertySheet.Ticket)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'isAccountable')

    def isAccountable(self):
        """Tickets are accountable.
      """
        return 1
class GroupCalendarAssignment(PresencePeriod):
    # CMF Type Definition
    meta_type = 'ERP5 Group Calendar Assignment'
    portal_type = 'Group Calendar Assignment'

    # Declarative security
    security = ClassSecurityInfo()
    security.declareObjectProtected(Permissions.AccessContentsInformation)

    # XXX GroupCalendarAssignment are not a delivery, but we enable this to be able
    # to search them by date in the module.
    isDelivery = ConstantGetter('isDelivery', value=True)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getPeriodList')

    def getPeriodList(self):
        period_list = []

        method = self._getTypeBasedMethod("getPeriodList")
        if method is None:
            group_calendar = self.getSpecialiseValue()
            if group_calendar is not None:
                period_list = group_calendar.objectValues(
                    portal_type=self.getPortalCalendarPeriodTypeList())
        else:
            period_list = method()
        return period_list

    def _getDatePeriodDataList(self):
        result = []
        start_date = self.getStartDate()
        stop_date = self.getStopDate()
        if not (None in (self.getDestinationUid(), start_date)):
            period_list = self.getPeriodList()
            if len(period_list):
                for period in period_list:
                    for date_period_data in period._getDatePeriodDataList():
                        if date_period_data['start_date'].greaterThanEqualTo(
                                start_date):
                            if stop_date is None or date_period_data[
                                    'stop_date'].lessThanEqualTo(stop_date):
                                result.append(date_period_data)
        return result
Пример #10
0
class BaseCategory(CMFBaseCategory, XMLObject):
    """
      Base Categories allow to implement virtual categories
      through acquisition
    """
    meta_type = 'ERP5 Base Category'
    portal_type = 'Base Category'  # maybe useful some day
    isCategory = ConstantGetter('isCategory', value=True)
    allowed_types = ('ERP5 Category', )

    # Declarative security
    security = ClassSecurityInfo()
    security.declareObjectProtected(Permissions.AccessContentsInformation)

    property_sheets = (PropertySheet.Base, PropertySheet.SimpleItem,
                       PropertySheet.CategoryCore, PropertySheet.BaseCategory,
                       PropertySheet.Predicate)

    # Experimental - WebDAV browsing support - ask JPS
    security.declareProtected(Permissions.AccessContentsInformation,
                              'experimental_listDAVObjects')

    def experimental_listDAVObjects(self):
        from zLOG import LOG
        LOG("BaseCategory listDAVObjects", 0, "listDAVObjects")
        return []
        result = self.objectValues(spec=('ERP5 Categorya',
                                         'ERP5 Base Category'))
        result.append(self.getParentValue())
        #result.extend(self.portal_catalog())
        return result

    security.declarePrivate('manage_afterAdd')

    def manage_afterAdd(self, item, container):
        """
         Reset Accessors
      """
        self.getPortalObject(
        ).portal_types.resetDynamicDocumentsOnceAtTransactionBoundary()
        CMFBaseCategory.manage_afterAdd(self, item, container)
Пример #11
0
class DomainGenerator(XMLObject):
    """
  This class defines a predicate as well as all necessary
  information to generate subdomains.

  Instances are stored in RAM as temp objects

  Generator API - DRAFT
  """
    meta_type = 'ERP5 Domain Generator'
    portal_type = 'Domain Generator'
    isPortalContent = ConstantGetter('isPortalContent', value=False)
    icon = None

    security = ClassSecurityInfo()
    security.declareObjectProtected(Permissions.AccessContentsInformation)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getDomainGeneratorList')

    def getDomainGeneratorList(self,
                               depth=0,
                               klass=None,
                               script='',
                               parent=None):
        """
    """
        # check parameters
        if script == '':
            return []
        if parent is None:
            parent = self
        if klass is None:
            # in casre we are not a temp object
            klass = self
        # We call a script which builds for us a list DomainGenerator instances
        # We need a way to know how deep we are in the domain generation
        # to prevent infinite recursion
        method = getattr(klass, script)
        return method(depth=depth, parent=parent)
Пример #12
0
class SupplyLine(Path, Amount, XMLMatrix):
    """A Supply Line is a path to define price
  """

    meta_type = 'ERP5 Supply Line'
    portal_type = 'Supply Line'
    add_permission = Permissions.AddPortalContent
    isPredicate = ConstantGetter('isPredicate', value=True)

    # Declarative security
    security = ClassSecurityInfo()
    security.declareObjectProtected(Permissions.AccessContentsInformation)

    # Declarative properties
    property_sheets = (PropertySheet.Base, PropertySheet.XMLObject,
                       PropertySheet.CategoryCore, PropertySheet.Amount,
                       PropertySheet.Task, PropertySheet.Arrow,
                       PropertySheet.Movement, PropertySheet.Price,
                       PropertySheet.SupplyLine, PropertySheet.VariationRange,
                       PropertySheet.Path, PropertySheet.FlowCapacity,
                       PropertySheet.Predicate, PropertySheet.Comment,
                       PropertySheet.Reference)

    #############################################
    # Pricing methods
    #############################################
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getPrice')

    def getPrice(self):
        # FIXME: this have to be done in an interaction wf
        if getattr(self, 'price', None) is None:
            self.price = 0.0
        # Return the price
        return self.price

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getTotalPrice')

    def getTotalPrice(self):
        """
      Returns the totals price for this line
    """
        quantity = self.getQuantity() or 0.0
        price = self.getPrice() or 0.0
        return quantity * price

    def _getPrice(self, context):
        return 0.0

    def _getTotalPrice(self, context):
        return 0.0

    security.declareProtected(Permissions.AccessContentsInformation,
                              'isAccountable')

    def isAccountable(self):
        """Supply Line are not accounted.
    """
        return 0

    #############################################
    # Predicate method
    #############################################
    asPredicate = Path.asPredicate

    #############################################
    # XMLMatrix methods
    # XXX to be removed if possible
    #############################################
    security.declareProtected(Permissions.AccessContentsInformation,
                              'hasCellContent')

    def hasCellContent(self, base_id='path'):
        """
        This method can be overriden
    """
        return XMLMatrix.hasCellContent(self, base_id=base_id)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getCellValueList')

    def getCellValueList(self, base_id='path'):
        """
        This method can be overriden
    """
        return XMLMatrix.getCellValueList(self, base_id=base_id)

    security.declareProtected(Permissions.AccessContentsInformation, 'getCell')

    def getCell(self, *kw, **kwd):
        """
        This method can be overriden
    """
        kwd.setdefault('base_id', 'path')
        return XMLMatrix.getCell(self, *kw, **kwd)

    security.declareProtected(Permissions.ModifyPortalContent, 'newCell')

    def newCell(self, *kw, **kwd):
        """
        This method creates a new cell
    """
        kwd.setdefault('base_id', 'path')
        return XMLMatrix.newCell(self, *kw, **kwd)

    ############################################################
    # Quantity predicate API
    ############################################################
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getQuantityPredicateIdList')

    def getQuantityPredicateIdList(self, price_parameter):
        """
      Return predicate id related to a price parameter.
    """
        predicate_id_start_with = "quantity_range_"
        if price_parameter not in ("base_price", "slice_base_price"):
            predicate_id_start_with = "%s_%s" % \
                (price_parameter, predicate_id_start_with)
        # XXX Hardcoded portal type name
        predicate_list = self.objectValues(portal_type='Predicate',
                                           sort_on=('int_index', 'id'))
        predicate_list.sort(key=lambda p: p.getIntIndex() or p.getId())
        predicate_id_list = [x.getId() for x in predicate_list]
        result = [x for x in predicate_id_list \
                  if x.startswith(predicate_id_start_with)]
        return result

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getQuantityPredicateValueList')

    def getQuantityPredicateValueList(self, price_parameter):
        """
      Return predicate related to a price parameter.
    """
        result = [self[x] for x in \
                self.getQuantityPredicateIdList(price_parameter)]
        return result

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getQuantityStepList')

    def getQuantityStepList(self, *args, **kw):
        """
      Return predicate step related to a price_parameter
    """
        # We need to keep compatibility with generated accessor
        price_parameter = kw.get('price_parameter', "base_price")
        if price_parameter in ("base_price", "slice_base_price"):
            method_name = "_baseGetQuantityStepList"
        else:
            method_name = 'get%sList' % \
                         convertToUpperCase("%s_quantity_step" % price_parameter)
        return getattr(self, method_name)() or []

    security.declareProtected(Permissions.ModifyPortalContent,
                              'updateQuantityPredicate')

    def updateQuantityPredicate(self, price_parameter):
        """
      Update the quantity predicate for this price parameter
    """
        quantity_step_list = self.getQuantityStepList(
            price_parameter=price_parameter)
        unused_predicate_id_set = self.getQuantityPredicateIdList(
            price_parameter)
        if quantity_step_list:
            quantity_step_list.sort()
            quantity_step_list = [0] + quantity_step_list + [None]
            getTitle = getattr(
                self,
                'SupplyLine_getTitle',
                lambda min, max: (('' if min is None else '%s <= ' %
                                   (min, )) + 'quantity' +
                                  ('' if max is None else ' < %s' % (max, ))),
            )
            predicate_id_start_with = "quantity_range"
            if price_parameter not in ("base_price", "slice_base_price"):
                predicate_id_start_with = "%s_%s" % (
                    price_parameter,
                    predicate_id_start_with,
                )
            for i in range(len(quantity_step_list) - 1):
                min_quantity = quantity_step_list[i]
                max_quantity = quantity_step_list[i + 1]
                predicate_id = '%s_%s' % (predicate_id_start_with, i)
                try:
                    predicate_value = self[predicate_id]
                except KeyError:
                    # XXX Hardcoded portal type name
                    predicate_value = self.newContent(
                        id='%s_%s' % (predicate_id_start_with, i),
                        portal_type='Predicate',
                        int_index=i + 1,
                    )
                else:
                    unused_predicate_id_set.remove(predicate_id)
                predicate_value.setTitle(
                    getTitle(min=min_quantity, max=max_quantity))
                predicate_value.setCriterionPropertyList(('quantity', ))
                predicate_value.setCriterion(
                    'quantity',
                    min=min_quantity,
                    max=(None if price_parameter == 'slice_base_price' else
                         max_quantity),
                )
        if unused_predicate_id_set:
            self.deleteContent(unused_predicate_id_set)
Пример #13
0
class ConfiguratorTool(BaseTool):
  """This tool provides a Configurator Tool.
  """

  id = 'portal_configurator'
  title = 'Configurations'
  meta_type = 'ERP5 Configurator Tool'
  portal_type = 'Configurator Tool'

  isPortalContent = ConstantGetter('isPortalContent', value=True)

  security = ClassSecurityInfo()

  security.declareProtected(Permissions.ManagePortal, 'manage_overview')

  def login(self, REQUEST):
    """ Login client and show next form. """
    bc = REQUEST.get('field_your_business_configuration')
    user_preferred_language = REQUEST.get(
        'field_my_user_preferred_language', None)
    if user_preferred_language:
      # Set language value to request so that next page after login
      # can get the value. Because cookie value is available from
      # next request.
      REQUEST.set(LANGUAGE_COOKIE_NAME, user_preferred_language)
      REQUEST.RESPONSE.setCookie(LANGUAGE_COOKIE_NAME,
                                   user_preferred_language,
                                   path='/',
                                   expires=(DateTime() + 30).rfc822())

    expires = (DateTime() + 1).toZone('GMT').rfc822()
    REQUEST.RESPONSE.setCookie(BUSINESS_CONFIGURATION_COOKIE_NAME,
                               bc,
                               expires=expires)
    REQUEST.set(BUSINESS_CONFIGURATION_COOKIE_NAME, bc)
    return self.next(REQUEST=REQUEST)

  #security.declareProtected(Permissions.ModifyPortalContent, 'next')
  def next(self, REQUEST):
    """ Validate settings and return a new form to the user.  """
    # check if user is allowed to access service
    portal = self.getPortalObject()
    kw = self.REQUEST.form.copy()
    business_configuration = REQUEST.get(BUSINESS_CONFIGURATION_COOKIE_NAME)
    bc = portal.restrictedTraverse(business_configuration)
    if bc is None:
      REQUEST.set('portal_status_message',
                   self.Base_translateString(
                     'You cannot Continue. Unable to find your Business Configuration.'))
      return self.view()
    response = self._next(business_configuration=bc, kw=kw)
    ## Parse server response
    if response["command"] == "show":
      return self.ConfiguratorTool_dialogForm(previous=response['previous'],
                                        form_html=response["data"],
                                        next=response['next'])
    elif response["command"] == "install":
      return self.startInstallation(bc, REQUEST=REQUEST)

  def _next(self, business_configuration, kw):
    """ Return next configuration form and validate previous. """
    form_kw = {}
    need_validation = 1
    validation_errors = None
    response = {}

    business_configuration.initializeWorkflow()

    ## initial state no previous form to validate
    if business_configuration.isInitialConfigurationState():
      need_validation = 0

    ## client can not go further hist business configuration is already built
    if business_configuration.isEndConfigurationState() or \
         business_configuration.getNextTransition() == None:
      return self._terminateConfigurationProcess(response)

    isMultiEntryTransition = business_configuration._isMultiEntryTransition()
    ## validate multiple forms
    if isMultiEntryTransition:
      html_forms = []
      failed_forms_counter = 0
      transition = business_configuration.getNextTransition()
      form = getattr(business_configuration, transition.getTransitionFormId())
      for form_key in [x for x in kw.keys() if x.startswith('field_')]:
        form_kw[form_key] = kw[form_key]
      ## iterate all forms
      for form_counter in range(0, isMultiEntryTransition):
        single_form_kw = {}
        for key, value in form_kw.items():
          if isinstance(value, list) or isinstance(value, tuple):
            ## we have more than one form shown
            single_form_kw[key] = value[form_counter]
            # save original value in request in some cases of multiple forms
            # we need it for validation
            single_form_kw['_original_%s' % key] = value
          else:
            ## even though we have multiple entry transition customer wants
            ## ONE form!
            single_form_kw[key] = value
        ## update properly REQUEST with current form data
        for key, value in single_form_kw.items():
          self.REQUEST.set(key, value)
        ## get validation status
        validation_status, dummy, validation_errors = \
           business_configuration._validateNextForm(**single_form_kw)

        ## clean up REQUEST from traces from validate_all_to_request
        ## otherwise next form will use previous forms details
        cleanup_keys = [x for x in self.REQUEST.other.keys() if x.startswith('my_') or x.startswith('your_')]
        for key in cleanup_keys:
          self.REQUEST.other.pop(key, None)
        ## render HTML code
        if validation_status != 0:
          failed_forms_counter += 1
          ## XXX: form can fail because a new
          ## http://localhost:9080/erp5/portal_wizard/next is issued
          ## without arguments. Improve this
          try:
            self.REQUEST.set('field_errors',
                form.ErrorFields(validation_errors))
          except Exception:
            pass
          single_form_html = form()
          self.REQUEST.other.pop('field_errors', None)
          self.REQUEST.form = {}
        else:
          single_form_html = form()
        ## wrap in form template
        single_form_html = self.Base_mainConfiguratorFormTemplate(
                                current_form_number = form_counter + 1,
                                max_form_numbers = isMultiEntryTransition,
                                form_html = single_form_html)
        ## add to list of forms as html code
        html_forms.append(single_form_html)
      ## return if failure
      if failed_forms_counter > 0:
        next_state = self.restrictedTraverse(business_configuration.getNextTransition()\
            .getDestination())
        html_data = self.Base_mainConfiguratorTemplate(
            form_html = "\n".join(html_forms),
            current_state = next_state,
            business_configuration = business_configuration)
        response.update(command = "show",
                  previous = self.Base_translateString("Previous"),
                  next = self.Base_translateString(transition.getTitle()),
                  data = html_data)
        return response

    ## show next form in transitions
    rendered = False
    while rendered is False:
      if need_validation == 1:
        if isMultiEntryTransition:
          ## multiple forms must be validated before
          validation_status = 0
        else:
          validation_status, form_kw, validation_errors = \
              business_configuration._validateNextForm(**kw)
        if validation_status == 1:
          need_validation = 0
        elif validation_status == 2:
          rendered = True
          need_validation = 0
          if business_configuration.getNextTransition() == None:
            ### client can not continue at the momen
            return self._terminateConfigurationProcess(response)
          response["previous"], html, _, response["next"] \
                  = business_configuration._displayNextForm()
        else:
          ## validation passed
          need_validation = 0
          business_configuration._executeTransition(form_kw=form_kw, request_kw=kw)
      elif need_validation == 0:
        if business_configuration.getNextTransition() == None:
          return self._terminateConfigurationProcess(response)
        ## validation failure
        rendered = True
        response["previous"], html, _, response["next"] =\
            business_configuration._displayNextForm(
                validation_errors=validation_errors)

    if html is None:
      ## we have no more forms proceed to build
      response.update(command="install", data=None)
    else:
      ## we have more forms
      next_state = self.restrictedTraverse(business_configuration.getNextTransition()\
          .getDestination())
      html_data = self.Base_mainConfiguratorTemplate(
          form_html = html,
          current_state = next_state,
          business_configuration = business_configuration)
      response.update(command = "show", data = html_data)
    return response

  def _terminateConfigurationProcess(self, response):
    """ Terminate process and return some explanations to client why
        he can no longer continue. """
    response.update(command="show",
                    next=None, \
                    previous=None,
                    data=self.BusinessConfiguration_viewStopForm())
    return response

  #security.declareProtected(Permissions.ModifyPortalContent, 'previous')
  def previous(self, REQUEST):
    """ Display the previous form. """
    # check if user is allowed to access service
    portal = self.getPortalObject()
    kw = self.REQUEST.form.copy()
    business_configuration = REQUEST.get(BUSINESS_CONFIGURATION_COOKIE_NAME)
    bc = portal.restrictedTraverse(business_configuration)
    response = self._previous(business_configuration=bc, kw=kw)
    return self.ConfiguratorTool_dialogForm(previous=response['previous'],
                                      form_html=response['data'],
                                      next=response['next'])

  def _previous(self, business_configuration, kw):
    """ Returns previous form. """
    response = {}
    ## client can not go further his business configuration is already built
    if business_configuration is None or \
         business_configuration.isEndConfigurationState():
      form_html = self.BusinessConfiguration_viewStopForm()
      return self.ConfiguratorTool_dialogForm(form_html = form_html,
                                        next = "Next")

    response['previous'], form_html, _, response['next'] = \
        business_configuration._displayPreviousForm()

    next_state = self.restrictedTraverse(
        business_configuration.getNextTransition().getDestination())

    response['data'] = self.Base_mainConfiguratorTemplate(
        form_html=form_html,
        current_state=next_state,
        business_configuration=business_configuration)
    return response

  security.declarePublic('getInstallationStatusReport')
  def getInstallationStatusReport(self,
                          active_process_id=None, REQUEST=None):
    """ Query local ERP5 instance for installation status.
        If installation is over the installation activities and reindexing
        activities should not exists.
    """
    portal_activities = self.getPortalObject().portal_activities

    if 0 == len(portal_activities.getMessageList()):
      html = self.ConfiguratorTool_viewSuccessfulConfigurationMessageRenderer()
    else:
      # only if bt5s are installed start tracking number of activities
      activity_list = portal_activities.getMessageList()
      installation_status['activity_list'].append(len(activity_list))
      html = self.ConfiguratorTool_viewRunningInstallationMessage(
          installation_status = installation_status)
    # set encoding as this is usually called from asynchronous JavaScript call
    self.REQUEST.RESPONSE.setHeader('Content-Type',
        'text/html; charset=utf-8')
    return html

  security.declareProtected(Permissions.ModifyPortalContent, 'startInstallation')
  def startInstallation(self, business_configuration, REQUEST):
    """ Start installation process as an activity which will
        download/install bt5 template files and meanwhile offer
        user a nice GUI to observe what's happening. """

    # init installation status
    installation_status['bt5']['all'] = 1
    installation_status['bt5']['current'] = 0
    installation_status['activity_list'] = []
    active_process = self.portal_activities.newActiveProcess()
    REQUEST.set('active_process_id', active_process.getId())
    business_configuration.activate(
           active_process=active_process, tag='initialERP5Setup'
        ).build()
    return self.ConfiguratorTool_viewInstallationStatus(REQUEST)
Пример #14
0
  def _migrateSimulationTree(self, get_matching_key,
                             get_original_property_dict, root_rule=None):
    """Migrate an entire simulation tree in order to use new rules

    This must be called on a root applied rule, with interaction workflows
    disabled. It is required that:
    - All related simulation trees are properly indexed (due to use of
      isSimulated). Unfortunately, this method temporarily unindexes everything,
      so you have to be careful when migrating several trees at once.
    - All simulation trees it may depend on are already migrated. It is adviced
      to first migrate all root applied rule for the first phase (usually
      order) and to continue respecting the order of phases.

    def get_matching_key(simulation_movement):
      # Return arbitrary value to match old and new simulation movements,
      # or null if the old simulation movement is dropped.

    def get_original_property_dict(tester, old_sm, sm, movement):
      # Return values to override on the new simulation movement.
      # In most cases, it would return the result of:
      #   tester.getUpdatablePropertyDict(old_sm, movement)

    root_rule # If not null and tree is about to be regenerated, 'specialise'
              # is changed on self to this relative url.
    """
    assert WorkflowMethod.disabled(), \
      "Interaction workflows must be disabled using WorkflowMethod.disable"
    simulation_tool = self.getParentValue()
    assert simulation_tool.getPortalType() == 'Simulation Tool'
    portal = simulation_tool.getPortalObject()
    delivery = self.getCausalityValue()
    # Check the whole history to not drop simulation in case of redraft
    draft_state_list = portal.getPortalDraftOrderStateList()
    workflow, = [wf for wf in portal.portal_workflow.getWorkflowsFor(delivery)
                    if wf.isInfoSupported(delivery, 'simulation_state')]
    for history_item in workflow.getInfoFor(delivery, 'history', ()):
      if history_item['simulation_state'] in draft_state_list:
        continue
      # Delivery is/was not is draft state
      resolveCategory = portal.portal_categories.resolveCategory
      order_dict = {}
      old_dict = {}
      # Caller may want to drop duplicate SM, like a unbuilt SM if there's
      # already a built one, or one with no quantity. So first call
      # 'get_matching_key' on SM that would be kept. 'get_matching_key' would
      # remember them and returns None for duplicates.
      sort_sm = lambda x: (not x.getDelivery(), not x.getQuantity(), x.getId())
      for sm in sorted(self.objectValues(), key=sort_sm):
        line = sm.getOrder() or sm.getDelivery()
        # Check SM is not orphan, which happened with old buggy trees.
        if resolveCategory(line) is not None:
          sm_dict = old_dict.setdefault(line, {})
          recurse_list = deque(({get_matching_key(sm): (sm,)},))
          while recurse_list:
            for k, x in recurse_list.popleft().iteritems():
              if not k:
                continue
              if len(x) > 1:
                x = [x for x in x if x.getDelivery() or x.getQuantity()]
                if len(x) > 1:
                  x.sort(key=sort_sm)
              sm_dict.setdefault(k, []).extend(x)
              for x in x:
                r = {}
                for x in x.objectValues():
                  sm_list = x.getMovementList()
                  if sm_list:
                    r.setdefault(x.getSpecialise(), []).append(sm_list)
                for x in r.values():
                  if len(x) > 1:
                    x = [y for y in x if any(z.getDelivery()
                           for z in y)] or x[:1]
                  x, = x
                  r = {}
                  for x in x:
                    r.setdefault(get_matching_key(x), []).append(x)
                  recurse_list.append(r)
        self._delObject(sm.getId())
      # Here Delivery.isSimulated works because Movement.isSimulated
      # does not see the simulated movements we've just deleted.
      if delivery.isSimulated():
        break
      # XXX: delivery.isSimulated may wrongly return False when a duplicate RAR
      #      was migrated but has not been reindexed yet. Delay migration of
      #      this one.
      rar_list = delivery.getCausalityRelatedValueList(
        portal_type='Applied Rule')
      rar_list.remove(self)
      if rar_list and portal.portal_activities.countMessage(
        path=[x.getPath() for x in rar_list],
        method_id=('immediateReindexObject',
                   'recursiveImmediateReindexObject',
                   'recursiveImmediateReindexSimulationMovement')):
        raise ConflictError
      # Do not try to keep simulation tree for draft delivery
      # if it was already out of sync.
      if delivery.getSimulationState() in draft_state_list and \
         any(x.getRelativeUrl() not in old_dict
             for x in delivery.getMovementList()):
        break
      if root_rule:
        self._setSpecialise(root_rule)
      delivery_set = {delivery}
      def updateMovementCollection(rule, context, *args, **kw):
        orig_updateMovementCollection(rule, context, *args, **kw)
        new_parent = context.getParentValue()
        for sm in context.getMovementList():
          delivery = sm.getDelivery()
          if delivery:
            sm_dict = old_dict.pop(delivery)
          else:
            sm_dict = order_dict[new_parent]
          order_dict[sm] = sm_dict
          k = get_matching_key(sm)
          sm_list = sm_dict.pop(k, ())
          if len(sm_list) > 1:
            # Heuristic to find matching old simulation movements for the
            # currently expanded applied rule. We first try to preserve same
            # tree structure (new & old parent SM match), then we look for an
            # old possible parent that is in the same branch.
            try:
              old_parent = old_dict[new_parent]
            except KeyError:
              old_parent = simulation_tool
            best_dict = {}
            for old_sm in sm_list:
              parent = old_sm.getParentValue().getParentValue()
              if parent is old_parent:
                parent = None
              elif not (parent.aq_inContextOf(old_parent) or
                        old_parent.aq_inContextOf(parent)):
                continue
              best_dict.setdefault(parent, []).append(old_sm)
            try:
              best_sm_list = best_dict[None]
            except KeyError:
              best_sm_list, = best_dict.values()
            if len(best_sm_list) < len(sm_list):
              sm_dict[k] = list(set(sm_list).difference(best_sm_list))
            sm_list = best_sm_list
            if len(sm_list) > 1:
              kw = sm.__dict__.copy()
          # We may have several old matching SM, e.g. in case of split.
          for old_sm in sm_list:
            movement = old_sm.getDeliveryValue()
            if sm is None:
              sm = context.newContent(portal_type=rule.movement_type)
              sm.__dict__ = dict(kw, **sm.__dict__)
              order_dict[sm] = sm_dict
            if delivery:
              assert movement.getRelativeUrl() == delivery
            elif movement is not None:
              sm._setDeliveryValue(movement)
              delivery_set.add(sm.getExplanationValue())
            try:
              sm.delivery_ratio = old_sm.aq_base.delivery_ratio
            except AttributeError:
              pass
            recorded_property_dict = {}
            edit_kw = {}
            kw['quantity'] = 0
            for tester in rule._getUpdatingTesterList():
              old = get_original_property_dict(tester, old_sm, sm, movement)
              if old is not None:
                new = tester.getUpdatablePropertyDict(sm, movement)
                if old != new:
                  edit_kw.update(old)
                  if 'quantity' in new and old_sm is not sm_list[-1]:
                    quantity = new.pop('quantity')
                    kw['quantity'] = quantity - old.pop('quantity')
                    if new != old or sm.quantity != quantity:
                      raise NotImplementedError # quantity_unit/efficiency ?
                  else:
                    recorded_property_dict.update(new)
            if recorded_property_dict:
              sm._recorded_property_dict = PersistentMapping(
                recorded_property_dict)
            sm._edit(**edit_kw)
            old_dict[sm] = old_sm
            sm = None
      deleted = old_dict.items()
      for delivery, sm_dict in deleted:
        if not sm_dict:
          del old_dict[delivery]
      from Products.ERP5.Document.SimulationMovement import SimulationMovement
      from Products.ERP5.mixin.movement_collection_updater import \
          MovementCollectionUpdaterMixin as mixin
      # Patch is already protected by WorkflowMethod.disable lock.
      orig_updateMovementCollection = mixin.__dict__['updateMovementCollection']
      try:
        AppliedRule.isIndexable = SimulationMovement.isIndexable = \
          ConstantGetter('isIndexable', value=False)
        mixin.updateMovementCollection = updateMovementCollection
        self.expand("immediate")
      finally:
        mixin.updateMovementCollection = orig_updateMovementCollection
        del AppliedRule.isIndexable, SimulationMovement.isIndexable
      self.recursiveReindexObject()
      assert str not in map(type, old_dict), old_dict
      return {k: sum(v.values(), []) for k, v in deleted}, delivery_set
    simulation_tool._delObject(self.getId())
Пример #15
0
class BaseCategory(Category):
    """
      Base Categories allow to implement virtual categories
      through acquisition
    """
    meta_type = 'CMF Base Category'
    portal_type = 'Base Category'  # maybe useful some day
    isBaseCategory = ConstantGetter('isBaseCategory', value=True)

    constructors = (manage_addBaseCategoryForm, addBaseCategory)

    property_sheets = (PropertySheet.Base, PropertySheet.SimpleItem,
                       PropertySheet.BaseCategory)

    # Declarative security
    security = ClassSecurityInfo()
    security.declareObjectProtected(Permissions.AccessContentsInformation)

    # BBB: Required to start instance with old
    #      version of erp5_property_sheets BT.
    related_locally_indexed = False
    security.declarePrivate('isRelatedLocallyIndexed')

    def isRelatedLocallyIndexed(self):
        """Determines if related values should be indexed on target documents"""
        return self.related_locally_indexed

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getBaseCategoryId')

    def getBaseCategoryId(self):
        """
        The base category of this object
        acquired through portal categories. Very
        useful to implement relations and virtual categories.
      """
        return self.getBaseCategory().id

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getBaseCategoryUid')

    def getBaseCategoryUid(self):
        """
        The base category uid of this object
        acquired through portal categories. Very
        useful to implement relations and virtual categories.
      """
        return self.getBaseCategory().getUid()

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getBaseCategoryValue')

    def getBaseCategoryValue(self):
        """
        The base category of this object
        acquired through portal categories. Very
        useful to implement relations and virtual categories.
      """
        return self

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getCategoryChildValueList')

    def getCategoryChildValueList(self,
                                  is_self_excluded=1,
                                  recursive=1,
                                  include_if_child=1,
                                  sort_on=None,
                                  sort_order=None,
                                  local_sort_method=None,
                                  local_sort_id=None,
                                  checked_permission=None,
                                  **kw):
        """
          List the child objects of this category and all its subcategories.

          recursive - if set to 1, list recursively

          include_if_child - if set to 1, then a category is listed even if
                      has childs. if set to 0, then don't list if child.
                      for example:
                        region/europe
                        region/europe/france
                        region/europe/germany
                        ...
                      becomes:
                        region/europe/france
                        region/europe/germany
                        ...
          sort_on, sort_order - sort categories in 'sort_order' by comparing
                  the 'sort_on' attribute. The default is to do a preorder tree
                  traversal on all subobjects.

          local_sort_method - When using the default preorder traversal, use
                              this function to sort objects of the same depth.

          local_sort_id     - When using the default preorder traversal, sort
                              objects of the same depth by comparing their
                              'local_sort_id' property. local_sort_id can be a
                              list, in this case properties are compared in the
                              same order than this list.

          Renderer parameters are also supported here.

      """
        if is_self_excluded:
            value_list = []
        else:
            value_list = [self]

        child_value_list = self.objectValues(self.allowed_types)
        if local_sort_id:
            if isinstance(local_sort_id, (tuple, list)):

                def sort_method(a, b):
                    for sort_id in local_sort_id:
                        diff = cmp(a.getProperty(sort_id, 0),
                                   b.getProperty(sort_id, 0))
                        if diff != 0:
                            return diff
                    return 0

                local_sort_method = sort_method
            else:
                local_sort_method = lambda a, b: cmp(
                    a.getProperty(local_sort_id, 0),
                    b.getProperty(local_sort_id, 0))
        if local_sort_method:
            # sort objects at the current level
            child_value_list = list(child_value_list)
            child_value_list.sort(local_sort_method)

        if recursive:
            for c in child_value_list:
                value_list.extend(
                    c.getCategoryChildValueList(
                        recursive=1,
                        is_self_excluded=0,
                        include_if_child=include_if_child,
                        local_sort_id=local_sort_id,
                        local_sort_method=local_sort_method))
        else:
            for c in child_value_list:
                if include_if_child:
                    value_list.append(c)
                else:
                    if len(c.objectIds(self.allowed_types)) == 0:
                        value_list.append(c)

        if checked_permission is not None:
            checkPermission = self.portal_membership.checkPermission

            def permissionFilter(obj):
                return checkPermission(checked_permission, obj)

            value_list = filter(permissionFilter, value_list)

        return sortValueList(value_list, sort_on, sort_order, **kw)

    # Alias for compatibility
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getBaseCategory')
    getBaseCategory = getBaseCategoryValue

    # Hardcode these getters as they are used when generating
    # accessors
    getReadPermission = BaseGetter('getReadPermission', 'read_permission',
                                   'string')

    getWritePermission = BaseGetter('getWritePermission', 'write_permission',
                                    'string')
Пример #16
0
class Document(DocumentExtensibleTraversableMixin, XMLObject, UrlMixin,
               CachedConvertableMixin, CrawlableMixin, TextConvertableMixin,
               DownloadableMixin, DocumentMixin, DiscoverableMixin):
    """Document is an abstract class with all methods related to document
  management in ERP5. This includes searchable text, explicit relations,
  implicit relations, metadata, versions, languages, etc.

  Documents may either store their content directly or cache content
  which is retrieved from a specified URL. The second case if often
  referred as "External Document". Standalone "External Documents" may
  be created by specifying a URL to the contribution tool which is in
  charge of initiating the download process and selecting the appropriate
  document type. Groups of "External Documents" may also be generated from
  so-called "External Source" (refer to ExternalSource class for more
  information).

  External Documents may be downloaded once or updated at regular interval.
  The later can be useful to update the content of an external source.
  Previous versions may be stored in place or kept in a separate file.
  This feature is known as the crawling API. It is mostly implemented
  in ContributionTool with wrappers in the Document class. It can be useful
  for create a small search engine.

  There are currently two types of Document subclasses:

  * File for binary file based documents. File has subclasses such as Image,
    OOoDocument, PDFDocument, etc. to implement specific conversion methods.

  * TextDocument for text based documents. TextDocument has subclasses such
    as Wiki to implement specific methods.
    TextDocument itself has a subclass (XSLTDocument) which provides
    XSLT based analysis and transformation of XML content based on XSLT
    templates.

  Conversion should be achieved through the convert method and other methods
  of the conversion API (convertToBaseFormat, etc.).
  Moreover, any Document subclass must ne able to convert documents to text
  (asText method) and HTML (asHTML method). Text is required for full text
  indexing. HTML is required for crawling.

  Instances can be created directly, or via portal_contributions tool which
  manages document ingestion process whereby a file can be uploaded by http
  or sent in by email or dropped in by webdav or in some other way as yet
  unknown. The ingestion process has the following steps:

  (1) portal type detection
  (2) object creation and upload of data
  (3) metadata discovery (optionally with conversion of data to another format)
  (4) other possible actions to finalise the ingestion (ex. by assigning
      a reference)

  This class handles (3) and calls a ZMI script to do (4).

  Metadata can be drawn from various sources:

  input      -   data supplied with http request or set on the object during (2) (e.g.
                 discovered from email text)
  filename   -   data which might be encoded in filename
  user_login -   information about user who is contributing the file
  content    -   data which might be derived from document content

  If a certain property is defined in more than one source, it is set according to
  preference order returned by a script
     Document_getPreferredDocumentMetadataDiscoveryOrderList
     (or any type-based version since discovery is type dependent)

  Methods for discovering metadata are:

    getPropertyDictFromInput
    getPropertyDictFromFilename
    getPropertyDictFromUserLogin
    getPropertyDictFromContent

  Methods for processing content are implemented either in Document class
  or in Base class:

    getSearchableReferenceList (Base)
    getSearchableText (Base)
    index_html (overriden in Document subclasses)

  Methods for handling relations are implemented either in Document class
  or in Base class:

    getImplicitSuccessorValueList (Base)
    getImplicitPredecessorValueList (Base)
    getImplicitSimilarValueList (Base)
    getSimilarCloudValueList (Document)

  Implicit relations consist in finding document references inside
  searchable text (ex. INV-23456) and deducting relations from that.
  Two customisable methods required. One to find a list of implicit references
  inside the content (getSearchableReferenceList) and one to convert a given
  document reference into a list of reference strings which could be present
  in other content (asSearchableReferenceList).

  document.getSearchableReferenceList() returns
    [
     {'reference':' INV-12367'},
     {'reference': 'INV-1112', 'version':'012}',
     {'reference': 'AB-CC-DRK', 'version':'011', 'language': 'en'}
    ]

  The Document class behaviour can be extended / customized through scripts
  (which are type-based so can be adjusted per portal type).

  * Document_getPropertyDictFromUserLogin - finds a user (by user_login or
    from session) and returns properties which should be set on the document

  * Document_getPropertyDictFromContent - analyzes document content and returns
    properties which should be set on the document

  * Base_getImplicitSuccessorValueList - finds appropriate all documents
    referenced in the current content

  * Base_getImplicitPredecessorValueList - finds document predecessors based on
    the document coordinates (can use only complete coordinates, or also partial)

  * Document_getPreferredDocumentMetadataDiscoveryOrderList - returns an order
    in which metadata should be set/overwritten

  * Document_finishIngestion - called by portal_activities after all the ingestion
    is completed (and after document has been converted, so text_content
    is available if the document has it)

  * Document_getNewRevision - calculates revision number which should be set
    on this document. Implementation depends on revision numbering policy which
    can be very different. Interaction workflow should call setNewRevision method.

  * Document_populateContent - analyses the document content and produces
    subcontent based on it (ex. images, news, etc.). This scripts can
    involve for example an XSLT transformation to process XML.

  Subcontent: documents may include subcontent (files, images, etc.)
  so that publication of rich content can be path independent. Subcontent
  can also be used to help the rendering in HTML of complex documents
  such as ODF documents.

  Consistency checking:
    Default implementation uses DocumentReferenceConstraint to check if the
    reference/language/version triplet is unique. Additional constraints
    can be added if necessary.

  NOTE: Document.py supports a notion of revision which is very specific.
  The underlying concept is that, as soon as a document has a reference,
  the association of (reference, version, language) must be unique accross
  the whole system. This means that a given document in a given version in a
  given language is unique. The underlying idea is similar to the one in a Wiki
  system in which each page is unique and acts the the atom of collaboration.
  In the case of ERP5, if a team collaborates on a Text document written with
  an offline word processor, all updates should be placed inside the same object.
  A Contribution will thus modify an existing document, if allowed from security
  point of view, and increase the revision number. Same goes for properties
  (title). Each change generates a new revision.

    conversion API - not same as document - XXX BAD
    XXX make multiple interfaces

  TODO:
    - move all implementation bits to MixIn classes
    - in the end, Document class should have zero code
      and only serve as a quick and easy way to create
      new types of documents (one could even consider
      that this class should be trashed)
    -
  """

    meta_type = 'ERP5 Document'
    portal_type = 'Document'
    add_permission = Permissions.AddPortalContent
    isDocument = ConstantGetter('isDocument', value=True)
    __dav_collection__ = 0

    zope.interface.implements(
        IConvertable,
        ITextConvertable,
        IHtmlConvertable,
        ICachedConvertable,
        IVersionable,
        IDownloadable,
        ICrawlable,
        IDocument,
        IDiscoverable,
        IUrl,
    )

    # Regular expressions
    # XXX those regex are weak, fast but not reliable.
    # this is a valid url than regex are not able to parse
    # http://www.example.com//I don't care i put what/ i want/
    href_parser = re.compile(r'<a[^>]*href=[\'"](.*?)[\'"]', re.IGNORECASE)
    body_parser = re.compile(r'<body[^>]*>(.*?)</body>',
                             re.IGNORECASE + re.DOTALL)
    title_parser = re.compile(r'<title[^>]*>(.*?)</title>',
                              re.IGNORECASE + re.DOTALL)
    base_parser = re.compile(r'<base[^>]*href=[\'"](.*?)[\'"][^>]*>',
                             re.IGNORECASE + re.DOTALL)
    charset_parser = re.compile(
        r'(?P<keyword>charset="?)(?P<charset>[a-z0-9\-]+)', re.IGNORECASE)

    # Declarative security
    security = ClassSecurityInfo()
    security.declareObjectProtected(Permissions.AccessContentsInformation)

    # Declarative properties
    property_sheets = (PropertySheet.Base, PropertySheet.XMLObject,
                       PropertySheet.CategoryCore, PropertySheet.DublinCore,
                       PropertySheet.Version, PropertySheet.Reference,
                       PropertySheet.Document, PropertySheet.ExternalDocument,
                       PropertySheet.Url, PropertySheet.Periodicity)

    index_html = DownloadableMixin.index_html

    security.declareProtected(Permissions.AccessContentsInformation,
                              'isExternalDocument')

    def isExternalDocument(self):
        """
    Return true if this document was obtained from an external source
    """
        return bool(self.getUrlString())

    ### Relation getters
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getSearchableReferenceList')

    def getSearchableReferenceList(self):
        """
      This method returns a list of dictionaries which can
      be used to find objects by reference. It uses for
      that a regular expression defined at system level
      preferences.
    """
        text = self.getSearchableText()  # XXX getSearchableText or asText ?
        return self._getSearchableReferenceList(text)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'isSearchableReference')

    def isSearchableReference(self):
        """
      Determine if current document's reference can be used for searching - i.e. follows
      certain defined at system level preferences format.
    """
        reference = self.getReference()
        if reference is not None:
            return len(self._getSearchableReferenceList(reference))
        return False

    def _getSearchableReferenceList(self, text):
        """
      Extract all reference alike strings from text using for that a
      regular expression defined at system level preferences.
    """
        regexp = self.portal_preferences.getPreferredDocumentReferenceRegularExpression(
        )
        try:
            rx_search = re.compile(regexp)
        except TypeError:  # no regexp in preference
            LOG(
                'ERP5/Document/Document.getSearchableReferenceList', 0,
                'Document regular expression must be set in portal preferences'
            )
            return ()
        result = []
        tmp = {}
        for match in rx_search.finditer(text):
            group = match.group()
            group_item_list = match.groupdict().items()
            group_item_list.sort()
            key = (group, tuple(group_item_list))
            tmp[key] = None
        for group, group_item_tuple in tmp.keys():
            result.append((group, dict(group_item_tuple)))
        return result

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getImplicitSuccessorValueList')

    def getImplicitSuccessorValueList(self):
        """
      Find objects which we are referencing (if our text_content contains
      references of other documents). The whole implementation is delegated to
      Base_getImplicitSuccessorValueList script.

      The implementation goes in 2 steps:

    - Step 1: extract with a regular expression
      a list of distionaries with various parameters such as
      reference, portal_type, language, version, user, etc. This
      part is configured through a portal preference.

    - Step 2: read the list of dictionaries
      and build a list of values by calling portal_catalog
      with appropriate parameters (and if possible build
      a complex query whenever this becomes available in
      portal catalog)

      The script is reponsible for calling getSearchableReferenceList
      so that it can use another approach if needed.

      NOTE: passing a group_by parameter may be useful at a
      later stage of the implementation.
    """
        tv = getTransactionalVariable()  # XXX Performance improvement required
        cache_key = ('getImplicitSuccessorValueList', self.getPhysicalPath())
        try:
            return tv[cache_key]
        except KeyError:
            pass

        reference_list = [r[1] for r in self.getSearchableReferenceList()]
        result = self._getTypeBasedMethod('getImplicitSuccessorValueList')(
            reference_list)
        tv[cache_key] = result
        return result

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getImplicitPredecessorValueList')

    def getImplicitPredecessorValueList(self):
        """
      This function tries to find document which are referencing us - by reference only, or
      by reference/language etc. Implementation is passed to
        Base_getImplicitPredecessorValueList

      The script should proceed in two steps:

      Step 1: build a list of references out of the context
      (ex. INV-123456, 123456, etc.)

      Step 2: search using the portal_catalog and use
      priorities (ex. INV-123456 before 123456)
      ( if possible build  a complex query whenever
      this becomes available in portal catalog )

      NOTE: passing a group_by parameter may be useful at a
      later stage of the implementation.
    """
        return self._getTypeBasedMethod('getImplicitPredecessorValueList')()

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getImplicitSimilarValueList')

    def getImplicitSimilarValueList(self):
        """
      Analyses content of documents to find out by the content which documents
      are similar. Not implemented yet.

      No cloud needed because transitive process
    """
        return []

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getSimilarCloudValueList')

    def getSimilarCloudValueList(self, depth=0):
        """
      Returns all documents which are similar to us, directly or indirectly, and
      in both directions. In other words, it is a transitive closure of similar
      relation. Every document is returned in the latest version available.
    """
        lista = {}
        depth = int(depth)

        #gettername = 'get%sValueList' % convertToUpperCase(category)
        #relatedgettername = 'get%sRelatedValueList' % convertToUpperCase(category)

        def getRelatedList(ob, level=0):
            level += 1
            #getter = getattr(self, gettername)
            #relatedgetter = getattr(self, relatedgettername)
            #res = getter() + relatedgetter()
            res = ob.getSimilarValueList() + ob.getSimilarRelatedValueList()
            for r in res:
                if lista.get(r) is None:
                    lista[r] = True  # we use dict keys to ensure uniqueness
                    if level != depth:
                        getRelatedList(r, level)

        getRelatedList(self)
        lista_latest = {}
        for o in lista.keys():
            lista_latest[o.getLatestVersionValue(
            )] = True  # get latest versions avoiding duplicates again

        # remove this document
        lista_latest.pop(self, None)
        # remove last version of document itself from related documents
        lista_latest.pop(self.getLatestVersionValue(), None)

        return lista_latest.keys()

    ### Version and language getters - might be moved one day to a mixin class in base
    security.declareProtected(Permissions.View, 'getLatestVersionValue')

    def getLatestVersionValue(self, language=None):
        """
      Tries to find the latest version with the latest revision
      of self which the current user is allowed to access.

      If language is provided, return the latest document
      in the language.

      If language is not provided, return the latest version
      in original language or in the user language if the version is
      the same.
    """
        if not self.getReference():
            return self
        catalog = self.getPortalObject().portal_catalog
        kw = dict(reference=self.getReference(),
                  sort_on=(('version', 'descending', 'SIGNED'), ))
        if language is not None:
            kw['language'] = language
        result_list = catalog(**kw)

        original_language = self.getOriginalLanguage()
        user_language = self.Localizer.get_selected_language()

        # if language was given return it (if there are any docs in this language)
        if language is not None:
            try:
                return result_list[0].getObject()
            except IndexError:
                return None
        elif result_list:
            first = result_list[0].getObject()
            in_original = None
            for record in result_list:
                document = record.getObject()
                if document.getVersion() != first.getVersion():
                    # we are out of the latest version - return in_original or first
                    if in_original is not None:
                        return in_original
                    else:
                        return first  # this shouldn't happen in real life
                if document.getLanguage() == user_language:
                    # we found it in the user language
                    return document
                if document.getLanguage() == original_language:
                    # this is in original language
                    in_original = document
        # this is the only doc in this version
        return self

    security.declareProtected(Permissions.View, 'getVersionValueList')

    def getVersionValueList(self, version=None, language=None):
        """
      Returns a list of documents with same reference, same portal_type
      but different version and given language or any language if not given.
    """
        catalog = self.getPortalObject().portal_catalog
        kw = dict(portal_type=self.getPortalType(),
                  reference=self.getReference(),
                  sort_on=(('version', 'descending', 'SIGNED'), ))
        if version:
            kw['version'] = version
        if language:
            kw['language'] = language
        return catalog(**kw)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'isVersionUnique')

    def isVersionUnique(self):
        """
      Returns true if no other document exists with the same
      reference, version and language, or if the current
      document has no reference.
    """
        if not self.getReference():
            return True
        kw = dict(portal_type=self.getPortalDocumentTypeList(),
                  reference=self.getReference(),
                  version=self.getVersion(),
                  language=self.getLanguage(),
                  query=NegatedQuery(
                      Query(validation_state=('cancelled', 'deleted'))))
        catalog = self.getPortalObject().portal_catalog
        self_count = catalog.unrestrictedCountResults(uid=self.getUid(),
                                                      **kw)[0][0]
        count = catalog.unrestrictedCountResults(**kw)[0][0]
        # If self is not indexed yet, then if count == 1, version is not unique
        return count <= self_count

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getRevision')

    def getRevision(self):
        """
      Returns the current revision by analysing the change log
      of the current object. The return value is a string
      in order to be consistent with the property sheet
      definition.
    """
        getInfoFor = self.getPortalObject().portal_workflow.getInfoFor
        revision = len(getInfoFor(self, 'history', (), 'edit_workflow'))
        # XXX Also look at processing_status_workflow for compatibility.
        revision += len([history_item for history_item in\
                     getInfoFor(self, 'history', (), 'processing_status_workflow')\
                     if history_item.get('action') == 'edit'])
        return str(revision)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getRevisionList')

    def getRevisionList(self):
        """
      Returns the list of revision numbers of the current document
      by by analysing the change log of the current object.
    """
        return map(str, range(1, 1 + int(self.getRevision())))

    security.declareProtected(Permissions.ModifyPortalContent, 'mergeRevision')

    def mergeRevision(self):
        """
      Merge the current document with any previous revision
      or change its version to make sure it is still unique.

      NOTE: revision support is implemented in the Document
      class rather than within the ContributionTool
      because the ingestion process requires to analyse the content
      of the document first. Hence, it is not possible to
      do any kind of update operation until the whole ingestion
      process is completed, since update requires to know
      reference, version, language, etc. In addition,
      we have chosen to try to merge revisions after each
      metadata discovery as a way to make sure that any
      content added in the system through the ContributionTool
      (ex. through webdav) will be merged if necessary.
      It may be posssible though to split disoverMetadata and
      finishIngestion.
    """
        document = self
        if self.getReference():
            invalid_validation_state_list = ('archived', 'cancelled',
                                             'deleted')
            catalog = self.getPortalObject().portal_catalog
            # Find all document with same (reference, version, language)
            kw = dict(
                portal_type=self.getPortalType(),
                reference=self.getReference(),
                query=NegatedQuery(
                    Query(validation_state=invalid_validation_state_list)),
                sort_on='creation_date')

            if self.getVersion():
                kw['version'] = self.getVersion()
            if self.getLanguage():
                kw['language'] = self.getLanguage()
            document_list = catalog.unrestrictedSearchResults(**kw)
            existing_document = None
            # Select the first one which is not self and which
            # shares the same coordinates
            for o in document_list:
                if o.getRelativeUrl() != self.getRelativeUrl() and\
                   o.getVersion() == self.getVersion() and\
                   o.getLanguage() == self.getLanguage():
                    existing_document = o.getObject()
                    if existing_document.getValidationState() not in \
                      invalid_validation_state_list:
                        break
            else:
                existing_document = None

            # We found an existing document to update
            if existing_document is not None:
                document = existing_document
                if not _checkPermission(Permissions.ModifyPortalContent,
                                        existing_document):
                    raise Unauthorized(
                        "[DMS] You are not allowed to update the existing document which has the same coordinates (id %s)"
                        % existing_document.getId())
                else:
                    update_kw = {}
                    for k in self.propertyIds():
                        if k not in FIXED_PROPERTY_IDS and self.hasProperty(k):
                            update_kw[k] = self.getProperty(k)
                    existing_document.edit(**update_kw)
                    # Erase self
                    self.getParentValue().manage_delObjects([
                        self.getId(),
                    ])
        return document

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getLanguageList')

    def getLanguageList(self, version=None):
        """
      Returns a list of languages which this document is available in
      for the current user.
    """
        if not self.getReference(): return []
        catalog = self.getPortalObject().portal_catalog
        kw = dict(portal_type=self.getPortalType(),
                  reference=self.getReference(),
                  group_by=('language', ))
        if version is not None:
            kw['version'] = version
        return [o.getLanguage() for o in catalog(**kw)]

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getOriginalLanguage')

    def getOriginalLanguage(self):
        """
      Returns the original language of this document.

      XXX-JPS not implemented yet ?
    """
        # Approach 1: use portal_catalog and creation dates
        # Approach 2: use workflow analysis (delegate to script if necessary)
        #             workflow analysis is the only way for multiple orginals
        # XXX - cache or set?
        reference = self.getReference()
        if not reference:
            return
        catalog = self.getPortalObject().portal_catalog
        result_list = catalog.unrestrictedSearchResults(
            reference=self.getReference(),
            sort_on=(('creation_date', 'ascending'), ))
        if result_list:
            return result_list[0].getLanguage()
        return

    security.declareProtected(Permissions.View, 'asSubjectText')

    def asSubjectText(self, **kw):
        """
      Converts the subject of the document to a textual representation.
    """
        subject = self.getSubject('')
        if not subject:
            # XXX not sure if this fallback is a good idea.
            subject = self.getTitle('')
        return str(subject)

    security.declareProtected(Permissions.View, 'asEntireHTML')

    def asEntireHTML(self, **kw):
        """
      Returns a complete HTML representation of the document
      (with body tags, etc.). Adds if necessary a base
      tag so that the document can be displayed in an iframe
      or standalone.

      Actual conversion is delegated to _asHTML
    """
        html = self._asHTML(**kw)
        if self.getUrlString():
            # If a URL is defined, add the base tag
            # if base is defined yet.
            html = str(html)
            if not html.find('<base') >= 0:
                base = '<base href="%s"/>' % self.getContentBaseURL()
                html = html.replace('<head>', '<head>%s' % base, 1)
            self.setConversion(html, mime='text/html', format='base-html')
        return html

    security.declarePrivate('_asHTML')

    def _asHTML(self, **kw):
        """
      A private method which converts to HTML. This method
      is the one to override in subclasses.
    """
        kw['format'] = 'html'
        _, html = self.convert(**kw)
        return html

    security.declareProtected(Permissions.View, 'asStrippedHTML')

    def asStrippedHTML(self, **kw):
        """
      Returns a stripped HTML representation of the document
      (without html and body tags, etc.) which can be used to inline
      a preview of the document.
    """
        return self._stripHTML(self._asHTML(**kw))

    security.declarePrivate('_guessEncoding')

    @deprecated
    def _guessEncoding(self, string, mime='text/html'):
        """
      Deprecated method
    """
        return guessEncodingFromText(string, content_type=mime)

    def _stripHTML(self, html, charset=None):
        """
      A private method which can be reused by subclasses
      to strip HTML content
    """
        body_list = re.findall(self.body_parser, str(html))
        if len(body_list):
            stripped_html = body_list[0]
        else:
            stripped_html = html
        return stripped_html

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getMetadataMappingDict')

    def getMetadataMappingDict(self):
        """
    Return a dict of metadata mapping used to update base metadata of the
    document
    """
        try:
            method = self._getTypeBasedMethod('getMetadataMappingDict')
        except (KeyError, AttributeError):
            method = None
        if method is not None:
            return method()
        else:
            return {}

    # Transformation API
    security.declareProtected(Permissions.ModifyPortalContent,
                              'populateContent')

    def populateContent(self):
        """
      Populates the Document with subcontent based on the
      document base data.

      This can be used for example to transform the XML
      of an RSS feed into a single piece per news or
      to transform an XML export from a database into
      individual records. Other application: populate
      an HTML text document with its images, used in
      conversion with convertToBaseFormat.
    """
        try:
            method = self._getTypeBasedMethod('populateContent')
        except (KeyError, AttributeError):
            method = None
        if method is not None: method()

    security.declareProtected(Permissions.ModifyPortalContent,
                              'updateContentFromURL')

    def updateContentFromURL(self,
                             repeat=MAX_REPEAT,
                             crawling_depth=0,
                             repeat_interval=1,
                             batch_mode=True):
        """
      Download and update content of this document from its source URL.
      Implementation is handled by ContributionTool.
    """
        self.portal_contributions.updateContentFromURL(
            self, repeat=repeat, crawling_depth=crawling_depth)

    security.declareProtected(Permissions.ModifyPortalContent, 'crawlContent')

    def crawlContent(self):
        """
      Initialises the crawling process on the current document.
    """
        self.portal_contributions.crawlContent(self)

    security.declareProtected(Permissions.View, 'isIndexContent')

    def isIndexContent(self, container=None):
        """
      Ask container if we are and index, or a content.
      In the vast majority of cases we are content.
      This method is required in a crawling process to make
      a difference between URLs which return an index (ex. the
      list of files in remote server which is accessed through HTTP)
      and the files themselves.
    """
        if container is None:
            container = self.getParentValue()
        if hasattr(aq_base(container), 'isIndexContent'):
            return container.isIndexContent(self)
        return False
Пример #17
0
class ComponentMixin(PropertyRecordableMixin, Base):
  """
  Mixin used for all ZODB Components. Most of the code is generic, thus actual
  ZODB Components should have almost nothing to defined...

  From a security point of view, only Developer Role defined on Component Tool
  can manage Components (as exec is used and anything potentially damaging
  could be done on the filesystem), while only Manager or Developer Roles can
  reset Component Packages (see ERP5Type.Permissions). All the permissions are
  defined on Component Tool itself and newly created Components just inherits
  permissions defined on the former.

  The Developer Role is not a typical Role as only users defined in Zope
  configuration can be added to this Role (which is displayed in the list of
  available Roles in ZMI). This is achieved by two monkey patches
  (ERP5Type.patches.{User,PropertiedUser}) and modifications in
  ERP5Security.ERP5UserFactory.

  Component source code is checked upon modification of text_content property
  whatever its Workflow state (checkSourceCode). On validated and modified
  state, checkConsistency() is called to check id, reference, version and
  errors/warnings messages (set when the Component is modified).
  """
  __metaclass__ = RecordablePropertyMetaClass

  isPortalContent = 1
  isRADContent = 1
  isDelivery = ConstantGetter('isDelivery', value=True)

  # Declarative security
  security = ClassSecurityInfo()
  security.declareObjectProtected(Permissions.AccessContentsInformation)

  # Declarative properties
  property_sheets = ('Base',
                     'XMLObject',
                     'CategoryCore',
                     'DublinCore',
                     'Version',
                     'Reference',
                     'TextDocument',
                     'Component')

  _recorded_property_name_tuple = (
    'reference',
    'version',
    'text_content')

  _message_reference_not_set = "Reference must be set"
  _message_invalid_reference = "Reference cannot end with '_version' or "\
      "start with '_' or be equal to find_module, load_module or reset"

  _message_version_not_set = "Version must be set"
  _message_invalid_version = "Version cannot start with '_'"
  _message_duplicated_version_reference = "${id} is validated has the same "\
       "Reference and Version"

  _message_text_content_not_set = "No source code"
  _message_text_content_error = "Error in Source Code: ${error_message}"

  security.declareProtected(Permissions.ModifyPortalContent, 'checkConsistency')
  def checkConsistency(self, *args, **kw):
    """
    Check the consistency of the Component upon validate or when being
    modified after being validated.

    Some keywords are forbidden for reference and version. As Version package
    always ends with '_version', reference is checked more carefully to avoid
    clashing with existing method names (such as the ones required for PEP
    302).

    XXX-arnau: separate Constraint class?
    """
    error_list = super(ComponentMixin, self).checkConsistency(*args, **kw)
    object_relative_url = self.getRelativeUrl()
    reference = self.getReference()
    reference_has_error = False
    if not reference:
      reference_has_error = True
      error_list.append(
        ConsistencyMessage(self,
                           object_relative_url,
                           message=self._message_reference_not_set,
                           mapping={}))

    elif (reference.endswith('_version') or
          reference[0] == '_' or
          reference in ('find_module', 'load_module', 'reset')):
      reference_has_error = True
      error_list.append(
        ConsistencyMessage(self,
                           object_relative_url,
                           message=self._message_invalid_reference,
                           mapping={}))

    version = self.getVersion()
    if not version:
      error_list.append(ConsistencyMessage(self,
                                           object_relative_url,
                                           message=self._message_version_not_set,
                                           mapping={}))
    elif version[0] == '_':
      error_list.append(ConsistencyMessage(self,
                                           object_relative_url,
                                           message=self._message_invalid_version,
                                           mapping={}))
    else:
      package = __import__(self._getDynamicModuleNamespace(), globals(),
                           fromlist=[self._getDynamicModuleNamespace()], level=0)
      component_id = None
      component_uid = None
      from Products.ERP5Type.dynamic import aq_method_lock
      with aq_method_lock:
        component_id_uid_tuple = package._registry_dict.get(
          self.getReference(), {}).get(self.getVersion(), None)
        if component_id_uid_tuple:
          component_id, component_uid = component_id_uid_tuple

      if (component_id is not None and component_uid is not None and
          not reference_has_error and
          component_uid != self._p_oid and component_id != self.getId()):
        error_list.append(
          ConsistencyMessage(self,
                             object_relative_url,
                             message=self._message_duplicated_version_reference,
                             mapping=dict(id=component_id)))

    text_content = self.getTextContent()
    if not text_content:
      error_list.append(
          ConsistencyMessage(self,
                             object_relative_url=object_relative_url,
                             message=self._message_text_content_not_set,
                             mapping={}))
    else:
      for error_message in self.getTextContentErrorMessageList():
        error_list.append(ConsistencyMessage(self,
                                             object_relative_url=object_relative_url,
                                             message=self._message_text_content_error,
                                             mapping=dict(error_message=error_message)))

    return error_list

  security.declareProtected(Permissions.ModifyPortalContent,
                            'checkConsistencyAndValidate')
  def checkConsistencyAndValidate(self):
    """
    When a Component is in validated or modified validation state and it is
    modified, modified state is set then this checks whether the Component can
    be validated again if checkConsistency returns no error. Otherwise, it
    stays in modified state and previously validated values are used for
    reference, version and text_content
    """
    if not self.checkConsistency():
      for property_name in self._recorded_property_name_tuple:
        self.clearRecordedProperty(property_name)

      self.validate()

  security.declareProtected(Permissions.ModifyPortalContent, 'checkSourceCode')
  def checkSourceCode(self):
    """
    Check Component source code through Pylint or compile() builtin if not
    available
    """
    return checkPythonSourceCode(self.getTextContent())

  security.declareProtected(Permissions.ModifyPortalContent, 'PUT')
  def PUT(self, REQUEST, RESPONSE):
    """
    Handle HTTP PUT requests for FTP/Webdav upload, which is object
    dependent. For now only set the text content...
    """
    self.dav__init(REQUEST, RESPONSE)
    self.dav__simpleifhandler(REQUEST, RESPONSE, refresh=1)

    text_content = REQUEST.get('BODY')
    if text_content is None:
      RESPONSE.setStatus(304)
    else:
      self.setTextContent(text_content)
      RESPONSE.setStatus(204)

    return RESPONSE

  security.declareProtected(Permissions.ModifyPortalContent, 'manage_FTPput')
  manage_FTPput = PUT

  security.declareProtected(Permissions.AccessContentsInformation,
                            'manage_FTPget')
  def manage_FTPget(self):
    """
    Get source for FTP/Webdav. The default implementation of GET for Webdav,
    available in webdav.Resource, calls manage_FTPget

    XXX-arnau: encoding issue?
    """
    return self.getTextContent()

  security.declareProtected(Permissions.ModifyPortalContent,
                            'importFromFilesystem')
  @classmethod
  def importFromFilesystem(cls, context, reference, version,
                           erase_existing=False):
    """
    Import a Component from the filesystem into ZODB and validate it so it can
    be loaded straightaway provided validate() does not raise any error of
    course
    """
    object_id = '%s.%s.%s' % (cls._getIdPrefix(), version, reference)
    obj = context._getOb(object_id, None)
    if obj is not None:
      if not erase_existing:
        # Validate the object if it has not been validated yet
        if obj.getValidationState() not in ('modified', 'validated'):
          obj.validate()

        return obj

      context.deleteContent(object_id)

    import os.path
    path = os.path.join(cls._getFilesystemPath(), reference + '.py')
    with open(path) as f:
      source_code = f.read()

    # Checking that the source code is syntactically correct is not
    # needed when importing from filesystem, moreover errors may occur
    # if in the same transaction a Component is created and another
    # one depending upon the former...
    new_component = context.newContent(id=object_id,
                                       reference=reference,
                                       version=version,
                                       text_content=source_code,
                                       portal_type=cls.portal_type)

    # Validate the Component once it is imported so it can be used
    # straightaway as there should be no error
    new_component.validate()

    return new_component

  security.declareProtected(Permissions.ModifyPortalContent,
                            'getTextContentHistoryRevisionDictList')
  def getTextContentHistoryRevisionDictList(self, limit=100):
    """
    TODO
    """
    history_dict_list = self._p_jar.db().history(self._p_oid, size=limit)
    if history_dict_list is None:
      # Storage doesn't support history
      return ()

    from struct import unpack
    from OFS.History import historicalRevision

    previous_text_content = None
    result = []
    for history_dict in history_dict_list:
      text_content = historicalRevision(self, history_dict['tid']).getTextContent()
      if text_content and text_content != previous_text_content:
        history_dict['time'] = history_dict['time']
        history_dict['user_name'] = history_dict['user_name'].strip()
        history_dict['key'] = '.'.join(map(str, unpack(">HHHH", history_dict['tid'])))
        del history_dict['tid']
        del history_dict['size']

        result.append(history_dict)
        previous_text_content = text_content

    return result

  security.declareProtected(Permissions.ModifyPortalContent,
                            'getTextContentHistory')
  def getTextContentHistory(self, key):
    """
    TODO
    """
    from struct import pack
    from OFS.History import historicalRevision

    serial = apply(pack, ('>HHHH',) + tuple(map(int, key.split('.'))))
    rev = historicalRevision(self, serial)

    return rev.getTextContent()
Пример #18
0
    def backupObject(self, trashbin, container_path, object_id, save, **kw):
        """
      Backup an object in a trash bin
    """
        #     LOG('Trash : backup object', 0, str((container_path, object_id)))
        if save:
            # recreate path of the backup object if necessary
            backup_object_container = trashbin
            for path in container_path:
                if 'portal' in path:
                    path += '_items'
                if path not in backup_object_container.objectIds():
                    if not hasattr(aq_base(backup_object_container),
                                   "newContent"):
                        backup_object_container.manage_addFolder(id=path, )
                        backup_object_container = backup_object_container._getOb(
                            path)
                    else:
                        backup_object_container = backup_object_container.newContent(
                            portal_type='Trash Folder',
                            id=path,
                            is_indexable=0)
                        backup_object_container.edit(isHidden=1)
                else:
                    backup_object_container = backup_object_container._getOb(
                        path)
            # backup the object
            # here we choose export/import to copy because cut/paste
            # do too many things and check for what we want to do
            object_path = container_path + [object_id]
            obj = self.unrestrictedTraverse(object_path, None)
            if obj is not None:
                connection = obj._p_jar
                o = obj
                while connection is None:
                    o = o.aq_parent
                    connection = o._p_jar
                if obj._p_oid is None:
                    LOG("Trash Tool backupObject", WARNING,
                        "Trying to backup uncommitted object %s" % object_path)
                    return {}
                if isinstance(obj, Broken):
                    LOG("Trash Tool backupObject", WARNING,
                        "Can't backup broken object %s" % object_path)
                    klass = obj.__class__
                    if klass.__module__[:27] in ('Products.ERP5Type.Document.',
                                                 'erp5.portal_type'):
                        # meta_type is required so that a broken object
                        # can be removed properly from a BTreeFolder2
                        # (unfortunately, we can only guess it)
                        klass.meta_type = 'ERP5' + re.subn(
                            '(?=[A-Z])', ' ', klass.__name__)[0]
                    return {}
                copy = connection.exportFile(obj._p_oid)
                # import object in trash
                connection = backup_object_container._p_jar
                o = backup_object_container
                while connection is None:
                    o = o.aq_parent
                    connection = o._p_jar
                copy.seek(0)
                try:
                    backup = connection.importFile(copy)
                    backup.isIndexable = ConstantGetter('isIndexable',
                                                        value=False)
                    # the isIndexable setting above avoids the recursion of
                    # manage_afterAdd on
                    # Products.ERP5Type.CopySupport.CopySupport.manage_afterAdd()
                    # but not on event subscribers, so we need to suppress_events,
                    # otherwise subobjects will be reindexed
                    backup_object_container._setObject(object_id,
                                                       backup,
                                                       suppress_events=True)
                except (AttributeError, ImportError):
                    # XXX we can go here due to formulator because attribute
                    # field_added doesn't not exists on parent if it is a Trash
                    # Folder and not a Form, or a module for the old object is
                    # already removed, and we cannot backup the object
                    LOG("Trash Tool backupObject", WARNING,
                        "Can't backup object %s" % object_path)
                    return {}

        keep_sub = kw.get('keep_subobjects', 0)
        subobjects_dict = {}

        if not keep_sub:
            # export subobjects
            if save:
                obj = backup_object_container._getOb(object_id, None)
            else:
                object_path = container_path + [object_id]
                obj = self.unrestrictedTraverse(object_path, None)
            if obj is not None:
                for subobject_id in list(obj.objectIds()):
                    subobject = obj[subobject_id]
                    subobjects_dict[
                        subobject_id] = subobject._p_jar.exportFile(
                            subobject._p_oid, StringIO())

                    if save:  # remove subobjecs from backup object
                        obj._delObject(subobject_id)
                        if subobject_id in obj.objectIds():
                            LOG('Products.ERP5.Tool.TrashTool', WARNING,
                                'Cleaning corrupted BTreeFolder2 object at %r.' % \
                                                                     (subobject.getRelativeUrl(),))
                            obj._cleanup()
        return subobjects_dict
Пример #19
0
class Delivery(XMLObject, ImmobilisationDelivery, SimulableMixin,
               CompositionMixin, AmountGeneratorMixin):
    """
        Each time delivery is modified, it MUST launch a reindexing of
        inventories which are related to the resources contained in the Delivery
    """
    # CMF Type Definition
    meta_type = 'ERP5 Delivery'
    portal_type = 'Delivery'
    isDelivery = ConstantGetter('isDelivery', value=True)

    # Declarative security
    security = ClassSecurityInfo()
    security.declareObjectProtected(Permissions.AccessContentsInformation)

    # Default Properties
    property_sheets = (PropertySheet.Base, PropertySheet.XMLObject,
                       PropertySheet.CategoryCore, PropertySheet.DublinCore,
                       PropertySheet.Task, PropertySheet.Arrow,
                       PropertySheet.Movement, PropertySheet.Delivery,
                       PropertySheet.Reference, PropertySheet.Price)

    # Declarative interfaces
    zope.interface.implements(interfaces.IAmountGenerator,
                              interfaces.IDivergenceController,
                              interfaces.IMovementCollection)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'isAccountable')

    def isAccountable(self):
        """
        Returns 1 if this needs to be accounted
        Only account movements which are not associated to a delivery
        Whenever delivery is there, delivery has priority
      """
        return 1

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getTotalPrice')

    def getTotalPrice(self,
                      fast=0,
                      src__=0,
                      base_contribution=None,
                      rounding=False,
                      **kw):
        """ Returns the total price for this order

        if the `fast` argument is set to a true value, then it use
        SQLCatalog to compute the price, otherwise it sums the total
        price of objects one by one.

        So if the order is not in the catalog, getTotalPrice(fast=1)
        will return 0, this is not a bug.

        base_contribution must be a relative url of a category. If passed, then
        fast parameter is ignored.
      """
        if 'portal_type' not in kw:
            kw['portal_type'] = self.getPortalObject() \
              .getPortalDeliveryMovementTypeList()
        if base_contribution is None:
            if fast:
                # XXX fast ignores base_contribution for now, but it should be possible
                # to use a related key
                kw['section_uid'] = self.getDestinationSectionUid()
                kw['stock.explanation_uid'] = self.getUid()
                return self.getPortalObject()\
                  .portal_simulation.getInventoryAssetPrice(**kw)

            result = sum([
                line.getTotalPrice(fast=0) for line in self.objectValues(**kw)
            ])
        else:
            # Find amounts from movements in the delivery.
            if isinstance(base_contribution, (tuple, list)):
                base_contribution_list = base_contribution
            else:
                base_contribution_list = (base_contribution, )
            base_contribution_value_list = []
            portal_categories = self.portal_categories
            for relative_url in base_contribution_list:
                base_contribution_value = portal_categories.getCategoryValue(
                    relative_url)
                if base_contribution_value is not None:
                    base_contribution_value_list.append(
                        base_contribution_value)
            if not base_contribution_value_list:
                # We cannot find any amount so that the result is 0.
                result = 0
            else:
                matched_movement_list = [
                    movement for movement in self.getMovementList()
                    if set(movement.getBaseContributionValueList()).
                    intersection(base_contribution_value_list)
                ]
                if rounding:
                    portal_roundings = self.portal_roundings
                    matched_movement_list = [
                        portal_roundings.getRoundingProxy(movement)
                        for movement in matched_movement_list
                    ]
                result = sum([
                    movement.getTotalPrice()
                    for movement in matched_movement_list
                ])

        method = self._getTypeBasedMethod('convertTotalPrice')
        if method is not None:
            return method(result)
        return result

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getTotalNetPrice')

    def getTotalNetPrice(self, fast=0, src__=0, **kw):
        """
        Same as getTotalPrice, but including Tax and Discount (from legacy
        simulation).

        This method is deprecated because it uses deprecated Tax & Discount
        portal types. You should use getTotalPrice(base_contribution=) instead.
      """
        total_price = self.getTotalPrice(fast=fast, src__=src__, **kw)
        kw['portal_type'] = self.getPortalObject(
        ).getPortalTaxMovementTypeList()
        return total_price + self.getTotalPrice(fast=fast, src__=src__, **kw)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getTotalQuantity')

    def getTotalQuantity(self, fast=0, src__=0, **kw):
        """ Returns the total quantity of this order.

        if the `fast` argument is set to a true value, then it use
        SQLCatalog to compute the quantity, otherwise it sums the total
        quantity of objects one by one.

        So if the order is not in the catalog, getTotalQuantity(fast=1)
        will return 0, this is not a bug.
      """
        if 'portal_type' not in kw:
            kw['portal_type'] = self.getPortalObject() \
              .getPortalDeliveryMovementTypeList()
        if fast:
            kw['section_uid'] = self.getDestinationSectionUid()
            kw['stock.explanation_uid'] = self.getUid()
            return self.getPortalObject().portal_simulation.getInventory(**kw)
        return sum([
            line.getTotalQuantity(fast=0) for line in self.objectValues(**kw)
        ])

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getDeliveryUid')

    def getDeliveryUid(self):
        return self.getUid()

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getDeliveryValue')

    def getDeliveryValue(self):
        """
      Deprecated, we should use getRootDeliveryValue instead
      """
        return self

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getRootDeliveryValue')

    def getRootDeliveryValue(self):
        """
      This method returns the delivery, it is usefull to retrieve the delivery
      from a line or a cell
      """
        return self

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getDelivery')

    def getDelivery(self):
        return self.getRelativeUrl()

    security.declareProtected(Permissions.AccessContentsInformation,
                              '_getMovementList')

    def _getMovementList(self, portal_type=None, **kw):
        """
      Return a list of movements
      """
        movement_portal_type_set = set(
            self.getPortalObject().getPortalMovementTypeList())
        movement_list = self.objectValues(portal_type=movement_portal_type_set,
                                          **kw)
        if movement_list:

            if isinstance(portal_type, str):
                portal_type = portal_type,
            elif isinstance(portal_type, (list, tuple)):
                portal_type = set(portal_type)

            # Browse lines recursively and collect leafs.
            stack = [iter(movement_list)]
            movement_list = []
            while stack:
                for sub_object in stack[-1]:
                    content_list = sub_object.objectValues(
                        portal_type=movement_portal_type_set, **kw)
                    if sub_object.hasCellContent():
                        cell_list = sub_object.getCellValueList()
                        if len(cell_list) != len(content_list):
                            content_list = set(content_list).difference(
                                cell_list)
                            if content_list:
                                stack.append(iter(content_list))
                                break
                        else:
                            movement_list.extend(
                                x for x in content_list if portal_type is None
                                or x.getPortalType() in portal_type)
                    elif content_list:
                        stack.append(iter(content_list))
                        break
                    elif portal_type is None or \
                         sub_object.getPortalType() in portal_type:
                        movement_list.append(sub_object)
                else:
                    del stack[-1]

        return movement_list

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getMovementList')

    def getMovementList(self, portal_type=None, **kw):
        """
       Return a list of movements.
      """
        return self._getMovementList(portal_type=portal_type, **kw)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getSimulatedMovementList')

    def getSimulatedMovementList(self):
        """
        Return a list of simulated movements.
        This does not contain Container Line or Container Cell.
      """
        return self.getMovementList(portal_type=self.getPortalObject().
                                    getPortalSimulatedMovementTypeList())

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getInvoiceMovementList')

    def getInvoiceMovementList(self):
        """
        Return a list of simulated movements.
        This does not contain Container Line or Container Cell.
      """
        return self.getMovementList(portal_type=self.getPortalObject().
                                    getPortalInvoiceMovementTypeList())

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getContainerList')

    def getContainerList(self):
        """
        Return a list of root containers.
        This does not contain sub-containers.
      """
        return self.objectValues(
            portal_type=self.getPortalObject().getPortalContainerTypeList())

    #######################################################
    # Causality computation
    security.declareProtected(Permissions.AccessContentsInformation,
                              'isConvergent')

    def isConvergent(self, **kw):
        """
        Returns 0 if the target is not met
      """
        return bool(not self.isDivergent(**kw))

    security.declareProtected(Permissions.AccessContentsInformation,
                              'isSimulated')

    def isSimulated(self):
        """
        Returns 1 if all non-null movements have a delivery counterpart
        in the simulation
      """
        for m in self.getMovementList():
            if m.getQuantity() and not m.isSimulated():
                return 0
        return 1

    security.declareProtected(Permissions.AccessContentsInformation,
                              'isDivergent')

    def isDivergent(self, fast=0, **kw):
        """Return True if this movement diverges from the its simulation.
      """
        ## Note that fast option was removed. Now, fast=1 is ignored.

        # Check if the total quantity equals the total of each simulation movement quantity
        for simulation_movement in self._getAllRelatedSimulationMovementList():
            if simulation_movement.isDivergent():
                return True
        return False

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getDivergenceList')

    def getDivergenceList(self, **kw):
        """
      Return a list of messages that contains the divergences
      """
        divergence_list = []
        for simulation_movement in self._getAllRelatedSimulationMovementList():
            divergence_list.extend(simulation_movement.getDivergenceList())
        return divergence_list

    security.declareProtected(Permissions.AccessContentsInformation,
                              'updateCausalityState')

    @UnrestrictedMethod
    def updateCausalityState(self, solve_automatically=True, **kw):
        """
      This is often called as an activity, it will check if the
      deliver is convergent, and if so it will put the delivery
      in a solved state, if not convergent in a diverged state
      """
        isTransitionPossible = \
            self.getPortalObject().portal_workflow.isTransitionPossible
        if isTransitionPossible(self, 'diverge') and \
            isTransitionPossible(self, 'converge'):
            if self.isDivergent(**kw):
                if solve_automatically and \
                    isTransitionPossible(self, 'solve_automatically'):
                    self.solveAutomatically()
                else:
                    self.diverge()
            else:
                self.converge()

    def updateSimulation(self, calculate=False, **kw):
        if calculate:
            path = self.getPath()
            self.activate(
                after_tag='build:' + path,
                after_path_and_method_id=(path, '_localBuild'),
            ).updateCausalityState()
        if kw:
            super(Delivery, self).updateSimulation(**kw)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'splitAndDeferMovementList')

    def splitAndDeferMovementList(self,
                                  start_date=None,
                                  stop_date=None,
                                  movement_uid_list=[],
                                  delivery_solver=None,
                                  target_solver='CopyToTarget',
                                  delivery_builder=None):
        """
      this method will unlink and delete movements in movement_uid_list and
      rebuild a new Packing List with them.
      1/ change date in simulation, call TargetSolver and expand
      2/ detach simulation movements from to-be-deleted movements
      3/ delete movements
        XXX make sure that all detached movements are deleted at the same
        time, else the interaction workflow would reattach them to a delivery
        rule.
      4/ call builder
      """
        tag_list = []
        movement_list = [
            x for x in self.getMovementList()
            if x.getUid() in movement_uid_list
        ]
        if not movement_list: return

        deferred_simulation_movement_list = []
        # defer simulation movements
        if start_date is not None or stop_date is not None:
            for movement in movement_list:
                start_date = start_date or movement.getStartDate()
                stop_date = stop_date or movement.getStopDate()
                for s_m in movement.getDeliveryRelatedValueList():
                    if s_m.getStartDate() != start_date or \
                        s_m.getStopDate() != stop_date:
                        s_m.edit(start_date=start_date, stop_date=stop_date)
                        deferred_simulation_movement_list.append(s_m)

        solver_tag = '%s_splitAndDefer_solver' % self.getRelativeUrl()
        expand_tag = '%s_splitAndDefer_expand' % self.getRelativeUrl()
        detach_tag = '%s_splitAndDefer_detach' % self.getRelativeUrl()
        build_tag = '%s_splitAndDefer_build' % self.getRelativeUrl()
        # call solver and expand on deferrd movements
        for movement in movement_list:
            movement.activate(tag=solver_tag).Movement_solveMovement(
                delivery_solver, target_solver)
        tag_list.append(solver_tag)
        kw = {'after_tag': tag_list[:], 'tag': expand_tag}
        for s_m in deferred_simulation_movement_list:
            s_m.expand('deferred', activate_kw=kw)
        tag_list.append(expand_tag)

        detached_movement_url_list = []
        deleted_movement_uid_list = []
        #detach simulation movements
        for movement in movement_list:
            movement_url = movement.getRelativeUrl()
            movement_uid = getattr(movement, 'uid', None)
            if movement_uid: deleted_movement_uid_list.append(movement_uid)
            for s_m in movement.getDeliveryRelatedValueList():
                delivery_list = \
                    [x for x in s_m.getDeliveryList() if x != movement_url]
                s_m.activate(after_tag=tag_list[:],
                             tag=detach_tag).setDeliveryList(delivery_list)
                detached_movement_url_list.append(s_m.getRelativeUrl())
        tag_list.append(detach_tag)

        #delete delivery movements
        # deleteContent uses the uid as a activity tag
        self.activate(after_tag=tag_list[:]).deleteContent(
            [movement.getId() for movement in movement_list])
        tag_list.extend(deleted_movement_uid_list)

        # update causality state on self, after deletion
        self.activate(after_tag=tag_list[:],
                      activity='SQLQueue').updateCausalityState()

        # call builder on detached movements
        builder = getattr(self.portal_deliveries, delivery_builder)
        builder.activate(after_tag=tag_list[:], tag=build_tag).build(
            movement_relative_url_list=detached_movement_url_list)

    #######################################################
    # Defer indexing process
    def reindexObject(self, *k, **kw):
        """
        Reindex children and simulation
      """
        self.recursiveReindexObject(*k, **kw)
        # do not reexpand simulation: this is a task for DSolver / TSolver

    #######################################################
    # Stock Management
    def _getMovementResourceList(self):
        resource_set = {
            m.getResource()
            for m in self.objectValues(
                portal_type=self.getPortalObject().getPortalMovementTypeList())
        }
        resource_set.discard(None)
        return list(resource_set)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getInventory')

    def getInventory(self, **kw):
        """
      Returns inventory
      """
        kw['resource'] = self._getMovementResourceList()
        return self.portal_simulation.getInventory(**kw)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getCurrentInventory')

    def getCurrentInventory(self, **kw):
        """
      Returns current inventory
      """
        kw['resource'] = self._getMovementResourceList()
        return self.portal_simulation.getCurrentInventory(**kw)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getAvailableInventory')

    def getAvailableInventory(self, **kw):
        """
      Returns available inventory
      (current inventory - deliverable)
      """
        kw['resource'] = self._getMovementResourceList()
        return self.portal_simulation.getAvailableInventory(**kw)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getFutureInventory')

    def getFutureInventory(self, **kw):
        """
      Returns inventory at infinite
      """
        kw['resource'] = self._getMovementResourceList()
        return self.portal_simulation.getFutureInventory(**kw)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getInventoryList')

    def getInventoryList(self, **kw):
        """
      Returns list of inventory grouped by section or site
      """
        kw['resource'] = self._getMovementResourceList()
        return self.portal_simulation.getInventoryList(**kw)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getCurrentInventoryList')

    def getCurrentInventoryList(self, **kw):
        """
      Returns list of inventory grouped by section or site
      """
        kw['resource'] = self._getMovementResourceList()
        return self.portal_simulation.getCurrentInventoryList(**kw)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getFutureInventoryList')

    def getFutureInventoryList(self, **kw):
        """
      Returns list of inventory grouped by section or site
      """
        kw['resource'] = self._getMovementResourceList()
        return self.portal_simulation.getFutureInventoryList(**kw)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getInventoryStat')

    def getInventoryStat(self, **kw):
        """
      Returns statistics of inventory grouped by section or site
      """
        kw['resource'] = self._getMovementResourceList()
        return self.portal_simulation.getInventoryStat(**kw)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getCurrentInventoryStat')

    def getCurrentInventoryStat(self, **kw):
        """
      Returns statistics of inventory grouped by section or site
      """
        kw['resource'] = self._getMovementResourceList()
        return self.portal_simulation.getCurrentInventoryStat(**kw)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getFutureInventoryStat')

    def getFutureInventoryStat(self, **kw):
        """
      Returns statistics of inventory grouped by section or site
      """
        kw['resource'] = self._getMovementResourceList()
        return self.portal_simulation.getFutureInventoryStat(**kw)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getInventoryChart')

    def getInventoryChart(self, **kw):
        """
      Returns list of inventory grouped by section or site
      """
        kw['resource'] = self._getMovementResourceList()
        return self.portal_simulation.getInventoryChart(**kw)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getCurrentInventoryChart')

    def getCurrentInventoryChart(self, **kw):
        """
      Returns list of inventory grouped by section or site
      """
        kw['resource'] = self._getMovementResourceList()
        return self.portal_simulation.getCurrentInventoryChart(**kw)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getFutureInventoryChart')

    def getFutureInventoryChart(self, **kw):
        """
      Returns list of inventory grouped by section or site
      """
        kw['resource'] = self._getMovementResourceList()
        return self.portal_simulation.getFutureInventoryChart(**kw)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getInventoryHistoryList')

    def getInventoryHistoryList(self, **kw):
        """
      Returns list of inventory grouped by section or site
      """
        kw['resource'] = self._getMovementResourceList()
        return self.portal_simulation.getInventoryHistoryList(**kw)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getInventoryHistoryChart')

    def getInventoryHistoryChart(self, **kw):
        """
      Returns list of inventory grouped by section or site
      """
        kw['resource'] = self._getMovementResourceList()
        return self.portal_simulation.getInventoryHistoryChart(**kw)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getMovementHistoryList')

    def getMovementHistoryList(self, **kw):
        """
      Returns list of inventory grouped by section or site
      """
        kw['resource'] = self._getMovementResourceList()
        return self.portal_simulation.getMovementHistoryList(**kw)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getMovementHistoryStat')

    def getMovementHistoryStat(self, **kw):
        """
      Returns list of inventory grouped by section or site
      """
        kw['resource'] = self._getMovementResourceList()
        return self.portal_simulation.getMovementHistoryStat(**kw)

# JPS: We must still decide if getInventoryAssetPrice is part of the Delivery API

#     security.declareProtected(Permissions.AccessContentsInformation, 'getInventoryAssetPrice')
#     def getInventoryAssetPrice(self, **kw):
#       """
#         Returns asset at infinite
#       """
#       kw['category'] = self._getMovementResourceList()
#       return self.portal_simulation.getInventoryAssetPrice(**kw)
#
#     security.declareProtected(Permissions.AccessContentsInformation, 'getFutureInventoryAssetPrice')
#     def getFutureInventoryAssetPrice(self, **kw):
#       """
#         Returns asset at infinite
#       """
#       kw['category'] = self._getMovementResourceList()
#       return self.portal_simulation.getFutureInventoryAssetPrice(**kw)
#
#     security.declareProtected(Permissions.AccessContentsInformation, 'getCurrentInventoryAssetPrice')
#     def getCurrentInventoryAssetPrice(self, **kw):
#       """
#         Returns asset at infinite
#       """
#       kw['category'] = self._getMovementResourceList()
#       return self.portal_simulation.getCurrentInventoryAssetPrice(**kw)
#
#     security.declareProtected(Permissions.AccessContentsInformation, 'getAvailableInventoryAssetPrice')
#     def getAvailableInventoryAssetPrice(self, **kw):
#       """
#         Returns asset at infinite
#       """
#       kw['category'] = self._getMovementResourceList()
#       return self.portal_simulation.getAvailableInventoryAssetPrice(**kw)

##########################################################################
# Applied Rule stuff

    security.declareProtected(Permissions.AccessContentsInformation,
                              'localBuild')

    def localBuild(self, activity_kw=()):
        """Activate builders for this delivery

      The generated activity will find all buildable business links for this
      delivery, and call related builders, which will select all simulation
      movements part of the same explanation(s) as the delivery.

      XXX: Consider moving it to SimulableMixin if it's useful for
           Subscription Items.
      """
        # XXX: Previous implementation waited for expand activities of related
        #      documents and even suggested to look at explanation tree,
        #      instead of causalities. Is it required ?
        kw = {'priority': 3}
        kw.update(activity_kw)
        after_tag = kw.pop('after_tag', None)
        if isinstance(after_tag, basestring):
            after_tag = [after_tag]
        else:
            after_tag = list(after_tag) if after_tag else []
        after_tag.append('build:' + self.getPath())
        sm = getSecurityManager()
        newSecurityManager(None, nobody)
        try:
            unrestricted_apply(
                self.activate(after_tag=after_tag, **kw)._localBuild)
        finally:
            setSecurityManager(sm)

    def _localBuild(self):
        """Do an immediate local build for this delivery"""
        return self.asComposedDocument().build(explanation=self)

    def _createRootAppliedRule(self):
        # Only create RAR if we are not in a "too early" or "too late" state.
        state = self.getSimulationState()
        if (state != 'deleted' and state
                not in self.getPortalObject().getPortalDraftOrderStateList()):
            return super(Delivery, self)._createRootAppliedRule()

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getRootCausalityValueList')

    def getRootCausalityValueList(self):
        """
        Returns the initial causality value for this movement.
        This method will look at the causality and check if the
        causality has already a causality
      """
        causality_value_list = self.getCausalityValueList()
        if causality_value_list:
            initial_list = []
            for causality in causality_value_list:
                # The causality may be something which has not this method
                # (e.g. item)
                try:
                    getRootCausalityValueList = causality.getRootCausalityValueList
                except AttributeError:
                    continue
                assert causality != self
                initial_list += [
                    x for x in getRootCausalityValueList()
                    if x not in initial_list
                ]
            return initial_list
        return [self]

    # XXX Temp hack, should be removed has soon as the structure of
    # the order/delivery builder will be reviewed. It might
    # be reviewed if we plan to configure movement groups in the zmi
    security.declareProtected(Permissions.ModifyPortalContent,
                              'setRootCausalityValueList')

    def setRootCausalityValueList(self, value):
        """
      This is a hack
      """
        pass

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getParentExplanationValue')

    def getParentExplanationValue(self):
        """
        This method should be removed as soon as movement groups
        will be rewritten. It is a temp hack
      """
        return self

    # XXX Temp hack, should be removed has soon as the structure of
    # the order/delivery builder will be reviewed. It might
    # be reviewed if we plan to configure movement groups in the zmi
    security.declareProtected(Permissions.ModifyPortalContent,
                              'setParentExplanationValue')

    def setParentExplanationValue(self, value):
        """
      This is a hack
      """
        pass

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getBuilderList')

    def getBuilderList(self):
        """Returns appropriate builder list."""
        return self._getTypeBasedMethod('getBuilderList')()
        # XXX - quite a hack, since no way to know...
        #       propper implementation should use business path definition
        #       however, the real question is "is this really necessary"
        #       since the main purpose of this method is superceded
        #       by IDivergenceController

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getRootSpecialiseValue')

    def getRootSpecialiseValue(self, portal_type_list):
        """Returns first specialise value matching portal type"""
        def findSpecialiseValue(context):
            if context.getPortalType() in portal_type_list:
                return context
            if getattr(context, 'getSpecialiseValueList', None) is not None:
                for specialise in context.getSpecialiseValueList():
                    specialise_value = findSpecialiseValue(specialise)
                    if specialise_value is not None:
                        return specialise_value
            return None

        return findSpecialiseValue(self)

    security.declareProtected(Permissions.ModifyPortalContent,
                              'disconnectSimulationMovementList')

    def disconnectSimulationMovementList(self, movement_list=None):
        """Disconnects simulation movements from delivery's lines

      If movement_list is passed only those movements will be disconnected
      from simulation.

      If movements in movement_list do not belong to current
      delivery they are silently ignored.

      Returns list of disconnected Simulation Movements.

      Known issues and open questions:
       * how to protect disconnection from completed delivery?
       * what to do if movements from movement_list do not belong to delivery?
       * it is required to remove causality relation from delivery or delivery
         lines??
      """
        delivery_movement_value_list = self.getMovementList()
        if movement_list is not None:
            movement_value_list = [
                self.restrictedTraverse(movement) for movement in movement_list
            ]
            # only those how are in this delivery
            movement_value_list = [
                movement_value for movement_value in movement_value_list
                if movement_value in delivery_movement_value_list
            ]
        else:
            movement_value_list = delivery_movement_value_list

        disconnected_simulation_movement_list = []
        for movement_value in movement_value_list:
            # Note: Relies on fact that is invoked, when simulation movements are
            # indexed properly
            for simulation_movement in movement_value \
                .getDeliveryRelatedValueList(portal_type='Simulation Movement'):
                simulation_movement.edit(delivery=None, delivery_ratio=None)
                disconnected_simulation_movement_list.append(
                    simulation_movement.getRelativeUrl())

        return disconnected_simulation_movement_list

    def _getAllRelatedSimulationMovementList(self):
        result = []
        for movement in self.getMovementList():
            result += movement.getDeliveryRelatedValueList()
        return result

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getDivergentTesterAndSimulationMovementList')

    def getDivergentTesterAndSimulationMovementList(self):
        """
      This method returns a list of (tester, simulation_movement) for each divergence.
      """
        divergent_tester_list = []
        for simulation_movement in self._getAllRelatedSimulationMovementList():
            simulation_movement = simulation_movement.getObject()
            rule = simulation_movement.getParentValue().getSpecialiseValue()
            for tester in rule._getDivergenceTesterList(
                    exclude_quantity=False):
                if tester.explain(simulation_movement) not in (None, []):
                    divergent_tester_list.append((tester, simulation_movement))
        return divergent_tester_list
Пример #20
0
class Category(CMFCategory, Predicate, MetaNode, MetaResource):
    """
        Category objects allow to define classification categories
        in an ERP5 portal. For example, a document may be assigned a color
        attribute (red, blue, green). Rather than assigning an attribute
        with a pop-up menu (which is still a possibility), we can prefer
        in certain cases to associate to the object a category. In this
        example, the category will be named color/red, color/blue or color/green

        Categories can include subcategories. For example, a region category can
        define
            region/europe
            region/europe/west/
            region/europe/west/france
            region/europe/west/germany
            region/europe/south/spain
            region/americas
            region/americas/north
            region/americas/north/us
            region/americas/south
            region/asia

        In this example the base category is 'region'.

        Categories are meant to be indexed with the ZSQLCatalog (and thus
        a unique UID will be automatically generated each time a category is
        indexed).

        Categories allow define sets and subsets of objects and can be used
        for many applications :

        - association of a document to a URL

        - description of organisations (geographical, professional)

        Through acquisition, it is possible to create 'virtual' classifications based
        on existing documents or categories. For example, if there is a document at
        the URL
            organisation/nexedi
        and there exists a base category 'client', then the portal_categories tool
        will allow to create a virtual category
            client/organisation/nexedi

        Virtual categories allow not to duplicate information while providing
        a representation power equivalent to RDF or relational databases.

        Categories are implemented as a subclass of BTreeFolders

        NEW: categories should also be able to act as a domain. We should add
        a Domain interface to categories so that we do not need to regenerate
        report trees for categories.
    """

    meta_type = 'ERP5 Category'
    portal_type = 'Category'  # may be useful in the future...
    isCategory = ConstantGetter('isCategory', value=True)
    allowed_types = ('ERP5 Category', )

    # Declarative security
    security = ClassSecurityInfo()
    security.declareProtected(
        Permissions.ManagePortal,
        'manage_editProperties',
        'manage_changeProperties',
        'manage_propertiesForm',
    )

    property_sheets = (PropertySheet.Base, PropertySheet.SimpleItem,
                       PropertySheet.CategoryCore, PropertySheet.Codification,
                       PropertySheet.Reference, PropertySheet.SortIndex)

    # Inheritance
    __init__ = Predicate.__init__

    # Experimental - virtual document access
    def _experimental_aq_dynamic(self, name):
        result = Base._aq_dynamic(self, name)
        if result is not None:
            return result
        # Do some optimisation here for names which can not be names of documents
        if name.startswith('_') or name.startswith('portal_')\
            or name.startswith('aq_') or name.startswith('selection_') \
            or name.startswith('sort-') or name == 'getLayout' \
            or name == 'getListItemUrl' or name.startswith('WebSite_'):
            return None
        # Use a non recursion variable
        cache_key = 'web_site_aq_cache'
        request = self.REQUEST
        # Prevent infinite recursion
        if not request.has_key(cache_key):
            request[cache_key] = {}
        elif request[cache_key].has_key(name):
            return request[cache_key][name]
        try:
            result_list = self.portal_catalog(portal_type="Person", id=name)
            if len(result_list):
                return result_list[0].getObject()
        except:
            # Cleanup non recursion dict in case of exception
            if request[cache_key].has_key(name):
                del request[cache_key][name]
            raise
        return None

    # Experimental - WebDAV browsing support - ask JPS
    security.declareProtected(Permissions.AccessContentsInformation,
                              'experimental_listDAVObjects')

    def experimental_listDAVObjects(self):
        """
      """
        LOG("Category listDAVObjects", 0, "listDAVObjects")
        # XXX - We should try to use only Lazy Maps and to set a limit to the
        # number of objects
        # First show the subcategories
        result = list(
            self.objectValues(spec=('ERP5 Category', 'ERP5 Base Category')))
        # Then show the related documents
        result.extend(
            self.portal_categories.getRelatedValueList(
                self, self.getBaseCategory().getId(), portal_type="Person"))
        return result
Пример #21
0
class Inventory(Delivery):
    """
  Inventory
  """
    # CMF Type Definition
    meta_type = 'ERP5 Inventory'
    portal_type = 'Inventory'
    isInventory = ConstantGetter('isInventory', value=True)

    # Declarative security
    security = ClassSecurityInfo()
    security.declareObjectProtected(Permissions.AccessContentsInformation)

    # Default Properties
    property_sheets = (PropertySheet.Base, PropertySheet.XMLObject,
                       PropertySheet.CategoryCore, PropertySheet.DublinCore,
                       PropertySheet.Task, PropertySheet.Arrow,
                       PropertySheet.Movement, PropertySheet.Delivery,
                       PropertySheet.Path, PropertySheet.FlowCapacity,
                       PropertySheet.Inventory)

    security.declarePublic('alternateReindexObject')

    def alternateReindexObject(self, **kw):
        """
    This method is called when an inventory object is included in a
    group of catalogged objects.
    """
        return self.immediateReindexObject(**kw)

    # method used to build category list that willbe set on tmp line
    def appendToCategoryListFromUid(self, category_list, uid, base_category):
        object_list = [x.getObject() for x in self.portal_catalog(uid=uid)]
        if len(object_list):
            category_list.append(
                "%s/%s" % (base_category, object_list[0].getRelativeUrl()))

    def appendToCategoryList(self, category_list, value, base_category):
        category_list.append("%s/%s" % (base_category, value))

    def splitAndExtendToCategoryList(self, category_list, value, *args, **kw):
        if value is not None:
            value_list = value.split('\n')
        else:
            value_list = []
        category_list.extend(value_list)

    def immediateReindexObject(self, temp_constructor=None, **kw):
        """
    Rewrite reindexObject so that we can insert lines in stock table
    which will be equal to the difference between stock values for
    resource in the inventory and the one before the date of this inventory

    temp_constructor is used in some particular cases where we want
    to have our own temp object constructor, this is usefull if we
    want to use some classes with some particular methods
    """
        sql_catalog_id = kw.pop("sql_catalog_id", None)
        disable_archive = kw.pop("disable_archive", 0)

        draft_state_list = list(self.getPortalDraftOrderStateList())
        # we need reindex when cancelling inventories
        if 'cancelled' in draft_state_list:
            draft_state_list.remove('cancelled')
        if self.getSimulationState() in draft_state_list:
            # this prevent from trying to calculate stock
            # with not all properties defined and thus making
            # request with no condition in mysql
            object_list = [self]
            immediate_reindex_archive = sql_catalog_id is not None
            self.portal_catalog.catalogObjectList(
                object_list,
                sql_catalog_id=sql_catalog_id,
                disable_archive=disable_archive,
                immediate_reindex_archive=immediate_reindex_archive)
            return

        connection_id = None
        if sql_catalog_id is not None:
            # try to get connection used in the catalog
            catalog = self.portal_catalog[sql_catalog_id]
            for method in catalog.objectValues():
                if method.meta_type == "Z SQL Method":
                    if 'deferred' not in method.connection_id \
                         and 'transactionless' not in method.connection_id:
                        connection_id = method.connection_id
                        break

        default_inventory_calculation_list = ({
            "inventory_params": {
                "section": self.getDestinationSection(),
                "node": self.getDestination(),
                "group_by_sub_variation": 1,
                "group_by_variation": 1,
                "group_by_resource": 1,
            },
            "list_method":
            "getMovementList",
            "first_level": (
                {
                    'key': 'resource_relative_url',
                    'getter': 'getResource',
                    'setter': ("appendToCategoryList", "resource")
                },
                {
                    'key': 'variation_text',
                    'getter': 'getVariationText',
                    'setter': "splitAndExtendToCategoryList"
                },
            ),
            "second_level": ({
                'key': 'sub_variation_text',
                'getter': 'getSubVariationText',
                'setter': "splitAndExtendToCategoryList"
            }, ),
        }, )

        method = self._getTypeBasedMethod('getDefaultInventoryCalculationList')
        if method is not None:
            default_inventory_calculation_list = method()

        if temp_constructor is None:
            from Products.ERP5Type.Document import newTempMovement
            temp_constructor = newTempMovement
        stop_date = self.getStopDate()

        stock_object_list = []
        stock_append = stock_object_list.append

        for inventory_calculation_dict in default_inventory_calculation_list:

            # build a dict containing all inventory for this node
            # group by resource/variation and then subvariation
            current_inventory_list = \
                self.getPortalObject().portal_simulation.getCurrentInventoryList(
                        to_date=stop_date,
                        connection_id=connection_id,
                        **inventory_calculation_dict['inventory_params']
                )
            current_inventory_dict = {}
            current_inventory_key_id_list = [
                x["key"] for x in inventory_calculation_dict['first_level']
            ]
            for line in current_inventory_list:
                current_inventory_key = [
                    line[x] for x in current_inventory_key_id_list
                ]
                for x in xrange(len(current_inventory_key)):
                    if current_inventory_key[x] is None:
                        current_inventory_key[x] = ""
                current_inventory_key = tuple(current_inventory_key)

                if inventory_calculation_dict.has_key("second_level"):
                    # two level of variation
                    try:
                        current_inventory_by_sub_variation = \
                            current_inventory_dict[current_inventory_key]
                    except KeyError:
                        current_inventory_by_sub_variation = \
                            current_inventory_dict[current_inventory_key] = {}
                    second_level_key_id_list = [
                        x['key']
                        for x in inventory_calculation_dict['second_level']
                    ]
                    second_level_key = tuple(
                        [line[x] for x in second_level_key_id_list])
                    current_inventory_by_sub_variation[
                        second_level_key] = line['total_quantity']
                else:
                    # only one level of variation
                    current_inventory_dict[current_inventory_key] = line[
                        'total_quantity']

            # Browse all movements on inventory and create diff line when necessary
            if self.isFullInventory():
                not_used_inventory_dict = current_inventory_dict
            else:
                not_used_inventory_dict = {}
            inventory_id = self.getId()
            list_method = inventory_calculation_dict['list_method']
            method = getattr(self, list_method)

            __order_id_counter_list = [0]

            def getOrderIdCounter():
                value = __order_id_counter_list[0]
                __order_id_counter_list[0] = value + 1
                return value

            for movement in method():
                if movement.getResourceValue() is not None and \
                    movement.getInventoriatedQuantity() not in (None, ''):

                    movement_quantity = movement.getInventoriatedQuantity()
                    # construct key to retrieve inventory into dict
                    getter_list = [
                        x['getter']
                        for x in inventory_calculation_dict['first_level']
                    ]
                    key_list = []
                    for getter in getter_list:
                        method = getattr(movement, getter, None)
                        if method is not None:
                            key_list.append(method())
                    inventory_value = current_inventory_dict.get(
                        tuple(key_list), 0)
                    second_key_list = []
                    if inventory_calculation_dict.has_key('second_level'):
                        if inventory_value == 0:
                            inventory_value = {}
                        # two level
                        second_getter_list = [
                            x['getter']
                            for x in inventory_calculation_dict['second_level']
                        ]
                        for getter in second_getter_list:
                            method = getattr(movement, getter, None)
                            if method is not None:
                                second_key_list.append(method())
                            second_key_list = tuple(second_key_list)
                            if inventory_value.has_key(second_key_list):
                                total_quantity = inventory_value.pop(
                                    second_key_list)
                                # Put remaining subvariation in a dict to know which one
                                # to removed at end
                                not_used_inventory_dict[tuple(
                                    key_list)] = inventory_value
                                diff_quantity = movement_quantity - total_quantity
                            else:
                                # Inventory for new resource/variation/sub_variation
                                diff_quantity = movement_quantity
                                # Put remaining subvariation in a dict to know which one
                                # to removed at end
                                not_used_inventory_dict[tuple(
                                    key_list)] = inventory_value
                    else:
                        # we got the quantity from first level key
                        diff_quantity = movement_quantity - inventory_value

                    # Create tmp movement
                    kwd = {
                        'uid': movement.getUid(),
                        'start_date': stop_date,
                        'order_id': getOrderIdCounter(),
                        'mirror_order_id': getOrderIdCounter()
                    }
                    temp_delivery_line = temp_constructor(self, inventory_id)

                    # set category on it only if quantity not null
                    # thus line with same uid will be deleted but we
                    # don't insert line with null quantity as we test
                    # some categories like resource/destination/source
                    # before insert but not before delete
                    if diff_quantity != 0:
                        kwd['quantity'] = diff_quantity
                        category_list = self.getCategoryList()

                        setter_list = [
                            x['setter']
                            for x in inventory_calculation_dict['first_level']
                        ]
                        if inventory_calculation_dict.has_key("second_level"):
                            setter_list.extend([
                                x['setter'] for x in
                                inventory_calculation_dict['second_level']
                            ])
                        value_list = list(key_list) + list(second_key_list)
                        for x in xrange(len(setter_list)):
                            value = value_list[x]
                            setter = setter_list[x]
                            base_category = ""
                            if isinstance(setter, (tuple, list)):
                                base_category = setter[1]
                                setter = setter[0]
                            method = getattr(self, setter, None)
                            if method is not None:
                                method(category_list, value, base_category)

                        kwd['category_list'] = category_list
                    temp_delivery_line.edit(**kwd)
                    stock_append(temp_delivery_line)

            # Now create line to remove some subvariation text not present
            # in new inventory
            if len(not_used_inventory_dict):
                inventory_uid = self.getUid()
                for first_level_key in not_used_inventory_dict.keys():
                    inventory_value = \
                        not_used_inventory_dict[tuple(first_level_key)]
                    # XXX-Aurel : this code does not work with only one level of variation
                    for second_level_key in inventory_value.keys():
                        diff_quantity = -inventory_value[tuple(
                            second_level_key)]

                        kwd = {
                            'uid': inventory_uid,
                            'start_date': stop_date,
                            'order_id': getOrderIdCounter(),
                            'mirror_order_id': getOrderIdCounter()
                        }

                        # create the tmp line and set category on it
                        temp_delivery_line = temp_constructor(
                            self, inventory_id)
                        kwd['quantity'] = diff_quantity
                        category_list = self.getCategoryList()

                        setter_list = [
                            x['setter']
                            for x in inventory_calculation_dict['first_level']
                        ]
                        if inventory_calculation_dict.has_key("second_level"):
                            setter_list.extend([
                                x['setter'] for x in
                                inventory_calculation_dict['second_level']
                            ])
                        value_list = list(first_level_key) + list(
                            second_level_key)
                        for x in xrange(len(setter_list)):
                            value = value_list[x]
                            setter = setter_list[x]
                            base_category = ""
                            if isinstance(setter, (tuple, list)):
                                base_category = setter[1]
                                setter = setter[0]
                            method = getattr(self, setter, None)
                            if method is not None:
                                method(category_list, value, base_category)

                        kwd['category_list'] = category_list
                        temp_delivery_line.edit(**kwd)
                        stock_append(temp_delivery_line)

        # Reindex objects
        object_list = [self]
        immediate_reindex_archive = sql_catalog_id is not None
        self.portal_catalog.catalogObjectList(
            object_list,
            sql_catalog_id=sql_catalog_id,
            disable_archive=disable_archive,
            immediate_reindex_archive=immediate_reindex_archive)

        if stock_object_list:
            # Delete existing stock records and old inventory_cache first.
            self.portal_catalog.catalogObjectList(
                stock_object_list[:],
                method_id_list=(
                    'z0_uncatalog_stock',
                    'SQLCatalog_trimInventoryCacheOnCatalog',
                ),
                sql_catalog_id=sql_catalog_id,
                disable_cache=1,
                check_uid=0,
                disable_archive=disable_archive,
                immediate_reindex_archive=immediate_reindex_archive)
            # Then insert new records without delete.
            self.portal_catalog.catalogObjectList(
                stock_object_list[:],
                method_id_list=
                ('z_catalog_stock_list_without_delete_for_inventory_virtual_movement',
                 ),
                sql_catalog_id=sql_catalog_id,
                disable_cache=1,
                check_uid=0,
                disable_archive=disable_archive,
                immediate_reindex_archive=immediate_reindex_archive)
Пример #22
0
class Predicate(XMLObject):
  """
    A Predicate object defines a list of criterions
    which can be applied to test a document or to search for documents.

    Predicates are defined by a combination of PropertySheet values
    (ex. membership_criterion_list) and criterion list (ex. quantity
    is between 0 and 10). An additional script can be associated to
    extend the standard Predicate semantic with any additional
    script based test.

    The idea between Predicate in ERP5 is to have a simple
    way of defining simple predicates which can be later
    searched through a simplistic rule based engine and which can
    still provide complete expressivity through additional scripting.

    The approach is intended to provide the expressivity of a rule
    based system without the burden of building a fully expressive
    rule engine.
  """
  meta_type = 'ERP5 Predicate'
  portal_type = 'Predicate'
  add_permission = Permissions.AddPortalContent
  isPredicate = ConstantGetter('isPredicate', value=True)

  # Declarative security
  security = ClassSecurityInfo()
  security.declareObjectProtected(Permissions.AccessContentsInformation)

  # Declarative properties
  property_sheets = ( PropertySheet.Base
                    , PropertySheet.Predicate
                    , PropertySheet.CategoryCore
                    , PropertySheet.SortIndex
                    )

  # Declarative interfaces
  zope.interface.implements( interfaces.IPredicate, )

  security.declareProtected( Permissions.AccessContentsInformation, 'test' )
  def test(self, context, tested_base_category_list=None, 
           strict_membership=0, isMemberOf=None, **kw):
    """
      A Predicate can be tested on a given context.
      Parameters can passed in order to ignore some conditions.

      - tested_base_category_list:  this is the list of category that we do
        want to test. For example, we might want to test only the
        destination or the source of a predicate.
      - if strict_membership is specified, we should make sure that we
        are strictly a member of tested categories
      - isMemberOf can be a function caching results for
        CategoryTool.isMemberOf: it is always called with given 'context' and
        'strict_membership' values, and different categories.
    """
    self = self.asPredicate()
    if self is None:
      # asPredicate returned None, so this predicate never applies.
      # But if we reach this it is because catalog is not up to date.
      return False

    result = 1
    if getattr(aq_base(self), '_identity_criterion', None) is None:
      self._identity_criterion = PersistentMapping()
      self._range_criterion = PersistentMapping()
#    LOG('PREDICATE TEST', 0,
#        'testing %s on context of %s' % \
#        (self.getRelativeUrl(), context.getRelativeUrl()))
    for property, value in self._identity_criterion.iteritems():
      if isinstance(value, (list, tuple)):
        result = context.getProperty(property) in value
      else:
        result = context.getProperty(property) == value
#      LOG('predicate test', 0,
#          '%s after prop %s : %s == %s' % \
#          (result, property, context.getProperty(property), value))
      if not result:
        return result
    for property, (min, max) in self._range_criterion.iteritems():
      value = context.getProperty(property)
      if min is not None:
        result = value >= min
#        LOG('predicate test', 0,
#            '%s after prop %s : %s >= %s' % \
#            (result, property, value, min))
        if not result:
          return result
      if max is not None:
        result = value < max
#        LOG('predicate test', 0,
#            '%s after prop %s : %s < %s' % \
#            (result, property, value, max))
        if not result:
          return result
    multimembership_criterion_base_category_list = \
        self.getMultimembershipCriterionBaseCategoryList()
    membership_criterion_base_category_list = \
        self.getMembershipCriterionBaseCategoryList()
    tested_base_category = {}
#    LOG('predicate test', 0,
#        'categories will be tested in multi %s single %s as %s' % \
#        (multimembership_criterion_base_category_list,
#        membership_criterion_base_category_list,
#        self.getMembershipCriterionCategoryList()))
    # Test category memberships. Enable the read-only transaction cache
    # because this part is strictly read-only, and context.isMemberOf
    # is very expensive when the category list has many items.
    if isMemberOf is None:
      isMemberOf = context._getCategoryTool().isMemberOf
    with readOnlyTransactionCache():
      for c in self.getMembershipCriterionCategoryList():
        bc = c.split('/', 1)[0]
        if tested_base_category_list is None or bc in tested_base_category_list:
          if bc in multimembership_criterion_base_category_list:
            if not isMemberOf(context, c, strict_membership=strict_membership):
              return 0
          elif bc in membership_criterion_base_category_list and \
               not tested_base_category.get(bc):
            tested_base_category[bc] = \
              isMemberOf(context, c, strict_membership=strict_membership)
    if 0 in tested_base_category.itervalues():
      return 0

    # Test method calls
    test_method_id_list = self.getTestMethodIdList()
    if test_method_id_list is not None :
      for test_method_id in test_method_id_list :
        if test_method_id is not None:
          method = getattr(context,test_method_id)
          try:
            result = method(self)
          except TypeError:
            if method.func_code.co_argcount != isinstance(method, MethodType):
              raise
            # backward compatibilty with script that takes no argument
            warn('Predicate %s uses an old-style method (%s) that does not'
                 ' take the predicate as argument' % (
               self.getRelativeUrl(), method.__name__), DeprecationWarning)
            result = method()
#          LOG('predicate test', 0,
#              '%s after method %s ' % (result, test_method_id))
          if not result:
            return result
    test_tales_expression = self.getTestTalesExpression()
    if test_tales_expression != 'python: True':
      expression = Expression(test_tales_expression)
      from Products.ERP5Type.Utils import createExpressionContext
      # evaluate a tales expression with the tested value as context
      result = expression(createExpressionContext(context))
    return result

  @UnrestrictedMethod
  def _unrestrictedResolveCategory(self, *args):
    # Categories used on predicate can be not available to user query, which
    # shall be applied with predicate.
    portal_categories = getToolByName(self, 'portal_categories')
    return portal_categories.resolveCategory(*args)

  security.declareProtected( Permissions.AccessContentsInformation,
                             'buildSQLQuery' )
  def buildSQLQuery(self, strict_membership=0, table='category',
                          join_table='catalog', join_column='uid',
                          **kw):
    """
      A Predicate can be rendered as an SQL expression. This
      can be used to generate SQL requests in reports or in
      catalog search queries.

      XXX - This method is not implemented yet
    """
    # Build the identity criterion
    catalog_kw = {}
    catalog_kw.update(kw) # query_table, REQUEST, ignore_empty_string, **kw
    criterion_list = self.getCriterionList()
    # BBB: accessor is not present on old Predicate property sheet.
    if criterion_list or getattr(self, 'isEmptyPredicateValid', lambda: True)():
      for criterion in criterion_list:
        if criterion.min and criterion.max:
          catalog_kw[criterion.property] = { 'query' : (criterion.min, criterion.max),
                                             'range' : 'minmax'
                                           }
        elif criterion.min:
          catalog_kw[criterion.property] = { 'query' : criterion.min,
                                             'range' : 'min'
                                           }
        elif criterion.max:
          catalog_kw[criterion.property] = { 'query' : criterion.max,
                                             'range' : 'max'
                                           }
        else:
          # if a filter was passed as argument
          if catalog_kw.has_key(criterion.property):
            if isinstance(catalog_kw[criterion.property], (tuple, list)):
              catalog_filter_set = set(catalog_kw[criterion.property])
            else:
              catalog_filter_set = set([catalog_kw[criterion.property]])
            if isinstance(criterion.identity, (tuple, list)):
              parameter_filter_set = set(criterion.identity)
            else:
              parameter_filter_set = set([criterion.identity])
            catalog_kw[criterion.property] = \
                list(catalog_filter_set.intersection(parameter_filter_set))
          else:
            catalog_kw[criterion.property] = criterion.identity
    else:
      # By catalog definition, no object has uid 0, so this condition forces an
      # empty result.
      catalog_kw['uid'] = 0

    portal_catalog = getToolByName(self, 'portal_catalog')

    from_table_dict = {}

    # First build SQL for membership criteria
    # It would be much nicer if all this was handled by the catalog in a central place
    membership_dict = {}
    for base_category in self.getMembershipCriterionBaseCategoryList():
      membership_dict[base_category] = [] # Init dict with valid base categories
    for category in self.getMembershipCriterionCategoryList():
      base_category = category.split('/')[0] # Retrieve base category
      if membership_dict.has_key(base_category):
        category_value = self._unrestrictedResolveCategory(category, None)
        if category_value is not None:
          table_alias = "single_%s_%s" % (table, base_category)
          from_table_dict[table_alias] = 'category'
          membership_dict[base_category].append(category_value.asSQLExpression(
                                          strict_membership=strict_membership,
                                          table=table_alias,
                                          base_category=base_category))
    membership_select_list = []
    for expression_list in membership_dict.values():
      or_expression = ' OR '.join(expression_list)
      if or_expression:
        membership_select_list.append('( %s )' % or_expression)

    # Then build SQL for multimembership_dict criteria
    multimembership_dict = {}
    for base_category in self.getMultimembershipCriterionBaseCategoryList():
      multimembership_dict[base_category] = [] # Init dict with valid base categories
    join_count = 0
    for category in self.getMembershipCriterionCategoryList():
      base_category = category.split('/')[0] # Retrieve base category
      if multimembership_dict.has_key(base_category):
        category_value = self._unrestrictedResolveCategory(category)
        if category_value is not None:
          join_count += 1
          table_alias = "multi_%s_%s" % (table, join_count)
          from_table_dict[table_alias] = 'category'
          multimembership_dict[base_category].append(category_value.asSQLExpression(
                                          strict_membership=strict_membership,
                                          table=table_alias,
                                          base_category=base_category))
    multimembership_select_list = []
    for expression_list in multimembership_dict.values():
      and_expression = ' AND '.join(expression_list)
      if and_expression:
        multimembership_select_list.append(and_expression)

    # Build the join where expression
    join_select_list = []
    for k in from_table_dict.iterkeys():
      join_select_list.append('%s.%s = %s.uid' % (join_table, join_column, k))

    sql_text = ' AND '.join(join_select_list + membership_select_list +
                            multimembership_select_list)

    # Now merge identity and membership criteria
    if len(sql_text):
      catalog_kw['where_expression'] = SQLQuery(sql_text)
    else:
      catalog_kw['where_expression'] = ''
    # force implicit join
    catalog_kw['implicit_join'] = True
    sql_query = portal_catalog.buildSQLQuery(**catalog_kw)
    # XXX from_table_list is None most of the time after the explicit_join work
    for alias, table in sql_query['from_table_list']:
      if from_table_dict.has_key(alias):
        raise KeyError, "The same table is used twice for an identity criterion and for a membership criterion"
      from_table_dict[alias] = table
    sql_query['from_table_list'] = from_table_dict.items()
    return sql_query

  # Compatibililty SQL Sql
  security.declareProtected( Permissions.AccessContentsInformation, 'buildSqlQuery' )
  buildSqlQuery = buildSQLQuery

  security.declareProtected( Permissions.AccessContentsInformation, 'asSQLExpression' )
  def asSQLExpression(self, strict_membership=0, table='category'):
    """
      A Predicate can be rendered as an SQL expression. This
      can be used to generate SQL requests in reports or in
      catalog search queries.
    """
    return self.buildSQLQuery(strict_membership=strict_membership, table=table)['where_expression']

  # Compatibililty SQL Sql
  security.declareProtected( Permissions.AccessContentsInformation, 'asSqlExpression' )
  asSqlExpression = asSQLExpression

  security.declareProtected( Permissions.AccessContentsInformation, 'asSQLJoinExpression' )
  def asSQLJoinExpression(self, strict_membership=0, table='category', join_table='catalog', join_column='uid'):
    """
    """
    table_list = self.buildSQLQuery(strict_membership=strict_membership, table=table)['from_table_list']
    sql_text_list = map(lambda (a,b): '%s AS %s' % (b,a), filter(lambda (a,b): a != join_table, table_list))
    return ' , '.join(sql_text_list)

  # Compatibililty SQL Sql
  security.declareProtected( Permissions.AccessContentsInformation, 'asSqlJoinExpression' )
  asSqlJoinExpression = asSQLJoinExpression

  def searchResults(self, **kw):
    """
    """
    portal_catalog = getToolByName(self, 'portal_catalog')
    return portal_catalog.searchResults(build_sql_query_method=self.buildSQLQuery,**kw)

  def countResults(self, REQUEST=None, used=None, **kw):
    """
    """
    portal_catalog = getToolByName(self, 'portal_catalog')
    return portal_catalog.countResults(build_sql_query_method=self.buildSQLQuery,**kw)

  security.declareProtected( Permissions.AccessContentsInformation, 'getCriterionList' )
  def getCriterionList(self, **kw):
    """
      Returns the list of criteria which are defined by the Predicate.

      Each criterion is returned in a TempBase instance intended to be
      displayed in a ListBox.

      XXX - It would be better to return criteria in a Criterion class
            instance
    """
    if getattr(aq_base(self), '_identity_criterion', None) is None:
      self._identity_criterion = PersistentMapping()
      self._range_criterion = PersistentMapping()
    criterion_dict = {}
    for p in self.getCriterionPropertyList():
      criterion_dict[p] = newTempBase(self, 'new_%s' % p)
      criterion_dict[p].identity = self._identity_criterion.get(p, None)
      criterion_dict[p].uid = 'new_%s' % p
      criterion_dict[p].property = p
      criterion_dict[p].min = self._range_criterion.get(p, (None, None))[0]
      criterion_dict[p].max = self._range_criterion.get(p, (None, None))[1]
    criterion_list = criterion_dict.values()
    criterion_list.sort()
    return criterion_list

  security.declareProtected( Permissions.ModifyPortalContent, 'setCriterion' )
  def setCriterion(self, property, identity=None, min=None, max=None, **kw):
    """
      This methods sets parameters of a criterion. There is at most one
      criterion per property. Defined parameters are

      identity -- if not None, allows for testing identity of the property
                  with the provided value

      min      -- if not None, allows for testing that the property
                  is greater than min

      max      -- if not None, allows for testing that the property
                  is greater than max

    """
    # XXX 'min' and 'max' are built-in functions.
    if getattr(aq_base(self), '_identity_criterion', None) is None:
      self._identity_criterion = PersistentMapping()
      self._range_criterion = PersistentMapping()
    if identity is not None :
      self._identity_criterion[property] = identity
    if min == '':
      min = None
    if max == '':
      max = None
    if min is None and max is None:
      try:
        del self._range_criterion[property]
      except KeyError:
        pass
    else:
      self._range_criterion[property] = (min, max)
    self.reindexObject()

  security.declareProtected( Permissions.ModifyPortalContent, 'edit' )
  def edit(self, **kwd):
    """
      The edit method is overriden so that any time a
      criterion_property_list property is defined, a list of criteria
      is created to match the provided criterion_property_list.
    """
    if getattr(aq_base(self), '_identity_criterion', None) is None:
      self._identity_criterion = PersistentMapping()
      self._range_criterion = PersistentMapping()
    if 'criterion_property_list' in kwd:
      criterion_property_list = kwd['criterion_property_list']
      identity_criterion = PersistentMapping()
      range_criterion = PersistentMapping()
      for criterion in self._identity_criterion.iterkeys() :
        if criterion in criterion_property_list :
          identity_criterion[criterion] = self._identity_criterion[criterion]
      for criterion in self._range_criterion.iterkeys() :
        if criterion in criterion_property_list :
          range_criterion[criterion] = self._range_criterion[criterion]
      self._identity_criterion = identity_criterion
      self._range_criterion = range_criterion
    kwd['reindex_object'] = 1
    return self._edit(**kwd)

  # Predicate fusion method
  security.declareProtected( Permissions.ModifyPortalContent, 'setPredicateCategoryList' )
  def setPredicateCategoryList(self, category_list):
    """
      This method updates a Predicate by implementing an
      AND operation on all predicates (or categories)
      provided in category_list. Categories behave as a
      special kind of predicate which only acts on category
      membership.

      WARNING: this method does not take into account scripts at
      this point.
    """
    category_tool = aq_inner(self.portal_categories)
    base_category_id_list = category_tool.objectIds()
    membership_criterion_category_list = []
    membership_criterion_base_category_list = []
    multimembership_criterion_base_category_list = []
    test_method_id_list = []
    criterion_property_list = []
    # reset criterions
    self._identity_criterion = PersistentMapping()
    self._range_criterion = PersistentMapping()

    for c in category_list:
      bc = c.split('/')[0]
      if bc in base_category_id_list:
        # This is a category
        membership_criterion_category_list.append(c)
        membership_criterion_base_category_list.append(bc)
      else:
        predicate_value = category_tool.resolveCategory(c)
        if predicate_value is not None:
          criterion_property_list.extend(predicate_value.getCriterionPropertyList())
          membership_criterion_category_list.extend(
                      predicate_value.getMembershipCriterionCategoryList())
          membership_criterion_base_category_list.extend(
                      predicate_value.getMembershipCriterionBaseCategoryList())
          multimembership_criterion_base_category_list.extend(
                      predicate_value.getMultimembershipCriterionBaseCategoryList())
          test_method_id_list += list(predicate_value.getTestMethodIdList() or [])
          for p in predicate_value.getCriterionList():
            self.setCriterion(p.property, identity=p.identity, min=p.min, max=p.max)
    self.setCriterionPropertyList(criterion_property_list)
    self._setMembershipCriterionCategoryList(membership_criterion_category_list)
    self._setMembershipCriterionBaseCategoryList(membership_criterion_base_category_list)
    self._setMultimembershipCriterionBaseCategoryList(multimembership_criterion_base_category_list)
    self._setTestMethodIdList(test_method_id_list)
    self.reindexObject()

  security.declareProtected(Permissions.AccessContentsInformation, 'generatePredicate')
  def generatePredicate(self, multimembership_criterion_base_category_list=(),
                        membership_criterion_base_category_list=(),
                        criterion_property_list=(),
                        identity_criterion=None,
                        range_criterion=None,):
    """
    This method generates a new temporary predicate based on an ad-hoc
    interpretation of local properties of an object. For example,
    a start_range_min property will be interpreted as a way to define
    a min criterion on start_date.

    The purpose of this method is to be called from
    a script called PortalType_asPredicate to ease the generation of
    Predicates based on range properties. It should be considered mostly
    as a trick to simplify the development of Predicates and forms.
    """
    new_membership_criterion_category_list = list(self.getMembershipCriterionCategoryList())
    new_membership_criterion_base_category_list = list(self.getMembershipCriterionBaseCategoryList())
    new_multimembership_criterion_base_category_list = list(self.getMultimembershipCriterionBaseCategoryList())

    for base_category in multimembership_criterion_base_category_list:
      category_list = self.getProperty(base_category + '_list')
      if category_list is not None and len(category_list)>0:
        for category in category_list:
          new_membership_criterion_category_list.append(base_category + '/' + category)
        if base_category not in new_multimembership_criterion_base_category_list:
          new_multimembership_criterion_base_category_list.append(base_category)

    for base_category in membership_criterion_base_category_list:
      category_list = self.getProperty(base_category + '_list')
      if category_list is not None and len(category_list)>0:
        for category in category_list:
          new_membership_criterion_category_list.append(base_category + '/' + category)
        if base_category not in new_membership_criterion_base_category_list:
          new_membership_criterion_base_category_list.append(base_category)

    new_criterion_property_list =  list(self.getCriterionPropertyList())

    # We need to build new criteria for asContext, and we should not
    # modify the original, so we always make copies. Since the usage is
    # temporary, use dicts instead of persistent mappings.
    new_identity_criterion = dict(getattr(self, '_identity_criterion', None) or
                                  {})
    new_identity_criterion.update(identity_criterion or {})
    new_range_criterion = dict(getattr(self, '_range_criterion', None) or {})
    new_range_criterion.update(range_criterion or {})

    # Look at local properties and make it criterion properties
    for property in criterion_property_list:
      if property not in self.getCriterionPropertyList() \
        and property in self.propertyIds():
          new_criterion_property_list.append(property)
          property_min = property + '_range_min'
          property_max = property + '_range_max'
          if getattr(self, 'get%s' % convertToUpperCase(property), None) is not None\
            and self.getProperty(property) is not None:
            new_identity_criterion[property] = self.getProperty(property)
          elif getattr(self, 'get%s' % convertToUpperCase(property_min), None) is not None:
            min = self.getProperty(property_min)
            max = self.getProperty(property_max)
            new_range_criterion[property] = (min,max)
    # Return a new context with new properties, like if
    # we have a predicate with local properties
    new_self = self.asContext(
        membership_criterion_category=new_membership_criterion_category_list,
        membership_criterion_base_category=new_membership_criterion_base_category_list,
        multimembership_criterion_base_category=new_multimembership_criterion_base_category_list,
        criterion_property_list=new_criterion_property_list,
        _identity_criterion=new_identity_criterion,
        _range_criterion=new_range_criterion)

    return new_self

  security.declareProtected(Permissions.AccessContentsInformation,
                            'asPredicate')
  def asPredicate(self, script_id=None):
    """
      This method tries to convert the current Document into a predicate
      looking up methods named Class_asPredictae, MetaType_asPredicate, PortalType_asPredicate
    """
    cache = getTransactionalVariable()
    key = id(self), script_id
    if 'asPredicate' in cache:
      cache = cache['asPredicate']
      if key in cache:
        return cache[key]
    else:
      cache = cache['asPredicate'] = {}
    script = self._getTypeBasedMethod('asPredicate', script_id)
    if script is not None:
      self = script()
    cache[key] = self
    return self

  def searchPredicate(self, **kw):
    """
      Returns a list of documents matching the predicate

      TO BE IMPLEMENTED using portal_catalog(**kw)
    """
    pass

  security.declareProtected(Permissions.AccessContentsInformation,
                            'getMembershipCriterionCategoryList')
  def getMembershipCriterionCategoryList(self, filter=None, **kw):
    """
    If filter is specified, return category only or document only
    in membership_criterion_category values.
    """
    all_list = self._baseGetMembershipCriterionCategoryList()
    if filter in ('category', 'document'):
      portal_categories = self.getPortalObject().portal_categories
      result_dict = {'category':[], 'document':[]}
      for x in all_list:
        try:
          if portal_categories.restrictedTraverse(x).getPortalType() == \
             'Category':
            result_dict['category'].append(x)
          else:
            result_dict['document'].append(x)
        except KeyError:
          result_dict['document'].append(x)
      return result_dict[filter]
    else:
      return all_list

  security.declareProtected(Permissions.ModifyPortalContent,
                            'setMembershipCriterionDocumentList' )
  def setMembershipCriterionDocumentList(self, document_list):
    """
    Appends to membership_criterion_category values.
    """
    return self.setMembershipCriterionCategoryList(
      (self.getMembershipCriterionCategoryList() + document_list))
Пример #23
0
class EmailDocument(TextDocument):
    """
    EmailDocument is a File which stores its metadata in a form which
    is similar to a TextDocument.
    A Text Document which stores raw HTML and can 
    convert it to various formats.
  """

    meta_type = 'ERP5 Email Document'
    portal_type = 'Email Document'
    add_permission = Permissions.AddPortalContent
    # XXX must be removed later - only event is a delivery
    isDelivery = ConstantGetter('isDelivery', value=True)

    # Declarative security
    security = ClassSecurityInfo()
    security.declareObjectProtected(Permissions.AccessContentsInformation)

    # Declarative properties
    property_sheets = (PropertySheet.Base, PropertySheet.XMLObject,
                       PropertySheet.CategoryCore, PropertySheet.DublinCore,
                       PropertySheet.Version, PropertySheet.Document,
                       PropertySheet.ExternalDocument, PropertySheet.Url,
                       PropertySheet.TextDocument, PropertySheet.Arrow,
                       PropertySheet.Task, PropertySheet.ItemAggregation,
                       PropertySheet.EmailHeader, PropertySheet.Reference,
                       PropertySheet.Data)

    # Mail processing API
    def _getMessage(self):
        result = getattr(self, '_v_message', None)
        if result is None:
            result = message_from_string(str(self.getData()))
            self._v_message = result
        return result

    def _setData(self, data):
        super(EmailDocument, self)._setData(data)
        try:
            del self._v_message
        except AttributeError:
            pass

    def _getMessageTextPart(self):
        """
    Return the main text part of the message data

    Based on rfc: http://tools.ietf.org/html/rfc2046#section-5.1.4)
    """
        # Default value if no text is found
        found_part = None

        part_list = [self._getMessage()]
        while part_list:
            part = part_list.pop(0)
            if part.is_multipart():
                if part.get_content_subtype() == 'alternative':
                    # Try to get the favourite text format defined on preference
                    preferred_content_type = self.getPortalObject().portal_preferences.\
                                                   getPreferredTextFormat('text/html')
                    favourite_part = None
                    for subpart in part.get_payload():
                        if subpart.get_content_type(
                        ) == preferred_content_type:
                            part_list.insert(0, subpart)
                        else:
                            part_list.append(subpart)
                else:
                    part_list.extend(part.get_payload())
            elif part.get_content_maintype() == 'text':
                found_part = part
                break

        return found_part

    security.declareProtected(Permissions.AccessContentsInformation,
                              'isSupportBaseDataConversion')

    def isSupportBaseDataConversion(self):
        """
    """
        return False

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getContentInformation')

    def getContentInformation(self):
        """
    Returns the content information from the header information.
    This is used by the metadata discovery system.

    Header information is converted in UTF-8 since this is the standard
    way of representing strings in ERP5.
    """
        result = {}
        for (name, value) in self._getMessage().items():
            try:
                decoded_header = decode_header(value)
            except HeaderParseError, error_message:
                decoded_header = ()
                LOG(
                    'EmailDocument.getContentInformation', INFO,
                    'Failed to decode %s header of %s with error: %s' %
                    (name, self.getPath(), error_message))
            for text, encoding in decoded_header:
                text, encoding = testCharsetAndConvert(text, 'text/plain',
                                                       encoding)
                if name in result:
                    result[name] = '%s %s' % (result[name], text)
                else:
                    result[name] = text
        return result
Пример #24
0
class InteractionWorkflow(Workflow):
  """
  An ERP5 Interaction Workflow (deprecate Products.ERP5.InteractionWorkflow
  based on DCWorkflow)
  """
  meta_type = 'ERP5 Interaction Workflow'
  portal_type = 'Interaction Workflow'
  add_permission = Permissions.AddPortalContent

  security = ClassSecurityInfo()
  security.declareObjectProtected(Permissions.AccessContentsInformation)

  property_sheets = (
    'Base',
    'XMLObject',
    'CategoryCore',
    'DublinCore',
    'Comment',
  )

  # Do not seem to be used for InteractionWorkflow but must be defined
  # (GuardMixin.checkGuard())
  isManagerBypass = ConstantGetter('isManagerBypass', value=False)

  security.declarePrivate('notifyCreated')
  def notifyCreated(self, ob):
    pass

  security.declareProtected(Permissions.View, 'getChainedPortalTypeList')
  def getChainedPortalTypeList(self):
    """
    Returns the list of portal types that are chained to this
    Interaction Workflow
    """
    interaction_workflow_id = self.getId()
    portal_type_list = []
    for portal_type in self.getPortalObject().portal_types.listTypeInfo():
      if interaction_workflow_id in portal_type.getTypeWorkflowList():
        portal_type_list.append(portal_type.getId())
    return portal_type_list

  security.declarePrivate('listObjectActions')
  def listObjectActions(self, info):
    return []

  security.declarePrivate('_changeStateOf')
  def _changeStateOf(self, ob, tdef=None, kwargs=None) :
    """
    InteractionWorkflow is stateless. Thus, this function should do nothing.
    """
    return

  security.declarePrivate('isInfoSupported')
  def isInfoSupported(self, ob, name):
    '''
    Returns a true value if the given info name is supported.
    '''
    vdef = self.getVariableValueDict().get(name, None)
    if vdef is None:
        return 0
    return 1

  security.declarePrivate('getInfoFor')
  def getInfoFor(self, ob, name, default):
    '''
    Allows the user to request information provided by the
    workflow.  This method must perform its own security checks.
    '''
    vdef = getattr(self, name, _MARKER)
    for r, v in six.iteritems(self.getVariableValueDict()):
      if r == name:
        vdef = v
        break
    if vdef is _MARKER:
      return default
    if not vdef.checkGuard(getSecurityManager(), self, ob):
      return default
    status = self._getStatusOf(ob)
    variable_default_expression = vdef.getVariableDefaultExpression()
    if status is not None and name in status:
      value = status[name]
    # Not set yet.  Use a default.
    elif variable_default_expression is not None:
      ec = createExpressionContext(StateChangeInfo(ob, self, status))
      value = variable_default_expression(ec)
    else:
      value = ''

    return value

  security.declarePrivate('isWorkflowMethodSupported')
  def isWorkflowMethodSupported(self, ob, tid):
    '''
    Returns a true value if the given workflow method
    is supported in the current state.
    '''
    tdef = self.getTransitionValueByReference(tid)
    return tdef is not None and self._checkTransitionGuard(tdef, ob)

  def _checkTransitionGuard(self, tdef, ob, **kw):
    if tdef.getTemporaryDocumentDisallowed():
      isTempDocument = getattr(ob, 'isTempDocument', None)
      if isTempDocument is not None:
        if isTempDocument():
          return 0
    return Workflow._checkTransitionGuard(self, tdef, ob, **kw)

  security.declarePrivate('getValidRoleList')
  def getValidRoleList(self):
    return sorted(self.getPortalObject().acl_users.valid_roles())

  security.declarePrivate('getinteraction_workflowVariableMatchDict')
  def getWorklistVariableMatchDict(self, info, check_guard=True):
    return None

  def _getWorkflowStateOf(self, ob, id_only=0):
    return None

  security.declarePrivate('getScriptValueList')
  def getScriptValueList(self):
    return self.objectValues(portal_type='Workflow Script')

  security.declarePrivate('getTransitionValueByReference')
  def getTransitionValueByReference(self, transition_id):
      return self._getOb('interaction_' + transition_id, default=None)

  security.declarePrivate('getTransitionValueList')
  def getTransitionValueList(self):
    return self.objectValues(portal_type="Interaction Workflow Interaction")

  security.declarePrivate('getTransitionValueByReference')
  def getTransitionValueByReference(self, transition_id):
      return self._getOb('interaction_' + transition_id, default=None)

  security.declarePrivate('getTransitionValueList')
  def getTransitionValueList(self):
    return self.objectValues(portal_type="Interaction Workflow Interaction")

  security.declarePrivate('getTransitionReferenceList')
  def getTransitionReferenceList(self):
    return [ob.getReference() for ob in self.objectValues(portal_type="Interaction Workflow Interaction")]

  security.declarePrivate('notifyWorkflowMethod')
  def notifyWorkflowMethod(self, ob, transition_list, args=None, kw=None):
    """ InteractionWorkflow is stateless. Thus, this function should do nothing.
    """
    pass

  security.declarePrivate('notifyBefore')
  def notifyBefore(self, ob, transition_list, args=None, kw=None):
    '''
    Notifies this workflow of an action before it happens,
    allowing veto by exception.  Unless an exception is thrown, either
    a notifySuccess() or notifyException() can be expected later on.
    The action usually corresponds to a method name.
    '''
    if isinstance(transition_list, basestring):
      return

    # Wrap args into kw since this is the only way to be compatible with
    # DCWorkflow. A better approach consists in extending DCWorkflow
    if kw is None:
      kw = {'workflow_method_args' : args}
    else:
      kw = kw.copy()
      kw['workflow_method_args'] = args
    filtered_transition_list = []
    append = filtered_transition_list.append
    for t_id in transition_list:
      tdef = self.getTransitionValueByReference(t_id)
      assert tdef.getTriggerType() == TRIGGER_WORKFLOW_METHOD
      append(tdef.getId())
      former_status = self._getStatusOf(ob)
      # Pass lots of info to the script in a single parameter.
      sci = StateChangeInfo(ob, self, former_status, tdef, None, None, kwargs=kw)
      for script in tdef.getBeforeScriptValueList():
        script(sci)  # May throw an exception.
    return filtered_transition_list

  security.declarePrivate('notifySuccess')
  def notifySuccess(self, ob, transition_list, result, args=None, kw=None):
    """
    Notifies this workflow that an action has taken place.
    """
    if isinstance(transition_list, basestring):
      return

    if kw is None:
      kw = {}
    else:
      kw = kw.copy()
    kw['workflow_method_args'] = args
    kw['workflow_method_result'] = result

    workflow_variable_list = self.getVariableValueList()
    for t_id in transition_list:
      tdef = self.getTransitionValueByReference(t_id)
      assert tdef.getTriggerType() == TRIGGER_WORKFLOW_METHOD

      # Initialize variables
      former_status = self._getStatusOf(ob)
      econtext = None
      sci = None

      # Update variables.
      status = {}
      for vdef in workflow_variable_list:
        id_ = vdef.getId()
        if not vdef.getStatusIncluded():
          continue
        expression = None
        value = ''
        if not vdef.getAutomaticUpdate() and id_ in former_status:
          # Preserve former value
          value = former_status[id_]
        else:
          variable_default_expression = vdef.getVariableDefaultExpression()
          if variable_default_expression is not None:
            expression = variable_default_expression
            if expression is not None:
              # Evaluate an expression.
              if econtext is None:
                # Lazily create the expression context.
                if sci is None:
                  sci = StateChangeInfo(
                      ob, self, former_status, tdef,
                      None, None, None)
                econtext = createExpressionContext(sci)
              value = expression(econtext)
        status[id_] = value

      sci = StateChangeInfo(
            ob, self, former_status, tdef, None, None, kwargs=kw)

      # Execute the "after" script.
      for script in tdef.getAfterScriptValueList():
        script(sci) # May throw an exception.

      # Queue the "Before Commit" scripts
      sm = getSecurityManager()
      for script in tdef.getBeforeCommitScriptValueList():
        transaction.get().addBeforeCommitHook(self._before_commit,
                                              (sci, script.getId(), sm))

      # Execute "activity" scripts
      for script in tdef.getActivateScriptValueList():
        ob.activate(activity='SQLQueue').activeInteractionScript(
          interaction_workflow_path=self.getPhysicalPath(),
          script_name=script.getId(),
          status=status,
          tdef_id=tdef.getId(),
        )

  def _before_commit(self, sci, script_name, security_manager):
    # check the object still exists before calling the script
    ob = sci.object
    while ob.isTempObject():
      ob = ob.getParentValue()
    if aq_base(self.unrestrictedTraverse(ob.getPhysicalPath(), None)) is \
       aq_base(ob):
      current_security_manager = getSecurityManager()
      try:
        # Who knows what happened to the authentication context
        # between here and when the interaction was executed... So we
        # need to switch to the security manager as it was back then
        setSecurityManager(security_manager)
        self._getOb(script_name)(sci)
      finally:
        setSecurityManager(current_security_manager)

  security.declarePrivate('activeScript')
  def activeScript(self, script_name, ob_url, status, tdef_id):
    # BBB for when an upgrade to callInterationScript still has unexecuted
    # activeScript activities leftover from the previous code.
    self.callInterationScript(
      script_name=script_name,
      ob=self.unrestrictedTraverse(ob_url),
      status=status,
      tdef_id=tdef_id,
    )

  security.declarePrivate('callInterationScript')
  def callInterationScript(self, script_name, ob, status, tdef_id):
    self._getOb(script_name)(
      StateChangeInfo(
        ob, self, status,
        self.getTransitionValueByReference(tdef_id),
        None, None, None,
      ),
    )

  security.declarePrivate('isActionSupported')
  def isActionSupported(self, ob, action, **kw):
    '''
    Returns a true value if the given action name
    is possible in the current state.
    '''
    sdef = self._getWorkflowStateOf(ob, id_only=0)
    if sdef is None:
      return 0

    if action in sdef.getDestinationIdList():
      tdef = self._getOb(action, None)
      if (tdef is not None and
        tdef.getTriggerType() == TRIGGER_USER_ACTION and
        self._checkTransitionGuard(tdef, ob, **kw)):
        return 1
    return 0

  security.declareProtected(Permissions.AccessContentsInformation,
    'getStateValueByReference')
  def getStateValueByReference(self, reference):
    return None

  security.declareProtected(Permissions.AccessContentsInformation,
    'getStateValueList')
  def getStateValueList(self):
    return []

  def _checkConsistency(self, fixit=False):
    return []

  security.declareProtected(Permissions.AccessContentsInformation, 'showAsXML')
  def showAsXML(self, root=None):
    from lxml import etree
    from lxml.etree import Element, SubElement

    if root is None:
      root = Element('erp5')
      return_as_object = False

    # Define a list of property to show to users:
    # It seems even in DC interaction workflow, creation guard hasn't been configured;
    # so it is not used? thus I didn't show creation guard as xml here. (zwj)
    interaction_workflow_prop_id_to_show = sorted(['description',
          'manager_bypass'])
    # workflow as XML, need to rename DC workflow's portal_type before comparison.
    interaction_workflow = SubElement(root, 'interaction_workflow',
                        attrib=dict(reference=self.getReference(),
                        portal_type=self.getPortalType()))

    for prop_id in sorted(interaction_workflow_prop_id_to_show):
      prop_value = self.getProperty(prop_id)
      prop_type = self.getPropertyType(prop_id)
      sub_object = SubElement(interaction_workflow, prop_id, attrib=dict(type=prop_type))
      if prop_value is None or prop_value == [] or prop_value == ():
        prop_value = ''
      sub_object.text = str(prop_value)

    # 1. Interaction as XML
    interaction_reference_list = []
    interaction_list = self.objectValues(portal_type='Interaction Workflow Interaction')
    interaction_prop_id_to_show = sorted(['activate_script_name',
    'after_script_name', 'before_commit_script_name', 'description',
    'groups', 'roles', 'expr', 'permissions', 'trigger_method_id',
    'trigger_once_per_transaction', 'portal_type_filter', 'portal_type_group_filter',
    'script_name', 'temporary_document_disallowed', 'trigger_type'])
    for tdef in interaction_list:
      interaction_reference_list.append(tdef.getReference())
    interactions = SubElement(interaction_workflow, 'interactions', attrib=dict(
      interaction_list=str(interaction_reference_list),
      number_of_element=str(len(interaction_reference_list))))
    for tdef in interaction_list:
      interaction = SubElement(interactions, 'interaction', attrib=dict(
            reference=tdef.getReference(),portal_type=tdef.getPortalType()))
      guard = SubElement(interaction, 'guard', attrib=dict(type='object'))
      for property_id in interaction_prop_id_to_show:
        # creationg guard
        if property_id in ['groups', 'permissions', 'roles']:
          if property_id == 'groups': prop_id = 'group_list'
          if property_id == 'permissions': prop_id = 'permission_list'
          if property_id == 'roles': prop_id = 'role_list'
          property_value = tdef.getProperty(prop_id)
          if property_value is not None:
            property_value = tuple(property_value)
          sub_object = SubElement(guard, property_id, attrib=dict(type='guard configuration'))
        elif property_id == 'expr':
          property_value = tdef.getGuardExpression()
          sub_object = SubElement(guard, property_id, attrib=dict(type='guard configuration'))
        # no-property definded action box configuration
        elif property_id == 'trigger_type':
          property_value = getattr(tdef, property_id, None)
          sub_object = SubElement(interaction, property_id, attrib=dict(type='string'))
        elif property_id in ['activate_script_name', 'after_script_name',
              'before_commit_script_name', 'portal_type_filter', 'trigger_method_id', 'method_id',
              'portal_type_group_filter', 'script_name', 'once_per_transaction',
              'temporary_document_disallowed']:
          if property_id == 'activate_script_name': prop_id = 'activate_script_name_list'
          if property_id == 'after_script_name': prop_id = 'after_script_name_list'
          if property_id == 'before_commit_script_name': prop_id = 'before_commit_script_name_list'
          if property_id in ('method_id', 'trigger_method_id'): prop_id = 'trigger_method_id_list'
          if property_id == 'once_per_transaction': prop_id = 'trigger_once_per_transaction'
          if property_id == 'portal_type_filter': prop_id = 'portal_type_filter_list'
          if property_id == 'portal_type_group_filter': prop_id = 'portal_type_group_filter_list'
          if property_id == 'script_name': prop_id = 'before_script_name_list'
          if property_id == 'temporary_document_disallowed': prop_id = 'temporary_document_disallowed'
          property_value = tdef.getProperty(prop_id)

          if property_id in ['activate_script_name', 'after_script_name',
            'before_commit_script_name','script_name'] and property_value is not None:
            list_temp =[]
            for value in property_value:
              list_temp.append(self._getOb(value).getReference())
            property_value = list_temp
          sub_object = SubElement(interaction, property_id, attrib=dict(type='string'))
        else:
          property_value = tdef.getProperty(property_id)
          property_type = tdef.getPropertyType(property_id)
          sub_object = SubElement(interaction, property_id, attrib=dict(type=property_type))
        if property_value is None or property_value == [] or property_value == ():
          property_value = ''
        sub_object.text = str(property_value)

    # 2. Variable as XML
    variable_reference_list = []
    variable_list = self.objectValues(portal_type='Workflow Variable')
    variable_prop_id_to_show = ['description', 'variable_default_expression',
          'for_catalog', 'for_status', 'automatic_update']
    for vdef in variable_list:
      variable_reference_list.append(vdef.getReference())
    variables = SubElement(interaction_workflow, 'variables', attrib=dict(variable_list=str(variable_reference_list),
                        number_of_element=str(len(variable_reference_list))))
    for vdef in variable_list:
      variable = SubElement(variables, 'variable', attrib=dict(reference=vdef.getReference(),
            portal_type=vdef.getPortalType()))
      for property_id in sorted(variable_prop_id_to_show):
        if property_id == 'automatic_update':
          property_value = vdef.getAutomaticUpdate()
          sub_object = SubElement(variable, property_id, attrib=dict(type='int'))
        else:
          property_value = vdef.getProperty(property_id)
          property_type = vdef.getPropertyType(property_id)
          sub_object = SubElement(variable, property_id, attrib=dict(type=property_type))
        if property_value is None or property_value ==() or property_value == []:
          property_value = ''
        sub_object.text = str(property_value)

    # 3. Script as XML
    script_reference_list = []
    script_list = self.objectValues(portal_type='Workflow Script')
    script_prop_id_to_show = sorted(['body', 'parameter_signature','proxy_roles'])
    for sdef in script_list:
      script_reference_list.append(sdef.getReference())
    scripts = SubElement(interaction_workflow, 'scripts', attrib=dict(script_list=str(script_reference_list),
                        number_of_element=str(len(script_reference_list))))
    for sdef in script_list:
      script = SubElement(scripts, 'script', attrib=dict(reference=sdef.getReference(),
        portal_type=sdef.getPortalType()))
      for property_id in script_prop_id_to_show:
        if property_id == 'proxy_roles':
          property_value = tuple(sdef.getProperty('proxy_role_list'))
          property_type = sdef.getPropertyType('proxy_role_list')
        else:
          property_value = sdef.getProperty(property_id)
          property_type = sdef.getPropertyType(property_id)
        sub_object = SubElement(script, property_id, attrib=dict(type=property_type))
        if property_value is None or property_value == [] or property_value == ():
          property_value = ''
        sub_object.text = str(property_value)

    # return xml object
    if return_as_object:
      return root
    return etree.tostring(root, encoding='utf-8',
                          xml_declaration=True, pretty_print=True)
Пример #25
0
class ComponentMixin(PropertyRecordableMixin, Base):
  """
  Mixin used for all ZODB Components. Most of the code is generic, thus actual
  ZODB Components should have almost nothing to defined...

  From a security point of view, only Developer Role defined on Component Tool
  can manage Components (as exec is used and anything potentially damaging
  could be done on the filesystem), while only Manager or Developer Roles can
  reset Component Packages (see ERP5Type.Permissions). All the permissions are
  defined on Component Tool itself and newly created Components just inherits
  permissions defined on the former.

  The Developer Role is not a typical Role as only users defined in Zope
  configuration can be added to this Role (which is displayed in the list of
  available Roles in ZMI). This is achieved by two monkey patches
  (ERP5Type.patches.{User,PropertiedUser}) and modifications in
  ERP5Security.ERP5UserFactory.

  Component source code is checked upon modification of text_content property
  whatever its Workflow state (checkSourceCode). On validated and modified
  state, checkConsistency() is called to check id, reference, version and
  errors/warnings messages (set when the Component is modified).
  """
  __metaclass__ = RecordablePropertyMetaClass

  isPortalContent = 1
  isRADContent = 1
  isDelivery = ConstantGetter('isDelivery', value=True)

  # Declarative security
  security = ClassSecurityInfo()
  security.declareObjectProtected(Permissions.AccessContentsInformation)

  # Declarative properties
  property_sheets = ('Base',
                     'XMLObject',
                     'CategoryCore',
                     'DublinCore',
                     'Version',
                     'Reference',
                     'TextDocument',
                     'Component')

  _recorded_property_name_tuple = (
    'reference',
    'version',
    'text_content')

  _message_reference_not_set = "Reference must be set"
  _message_invalid_reference = "Reference cannot end with '_version' or "\
      "start with '_' or be equal to find_module, load_module or reset"

  _message_version_not_set = "Version must be set"
  _message_invalid_version = "Version cannot start with '_'"
  _message_duplicated_version_reference = "${id} is validated has the same "\
       "Reference and Version"

  _message_text_content_not_set = "No source code"
  _message_text_content_error = "Error in Source Code: ${error_message}"

  security.declareProtected(Permissions.ModifyPortalContent, 'checkConsistency')
  def checkConsistency(self, *args, **kw):
    """
    Check the consistency of the Component upon validate or when being
    modified after being validated.

    Some keywords are forbidden for reference and version. As Version package
    always ends with '_version', reference is checked more carefully to avoid
    clashing with existing method names (such as the ones required for PEP
    302).

    XXX-arnau: separate Constraint class?
    """
    error_list = super(ComponentMixin, self).checkConsistency(*args, **kw)
    object_relative_url = self.getRelativeUrl()
    reference = self.getReference()
    reference_has_error = False
    if not reference:
      reference_has_error = True
      error_list.append(
        ConsistencyMessage(self,
                           object_relative_url,
                           message=self._message_reference_not_set,
                           mapping={}))

    elif (reference.endswith('_version') or
          reference[0] == '_' or
          reference in ('find_module', 'load_module', 'reset')):
      reference_has_error = True
      error_list.append(
        ConsistencyMessage(self,
                           object_relative_url,
                           message=self._message_invalid_reference,
                           mapping={}))

    version = self.getVersion()
    if not version:
      error_list.append(ConsistencyMessage(self,
                                           object_relative_url,
                                           message=self._message_version_not_set,
                                           mapping={}))
    elif version[0] == '_':
      error_list.append(ConsistencyMessage(self,
                                           object_relative_url,
                                           message=self._message_invalid_version,
                                           mapping={}))
    else:
      package = __import__(self._getDynamicModuleNamespace(), globals(),
                           fromlist=[self._getDynamicModuleNamespace()], level=0)
      component_id = None
      component_uid = None
      from Products.ERP5Type.dynamic import aq_method_lock
      with aq_method_lock:
        component_id_uid_tuple = package._registry_dict.get(
          self.getReference(), {}).get(self.getVersion(), None)
        if component_id_uid_tuple:
          component_id, component_uid = component_id_uid_tuple

      if (component_id is not None and component_uid is not None and
          not reference_has_error and
          component_uid != self.getUid() and component_id != self.getId()):
        error_list.append(
          ConsistencyMessage(self,
                             object_relative_url,
                             message=self._message_duplicated_version_reference,
                             mapping=dict(id=component_id)))

    text_content = self.getTextContent()
    if not text_content:
      error_list.append(
          ConsistencyMessage(self,
                             object_relative_url=object_relative_url,
                             message=self._message_text_content_not_set,
                             mapping={}))
    else:
      for error_message in self.getTextContentErrorMessageList():
        error_list.append(ConsistencyMessage(self,
                                             object_relative_url=object_relative_url,
                                             message=self._message_text_content_error,
                                             mapping=dict(error_message=error_message)))

    return error_list

  security.declareProtected(Permissions.ModifyPortalContent,
                            'checkConsistencyAndValidate')
  def checkConsistencyAndValidate(self):
    """
    When a Component is in validated or modified validation state and it is
    modified, modified state is set then this checks whether the Component can
    be validated again if checkConsistency returns no error. Otherwise, it
    stays in modified state and previously validated values are used for
    reference, version and text_content
    """
    if not self.checkConsistency():
      text_content = self.getTextContent()
      # Even if pylint should report all errors, make sure that there is no
      # error when executing the source code pylint before validating
      try:
        exec text_content in {}
      except BaseException, e:
        self.setErrorMessageList(self.getTextContentErrorMessageList() +
                                 [str(e)])
      else:
        for property_name in self._recorded_property_name_tuple:
          self.clearRecordedProperty(property_name)

        self.validate()
Пример #26
0
 def _setNonIndexable(self):
   self.isIndexable = ConstantGetter('isIndexable', value=False)
Пример #27
0
class SyncMLSignature(XMLObject):
    """
  A Signature represent a document that is synchronized

  It contains as attribute the xml representation of the object which is used
  to generate the diff of the object between two synchronization

  It also contains the list of conflict as sub-objects when it happens
  """
    meta_type = 'ERP5 Signature'
    portal_type = 'SyncML Signature'
    isIndexable = ConstantGetter('isIndexable', value=False)

    security = ClassSecurityInfo()
    security.declareObjectProtected(Permissions.AccessContentsInformation)

    # Declarative properties
    property_sheets = (PropertySheet.Base, PropertySheet.XMLObject,
                       PropertySheet.CategoryCore, PropertySheet.DublinCore,
                       PropertySheet.Reference, PropertySheet.Data,
                       PropertySheet.Document, PropertySheet.SyncMLSignature)

    security.declareProtected(Permissions.ModifyPortalContent, 'synchronize')

    def synchronize(self):
        """
    This is call when subscription get confirmation of the data synchronization
    This copy & reset some properties if needed
    """
        edit_kw = {}
        temporary_data = self.getTemporaryData()
        if temporary_data is not None:
            # This happens when we have sent the xml
            # and we just get the confirmation
            self.setData(temporary_data)
            edit_kw["temporary_data"] = None
        if self.isForce():
            edit_kw["force"] = False
        if self.hasPartialData():
            edit_kw["partial_data"] = None
        if self.hasSubscriberXupdate():
            edit_kw["subscriber_xupdate"] = None
        if self.hasPublisherXupdate():
            edit_kw["publisher_xupdate"] = None

        if len(edit_kw):
            self.edit(**edit_kw)

    security.declareProtected(Permissions.ModifyPortalContent, 'setData')

    def setData(self, value):
        """
    Set the XML corresponding to the object
    """
        if value:
            # convert the string to Pdata
            pdata_wrapper = PdataHelper(self.getPortalObject(), value)
            self._setData(pdata_wrapper)
            self.setTemporaryData(
                None)  # We make sure that the data will not be erased
            self.setContentMd5(pdata_wrapper.getContentMd5())
        else:
            self._setData(None)
            self.setContentMd5(None)

    security.declareProtected(Permissions.AccessContentsInformation, 'getData')

    def getData(self, default=_MARKER):
        """
    Get the XML corresponding to the object
    """
        if self.hasData():
            return str(self._baseGetData())
        elif default is _MARKER:
            return self._baseGetData()
        else:
            return self._baseGetData(default)

    security.declareProtected(Permissions.ModifyPortalContent,
                              'setTemporaryData')

    def setTemporaryData(self, value):
        """
    This is the xml temporarily saved, it will
    be stored with setXML when we will receive
    the confirmation of synchronization
    """
        if value:
            self._setTemporaryData(PdataHelper(self.getPortalObject(), value))
        else:
            self._setTemporaryData(None)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getTemporaryData')

    def getTemporaryData(self, default=_MARKER):
        """
    Return the temp xml as string
    """
        if self.hasTemporaryData():
            return str(self._baseGetTemporaryData())
        elif default is _MARKER:
            return self._baseGetTemporaryData()
        else:
            return self._baseGetTemporaryData(default)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'checkMD5')

    def checkMD5(self, xml_string):
        """
    Check if the given md5_object returns the same things as the one stored in
    this signature, this is very usefull if we want to know if an objects has
    changed or not
    Returns 1 if MD5 are equals, else it returns 0
    """
        if isinstance(xml_string, unicode):
            xml_string = xml_string.encode('utf-8')
        return md5(xml_string).hexdigest() == self.getContentMd5()

    security.declareProtected(Permissions.ModifyPortalContent,
                              'setPartialData')

    def setPartialData(self, value):
        """
    Set the partial string we will have to deliver in the future
    """
        if value:
            if not isinstance(value, PdataHelper):
                value = PdataHelper(self.getPortalObject(), value)
            self._setPartialData(value)
            self.setLastDataPartialData(value.getLastPdata())
        else:
            self._setPartialData(None)
            self.setLastDataPartialData(None)

    security.declareProtected(Permissions.ModifyPortalContent, 'setLastData')

    def setLastData(self, value):
        """
      This is the xml temporarily saved, it will be stored with setXML when we
      will receive the confirmation of synchronization
    """
        if value:
            self._setLastData(PdataHelper(self.getPortalObject(), value))
        else:
            self._setLastData(None)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getPartialData')

    def getPartialData(self, default=_MARKER):
        """
    Return the patial xml as string
    """
        if self.hasPartialData():
            return str(self._baseGetPartialData())
        elif default is _MARKER:
            return self._baseGetPartialData()
        else:
            return self._baseGetPartialData(default)

    security.declareProtected(Permissions.ModifyPortalContent,
                              'appendPartialData')

    def appendPartialData(self, value):
        """
    Append the partial string we will have to deliver in the future
    """
        if value:
            if not isinstance(value, PdataHelper):
                value = PdataHelper(self.getPortalObject(), value)
            last_data = value.getLastPdata()
            if self.hasLastDataPartialData():
                last_data_partial_data = self.getLastDataPartialData()
                last_data_partial_data.next = value._data
                self.setLastDataPartialData(last_data_partial_data)
            else:
                self.setPartialData(value)
            self.setLastDataPartialData(last_data)

    security.declareProtected(Permissions.ModifyPortalContent,
                              'getFirstPdataChunk')

    def getFirstPdataChunk(self, max_len):
        """
    """
        partial_data = self._baseGetPartialData()
        chunk = partial_data[:max_len]
        rest_in_queue = partial_data[max_len:]
        if rest_in_queue is not None:
            self.setPartialData(rest_in_queue)
        return str(chunk)

    security.declareProtected(Permissions.ModifyPortalContent,
                              'setSubscriberXupdate')

    def setSubscriberXupdate(self, value):
        """
    This is the xml temporarily saved, it will be stored with setXML when we
    will receive the confirmation of synchronization
    """
        if value:
            self._setSubscriberXupdate(
                PdataHelper(self.getPortalObject(), value))
        else:
            self._setSubscriberXupdate(None)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getSubscriberXupdate')

    def getSubscriberXupdate(self, default=_MARKER):
        """
    Return the patial xml as string
    """
        if self.hasSubscriberXupdate():
            return str(self._baseGetSubscriberXupdate())
        elif default is _MARKER:
            return self._baseGetSubscriberXupdate()
        else:
            return self._baseGetSubscriberXupdate(default)

    security.declareProtected(Permissions.ModifyPortalContent,
                              'setPublisherXupdate')

    def setPublisherXupdate(self, value):
        """
    This is the xml temporarily saved, it will be stored with setXML when we
    will receive the confirmation of synchronization
    """
        if value:
            self._setPublisherXupdate(
                PdataHelper(self.getPortalObject(), value))
        else:
            self._setPublisherXupdate(None)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getPublisherXupdate')

    def getPublisherXupdate(self, default=_MARKER):
        """
    Return the partial xml as string
    """
        if self.hasPublisherXupdate():
            return str(self._baseGetPublisherXupdate())
        elif default is _MARKER:
            return self._baseGetPublisherXupdate()
        else:
            return self._baseGetPublisherXupdate(default)

    security.declareProtected(Permissions.ModifyPortalContent,
                              'getConflictList')

    def getConflictList(self):
        """
    Return the actual action for a partial synchronization
    """
        return self.contentValues()

    security.declareProtected(Permissions.ModifyPortalContent, 'delConflict')

    def delConflict(self, conflict):
        """
    Delete provided conflict object
    """
        self.manage_delObjects([
            conflict.getId(),
        ])
Пример #28
0
class Event(Movement, EmailDocument, AcknowledgeableMixin):
    """
    Event is the base class for all events in ERP5.

    Event objects include emails, phone calls,

    The purpose of an Event object is to keep track
    of the interface between the ERP and third parties.

    Events have a start and stop date.
  """

    meta_type = 'ERP5 Event'
    portal_type = 'Event'
    # XXX this is hack so we can search event by delivery.start_date
    isDelivery = ConstantGetter('isDelivery', value=True)

    # Declarative security
    security = ClassSecurityInfo()
    security.declareObjectProtected(Permissions.AccessContentsInformation)

    # Declarative properties
    property_sheets = (PropertySheet.Base, PropertySheet.XMLObject,
                       PropertySheet.CategoryCore, PropertySheet.Document,
                       PropertySheet.DublinCore, PropertySheet.Task,
                       PropertySheet.Url, PropertySheet.TextDocument,
                       PropertySheet.Arrow, PropertySheet.Movement,
                       PropertySheet.Event, PropertySheet.Delivery,
                       PropertySheet.ItemAggregation)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'isAccountable')

    def isAccountable(self):
        """Events are accountable
    """
        return 1

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getQuantity')

    def getQuantity(self, default=1.):
        """Quantity is by default 1.0 on events.
    """
        return self._baseGetQuantity(default)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getExplanationValue')

    def getExplanationValue(self):
        """
      An event is it's own explanation
    """
        return self

    security.declareProtected(Permissions.UseMailhostServices, 'send')

    def send(self,
             from_url=None,
             to_url=None,
             reply_url=None,
             subject=None,
             body=None,
             attachment_format=None,
             attachment_list=None,
             download=False,
             **kw):
        """
      Make the send method overridable by typed based script
      so that special kinds of events can use a different gateway
      to send messages. This is useful for example to send
      faxes through fax server or to send letters by printing
      them to the printer or to send SMS through a custom
      gateway. In the most usual case, sending will only consist
      in changing the destination.
    """
        send_script = self._getTypeBasedMethod('send')
        if send_script is None:
            return

        return send_script(from_url, to_url, reply_url, subject, body,
                           attachment_format, attachment_list, download, **kw)
Пример #29
0
class WizardTool(BaseTool):
    """ WizardTool is able to generate custom business templates. """

    id = 'portal_wizard'
    meta_type = 'ERP5 Wizard Tool'
    portal_type = 'Wizard Tool'
    isPortalContent = ConstantGetter('isPortalContent', value=True)
    property_sheets = ()
    security = ClassSecurityInfo()
    security.declareProtected(Permissions.ManagePortal, 'manage_overview')
    manage_overview = DTMLFile('explainWizardTool', _dtmldir)

    # Stop traversing a concatenated path after the proxy method.
    def __before_publishing_traverse__(self, self2, request):
        path = request['TraversalRequestNameStack']
        if path and path[-1] == 'proxy':
            subpath = path[:-1]
            subpath.reverse()
            request.set('traverse_subpath', subpath)
            # initialize our root proxy URL which we use for a referer
            global referer
            path[:-1] = []
            if referer is None:
                referer = '%s/portal_wizard/proxy/%s/view' % (
                    self.getPortalObject().absolute_url(), '/'.join(
                        subpath[:3]))

    def _getProxyURL(self, subpath='', query=''):
        # Helper method to construct an URL appropriate for proxying a request.
        # This makes sure that URLs generated by absolute_url at a remote site
        # will be always towards the proxy method again.
        #
        # Note that the code assumes that VirtualHostBase is visible. The setting
        # of a front-end server must allow this.
        #
        # This should generate an URL like this:
        #
        # http://remotehost:9080/VirtualHostBase/http/localhost:8080/VirtualHostRoot/_vh_erp5/_vh_portal_wizard/_vh_proxy/erp5/person_module/2
        part_list = []

        server_url = self.getServerUrl().rstrip('/')
        part_list.append(server_url)

        part_list.append('VirtualHostBase')

        portal_url = self.getPortalObject().absolute_url()
        scheme, rest = urllib.splittype(portal_url)
        addr, path = urllib.splithost(rest)
        host, port = urllib.splitnport(addr, scheme == 'http' and 80 or 443)
        part_list.append(scheme)
        part_list.append('%s:%s' % (host, port))

        part_list.append('VirtualHostRoot')

        method_path = self.absolute_url_path() + '/proxy'
        part_list.extend(('_vh_' + p for p in method_path.split('/') if p))

        server_root = self.getServerRoot().strip('/')

        if isinstance(subpath, (list, tuple)):
            subpath = '/'.join(subpath)

        if not subpath.startswith(server_root):
            part_list.append(server_root)

        part_list.append(subpath)

        url = '/'.join((p for p in part_list if p))
        if query:
            url = url + '?' + query
        return url

    def _getSubsribedUserAndPassword(self):
        """Retrieve the username and password for the subscription from
    the system."""
        user = CachingMethod(self.getExpressConfigurationPreference,
                             'WizardTool_preferred_express_user_id',
                             cache_factory='erp5_content_long')(
                                 'preferred_express_user_id', '')
        pw = CachingMethod(self.getExpressConfigurationPreference,
                           'WizardTool_preferred_express_password',
                           cache_factory='erp5_content_long')(
                               'preferred_express_password', '')
        return (user, pw)

    # This is a custom opener director for not handling redirections
    # and errors automatically. This is necessary because the proxy
    # should pass all results to a client as they are.
    simple_opener_director = urllib2.OpenerDirector()
    for name in ('ProxyHandler', 'UnknownHandler', \
                 'HTTPHandler', 'FTPHandler',
                 'FileHandler', 'HTTPSHandler',):
        handler = getattr(urllib2, name, None)
        if handler is not None:
            simple_opener_director.add_handler(handler())
    # add cookie support
    simple_opener_director.add_handler(urllib2.HTTPCookieProcessor(cookiejar))

    security.declareProtected(Permissions.View, 'proxy')

    def proxy(self, **kw):
        """Proxy a request to a server."""
        global cookiejar, referer, last_loggedin_user_and_password
        if self.REQUEST['REQUEST_METHOD'] != 'GET':
            # XXX this depends on the internal of HTTPRequest.
            pos = self.REQUEST.stdin.tell()
            self.REQUEST.stdin.seek(0)
            # XXX if filesize is too big, this might cause a problem.
            data = self.REQUEST.stdin.read()
            self.REQUEST.stdin.seek(pos)
        else:
            data = None

        content_type = self.REQUEST.get_header('content-type')

        # XXX if ":method" trick is used, then remove it from subpath.
        if self.REQUEST.traverse_subpath:
            if data is not None:
                user_input = data
            else:
                user_input = self.REQUEST.QUERY_STRING
            if user_input:
                mark = ':method'
                content_type_value = None
                content_type_dict = None
                if content_type:
                    content_type_value, content_type_dict = cgi.parse_header(
                        content_type)
                if content_type_value == 'multipart/form-data':
                    fp = StringIO(user_input)
                    user_input_dict = cgi.parse_multipart(
                        fp, content_type_dict)
                else:
                    user_input_dict = cgi.parse_qs(user_input)

                for i in user_input_dict:
                    if i.endswith(mark):
                        method_name = i[:-len(mark)]
                        method_path = method_name.split('/')
                        if self.REQUEST.traverse_subpath[
                                -len(method_path):] == method_path:
                            del self.REQUEST.traverse_subpath[-len(method_path
                                                                   ):]
                            break

        url = self._getProxyURL(self.REQUEST.traverse_subpath,
                                self.REQUEST['QUERY_STRING'])

        # XXX this will send the password unconditionally!
        # I hope https will be good enough.
        header_dict = {}
        user_and_password = self._getSubsribedUserAndPassword()
        if (len(user_and_password) == 2 and user_and_password[0]
                and user_and_password[1]):
            if user_and_password != last_loggedin_user_and_password:
                # credentials changed we need to renew __ac cookie from server as well
                cookiejar.clear()
            # try login to server only once using cookie method
            if not _isUserAcknowledged(cookiejar):
                server_url = self.getServerUrl()
                f = _getAcCookieFromServer('%s/WebSite_login' % server_url,
                                           self.simple_opener_director,
                                           cookiejar, user_and_password[0],
                                           user_and_password[1])
                # if server doesn't support cookie authentication try basic
                # authentication
                if not _isUserAcknowledged(cookiejar):
                    auth = 'Basic %s' % base64.standard_b64encode(
                        '%s:%s' % user_and_password)
                    header_dict['Authorization'] = auth
                # save last credentials we passed to server
                last_loggedin_user_and_password = user_and_password
        if content_type:
            header_dict['Content-Type'] = content_type

        # send locally saved cookies to remote web server
        if not header_dict.has_key('Cookie'):
            header_dict['Cookie'] = ''
        for cookie in cookiejar:
            # unconditionally send all cookies (no matter if expired or not) as URL
            # is always the same
            header_dict['Cookie'] += '%s=%s;' % (cookie.name, cookie.value)
        # include cookies from local browser (like show/hide tabs) which are set
        # directly by client JavaScript code (i.e. not sent from server)
        for cookie_name, cookie_value in self.REQUEST.cookies.items():
            header_dict['Cookie'] += '%s=%s;' % (cookie_name, cookie_value)

        # add HTTP referer (especially useful in Localizer when changing language)
        header_dict['REFERER'] = self.REQUEST.get('HTTP_REFERER',
                                                  None) or referer
        request = urllib2.Request(url, data, header_dict)
        f = self.simple_opener_director.open(request)

        try:
            data = f.read()
            metadata = f.info()
            response = self.REQUEST.RESPONSE
            if f.code > 300 and f.code < 400:
                # adjust return url which my contain proxy URLs as arguments
                location = metadata.getheader('location')
                if location is not None:
                    parsed_url = list(urlparse(location))
                    local_site_url_prefix = urllib.quote(
                        '%s/portal_wizard/proxy' %
                        self.getPortalObject().absolute_url())
                    remote_url_parsed = urlparse(self.getServerUrl())
                    remote_site_url_prefix = '%s://%s/kb' % (
                        remote_url_parsed[0], remote_url_parsed[1])
                    # fix arguments for returned location URL
                    parsed_url[4] = parsed_url[4].replace(
                        local_site_url_prefix, remote_site_url_prefix)
                    response['location'] = urlunparse(parsed_url)

            response.setStatus(f.code, f.msg)
            response.setHeader('content-type',
                               metadata.getheader('content-type'))
            # FIXME this list should be confirmed with the RFC 2616.
            for k in ('uri', 'cache-control', 'last-modified', 'etag',
                      'if-matched', 'if-none-match', 'if-range',
                      'content-language', 'content-range'
                      'content-location', 'content-md5', 'expires',
                      'content-encoding', 'vary', 'pragma',
                      'content-disposition', 'content-length', 'age'):
                if k in metadata:
                    response.setHeader(k, metadata.getheader(k))
            return data
        finally:
            f.close()

    def _getRemoteWitchTool(self, url, user_name=None, password=None):
        """
      Return remote portal_witch tool interface.
    """
        handle = self.getPortalObject().portal_web_services.connect(
            url=url,
            user_name=user_name,
            password=password,
            transport='xml-rpc')
        return handle.portal_witch

    def callRemoteProxyMethod(self,
                              distant_method,
                              server_url=None,
                              use_cache=1,
                              ignore_exceptions=1,
                              **kw):
        """ Call proxy method on server. """
        configurator_user_preferred_language = self\
            .getConfiguratorUserPreferredLanguage()

        def wrapper(distant_method, **kw):
            return self._callRemoteMethod(distant_method,
                                          use_proxy=1,
                                          ignore_exceptions=ignore_exceptions,
                                          **kw)['data']

        if use_cache:
            wrapper = CachingMethod(
                wrapper,
                id='callRemoteProxyMethod_%s_%s' %
                (distant_method, configurator_user_preferred_language),
                cache_factory='erp5_ui_medium')
        rc = wrapper(distant_method, **kw)
        return rc

    def _callRemoteMethod(self,
                          distant_method,
                          server_url=None,
                          use_proxy=0,
                          ignore_exceptions=1,
                          **kw):
        """ Call remote method on server and get result. """
        result_call = GeneratorCall()
        user_name = None
        password = None
        if server_url is None:
            # calculate it
            server_url = self.getServerUrl() + self.getServerRoot()
            # include authentication if possible
            user_and_password = self._getSubsribedUserAndPassword()
            if (len(user_and_password) == 2 and user_and_password[0]
                    and user_and_password[1]):
                user_name, password = user_and_password
        witch_tool = self._getRemoteWitchTool(server_url, user_name, password)
        parameter_dict = self.REQUEST.form.copy()
        if use_proxy:
            # add remote method arguments
            parameter_dict['method_id'] = distant_method
            parameter_dict['method_kw'] = kw
            distant_method = 'proxyMethodHandler'
        ## add client arguments
        self._updateParameterDictWithServerInfo(parameter_dict)
        ## handle file upload
        self._updateParameterDictWithFileUpload(parameter_dict)
        message = None
        ## call remote method
        try:
            method = getattr(witch_tool, distant_method)
            html = method(parameter_dict)
        except socket.error, message:
            html = _generateErrorXML("""Cannot contact the server: %s.
                                  Please check your network settings.""" %
                                     server_url)
            zLOG.LOG('Wizard Tool socket error', zLOG.ERROR, message)
            result_call.update({
                "command": "show",
                "data": html,
                "next": None,
                "previous": None
            })
        except xmlrpclib.ProtocolError, message:
            html = _generateErrorXML("""The server %s refused to reply.
                                  Please contact [email protected]""" %
                                     server_url)
            zLOG.LOG('Wizard Tool xmlrpc protocol error', zLOG.ERROR, message)
            result_call.update({
                "command": "show",
                "data": html,
                "next": None,
                "previous": None
            })
Пример #30
0
from Products.ERP5Type.dynamic.accessor_holder import createAllAccessorHolderList
from Products.ERP5Type.Accessor.Constant import Getter as ConstantGetter
from Products.ERP5Type.TransactionalVariable import TransactionalResource

from zLOG import LOG, ERROR, INFO, WARNING, PANIC

ACQUIRE_LOCAL_ROLE_GETTER_ID = '_getAcquireLocalRoles'
ACQUIRE_LOCAL_ROLE_GETTER_DICT = {
    acquire_local_role: type(
        'GetAcquireLocalRolesMixIn',
        (object, ),
        {
            ACQUIRE_LOCAL_ROLE_GETTER_ID:
            ConstantGetter(
                id=ACQUIRE_LOCAL_ROLE_GETTER_ID,
                key=None,
                value=acquire_local_role,
            ),
        },
    )
    for acquire_local_role in (False, True)
}


def _importClass(classpath):
    try:
        module_path, class_name = classpath.rsplit('.', 1)
        module = __import__(module_path, {}, {}, (module_path, ))
        klass = getattr(module, class_name)

        # XXX is this required? (here?)