Selection.py 18.4 KB
Newer Older
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1 2
##############################################################################
#
3
# Copyright (c) 2002,2007 Nexedi SARL and Contributors. All Rights Reserved.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
4
#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5 6
#
# WARNING: This program as such is intended to be used by professional
7
# programmers who take the whole responsibility of assessing all potential
Jean-Paul Smets's avatar
Jean-Paul Smets committed
8 9
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
10
# guarantees and support are strongly adviced to contract a Free Software
Jean-Paul Smets's avatar
Jean-Paul Smets committed
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
# 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 Globals import InitializeClass, Persistent, Acquisition
30
from Acquisition import aq_base, aq_inner, aq_parent, aq_self
Jean-Paul Smets's avatar
Jean-Paul Smets committed
31 32 33 34
from OFS.SimpleItem import SimpleItem
from OFS.Traversable import Traversable
from AccessControl import ClassSecurityInfo
from Products.ERP5Type import Permissions as ERP5Permissions
35
from Products.PythonScripts.Utility import allow_class
Jean-Paul Smets's avatar
Jean-Paul Smets committed
36 37
import string

38 39 40 41
# Put a try in front XXX
from Products.CMFCategory.Category import Category
from Products.ERP5.Document.Domain import Domain

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

44 45
from Products.ERP5Type.Tool.MemcachedTool import MEMCACHED_TOOL_MODIFIED_FLAG_PROPERTY_ID

Jean-Paul Smets's avatar
Jean-Paul Smets committed
46 47 48 49 50 51 52 53 54 55
class Selection(Acquisition.Implicit, Traversable, Persistent):
    """
        Selection

        A Selection instance allows a ListBox object to browse the data
        resulting from a method call such as an SQL Method Call. Selection
        instances are used to implement persistent selections in ERP5.

        Selection uses the following control variables

56
        - method      --  a method which will be used
Jean-Paul Smets's avatar
Jean-Paul Smets committed
57 58
                                    to select objects

59
        - params      --  a dictionnary of parameters to call the
Jean-Paul Smets's avatar
Jean-Paul Smets committed
60 61
                                    method with

62
        - sort_on     --  a dictionnary of parameters to sort
Jean-Paul Smets's avatar
Jean-Paul Smets committed
63 64
                                    the selection

65
        - uids        --  a list of object uids which defines the
Jean-Paul Smets's avatar
Jean-Paul Smets committed
66 67
                                    selection

68
        - invert_mode --  defines the mode of the selection
Jean-Paul Smets's avatar
Jean-Paul Smets committed
69 70 71
                                    if mode is 1, then only show the
                                    ob

72
        - list_url    --  the URL to go back to list mode
Jean-Paul Smets's avatar
Jean-Paul Smets committed
73

74
        - checked_uids --  a list of uids checked
Jean-Paul Smets's avatar
Jean-Paul Smets committed
75

76
        - domain_path --  the path to the root of the selection tree
Jean-Paul Smets's avatar
Jean-Paul Smets committed
77 78


79 80
        - domain_list --  the relative path of the current selected domain
                                    XXX this will have to be updated for cartesion product
Jean-Paul Smets's avatar
Jean-Paul Smets committed
81

82
        - report_path --  the report path
Jean-Paul Smets's avatar
Jean-Paul Smets committed
83

84 85
        - report_list -- list of open report nodes
                                    XXX this will have to be updated for cartesion product
86 87 88 89

        - domain                -- a DomainSelection instance

        - report                -- a DomainSelection instance
Jean-Paul Smets's avatar
Jean-Paul Smets committed
90

91
        - flat_list_mode  --
92 93 94 95 96

        - domain_tree_mode --

        - report_tree_mode --

Jean-Paul Smets's avatar
Jean-Paul Smets committed
97
    """
98

99
    method_path=None
100
    params=None
101
    sort_on=()
102
    default_sort_on=()
103 104 105 106 107 108 109 110 111 112 113 114 115
    uids=()
    invert_mode=0
    list_url=''
    columns=()
    checked_uids=()
    name=None
    index=None
    domain_path = ('portal_categories',)
    domain_list = ((),)
    report_path = ('portal_categories',)
    report_list = ((),)
    domain=None
    report=None
