Predicate.py 28.2 KB
Newer Older
1
# -*- coding: utf-8 -*-
Jean-Paul Smets's avatar
Jean-Paul Smets committed
2 3 4
##############################################################################
#
# Copyright (c) 2002 Nexedi SARL and Contributors. All Rights Reserved.
5
#                    Jean-Paul Smets-Solanes <jp@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 types import MethodType
31
import zope.interface
32
from warnings import warn
Jean-Paul Smets's avatar
Jean-Paul Smets committed
33
from AccessControl import ClassSecurityInfo
Jean-Paul Smets's avatar
Jean-Paul Smets committed
34 35
from Acquisition import aq_base, aq_inner

36 37
from Products.CMFCore.utils import getToolByName

38
from Products.ERP5Type import Permissions, PropertySheet, interfaces
39
from Products.ERP5Type.Accessor.Constant import PropertyGetter as ConstantGetter
Jean-Paul Smets's avatar
Jean-Paul Smets committed
40
from Products.ERP5Type.Document import newTempBase
41
from Products.ERP5Type.XMLObject import XMLObject
Jean-Paul Smets's avatar
Jean-Paul Smets committed
42
from Products.ERP5Type.Utils import convertToUpperCase
43
from Products.ERP5Type.Cache import getReadOnlyTransactionCache, enableReadOnlyTransactionCache, disableReadOnlyTransactionCache
44
from Products.ZSQLCatalog.SQLCatalog import SQLQuery
45
from Products.ERP5Type.Globals import PersistentMapping
46
from Products.ERP5Type.UnrestrictedMethod import UnrestrictedMethod
Jean-Paul Smets's avatar
Jean-Paul Smets committed
47

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

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

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

64
    The approach is intended to provide the expressivity of a rule
65
    based system without the burden of building a fully expressive
66
    rule engine.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
67
  """
Jean-Paul Smets's avatar
Jean-Paul Smets committed
68 69 70
  meta_type = 'ERP5 Predicate'
  portal_type = 'Predicate'
  add_permission = Permissions.AddPortalContent
71
  isPredicate = ConstantGetter('isPredicate', value=True)
72

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

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

  # Declarative interfaces
85
  zope.interface.implements( interfaces.IPredicate, )
Jean-Paul Smets's avatar
Jean-Paul Smets committed
86

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

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

151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
    # 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 \
169 170
                                     context.isMemberOf(c, 
                                         strict_membership=strict_membership)
171
#        LOG('predicate test', 0,
172 173
#            '%s after multi membership to %s' % \
#            (tested_base_category[bc], c))
174 175
        elif (bc in membership_criterion_base_category_list):
          tested_base_category[bc] = tested_base_category[bc] or \
176 177
                                     context.isMemberOf(c,
                                         strict_membership=strict_membership)
178 179 180
    finally:
      if not enabled:
        disableReadOnlyTransactionCache(self)
181

182
#        LOG('predicate test', 0,
183 184
#            '%s after single membership to %s' % \
#            (tested_base_category[bc], c))
185
    result = 0 not in tested_base_category.values()
186
#    LOG('predicate test', 0,
187
#        '%s after category %s ' % (result, tested_base_category.items()))
188 189
    if not result:
      return result
Jean-Paul Smets's avatar
Jean-Paul Smets committed
190
    # Test method calls
191 192 193
    test_method_id_list = self.getTestMethodIdList()
    if test_method_id_list is not None :
      for test_method_id in test_method_id_list :
194
        if test_method_id is not None:
195
          method = getattr(context,test_method_id)
196
          try:
197
            result = method(self)
198
          except TypeError:
199
            if method.func_code.co_argcount != isinstance(method, MethodType):
200
              raise
201
            # backward compatibilty with script that takes no argument
202 203 204
            warn('Predicate %s uses an old-style method (%s) that does not'
                 ' take the predicate as argument' % (
               self.getRelativeUrl(), method.__name__), DeprecationWarning)
205 206 207 208 209
            result = method()
#          LOG('predicate test', 0,
#              '%s after method %s ' % (result, test_method_id))
          if not result:
            return result
Jean-Paul Smets's avatar
Jean-Paul Smets committed
210 211
    return result

212 213 214 215 216 217 218
  @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)

219
  security.declareProtected( Permissions.AccessContentsInformation,
Jérome Perrin's avatar
Jérome Perrin committed
220 221
                             'buildSQLQuery' )
  def buildSQLQuery(self, strict_membership=0, table='category',
222 223
                          join_table='catalog', join_column='uid',
                          **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
224
    """
