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
Jean-Paul Smets's avatar
Jean-Paul Smets committed
42
from Products.ERP5Form import _dtmldir
43
from Products.ERP5Form.Selection import Selection, DomainSelection
44
from ZPublisher.HTTPRequest import FileUpload
45
from hashlib import md5
46
import string, re
47
from six.moves.urllib.parse import urlsplit, urlunsplit
48
from zLOG import LOG, INFO, WARNING
49
from Acquisition import aq_base
50
from Products.ERP5Type.Message import translateString
51
from Products.ZSQLCatalog.SQLCatalog import SimpleQuery, ComplexQuery
52
import warnings
53
import six
54

55 56
_MARKER = []

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

60
class SelectionTool( BaseTool, SimpleItem ):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
61 62 63 64 65 66 67 68
    """
      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'
69
    portal_type     = 'Selection Tool'
70
    title           = 'Selections'
Jean-Paul Smets's avatar
Jean-Paul Smets committed
71 72 73 74 75 76 77
    security = ClassSecurityInfo()

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

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

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

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

98 99 100 101 102 103 104 105 106 107 108
    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'))

109 110 111 112 113 114 115 116 117 118 119
    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'))

120 121 122 123 124 125 126 127 128 129 130
    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'))

131
    # storages of SelectionTool
132 133 134 135 136 137 138 139 140 141 142
    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
143

144
    security.declareProtected(ERP5Permissions.ModifyPortalContent, 'clearCachedContainer')
145 146 147 148 149 150 151 152 153 154 155 156 157
    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)

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

177
    security.declareProtected( ERP5Permissions.ManagePortal, 'getStorage')
178
    def getStorage(self, default='selection_data'):
179 180
      """return the selected storage
      """
181
      storage = getattr(aq_base(self), 'storage', default)
182
      if storage is not default:
183 184 185 186 187
        #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')
188 189 190 191
          if len(memcached_plugin_list):
            storage = memcached_plugin_list[0].getRelativeUrl()
          else:
            storage = 'selection_data'
192 193
      return storage

194 195 196 197 198 199
    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
200 201
    def _redirectToOriginalForm(self, REQUEST=None, form_id=None, dialog_id=None,
                                query_string=None,
202
                                no_reset=False, no_report_depth=False):
Vincent Pelletier's avatar
Vincent Pelletier committed
203 204
      """Redirect to the original form or dialog, using the information given
         as parameters.
205 206
         (Actually does not redirect  in the HTTP meaning because of URL
         limitation problems.)
Vincent Pelletier's avatar
Vincent Pelletier committed
207 208 209

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

Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
217
      form = REQUEST.form
218
      if no_reset and 'reset' in form:
Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
219 220
        form['noreset'] = form['reset'] # Kept for compatibility - might no be used anymore
        del form['reset']
221
      if no_report_depth and 'report_depth' in form:
Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
222 223
        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
224

225
      if query_string is not None:
226 227
        warnings.warn('DEPRECATED: _redirectToOriginalForm got called with a query_string. The variables must be passed in REQUEST.form.',
                      DeprecationWarning)
228
      context = REQUEST['PARENTS'][0]
229 230 231
      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
232
      return getattr(context, form_id)()
233

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

241 242 243 244 245 246 247 248
    # 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)

249
    security.declareProtected(ERP5Permissions.View, 'callSelectionFor')
250
    def callSelectionFor(self, selection_name, method=None, context=None,
251 252 253
                                               REQUEST=None, params=None):
      """
      Calls the selection and return the list of selected documents
254
      or objects. Seledction method, context and parameters may be
255 256 257 258 259 260 261 262 263
      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

264
      REQUEST -- optional REQUEST parameters (not used, only to
265 266 267 268 269 270 271 272
                 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)
      """
273 274 275 276
      if context is None: context = self
      selection = self.getSelectionFor(selection_name, REQUEST=REQUEST)
      if selection is None:
        return None
277
      return selection(method=method, context=context, REQUEST=REQUEST, params=params)
278

279 280 281 282 283 284 285 286 287 288 289
    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
