Predicate.py 24.8 KB
Newer Older
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1 2 3
##############################################################################
#
# Copyright (c) 2002 Nexedi SARL and Contributors. All Rights Reserved.
4
#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
#
# 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.
#
##############################################################################

29
from warnings import warn
Jean-Paul Smets's avatar
Jean-Paul Smets committed
30
from Globals import InitializeClass
Jean-Paul Smets's avatar
Jean-Paul Smets committed
31
from AccessControl import ClassSecurityInfo
Jean-Paul Smets's avatar
Jean-Paul Smets committed
32 33
from Acquisition import aq_base, aq_inner

34 35
from Products.CMFCore.utils import getToolByName

Jean-Paul Smets's avatar
Jean-Paul Smets committed
36
from Products.ERP5Type import Permissions, PropertySheet, Constraint, Interface
37
from Products.ERP5Type.Core.Folder import Folder
Jean-Paul Smets's avatar
Jean-Paul Smets committed
38
from Products.ERP5Type.Document import newTempBase
39
from Products.ERP5Type.XMLObject import XMLObject
Jean-Paul Smets's avatar
Jean-Paul Smets committed
40
from Products.ERP5Type.Utils import convertToUpperCase
41
from Products.ERP5Type.Cache import getReadOnlyTransactionCache, enableReadOnlyTransactionCache, disableReadOnlyTransactionCache
Jean-Paul Smets's avatar
Jean-Paul Smets committed
42 43

from zLOG import LOG
Jean-Paul Smets's avatar
Jean-Paul Smets committed
44

45
class Predicate(XMLObject, Folder):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
46
  """
47 48
    A Predicate object defines a list of criterions
    which can be applied to test a document or to search for documents.
49

50
    Predicates are defined by a combination of PropertySheet values
51 52 53
    (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
54
    script based test.
55 56

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

61
    The approach is intended to provide the expressivity of a rule
62
    based system without the burden of building a fully expressive
63
    rule engine.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
64
  """
Jean-Paul Smets's avatar
Jean-Paul Smets committed
65 66 67 68 69 70
  meta_type = 'ERP5 Predicate'
  portal_type = 'Predicate'
  add_permission = Permissions.AddPortalContent
  isPortalContent = 1
  isRADContent = 1
  isPredicate = 1
71

Jean-Paul Smets's avatar
Jean-Paul Smets committed
72 73
  # Declarative security
  security = ClassSecurityInfo()
74
  security.declareObjectProtected(Permissions.AccessContentsInformation)
75

Jean-Paul Smets's avatar
Jean-Paul Smets committed
76 77 78 79 80 81 82 83 84
  # Declarative properties
  property_sheets = ( PropertySheet.Base
                    , PropertySheet.Predicate
                    , PropertySheet.SortIndex
                    )

  # Declarative interfaces
  __implements__ = ( Interface.Predicate )

Yoshinori Okuji's avatar
Yoshinori Okuji committed
85
  security.declareProtected( Permissions.AccessContentsInformation, 'test' )