225 226 227
      A Predicate can be rendered as an SQL expression. This
      can be used to generate SQL requests in reports or in
      catalog search queries.
228

229
      XXX - This method is not implemented yet
Jean-Paul Smets's avatar
Jean-Paul Smets committed
230
    """
231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
    # 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:
248 249 250 251 252 253 254 255 256 257 258 259 260 261
        # 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
262 263

    portal_catalog = getToolByName(self, 'portal_catalog')
264 265

    from_table_dict = {}
266

267 268 269 270 271 272 273 274
    # 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):
275
        category_value = self._unrestrictedResolveCategory(category, None)
276 277 278
        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
279
          membership_dict[base_category].append(category_value.asSQLExpression(
280 281 282
                                          strict_membership=strict_membership,
                                          table=table_alias,
                                          base_category=base_category))
283 284 285 286 287
    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)
288

Jean-Paul Smets's avatar
Jean-Paul Smets committed
289
    # Then build SQL for multimembership_dict criteria
290 291 292 293 294 295 296
    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):
297
        category_value = self._unrestrictedResolveCategory(category)
298 299 300 301
        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
302
          multimembership_dict[base_category].append(category_value.asSQLExpression(
303 304 305
                                          strict_membership=strict_membership,
                                          table=table_alias,
                                          base_category=base_category))
306 307 308 309 310
    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)
311 312

    # Build the join where expression
313
    join_select_list = []
Yoshinori Okuji's avatar
Yoshinori Okuji committed
314
    for k in from_table_dict.iterkeys():
315
      join_select_list.append('%s.%s = %s.uid' % (join_table, join_column, k))
316 317

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

320
    # Now merge identity and membership criteria
321 322 323 324
    if len(sql_text):
      catalog_kw['where_expression'] = SQLQuery(sql_text)
    else:
      catalog_kw['where_expression'] = ''
325 326 327 328 329 330 331
    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
332

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

Jérome Perrin's avatar
Jérome Perrin committed
337 338
  security.declareProtected( Permissions.AccessContentsInformation, 'asSQLExpression' )
  def asSQLExpression(self, strict_membership=0, table='category'):
339
    """
Jean-Paul Smets's avatar
Jean-Paul Smets committed
340 341 342
      A Predicate can be rendered as an SQL expression. This
      can be used to generate SQL requests in reports or in
      catalog search queries.
343
    """
Jérome Perrin's avatar
Jérome Perrin committed
344
    return self.buildSQLQuery(strict_membership=strict_membership, table=table)['where_expression']
Jean-Paul Smets's avatar
Jean-Paul Smets committed
345

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

Jérome Perrin's avatar
Jérome Perrin committed
350 351
  security.declareProtected( Permissions.AccessContentsInformation, 'asSQLJoinExpression' )
  def asSQLJoinExpression(self, strict_membership=0, table='category', join_table='catalog', join_column='uid'):
352 353
    """
    """
Jérome Perrin's avatar
Jérome Perrin committed
354
    table_list = self.buildSQLQuery(strict_membership=strict_membership, table=table)['from_table_list']
355
    sql_text_list = map(lambda (a,b): '%s AS %s' % (b,a), filter(lambda (a,b): a != join_table, table_list))
356
    return ' , '.join(sql_text_list)
357

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

362 363 364 365 366 367 368 369 370 371 372 373
  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
374 375
  security.declareProtected( Permissions.AccessContentsInformation, 'getCriterionList' )
  def getCriterionList(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
376
    """
