Selection.py 18.6 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
    def __call__(self, method = None, context=None, REQUEST=None):
189
        #LOG("Selection", 0, str((self.__dict__)))
190 191
        #LOG("Selection", 0, str(method))
        #LOG('Selection', 0, "self.invert_mode = %s" % repr(self.invert_mode))
192
        kw = self.params.copy()
193 194
        # Always remove '-C'-named parameter.
        kw.pop('-C', None)
195
        if self.invert_mode is not 0:
196
          kw['uid'] = self.uids
197
        if method is None or isinstance(method, str):
198 199 200 201 202 203 204 205
          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:
206 207
          kw['sort_on'] = sort_on
        elif kw.has_key('sort_on'):
208
          del kw['sort_on'] # We should not sort if no sort was defined
209 210
        # We should always set selection_name with self.name
        kw['selection_name'] = self.name
211 212
        # XXX: Use of selection parameter is deprecated. Use selection_name
        # instead, and access the selection via selection_tool.
213 214 215 216 217 218 219
        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
220
        else:
221 222
          result = []
        return result
Jean-Paul Smets's avatar
Jean-Paul Smets committed
223 224 225 226

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

227 228
    security.declarePublic('getName')
    def getName(self):
229 230 231
        """
          Get the name of this selection.
        """
232
        return self.name
233

234 235
    security.declarePublic('getIndex')
    def getIndex(self):
236 237 238
        """
          Get the index of this selection.
        """
239
        return self.index
240

241 242 243 244 245 246 247
    security.declarePublic('getDomain')
    def getDomain(self):
        """
          Get the domain selection of this selection.
        """
        return self.domain

248 249 250 251 252 253 254
    security.declarePublic('getReport')
    def getReport(self):
        """
          Get the report selection of this selection.
        """
        return self.report

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

267 268 269 270 271 272 273
    security.declarePublic('getSortOrder')
    def getSortOrder(self):
        """
          Return sort order stored in selection
        """
        return self.sort_on

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

284 285 286 287 288 289 290 291 292
    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')
293
    def getDomainPath(self, default=None):
294
        if self.domain_path is None:
295 296
          if default is None:
            self.domain_path = self.getDomainList()[0]
297
          else:
298
            self.domain_path = default
299 300 301 302 303 304 305 306 307
        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')
308
    def getReportPath(self, default=None):
309
        if self.report_path is None:
310 311
          if default is None:
            self.report_path = self.getReportList()[0]
312
          else:
313
            self.report_path = default
314
        return self.report_path
315

316 317 318 319 320
    security.declarePublic('getZoom')
    def getZoom(self):
      try:
        current_zoom=self.params['zoom']
        if current_zoom != None:
321
          return current_zoom
322
        else:
323
          return 1
Yoshinori Okuji's avatar
Yoshinori Okuji committed
324
      except KeyError:
325
        return 1
326

327 328 329 330 331 332
    security.declarePublic('getReportList')
    def getReportList(self):
        if self.report_list is None:
          self.report_list = (('portal_categories',),)
        return self.report_list

333 334 335 336 337 338
    security.declarePublic('isReportOpened')
    def isReportOpened(self):
        if self.report_opened is None:
          self.report_opened = 1
        return self.report_opened

339 340 341
    security.declarePublic('isInvertMode')
    def isInvertMode(self):
        return self.invert_mode
342

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

    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)

355
InitializeClass(Selection)
356 357
allow_class(Selection)

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

365
    Example 1: (hand coded)
366

367 368 369 370 371
    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.

372 373 374 375 376
    <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)
377

378 379 380
    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.
381

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

385 386
    Example 3: (mixed)

387 388 389 390
    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
391
    <dtml-var "selection.domain.eip.asSQLExpresion(table="resource_category")">
392

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

398 399
  security = ClassSecurityInfo()
  security.declareObjectPublic()
400

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

  def __len__(self):
410 411
    return len(self.domain_dict)

412 413
  security.declarePublic('getCategoryList')
  def getCategoryList(self):
414
    return
415

416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439
  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
440
  security.declarePublic('asSQLExpression')
441
  def asSQLExpression(self, table_map=None, domain_id=None,
442
                      exclude_domain_id=None, strict_membership=0,
443
                      join_table="catalog", join_column="uid",
444
                      base_category=None, category_table_alias='category'):
445
    select_expression = []
446 447 448 449
    portal = self.getPortalObject()
    for k, d in self.domain_dict.iteritems():
      d = self._getDomainObject(portal, d)

450 451
      if k == 'parent':
        # Special treatment for parent
452 453 454
        select_expression.append(
            d.getParentSQLExpression(table='catalog',
                                     strict_membership=strict_membership))
455 456
      elif k is not None:
        if getattr(aq_base(d), 'isPredicate', 0):
457 458 459
          select_expression.append(
              d.asSQLExpression(table='%s_%s' % (k, category_table_alias),
                                strict_membership=strict_membership))
460 461
        else:
          # This is a category, we must join
462
          select_expression.append('%s.%s = %s_%s.uid' % \
463
                                (join_table, join_column,
464 465 466
                                 k, category_table_alias))
          select_expression.append(
              d.asSQLExpression(table='%s_%s' % (k, category_table_alias),
467
                                base_category=k,
468 469 470 471 472 473 474
                                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 = ''
475
    #LOG('DomainSelection', 0, 'asSQLExpression returns %r' % (result,))
476
    return result
477

478 479 480
  # Compatibility SQL Sql
  security.declarePublic('asSqlExpression')
  asSqlExpression = asSQLExpression
481

Jérome Perrin's avatar
Jérome Perrin committed
482
  security.declarePublic('asSQLJoinExpression')
483
  def asSQLJoinExpression(self, domain_id=None, exclude_domain_id=None,
484
                          category_table_alias='category'):
485
    join_expression = []
486
    #LOG('DomainSelection', 0, 'domain_id = %r, exclude_domain_id = %r, self.domain_dict = %r' % (domain_id, exclude_domain_id, self.domain_dict))
487 488 489 490
    portal = self.getPortalObject()
    for k, d in self.domain_dict.iteritems():
      d = self._getDomainObject(portal, d)

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

503 504 505 506
  # Compatibility SQL Sql
  security.declarePublic('asSqlJoinExpression')
  asSqlJoinExpression = asSQLJoinExpression

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

  security.declarePublic('asDomainItemDict')
  def asDomainItemDict(self, domain_id=None, exclude_domain_id=None):
513 514 515 516 517
    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
518 519 520

  security.declarePublic('updateDomain')
  def updateDomain(self, domain):
521 522
    pass

523
InitializeClass(DomainSelection)
524
allow_class(DomainSelection)