Selection.py 19.2 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
# 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 30
from Products.ERP5Type.Globals import InitializeClass, Persistent
import Acquisition
31
from Acquisition import aq_base
Jean-Paul Smets's avatar
Jean-Paul Smets committed
32 33 34
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 38 39 40
# 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
41 42
from zLOG import LOG

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

Jean-Paul Smets's avatar
Jean-Paul Smets committed
45 46 47 48 49 50 51 52 53 54
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

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

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

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

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

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

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

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

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


78 79
        - 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
80

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

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

        - domain                -- a DomainSelection instance

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

90
        - flat_list_mode  --
91 92 93 94 95

        - domain_tree_mode --

        - report_tree_mode --

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

98
    method_path=None
99
    params=None
100
    sort_on=()
101
    default_sort_on=()
102 103 104 105 106 107 108 109 110 111 112 113 114
    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
115
    report_opened=None
116

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

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

123 124
    def getId(self):
      return self.name
125

126
    def __init__(self, method_path=None, params=None, sort_on=None, default_sort_on=None,
127
                 uids=None, invert_mode=0, list_url='', domain=None, report=None,
128
                 columns=None, checked_uids=None, name=None, index=None):
129 130
        if params is None: params = {}
        if sort_on is None: sort_on = []
131
        if default_sort_on is None: default_sort_on = []
132 133 134
        if uids is None: uids = []
        if columns is None: columns = []
        if checked_uids is None: checked_uids = []
135 136 137 138
        # 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')
139 140 141 142 143 144 145 146 147 148 149 150
        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',)
151
        self.domain_list = ()
152
        self.report_path = None
153
        self.report_list = ()
154 155
        self.domain = None
        self.report = None
156
        self.report_opened = None
157 158

    security.declarePrivate('edit')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
159
    def edit(self, params=None, **kw):
160
        setattr(self, MEMCACHED_TOOL_MODIFIED_FLAG_PROPERTY_ID, True)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
161
        if params is not None:
162 163 164 165 166 167 168
          # 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
169
        if kw is not None:
170
          for k,v in kw.iteritems():
171
            if k in ('domain', 'report', 'domain_path', 'report_path', 'domain_list', 'report_list') or v is not None:
172 173
              # XXX Because method_path is an URI, it must be in ASCII.
              #     Shouldn't Zope automatically does this conversion? -yo
174
              if k == 'method_path' and isinstance(v, unicode):
175
                v = v.encode('ascii')
176 177
              if getattr(self, k, None) != v:
                setattr(self, k, v)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
178

179 180 181 182 183 184 185 186 187
    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

188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
    def __call__(self, method=None, context=None, REQUEST=None, params=None):
        """
        Calls the selection and return the list of selected documents
        or objects. Seledction method, context and parameters may be 
        overriden in a non persistent way.

        method -- optional method (callable) or method path (string)
                  to use instead of the persistent selection method

        context -- optional context to call the selection method on

        REQUEST -- optional REQUEST parameters (not used, only to 
                   provide API compatibility)

        params -- optional parameters which can be used to override
                  default params
        """
205
        #LOG("Selection", 0, str((self.__dict__)))
206 207
        #LOG("Selection", 0, str(method))
        #LOG('Selection', 0, "self.invert_mode = %s" % repr(self.invert_mode))
208 209 210 211
        if not params:
          kw = self.params.copy()
        else:
          kw = params.copy()
212 213
        # Always remove '-C'-named parameter.
        kw.pop('-C', None)
214
        if self.invert_mode is not 0:
215
          kw['uid'] = self.uids
216
        if method is None or isinstance(method, str):
217 218 219 220 221 222 223 224
          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:
225 226
          kw['sort_on'] = sort_on
        elif kw.has_key('sort_on'):
227
          del kw['sort_on'] # We should not sort if no sort was defined
228 229
        # We should always set selection_name with self.name
        kw['selection_name'] = self.name
230 231
        # XXX: Use of selection parameter is deprecated. Use selection_name
        # instead, and access the selection via selection_tool.