116
    report_opened=None
117

Jean-Paul Smets's avatar
Jean-Paul Smets committed
118
    security = ClassSecurityInfo()
119
    security.declareObjectPublic()
Jean-Paul Smets's avatar
Jean-Paul Smets committed
120

121
    security.declarePublic('domain')
122 123
    security.declarePublic('report')

124 125 126
    def getId(self):
      return self.name
      
127
    def __init__(self, method_path=None, params=None, sort_on=None, default_sort_on=None,
128
                 uids=None, invert_mode=0, list_url='', domain=None, report=None,
129
                 columns=None, checked_uids=None, name=None, index=None):
130 131
        if params is None: params = {}
        if sort_on is None: sort_on = []
132
        if default_sort_on is None: default_sort_on = []
133 134 135
        if uids is None: uids = []
        if columns is None: columns = []
        if checked_uids is None: checked_uids = []
136 137 138 139
        # XXX Because method_path is an URI, it must be in ASCII.
        #     Shouldn't Zope automatically does this conversion? -yo
        if type(method_path) is type(u'a'):
          method_path = method_path.encode('ascii')
140 141 142 143 144 145 146 147 148 149 150 151
        self.method_path = method_path
        self.params = params
        self.uids = uids
        self.invert_mode = invert_mode
        self.list_url = list_url
        self.columns = columns
        self.sort_on = sort_on
        self.default_sort_on = default_sort_on
        self.checked_uids = checked_uids
        self.name = name
        self.index = index
        self.domain_path = ('portal_categories',)
152
        self.domain_list = ()
153
        self.report_path = None
154
        self.report_list = ()
155 156
        self.domain = None
        self.report = None
157
        self.report_opened = None
158 159

    security.declarePrivate('edit')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
160
    def edit(self, params=None, **kw):
161
        setattr(self, MEMCACHED_TOOL_MODIFIED_FLAG_PROPERTY_ID, True)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
162
        if params is not None:
163 164 165 166 167 168 169
          # We should only keep params which do not start with field_
          # in order to make sure we do not collect unwanted params
          # resulting form the REQUEST generated by an ERP5Form submit
          params = dict([item for item in params.iteritems() \
                         if not item[0].startswith('field_')])
          if self.params != params:
            self.params = params
Jean-Paul Smets's avatar
Jean-Paul Smets committed
170
        if kw is not None:
171
          for k,v in kw.iteritems():
172
            if k in ('domain', 'report', 'domain_path', 'report_path', 'domain_list', 'report_list') or v is not None:
173 174
              # XXX Because method_path is an URI, it must be in ASCII.
              #     Shouldn't Zope automatically does this conversion? -yo
175
              if k == 'method_path' and isinstance(v, unicode):
176
                v = v.encode('ascii')
177 178
              if getattr(self, k, None) != v:
                setattr(self, k, v)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
179

180 181 182 183 184 185 186 187 188
    def _p_independent(self) :
      return 1

    def _p_resolveConflict(self, oldState, savedState, newState) :
      """Selection are edited by listboxs, so many conflicts can happen,
         this is a workaround, so that no unnecessary transaction is
         restarted."""
      return newState

189
    def __call__(self, method = None, context=None, REQUEST=None):
190
        #LOG("Selection", 0, str((self.__dict__)))
191 192
        #LOG("Selection", 0, str(method))
        #LOG('Selection', 0, "self.invert_mode = %s" % repr(self.invert_mode))
193 194
        kw = self.params.copy()
        if self.invert_mode is not 0:
195
          kw['uid'] = self.uids
196
        if method is None or isinstance(method, str):
197 198 199 200 201 202 203 204
          method_path = method or self.method_path
          method = context.unrestrictedTraverse(method_path)
        if type(method) is type('a'):
          method = context.unrestrictedTraverse(self.method_path)
        sort_on = getattr(self, 'sort_on', [])
        if len(sort_on) == 0:
          sort_on = getattr(self, 'default_sort_on', [])
        if len(sort_on) > 0:
