DomainTool.py 18 KB
Newer Older
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1 2
##############################################################################
#
Romain Courteaud's avatar
Romain Courteaud committed
3
# Copyright (c) 2002, 2005 Nexedi SARL and Contributors. All Rights Reserved.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
4
#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
5
#                    Sebastien Robin <seb@nexedi.com>
Jean-Paul Smets's avatar
Jean-Paul Smets committed
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
##############################################################################

30
from collections import defaultdict
Jean-Paul Smets's avatar
Jean-Paul Smets committed
31
from AccessControl import ClassSecurityInfo
32
from Products.ERP5Type.Globals import InitializeClass, DTMLFile
Jean-Paul Smets's avatar
Jean-Paul Smets committed
33
from Products.ERP5Type import Permissions
34
from Products.ERP5 import _dtmldir
Jean-Paul Smets's avatar
Jean-Paul Smets committed
35
from Products.ERP5Type.Tool.BaseTool import BaseTool
36
from Products.ZSQLCatalog.SQLCatalog import SQLQuery, Query, ComplexQuery
37
from zLOG import LOG
38
from DateTime import DateTime
Jean-Paul Smets's avatar
Jean-Paul Smets committed
39

40 41
_MARKER = []

Jean-Paul Smets's avatar
Jean-Paul Smets committed
42 43
class DomainTool(BaseTool):
    """
Jean-Paul Smets's avatar
Jean-Paul Smets committed
44 45
        A tool to define reusable ranges and subranges through
        predicate trees
Jean-Paul Smets's avatar
Jean-Paul Smets committed
46 47 48 49
    """
    id = 'portal_domains'
    meta_type = 'ERP5 Domain Tool'    
    portal_type     = 'Domain Tool'
Romain Courteaud's avatar
Romain Courteaud committed
50
    allowed_types   = ('ERP5 Domain', )
Jean-Paul Smets's avatar
Jean-Paul Smets committed
51 52 53 54

    # Declarative Security
    security = ClassSecurityInfo()

Romain Courteaud's avatar
Romain Courteaud committed
55 56
    security.declareProtected(Permissions.ManagePortal, 'manage_overview')
    manage_overview = DTMLFile('explainDomainTool', _dtmldir)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
57

Romain Courteaud's avatar
Romain Courteaud committed
58 59
    # XXX FIXME method should not be public 
    # (some users are not able to see resource's price)
60
    security.declarePublic('searchPredicateList')
61 62 63 64 65 66 67 68 69
    def searchPredicateList(self, *args, **kw):
      return self._searchPredicateList(restricted=True, *args, **kw)

    def _searchPredicateList(self, context, test=1, sort_method=None,
                             ignored_category_list=None,
                             tested_base_category_list=None,
                             filter_method=None, acquired=1,
                             strict=True, sort_key_method=None, query=None,
                             restricted=False, **kw):
70 71 72 73
      # XXX: about "strict" parameter: This is a transition parameter,
      # allowing someone hitting a bug to revert to original behaviour easily.
      # It is not a correct name, as pointed out by Jerome. But instead of
      # searching for another name, it would be much better to just remove it.
74
      """
Romain Courteaud's avatar
Romain Courteaud committed
75 76
      Search all predicates which corresponds to this particular 
      context.
77

Yoshinori Okuji's avatar
Yoshinori Okuji committed
78 79
      - sort_method parameter should not be used, if possible, because
        it can be very slow. Use sort_key_method instead.
80 81 82

      - sort_key_method parameter is passed to list.sort as key parameter if it
        is not None. This allows to sort the list of predicates found. The most
83 84
        important predicate is the first one in the list.

Romain Courteaud's avatar
Romain Courteaud committed
85
      - ignored_category_list:  this is the list of category that we do
86 87 88
        not want to test. For example, we might want to not test the 
        destination or the source of a predicate.

89 90 91 92
      - 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.

Romain Courteaud's avatar
Romain Courteaud committed
93
      - the acquired parameter allows to define if we want to use
94
        acquisition for categories. By default we want.
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109

      - strict: if True, generate SQL which will match predicates matching
        all those categories at the same time, except for categories they do
        not check at all. Example:
          Predicate_1 checks foo/bar
          Predicate_2 checks foo/baz region/somewhere
          Predicate_3 checks foo/bar region/somewhere
          When called with category list ['foo/bar', 'region/somewhere'] and
          strict parameter to True, it will return [Predicate_1, Predicate_3].
          With strict to False or by not giving a category list, it would also
          return Predicate_2, because it matches on one criterion out of the 2
          it checks.
        Note that it changes the value returned by this function if it was
        invoked with "test=False" value. Otherwise, it should only change
        execution duration.
110
      """
