SelectionTool.py 80.4 KB
Newer Older
1
# -*- coding: utf-8 -*-
Jean-Paul Smets's avatar
Jean-Paul Smets committed
2 3
##############################################################################
#
4
# Copyright (c) 2002,2007 Nexedi SARL and Contributors. All Rights Reserved.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5
#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
Jean-Paul Smets's avatar
Jean-Paul Smets committed
6 7
#
# WARNING: This program as such is intended to be used by professional
8
# programmers who take the whole responsibility of assessing all potential
Jean-Paul Smets's avatar
Jean-Paul Smets committed
9 10
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
11
# guarantees and support are strongly adviced to contract a Free Software
Jean-Paul Smets's avatar
Jean-Paul Smets committed
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.
#
##############################################################################

30 31
"""
  ERP5 portal_selection tool.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
32 33 34
"""

from OFS.SimpleItem import SimpleItem
35
from Products.ERP5Type.Utils import ensure_list
36
from Products.ERP5Type.Globals import InitializeClass, DTMLFile, PersistentMapping, get_request
Jean-Paul Smets's avatar
Jean-Paul Smets committed
37
from AccessControl import ClassSecurityInfo
38
from ZTUtils import make_query
39
from Products.ERP5Type.Tool.BaseTool import BaseTool
Jean-Paul Smets's avatar
Jean-Paul Smets committed
40
from Products.ERP5Type import Permissions as ERP5Permissions
41
from Products.ERP5Type.TransactionalVariable import getTransactionalVariable
42
from Products.ERP5Type.Utils import str2bytes
Jean-Paul Smets's avatar
Jean-Paul Smets committed
43
from Products.ERP5Form import _dtmldir
44
from Products.ERP5Form.Selection import Selection, DomainSelection
45
from ZPublisher.HTTPRequest import FileUpload
46
from hashlib import md5
47
import string, re
48
from six.moves.urllib.parse import urlsplit, urlunsplit
49
from zLOG import LOG, INFO, WARNING
50
from Acquisition import aq_base
51
from Products.ERP5Type.Message import translateString
52
from Products.ZSQLCatalog.SQLCatalog import SimpleQuery, ComplexQuery
53
import warnings
54
import six
55

56 57
_MARKER = []

Jean-Paul Smets's avatar
Jean-Paul Smets committed
58 59 60
class SelectionError( Exception ):
    pass

61
class SelectionTool( BaseTool, SimpleItem ):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
62 63 64 65 66 67 68 69
    """
      The SelectionTool object is the place holder for all
      methods and algorithms related to persistent selections
      in ERP5.
    """

    id              = 'portal_selections'
    meta_type       = 'ERP5 Selections'
70
    portal_type     = 'Selection Tool'
71
    title           = 'Selections'
Jean-Paul Smets's avatar
Jean-Paul Smets committed
72 73 74 75 76 77 78
    security = ClassSecurityInfo()

    #
    #   ZMI methods
    #
    manage_options = ( ( { 'label'      : 'Overview'
                         , 'action'     : 'manage_overview'
79 80
                         },
                         { 'label'      : 'View Selections'
81
                         , 'action'     : 'manage_viewSelections'
82 83 84
                         },
                         { 'label'      : 'Configure'
                         , 'action'     : 'manage_configure'
85
                         } ))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
86 87 88

    security.declareProtected( ERP5Permissions.ManagePortal
                             , 'manage_overview' )
89
    manage_overview = DTMLFile( 'explainSelectionTool', _dtmldir )
Jean-Paul Smets's avatar
Jean-Paul Smets committed
90

91
    security.declareProtected( ERP5Permissions.ManagePortal
92 93
                             , 'manage_viewSelections' )
    manage_viewSelections = DTMLFile( 'SelectionTool_manageViewSelections', _dtmldir )
94

95 96 97 98
    security.declareProtected( ERP5Permissions.ManagePortal
                             , 'manage_configure' )
    manage_configure = DTMLFile( 'SelectionTool_configure', _dtmldir )

99 100 101 102 103 104 105 106 107 108 109
    security.declareProtected( ERP5Permissions.ManagePortal
                             , 'manage_deleteSelectionForUser' )
    def manage_deleteSelectionForUser(self, selection_name, user_id, REQUEST=None):
      """
        Delete a specified selection
      """
      self._deleteSelectionForUserFromContainer(selection_name, user_id)
      if REQUEST is not None:
        return REQUEST.RESPONSE.redirect('%s/%s' %
                (self.absolute_url(), 'manage_viewSelections'))

110 111 112 113 114 115 116 117 118 119 120
    security.declareProtected( ERP5Permissions.ManagePortal
                             , 'manage_deleteSelection' )
    def manage_deleteSelection(self, selection_name, REQUEST=None):
      """
        Relete a specified selection
      """
      self._deleteSelectionFromContainer(selection_name)
      if REQUEST is not None:
        return REQUEST.RESPONSE.redirect('%s/%s' %
                (self.absolute_url(), 'manage_viewSelections'))

121 122 123 124 125 126 127 128 129 130 131
    security.declareProtected( ERP5Permissions.ManagePortal
                             , 'manage_deleteGlobalSelection' )
    def manage_deleteGlobalSelection(self, selection_name, REQUEST=None):
      """
        Relete a specified selection
      """
      self._deleteGlobalSelectionFromContainer(selection_name)
      if REQUEST is not None:
        return REQUEST.RESPONSE.redirect('%s/%s' %
                (self.absolute_url(), 'manage_viewSelections'))

132
    # storages of SelectionTool
133 134 135 136 137 138 139 140 141 142 143
    security.declareProtected(ERP5Permissions.ManagePortal
                              , 'getStorageItemList')
    def getStorageItemList(self):
      """Return the list of available storages
      """
      #storage_item_list = [('Persistent Mapping', 'selection_data',)]
      #list of tuple may fail dtml code: zope/documenttemplate/dt_in.py +578
      storage_item_list = [['Persistent Mapping', 'selection_data']]
      memcached_plugin_list = self.portal_memcached.contentValues(portal_type='Memcached Plugin', sort_on='int_index')
      storage_item_list.extend([['/'.join((mp.getParentValue().getTitle(), mp.getTitle(),)), mp.getRelativeUrl()] for mp in memcached_plugin_list])
      return storage_item_list
144

145
    security.declareProtected(ERP5Permissions.ModifyPortalContent, 'clearCachedContainer')
146 147 148 149 150 151 152 153 154 155 156 157 158
    def clearCachedContainer(self, is_anonymous=False):
      """
      Clear Container currently being used for Selection Tool because either the
      storage has changed or the its settings
      """
      if is_anonymous:
        container_id = '_v_anonymous_selection_container'
      else:
        container_id = '_v_selection_container'

      if getattr(aq_base(self), container_id, None):
        delattr(self, container_id)

159
    security.declareProtected( ERP5Permissions.ManagePortal, 'setStorage')
160
    def setStorage(self, storage, anonymous_storage=None, RESPONSE=None):
161 162 163
      """
        Set the storage of Selection Tool.
      """
164 165
      if storage in [item[1] for item in self.getStorageItemList()]:
        self.storage = storage
166
        self.clearCachedContainer()
167
      else:
168
        raise ValueError('Given storage type (%s) is now supported.' % (storage,))
169 170 171
      anonymous_storage = anonymous_storage or None
      if anonymous_storage in [item[1] for item in self.getStorageItemList()] + [None]:
        self.anonymous_storage = anonymous_storage
172
        self.clearCachedContainer(is_anonymous=True)
173
      else:
174
        raise ValueError('Given storage type (%s) is now supported.' % (anonymous_storage,))
175 176 177
      if RESPONSE is not None:
        RESPONSE.redirect('%s/manage_configure' % (self.absolute_url()))

178
    security.declareProtected( ERP5Permissions.ManagePortal, 'getStorage')
179
    def getStorage(self, default='selection_data'):
180 181
      """return the selected storage
      """
182
      storage = getattr(aq_base(self), 'storage', default)
183
      if storage is not default:
184 185 186 187 188
        #Backward compatibility
        if storage == 'Persistent Mapping':
          storage = 'selection_data'
        elif storage == 'Memcached Tool':
          memcached_plugin_list = self.portal_memcached.contentValues(portal_type='Memcached Plugin', sort_on='int_index')
189 190 191 192
          if len(memcached_plugin_list):
            storage = memcached_plugin_list[0].getRelativeUrl()
          else:
            storage = 'selection_data'
193 194
      return storage

195 196 197 198 199 200
    security.declareProtected( ERP5Permissions.ManagePortal, 'getAnonymousStorage')
    def getAnonymousStorage(self, default=None):
      """return the selected storage
      """
      return getattr(aq_base(self), 'anonymous_storage', default)

Vincent Pelletier's avatar
Vincent Pelletier committed
201 202
    def _redirectToOriginalForm(self, REQUEST=None, form_id=None, dialog_id=None,
                                query_string=None,
203
                                no_reset=False, no_report_depth=False):
Vincent Pelletier's avatar
Vincent Pelletier committed
204 205
      """Redirect to the original form or dialog, using the information given
         as parameters.
206 207
         (Actually does not redirect  in the HTTP meaning because of URL
         limitation problems.)
Vincent Pelletier's avatar
Vincent Pelletier committed
208 209 210

         DEPRECATED parameters :
         query_string is used to transmit parameters from caller to callee.
211
         If no_reset is True, replace reset parameters with noreset.
Vincent Pelletier's avatar
Vincent Pelletier committed
212 213
         If no_report_depth is True, replace report_depth parameters with
         noreport_depth.
214 215 216 217
      """
      if REQUEST is None:
        return

Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
218
      form = REQUEST.form
219
      if no_reset and 'reset' in form:
Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
220 221
        form['noreset'] = form['reset'] # Kept for compatibility - might no be used anymore
        del form['reset']
222
      if no_report_depth and 'report_depth' in form:
Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
223 224
        form['noreport_depth'] = form['report_depth'] # Kept for compatibility - might no be used anymore
        del form['report_depth']
Vincent Pelletier's avatar
Vincent Pelletier committed
225

226
      if query_string is not None:
227 228
        warnings.warn('DEPRECATED: _redirectToOriginalForm got called with a query_string. The variables must be passed in REQUEST.form.',
                      DeprecationWarning)
229
      context = REQUEST['PARENTS'][0]
230 231 232
      form_id = dialog_id or REQUEST.get('dialog_id', None) or form_id or REQUEST.get('form_id', None)
      if form_id is None:
        return context()
Jean-Paul Smets's avatar
Jean-Paul Smets committed
233
      return getattr(context, form_id)()
234

