DomainTool.py 18.5 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 30
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
##############################################################################

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

39 40
_MARKER = []

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

    # Declarative Security
    security = ClassSecurityInfo()

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

Romain Courteaud's avatar
Romain Courteaud committed
57 58
    # XXX FIXME method should not be public 
    # (some users are not able to see resource's price)
59
    security.declarePublic('searchPredicateList')
Romain Courteaud's avatar
Romain Courteaud committed
60
    def searchPredicateList(self, context, test=1, sort_method=None,
61 62
                            ignored_category_list=None,
                            tested_base_category_list=None,
63
                            filter_method=None, acquired=1,
64
                            strict=True, sort_key_method=None, query=None, **kw):
65 66 67 68
      # 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.
69
      """
Romain Courteaud's avatar
Romain Courteaud committed
70 71
      Search all predicates which corresponds to this particular 
      context.
72

Yoshinori Okuji's avatar
Yoshinori Okuji committed
73 74
      - sort_method parameter should not be used, if possible, because
        it can be very slow. Use sort_key_method instead.
75 76 77

      - 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
78 79
        important predicate is the first one in the list.

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

84 85 86 87
      - 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
88
      - the acquired parameter allows to define if we want to use
89
        acquisition for categories. By default we want.
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104

      - 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.
105
      """
106 107 108 109
      portal = self.getPortalObject()
      portal_catalog = portal.portal_catalog
      portal_categories = portal.portal_categories
      portal_preferences = portal.portal_preferences
110 111 112
      # Search the columns of the predicate table
      column_list = [x.split('.')[1] for x in portal_catalog.getColumnIds()
                     if x.startswith('predicate.')]
113 114 115
      expression_list = []
      checked_column_list = []
      sql_kw = {}
116 117 118 119
      query_list = []
      if query is not None:
        query_list = [query]

120
      for column in column_list:
Romain Courteaud's avatar
Romain Courteaud committed
121 122 123 124 125
        if column not in checked_column_list:
          range_property = 0
          if (column.endswith('_range_min')) or \
             (column.endswith('_range_max')):
            range_property = 1
126
            property_name = column[:-len('_range_min')]
Romain Courteaud's avatar
Romain Courteaud committed
127 128
          if ('%s_range_min' % column) in column_list:
            range_property = 1
129
            property_name = column
Romain Courteaud's avatar
Romain Courteaud committed
130 131
          if range_property:
            # We have to check a range property
132 133 134 135 136 137 138 139 140 141 142 143
            equality = 'predicate.%s' % property_name
            range_min = 'predicate.%s_range_min' % property_name
            range_max = 'predicate.%s_range_max' % property_name
            
            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
144
            if value is not None:
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
              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)

            checked_column_list.append('%s' % property_name)
            checked_column_list.append('%s_range_min' % property_name)
            checked_column_list.append('%s_range_max' % property_name)
172 173

      # Add category selection
174 175 176 177 178
      if tested_base_category_list is None:
        if acquired:
          category_list = context.getAcquiredCategoryList()
        else:
          category_list = context.getCategoryList()
179
      else:
180
        if acquired:
181
          getter = context.getAcquiredCategoryMembershipList
182
        else:
183 184 185 186 187
          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))
188 189

      if tested_base_category_list != []:
190 191
        preferred_predicate_category_list = portal_preferences.getPreferredPredicateCategoryList()

192 193 194
        if (preferred_predicate_category_list and
            tested_base_category_list is not None and
            set(preferred_predicate_category_list).issuperset(set(tested_base_category_list))):
195 196 197 198
          # 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 = []
199
          category_dict = {}
200 201
          for relative_url in category_list:
            category_value = portal_categories.getCategoryValue(relative_url)
202 203
            base_category_id = portal_categories.getBaseCategoryId(relative_url)
            base_category_value = portal_categories.getCategoryValue(base_category_id)
204 205 206 207 208
            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():
209 210 211 212 213 214 215 216 217
            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(
218
                          Query(predicate_category_category_uid=[category_value.getUid() for category_value in category_value_list], table_alias_list=table_alias_list),
219 220 221 222 223 224 225 226 227 228 229 230
                          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)
231
        else:
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248
          # Traditional behavior
          if len(category_list)==0:
            category_list = ['NULL']
          category_expression_dict = portal_categories.buildAdvancedSQLSelector(
                                             category_list,
                                             query_table='predicate_category',
                                             none_sql_value=0,
                                             strict=strict)
          where_expression = category_expression_dict['where_expression']
          if where_expression:
            sql_kw['where_expression'] = SQLQuery(where_expression)

          if 'from_expression' in category_expression_dict:
            sql_kw['from_expression'] = category_expression_dict['from_expression']
          else:
            # Add predicate_category.uid for automatic join
            sql_kw['predicate_category.uid'] = '!=NULL'
249

250
      kw.update(sql_kw)
251 252
      if query_list:
        kw['query'] = ComplexQuery(logical_operator='AND', *query_list)
253 254

      sql_result_list = portal_catalog.searchResults(**kw)
Romain Courteaud's avatar
Romain Courteaud committed
255
      if kw.get('src__'):
Sebastien Robin's avatar
Sebastien Robin committed
256
        return sql_result_list
257 258
      result_list = []
      for predicate in [x.getObject() for x in sql_result_list]:
259 260
        if (not test) or predicate.test(
                       context,
261
                       tested_base_category_list=tested_base_category_list):
262
          result_list.append(predicate)
Sebastien Robin's avatar
Sebastien Robin committed
263 264
      if filter_method is not None:
        result_list = filter_method(result_list)
265 266 267 268
      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)
269 270
      return result_list

Romain Courteaud's avatar
Romain Courteaud committed
271 272
    # XXX FIXME method should not be public 
    # (some users are not able to see resource's price)