290 291 292 293 294
    security.declareProtected(ERP5Permissions.View, 'getSelectionFor')
    def getSelectionFor(self, selection_name, REQUEST=None):
      """
        Returns the selection instance for a given selection_name
      """
295 296
      if isinstance(selection_name, (tuple, list)):
        selection_name = selection_name[0]
297 298
      if not selection_name:
        return None
299
      selection = self._getSelectionFromContainer(selection_name)
300
      if selection is None and self.isAnonymous():
301 302 303 304
        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)
305 306
      if selection is not None:
        return selection.__of__(self)
307

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

Jean-Paul Smets's avatar
Jean-Paul Smets committed
311 312 313 314 315
    security.declareProtected(ERP5Permissions.View, 'setSelectionFor')
    def setSelectionFor(self, selection_name, selection_object, REQUEST=None):
      """
        Sets the selection instance for a given selection_name
      """
316 317
      if not selection_name:
        return
318 319 320 321 322 323
      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))
324
      elif self.getSelectionFor(selection_name, REQUEST=REQUEST) != selection_object:
325
        self._setSelectionToContainer(selection_name, selection_object)
326
      if selection_object is None and self.isAnonymous():
327 328 329 330 331 332
        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
333

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

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

Jean-Paul Smets's avatar
Jean-Paul Smets committed
351 352 353 354 355 356
    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)
357
      if selection_object is not None:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
358 359
        selection_object.edit(params=params)
      else:
360
        selection_object = Selection(selection_name, params=params)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
361 362
      self.setSelectionFor(selection_name, selection_object, REQUEST)

363
    security.declareProtected(ERP5Permissions.View, 'getSelectionDomainDictFor')
364 365 366 367 368 369 370
    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:
371
          return selection.getDomain().asDomainDict()
372 373 374
        except AttributeError:
          return {}

375
    security.declareProtected(ERP5Permissions.View, 'getSelectionReportDictFor')
376 377 378 379 380 381 382
    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:
383
          return selection.getReport().asDomainDict()
384 385 386
        except AttributeError:
          return {}

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

399
    security.declareProtected(ERP5Permissions.View, 'updateSelectionCheckedUidList')
400 401
    def updateSelectionCheckedUidList(self, selection_name, listbox_uid, uids, REQUEST=None):
      """
402 403
        Updates the unchecked uids(listbox_uids) and checked uids (uids)
        for a given selection_name
404 405 406 407 408 409 410 411
      """
      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)

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

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

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

Jean-Paul Smets's avatar
Jean-Paul Smets committed
464 465 466 467 468 469 470
    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:
471
        url = selection.getListUrl()
472
        if self.isAnonymous() and '?' in url:
473 474
          url += '&selection_key=%s' % self._getSelectionKeyFromRequest(selection_name, REQUEST)
        return url
Jean-Paul Smets's avatar
Jean-Paul Smets committed
475 476
      else:
        return None
477

478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494
    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
495

496
    security.declareProtected(ERP5Permissions.View, 'getSelectionInvertModeUidListFor')
497 498 499 500 501 502 503 504
    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

505 506 507 508 509 510 511 512 513
    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
514 515 516 517 518 519 520
    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:
521
        selection.edit(invert_mode=1, uids=selection_uids, checked_uids=selection_uids)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
522 523

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

    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')
547
    def setSelectionQuickSortOrder(self, selection_name=None, sort_on=None, REQUEST=None,
548
                                   query_string=None, form_id=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
549 550 551 552
      """
        Defines the sort order of the selection directly from the listbox
        In this method, sort_on is just a string that comes from url
      """
553 554
      # 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
555
      listbox_id = None
556 557
      if REQUEST is not None:
        form = REQUEST.form
558
      if sort_on is None:
Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
559
        listbox_id, sort_on = form["setSelectionQuickSortOrder"].split(".", 1)
560

561
      # Sort order can be specified in sort_on.
562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581
      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]
582

583 584 585 586 587 588
      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']
589

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

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

617 618
        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
619 620 621 622 623 624 625 626

    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 ()
627
      return selection.sort_on
Jean-Paul Smets's avatar
Jean-Paul Smets committed
628 629 630 631 632 633 634 635 636 637

    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')