111 112 113
      portal = self.getPortalObject()
      portal_catalog = portal.portal_catalog
      portal_categories = portal.portal_categories
114
      # Search the columns of the predicate table
115 116 117 118 119 120 121 122
      range_column_set = set()
      query_list = [] if query is None else [query]
      for column in portal_catalog.getColumnIds():
        if column[:10] == 'predicate.' and \
           column[-10:] in ('_range_min', '_range_max'):
          property_name = column[10:-10]
          if property_name not in range_column_set:
            range_column_set.add(property_name)
Romain Courteaud's avatar
Romain Courteaud committed
123
            # We have to check a range property
124 125 126 127
            equality = 'predicate.' + property_name
            range_min = equality + '_range_min'
            range_max = equality + '_range_max'

128 129 130 131 132 133 134 135
            value = context.getProperty(property_name)

            query = ComplexQuery(
                Query(**{equality: None}),
                Query(**{range_min: None}),
                Query(**{range_max: None}),
                logical_operator='AND')

Romain Courteaud's avatar
Romain Courteaud committed
136
            if value is not None:
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
              query = ComplexQuery(
                  query,
                  ComplexQuery(
                    Query(**{equality: value}),
                    ComplexQuery(
                      ComplexQuery(
                        Query(**{range_min: dict(query=value, range='ngt',)}),
                        Query(**{range_max: None}),
                        logical_operator='AND',),
                      ComplexQuery(
                        Query(**{range_min: None}),
                        Query(**{range_max: dict(query=value, range='min',)}),
                        logical_operator='AND',),
                      ComplexQuery(
                        Query(**{range_min: dict(query=value, range='ngt',)}),
                        Query(**{range_max: dict(query=value, range='min',)}),
                        logical_operator='AND',),
                      logical_operator='OR',),
                    logical_operator='OR',
                    ),
                  logical_operator='OR')

            query_list.append(query)

161
      # Add category selection
162 163 164 165 166
      if tested_base_category_list is None:
        if acquired:
          category_list = context.getAcquiredCategoryList()
        else:
          category_list = context.getCategoryList()
167
      else:
168
        if acquired:
169
          getter = context.getAcquiredCategoryMembershipList
170
        else:
171 172 173 174 175
          getter = context.getCategoryMembershipList
        category_list = []
        extend = category_list.extend
        for tested_base_category in tested_base_category_list:
          extend(getter(tested_base_category, base=1))
176 177

      if tested_base_category_list != []:
178
        preferred_predicate_category_list = portal.portal_preferences.getPreferredPredicateCategoryList()
179

180 181
        if (preferred_predicate_category_list and
            tested_base_category_list is not None and
182
            set(preferred_predicate_category_list).issuperset(tested_base_category_list)):
183 184 185 186
          # New behavior is enabled only if preferred predicate category is
          # defined and tested_base_category_list is passed.
          predicate_category_query_list = []
          predicate_category_table_name_list = []
187
          category_dict = {}
188 189
          for relative_url in category_list:
            category_value = portal_categories.getCategoryValue(relative_url)
190 191
            base_category_id = portal_categories.getBaseCategoryId(relative_url)
            base_category_value = portal_categories.getCategoryValue(base_category_id)
192 193 194 195 196
            if not base_category_value in category_dict:
              category_dict[base_category_value] = []
            category_dict[base_category_value].append(category_value)

          for base_category_value, category_value_list in category_dict.iteritems():
197 198 199 200 201 202 203 204 205
            if base_category_value.getId() in preferred_predicate_category_list:
              table_index = len(predicate_category_query_list)
              predicate_category_table_name = 'predicate_category_for_domain_tool_%s' % table_index
              table_alias_list = [('predicate_category', predicate_category_table_name)]
              predicate_category_query_list.append(
                  ComplexQuery(
                      Query(predicate_category_base_category_uid=base_category_value.getUid(), table_alias_list=table_alias_list),
                      Query(predicate_category_category_strict_membership=1, table_alias_list=table_alias_list),
                      ComplexQuery(
206
                          Query(predicate_category_category_uid=[category_value.getUid() for category_value in category_value_list], table_alias_list=table_alias_list),
207 208 209 210 211 212 213 214 215 216 217 218
                          Query(predicate_category_category_uid='NULL', table_alias_list=table_alias_list),
                          logical_operator='OR'),
                      logical_operator='AND'))

          if not predicate_category_query_list:
            # Prevent matching everything
            predicate_category_query_list.append(Query(predicate_category_base_category_uid=0))

          predicate_category_query = ComplexQuery(
              logical_operator='AND',
              *predicate_category_query_list)
          query_list.append(predicate_category_query)
