Selection.py 17.2 KB
Newer Older
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1 2 3
##############################################################################
#
# Copyright (c) 2002 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 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
##############################################################################

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 100 101 102 103 104 105 106 107 108 109 110 111 112 113
    method_path=None
    params={}
    sort_on=()
    default_sort_on=None
    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 130 131 132
        if params is None: params = {}
        if sort_on is None: sort_on = []
        if uids is None: uids = []
        if columns is None: columns = []
        if checked_uids is None: checked_uids = []
133 134 135 136
        # 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')
137 138 139 140 141 142 143 144 145 146 147 148
        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',)
149
        self.domain_list = ()
150
        self.report_path = None
151
        self.report_list = ()
152 153
        self.domain = None
        self.report = None
154
        self.report_opened = None
155 156

    security.declarePrivate('edit')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
157 158
    def edit(self, params=None, **kw):
        if params is not None:
159
          self.params = {}
Jean-Paul Smets's avatar
Jean-Paul Smets committed
160 161 162 163 164
          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_':
165
              self.params[key] = params[key]
Jean-Paul Smets's avatar
Jean-Paul Smets committed
166 167
        if kw is not None:
          for k,v in kw.items():
168
            if k in ('domain', 'report', 'domain_path', 'report_path', 'domain_list', 'report_list') or v is not None:
169 170 171 172
              # 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')
173
              setattr(self, k, v)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
174

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

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

214 215
    security.declarePublic('getName')
    def getName(self):
216 217 218
        """
          Get the name of this selection.
        """
219
        return self.name
220

221 222
    security.declarePublic('getIndex')
    def getIndex(self):
223 224 225
        """
          Get the index of this selection.
        """
226
        return self.index
227

228 229 230 231 232 233 234
    security.declarePublic('getDomain')
    def getDomain(self):
        """
          Get the domain selection of this selection.
        """
        return self.domain

235 236 237 238 239 240 241
    security.declarePublic('getReport')
    def getReport(self):
        """
          Get the report selection of this selection.
        """
        return self.report

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

254 255 256 257 258 259 260
    security.declarePublic('getSortOrder')
    def getSortOrder(self):
        """
          Return sort order stored in selection
        """
        return self.sort_on

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

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

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

320 321 322 323 324 325
    security.declarePublic('isReportOpened')
    def isReportOpened(self):
        if self.report_opened is None:
          self.report_opened = 1
        return self.report_opened

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

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

345
    Example 1: (hand coded)
346

347 348 349 350 351
    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.

352 353 354 355 356
    <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)
357 358 359 360
    
    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.
361

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

365 366
    Example 3: (mixed)

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

373 374 375
    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.
376 377
  """

378 379
  security = ClassSecurityInfo()
  security.declareObjectPublic()
380

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

  def __len__(self):
390 391
    return len(self.domain_dict)

392 393
  security.declarePublic('getCategoryList')
  def getCategoryList(self):
394
    return
395

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

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

Jérome Perrin's avatar
Jérome Perrin committed
453 454
  security.declarePublic('asSQLJoinExpression')
  def asSQLJoinExpression(self, domain_id=None, exclude_domain_id=None):
455
    join_expression = []
456
    #LOG('DomainSelection', 0, 'domain_id = %r, exclude_domain_id = %r, self.domain_dict = %r' % (domain_id, exclude_domain_id, self.domain_dict))
457 458 459 460
    portal = self.getPortalObject()
    for k, d in self.domain_dict.iteritems():
      d = self._getDomainObject(portal, d)

461 462
      if k == 'parent':
        pass
463 464
      elif k is not None:
        if getattr(aq_base(d), 'isPredicate', 0):
Jérome Perrin's avatar
Jérome Perrin committed
465
          join_expression.append(d.asSQLJoinExpression(table='%s_category' % k))
466 467 468
        else:
          # This is a category, we must join
          join_expression.append('category AS %s_category' % k)
469
    result = "%s" % ' , '.join(join_expression)
Jérome Perrin's avatar
Jérome Perrin committed
470
    #LOG('DomainSelection', 0, 'asSQLJoinExpression returns %r' % (result,))
471 472 473 474
    return result

  security.declarePublic('asDomainDict')
  def asDomainDict(self, domain_id=None, exclude_domain_id=None):
475
    return self.domain_dict
476 477 478 479 480 481 482

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

  security.declarePublic('updateDomain')
  def updateDomain(self, domain):
483 484
    pass

485
InitializeClass(DomainSelection)
486
allow_class(DomainSelection)