638
    def getSelectionColumns(self, selection_name, columns=None, REQUEST=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
639
      """
640 641
        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
642
      """
643
      if columns is None: columns = []
Jean-Paul Smets's avatar
Jean-Paul Smets committed
644 645
      selection = self.getSelectionFor(selection_name, REQUEST=REQUEST)
      if selection is not None:
646 647
        if len(selection.columns) > 0:
          return selection.columns
648
      return columns
Jean-Paul Smets's avatar
Jean-Paul Smets committed
649 650 651 652 653 654 655 656 657 658 659


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

673 674 675 676 677 678 679
    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.
680 681 682 683 684 685 686
        # 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):
687 688
          return form_id
      return 'view'
Jean-Paul Smets's avatar
Jean-Paul Smets committed
689 690 691 692 693 694

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

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

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

    security.declareProtected(ERP5Permissions.View, 'viewPrevious')
    def viewPrevious(self, selection_index='', selection_name='', form_id='view', REQUEST=None):
      """
        Access previous item in a selection
      """
716 717 718
      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
719 720 721 722
      if not REQUEST:
        REQUEST = get_request()
      selection = self.getSelectionFor(selection_name, REQUEST=REQUEST)
      if selection:
723
        method = self.unrestrictedTraverse(selection.method_path)
724
        selection_list = selection(method = method, context=self, REQUEST=REQUEST)
725
        if len(selection_list):
726 727 728
          if selection_index > 0:
            selection_index = selection_index % len(selection_list)
          o = selection_list[selection_index]
729
          url = o.absolute_url()
730
          form_id = self._getExistingFormId(o.getObject(), form_id)
731 732
        else:
          url = REQUEST.getURL()
Jean-Paul Smets's avatar
Jean-Paul Smets committed
733
      else:
734
        url = REQUEST.getURL()
Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
735 736
      if form_id != 'view':
        url += '/%s' % form_id
737 738 739 740 741 742
      query_kw = {
        'selection_index': selection_index,
        'selection_name': selection_name,
      }
      if int(REQUEST.get('ignore_layout', 0)):
        query_kw['ignore_layout'] = 1
743
      if self.isAnonymous():
744 745
        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
746 747

    # ListBox related methods
748 749

    security.declareProtected(ERP5Permissions.View, 'firstPage')
Vincent Pelletier's avatar
Vincent Pelletier committed
750
    def firstPage(self, list_selection_name, listbox_uid, uids=None, REQUEST=None):
751 752 753 754
      """
        Access the first page of a list
      """
      if uids is None: uids = []
755 756 757 758 759
      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
760 761
      self.uncheckAll(list_selection_name, listbox_uid)
      return self.checkAll(list_selection_name, uids, REQUEST=REQUEST)
762 763

    security.declareProtected(ERP5Permissions.View, 'lastPage')
Vincent Pelletier's avatar
Vincent Pelletier committed
764
    def lastPage(self, list_selection_name, listbox_uid, uids=None, REQUEST=None):
765 766 767 768
      """
        Access the last page of a list
      """
      if uids is None: uids = []
Vincent Pelletier's avatar
Vincent Pelletier committed
769
      selection = self.getSelectionFor(list_selection_name, REQUEST)
770 771 772 773 774 775 776
      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
777
        total_lines = int(params.get('total_size', BIG_INT))
778
        if total_lines != BIG_INT:
779 780 781 782 783 784 785 786 787
          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
788 789
        params['list_start'] = last_page_start
        selection.edit(params=params)
Vincent Pelletier's avatar
Vincent Pelletier committed
790 791
      self.uncheckAll(list_selection_name, listbox_uid)
      return self.checkAll(list_selection_name, uids, REQUEST=REQUEST)
792

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

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

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

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

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

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

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

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

944 945 946 947 948 949
    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
950

951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973
    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,
      )

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

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

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

991 992 993
      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
994

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

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

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

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

1015

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

1026 1027 1028
      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
1029 1030

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

1041 1042 1043
      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
1044 1045

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

1057 1058 1059
      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
1060

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

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