86 87
  def test(self, context, tested_base_category_list=None, 
           strict_membership=0, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
88
    """
89 90
      A Predicate can be tested on a given context.
      Parameters can passed in order to ignore some conditions.
91 92

      - tested_base_category_list:  this is the list of category that we do
93
        want to test. For example, we might want to test only the
94
        destination or the source of a predicate.
95 96
      - if strict_membership is specified, we should make sure that we
        are strictly a member of tested categories
Jean-Paul Smets's avatar
Jean-Paul Smets committed
97 98 99
    """
    self = self.asPredicate()
    result = 1
100
    if getattr(aq_base(self), '_identity_criterion', None) is None:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
101 102
      self._identity_criterion = {}
      self._range_criterion = {}
103
#    LOG('PREDICATE TEST', 0,
104 105
#        'testing %s on context of %s' % \
#        (self.getRelativeUrl(), context.getRelativeUrl()))
106
    for property, value in self._identity_criterion.iteritems():
Jean-Paul Smets's avatar
Jean-Paul Smets committed
107
      result = result and (context.getProperty(property) == value)
108
#      LOG('predicate test', 0,
109 110
#          '%s after prop %s : %s == %s' % \
#          (result, property, context.getProperty(property), value))
111
    for property, (min, max) in self._range_criterion.iteritems():
Jean-Paul Smets's avatar
Jean-Paul Smets committed
112 113 114
      value = context.getProperty(property)
      if min is not None:
        result = result and (value >= min)
115
#        LOG('predicate test', 0,
116 117
#            '%s after prop %s : %s >= %s' % \
#            (result, property, value, min))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
118 119
      if max is not None:
        result = result and (value < max)
120
#        LOG('predicate test', 0,
121 122
#            '%s after prop %s : %s < %s' % \
#            (result, property, value, max))
Romain Courteaud's avatar
Romain Courteaud committed
123 124 125 126
    multimembership_criterion_base_category_list = \
        self.getMultimembershipCriterionBaseCategoryList()
    membership_criterion_base_category_list = \
        self.getMembershipCriterionBaseCategoryList()
Jean-Paul Smets's avatar
Jean-Paul Smets committed
127
    tested_base_category = {}
128
#    LOG('predicate test', 0,
129
#        'categories will be tested in multi %s single %s as %s' % \
130 131
#        (multimembership_criterion_base_category_list,
#        membership_criterion_base_category_list,
132
#        self.getMembershipCriterionCategoryList()))
133 134 135 136
    membership_criterion_category_list = \
                            self.getMembershipCriterionCategoryList()
    if tested_base_category_list is not None:
      membership_criterion_category_list = [x for x in \
Yoshinori Okuji's avatar
Yoshinori Okuji committed
137
          membership_criterion_category_list if x.split('/', 1)[0] in \
138 139
          tested_base_category_list]

140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
    # Test category memberships. Enable the read-only transaction cache
    # temporarily, if not enabled, because this part is strictly read-only,
    # and context.isMemberOf is very expensive, when the category list has
    # many items.
    enabled = (getReadOnlyTransactionCache(self) is not None)
    try:
      if not enabled:
        enableReadOnlyTransactionCache(self)
      for c in membership_criterion_category_list:
        bc = c.split('/', 1)[0]
        if (bc not in tested_base_category) and \
           (bc in multimembership_criterion_base_category_list):
          tested_base_category[bc] = 1
        elif (bc not in tested_base_category) and \
             (bc in membership_criterion_base_category_list):
          tested_base_category[bc] = 0
        if (bc in multimembership_criterion_base_category_list):
          tested_base_category[bc] = tested_base_category[bc] and \
158 159
                                     context.isMemberOf(c, 
                                         strict_membership=strict_membership)
160
#        LOG('predicate test', 0,
161 162
#            '%s after multi membership to %s' % \
#            (tested_base_category[bc], c))
163 164
        elif (bc in membership_criterion_base_category_list):
          tested_base_category[bc] = tested_base_category[bc] or \
165 166
                                     context.isMemberOf(c,
                                         strict_membership=strict_membership)
167 168 169
    finally:
      if not enabled:
        disableReadOnlyTransactionCache(self)
170

171
#        LOG('predicate test', 0,
172 173
#            '%s after single membership to %s' % \
#            (tested_base_category[bc], c))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
174
    result = result and (0 not in tested_base_category.values())
175
#    LOG('predicate test', 0,
176
#        '%s after category %s ' % (result, tested_base_category.items()))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
177
    # Test method calls
178 179 180 181 182
    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) and result:
          method = getattr(context,test_method_id)
183 184 185
          try:
            result = result and method(self)
          except TypeError:
186 187
            if method.func_code.co_argcount != 0:
              raise
188
            # backward compatibilty with script that takes no argument
189 190 191
            warn('Predicate %s uses an old-style method (%s) that does not'
                 ' take the predicate as argument' % (
               self.getRelativeUrl(), method.__name__), DeprecationWarning)
192
            result = result and method()