219
        else:
220 221
          # Traditional behavior
          category_expression_dict = portal_categories.buildAdvancedSQLSelector(
222
                                             category_list or ['NULL'],
223 224 225 226 227
                                             query_table='predicate_category',
                                             none_sql_value=0,
                                             strict=strict)
          where_expression = category_expression_dict['where_expression']
          if where_expression:
228
            kw['where_expression'] = SQLQuery(where_expression)
229 230

          if 'from_expression' in category_expression_dict:
231
            kw['from_expression'] = category_expression_dict['from_expression']
232 233
          else:
            # Add predicate_category.uid for automatic join
234
            kw['predicate_category.uid'] = '!=NULL'
235

236 237
      if query_list:
        kw['query'] = ComplexQuery(logical_operator='AND', *query_list)
238

239 240 241 242
      if restricted:
        sql_result_list = portal_catalog.searchResults(**kw)
      else:
        sql_result_list = portal_catalog.unrestrictedSearchResults(**kw)
Romain Courteaud's avatar
Romain Courteaud committed
243
      if kw.get('src__'):
Sebastien Robin's avatar
Sebastien Robin committed
244
        return sql_result_list
245
      result_list = []
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265
      if sql_result_list:
        if test:
          cache = {}
          def isMemberOf(context, c, strict_membership):
            if c in cache:
              return cache[c]
            cache[c] = result = portal_categories.isMemberOf(
              context, c, strict_membership=strict_membership)
            return result
        for predicate in sql_result_list:
          predicate = predicate.getObject()
          if not test or predicate.test(context, tested_base_category_list,
                                        isMemberOf=isMemberOf):
            result_list.append(predicate)
        if filter_method is not None:
          result_list = filter_method(result_list)
        if sort_key_method is not None:
          result_list.sort(key=sort_key_method)
        elif sort_method is not None:
          result_list.sort(cmp=sort_method)
266 267
      return result_list

Romain Courteaud's avatar
Romain Courteaud committed
268 269
    # XXX FIXME method should not be public 
    # (some users are not able to see resource's price)
270
    security.declarePublic('generateMappedValue')
Romain Courteaud's avatar
Romain Courteaud committed
271
    def generateMappedValue(self, context, test=1, predicate_list=None, **kw):
272
      """
Romain Courteaud's avatar
Romain Courteaud committed
273
      We will generate a mapped value with the list of all predicates 
Alexandre Boeglin's avatar
Alexandre Boeglin committed
274
      found. 
Romain Courteaud's avatar
Romain Courteaud committed
275
      Let's say we have 3 predicates (in the order we want) like this:
Sebastien Robin's avatar
Sebastien Robin committed
276 277 278
      Predicate 1   [ base_price1,           ,   ,   ,    ,    , ]
      Predicate 2   [ base_price2, quantity2 ,   ,   ,    ,    , ]
      Predicate 3   [ base_price3, quantity3 ,   ,   ,    ,    , ]
Alexandre Boeglin's avatar
Alexandre Boeglin committed
279
      Our generated MappedValue will have the base_price of the 
Romain Courteaud's avatar
Romain Courteaud committed
280 281 282
      predicate1, and the quantity of the Predicate2, because Predicate
      1 is the first one which defines a base_price and the Predicate2
      is the first one wich defines a quantity.
283
      """
Sebastien Robin's avatar
Sebastien Robin committed
284
      # First get the list of predicates
285
      if predicate_list is None:
Romain Courteaud's avatar
Romain Courteaud committed
286
        predicate_list = self.searchPredicateList(context, test=test, **kw)
287
      if len(predicate_list)==0:
Romain Courteaud's avatar
Romain Courteaud committed
288 289 290 291
        # No predicate, return None
        mapped_value = None
      else:
        # Generate tempDeliveryCell
292 293
        from Products.ERP5Type.Document import newTempSupplyCell
        mapped_value = newTempSupplyCell(self.getPortalObject(),
Romain Courteaud's avatar
Romain Courteaud committed
294 295 296 297 298
                                           'new_mapped_value')
        mapped_value_property_dict = {}
        # Look for each property the first predicate which defines the 
        # property
        for predicate in predicate_list:
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313
          getMappedValuePropertyList = getattr(predicate,
            'getMappedValuePropertyList', None)
          # searchPredicateList returns a list of any kind of predicate, which
          # includes predicates not containing any mapped value (for exemple, 
          # domains). In such case, it has no meaning to handle them here.
          # A better way would be to tell catalog not to provide us with those
          # extra object, but there is no simple way (many portal types inherit
          # from MappedValue defining the accessor).
          # Feel free to improve.
          if getMappedValuePropertyList is not None:
            for mapped_value_property in predicate.getMappedValuePropertyList():
              if not mapped_value_property_dict.has_key(mapped_value_property):
                value = predicate.getProperty(mapped_value_property)
                if value is not None:
                  mapped_value_property_dict[mapped_value_property] = value