Jean-Paul Smets's avatar
Jean-Paul Smets committed
1073
    security.declareProtected(ERP5Permissions.View, 'setListboxDisplayMode')
1074
    def setListboxDisplayMode(self, REQUEST, listbox_display_mode,
1075 1076
                              selection_name=None, redirect=0,
                              form_id=None, query_string=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1077
      """
1078
        Toggle display of the listbox
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1079 1080
      """
      request = REQUEST
1081 1082 1083 1084 1085 1086 1087
      # 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
1088 1089
      # Possible fix: currently, display mode icon are implemented as
      # method. It could be easier to generate them as link (where we
1090 1091 1092 1093 1094 1095 1096 1097 1098
      # 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
1099
      selection = self.getSelectionFor(selection_name, REQUEST)
1100
      if selection is None:
1101
        selection = Selection(selection_name)
1102
        self.setSelectionFor(selection_name, selection, REQUEST=REQUEST)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115

      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
1116 1117 1118
      else:
        flat_list_mode = 0
        domain_tree_mode = 0
1119
        report_tree_mode = 0
1120

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

1129
      if redirect:
1130 1131 1132
        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
1133 1134

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

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

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

1161 1162 1163 1164 1165 1166
    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)
1167 1168
      if selection is None:
        return []
1169
      return selection(method=selection_method, context=context, REQUEST=REQUEST)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1170

1171 1172 1173 1174 1175 1176
    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)
1177 1178
      if selection is None:
        return []
1179
      uid_list = selection.getCheckedUids()
1180 1181 1182 1183 1184 1185 1186 1187 1188 1189
      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:
1190 1191
        value_list = self.getSelectionSelectedValueList(
                                            selection_name,
1192 1193
                                            REQUEST=REQUEST,
                                            selection_method=selection_method,
1194
                                            context=context)
1195
      return value_list
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1196

Jean-Paul Smets's avatar
Jean-Paul Smets committed
1197 1198 1199
    security.declareProtected(ERP5Permissions.View, 'getSelectionUidList')
    def getSelectionUidList(self, selection_name, REQUEST=None, selection_method=None, context=None):
      """
1200
        Get the list of uids checked or selected for 'selection_name'
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1201
      """
1202
      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
1203

Sebastien Robin's avatar
Sebastien Robin committed
1204 1205 1206 1207 1208
    security.declareProtected(ERP5Permissions.View, 'selectionHasChanged')
    def selectionHasChanged(self, md5_string, object_uid_list):
      """
        We want to be sure that the selection did not change
      """
1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225
      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
1226
      # XXX To avoid the difference of the string representations of int and long,
1227
      # convert each element to a string.
1228 1229 1230 1231
      if six.PY3:
          return md5(str(sorted(uid_list)).encode()).hexdigest()
      else:
          return md5(str(sorted(map(str, uid_list)))).hexdigest()
Sebastien Robin's avatar
Sebastien Robin committed
1232

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


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

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

1291 1292 1293 1294 1295 1296
          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
1297 1298
          # Checked current uid
          kw ={}
1299 1300
          catalog_index = field.get_value('catalog_index')
          kw[catalog_index] = field_value
1301
          self.setSelectionParamsFor(selection_name,
Vincent Pelletier's avatar
Vincent Pelletier committed
1302 1303 1304
                                     kw.copy())
          self.setSelectionCheckedUidsFor(selection_name,
                                          current_uid_list)
1305 1306 1307 1308
          field_value = str(field_value)
          if len(field_value):
            sql_catalog = self.portal_catalog.getSQLCatalog()
            field_value = sql_catalog.buildQuery({
1309 1310
              catalog_index:{'query':field_value.splitlines(),
                             'key':'ExactMatch',},
1311 1312
            }).asSearchTextExpression(sql_catalog, column='')

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


1319 1320
        # Save the current REQUEST form
        # We can't put FileUpload instances because we can't pickle them
1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339
        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'))
1340 1341 1342
        # remove ignore_layout parameter from cancel_url otherwise we
        # will have two ignore_layout parameters after clicking cancel
        # button.