193
#        LOG('predicate test', 0,
194
#            '%s after method %s ' % (result, test_method_id))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
195 196
    return result

197
  security.declareProtected( Permissions.AccessContentsInformation,
Jérome Perrin's avatar
Jérome Perrin committed
198 199
                             'buildSQLQuery' )
  def buildSQLQuery(self, strict_membership=0, table='category',
200 201
                          join_table='catalog', join_column='uid',
                          **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
202
    """
203 204 205
      A Predicate can be rendered as an SQL expression. This
      can be used to generate SQL requests in reports or in
      catalog search queries.
206

207
      XXX - This method is not implemented yet
Jean-Paul Smets's avatar
Jean-Paul Smets committed
208
    """
209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228
    # Build the identity criterion
    catalog_kw = {}
    catalog_kw.update(kw) # query_table, REQUEST, ignore_empty_string, **kw
    for criterion in self.getCriterionList():
      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:
        catalog_kw[criterion.property] = criterion.identity

    portal_catalog = getToolByName(self, 'portal_catalog')
229 230 231
    portal_categories = getToolByName(self, 'portal_categories')

    from_table_dict = {}
232

233 234 235 236 237 238 239 240
    # 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):
241
        category_value = portal_categories.resolveCategory(category, None)
242 243 244
        if category_value is not None:
          table_alias = "single_%s_%s" % (table, base_category)
          from_table_dict[table_alias] = 'category'
Jérome Perrin's avatar
Jérome Perrin committed
245
          membership_dict[base_category].append(category_value.asSQLExpression(
246 247 248
                                          strict_membership=strict_membership,
                                          table=table_alias,
                                          base_category=base_category))
249 250 251 252 253
    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)
254

Jean-Paul Smets's avatar
Jean-Paul Smets committed
255
    # Then build SQL for multimembership_dict criteria
256 257 258 259 260 261 262 263 264 265 266 267
    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 = portal_categories.resolveCategory(category)
        if category_value is not None:
          join_count += 1
          table_alias = "multi_%s_%s" % (table, join_count)
          from_table_dict[table_alias] = 'category'
Jérome Perrin's avatar
Jérome Perrin committed
268
          multimembership_dict[base_category].append(category_value.asSQLExpression(
269 270 271
                                          strict_membership=strict_membership,
                                          table=table_alias,
                                          base_category=base_category))
272 273 274 275 276
    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)
277 278

    # Build the join where expression
279
    join_select_list = []
Yoshinori Okuji's avatar
Yoshinori Okuji committed
280
    for k in from_table_dict.iterkeys():
281
      join_select_list.append('%s.%s = %s.uid' % (join_table, join_column, k))
282 283

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

286 287 288 289 290 291 292 293 294
    # Now merge identity and membership criteria
    catalog_kw['where_expression'] = sql_text
    sql_query = portal_catalog.buildSQLQuery(**catalog_kw)
    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
295

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

Jérome Perrin's avatar
Jérome Perrin committed
300 301
  security.declareProtected( Permissions.AccessContentsInformation, 'asSQLExpression' )
  def asSQLExpression(self, strict_membership=0, table='category'):
302
    """
Jean-Paul Smets's avatar
Jean-Paul Smets committed
303 304 305
      A Predicate can be rendered as an SQL expression. This
      can be used to generate SQL requests in reports or in
      catalog search queries.
306
    """
Jérome Perrin's avatar
Jérome Perrin committed
307
    return self.buildSQLQuery(strict_membership=strict_membership, table=table)['where_expression']
Jean-Paul Smets's avatar
Jean-Paul Smets committed
308

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

Jérome Perrin's avatar
Jérome Perrin committed
313 314
  security.declareProtected( Permissions.AccessContentsInformation, 'asSQLJoinExpression' )
  def asSQLJoinExpression(self, strict_membership=0, table='category', join_table='catalog', join_column='uid'):
315 316
    """
    """
Jérome Perrin's avatar
Jérome Perrin committed
317
    table_list = self.buildSQLQuery(strict_membership=strict_membership, table=table)['from_table_list']