232 233 234 235 236 237 238
        kw['selection'] = self
        if self.domain is not None:
          kw['selection_domain'] = self.domain
        if self.report is not None:
          kw['selection_report'] = self.report
        if callable(method):
          result = method(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
239
        else:
240 241
          result = []
        return result
Jean-Paul Smets's avatar
Jean-Paul Smets committed
242 243 244 245

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

246 247
    security.declarePublic('getName')
    def getName(self):
248 249 250
        """
          Get the name of this selection.
        """
251
        return self.name
252

253 254
    security.declarePublic('getIndex')
    def getIndex(self):
255 256 257
        """
          Get the index of this selection.
        """
258
        return self.index
259

260 261 262 263 264 265 266
    security.declarePublic('getDomain')
    def getDomain(self):
        """
          Get the domain selection of this selection.
        """
        return self.domain

267 268 269 270 271 272 273
    security.declarePublic('getReport')
    def getReport(self):
        """
          Get the report selection of this selection.
        """
        return self.report

274 275
    security.declarePublic('getParams')
    def getParams(self):
276 277 278
        """
          Get a dictionary of parameters in this selection.
        """
279
        if not isinstance(self.params, dict):
280
          self.params = {}
281
        return self.params.copy()
282

283 284 285 286 287 288 289
    security.declarePublic('getSortOrder')
    def getSortOrder(self):
        """
          Return sort order stored in selection
        """
        return self.sort_on

290 291
    security.declarePublic('getListUrl')
    def getListUrl(self):
Sebastien Robin's avatar
Sebastien Robin committed
292
        result = ''
Yoshinori Okuji's avatar
Yoshinori Okuji committed
293
        #LOG('getListUrl', 0, 'list_url = %s' % str(self.list_url))
294 295
        if self.list_url is None:
          self.list_url = ''
Sebastien Robin's avatar
Sebastien Robin committed
296
        else:
297
          result = self.list_url
Sebastien Robin's avatar
Sebastien Robin committed
298
        return result
Jean-Paul Smets's avatar
Jean-Paul Smets committed
299

300 301 302 303 304 305 306 307 308
    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')
309
    def getDomainPath(self, default=None):
310
        if self.domain_path is None:
311 312
          if default is None:
            self.domain_path = self.getDomainList()[0]
313
          else:
314
            self.domain_path = default
315 316 317 318 319 320 321 322 323
        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')
324
    def getReportPath(self, default=None):
325
        if self.report_path is None:
326 327
          if default is None:
            self.report_path = self.getReportList()[0]
328
          else:
329
            self.report_path = default
330
        return self.report_path
331

332 333 334 335 336
    security.declarePublic('getZoom')
    def getZoom(self):
      try:
        current_zoom=self.params['zoom']
        if current_zoom != None:
337
          return current_zoom
338
        else:
339
          return 1
Yoshinori Okuji's avatar
Yoshinori Okuji committed
340
      except KeyError:
341
        return 1
342

343 344 345 346 347 348
    security.declarePublic('getReportList')
    def getReportList(self):
        if self.report_list is None:
          self.report_list = (('portal_categories',),)
        return self.report_list

349 350 351 352 353 354
    security.declarePublic('isReportOpened')
    def isReportOpened(self):
        if self.report_opened is None:
          self.report_opened = 1
        return self.report_opened

355 356 357
    security.declarePublic('isInvertMode')
    def isInvertMode(self):
        return self.invert_mode
358

Jérome Perrin's avatar
Jérome Perrin committed
359 360 361
    security.declarePublic('getInvertModeUidList')
    def getInvertModeUidList(self):
        return self.uids
362 363 364 365 366 367 368 369 370

    security.declarePublic('getDomainTreeMode')
    def getDomainTreeMode(self):
        return getattr(self, 'domain_tree_mode', 0)

    security.declarePublic('getReportTreeMode')
    def getReportTreeMode(self):
        return getattr(self, 'report_tree_mode', 0)

371
InitializeClass(Selection)
372 373
allow_class(Selection)

374 375 376
class DomainSelection(Acquisition.Implicit, Traversable, Persistent):
  """
    A class to store a selection of domains which defines a report
377
    section. There are different ways to use DomainSelection in
378 379
    SQL methods. As a general principle, SQL methods are passed
    DomainSelection instances as a parameter.
380

381
    Example 1: (hand coded)
382

383 384 385 386 387
    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.

388 389 390 391 392
    <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)
393

394 395 396
    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.
397

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

401 402
    Example 3: (mixed)

403 404 405 406
    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
407
    <dtml-var "selection.domain.eip.asSQLExpresion(table="resource_category")">
408

409 410 411
    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.
412 413
  """

414 415
  security = ClassSecurityInfo()
  security.declareObjectPublic()
416

417
  def __init__(self, domain_dict = None):
418
    #LOG('DomainSelection', 0, '__init__ is called with %r' % (domain_dict,))
419 420
    if domain_dict is not None:
      self.domain_dict = domain_dict
421
      for k, v in domain_dict.iteritems():
422 423 424 425
        if k is not None:
          setattr(self, k, v)

  def __len__(self):
426 427
    return len(self.domain_dict)

428 429
  security.declarePublic('getCategoryList')
  def getCategoryList(self):
430
    return
431

432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455
  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
456
  security.declarePublic('asSQLExpression')
457
  def asSQLExpression(self, table_map=None, domain_id=None,
458
                      exclude_domain_id=None, strict_membership=0,
459
                      join_table="catalog", join_column="uid",
460
                      base_category=None, category_table_alias='category'):
461
    select_expression = []
462 463 464 465
    portal = self.getPortalObject()
    for k, d in self.domain_dict.iteritems():
      d = self._getDomainObject(portal, d)

466 467
      if k == 'parent':
        # Special treatment for parent
468 469 470
        select_expression.append(
            d.getParentSQLExpression(table='catalog',
                                     strict_membership=strict_membership))
471 472
      elif k is not None:
        if getattr(aq_base(d), 'isPredicate', 0):
473 474 475
          select_expression.append(
              d.asSQLExpression(table='%s_%s' % (k, category_table_alias),
                                strict_membership=strict_membership))
476 477
        else:
          # This is a category, we must join
478
          select_expression.append('%s.%s = %s_%s.uid' % \
479
                                (join_table, join_column,
480 481 482
                                 k, category_table_alias))
          select_expression.append(
              d.asSQLExpression(table='%s_%s' % (k, category_table_alias),
483
                                base_category=k,
484 485 486 487 488 489 490
                                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 = ''
491
    #LOG('DomainSelection', 0, 'asSQLExpression returns %r' % (result,))
492
    return result
493

494 495 496
  # Compatibility SQL Sql
  security.declarePublic('asSqlExpression')
  asSqlExpression = asSQLExpression
497

Jérome Perrin's avatar
Jérome Perrin committed
498
  security.declarePublic('asSQLJoinExpression')
499
  def asSQLJoinExpression(self, domain_id=None, exclude_domain_id=None,
500
                          category_table_alias='category'):
501
    join_expression = []
502
    #LOG('DomainSelection', 0, 'domain_id = %r, exclude_domain_id = %r, self.domain_dict = %r' % (domain_id, exclude_domain_id, self.domain_dict))
503 504 505 506
    portal = self.getPortalObject()
    for k, d in self.domain_dict.iteritems():
      d = self._getDomainObject(portal, d)

507 508
      if k == 'parent':
        pass
509 510
      elif k is not None:
        if getattr(aq_base(d), 'isPredicate', 0):
511
          join_expression.append(d.asSQLJoinExpression(table='%s_%s' % (k, category_table_alias)))
512 513
        else:
          # This is a category, we must join
514
          join_expression.append('category AS %s_%s' % (k, category_table_alias))
515
    result = "%s" % ' , '.join(join_expression)
Jérome Perrin's avatar
Jérome Perrin committed
516
    #LOG('DomainSelection', 0, 'asSQLJoinExpression returns %r' % (result,))
517 518
    return result

519 520 521 522
  # Compatibility SQL Sql
  security.declarePublic('asSqlJoinExpression')
  asSqlJoinExpression = asSQLJoinExpression

523 524
  security.declarePublic('asDomainDict')
  def asDomainDict(self, domain_id=None, exclude_domain_id=None):
525
    return self.domain_dict
526 527 528

  security.declarePublic('asDomainItemDict')
  def asDomainItemDict(self, domain_id=None, exclude_domain_id=None):
529 530 531 532 533
    domain_item_dict = {}
    portal = self.getPortalObject()
    for k, d in self.domain_dict.iteritems():
      domain_item_dict[k] = self._getDomainObject(portal,d) 
    return domain_item_dict
534 535 536

  security.declarePublic('updateDomain')
  def updateDomain(self, domain):
537 538
    pass

539
InitializeClass(DomainSelection)
540
allow_class(DomainSelection)