composition.py 7.5 KB
Newer Older
1 2 3 4
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (c) 2010 Nexedi SA and Contributors. All Rights Reserved.
5
#                    Julien Muchembled <jm@nexedi.com>
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# guarantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
##############################################################################

from AccessControl import ClassSecurityInfo
from Acquisition import aq_base
from Products.ERP5Type import Permissions
33
from Products.ERP5Type.Cache import transactional_cached
34 35 36
from Products.ERP5.Document.Predicate import Predicate
from Products.ZSQLCatalog.SQLCatalog import Query, ComplexQuery

37 38

@transactional_cached()
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
def _getEffectiveModel(self, start_date=None, stop_date=None):
  """Return the most appropriate model using effective_date, expiration_date
  and version number.
  An effective model is a model which start and stop_date are equal (or
  excluded) to the range of the given start and stop_date and with the
  higher version number (if there is more than one)

  XXX Should we moved this function to a class ? Which one ?
      What about reusing IVersionable ?
  """
  reference = self.getReference()
  if not reference:
    return self

  query_list = [Query(reference=reference),
                Query(portal_type=self.getPortalType()),
                Query(validation_state=('deleted', 'invalidated'),
                      operator='NOT')]
  if start_date is not None:
    query_list.append(ComplexQuery(Query(effective_date=None),
                                   Query(effective_date=start_date,
                                         range='ngt'),
                                   logical_operator='OR'))
  if stop_date is not None:
    query_list.append(ComplexQuery(Query(expiration_date=None),
                                   Query(expiration_date=stop_date,
                                         range='min'),
                                   logical_operator='OR'))

  # XXX What to do the catalog returns nothing (either because 'self' was just
  #     created and not yet indexed, or because it was invalidated) ?
  #     For the moment, we raise.
  model_list = self.getPortalObject().portal_catalog.unrestrictedSearchResults(
      query=ComplexQuery(logical_operator='AND', *query_list),
      sort_on=(('version', 'descending'),))
  return model_list[0].getObject()

76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141

@transactional_cached()
def _findPredicateList(*container_list):
  predicate_list = []
  reference_dict = {}
  line_count = 0
  for container in container_list:
    for ob in container.contentValues():
      if isinstance(ob, Predicate):
        # reference is used to hide lines on farther containers
        reference = ob.getProperty('reference')
        if reference:
          reference_set = reference_dict.setdefault(ob.getPortalType(), set())
          if reference in reference_set:
            continue
          reference_set.add(reference)
        id = str(line_count)
        line_count += 1
        predicate_list.append(aq_base(ob.asContext(id=id)))
  return predicate_list


class _asComposedDocument(object):
  """Return a temporary object which is the composition of all effective models

  The returned value is a temporary copy of the given object. The list of all
  effective models (specialise values) is stored in a private attribute.
  Collecting predicates (from effective models) is done lazily. Predicates can
  be accessed through standard Folder API (ex: contentValues).
  """

  def __new__(cls, orig_self):
    if '_effective_model_list' in orig_self.__dict__:
      return orig_self # if asComposedDocument is called on a composed
                       # document after any access to its subobjects
    self = orig_self.asContext()
    self._initBTrees()
    base_class = self.__class__
    # this allows to intercept first access to '_folder_handler'
    self.__class__ = type(base_class.__name__, (cls, base_class),
                          {'__module__': base_class.__module__})
    self._effective_model_list = orig_self._findEffectiveSpecialiseValueList()
    return self

  def __init__(self, orig_self):
    # __new__ does not call __init__ because returned object
    # is wrapped in an acquisition context.
    assert False

  def asComposedDocument(self):
    return self # if asComposedDocument is called on a composed
                # document before any access to its subobjects

  @property
  def _folder_handler(self):
    # restore the original class
    # because we don't need to hook _folder_handler anymore
    self.__class__ = self.__class__.__bases__[1]
    # we filter out objects without any subobject to make the cache of
    # '_findPredicateList' useful. Otherwise, the key would be always different
    # (starting with 'orig_self').
    for ob in _findPredicateList(*filter(None, self._effective_model_list)):
      self._setOb(ob.id, ob)
    return self._folder_handler


142 143 144 145 146 147 148 149 150 151
class CompositionMixin:
  """
  """

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

  security.declareProtected(Permissions.AccessContentsInformation,
                            'asComposedDocument')
152 153 154 155
  asComposedDocument = transactional_cached()(_asComposedDocument)

  # XXX add accessors to get properties from '_effective_model_list' ?
  #     (cf PaySheetModel)
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174

  def _findEffectiveSpecialiseValueList(self):
    """Return a list of effective specialised objects that is the
    inheritance tree.
    An effective object is an object which have start_date and stop_date
    included to the range of the given parameters start_date and stop_date.

    This algorithm uses Breadth First Search.
    """
    start_date = self.getStartDate()
    stop_date = self.getStopDate()
    def getEffectiveModel(model):
      return _getEffectiveModel(model, start_date, stop_date)
    model_list = [self]
    model_set = set(model_list)
    model_index = 0
    while model_index < len(model_list):
      model = model_list[model_index]
      model_index += 1
175 176
      # we don't use getSpecialiseValueList to avoid acquisition on the parent
      for model in map(getEffectiveModel, model.getValueList('specialise')):
177 178
        if model not in model_set:
          model_set.add(model)
179
          if 1: #model.test(self): # XXX
180
            model_list.append(model)
181 182 183 184 185 186 187 188
    try:
      parent_asComposedDocument = self.getParentValue().asComposedDocument
    except AttributeError:
      pass
    else:
      model_list += [model
        for model in parent_asComposedDocument()._effective_model_list
        if model not in model_set]
189
    return model_list