273
    security.declarePublic('generateMappedValue')
Romain Courteaud's avatar
Romain Courteaud committed
274
    def generateMappedValue(self, context, test=1, predicate_list=None, **kw):
275
      """
Romain Courteaud's avatar
Romain Courteaud committed
276
      We will generate a mapped value with the list of all predicates 
Alexandre Boeglin's avatar
Alexandre Boeglin committed
277
      found. 
Romain Courteaud's avatar
Romain Courteaud committed
278
      Let's say we have 3 predicates (in the order we want) like this:
Sebastien Robin's avatar
Sebastien Robin committed
279 280 281
      Predicate 1   [ base_price1,           ,   ,   ,    ,    , ]
      Predicate 2   [ base_price2, quantity2 ,   ,   ,    ,    , ]
      Predicate 3   [ base_price3, quantity3 ,   ,   ,    ,    , ]
Alexandre Boeglin's avatar
Alexandre Boeglin committed
282
      Our generated MappedValue will have the base_price of the 
Romain Courteaud's avatar
Romain Courteaud committed
283 284 285
      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.
286
      """
Sebastien Robin's avatar
Sebastien Robin committed
287
      # First get the list of predicates
288
      if predicate_list is None:
Romain Courteaud's avatar
Romain Courteaud committed
289
        predicate_list = self.searchPredicateList(context, test=test, **kw)
290
      if len(predicate_list)==0:
Romain Courteaud's avatar
Romain Courteaud committed
291 292 293 294
        # No predicate, return None
        mapped_value = None
      else:
        # Generate tempDeliveryCell
295 296
        from Products.ERP5Type.Document import newTempSupplyCell
        mapped_value = newTempSupplyCell(self.getPortalObject(),
Romain Courteaud's avatar
Romain Courteaud committed
297 298 299 300 301
                                           'new_mapped_value')
        mapped_value_property_dict = {}
        # Look for each property the first predicate which defines the 
        # property
        for predicate in predicate_list:
302 303 304 305 306 307 308 309 310 311 312 313 314 315 316
          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
317
        # Update mapped value
Vincent Pelletier's avatar
Vincent Pelletier committed
318
        mapped_value.edit(**mapped_value_property_dict)
Sebastien Robin's avatar
Sebastien Robin committed
319
      return mapped_value
320

Alexandre Boeglin's avatar
Alexandre Boeglin committed
321 322 323
    # XXX FIXME method should not be public 
    # (some users are not able to see resource's price)
    security.declarePublic('generateMultivaluedMappedValue')
324 325
    def generateMultivaluedMappedValue(self, context, test=1,
        predicate_list=None, explanation_only=0, **kw):
Alexandre Boeglin's avatar
Alexandre Boeglin committed
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348
      """
      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)
      if len(predicate_list)==0:
        # No predicate, return None
        mapped_value = None
      else:
        # Generate tempDeliveryCell
        from Products.ERP5Type.Document import newTempSupplyCell
        mapped_value = newTempSupplyCell(self.getPortalObject(),
                                           'new_mapped_value')
        mapped_value_property_dict = {}
        processed_dict = {}
349
        explanation_dict = {}
Alexandre Boeglin's avatar
Alexandre Boeglin committed
350 351 352 353 354 355 356 357
        # Look for each property the first predicate with unique criterion
        # categories which defines the property
        for predicate in predicate_list:
          predicate_category_list = \
              tuple(predicate.getMembershipCriterionCategoryList())

          for mapped_value_property in predicate.getMappedValuePropertyList():
            prop_list = processed_dict.setdefault(predicate_category_list, [])
358 359
            full_prop_dict = explanation_dict.setdefault(
                predicate_category_list, {})
Alexandre Boeglin's avatar
Alexandre Boeglin committed
360 361 362 363 364 365
            if mapped_value_property in prop_list:
              # we already have one value for this (categories, property)
              continue

            value = predicate.getProperty(mapped_value_property)
            if value is not None:
366
              prop_list.append(mapped_value_property)
367
              full_prop_dict[mapped_value_property] = value
Alexandre Boeglin's avatar
Alexandre Boeglin committed
368 369 370 371
              mv_prop_list = \
                  mapped_value_property_dict.setdefault(
                  mapped_value_property, [])
              mv_prop_list.append(value)
372 373
        if explanation_only:
          return explanation_dict
Alexandre Boeglin's avatar
Alexandre Boeglin committed
374
        # Update mapped value
375
        mapped_value = mapped_value.asContext(**mapped_value_property_dict)
Alexandre Boeglin's avatar
Alexandre Boeglin committed
376
      return mapped_value
377 378


379
    def getChildDomainValueList(self, parent, **kw):
380 381 382 383 384
      """
      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
385
      # get dynamic object generated from script
386
      object_list.extend(parent.getDomainGeneratorList(**kw))
387 388
      return object_list

389 390

    def getDomainByPath(self, path, default=_MARKER):
391 392 393
      """
      Return the domain object for a given path
      """
394 395
      path = path.split('/')
      base_domain_id = path[0]
396 397 398 399 400
      if default is _MARKER:
        domain = self[base_domain_id]
      else:
        domain = self.get(base_domain_id, _MARKER)
        if domain is _MARKER: return default
401 402 403
      for depth, subdomain in enumerate(path[1:]):
        domain_list = self.getChildDomainValueList(domain, depth=depth)
        for d in domain_list:
404 405
          if d.getId() == subdomain:
            domain = d
406 407
            break
        else:
408
          if domain is _MARKER: return default
409
          raise KeyError, subdomain
410 411
      return domain
  
Jean-Paul Smets's avatar
Jean-Paul Smets committed
412
InitializeClass(DomainTool)