1343 1344 1345
        split_referer = list(urlsplit(REQUEST.get('HTTP_REFERER')))
        split_referer[3] = '&'.join([x for x in \
                                     split_referer[3].split('&') \
1346
                                     if not re.match('^ignore_layout[:=]', x)])
1347
        kw['cancel_url'] = urlunsplit(split_referer)
1348

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

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

        # 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)
1367
        # Return the search dialog
1368
        return getattr(o, dialog_id)(REQUEST=REQUEST)
1369

1370
    security.declarePublic('asDomainQuery')
1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383
    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)
1384
      for key, value in six.iteritems(domain_item_dict):
1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401
        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='>')

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

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

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

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

1451
            return getattr(self, name)
1452
      return SelectionTool.inheritedAttribute('_aq_dynamic')(self, name)
1453

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

1463
    security.declarePrivate('getTemporarySelectionDict')
1464 1465 1466 1467
    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."""
1468
      tv = getTransactionalVariable()
1469 1470 1471 1472 1473
      return tv.setdefault('_temporary_selection_dict', {})

    def pushSelection(self, selection_name):
      selection = self.getSelectionFor(selection_name)
      # a temporary selection is kept in transaction.
1474
      temp_selection = Selection(selection_name)
1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485
      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()

1486 1487 1488 1489 1490 1491 1492
    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):
1493
      if not self.isAnonymous():
1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504
        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

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

      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]

1515
      return self._getContainer().getSelection(user_id, selection_name)
1516 1517 1518 1519

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

      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

1528
      self._getContainer().setSelection(user_id, selection_name, selection)
1529

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

1534 1535 1536 1537
    def _deleteSelectionFromContainer(self, selection_name):
      user_id = self._getUserId()
      self._deleteSelectionForUserFromContainer(selection_name, user_id)

1538
    def _deleteGlobalSelectionFromContainer(self, selection_name):
1539
      self._getContainer().deleteGlobalSelection(self, selection_name)
1540

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

1546
    def isAnonymous(self):
1547
      return self._getUserId() == 'Anonymous User'
1548

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

    def _getContainerFromStorage(self, container_id, storage):
1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575
      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
1576

Jean-Paul Smets's avatar
Jean-Paul Smets committed
1577
InitializeClass( SelectionTool )
1578

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

1583
class MemcachedContainer(BaseContainer):
1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601
  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

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

1606
class PersistentMappingContainer(BaseContainer):
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 1633 1634
  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):
1635
    for user_container in six.itervalues(self._container):
1636 1637 1638 1639
      try:
        del(user_container[selection_name])
      except KeyError:
        pass
1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650

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):
1651
    # BUG: we should not modify newState
1652
    # update keys that only savedState has
1653 1654
    newState['data'].update(savedState['data'])
    return newState
1655 1656


1657 1658 1659 1660 1661 1662 1663 1664
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
1665

1666 1667
  def getObject(self):
    return self.object
1668

1669 1670
  def getIsPureSummary(self):
    return self.is_pure_summary
1671

1672 1673
  def getDepth(self):
    return self.depth
1674

1675 1676
  def getIsOpen(self):
    return self.is_open
1677 1678

  def getSelectDomainDict(self):
1679
    return self.select_domain_dict
1680

1681 1682
  def getExceptionUidList(self):
    return self.exception_uid_list
1683

1684

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

    select_domain_dict is a dictionary of  associative list of (id, domain)
  """
1694 1695
  if isinstance(report_path, str):
    report_path = report_path.split('/')
1696 1697 1698

  portal_categories = getattr(form, 'portal_categories', None)
  portal_domains = getattr(form, 'portal_domains', None)
1699
  portal_object = form.getPortalObject()
1700 1701 1702 1703 1704 1705 1706 1707
  if len(report_path):
    base_category = report_path[0]

  if root_dict is None:
    root_dict = {}

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

  tree_list = []
1742
  if root is None:
1743
    return tree_list
1744

1745
  if base_category == 'parent':
1746 1747 1748 1749 1750
    # 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)
1751
    else:
1752
      if filtered_portal_types not in [[],None,'']:
1753 1754
        object_list = list_method(portal_type=filtered_portal_types,
                                  sort_on=sort_on)