377
      Returns the list of criteria which are defined by the Predicate.
378

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

382
      XXX - It would be better to return criteria in a Criterion class
383
            instance
Jean-Paul Smets's avatar
Jean-Paul Smets committed
384
    """
Yoshinori Okuji's avatar
Yoshinori Okuji committed
385
    if getattr(aq_base(self), '_identity_criterion', None) is None:
386 387
      self._identity_criterion = PersistentMapping()
      self._range_criterion = PersistentMapping()
Jean-Paul Smets's avatar
Jean-Paul Smets committed
388 389 390 391 392 393 394 395 396 397 398 399 400 401
    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):
402 403 404
    """
      This methods sets parameters of a criterion. There is at most one
      criterion per property. Defined parameters are
405

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

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

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

415
    """
416
    # XXX 'min' and 'max' are built-in functions.
Yoshinori Okuji's avatar
Yoshinori Okuji committed
417
    if getattr(aq_base(self), '_identity_criterion', None) is None:
418 419
      self._identity_criterion = PersistentMapping()
      self._range_criterion = PersistentMapping()
Yoshinori Okuji's avatar
Yoshinori Okuji committed
420
    if identity is not None :
Jean-Paul Smets's avatar
Jean-Paul Smets committed
421
      self._identity_criterion[property] = identity
422 423 424 425 426 427 428 429 430 431
    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
432 433 434 435
      self._range_criterion[property] = (min, max)
    self.reindexObject()

  security.declareProtected( Permissions.ModifyPortalContent, 'edit' )
436 437 438 439 440 441
  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
442
    if getattr(aq_base(self), '_identity_criterion', None) is None:
443 444
      self._identity_criterion = PersistentMapping()
      self._range_criterion = PersistentMapping()
Yoshinori Okuji's avatar
Yoshinori Okuji committed
445
    if 'criterion_property_list' in kwd:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
446 447 448
      criterion_property_list = kwd['criterion_property_list']
      identity_criterion = {}
      range_criterion = {}
Yoshinori Okuji's avatar
Yoshinori Okuji committed
449
      for criterion in self._identity_criterion.iterkeys() :
Jean-Paul Smets's avatar
Jean-Paul Smets committed
450 451
        if criterion in criterion_property_list :
          identity_criterion[criterion] = self._identity_criterion[criterion]
Yoshinori Okuji's avatar
Yoshinori Okuji committed
452
      for criterion in self._range_criterion.iterkeys() :
Jean-Paul Smets's avatar
Jean-Paul Smets committed
453 454 455 456
        if criterion in criterion_property_list :
          range_criterion[criterion] = self._range_criterion[criterion]
      self._identity_criterion = identity_criterion
      self._range_criterion = range_criterion
457
    kwd['reindex_object'] = 1
Jean-Paul Smets's avatar
Jean-Paul Smets committed
458 459 460
    return self._edit(**kwd)

  # Predicate fusion method
Yoshinori Okuji's avatar
Yoshinori Okuji committed
461
  security.declareProtected( Permissions.ModifyPortalContent, 'setPredicateCategoryList' )
Jean-Paul Smets's avatar
Jean-Paul Smets committed
462
  def setPredicateCategoryList(self, category_list):
463 464 465 466 467 468
    """
      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.
469 470

      WARNING: this method does not take into account scripts at
