Selection.py 17.9 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 100 101
    method_path=None
    params={}
    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
          self.params = {}
Jean-Paul Smets's avatar
Jean-Paul Smets committed
164 165 166 167 168
          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_':
169
              self.params[key] = params[key]
Jean-Paul Smets's avatar
Jean-Paul Smets committed
170 171
        if kw is not None:
          for k,v in kw.items():
172
            if k in ('domain', 'report', 'domain_path', 'report_path', 'domain_list', 'report_list') or v is not None:
173 174 175 176
              # 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')
177
              setattr(self, k, v)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
178

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

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

220 221
    security.declarePublic('getName')
    def getName(self):
222 223 224
        """
          Get the name of this selection.
        """
225
        return self.name
226

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

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

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

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

260 261 262 263 264 265 266
    security.declarePublic('getSortOrder')
    def getSortOrder(self):
        """
          Return sort order stored in selection
        """
        return self.sort_on

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

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

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

326 327 328 329 330 331
    security.declarePublic('isReportOpened')
    def isReportOpened(self):
        if self.report_opened is None:
          self.report_opened = 1
        return self.report_opened

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

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

351
    Example 1: (hand coded)
352

353 354 355 356 357
    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.

358 359 360 361 362
    <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)
363 364 365 366
    
    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.
367

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

371 372
    Example 3: (mixed)

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

379 380 381
    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.
382 383
  """

384 385
  security = ClassSecurityInfo()
  security.declareObjectPublic()
386

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

  def __len__(self):
396 397
    return len(self.domain_dict)

398 399
  security.declarePublic('getCategoryList')
  def getCategoryList(self):
400
    return
401

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

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

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

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

484 485 486 487
  # Compatibility SQL Sql
  security.declarePublic('asSqlJoinExpression')
  asSqlJoinExpression = asSQLJoinExpression

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

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

  security.declarePublic('updateDomain')
  def updateDomain(self, domain):
498 499
    pass

500
InitializeClass(DomainSelection)
501
allow_class(DomainSelection)