1755
      else:
1756
        object_list = list_method(sort_on=sort_on)
1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772
    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)
1773
          tree_list += [TreeListLine(o, 1, depth, 1, selection_domain, exception_uid_list)]
1774 1775
          if is_report_opened :
            # List (contents, closed, must be strict selection)
1776 1777 1778
            tree_list += [TreeListLine(o, 0, depth, 0, selection_domain, exception_uid_list)]

          tree_list += makeTreeList(here, form, new_root_dict, report_path,
1779 1780 1781
                    base_category, depth + 1, unfolded_list, form_id,
                    selection_name, report_depth,
                    is_report_opened=is_report_opened, sort_on=sort_on)
1782 1783
        else:
          tree_list += [TreeListLine(o, 1, depth, 0, selection_domain, ())] # Summary (closed)
1784
  else:
1785 1786 1787 1788 1789 1790
    # 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:
1791 1792 1793 1794 1795 1796 1797
      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)
1798 1799
        tree_list += makeTreeList(here, form, new_root_dict, report_path, base_category, depth + 1,
            unfolded_list, form_id, selection_name, report_depth,
1800 1801 1802 1803
            is_report_opened=is_report_opened, sort_on=sort_on)
      else:

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

1805 1806
  return tree_list

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

from Products.CMFCore.utils import getToolByName
1812
from Products.ERP5Type.Core.Folder import FolderMixIn
1813 1814
from ZPublisher.mapply import mapply

1815
method_id_filter_list = [x for x in FolderMixIn.__dict__ if callable(getattr(FolderMixIn, x))]
1816 1817 1818 1819 1820 1821 1822 1823 1824
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)
1825
  if roles is None or roles == ():
1826 1827 1828 1829
    continue
  if roles.__name__ == ERP5Permissions.ManagePortal:
    continue
  candidate_method_id_list.append(x)
1830

1831 1832 1833
# Monkey patch FolderMixIn with SelectionTool methods, and wrapper methods
# ('listbox_<WRAPPED_METHOD_NAME>()') used to set ListBox properties for
# pagination
1834 1835 1836 1837 1838 1839 1840 1841 1842 1843
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)
1844
  setattr(FolderMixIn, property_id, portal_selection_wrapper)
1845 1846 1847
  security_property_id = '%s__roles__' % (property_id, )
  security_property = getattr(SelectionTool, security_property_id, None)
  if security_property is not None:
1848
    setattr(FolderMixIn, security_property_id, security_property)
1849

1850 1851 1852 1853 1854 1855
  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
1856 1857
    listbox_id = request.form.get('listbox_%s' % wrapper_property_id, None)
    if not listbox_id:
1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872
      # 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
1873
    if selection_name_property_id in request:
1874
      request.form['list_selection_name'] = request[selection_name_property_id]
1875
    if listbox_uid_property_id in request:
1876
      request.form['listbox_uid'] = request[listbox_uid_property_id]
1877
    if list_start_property_id in request:
1878
      request.form['list_start'] = request[list_start_property_id]
1879
    if page_start_property_id in request:
1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892
      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)

1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903
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.
1904 1905 1906 1907

  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.
1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919
  """
  # 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.
      """
1920 1921 1922 1923
      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'" %
1924
        (wrapper_listbox_id, wrapper_property_id, wrapper_listbox_id),
1925 1926
        DeprecationWarning)

1927 1928 1929 1930 1931
      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
1932
      page_start_property_id = "%s_page_start" % listbox_id
1933
      # Rename request parameters
1934
      if selection_name_property_id in request:
1935
        request.form['list_selection_name'] = request[selection_name_property_id]
1936
      if listbox_uid_property_id in request:
1937
        request.form['listbox_uid'] = request[listbox_uid_property_id]
1938
      if list_start_property_id in request:
1939
        request.form['list_start'] = request[list_start_property_id]
1940
      if page_start_property_id in request:
Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
1941
        request.form['page_start'] = request[page_start_property_id]
1942 1943 1944 1945 1946 1947 1948 1949 1950 1951
      # 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, )
1952
      setattr(FolderMixIn, new_security_property_id, security_property)