235 236
    security.declareProtected(ERP5Permissions.View, 'getSelectionNameList')
    def getSelectionNameList(self, context=None, REQUEST=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
237 238 239
      """
        Returns the selection names of the current user.
      """
240
      return sorted(self._getSelectionNameListFromContainer())
241

242 243 244 245 246 247 248 249
    # backward compatibility
    security.declareProtected(ERP5Permissions.View, 'getSelectionNames')
    def getSelectionNames(self, context=None, REQUEST=None):
      warnings.warn("getSelectionNames() is deprecated.\n"
                    "Please use getSelectionNameList() instead.",
                    DeprecationWarning)
      return self.getSelectionNameList(context, REQUEST)

250
    security.declareProtected(ERP5Permissions.View, 'callSelectionFor')
251
    def callSelectionFor(self, selection_name, method=None, context=None,
252 253 254
                                               REQUEST=None, params=None):
      """
      Calls the selection and return the list of selected documents
255
      or objects. Seledction method, context and parameters may be
256 257 258 259 260 261 262 263 264
      overriden in a non persistent way.

      selection_name -- the name of the selectoin (string)

      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

265
      REQUEST -- optional REQUEST parameters (not used, only to
266 267 268 269 270 271 272 273
                 provide API compatibility)

      params -- optional parameters which can be used to override
                default params

      TODO: is it acceptable to keep method in the API at this level
            for security reasons (XXX-JPS)
      """
274 275 276 277
      if context is None: context = self
      selection = self.getSelectionFor(selection_name, REQUEST=REQUEST)
      if selection is None:
        return None
278
      return selection(method=method, context=context, REQUEST=REQUEST, params=params)
279

280 281 282 283 284 285 286 287 288 289 290
    def _getRequest(self, REQUEST=None):
      if REQUEST is None:
        REQUEST = getattr(self, 'REQUEST', None)
      return REQUEST

    def _getSelectionKeyFromRequest(self, selection_name, REQUEST):
      REQUEST = self._getRequest(REQUEST=REQUEST)
      if REQUEST is not None:
        return REQUEST.get('%s_selection_key' % selection_name, None) or \
          REQUEST.get('selection_key', None)

Jean-Paul Smets's avatar
Jean-Paul Smets committed
291 292 293 294 295
    security.declareProtected(ERP5Permissions.View, 'getSelectionFor')
    def getSelectionFor(self, selection_name, REQUEST=None):
      """
        Returns the selection instance for a given selection_name
      """
296 297
      if isinstance(selection_name, (tuple, list)):
        selection_name = selection_name[0]
298 299
      if not selection_name:
        return None
300
      selection = self._getSelectionFromContainer(selection_name)
301
      if selection is None and self.isAnonymous():
302 303 304 305
        selection_key = self._getSelectionKeyFromRequest(selection_name, REQUEST)
        if selection_key is not None:
          selection = self.getAnonymousSelection(selection_key, selection_name)
          self._setSelectionToContainer(selection_name, selection)
306 307
      if selection is not None:
        return selection.__of__(self)
308

309 310 311
    def __getitem__(self, key):
        return self.getSelectionParamsFor(key)

Jean-Paul Smets's avatar
Jean-Paul Smets committed
312 313 314 315 316
    security.declareProtected(ERP5Permissions.View, 'setSelectionFor')
    def setSelectionFor(self, selection_name, selection_object, REQUEST=None):
      """
        Sets the selection instance for a given selection_name
      """
317 318
      if not selection_name:
        return
319 320 321 322 323 324
      if not (selection_object is None or
              selection_name == selection_object.name):
        LOG('SelectionTool', WARNING,
            "Selection not set: new Selection name ('%s') differs from existing one ('%s')" % \
            (selection_name,
             selection_object.name))
325
      elif self.getSelectionFor(selection_name, REQUEST=REQUEST) != selection_object:
326
        self._setSelectionToContainer(selection_name, selection_object)
327
      if selection_object is None and self.isAnonymous():
328 329 330 331 332 333
        REQUEST = self._getRequest(REQUEST=REQUEST)
        for key in ('%s_selection_key' % selection_name, 'selection_key'):
          try:
            del REQUEST.form[key]
          except KeyError:
            pass
Jean-Paul Smets's avatar
Jean-Paul Smets committed
334

335 336
    security.declareProtected(ERP5Permissions.View, 'getSelectionParamsFor')
    def getSelectionParamsFor(self, selection_name, params=None, REQUEST=None):
337 338 339
      """
        Returns the params in the selection
      """
340 341
      if params is None:
        params = {}
342 343
      selection = self.getSelectionFor(selection_name, REQUEST=REQUEST)
      if selection is not None:
344
        if selection.params:
345
          return selection.getParams()
346
      return params
347 348

    # backward compatibility
349 350
    security.declareProtected(ERP5Permissions.View, 'getSelectionParams')
    getSelectionParams = getSelectionParamsFor
351

Jean-Paul Smets's avatar
Jean-Paul Smets committed
352 353 354 355 356 357
    security.declareProtected(ERP5Permissions.View, 'setSelectionParamsFor')
    def setSelectionParamsFor(self, selection_name, params, REQUEST=None):
      """
        Sets the selection params for a given selection_name
      """
      selection_object = self.getSelectionFor(selection_name, REQUEST)
358
      if selection_object is not None:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
359 360
        selection_object.edit(params=params)
      else:
361
        selection_object = Selection(selection_name, params=params)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
362 363
      self.setSelectionFor(selection_name, selection_object, REQUEST)

364
    security.declareProtected(ERP5Permissions.View, 'getSelectionDomainDictFor')
365 366 367 368 369 370 371
    def getSelectionDomainDictFor(self, selection_name, REQUEST=None):
      """
        Returns the Domain dict for a given selection_name
      """
      selection = self.getSelectionFor(selection_name, REQUEST=REQUEST)
      if selection is not None:
        try:
372
          return selection.getDomain().asDomainDict()
373 374 375
        except AttributeError:
          return {}

376
    security.declareProtected(ERP5Permissions.View, 'getSelectionReportDictFor')
377 378 379 380 381 382 383
    def getSelectionReportDictFor(self, selection_name, REQUEST=None):
      """
        Returns the Report dict for a given selection_name
      """
      selection = self.getSelectionFor(selection_name, REQUEST=REQUEST)
      if selection is not None:
        try:
384
          return selection.getReport().asDomainDict()
385 386 387
        except AttributeError:
          return {}

Jean-Paul Smets's avatar
Jean-Paul Smets committed
388 389 390
    security.declareProtected(ERP5Permissions.View, 'setSelectionCheckedUidsFor')
    def setSelectionCheckedUidsFor(self, selection_name, checked_uids, REQUEST=None):
      """
391
        Sets the checked uids for a given selection_name
Jean-Paul Smets's avatar
Jean-Paul Smets committed
392 393 394 395 396
      """
      selection_object = self.getSelectionFor(selection_name, REQUEST)
      if selection_object:
        selection_object.edit(checked_uids=checked_uids)
      else:
397
        selection_object = Selection(selection_name, checked_uids=checked_uids)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
398 399
      self.setSelectionFor(selection_name, selection_object, REQUEST)

400
    security.declareProtected(ERP5Permissions.View, 'updateSelectionCheckedUidList')
401 402
    def updateSelectionCheckedUidList(self, selection_name, listbox_uid, uids, REQUEST=None):
      """
403 404
        Updates the unchecked uids(listbox_uids) and checked uids (uids)
        for a given selection_name
405 406 407 408 409 410 411 412
      """
      if listbox_uid is None:
        listbox_uid = []
      if uids is None:
        uids = []
      self.uncheckAll(selection_name,listbox_uid,REQUEST=REQUEST)
      self.checkAll(selection_name,uids,REQUEST=REQUEST)

413 414 415
    security.declareProtected(ERP5Permissions.View, 'getSelectionCheckedUidsFor')
    def getSelectionCheckedUidsFor(self, selection_name, REQUEST=None):
      """
416
        Returns the checked uids for a given selection_name
417 418 419
      """
      selection_object = self.getSelectionFor(selection_name, REQUEST)
      if selection_object:
420
        return selection_object.getCheckedUids()
421 422 423
      return []

    security.declareProtected(ERP5Permissions.View, 'checkAll')
424
    def checkAll(self, list_selection_name, listbox_uid=[], REQUEST=None,
425
                 query_string=None, form_id=None):
426
      """
427
        Check uids in a given listbox_uid list for a given list_selection_name
428
      """
429
      selection_object = self.getSelectionFor(list_selection_name, REQUEST)
430 431
      if selection_object:
        selection_uid_dict = {}
432
        for uid in selection_object.checked_uids:
433 434
          selection_uid_dict[uid] = 1
        for uid in listbox_uid:
435 436
          try:
            selection_uid_dict[int(uid)] = 1
437
          except (ValueError, TypeError):
438
            selection_uid_dict[uid] = 1
439
        self.setSelectionCheckedUidsFor(list_selection_name, ensure_list(selection_uid_dict.keys()), REQUEST=REQUEST)
440 441 442
      if REQUEST is not None:
        return self._redirectToOriginalForm(REQUEST=REQUEST, form_id=form_id,
                                            query_string=query_string, no_reset=True)
443 444

    security.declareProtected(ERP5Permissions.View, 'uncheckAll')
445
    def uncheckAll(self, list_selection_name, listbox_uid=[], REQUEST=None,
446
                   query_string=None, form_id=None):
447
      """
448
        Uncheck uids in a given listbox_uid list for a given list_selection_name
449
      """
450
      selection_object = self.getSelectionFor(list_selection_name, REQUEST)
451 452
      if selection_object:
        selection_uid_dict = {}
453
        for uid in selection_object.checked_uids:
454 455
          selection_uid_dict[uid] = 1
        for uid in listbox_uid:
456
          try:
457
            if int(uid) in selection_uid_dict: del selection_uid_dict[int(uid)]
458
          except (ValueError, TypeError):
459
            if uid in selection_uid_dict: del selection_uid_dict[uid]
460
        self.setSelectionCheckedUidsFor(list_selection_name, selection_uid_dict.keys(), REQUEST=REQUEST)
461 462 463
      if REQUEST is not None:
        return self._redirectToOriginalForm(REQUEST=REQUEST, form_id=form_id,
                                            query_string=query_string, no_reset=True)
464

Jean-Paul Smets's avatar
Jean-Paul Smets committed
465 466 467 468 469 470 471
    security.declareProtected(ERP5Permissions.View, 'getSelectionListUrlFor')
    def getSelectionListUrlFor(self, selection_name, REQUEST=None):
      """
        Returns the URL of the list mode of selection instance
      """
      selection = self.getSelectionFor(selection_name, REQUEST=REQUEST)
      if selection:
472
        url = selection.getListUrl()
473
        if self.isAnonymous() and '?' in url:
474 475
          url += '&selection_key=%s' % self._getSelectionKeyFromRequest(selection_name, REQUEST)
        return url
Jean-Paul Smets's avatar
Jean-Paul Smets committed
476 477
      else:
        return None
478

479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495
    security.declareProtected(ERP5Permissions.View, 'getSelectionInvertModeFor')
    def getSelectionInvertModeFor(self, selection_name, REQUEST=None):
      """Get the 'invert_mode' parameter of a selection.
      """
      selection = self.getSelectionFor(selection_name, REQUEST=REQUEST)
      if selection is not None:
        return selection.isInvertMode()
      return 0

    security.declareProtected(ERP5Permissions.View, 'setSelectionInvertModeFor')
    def setSelectionInvertModeFor(self, selection_name,
                                  invert_mode, REQUEST=None):
      """Change the 'invert_mode' parameter of a selection.
      """
      selection = self.getSelectionFor(selection_name, REQUEST=REQUEST)
      if selection is not None:
        selection.edit(invert_mode=invert_mode)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
496

497
    security.declareProtected(ERP5Permissions.View, 'getSelectionInvertModeUidListFor')
498 499 500 501 502 503 504 505
    def getSelectionInvertModeUidListFor(self, selection_name, REQUEST=None):
      """Get the 'invert_mode' parameter of a selection.
      """
      selection = self.getSelectionFor(selection_name, REQUEST=REQUEST)
      if selection is not None:
        return selection.getInvertModeUidList()
      return 0

506 507 508 509 510 511 512 513 514
    security.declareProtected(ERP5Permissions.View, 'getSelectionIndexFor')
    def getSelectionIndexFor(self, selection_name, REQUEST=None):
      """Get the 'index' parameter of a selection.
      """
      selection = self.getSelectionFor(selection_name, REQUEST=REQUEST)
      if selection is not None:
        return selection.getIndex()
      return None

Jean-Paul Smets's avatar
Jean-Paul Smets committed
515 516 517 518 519 520 521
    security.declareProtected(ERP5Permissions.View, 'setSelectionToIds')
    def setSelectionToIds(self, selection_name, selection_uids, REQUEST=None):
      """
        Sets the selection to a small list of uids of documents
      """
      selection = self.getSelectionFor(selection_name, REQUEST=REQUEST)
      if selection is not None:
522
        selection.edit(invert_mode=1, uids=selection_uids, checked_uids=selection_uids)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
523 524

    security.declareProtected(ERP5Permissions.View, 'setSelectionToAll')
525 526
    def setSelectionToAll(self, selection_name, REQUEST=None,
                          reset_domain_tree=False, reset_report_tree=False):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
527 528 529 530 531
      """
        Resets the selection
      """
      selection = self.getSelectionFor(selection_name, REQUEST=REQUEST)
      if selection is not None:
532
        selection.edit(invert_mode=0, params={}, checked_uids=[], report_opened=1)
533
        if reset_domain_tree:
534
          selection.edit(domain=None, domain_path=None, domain_list=None)
535
        if reset_report_tree:
536
          selection.edit(report=None, report_path=None, report_list=None)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
537 538 539 540 541 542 543 544 545 546 547

    security.declareProtected(ERP5Permissions.View, 'setSelectionSortOrder')
    def setSelectionSortOrder(self, selection_name, sort_on, REQUEST=None):
      """
        Defines the sort order of the selection
      """
      selection = self.getSelectionFor(selection_name, REQUEST=REQUEST)
      if selection is not None:
        selection.edit(sort_on=sort_on)

    security.declareProtected(ERP5Permissions.View, 'setSelectionQuickSortOrder')
548
    def setSelectionQuickSortOrder(self, selection_name=None, sort_on=None, REQUEST=None,
549
                                   query_string=None, form_id=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
550 551 552 553
      """
        Defines the sort order of the selection directly from the listbox
        In this method, sort_on is just a string that comes from url
      """
554 555
      # selection_name, sort_on and form_id params are kept only for bacward compatibilty
      # as some test call setSelectionQuickSortOrder in url with these params
Aurel's avatar
Aurel committed
556
      listbox_id = None
557 558
      if REQUEST is not None:
        form = REQUEST.form
559
      if sort_on is None:
Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
560
        listbox_id, sort_on = form["setSelectionQuickSortOrder"].split(".", 1)
561

562
      # Sort order can be specified in sort_on.
563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582
      if sort_on.endswith(':asc'):
        order = 'ascending'
        sort_on = sort_on[:-4]
      elif sort_on.endswith(':desc'):
        order = 'descending'
        sort_on = sort_on[:-5]
      elif sort_on.endswith(':none'):
        order = 'none'
        sort_on = sort_on[:-5]
      else:
        order = None
      # ... as well as cast type
      i = sort_on.find(':')
      if i < 0:
        as_type = None
      else:
        as_type = sort_on[i+1:]
        if as_type != 'float':
          return
        sort_on = sort_on[:i]
583

584 585 586 587 588 589
      if REQUEST is not None:
        if listbox_id is not None:
            selection_name_key = "%s_list_selection_name" %listbox_id
            selection_name = form[selection_name_key]
        elif selection_name is None:
            selection_name = form['selection_name']
590

Jean-Paul Smets's avatar
Jean-Paul Smets committed
591 592
      selection = self.getSelectionFor(selection_name, REQUEST=REQUEST)
      if selection is not None:
593
        if order is not None:
594
          # Allow user to sort by multiple columns
595 596 597 598
          new_sort_on = [s for s in selection.sort_on if s[0] != sort_on]
          if order != 'none':
            new_sort_on.append((sort_on, order, as_type) if as_type else
                               (sort_on, order))
599 600 601
        else:
          # We must first switch from asc to desc and vice-versa if sort_order exists
          # in selection
602 603
          order = 'ascending'
          for current in selection.sort_on:
604
            if current[0] == sort_on:
605 606 607 608 609
              if current[1] == order:
                order = 'descending'
              break
          new_sort_on = ((sort_on, order, as_type) if as_type else
                         (sort_on, order),)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
610 611
        selection.edit(sort_on=new_sort_on)

612
      if REQUEST is not None:
613 614
        if 'listbox_uid' in form and \
            'uids' in form:
615 616 617
          self.uncheckAll(selection_name, REQUEST.get('listbox_uid'))
          self.checkAll(selection_name, REQUEST.get('uids'))

618 619
        return self._redirectToOriginalForm(REQUEST=REQUEST, form_id=form_id,
                                            query_string=query_string, no_reset=True)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
620 621 622 623 624 625 626 627

    security.declareProtected(ERP5Permissions.View, 'getSelectionSortOrder')
    def getSelectionSortOrder(self, selection_name, REQUEST=None):
      """
        Returns the sort order of the selection
      """
      selection = self.getSelectionFor(selection_name, REQUEST=REQUEST)
      if selection is None: return ()
628
      return selection.sort_on
Jean-Paul Smets's avatar
Jean-Paul Smets committed
629 630 631 632 633 634 635 636 637 638

    security.declareProtected(ERP5Permissions.View, 'setSelectionColumns')
    def setSelectionColumns(self, selection_name, columns, REQUEST=None):
      """
        Defines the columns in the selection
      """
      selection = self.getSelectionFor(selection_name, REQUEST=REQUEST)
      selection.edit(columns=columns)

    security.declareProtected(ERP5Permissions.View, 'getSelectionColumns')
639
    def getSelectionColumns(self, selection_name, columns=None, REQUEST=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
640
      """
641 642
        Returns the columns in the selection if not empty, otherwise
        returns the value of columns argument
Jean-Paul Smets's avatar
Jean-Paul Smets committed
643
      """
644
      if columns is None: columns = []
Jean-Paul Smets's avatar
Jean-Paul Smets committed
645 646
      selection = self.getSelectionFor(selection_name, REQUEST=REQUEST)
      if selection is not None:
647 648
        if len(selection.columns) > 0:
          return selection.columns
649
      return columns
Jean-Paul Smets's avatar
Jean-Paul Smets committed
650 651 652 653 654 655 656 657 658 659 660


    security.declareProtected(ERP5Permissions.View, 'setSelectionStats')
    def setSelectionStats(self, selection_name, stats, REQUEST=None):
      """
        Defines the stats in the selection
      """
      selection = self.getSelectionFor(selection_name, REQUEST=REQUEST)
      selection.edit(stats=stats)

    security.declareProtected(ERP5Permissions.View, 'getSelectionStats')
661
    def getSelectionStats(self, selection_name, stats=_MARKER, REQUEST=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
662 663 664
      """
        Returns the stats in the selection
      """
665 666 667 668
      if stats is not _MARKER:
        default_stats = stats
      else:
        default_stats = [' '] * 6
Jean-Paul Smets's avatar
Jean-Paul Smets committed
669 670
      selection = self.getSelectionFor(selection_name, REQUEST=REQUEST)
      if selection is not None:
671
        return getattr(aq_base(selection), 'stats', default_stats)
672
      return default_stats
Jean-Paul Smets's avatar
Jean-Paul Smets committed
673

674 675 676 677 678 679 680
    def _getExistingFormId(self, document, form_id):
      portal = document.getPortalObject()
      for url in [q['url'] for q in portal.portal_actions\
          .listFilteredActionsFor(document).get('object_view', [])]:
        # XXX-Luke: As this is not possible to do form_id -> action_id the
        # only way to know if form_id is implemented by action of document
        # is to use string matching.
681 682 683 684 685 686 687
        # This re will (form_id = Base_view):
        # qdsqdsq/Base_view --> match
        # qdsqdsq/Base_view?qsdsqd --> matches
        # qdsqdsq/Base_view/qsdsqd --> matches
        # qdsqdsq/Base_viewAaa --> doesn't match
        # qdsqdsq/Umpa_view --> doesn't match
        if re.search('/%s($|\W+)' % form_id, url):
688 689
          return form_id
      return 'view'
Jean-Paul Smets's avatar
Jean-Paul Smets committed
690 691 692 693 694 695

    security.declareProtected(ERP5Permissions.View, 'viewFirst')
    def viewFirst(self, selection_index='', selection_name='', form_id='view', REQUEST=None):
      """
        Access first item in a selection
      """
696
      return self._redirectToIndex(0, selection_name, form_id, REQUEST)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
697 698 699 700

    security.declareProtected(ERP5Permissions.View, 'viewLast')
    def viewLast(self, selection_index='', selection_name='', form_id='view', REQUEST=None):
      """
701
        Access last item in a selection
Jean-Paul Smets's avatar
Jean-Paul Smets committed
702
      """
703
      return self._redirectToIndex(-1, selection_name, form_id, REQUEST)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
704 705 706 707 708 709

    security.declareProtected(ERP5Permissions.View, 'viewNext')
    def viewNext(self, selection_index='', selection_name='', form_id='view', REQUEST=None):
      """
        Access next item in a selection
      """
710
      return self._redirectToIndex(int(selection_index) + 1, selection_name, form_id, REQUEST)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
711 712 713 714 715 716

    security.declareProtected(ERP5Permissions.View, 'viewPrevious')
    def viewPrevious(self, selection_index='', selection_name='', form_id='view', REQUEST=None):
      """
        Access previous item in a selection
      """
717 718 719
      return self._redirectToIndex(int(selection_index) - 1, selection_name, form_id, REQUEST)

    def _redirectToIndex(self, selection_index, selection_name, form_id, REQUEST):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
720 721 722 723
      if not REQUEST:
        REQUEST = get_request()
      selection = self.getSelectionFor(selection_name, REQUEST=REQUEST)
      if selection:
724
        method = self.unrestrictedTraverse(selection.method_path)
725
        selection_list = selection(method = method, context=self, REQUEST=REQUEST)
726
        if len(selection_list):
727 728 729
          if selection_index > 0:
            selection_index = selection_index % len(selection_list)
          o = selection_list[selection_index]
730
          url = o.absolute_url()
731
          form_id = self._getExistingFormId(o.getObject(), form_id)
732 733
        else:
          url = REQUEST.getURL()
Jean-Paul Smets's avatar
Jean-Paul Smets committed
734
      else:
735
        url = REQUEST.getURL()
Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
736 737
      if form_id != 'view':
        url += '/%s' % form_id
738 739 740 741 742 743
      query_kw = {
        'selection_index': selection_index,
        'selection_name': selection_name,
      }
      if int(REQUEST.get('ignore_layout', 0)):
        query_kw['ignore_layout'] = 1
744
      if self.isAnonymous():
745 746
        query_kw['selection_key'] = self.getAnonymousSelectionKey(selection_name, REQUEST=REQUEST)
      REQUEST.RESPONSE.redirect('%s?%s' % (url, make_query(query_kw)))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
747 748

    # ListBox related methods
749 750

    security.declareProtected(ERP5Permissions.View, 'firstPage')
Vincent Pelletier's avatar
Vincent Pelletier committed
751
    def firstPage(self, list_selection_name, listbox_uid, uids=None, REQUEST=None):
752 753 754 755
      """
        Access the first page of a list
      """
      if uids is None: uids = []
756 757 758 759 760
      selection = self.getSelectionFor(list_selection_name, REQUEST)
      if selection is not None:
        params = selection.getParams()
        params['list_start'] = 0
        selection.edit(params=params)
Vincent Pelletier's avatar
Vincent Pelletier committed
761 762
      self.uncheckAll(list_selection_name, listbox_uid)
      return self.checkAll(list_selection_name, uids, REQUEST=REQUEST)
763 764

    security.declareProtected(ERP5Permissions.View, 'lastPage')
Vincent Pelletier's avatar
Vincent Pelletier committed
765
    def lastPage(self, list_selection_name, listbox_uid, uids=None, REQUEST=None):
766 767 768 769
      """
        Access the last page of a list
      """
      if uids is None: uids = []
Vincent Pelletier's avatar
Vincent Pelletier committed
770
      selection = self.getSelectionFor(list_selection_name, REQUEST)
771 772 773 774 775 776 777
      if selection is not None:
        params = selection.getParams()
        # XXX This will not work if the number of lines shown in the listbox is greater
        #       than the BIG_INT constan. Such a case has low probability but is not
        #       impossible. If you are in this case, send me a mail ! -- Kev
        BIG_INT = 10000000
        last_page_start = BIG_INT
778
        total_lines = int(params.get('total_size', BIG_INT))
779
        if total_lines != BIG_INT:
780 781 782 783 784 785 786 787 788
          lines_per_page  = int(params.get('list_lines', 1))
          if total_lines % lines_per_page:
            # Example: if we have 105 documents and display 40 per line,
            # it is 105 // 40 * 40 = 80
            last_page_start = total_lines // lines_per_page * lines_per_page
          else:
            # Example; if we have 120 documents and display 40 per line,
            # it is 120 // (40 - 1) * 40 = 80
            last_page_start = (total_lines // lines_per_page - 1) * lines_per_page
789 790
        params['list_start'] = last_page_start
        selection.edit(params=params)
Vincent Pelletier's avatar
Vincent Pelletier committed
791 792
      self.uncheckAll(list_selection_name, listbox_uid)
      return self.checkAll(list_selection_name, uids, REQUEST=REQUEST)
793

Jean-Paul Smets's avatar
Jean-Paul Smets committed
794
    security.declareProtected(ERP5Permissions.View, 'nextPage')
Vincent Pelletier's avatar
Vincent Pelletier committed
795
    def nextPage(self, list_selection_name, listbox_uid, uids=None, REQUEST=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
796 797 798
      """
        Access the next page of a list
      """
799
      if uids is None: uids = []
Vincent Pelletier's avatar
Vincent Pelletier committed
800
      selection = self.getSelectionFor(list_selection_name, REQUEST)
801 802
      if selection is not None:
        params = selection.getParams()
803 804
        lines = int(params.get('list_lines', 0))
        form = REQUEST.form
805
        if 'page_start' in form:
806 807
          try:
            list_start = (int(form.pop('page_start', 0)) - 1) * lines
808
          except (ValueError, TypeError):
809
            list_start = 0
810 811
        else:
          list_start = int(form.pop('list_start', 0))
812
        params['list_start'] = max(list_start + lines, 0)
813
        selection.edit(params=params)
Vincent Pelletier's avatar
Vincent Pelletier committed
814 815
      self.uncheckAll(list_selection_name, listbox_uid)
      return self.checkAll(list_selection_name, uids, REQUEST=REQUEST)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
816 817

    security.declareProtected(ERP5Permissions.View, 'previousPage')
Vincent Pelletier's avatar
Vincent Pelletier committed
818
    def previousPage(self, list_selection_name, listbox_uid, uids=None, REQUEST=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
819 820 821
      """
        Access the previous page of a list
      """
822
      if uids is None: uids = []
Vincent Pelletier's avatar
Vincent Pelletier committed
823
      selection = self.getSelectionFor(list_selection_name, REQUEST)
824 825
      if selection is not None:
        params = selection.getParams()
826 827
        lines = int(params.get('list_lines', 0))
        form = REQUEST.form
828
        if 'page_start' in form:
829 830
          try:
            list_start = (int(form.pop('page_start', 0)) - 1) * lines
831
          except (ValueError, TypeError):
832
            list_start = 0
833 834 835
        else:
          list_start = int(form.pop('list_start', 0))
        params['list_start'] = max(list_start - lines, 0)
836
        selection.edit(params=params)
Vincent Pelletier's avatar
Vincent Pelletier committed
837 838
      self.uncheckAll(list_selection_name, listbox_uid)
      return self.checkAll(list_selection_name, uids, REQUEST=REQUEST)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
839 840

    security.declareProtected(ERP5Permissions.View, 'setPage')
Vincent Pelletier's avatar
Vincent Pelletier committed
841
    def setPage(self, list_selection_name, listbox_uid, query_string=None, uids=None, REQUEST=None):
842
      """
Jean-Paul Smets's avatar
Jean-Paul Smets committed
843
         Sets the current displayed page in a selection
844
      """
845
      if uids is None: uids = []
Vincent Pelletier's avatar
Vincent Pelletier committed
846
      selection = self.getSelectionFor(list_selection_name, REQUEST)
847 848
      if selection is not None:
        params = selection.getParams()
849 850
        lines = int(params.get('list_lines', 0))
        form = REQUEST.form
851
        if 'page_start' in form:
852 853
          try:
            list_start = (int(form.pop('page_start', 0)) - 1) * lines
854
          except (ValueError, TypeError):
855
            list_start = 0
856 857
        else:
          list_start = int(form.pop('list_start', 0))
858
        params['list_start'] = max(list_start, 0)
859 860
        selection.edit(params=params)
        self.uncheckAll(list_selection_name, listbox_uid)
Vincent Pelletier's avatar
Vincent Pelletier committed
861
      return self.checkAll(list_selection_name, uids, REQUEST=REQUEST, query_string=query_string)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
862

863
    # PlanningBox related methods
864 865
    security.declareProtected(ERP5Permissions.View, 'setLanePath')
    def setLanePath(self, uids=None, REQUEST=None, form_id=None,
866
                     query_string=None):
867 868 869
      """
      Set graphic zoom level in PlanningBox
      """
870 871
      if uids is None:
        uids = []
872 873 874 875 876
      request = REQUEST
      selection_name=request.list_selection_name
      selection = self.getSelectionFor(selection_name, REQUEST=REQUEST)
      if selection is not None:
        params = selection.getParams()
877 878
        lane_path = request.form.get('lane_path', None)
        if lane_path is None:
879
          # If lane_path is not defined try to
880
          # use the last one from params
881 882 883 884
          lane_path = params.get('lane_path',1)
        bound_start = request.form.get('bound_start', None)
        if bound_start is not None:
          params['bound_start'] = bound_start
885
        params['lane_path'] = lane_path
886
        params['zoom_variation'] = 0
887
        selection.edit(params=params)
888
      if REQUEST is not None:
889 890 891
        return self._redirectToOriginalForm(REQUEST=REQUEST,
                                            form_id=form_id,
                                            query_string=query_string)
892

893 894
    security.declareProtected(ERP5Permissions.View, 'nextLanePage')
    def nextLanePage(self, uids=None, REQUEST=None, form_id=None, query_string=None):
895 896 897
      """
      Set next graphic zoom start in PlanningBox
      """
898 899
      if uids is None:
        uids = []
900 901 902 903 904
      request = REQUEST
      selection_name=request.list_selection_name
      selection = self.getSelectionFor(selection_name, REQUEST=REQUEST)
      if selection is not None:
        params = selection.getParams()
905
        params['bound_variation'] = 1
906
        selection.edit(params=params)
907
      if REQUEST is not None:
908 909 910
        return self._redirectToOriginalForm(REQUEST=REQUEST,
                                            form_id=form_id,
                                             query_string=query_string)
911

912 913
    security.declareProtected(ERP5Permissions.View, 'previousLanePage')
    def previousLanePage(self, uids=None, REQUEST=None, form_id=None, query_string=None):
914 915 916
      """
      Set previous graphic zoom in PlanningBox
      """
917 918
      if uids is None:
        uids = []
919 920 921 922 923
      request = REQUEST
      selection_name=request.list_selection_name
      selection = self.getSelectionFor(selection_name, REQUEST=REQUEST)
      if selection is not None:
        params = selection.getParams()
924
        params['bound_variation'] = -1
925 926 927 928 929
        selection.edit(params=params)
      if REQUEST is not None:
        return self._redirectToOriginalForm(REQUEST=REQUEST,
                                            form_id=form_id,
                                             query_string=query_string)
930

Jean-Paul Smets's avatar
Jean-Paul Smets committed
931
    security.declareProtected(ERP5Permissions.View, 'setDomainRoot')
932
    def setDomainRoot(self, REQUEST, form_id=None, query_string=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
933 934 935
      """
        Sets the root domain for the current selection
      """
936
      selection_name = REQUEST.list_selection_name
Jean-Paul Smets's avatar
Jean-Paul Smets committed
937
      selection = self.getSelectionFor(selection_name, REQUEST)
938
      root_url = REQUEST.form.get('domain_root_url','portal_categories')
939
      selection.edit(domain_path=root_url, domain_list=())
Jean-Paul Smets's avatar
Jean-Paul Smets committed
940

941 942 943
      if REQUEST is not None:
        return self._redirectToOriginalForm(REQUEST=REQUEST, form_id=form_id,
                                            query_string=query_string)
944

945 946 947 948 949 950
    security.declareProtected(ERP5Permissions.View, 'setDomainRootFromParam')
    def setDomainRootFromParam(self, REQUEST, selection_name, domain_root):
      if REQUEST is None:
        return
      selection = self.getSelectionFor(selection_name, REQUEST)
      selection.edit(domain_path=domain_root, domain_list=())
Jean-Paul Smets's avatar
Jean-Paul Smets committed
951

952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974
    security.declareProtected(ERP5Permissions.View, 'setDomainDictFromParam')
    def setDomainDictFromParam(self, selection_name, domain_dict):
      domain_list = []
      domain_path = []
      for key, value in domain_dict.items():
        domain_path.append(key)
        splitted_domain_list = value[1].split('/')[1:]
        for i in range(len(splitted_domain_list)):
          domain_list.append('%s/%s' % (key, '/'.join(splitted_domain_list[:i + 1])))

      if len(domain_path) == 1:
        domain_path = domain_path[0]

      selection = self.getSelectionFor(selection_name)
      selection.edit(
        domain_list=domain_list,
        domain_path=domain_path,
        domain=DomainSelection(domain_dict=domain_dict),
        flat_list_mode=0,
        domain_tree_mode=1,
        report_tree_mode=0,
      )

975
    security.declareProtected(ERP5Permissions.View, 'unfoldDomain')
976
    def unfoldDomain(self, REQUEST, form_id=None, query_string=None):
977 978 979
      """
        Unfold domain for the current selection
      """
980
      selection_name = REQUEST.list_selection_name
Jean-Paul Smets's avatar
Jean-Paul Smets committed
981
      selection = self.getSelectionFor(selection_name, REQUEST)
982

983 984
      unfoldDomain = REQUEST.form.get('unfoldDomain', None)
      domain_url, domain_depth = unfoldDomain.split('.', 2)
985 986
      domain_depth = int(domain_depth)

987 988
      domain_list = list(selection.getDomainList())
      domain_list = domain_list[0:min(domain_depth, len(domain_list))]
989
      if isinstance(domain_url, str):
990
        selection.edit(domain_list = domain_list + [domain_url])
Jean-Paul Smets's avatar
Jean-Paul Smets committed
991

992 993 994
      if REQUEST is not None:
        return self._redirectToOriginalForm(REQUEST=REQUEST, form_id=form_id,
                                            query_string=query_string)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
995

996
    security.declareProtected(ERP5Permissions.View, 'foldDomain')
997
    def foldDomain(self, REQUEST, form_id=None, query_string=None):
998 999 1000
      """
        Fold domain for the current selection
      """
1001
      selection_name = REQUEST.list_selection_name
1002
      selection = self.getSelectionFor(selection_name, REQUEST)
1003

1004 1005 1006
      foldDomain = REQUEST.form.get('foldDomain', None)
      domain_url, domain_depth = foldDomain.split('.', 2)
      domain_depth = int(domain_depth)
1007

1008 1009
      domain_list = list(selection.getDomainList())
      domain_list = domain_list[0:min(domain_depth, len(domain_list))]
1010
      selection.edit(domain_list=[x for x in domain_list if x != domain_url])
1011

1012 1013 1014
      if REQUEST is not None:
        return self._redirectToOriginalForm(REQUEST=REQUEST, form_id=form_id,
                                            query_string=query_string)
1015

1016

Jean-Paul Smets's avatar
Jean-Paul Smets committed
1017
    security.declareProtected(ERP5Permissions.View, 'setReportRoot')
1018
    def setReportRoot(self, REQUEST, form_id=None, query_string=None):
1019 1020 1021
      """
        Sets the root report for the current selection
      """
1022
      selection_name = REQUEST.list_selection_name
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1023
      selection = self.getSelectionFor(selection_name, REQUEST)
1024
      root_url = REQUEST.form.get('report_root_url','portal_categories')
1025
      selection.edit(report_path=root_url, report_list=())
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1026

1027 1028 1029
      if REQUEST is not None:
        return self._redirectToOriginalForm(REQUEST=REQUEST, form_id=form_id,
                                            query_string=query_string)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1030 1031

    security.declareProtected(ERP5Permissions.View, 'unfoldReport')
1032
    def unfoldReport(self, REQUEST, form_id=None, query_string=None):
1033 1034 1035
      """
        Unfold report for the current selection
      """
1036
      selection_name = REQUEST.list_selection_name
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1037
      selection = self.getSelectionFor(selection_name, REQUEST)
1038
      report_url = REQUEST.form.get('report_url',None)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1039
      if type(report_url) == type('a'):
1040
        selection.edit(report_list=list(selection.getReportList()) + [report_url])
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1041

1042 1043 1044
      return self._redirectToOriginalForm(REQUEST=REQUEST, form_id=form_id,
                                          query_string=query_string,
                                          no_report_depth=True)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1045 1046

    security.declareProtected(ERP5Permissions.View, 'foldReport')
1047
    def foldReport(self, REQUEST, form_id=None, query_string=None):
1048 1049 1050
      """
        Fold domain for the current selection
      """
1051
      selection_name = REQUEST.list_selection_name
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1052
      selection = self.getSelectionFor(selection_name, REQUEST)
1053
      report_url = REQUEST.form.get('report_url',None)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1054
      if type(report_url) == type('a'):
1055
        report_list = selection.getReportList()
1056
        selection.edit(report_list=[x for x in report_list if x != report_url])
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1057

1058 1059 1060
      return self._redirectToOriginalForm(REQUEST=REQUEST, form_id=form_id,
                                          query_string=query_string,
                                          no_report_depth=True)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1061

1062 1063 1064 1065
    security.declareProtected(ERP5Permissions.View, 'getListboxDisplayMode')
    def getListboxDisplayMode(self, selection_name, REQUEST=None):
      if REQUEST is None:
        REQUEST = get_request()
1066
      selection = self.getSelectionFor(selection_name, REQUEST)
1067

1068 1069 1070 1071 1072
      if getattr(selection, 'report_tree_mode', 0):
        return 'ReportTreeMode'
      elif getattr(selection, 'domain_tree_mode', 0):
        return 'DomainTreeMode'
      return 'FlatListMode'
1073

Jean-Paul Smets's avatar
Jean-Paul Smets committed
1074
    security.declareProtected(ERP5Permissions.View, 'setListboxDisplayMode')
1075
    def setListboxDisplayMode(self, REQUEST, listbox_display_mode,
1076 1077
                              selection_name=None, redirect=0,
                              form_id=None, query_string=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1078
      """
1079
        Toggle display of the listbox
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1080 1081
      """
      request = REQUEST
1082 1083 1084 1085 1086 1087 1088
      # XXX FIXME
      # Dirty fix: we must be able to change the display mode of a listbox
      # in form_view
      # But, form can have multiple listbox...
      # This need to be cleaned
      # Beware, this fix may break the report system...
      # and we don't have test for this
1089 1090
      # Possible fix: currently, display mode icon are implemented as
      # method. It could be easier to generate them as link (where we
1091 1092 1093 1094 1095 1096 1097 1098 1099
      # can define explicitely parameters through the url).
      try:
        list_selection_name = request.list_selection_name
      except AttributeError:
        pass
      else:
        if list_selection_name is not None:
          selection_name = request.list_selection_name
      # Get the selection
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1100
      selection = self.getSelectionFor(selection_name, REQUEST)
1101
      if selection is None:
1102
        selection = Selection(selection_name)
1103
        self.setSelectionFor(selection_name, selection, REQUEST=REQUEST)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116

      if listbox_display_mode == 'FlatListMode':
        flat_list_mode = 1
        domain_tree_mode = 0
        report_tree_mode = 0
      elif listbox_display_mode == 'DomainTreeMode':
        flat_list_mode = 0
        domain_tree_mode = 1
        report_tree_mode = 0
      elif listbox_display_mode == 'ReportTreeMode':
        flat_list_mode = 0
        domain_tree_mode = 0
        report_tree_mode = 1
1117 1118 1119
      else:
        flat_list_mode = 0
        domain_tree_mode = 0
1120
        report_tree_mode = 0
1121

1122 1123 1124
      selection.edit(flat_list_mode=flat_list_mode,
                     domain_tree_mode=domain_tree_mode,
                     report_tree_mode=report_tree_mode)
1125
      # It is better to reset the query when changing the display mode.
1126 1127
      params = selection.getParams()
      if 'where_expression' in params: del params['where_expression']
1128
      selection.edit(params=params)
1129

1130
      if redirect:
1131 1132 1133
        return self._redirectToOriginalForm(REQUEST=request, form_id=form_id,
                                            query_string=query_string,
                                            no_reset=True)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1134 1135

    security.declareProtected(ERP5Permissions.View, 'setFlatListMode')
1136
    def setFlatListMode(self, REQUEST, selection_name=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1137 1138 1139
      """
        Set display of the listbox to FlatList mode
      """
1140
      return self.setListboxDisplayMode(
1141
                       REQUEST=REQUEST, listbox_display_mode='FlatListMode',
1142
                       selection_name=selection_name, redirect=1)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1143 1144

    security.declareProtected(ERP5Permissions.View, 'setDomainTreeMode')
1145
    def setDomainTreeMode(self, REQUEST, selection_name=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1146 1147 1148
      """
         Set display of the listbox to DomainTree mode
      """
1149
      return self.setListboxDisplayMode(
1150
                       REQUEST=REQUEST, listbox_display_mode='DomainTreeMode',
1151
                       selection_name=selection_name, redirect=1)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1152 1153

    security.declareProtected(ERP5Permissions.View, 'setReportTreeMode')
1154
    def setReportTreeMode(self, REQUEST, selection_name=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1155 1156 1157
      """
        Set display of the listbox to ReportTree mode
      """
1158 1159 1160
      return self.setListboxDisplayMode(
                       REQUEST=REQUEST, listbox_display_mode='ReportTreeMode',
                       selection_name=selection_name, redirect=1)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1161

1162 1163 1164 1165 1166 1167
    security.declareProtected(ERP5Permissions.View, 'getSelectionSelectedValueList')
    def getSelectionSelectedValueList(self, selection_name, REQUEST=None, selection_method=None, context=None):
      """
        Get the list of values selected for 'selection_name'
      """
      selection = self.getSelectionFor(selection_name, REQUEST=REQUEST)
1168 1169
      if selection is None:
        return []
1170
      return selection(method=selection_method, context=context, REQUEST=REQUEST)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1171

1172 1173 1174 1175 1176 1177
    security.declareProtected(ERP5Permissions.View, 'getSelectionCheckedValueList')
    def getSelectionCheckedValueList(self, selection_name, REQUEST=None):
      """
        Get the list of values checked for 'selection_name'
      """
      selection = self.getSelectionFor(selection_name, REQUEST=REQUEST)
1178 1179
      if selection is None:
        return []
1180
      uid_list = selection.getCheckedUids()
1181 1182 1183 1184 1185 1186 1187 1188 1189 1190
      value_list = self.portal_catalog.getObjectList(uid_list)
      return value_list

    security.declareProtected(ERP5Permissions.View, 'getSelectionValueList')
    def getSelectionValueList(self, selection_name, REQUEST=None, selection_method=None, context=None):
      """
        Get the list of values checked or selected for 'selection_name'
      """
      value_list = self.getSelectionCheckedValueList(selection_name, REQUEST=REQUEST)
      if len(value_list) == 0:
1191 1192
        value_list = self.getSelectionSelectedValueList(
                                            selection_name,
1193 1194
                                            REQUEST=REQUEST,
                                            selection_method=selection_method,
1195
                                            context=context)
1196
      return value_list
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1197

Jean-Paul Smets's avatar
Jean-Paul Smets committed
1198 1199 1200
    security.declareProtected(ERP5Permissions.View, 'getSelectionUidList')
    def getSelectionUidList(self, selection_name, REQUEST=None, selection_method=None, context=None):
      """
1201
        Get the list of uids checked or selected for 'selection_name'
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1202
      """
1203
      return [x.getObject().getUid() for x in self.getSelectionValueList(selection_name, REQUEST=REQUEST, selection_method=selection_method, context=context)]
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1204

Sebastien Robin's avatar
Sebastien Robin committed
1205 1206 1207 1208 1209
    security.declareProtected(ERP5Permissions.View, 'selectionHasChanged')
    def selectionHasChanged(self, md5_string, object_uid_list):
      """
        We want to be sure that the selection did not change
      """
1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226
      return md5_string != self._getUIDListChecksum(object_uid_list)

    security.declareProtected(ERP5Permissions.View, 'getSelectionChecksum')
    def getSelectionChecksum(self, selection_name, uid_list=None):
      """Generate an MD5 checksum against checked uids. This is used to confirm
      that selected values do not change between a display of a dialog and an
      execution.
      uid_list (deprecated)
        For backward compatibility with code not updating selected uids.
      """
      if uid_list is None:
        uid_list = self.getSelectionCheckedUidsFor(selection_name)
      return self._getUIDListChecksum(uid_list)

    def _getUIDListChecksum(self, uid_list):
      if uid_list is None:
          return None
1227
      # XXX To avoid the difference of the string representations of int and long,
1228
      # convert each element to a string.
1229
      return md5(str2bytes(repr(sorted(str(e) for e in uid_list)))).hexdigest()
Sebastien Robin's avatar
Sebastien Robin committed
1230

1231
    # Related document searching
1232
    security.declarePublic('viewSearchRelatedDocumentDialog')
1233
    def viewSearchRelatedDocumentDialog(self, index, form_id,
1234
                                        REQUEST=None, sub_index=None, **kw):
1235
      """
1236 1237
      Returns a search related document dialog
      A set of forwarders us defined to circumvent limitations of HTML
1238
      """
Romain Courteaud's avatar
Romain Courteaud committed
1239 1240
      if sub_index != None:
        REQUEST.form['sub_index'] = sub_index
1241
      object_path = REQUEST.form['object_path']
1242
      # Find the object which needs to be updated
1243
      o = self.restrictedTraverse(object_path)
1244
      # Find the field which was clicked on
1245
      # Important to get from the object instead of self
1246
      form = getattr(o, form_id)
1247
      field = None
1248 1249
      # Search the correct field
      relation_field_found = 0
1250
      relation_index = 0
1251
      # XXX may be should support another parameter,
1252
      for field in form.get_fields(include_disabled=0):
1253 1254
        if field.get_value('editable', REQUEST=REQUEST):
          try:
1255 1256
           field.get_value('is_relation_field')
          except KeyError:
1257
            pass
1258
          else:
1259 1260 1261 1262 1263
            if index == relation_index:
              relation_field_found = 1
              break
            else:
              relation_index += 1
1264
      if not relation_field_found:
1265
        # We didn't find the field...
1266 1267
        raise SelectionError("SelectionTool: can not find the relation field %s"
                             % index)
1268 1269 1270 1271
      else:
        # Field found
        field_key = field.generate_field_key()
        field_value = REQUEST.form[field_key]
1272 1273
        dialog_id = field.get_value('relation_form_id') or \
                                                   'Base_viewRelatedObjectList'
1274
        redirect_form = getattr(o, dialog_id)
1275 1276 1277
        # XXX Hardcoded listbox field
        selection_name = redirect_form.listbox.get_value('selection_name')
        # Reset current selection
1278
        self.setSelectionFor(selection_name, None)
1279 1280 1281 1282


        if (field.get_value('is_multi_relation_field')) and \
           (sub_index is None):
1283 1284 1285 1286
          # user click on the wheel, not on the validation button
          # we need to facilitate user search

          # first: store current field value in the selection
1287
          base_category = field.get_value('base_category')
1288

1289 1290 1291 1292 1293 1294
          property_get_related_uid_method_name = \
            "get%sUidList" % ''.join(['%s%s' % (x[0].upper(), x[1:]) \
                                      for x in base_category.split('_')])
          current_uid_list = getattr(o, property_get_related_uid_method_name)\
                               (portal_type=[x[0] for x in \
                                  field.get_value('portal_type')])
Romain Courteaud's avatar
Romain Courteaud committed
1295 1296
          # Checked current uid
          kw ={}
1297 1298
          catalog_index = field.get_value('catalog_index')
          kw[catalog_index] = field_value
1299
          self.setSelectionParamsFor(selection_name,
Vincent Pelletier's avatar
Vincent Pelletier committed
1300 1301 1302
                                     kw.copy())
          self.setSelectionCheckedUidsFor(selection_name,
                                          current_uid_list)
1303 1304 1305 1306
          field_value = str(field_value)
          if len(field_value):
            sql_catalog = self.portal_catalog.getSQLCatalog()
            field_value = sql_catalog.buildQuery({
1307 1308
              catalog_index:{'query':field_value.splitlines(),
                             'key':'ExactMatch',},
1309 1310
            }).asSearchTextExpression(sql_catalog, column='')

1311
          REQUEST.form[field_key] = field_value
1312
          portal_status_message = translateString("Please select one (or more) object.")
1313
        else:
1314
          portal_status_message = translateString("Please select one object.")
1315 1316


1317 1318
        # Save the current REQUEST form
        # We can't put FileUpload instances because we can't pickle them
1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337
        saved_form_data = {key: value
          for key, value in REQUEST.form.items()
          if not isinstance(value, FileUpload)}

        kw = {
          'dialog_id': dialog_id,
          'selection_name': selection_name,
          'selection_index': 0, # We start on the first page
          'field_id': field.id,
          'reset': 0,
          'base_category': field.get_value( 'base_category'),
          'form_id': form_id,
          field.get_value('catalog_index'): field_value,
          'portal_status_message': portal_status_message,
          'saved_form_data': saved_form_data,
          'ignore_layout': int(REQUEST.get('ignore_layout', 0)),
          'ignore_hide_rows': 1,
        }
        kw.update(field.get_value('parameter_list'))
1338 1339 1340
        # remove ignore_layout parameter from cancel_url otherwise we
        # will have two ignore_layout parameters after clicking cancel
        # button.
1341 1342 1343
        split_referer = list(urlsplit(REQUEST.get('HTTP_REFERER')))
        split_referer[3] = '&'.join([x for x in \
                                     split_referer[3].split('&') \
1344
                                     if not re.match('^ignore_layout[:=]', x)])
1345
        kw['cancel_url'] = urlunsplit(split_referer)
1346

1347 1348
        proxy_listbox_ids = field.get_value('proxy_listbox_ids')
        REQUEST.set('proxy_listbox_ids', proxy_listbox_ids)
1349
        if len(proxy_listbox_ids) > 0:
1350 1351
          REQUEST.set('proxy_listbox_id', proxy_listbox_ids[0][0])
        else:
1352
          REQUEST.set('proxy_listbox_id',
1353 1354 1355
                       "Base_viewRelatedObjectListBase/listbox")

        # Empty the selection (uid)
1356 1357
        REQUEST.form = kw # New request form
        # Define new HTTP_REFERER
Romain Courteaud's avatar
Romain Courteaud committed
1358
        REQUEST.HTTP_REFERER = '%s/%s' % (o.absolute_url(),
1359
                                          dialog_id)
1360 1361 1362 1363 1364

        # If we are called from a Web Site, we should return
        # in the context of the Web Section
        if self.getApplicableLayout() is not None:
          return getattr(o.__of__(self.getWebSectionValue()), dialog_id)(REQUEST=REQUEST)
1365
        # Return the search dialog
1366
        return getattr(o, dialog_id)(REQUEST=REQUEST)
1367

1368
    security.declarePublic('asDomainQuery')
1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381
    def asDomainQuery(self, domain, strict_membership=False):
      if isinstance(domain, DomainSelection):
        warnings.warn("To pass a DomainSelection instance is deprecated.\n"
                      "Please use a domain dict instead.",
                      DeprecationWarning)
      else:
        domain = DomainSelection(domain).__of__(self)
      relation_dict = {}
      query_list = []
      append = query_list.append
      domain_item_dict = domain.asDomainItemDict()
      # XXX: why even put Nones in domain if they are ignored ?
      domain_item_dict.pop(None, None)
1382
      for key, value in six.iteritems(domain_item_dict):
1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399
        if getattr(aq_base(value), 'isPredicate', 0):
          append(
            value.asQuery(strict_membership=strict_membership),
          )
        else:
          relation_dict[key] = [value]
      if relation_dict:
        append(
          self.getPortalObject().portal_catalog.getCategoryValueDictParameterDict(
            relation_dict,
            strict_membership=strict_membership,
          ),
        )
      if query_list:
        return ComplexQuery(query_list)
      return SimpleQuery(uid=0, comparison_operator='>')

1400 1401
    def _aq_dynamic(self, name):
      """
1402
        Generate viewSearchRelatedDocumentDialog0,
1403
                 viewSearchRelatedDocumentDialog1,... if necessary
1404 1405 1406
      """
      aq_base_name = getattr(aq_base(self), name, None)
      if aq_base_name == None:
1407 1408 1409
        DYNAMIC_METHOD_NAME = 'viewSearchRelatedDocumentDialog'
        method_name_length = len(DYNAMIC_METHOD_NAME)

1410
        zope_security = '__roles__'
1411
        if (name[:method_name_length] == DYNAMIC_METHOD_NAME) and \
1412
           (name[-len(zope_security):] != zope_security):
1413
          method_count_string_list = name[method_name_length:].split('_')
1414 1415 1416 1417
          method_count_string = method_count_string_list[0]
          # be sure that method name is correct
          try:
            method_count = string.atoi(method_count_string)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
1418
          except TypeError:
1419
            return aq_base_name
1420
          else:
1421 1422 1423
            if len(method_count_string_list) > 1:
              # be sure that method name is correct
              try:
Romain Courteaud's avatar
Romain Courteaud committed
1424
                sub_index = string.atoi(method_count_string_list[1])
Yoshinori Okuji's avatar
Yoshinori Okuji committed
1425
              except TypeError:
1426 1427
                return aq_base_name
            else:
Romain Courteaud's avatar
Romain Courteaud committed
1428
              sub_index = None
1429

1430
            # generate dynamicaly needed forwarder methods
1431
            def viewSearchRelatedDocumentDialogWrapper(self, form_id,
1432
                                                       REQUEST=None, **kw):
1433 1434 1435
              """
                viewSearchRelatedDocumentDialog Wrapper
              """
1436 1437
#               LOG('SelectionTool.viewSearchRelatedDocumentDialogWrapper, kw',
#                   0, kw)
1438
              return self.viewSearchRelatedDocumentDialog(
1439
                                   method_count, form_id,
1440
                                   REQUEST=REQUEST, sub_index=sub_index, **kw)
1441
            setattr(self.__class__, name,
1442
                    viewSearchRelatedDocumentDialogWrapper)
1443 1444

            klass = aq_base(self).__class__
1445 1446 1447 1448
            security_property_id = '%s__roles__' % (name, )
            # Declare method as public
            setattr(klass, security_property_id, None)

1449
            return getattr(self, name)
1450
      return SelectionTool.inheritedAttribute('_aq_dynamic')(self, name)
1451

1452
    def _getUserId(self):
1453
      tv = getTransactionalVariable()
1454 1455 1456
      user_id = tv.get('_user_id', None)
      if user_id is not None:
        return user_id
1457
      user_id = self.portal_membership.getAuthenticatedMember().getIdOrUserName()
1458 1459
      tv['_user_id'] = user_id
      return user_id
1460

1461
    security.declarePrivate('getTemporarySelectionDict')
1462 1463 1464 1465
    def getTemporarySelectionDict(self):
      """ Temporary selections are used in push/pop nested scope,
      to prevent from editting for stored selection in the scope.
      Typically, it is used for ReportSection."""
1466
      tv = getTransactionalVariable()
1467 1468 1469 1470 1471
      return tv.setdefault('_temporary_selection_dict', {})

    def pushSelection(self, selection_name):
      selection = self.getSelectionFor(selection_name)
      # a temporary selection is kept in transaction.
1472
      temp_selection = Selection(selection_name)
1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483
      if selection:
        temp_selection.__dict__.update(selection.__dict__)
      self.getTemporarySelectionDict()\
        .setdefault(selection_name, []).append(temp_selection)

    def popSelection(self, selection_name):
      temporary_selection_dict = self.getTemporarySelectionDict()
      if selection_name in temporary_selection_dict and \
         temporary_selection_dict[selection_name]:
        temporary_selection_dict[selection_name].pop()

1484 1485 1486 1487 1488 1489 1490
    def getAnonymousSelection(self, key, selection_name):
      container_id = '_v_anonymous_selection_container'
      storage = self.getAnonymousStorage() or self.getStorage()
      container = self._getContainerFromStorage(container_id, storage)
      return container.getSelection(key, selection_name)

    def getAnonymousSelectionKey(self, selection_name, REQUEST=None):
1491
      if not self.isAnonymous():
1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502
        return ''
      selection = self.getSelectionFor(selection_name, REQUEST=REQUEST)
      if selection is None:
        return ''
      key = selection.getAnonymousSelectionKey()
      container_id = '_v_anonymous_selection_container'
      storage = self.getAnonymousStorage() or self.getStorage()
      container = self._getContainerFromStorage(container_id, storage)
      container.setSelection(key, selection_name, selection)
      return key

1503 1504 1505
    def _getSelectionFromContainer(self, selection_name):
      user_id = self._getUserId()
      if user_id is None: return None
1506 1507 1508 1509 1510 1511 1512

      temporary_selection_dict = self.getTemporarySelectionDict()
      if temporary_selection_dict and selection_name in temporary_selection_dict:
        if temporary_selection_dict[selection_name]:
          # focus the temporary selection in the most narrow scope.
          return temporary_selection_dict[selection_name][-1]

1513
      return self._getContainer().getSelection(user_id, selection_name)
1514 1515 1516 1517

    def _setSelectionToContainer(self, selection_name, selection):
      user_id = self._getUserId()
      if user_id is None: return
1518 1519 1520 1521 1522 1523 1524 1525

      temporary_selection_dict = self.getTemporarySelectionDict()
      if temporary_selection_dict and selection_name in temporary_selection_dict:
        if temporary_selection_dict[selection_name]:
          # focus the temporary selection in the most narrow scope.
          temporary_selection_dict[selection_name][-1] = selection
          return

1526
      self._getContainer().setSelection(user_id, selection_name, selection)
1527

1528
    def _deleteSelectionForUserFromContainer(self, selection_name, user_id):
1529
      if user_id is None: return None
1530
      self._getContainer().deleteSelection(user_id, selection_name)
1531

1532 1533 1534 1535
    def _deleteSelectionFromContainer(self, selection_name):
      user_id = self._getUserId()
      self._deleteSelectionForUserFromContainer(selection_name, user_id)

1536
    def _deleteGlobalSelectionFromContainer(self, selection_name):
1537
      self._getContainer().deleteGlobalSelection(self, selection_name)
1538

1539
    def _getSelectionNameListFromContainer(self):
1540
      user_id = self._getUserId()
1541
      return list(set(self._getContainer().getSelectionNameList(user_id) +
1542 1543
                      self.getTemporarySelectionDict().keys()))

1544
    def isAnonymous(self):
1545
      return self._getUserId() == 'Anonymous User'
1546

1547
    def _getContainer(self):
1548
      if self.isAnonymous():
1549 1550 1551 1552
        tv = getTransactionalVariable()
        storage = tv.setdefault('_transactional_selection_container', {})
        container = TransactionalCacheContainer(storage)
        return container
1553 1554 1555
      else:
        container_id = '_v_selection_container'
        storage = self.getStorage()
1556 1557 1558
        return self._getContainerFromStorage(container_id, storage)

    def _getContainerFromStorage(self, container_id, storage):
1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573
      container = getattr(aq_base(self), container_id, None)
      if container is None:
        if storage.startswith('portal_memcached/'):
          plugin_path = storage
          value = self.getPortalObject().\
                  portal_memcached.getMemcachedDict(key_prefix='selection_tool',
                                                    plugin_path=plugin_path)
          container = MemcachedContainer(value)
        else:
          if getattr(aq_base(self), 'selection_data', None) is None:
            self.selection_data = PersistentMapping()
          value = self.selection_data
          container = PersistentMappingContainer(value)
        setattr(self, container_id, container)
      return container
1574

Jean-Paul Smets's avatar
Jean-Paul Smets committed
1575
InitializeClass( SelectionTool )
1576

1577
class BaseContainer(object):
1578 1579 1580
  def __init__(self, container):
    self._container = container

1581
class MemcachedContainer(BaseContainer):
1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599
  def getSelectionNameList(self, user_id):
    return []

  def getSelection(self, user_id, selection_name):
    try:
      return self._container.get('%s-%s' % (user_id, selection_name))
    except KeyError:
      return None

  def setSelection(self, user_id, selection_name, selection):
    self._container.set('%s-%s' % (user_id, selection_name), aq_base(selection))

  def deleteSelection(self, user_id, selection_name):
    del(self._container['%s-%s' % (user_id, selection_name)])

  def deleteGlobalSelection(self, user_id, selection_name):
    pass

1600 1601 1602
class TransactionalCacheContainer(MemcachedContainer):
  def setSelection(self, user_id, selection_name, selection):
    self._container.__setitem__('%s-%s' % (user_id, selection_name), aq_base(selection))
1603

1604
class PersistentMappingContainer(BaseContainer):
1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632
  def getSelectionNameList(self, user_id):
    try:
      return self._container[user_id].keys()
    except KeyError:
      return []

  def getSelection(self, user_id, selection_name):
    try:
      return self._container[user_id][selection_name]
    except KeyError:
      return None

  def setSelection(self, user_id, selection_name, selection):
    try:
      user_container = self._container[user_id]
    except KeyError:
      user_container = SelectionPersistentMapping()
      self._container[user_id] = user_container
    user_container[selection_name] = aq_base(selection)

  def deleteSelection(self, user_id, selection_name):
    try:
      user_container = self._container[user_id]
      del(user_container[selection_name])
    except KeyError:
      pass

  def deleteGlobalSelection(self, user_id, selection_name):
1633
    for user_container in six.itervalues(self._container):
1634 1635 1636 1637
      try:
        del(user_container[selection_name])
      except KeyError:
        pass
1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648

class SelectionPersistentMapping(PersistentMapping):
  """A conflict-free PersistentMapping.

  Like selection objects, the purpose is to only prevent restarting
  transactions.
  """
  def _p_independent(self) :
    return 1

  def _p_resolveConflict(self, oldState, savedState, newState):
1649
    # BUG: we should not modify newState
1650
    # update keys that only savedState has
1651 1652
    newState['data'].update(savedState['data'])
    return newState
1653 1654


1655 1656 1657 1658 1659 1660 1661 1662
class TreeListLine:
  def __init__(self,object,is_pure_summary,depth, is_open,select_domain_dict,exception_uid_list):
    self.object=object
    self.is_pure_summary=is_pure_summary
    self.depth=depth
    self.is_open=is_open
    self.select_domain_dict=select_domain_dict
    self.exception_uid_list=exception_uid_list
1663

1664 1665
  def getObject(self):
    return self.object
1666

1667 1668
  def getIsPureSummary(self):
    return self.is_pure_summary
1669

1670 1671
  def getDepth(self):
    return self.depth
1672

1673 1674
  def getIsOpen(self):
    return self.is_open
1675 1676

  def getSelectDomainDict(self):
1677
    return self.select_domain_dict
1678

1679 1680
  def getExceptionUidList(self):
    return self.exception_uid_list
1681

1682

1683
def makeTreeList(here, form, root_dict, report_path, base_category,
Nicolas Delaby's avatar
Nicolas Delaby committed
1684 1685 1686
                 depth, unfolded_list, form_id, selection_name,
                 report_depth, is_report_opened=1, list_method=None,
                 filtered_portal_types=[] ,sort_on = (('id', 'ASC'),)):
1687 1688 1689 1690 1691
  """
    (object, is_pure_summary, depth, is_open, select_domain_dict)

    select_domain_dict is a dictionary of  associative list of (id, domain)
  """
1692 1693
  if isinstance(report_path, str):
    report_path = report_path.split('/')
1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705

  portal_categories = getattr(form, 'portal_categories', None)
  portal_domains = getattr(form, 'portal_domains', None)
  portal_object = form.portal_url.getPortalObject()
  if len(report_path):
    base_category = report_path[0]

  if root_dict is None:
    root_dict = {}

  is_empty_level = 1
  while is_empty_level:
1706
    if base_category not in root_dict:
1707 1708
      root = None
      if portal_categories is not None:
1709
        if portal_categories._getOb(base_category, None) is not None:
1710 1711 1712 1713
          if base_category == 'parent':
            # parent has a special treatment
            root = root_dict[base_category] = root_dict[None] = here
            report_path = report_path[1:]
1714
          else:
1715 1716
            root = root_dict[base_category] = root_dict[None] = \
                                               portal_categories[base_category]
1717 1718
            report_path = report_path[1:]
      if root is None and portal_domains is not None:
1719 1720 1721
        if portal_domains._getOb(base_category, None) is not None:
          root = root_dict[base_category] = root_dict[None] = \
                                               portal_domains[base_category]
1722 1723 1724
          report_path = report_path[1:]
      if root is None:
        try:
1725 1726
          root = root_dict[None] = \
              portal_object.unrestrictedTraverse(report_path)
1727
        except KeyError:
1728
          LOG('SelectionTool', INFO, "Not found %s" % str(report_path))
1729 1730 1731 1732 1733
          root = None
        report_path = ()
    else:
      root = root_dict[None] = root_dict[base_category]
      report_path = report_path[1:]
1734 1735
    is_empty_level = (root is not None) and \
        (root.objectCount() == 0) and (len(report_path) != 0)
1736
    if is_empty_level:
1737
      base_category = report_path[0]
1738 1739

  tree_list = []
1740
  if root is None:
1741
    return tree_list
1742

1743
  if base_category == 'parent':
1744 1745 1746 1747 1748
    # Use searchFolder as default
    if list_method is None:
      if hasattr(aq_base(root), 'objectValues'):
        # If this is a folder, try to browse the hierarchy
        object_list = root.searchFolder(sort_on=sort_on)
1749
    else:
1750
      if filtered_portal_types not in [[],None,'']:
1751 1752
        object_list = list_method(portal_type=filtered_portal_types,
                                  sort_on=sort_on)
1753
      else:
1754
        object_list = list_method(sort_on=sort_on)
1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770
    for zo in object_list:
      o = zo.getObject()
      if o is not None:
        new_root_dict = root_dict.copy()
        new_root_dict[None] = new_root_dict[base_category] = o

        selection_domain = DomainSelection(domain_dict = new_root_dict)
        if (report_depth is not None and depth <= (report_depth - 1)) or \
                                          o.getRelativeUrl() in unfolded_list:
          exception_uid_list = [] # Object we do not want to display

          for sub_zo in o.searchFolder(sort_on=sort_on):
            sub_o = sub_zo.getObject()
            if sub_o is not None and hasattr(aq_base(root), 'objectValues'):
              exception_uid_list.append(sub_o.getUid())
          # Summary (open)
1771
          tree_list += [TreeListLine(o, 1, depth, 1, selection_domain, exception_uid_list)]
1772 1773
          if is_report_opened :
            # List (contents, closed, must be strict selection)
1774 1775 1776
            tree_list += [TreeListLine(o, 0, depth, 0, selection_domain, exception_uid_list)]

          tree_list += makeTreeList(here, form, new_root_dict, report_path,
1777 1778 1779
                    base_category, depth + 1, unfolded_list, form_id,
                    selection_name, report_depth,
                    is_report_opened=is_report_opened, sort_on=sort_on)
1780 1781
        else:
          tree_list += [TreeListLine(o, 1, depth, 0, selection_domain, ())] # Summary (closed)
1782
  else:
1783 1784 1785 1786 1787 1788
    # process to recover objects in case a generation script is used
    if hasattr(root,'getChildDomainValueList'):
      oblist = root.getChildDomainValueList(root,depth=depth)
    else:
      oblist = root.objectValues()
    for o in oblist:
1789 1790 1791 1792 1793 1794 1795
      new_root_dict = root_dict.copy()
      new_root_dict[None] = new_root_dict[base_category] = o
      selection_domain = DomainSelection(domain_dict = new_root_dict)
      if (report_depth is not None and depth <= (report_depth - 1)) or o.getRelativeUrl() in unfolded_list:
        tree_list += [TreeListLine(o, 1, depth, 1, selection_domain, None)] # Summary (open)
        if is_report_opened :
          tree_list += [TreeListLine(o, 0, depth, 0, selection_domain, None)] # List (contents, closed, must be strict selection)
1796 1797
        tree_list += makeTreeList(here, form, new_root_dict, report_path, base_category, depth + 1,
            unfolded_list, form_id, selection_name, report_depth,
1798 1799 1800 1801
            is_report_opened=is_report_opened, sort_on=sort_on)
      else:

        tree_list += [TreeListLine(o, 1, depth, 0, selection_domain, None)] # Summary (closed)
1802

1803 1804
  return tree_list

1805
# Automaticaly add wrappers on Folder so it can access portal_selections.
1806
# Cannot be done in ERP5Type/Document/Folder.py because ERP5Type must not
1807
# depend on ERP5Form.
1808 1809

from Products.CMFCore.utils import getToolByName
1810
from Products.ERP5Type.Core.Folder import FolderMixIn
1811 1812
from ZPublisher.mapply import mapply

1813
method_id_filter_list = [x for x in FolderMixIn.__dict__ if callable(getattr(FolderMixIn, x))]
1814 1815 1816 1817 1818 1819 1820 1821 1822
candidate_method_id_list = []
for x in SelectionTool.__dict__:
  if not callable(getattr(SelectionTool, x)):
    continue
  if x.startswith('_') or x.endswith('__roles__'):
    continue
  if x in method_id_filter_list:
    continue
  roles = getattr(SelectionTool, '%s__roles__' % x, None)
1823
  if roles is None or roles == ():
1824 1825 1826 1827
    continue
  if roles.__name__ == ERP5Permissions.ManagePortal:
    continue
  candidate_method_id_list.append(x)
1828

1829 1830 1831
# Monkey patch FolderMixIn with SelectionTool methods, and wrapper methods
# ('listbox_<WRAPPED_METHOD_NAME>()') used to set ListBox properties for
# pagination
1832 1833 1834 1835 1836 1837 1838 1839 1840 1841
for property_id in candidate_method_id_list:
  def portal_selection_wrapper(self, wrapper_property_id=property_id, *args, **kw):
    """
      Wrapper method for SelectionTool.
    """
    portal_selection = getToolByName(self, 'portal_selections')
    request = self.REQUEST
    method = getattr(portal_selection, wrapper_property_id)
    return mapply(method, positional=args, keyword=request,
                  context=self, bind=1)
1842
  setattr(FolderMixIn, property_id, portal_selection_wrapper)
1843 1844 1845
  security_property_id = '%s__roles__' % (property_id, )
  security_property = getattr(SelectionTool, security_property_id, None)
  if security_property is not None:
1846
    setattr(FolderMixIn, security_property_id, security_property)
1847

1848 1849 1850 1851 1852 1853
  def portal_selection_wrapper(self, wrapper_property_id=property_id, *args, **kw):
    """
      Wrapper method for SelectionTool.
    """
    portal_selection = getToolByName(self, 'portal_selections')
    request = self.REQUEST
1854 1855
    listbox_id = request.form.get('listbox_%s' % wrapper_property_id, None)
    if not listbox_id:
1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870
      # Backward-compatibility: Should be removed as soon as
      # createFolderMixInPageSelectionMethod has been removed
      warnings.warn(
        "DEPRECATED: listbox_%s: 'value' attribute of the submit button "
        "should be set to the ListBox ID and the method name 'listbox_%s" %
        (wrapper_property_id, wrapper_property_id),
        DeprecationWarning)

      listbox_id = 'listbox'

    selection_name_property_id = "%s_list_selection_name" % listbox_id
    listbox_uid_property_id = "%s_uid" % listbox_id
    list_start_property_id = "%s_list_start" % listbox_id
    page_start_property_id = "%s_page_start" % listbox_id
    # Rename request parameters
1871
    if selection_name_property_id in request:
1872
      request.form['list_selection_name'] = request[selection_name_property_id]
1873
    if listbox_uid_property_id in request:
1874
      request.form['listbox_uid'] = request[listbox_uid_property_id]
1875
    if list_start_property_id in request:
1876
      request.form['list_start'] = request[list_start_property_id]
1877
    if page_start_property_id in request:
1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890
      request.form['page_start'] = request[page_start_property_id]
    # Call the wrapper
    method = getattr(portal_selection, wrapper_property_id)
    return mapply(method, positional=args, keyword=request,
                  context=self, bind=1)
  new_property_id = "listbox_%s" % property_id
  setattr(FolderMixIn, new_property_id, portal_selection_wrapper)
  security_property_id = '%s__roles__' % (property_id, )
  security_property = getattr(SelectionTool, security_property_id, None)
  if security_property is not None:
    new_security_property_id = '%s__roles__' % (new_property_id, )
    setattr(FolderMixIn, new_security_property_id, security_property)

1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901
def createFolderMixInPageSelectionMethod(listbox_id):
  """
  This method must be called by listbox at rendering time.
  It dynamically creates methods on FolderMixIn in line
  with the naming of the listbox field. Generated method
  are able to convert request parameters in order to
  mimic the API of a listbox with ID "listbox". This
  approach was required for example to implement
  multiple multi-page listboxes in view mode. It also
  opens the way towards multiple editable listboxes in the same
  page although this is something which we can not recommend.
1902 1903 1904 1905

  Deprecated because these methods are generated when rendering the
  ListBox. Therefore, they are only available on the ZEO client
  where it has been rendered but not the other ZEO clients.
1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917
  """
  # Immediately return in the method already exists
  test_method_id = "%s_nextPage" % listbox_id
  if hasattr(FolderMixIn, test_method_id):
    return
  # Monkey patch FolderMixIn
  for property_id in candidate_method_id_list:
    def portal_selection_wrapper(self, wrapper_listbox_id=listbox_id,
                                       wrapper_property_id=property_id, *args, **kw):
      """
        Wrapper method for SelectionTool.
      """
1918 1919 1920 1921
      warnings.warn(
        "DEPRECATED: %s_%s: The ListBox ID must not be contained anymore in the "
        "method name, but instead be in the 'value' attribute of the submit "
        "button and the method name should be 'listbox_%s'" %
1922
        (wrapper_listbox_id, wrapper_property_id, wrapper_listbox_id),
1923 1924
        DeprecationWarning)

1925 1926 1927 1928 1929
      portal_selection = getToolByName(self, 'portal_selections')
      request = self.REQUEST
      selection_name_property_id = "%s_list_selection_name" % listbox_id
      listbox_uid_property_id = "%s_uid" % listbox_id
      list_start_property_id = "%s_list_start" % listbox_id
Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
1930
      page_start_property_id = "%s_page_start" % listbox_id
1931
      # Rename request parameters
1932
      if selection_name_property_id in request:
1933
        request.form['list_selection_name'] = request[selection_name_property_id]
1934
      if listbox_uid_property_id in request:
1935
        request.form['listbox_uid'] = request[listbox_uid_property_id]
1936
      if list_start_property_id in request:
1937
        request.form['list_start'] = request[list_start_property_id]
1938
      if page_start_property_id in request:
Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
1939
        request.form['page_start'] = request[page_start_property_id]
1940 1941 1942 1943 1944 1945 1946 1947 1948 1949
      # Call the wrapper
      method = getattr(portal_selection, wrapper_property_id)
      return mapply(method, positional=args, keyword=request,
                    context=self, bind=1)
    new_property_id = "%s_%s" % (listbox_id, property_id)
    setattr(FolderMixIn, new_property_id, portal_selection_wrapper)
    security_property_id = '%s__roles__' % (property_id, )
    security_property = getattr(SelectionTool, security_property_id, None)
    if security_property is not None:
      new_security_property_id = '%s__roles__' % (new_property_id, )
1950
      setattr(FolderMixIn, new_security_property_id, security_property)