318
    sql_text_list = map(lambda (a,b): '%s AS %s' % (b,a), filter(lambda (a,b): a != join_table, table_list))
319
    return ' , '.join(sql_text_list)
320

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

325 326 327 328 329 330 331 332 333 334 335 336
  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)

Jean-Paul Smets's avatar
Jean-Paul Smets committed
337 338
  security.declareProtected( Permissions.AccessContentsInformation, 'getCriterionList' )
  def getCriterionList(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
339
    """
340
      Returns the list of criteria which are defined by the Predicate.
341

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

345
      XXX - It would be better to return criteria in a Criterion class
346
            instance
Jean-Paul Smets's avatar
Jean-Paul Smets committed
347
    """
Yoshinori Okuji's avatar
Yoshinori Okuji committed
348
    if getattr(aq_base(self), '_identity_criterion', None) is None:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364
      self._identity_criterion = {}
      self._range_criterion = {}
    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):
365 366 367
    """
      This methods sets parameters of a criterion. There is at most one
      criterion per property. Defined parameters are
368

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

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

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

378
    """
379
    # XXX 'min' and 'max' are built-in functions.
Yoshinori Okuji's avatar
Yoshinori Okuji committed
380
    if getattr(aq_base(self), '_identity_criterion', None) is None:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
381 382
      self._identity_criterion = {}
      self._range_criterion = {}
Yoshinori Okuji's avatar
Yoshinori Okuji committed
383
    if identity is not None :
Jean-Paul Smets's avatar
Jean-Paul Smets committed
384
      self._identity_criterion[property] = identity
385 386 387 388 389 390 391 392 393 394
    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:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
395 396 397 398
      self._range_criterion[property] = (min, max)
    self.reindexObject()

  security.declareProtected( Permissions.ModifyPortalContent, 'edit' )
399 400 401 402 403 404
  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.
    """
Yoshinori Okuji's avatar
Yoshinori Okuji committed
405
    if getattr(aq_base(self), '_identity_criterion', None) is None:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
406 407
      self._identity_criterion = {}
      self._range_criterion = {}
Yoshinori Okuji's avatar
Yoshinori Okuji committed
408
    if 'criterion_property_list' in kwd:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
409 410 411
      criterion_property_list = kwd['criterion_property_list']
      identity_criterion = {}
      range_criterion = {}
Yoshinori Okuji's avatar
Yoshinori Okuji committed
412
      for criterion in self._identity_criterion.iterkeys() :
Jean-Paul Smets's avatar
Jean-Paul Smets committed
413 414
        if criterion in criterion_property_list :
          identity_criterion[criterion] = self._identity_criterion[criterion]
Yoshinori Okuji's avatar
Yoshinori Okuji committed
415
      for criterion in self._range_criterion.iterkeys() :
Jean-Paul Smets's avatar
Jean-Paul Smets committed
416 417 418 419
        if criterion in criterion_property_list :
          range_criterion[criterion] = self._range_criterion[criterion]
      self._identity_criterion = identity_criterion
      self._range_criterion = range_criterion
420
    kwd['reindex_object'] = 1
Jean-Paul Smets's avatar
Jean-Paul Smets committed
421 422 423
    return self._edit(**kwd)

  # Predicate fusion method
Yoshinori Okuji's avatar
Yoshinori Okuji committed
424
  security.declareProtected( Permissions.ModifyPortalContent, 'setPredicateCategoryList' )
Jean-Paul Smets's avatar
Jean-Paul Smets committed
425
  def setPredicateCategoryList(self, category_list):
426 427 428 429 430 431
    """
      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.
432 433

      WARNING: this method does not take into account scripts at