205 206
          kw['sort_on'] = sort_on
        elif kw.has_key('sort_on'):
207
          del kw['sort_on'] # We should not sort if no sort was defined
208 209
        # We should always set selection_name with self.name
        kw['selection_name'] = self.name
210 211 212 213
        if method is not None:
          if callable(method):
            if self.domain is not None and self.report is not None:
              result = method(selection_domain = self.domain,
214
                              selection_report = self.report, selection=self, **kw)
215
            elif self.domain is not None:
216
              result = method(selection_domain = self.domain, selection=self, **kw)
217
            elif self.report is not None:
218
              result = method(selection_report = self.report, selection=self, **kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
219
            else:
220
              result = method(selection=self, **kw)
221
            return result
Jean-Paul Smets's avatar
Jean-Paul Smets committed
222 223 224
          else:
            return []
        else:
225
          return []
Jean-Paul Smets's avatar
Jean-Paul Smets committed
226 227 228 229

    def __getitem__(self, index, REQUEST=None):
        return self(REQUEST)[index]

230 231
    security.declarePublic('getName')
    def getName(self):
232 233 234
        """
          Get the name of this selection.
        """
235
        return self.name
236

237 238
    security.declarePublic('getIndex')
    def getIndex(self):
239 240 241
        """
          Get the index of this selection.
        """
242
        return self.index
243

244 245 246 247 248 249 250
    security.declarePublic('getDomain')
    def getDomain(self):
        """
          Get the domain selection of this selection.
        """
        return self.domain

251 252 253 254 255 256 257
    security.declarePublic('getReport')
    def getReport(self):
        """
          Get the report selection of this selection.
        """
        return self.report

258 259
    security.declarePublic('getParams')
    def getParams(self):
260 261 262
        """
          Get a dictionary of parameters in this selection.
        """
Yoshinori Okuji's avatar
Yoshinori Okuji committed
263
        #LOG('getParams',0,'params: %s' % str(self.params))
264 265 266 267 268 269
        if self.params is None:
          self.params = {}
        if type(self.params) != type({}):
          self.params = {}
        return self.params

270 271 272 273 274 275 276
    security.declarePublic('getSortOrder')
    def getSortOrder(self):
        """
          Return sort order stored in selection
        """
        return self.sort_on

277 278
    security.declarePublic('getListUrl')
    def getListUrl(self):
Sebastien Robin's avatar
Sebastien Robin committed
279
        result = ''
Yoshinori Okuji's avatar
Yoshinori Okuji committed
280
        #LOG('getListUrl', 0, 'list_url = %s' % str(self.list_url))
281 282
        if self.list_url is None:
          self.list_url = ''
Sebastien Robin's avatar
Sebastien Robin committed
283
        else:
284
          result = self.list_url
Sebastien Robin's avatar
Sebastien Robin committed
285
        return result
Jean-Paul Smets's avatar
Jean-Paul Smets committed
286

287 288 289 290 291 292 293 294 295
    security.declarePublic('getCheckedUids')
    def getCheckedUids(self):
        if not hasattr(self, 'checked_uids'):
          self.checked_uids = []
        elif self.checked_uids is None:
          self.checked_uids = []
        return self.checked_uids

    security.declarePublic('getDomainPath')
296
    def getDomainPath(self, default=None):
297
        if self.domain_path is None:
298 299
          if default is None:
            self.domain_path = self.getDomainList()[0]
300
          else:
301
            self.domain_path = default
302 303 304 305 306 307 308 309 310
        return self.domain_path

    security.declarePublic('getDomainList')
    def getDomainList(self):
        if self.domain_list is None:
          self.domain_list = (('portal_categories',),)
        return self.domain_list

    security.declarePublic('getReportPath')
311
    def getReportPath(self, default=None):
312
        if self.report_path is None:
313 314
          if default is None:
            self.report_path = self.getReportList()[0]
315
          else:
316
            self.report_path = default
317
        return self.report_path
318

319 320 321 322 323
    security.declarePublic('getZoom')
    def getZoom(self):
      try:
        current_zoom=self.params['zoom']
        if current_zoom != None:
Yoshinori Okuji's avatar
Yoshinori Okuji committed
324
          return current_zoom 
325 326
        else:
          return 1  
Yoshinori Okuji's avatar
Yoshinori Okuji committed
327
      except KeyError:
328 329
        return 1
    
330 331 332 333 334 335
    security.declarePublic('getReportList')
    def getReportList(self):
        if self.report_list is None:
          self.report_list = (('portal_categories',),)
        return self.report_list

336 337 338 339 340 341
    security.declarePublic('isReportOpened')
    def isReportOpened(self):
        if self.report_opened is None:
          self.report_opened = 1
        return self.report_opened

342 343 344 345
    security.declarePublic('isInvertMode')
    def isInvertMode(self):
        return self.invert_mode
 
Jérome Perrin's avatar
Jérome Perrin committed
346 347 348 349 350
    security.declarePublic('getInvertModeUidList')
    def getInvertModeUidList(self):
        return self.uids
     
 
351
InitializeClass(Selection)
352 353
allow_class(Selection)

354 355 356
class DomainSelection(Acquisition.Implicit, Traversable, Persistent):
  """
    A class to store a selection of domains which defines a report
357 358 359
    section. There are different ways to use DomainSelection in 
    SQL methods. As a general principle, SQL methods are passed
    DomainSelection instances as a parameter.
360

361
    Example 1: (hand coded)
362

363 364 365 366 367
    The domain is accessed directly from the selection and a list of
    uids is gathered from the ZODB to feed the SQL request. This
    approach is only suitable for categories and relations. It is
    not suitable for predicates. Do not use it unless there is no other way.

368 369 370 371 372
    <dtml-if selection.domain.eip>
      <dtml-in "selection.domain.eip.getCategoryChildUidList()">uid = <dtml-sqlvar sequence-item type="int"></dtml-in>
    </dtml-if>

    Example 2: (auto generated)
373 374 375 376
    
    The domain object is in charge of generating automatically all
    SQL expressions to feed the SQL method (or the catalog). This
    is the recommended approach.
377

Jérome Perrin's avatar
Jérome Perrin committed
378 379
    <dtml-var "selection.domain.asSQLExpression(table_map=(('eip','movement'), ('group', 'catalog')))">
    <dtml-var "selection.domain.asSQLJoinExpression(table_map=(('eip','movement'), ('group', 'catalog')))">
380

381 382
    Example 3: (mixed)

383 384 385 386
    The category or predicate of the domain object is accessed. SQL
    code generation is invoked on it. This is better than the manual
    approach.

Jérome Perrin's avatar
Jérome Perrin committed
387
    <dtml-var "selection.domain.eip.asSQLExpresion(table="resource_category")">
388

389 390 391
    Current implementation is only suitable for categories.
    It needs to be extended to support also predicates. The right approach
    would be to turn any category into a predicate.
392 393
  """

394 395
  security = ClassSecurityInfo()
  security.declareObjectPublic()
396

397
  def __init__(self, domain_dict = None):
398
    #LOG('DomainSelection', 0, '__init__ is called with %r' % (domain_dict,))
399 400
    if domain_dict is not None:
      self.domain_dict = domain_dict
401
      for k, v in domain_dict.iteritems():
402 403 404 405
        if k is not None:
          setattr(self, k, v)

  def __len__(self):
406 407
    return len(self.domain_dict)

408 409
  security.declarePublic('getCategoryList')
  def getCategoryList(self):
410
    return
411

412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435
  def _getDomainObject(self, portal, domain):
    """Return a domain or category object.
    """
    if isinstance(domain, tuple):
      # This is the new form. The first item describes the name of a tool or
      # None if a domain is under a module. The second item is the relative
      # URL of a domain.
      tool = domain[0]
      if tool is None:
        obj = portal.restrictedTraverse(domain[1])
      elif tool == 'portal_domains':
        # Special case, as Domain Tool may generate a domain dynamically.
        obj = portal.portal_domains.getDomainByPath(domain[1])
      else:
        obj = portal[tool].restrictedTraverse(domain[1])
    elif isinstance(domain, str):
      # XXX backward compatibility: a domain was represented by a string previously.
      obj = portal.portal_domains.getDomainByPath(domain)
    else:
      # XXX backward compatibility: a category was represented by an object itself.
      obj = aq_base(domain).__of__(portal)

    return obj

Jérome Perrin's avatar
Jérome Perrin committed
436 437
  security.declarePublic('asSQLExpression')
  def asSQLExpression(self, table_map=None, domain_id=None, 
438
                      exclude_domain_id=None, strict_membership=0,
439 440
                      join_table="catalog", join_column="uid", 
                      base_category=None, category_table_alias='category'):
441
    select_expression = []
442 443 444 445
    portal = self.getPortalObject()
    for k, d in self.domain_dict.iteritems():
      d = self._getDomainObject(portal, d)

446 447
      if k == 'parent':
        # Special treatment for parent
448 449 450
        select_expression.append(
            d.getParentSQLExpression(table='catalog',
                                     strict_membership=strict_membership))
451 452
      elif k is not None:
        if getattr(aq_base(d), 'isPredicate', 0):
453 454 455
          select_expression.append(
              d.asSQLExpression(table='%s_%s' % (k, category_table_alias),
                                strict_membership=strict_membership))
456 457
        else:
          # This is a category, we must join
458
          select_expression.append('%s.%s = %s_%s.uid' % \
459 460 461 462
                                (join_table, join_column, 
                                 k, category_table_alias))
          select_expression.append(
              d.asSQLExpression(table='%s_%s' % (k, category_table_alias),
463
                                base_category=k,
464 465 466 467 468 469 470
                                strict_membership=strict_membership))
                                # XXX We should take into account k explicitely
                                # if we want to support category acquisition
    if select_expression:
      result = "( %s )" % ' AND '.join(select_expression)
    else:
      result = ''
471
    #LOG('DomainSelection', 0, 'asSQLExpression returns %r' % (result,))
472
    return result
473

474 475 476 477
  # Compatibility SQL Sql
  security.declarePublic('asSqlExpression')
  asSqlExpression = asSQLExpression
  
Jérome Perrin's avatar
Jérome Perrin committed
478
  security.declarePublic('asSQLJoinExpression')
479 480
  def asSQLJoinExpression(self, domain_id=None, exclude_domain_id=None, 
                          category_table_alias='category'):
481
    join_expression = []
482
    #LOG('DomainSelection', 0, 'domain_id = %r, exclude_domain_id = %r, self.domain_dict = %r' % (domain_id, exclude_domain_id, self.domain_dict))
483 484 485 486
    portal = self.getPortalObject()
    for k, d in self.domain_dict.iteritems():
      d = self._getDomainObject(portal, d)

487 488
      if k == 'parent':
        pass
489 490
      elif k is not None:
        if getattr(aq_base(d), 'isPredicate', 0):
491
          join_expression.append(d.asSQLJoinExpression(table='%s_%s' % (k, category_table_alias)))
492 493
        else:
          # This is a category, we must join
494
          join_expression.append('category AS %s_%s' % (k, category_table_alias))
495
    result = "%s" % ' , '.join(join_expression)
Jérome Perrin's avatar
Jérome Perrin committed
496
    #LOG('DomainSelection', 0, 'asSQLJoinExpression returns %r' % (result,))
497 498
    return result

499 500 501 502
  # Compatibility SQL Sql
  security.declarePublic('asSqlJoinExpression')
  asSqlJoinExpression = asSQLJoinExpression

503 504
  security.declarePublic('asDomainDict')
  def asDomainDict(self, domain_id=None, exclude_domain_id=None):
505
    return self.domain_dict
506 507 508 509 510 511 512

  security.declarePublic('asDomainItemDict')
  def asDomainItemDict(self, domain_id=None, exclude_domain_id=None):
    pass

  security.declarePublic('updateDomain')
  def updateDomain(self, domain):
513 514
    pass

515
InitializeClass(DomainSelection)
516
allow_class(DomainSelection)