Romain Courteaud's avatar
Romain Courteaud committed
314
        # Update mapped value
Vincent Pelletier's avatar
Vincent Pelletier committed
315
        mapped_value.edit(**mapped_value_property_dict)
Sebastien Robin's avatar
Sebastien Robin committed
316
      return mapped_value
317

Alexandre Boeglin's avatar
Alexandre Boeglin committed
318 319 320
    # XXX FIXME method should not be public 
    # (some users are not able to see resource's price)
    security.declarePublic('generateMultivaluedMappedValue')
321 322
    def generateMultivaluedMappedValue(self, context, test=1,
        predicate_list=None, explanation_only=0, **kw):
Alexandre Boeglin's avatar
Alexandre Boeglin committed
323 324 325 326 327 328 329 330 331 332 333 334 335
      """
      We will generate a mapped value with the list of all predicates 
      found. 
      Let's say we have 3 predicates (in the order we want) like this:
      Predicate 1   [ base_price1,           ,   ,   ,    ,    , ]
      Predicate 2   [ base_price2, additional_price2 ,   ,   ,    ,    , ]
      Predicate 3   [ base_price3, additional_price3 ,   ,   ,    ,    , ]
      Our generated MappedValue will take all values for each property and put
      them in lists, unless predicates define the same list of criterion categories
      """
      # First get the list of predicates
      if predicate_list is None:
        predicate_list = self.searchPredicateList(context, test=test, **kw)
336
      if predicate_list:
Alexandre Boeglin's avatar
Alexandre Boeglin committed
337
        from Products.ERP5Type.Document import newTempSupplyCell
338 339
        mapped_value_property_dict = defaultdict(list)
        explanation_dict = defaultdict(dict)
Alexandre Boeglin's avatar
Alexandre Boeglin committed
340 341 342
        # Look for each property the first predicate with unique criterion
        # categories which defines the property
        for predicate in predicate_list:
343 344
          full_prop_dict = explanation_dict[
            tuple(predicate.getMembershipCriterionCategoryList())]
Alexandre Boeglin's avatar
Alexandre Boeglin committed
345
          for mapped_value_property in predicate.getMappedValuePropertyList():
346
            if mapped_value_property in full_prop_dict:
Alexandre Boeglin's avatar
Alexandre Boeglin committed
347 348 349 350
              # we already have one value for this (categories, property)
              continue
            value = predicate.getProperty(mapped_value_property)
            if value is not None:
351
              full_prop_dict[mapped_value_property] = value
352
              mapped_value_property_dict[mapped_value_property].append(value)
353
        if explanation_only:
354 355
          return dict(explanation_dict)
        mapped_value = newTempSupplyCell(self.getPortalObject(),
356 357 358
                                         'multivalued_mapped_value')
        mapped_value._setMappedValuePropertyList(
          mapped_value_property_dict.keys())
359 360
        mapped_value.__dict__.update(mapped_value_property_dict)
        return mapped_value
361 362


363
    def getChildDomainValueList(self, parent, **kw):
364 365 366 367 368
      """
      Return child domain objects already present adn thois generetaded dynamically
      """
      # get static domain
      object_list = list(parent.objectValues())
Alexandre Boeglin's avatar
Alexandre Boeglin committed
369
      # get dynamic object generated from script
370
      object_list.extend(parent.getDomainGeneratorList(**kw))
371 372
      return object_list

373 374

    def getDomainByPath(self, path, default=_MARKER):
375 376 377
      """
      Return the domain object for a given path
      """
378 379
      path = path.split('/')
      base_domain_id = path[0]
380 381 382 383 384
      if default is _MARKER:
        domain = self[base_domain_id]
      else:
        domain = self.get(base_domain_id, _MARKER)
        if domain is _MARKER: return default
385 386 387
      for depth, subdomain in enumerate(path[1:]):
        domain_list = self.getChildDomainValueList(domain, depth=depth)
        for d in domain_list:
388 389
          if d.getId() == subdomain:
            domain = d
390 391
            break
        else:
392
          if domain is _MARKER: return default
393
          raise KeyError, subdomain
394 395
      return domain
  
Jean-Paul Smets's avatar
Jean-Paul Smets committed
396
InitializeClass(DomainTool)