434 435
      this point.
    """
Jean-Paul Smets's avatar
Jean-Paul Smets committed
436 437 438 439 440 441 442
    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 = []
443 444 445
    # reset criterions
    self._identity_criterion = {}
    self._range_criterion = {}
446

Jean-Paul Smets's avatar
Jean-Paul Smets committed
447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469
    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)
470
    self._setTestMethodIdList(test_method_id_list)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
471 472
    self.reindexObject()

Yoshinori Okuji's avatar
Yoshinori Okuji committed
473
  security.declareProtected(Permissions.AccessContentsInformation, 'generatePredicate')
474
  def generatePredicate(self, multimembership_criterion_base_category_list=(),
475 476
                        membership_criterion_base_category_list=(),
                        criterion_property_list=()):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
477
    """
478
    This method generates a new temporary predicate based on an ad-hoc
479
    interpretation of local properties of an object. For example,
480
    a start_range_min property will be interpreted as a way to define
481
    a min criterion on start_date.
482

483
    The purpose of this method is to be called from
484 485 486
    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.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
487
    """
488
    new_membership_criterion_category_list = list(self.getMembershipCriterionCategoryList())
489
    new_membership_criterion_base_category_list = list(self.getMembershipCriterionBaseCategoryList())
490
    new_multimembership_criterion_base_category_list = list(self.getMultimembershipCriterionBaseCategoryList())
491

492
    for base_category in multimembership_criterion_base_category_list:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
493 494 495
      category_list = self.getProperty(base_category + '_list')
      if category_list is not None and len(category_list)>0:
        for category in category_list:
496
          new_membership_criterion_category_list.append(base_category + '/' + category)
497
        if base_category not in new_multimembership_criterion_base_category_list:
498
          new_multimembership_criterion_base_category_list.append(base_category)
499

500 501 502 503 504
    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)
505
        if base_category not in new_membership_criterion_base_category_list:
506
          new_membership_criterion_base_category_list.append(base_category)
507

508
    new_criterion_property_list =  list(self.getCriterionPropertyList())
Jean-Paul Smets's avatar
Jean-Paul Smets committed
509 510 511
    identity_criterion = getattr(self,'_identity_criterion',{})
    range_criterion = getattr(self,'_range_criterion',{})
    # Look at local properties and make it criterion properties
512
    for property in criterion_property_list:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
513 514
      if property not in self.getCriterionPropertyList() \
        and property in self.propertyIds():
515
          new_criterion_property_list.append(property)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
516 517
          property_min = property + '_range_min'
          property_max = property + '_range_max'
518
          if getattr(self, 'get%s' % convertToUpperCase(property), None) is not None\
Jean-Paul Smets's avatar
Jean-Paul Smets committed
519 520
            and self.getProperty(property) is not None:
            identity_criterion[property] = self.getProperty(property)
521
          elif getattr(self, 'get%s' % convertToUpperCase(property_min), None) is not None:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
522 523 524 525 526 527
            min = self.getProperty(property_min)
            max = self.getProperty(property_max)
            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(
528
        membership_criterion_category=new_membership_criterion_category_list,
529
        membership_criterion_base_category=new_membership_criterion_base_category_list,
530 531
        multimembership_criterion_base_category=new_multimembership_criterion_base_category_list,
        criterion_property_list=new_criterion_property_list,
Jean-Paul Smets's avatar
Jean-Paul Smets committed
532 533 534
        _identity_criterion=identity_criterion,
        _range_criterion=range_criterion)

535 536 537
    return new_self

  # Predicate handling
538 539 540
  security.declareProtected(Permissions.AccessContentsInformation,
                            'asPredicate')
  def asPredicate(self, script_id=None):
541
    """
542
      This method tries to convert the current Document into a predicate
543 544
      looking up methods named ${PortalType}_asPredicate,
      ${MetaType}_asPredicate, ${Class}_asPredicate     
545
    """
546 547 548 549
    if script_id is not None:
      script = getattr(self, script_id, None)
    else:
      script = self._getTypeBasedMethod('asPredicate')
550
    if script is not None:
551 552
      return script()
    return self
553 554 555 556

  def searchPredicate(self, **kw):
    """
      Returns a list of documents matching the predicate
Jean-Paul Smets's avatar
Jean-Paul Smets committed
557 558

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