Selection.py 17.7 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 44 45 46 47 48 49 50 51 52 53
from zLOG import LOG

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

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

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

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

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

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

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

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

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


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

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

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

        - domain                -- a DomainSelection instance

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

89
        - flat_list_mode  --
90 91 92 93 94

        - domain_tree_mode --

        - report_tree_mode --

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

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

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

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

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

    security.declarePrivate('edit')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
158 159
    def edit(self, params=None, **kw):
        if params is not None:
160
          self.params = {}
Jean-Paul Smets's avatar
Jean-Paul Smets committed
161 162 163 164 165
          for key in params.keys():
            # 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
            if key[0:6] != 'field_':
166
              self.params[key] = params[key]
Jean-Paul Smets's avatar
Jean-Paul Smets committed
167 168
        if kw is not None:
          for k,v in kw.items():
169
            if k in ('domain', 'report', 'domain_path', 'report_path', 'domain_list', 'report_list') or v is not None:
170 171 172 173
              # XXX Because method_path is an URI, it must be in ASCII.
              #     Shouldn't Zope automatically does this conversion? -yo
              if k == 'method_path' and type(v) is type(u'a'):
                v = v.encode('ascii')
174
              setattr(self, k, v)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
175

176
    def __call__(self, method = None, context=None, REQUEST=None):
177
        #LOG("Selection", 0, str((self.__dict__)))
178 179
        #LOG("Selection", 0, str(method))
        #LOG('Selection', 0, "self.invert_mode = %s" % repr(self.invert_mode))
180 181
        kw = self.params.copy()
        if self.invert_mode is not 0:
182
          kw['uid'] = self.uids
183
        if method is None or isinstance(method, str):
184 185 186 187 188 189 190 191
          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:
192 193
          kw['sort_on'] = sort_on
        elif kw.has_key('sort_on'):
194
          del kw['sort_on'] # We should not sort if no sort was defined
195 196
        # We should always set selection_name with self.name
        kw['selection_name'] = self.name
197 198 199 200
        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,
201
                              selection_report = self.report, selection=self, **kw)
202
            elif self.domain is not None:
203
              result = method(selection_domain = self.domain, selection=self, **kw)
204
            elif self.report is not None:
205
              result = method(selection_report = self.report, selection=self, **kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
206
            else:
207
              result = method(selection=self, **kw)
208
            return result
Jean-Paul Smets's avatar
Jean-Paul Smets committed
209 210 211
          else:
            return []
        else:
212
          return []
Jean-Paul Smets's avatar
Jean-Paul Smets committed
213 214 215 216

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

217 218
    security.declarePublic('getName')
    def getName(self):
219 220 221
        """
          Get the name of this selection.
        """
222
        return self.name
223

224 225
    security.declarePublic('getIndex')
    def getIndex(self):
226 227 228
        """
          Get the index of this selection.
        """
229
        return self.index
230

231 232 233 234 235 236 237
    security.declarePublic('getDomain')
    def getDomain(self):
        """
          Get the domain selection of this selection.
        """
        return self.domain

238 239 240 241 242 243 244
    security.declarePublic('getReport')
    def getReport(self):
        """
          Get the report selection of this selection.
        """
        return self.report

245 246
    security.declarePublic('getParams')
    def getParams(self):
247 248 249
        """
          Get a dictionary of parameters in this selection.
        """
Yoshinori Okuji's avatar
Yoshinori Okuji committed
250
        #LOG('getParams',0,'params: %s' % str(self.params))
251 252 253 254 255 256
        if self.params is None:
          self.params = {}
        if type(self.params) != type({}):
          self.params = {}
        return self.params

257 258 259 260 261 262 263
    security.declarePublic('getSortOrder')
    def getSortOrder(self):
        """
          Return sort order stored in selection
        """
        return self.sort_on

264 265
    security.declarePublic('getListUrl')
    def getListUrl(self):
Sebastien Robin's avatar
Sebastien Robin committed
266
        result = ''
Yoshinori Okuji's avatar
Yoshinori Okuji committed
267
        #LOG('getListUrl', 0, 'list_url = %s' % str(self.list_url))
268 269
        if self.list_url is None:
          self.list_url = ''
Sebastien Robin's avatar
Sebastien Robin committed
270
        else:
271
          result = self.list_url
Sebastien Robin's avatar
Sebastien Robin committed
272
        return result
Jean-Paul Smets's avatar
Jean-Paul Smets committed
273

274 275 276 277 278 279 280 281 282
    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')
283
    def getDomainPath(self, default=None):
284
        if self.domain_path is None:
285 286
          if default is None:
            self.domain_path = self.getDomainList()[0]
287
          else:
288
            self.domain_path = default
289 290 291 292 293 294 295 296 297
        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')
298
    def getReportPath(self, default=None):
299
        if self.report_path is None:
300 301
          if default is None:
            self.report_path = self.getReportList()[0]
302
          else:
303
            self.report_path = default
304
        return self.report_path
305

306 307 308 309 310
    security.declarePublic('getZoom')
    def getZoom(self):
      try:
        current_zoom=self.params['zoom']
        if current_zoom != None:
Yoshinori Okuji's avatar
Yoshinori Okuji committed
311
          return current_zoom 
312 313
        else:
          return 1  
Yoshinori Okuji's avatar
Yoshinori Okuji committed
314
      except KeyError:
315 316
        return 1
    
317 318 319 320 321 322
    security.declarePublic('getReportList')
    def getReportList(self):
        if self.report_list is None:
          self.report_list = (('portal_categories',),)
        return self.report_list

323 324 325 326 327 328
    security.declarePublic('isReportOpened')
    def isReportOpened(self):
        if self.report_opened is None:
          self.report_opened = 1
        return self.report_opened

329 330 331 332
    security.declarePublic('isInvertMode')
    def isInvertMode(self):
        return self.invert_mode
 
Jérome Perrin's avatar
Jérome Perrin committed
333 334 335 336 337
    security.declarePublic('getInvertModeUidList')
    def getInvertModeUidList(self):
        return self.uids
     
 
338
InitializeClass(Selection)
339 340
allow_class(Selection)

341 342 343
class DomainSelection(Acquisition.Implicit, Traversable, Persistent):
  """
    A class to store a selection of domains which defines a report
344 345 346
    section. There are different ways to use DomainSelection in 
    SQL methods. As a general principle, SQL methods are passed
    DomainSelection instances as a parameter.
347

348
    Example 1: (hand coded)
349

350 351 352 353 354
    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.

355 356 357 358 359
    <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)
360 361 362 363
    
    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.
364

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

368 369
    Example 3: (mixed)

370 371 372 373
    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
374
    <dtml-var "selection.domain.eip.asSQLExpresion(table="resource_category")">
375

376 377 378
    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.
379 380
  """

381 382
  security = ClassSecurityInfo()
  security.declareObjectPublic()
383

384
  def __init__(self, domain_dict = None):
385
    #LOG('DomainSelection', 0, '__init__ is called with %r' % (domain_dict,))
386 387
    if domain_dict is not None:
      self.domain_dict = domain_dict
388
      for k, v in domain_dict.iteritems():
389 390 391 392
        if k is not None:
          setattr(self, k, v)

  def __len__(self):
393 394
    return len(self.domain_dict)

395 396
  security.declarePublic('getCategoryList')
  def getCategoryList(self):
397
    return
398

399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422
  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
423 424
  security.declarePublic('asSQLExpression')
  def asSQLExpression(self, table_map=None, domain_id=None, 
425
                      exclude_domain_id=None, strict_membership=0,
426 427
                      join_table="catalog", join_column="uid", base_category=None,
                      category_table_alias='category'):
428
    select_expression = []
429 430 431 432
    portal = self.getPortalObject()
    for k, d in self.domain_dict.iteritems():
      d = self._getDomainObject(portal, d)

433 434
      if k == 'parent':
        # Special treatment for parent
435
        select_expression.append(d.getParentSQLExpression(table='catalog',
436
                               strict_membership=strict_membership))
437 438
      elif k is not None:
        if getattr(aq_base(d), 'isPredicate', 0):
439
          select_expression.append(d.asSQLExpression(table='%s_%s' % (k, category_table_alias),
440 441 442
                                                     strict_membership=strict_membership))
        else:
          # This is a category, we must join
443 444 445
          select_expression.append('%s.%s = %s_%s.uid' % \
                                (join_table, join_column, k, category_table_alias))
          select_expression.append(d.asSQLExpression(table='%s_%s' % (k, category_table_alias),
446
                                base_category=k,
447 448 449 450 451 452 453
                                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 = ''
454
    #LOG('DomainSelection', 0, 'asSQLExpression returns %r' % (result,))
455
    return result
456

457 458 459 460
  # Compatibility SQL Sql
  security.declarePublic('asSqlExpression')
  asSqlExpression = asSQLExpression
  
Jérome Perrin's avatar
Jérome Perrin committed
461
  security.declarePublic('asSQLJoinExpression')
462
  def asSQLJoinExpression(self, domain_id=None, exclude_domain_id=None, category_table_alias='category'):
463
    join_expression = []
464
    #LOG('DomainSelection', 0, 'domain_id = %r, exclude_domain_id = %r, self.domain_dict = %r' % (domain_id, exclude_domain_id, self.domain_dict))
465 466 467 468
    portal = self.getPortalObject()
    for k, d in self.domain_dict.iteritems():
      d = self._getDomainObject(portal, d)

469 470
      if k == 'parent':
        pass
471 472
      elif k is not None:
        if getattr(aq_base(d), 'isPredicate', 0):
473
          join_expression.append(d.asSQLJoinExpression(table='%s_%s' % (k, category_table_alias)))
474 475
        else:
          # This is a category, we must join
476
          join_expression.append('category AS %s_%s' % (k, category_table_alias))
477
    result = "%s" % ' , '.join(join_expression)
Jérome Perrin's avatar
Jérome Perrin committed
478
    #LOG('DomainSelection', 0, 'asSQLJoinExpression returns %r' % (result,))
479 480
    return result

481 482 483 484
  # Compatibility SQL Sql
  security.declarePublic('asSqlJoinExpression')
  asSqlJoinExpression = asSQLJoinExpression

485 486
  security.declarePublic('asDomainDict')
  def asDomainDict(self, domain_id=None, exclude_domain_id=None):
487
    return self.domain_dict
488 489 490 491 492 493 494

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

  security.declarePublic('updateDomain')
  def updateDomain(self, domain):
495 496
    pass

497
InitializeClass(DomainSelection)
498
allow_class(DomainSelection)