471 472
      this point.
    """
Jean-Paul Smets's avatar
Jean-Paul Smets committed
473 474 475 476 477 478 479
    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 = []
480
    # reset criterions
481 482
    self._identity_criterion = PersistentMapping()
    self._range_criterion = PersistentMapping()
483

Jean-Paul Smets's avatar
Jean-Paul Smets committed
484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506
    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)
507
    self._setTestMethodIdList(test_method_id_list)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
508 509
    self.reindexObject()

Yoshinori Okuji's avatar
Yoshinori Okuji committed
510
  security.declareProtected(Permissions.AccessContentsInformation, 'generatePredicate')
511
  def generatePredicate(self, multimembership_criterion_base_category_list=(),
512 513
                        membership_criterion_base_category_list=(),
                        criterion_property_list=()):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
514
    """
515
    This method generates a new temporary predicate based on an ad-hoc
516
    interpretation of local properties of an object. For example,
517
    a start_range_min property will be interpreted as a way to define
518
    a min criterion on start_date.
519

520
    The purpose of this method is to be called from
521 522 523
    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
524
    """
525
    new_membership_criterion_category_list = list(self.getMembershipCriterionCategoryList())
526
    new_membership_criterion_base_category_list = list(self.getMembershipCriterionBaseCategoryList())
527
    new_multimembership_criterion_base_category_list = list(self.getMultimembershipCriterionBaseCategoryList())
528

529
    for base_category in multimembership_criterion_base_category_list:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
530 531 532
      category_list = self.getProperty(base_category + '_list')
      if category_list is not None and len(category_list)>0:
        for category in category_list:
533
          new_membership_criterion_category_list.append(base_category + '/' + category)
534
        if base_category not in new_multimembership_criterion_base_category_list:
535
          new_multimembership_criterion_base_category_list.append(base_category)
536

537 538 539 540 541
    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)
542
        if base_category not in new_membership_criterion_base_category_list:
543
          new_membership_criterion_base_category_list.append(base_category)
544

545
    new_criterion_property_list =  list(self.getCriterionPropertyList())
546 547 548 549 550 551 552 553 554 555 556 557 558 559 560

    # 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.
    identity_criterion = getattr(self, '_identity_criterion', None)
    if identity_criterion is None:
      identity_criterion = {}
    else:
      identity_criterion = dict(identity_criterion)
    range_criterion = getattr(self, '_range_criterion', None)
    if range_criterion is None:
      range_criterion = {}
    else:
      range_criterion = dict(range_criterion)

Jean-Paul Smets's avatar
Jean-Paul Smets committed
561
    # Look at local properties and make it criterion properties
562
    for property in criterion_property_list:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
563 564
      if property not in self.getCriterionPropertyList() \
        and property in self.propertyIds():
565
          new_criterion_property_list.append(property)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
566 567
          property_min = property + '_range_min'
          property_max = property + '_range_max'
568
          if getattr(self, 'get%s' % convertToUpperCase(property), None) is not None\
Jean-Paul Smets's avatar
Jean-Paul Smets committed
569 570
            and self.getProperty(property) is not None:
            identity_criterion[property] = self.getProperty(property)
571
          elif getattr(self, 'get%s' % convertToUpperCase(property_min), None) is not None:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
572 573 574 575 576 577
            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(
578
        membership_criterion_category=new_membership_criterion_category_list,
579
        membership_criterion_base_category=new_membership_criterion_base_category_list,
580 581
        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
582 583 584
        _identity_criterion=identity_criterion,
        _range_criterion=range_criterion)

585 586 587
    return new_self

  # Predicate handling
588 589 590
  security.declareProtected(Permissions.AccessContentsInformation,
                            'asPredicate')
  def asPredicate(self, script_id=None):
591
    """
592
      This method tries to convert the current Document into a predicate
593 594
      looking up methods named ${PortalType}_asPredicate,
      ${MetaType}_asPredicate, ${Class}_asPredicate     
595
    """
596 597 598 599
    if script_id is not None:
      script = getattr(self, script_id, None)
    else:
      script = self._getTypeBasedMethod('asPredicate')
600
    if script is not None:
601 602
      return script()
    return self
603 604 605 606

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

      TO BE IMPLEMENTED using portal_catalog(**kw)
609
    """
610
    pass
611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643

  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))