• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zopefoundation / Products.CMFCore / 6246931310

20 Sep 2023 09:54AM UTC coverage: 86.008% (-0.3%) from 86.266%
6246931310

Pull #131

github

mauritsvanrees
gha: don't need setup-python on 27 as we use the 27 container.
Pull Request #131: Make decodeFolderFilter and encodeFolderFilter non-public.

2466 of 3689 branches covered (0.0%)

Branch coverage included in aggregate %.

6 of 6 new or added lines in 1 file covered. (100.0%)

17297 of 19289 relevant lines covered (89.67%)

0.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.89
/src/Products/CMFCore/ActionInformation.py
1
##############################################################################
2
#
3
# Copyright (c) 2002 Zope Foundation and Contributors.
4
#
5
# This software is subject to the provisions of the Zope Public License,
6
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
7
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
8
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
9
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
10
# FOR A PARTICULAR PURPOSE.
11
#
12
##############################################################################
13
""" Information about customizable actions.
1✔
14
"""
15

16
import six
1✔
17
from six.moves import UserDict
1✔
18

19
from AccessControl.class_init import InitializeClass
1✔
20
from AccessControl.SecurityInfo import ClassSecurityInfo
1✔
21
from Acquisition import aq_base
1✔
22
from Acquisition import aq_inner
1✔
23
from Acquisition import aq_parent
1✔
24
from OFS.ObjectManager import IFAwareObjectManager
1✔
25
from OFS.OrderedFolder import OrderedFolder
1✔
26
from OFS.PropertyManager import PropertyManager
1✔
27
from OFS.SimpleItem import SimpleItem
1✔
28
from zope.component import getUtility
1✔
29
from zope.globalrequest import getRequest
1✔
30
from zope.i18nmessageid import Message
1✔
31
from zope.interface import implementer
1✔
32

33
from .Expression import Expression
1✔
34
from .interfaces import IAction
1✔
35
from .interfaces import IActionCategory
1✔
36
from .interfaces import IActionInfo
1✔
37
from .interfaces import IMembershipTool
1✔
38
from .interfaces import IURLTool
1✔
39
from .permissions import View
1✔
40
from .utils import _checkPermission
1✔
41

42

43
_unchanged = []  # marker
1✔
44

45

46
@implementer(IActionCategory)
1✔
47
class ActionCategory(IFAwareObjectManager, OrderedFolder):
1✔
48

49
    """ Group of Action objects.
50
    """
51

52
    _product_interfaces = (IActionCategory, IAction)
1✔
53

54
    security = ClassSecurityInfo()
1✔
55

56
    @security.private
1✔
57
    def listActions(self):
1✔
58
        """ List the actions defined in this category and its subcategories.
59
        """
60
        actions = []
1✔
61

62
        for obj in self.objectValues():
1✔
63
            if IActionCategory.providedBy(obj):
1!
64
                actions.extend(obj.listActions())
×
65
            elif IAction.providedBy(obj):
1!
66
                actions.append(obj)
1✔
67

68
        return tuple(actions)
1✔
69

70

71
InitializeClass(ActionCategory)
1✔
72

73

74
@implementer(IAction)
1✔
75
class Action(PropertyManager, SimpleItem):
1✔
76

77
    """ Reference to an action.
78
    """
79

80
    i18n_domain = 'cmf_default'
1✔
81
    link_target = ''
1✔
82

83
    security = ClassSecurityInfo()
1✔
84

85
    _properties = (
1✔
86
        {'id': 'title', 'type': 'string', 'mode': 'w',
87
         'label': 'Title'},
88
        {'id': 'description', 'type': 'text', 'mode': 'w',
89
         'label': 'Description'},
90
        {'id': 'i18n_domain', 'type': 'string', 'mode': 'w',
91
         'label': 'I18n Domain'},
92
        {'id': 'url_expr', 'type': 'string', 'mode': 'w',
93
         'label': 'URL (Expression)'},
94
        {'id': 'link_target', 'type': 'string', 'mode': 'w',
95
         'label': 'Link Target'},
96
        {'id': 'icon_expr', 'type': 'string', 'mode': 'w',
97
         'label': 'Icon (Expression)'},
98
        {'id': 'available_expr', 'type': 'string', 'mode': 'w',
99
         'label': 'Condition (Expression)'},
100
        {'id': 'permissions', 'type': 'multiple selection', 'mode': 'w',
101
         'label': 'Permissions', 'select_variable': 'possible_permissions'},
102
        {'id': 'visible', 'type': 'boolean', 'mode': 'w',
103
         'label': 'Visible?'},
104
        )
105

106
    manage_options = (
1✔
107
        PropertyManager.manage_options +
108
        SimpleItem.manage_options)
109

110
    def __init__(self, id, **kw):
1✔
111
        self.id = id
1✔
112
        self._setPropValue('title', kw.get('title', ''))
1✔
113
        self._setPropValue('description', kw.get('description', ''))
1✔
114
        self._setPropValue('i18n_domain', kw.get('i18n_domain', ''))
1✔
115
        self._setPropValue('url_expr', kw.get('url_expr', ''))
1✔
116
        self._setPropValue('link_target', kw.get('link_target', ''))
1✔
117
        self._setPropValue('icon_expr', kw.get('icon_expr', ''))
1✔
118
        self._setPropValue('available_expr', kw.get('available_expr', ''))
1✔
119
        self._setPropValue('permissions', kw.get('permissions', ()))
1✔
120
        self._setPropValue('visible', kw.get('visible', True))
1✔
121

122
    def _setPropValue(self, id, value):
1✔
123
        self._wrapperCheck(value)
1✔
124
        if isinstance(value, list):
1!
125
            value = tuple(value)
×
126
        setattr(self, id, value)
1✔
127
        if id.endswith('_expr'):
1✔
128
            attr = '%s_object' % id
1✔
129
            if value:
1✔
130
                setattr(self, attr, Expression(value))
1✔
131
            elif hasattr(self, attr):
1✔
132
                delattr(self, attr)
1✔
133

134
    @security.private
1✔
135
    def getInfoData(self):
1✔
136
        """ Get the data needed to create an ActionInfo.
137
        """
138
        category_path = []
1✔
139
        lazy_keys = []
1✔
140
        lazy_map = {}
1✔
141

142
        lazy_map['id'] = self.getId()
1✔
143

144
        parent = aq_parent(self)
1✔
145
        while parent is not None and parent.getId() != 'portal_actions':
1!
146
            category_path.append(parent.getId())
×
147
            parent = aq_parent(parent)
×
148
        lazy_map['category'] = '/'.join(category_path[::-1])
1✔
149

150
        for id, val in self.propertyItems():
1✔
151
            if id.endswith('_expr'):
1✔
152
                id = id[:-5]
1✔
153
                if val:
1✔
154
                    val = getattr(self, '%s_expr_object' % id)
1✔
155
                    lazy_keys.append(id)
1✔
156
                elif id == 'available':
1✔
157
                    val = True
1✔
158
            elif id == 'i18n_domain':
1✔
159
                continue
1✔
160
            elif id == 'link_target':
1✔
161
                val = val or None
1✔
162
            elif self.i18n_domain and id in ('title', 'description'):
1!
163
                val = Message(val, self.i18n_domain)
×
164
            lazy_map[id] = val
1✔
165

166
        return (lazy_map, lazy_keys)
1✔
167

168

169
InitializeClass(Action)
1✔
170

171

172
@implementer(IActionInfo)
1✔
173
class ActionInfo(UserDict):
1✔
174

175
    """ A lazy dictionary for Action infos.
176
    """
177

178
    __allow_access_to_unprotected_subobjects__ = 1
1✔
179

180
    def __init__(self, action, ec):
1✔
181
        if isinstance(action, dict):
1✔
182
            lazy_keys = []
1✔
183
            UserDict.__init__(self, action)
1✔
184
            if 'name' in self.data:
1!
185
                self.data.setdefault('id', self.data['name'].lower())
1✔
186
                self.data.setdefault('title', self.data['name'])
1✔
187
                del self.data['name']
1✔
188
            self.data.setdefault('url', '')
1✔
189
            self.data.setdefault('link_target', None)
1✔
190
            self.data.setdefault('icon', '')
1✔
191
            self.data.setdefault('category', 'object')
1✔
192
            self.data.setdefault('visible', True)
1✔
193
            self.data['available'] = True
1✔
194
        else:
195
            # if action isn't a dict, it has to implement IAction
196
            (lazy_map, lazy_keys) = action.getInfoData()
1✔
197
            UserDict.__init__(self, lazy_map)
1✔
198

199
        self.data.setdefault('allowed', True)
1✔
200
        permissions = self.data.pop('permissions', ())
1✔
201
        if permissions:
1✔
202
            self.data['allowed'] = self._checkPermissions
1✔
203
            lazy_keys.append('allowed')
1✔
204

205
        self._ec = ec
1✔
206
        self._lazy_keys = lazy_keys
1✔
207
        self._permissions = permissions
1✔
208

209
    def __getitem__(self, key):
1✔
210
        value = UserDict.__getitem__(self, key)
1✔
211
        if key in self._lazy_keys:
1✔
212
            value = self.data[key] = value(self._ec)
1✔
213
            self._lazy_keys.remove(key)
1✔
214
        return value
1✔
215

216
    def __eq__(self, other):
1✔
217
        # this is expensive, use it with care
218
        [self.__getitem__(key) for key in self._lazy_keys[:]]
1✔
219

220
        if isinstance(other, self.__class__):
1!
221
            [other[key] for key in other._lazy_keys]
×
222
            return self.data == other.data
×
223
        elif isinstance(other, UserDict):
1!
224
            return self.data == other.data
×
225
        else:
226
            return self.data == other
1✔
227

228
    def copy(self):
1✔
229
        c = UserDict.copy(self)
1✔
230
        c._lazy_keys = self._lazy_keys[:]
1✔
231
        return c
1✔
232

233
    def update(*args, **kwargs):
1✔
234
        if not args:
1!
235
            raise TypeError("descriptor 'update' of 'UserDict' object "
×
236
                            'needs an argument')
237
        self = args[0]
1✔
238
        args = args[1:]
1✔
239
        if len(args) > 1:
1!
240
            raise TypeError('expected at most 1 arguments, got %d' % len(args))
×
241
        if args:
1!
242
            dict = args[0]
1✔
243
        elif 'dict' in kwargs:
×
244
            dict = kwargs.pop('dict')
×
245
            import warnings
×
246
            warnings.warn("Passing 'dict' as keyword argument is deprecated",
×
247
                          PendingDeprecationWarning, stacklevel=2)
248
        else:
249
            dict = None
×
250
        if dict is None:
1!
251
            pass
×
252
        elif isinstance(dict, UserDict):
1✔
253
            self.data.update(dict.data)
1✔
254
        elif isinstance(dict, type({})) or not hasattr(dict, 'items'):
1!
255
            self.data.update(dict)
1✔
256
        else:
257
            for k, v in dict.items():
×
258
                self[k] = v
×
259
        if len(kwargs):
1!
260
            self.data.update(kwargs)
×
261

262
    def _checkPermissions(self, ec):
1✔
263
        """ Check permissions in the current context.
264
        """
265
        category = self['category']
1✔
266
        object = ec.contexts['object']
1✔
267
        if object is not None and (category.startswith('object') or
1✔
268
                                   category.startswith('workflow') or
269
                                   category.startswith('document')):
270
            context = object
1✔
271
        else:
272
            folder = ec.contexts['folder']
1✔
273
            if folder is not None and category.startswith('folder'):
1✔
274
                context = folder
1✔
275
            else:
276
                context = ec.contexts['portal']
1✔
277

278
        for permission in self._permissions:
1✔
279
            if _checkPermission(permission, context):
1✔
280
                return True
1✔
281
        return False
1✔
282

283

284
@implementer(IAction)
1✔
285
class ActionInformation(SimpleItem):
1✔
286

287
    """ Represent a single selectable action.
288

289
    Actions generate links to views of content, or to specific methods
290
    of the site.  They can be filtered via their conditions.
291
    """
292

293
    link_target = ''
1✔
294

295
    __allow_access_to_unprotected_subobjects__ = 1
1✔
296

297
    security = ClassSecurityInfo()
1✔
298

299
    def __init__(self,
1✔
300
                 id,
301
                 title='',
302
                 description='',
303
                 category='object',
304
                 condition='',
305
                 permissions=(),
306
                 priority=10,
307
                 visible=True,
308
                 action='',
309
                 icon_expr='',
310
                 link_target=''):
311
        """ Set up an instance.
312
        """
313
        self.edit(id,
1✔
314
                  title,
315
                  description,
316
                  category,
317
                  condition,
318
                  permissions,
319
                  priority,
320
                  visible,
321
                  action,
322
                  icon_expr,
323
                  link_target)
324

325
    @security.private
1✔
326
    def edit(self,
1✔
327
             id=_unchanged,
328
             title=_unchanged,
329
             description=_unchanged,
330
             category=_unchanged,
331
             condition=_unchanged,
332
             permissions=_unchanged,
333
             priority=_unchanged,
334
             visible=_unchanged,
335
             action=_unchanged,
336
             icon_expr=_unchanged,
337
             link_target=_unchanged):
338
        """Edit the specified properties.
339
        """
340

341
        if id is not _unchanged:
1✔
342
            self.id = id
1✔
343
        if title is not _unchanged:
1!
344
            self.title = title
1✔
345
        if description is not _unchanged:
1✔
346
            self.description = description
1✔
347
        if category is not _unchanged:
1✔
348
            self.category = category
1✔
349
        if condition is not _unchanged:
1✔
350
            if condition and isinstance(condition, six.string_types):
1✔
351
                condition = Expression(condition)
1✔
352
            self.condition = condition
1✔
353
        if permissions is not _unchanged:
1✔
354
            if permissions == ('',):
1✔
355
                permissions = ()
1✔
356
            self.permissions = permissions
1✔
357
        if priority is not _unchanged:
1✔
358
            self.priority = priority
1✔
359
        if visible is not _unchanged:
1✔
360
            self.visible = visible
1✔
361
        if action is not _unchanged:
1✔
362
            if action and isinstance(action, six.string_types):
1✔
363
                action = Expression(action)
1✔
364
            self.setActionExpression(action)
1✔
365
        if icon_expr is not _unchanged:
1✔
366
            if icon_expr and isinstance(icon_expr, six.string_types):
1✔
367
                icon_expr = Expression(icon_expr)
1✔
368
            self.setIconExpression(icon_expr)
1✔
369
        if link_target is not _unchanged:
1✔
370
            self.link_target = link_target
1✔
371

372
    @security.protected(View)
1✔
373
    def Title(self):
1✔
374
        """ Return the Action title.
375
        """
376
        return self.title or self.getId()
1✔
377

378
    @security.protected(View)
1✔
379
    def Description(self):
1✔
380
        """ Return a description of the action.
381
        """
382
        return self.description
1✔
383

384
    @security.private
1✔
385
    def testCondition(self, ec):
1✔
386
        """ Evaluate condition using context, 'ec', and return 0 or 1.
387
        """
388
        if self.condition:
1!
389
            return bool(self.condition(ec))
1✔
390
        else:
391
            return True
×
392

393
    @security.public
1✔
394
    def getAction(self, ec):
1✔
395
        """ Compute the action using context, 'ec'; return a mapping of
396
            info about the action.
397
        """
398
        return ActionInfo(self, ec)
×
399

400
    @security.private
1✔
401
    def _getActionObject(self):
1✔
402
        """ Find the action object, working around name changes.
403
        """
404
        action = getattr(self, 'action', None)
1✔
405

406
        if action is None:  # Forward compatibility, used to be '_action'
1!
407
            action = getattr(self, '_action', None)
×
408
            if action is not None:
×
409
                self.action = self._action
×
410
                del self._action
×
411

412
        return action
1✔
413

414
    @security.public
1✔
415
    def getActionExpression(self):
1✔
416
        """ Return the text of the TALES expression for our URL.
417
        """
418
        action = self._getActionObject()
1✔
419
        expr = action and action.text or ''
1✔
420
        if expr and isinstance(expr, six.string_types):
1✔
421
            if not expr.startswith('string:') and \
1✔
422
               not expr.startswith('python:'):
423
                expr = 'string:${object_url}/%s' % expr
1✔
424
                self.action = Expression(expr)
1✔
425
        return expr
1✔
426

427
    @security.private
1✔
428
    def setActionExpression(self, action):
1✔
429
        if action and isinstance(action, six.string_types):
1✔
430
            if not action.startswith('string:') and \
1!
431
               not action.startswith('python:'):
432
                action = 'string:${object_url}/%s' % action
×
433
            action = Expression(action)
1✔
434
        self.action = action
1✔
435

436
    @security.private
1✔
437
    def _getIconExpressionObject(self):
1✔
438
        """ Find the icon expression object, working around name changes.
439
        """
440
        return getattr(self, 'icon_expr', None)
1✔
441

442
    @security.public
1✔
443
    def getIconExpression(self):
1✔
444
        """ Return the text of the TALES expression for our icon URL.
445
        """
446
        icon_expr = self._getIconExpressionObject()
1✔
447
        expr = icon_expr and icon_expr.text or ''
1✔
448
        if expr and isinstance(expr, six.string_types):
1✔
449
            if not expr.startswith('string:') and \
1!
450
               not expr.startswith('python:'):
451
                expr = 'string:${object_url}/%s' % expr
×
452
                self.icon_expr = Expression(expr)
×
453
        return expr
1✔
454

455
    @security.private
1✔
456
    def setIconExpression(self, icon_expr):
1✔
457
        if icon_expr and isinstance(icon_expr, six.string_types):
1!
458
            if not icon_expr.startswith('string:') and \
×
459
               not icon_expr.startswith('python:'):
460
                icon_expr = 'string:${object_url}/%s' % icon_expr
×
461
            icon_expr = Expression(icon_expr)
×
462
        self.icon_expr = icon_expr
1✔
463

464
    @security.public
1✔
465
    def getCondition(self):
1✔
466
        """ Return the text of the TALES expression for our condition.
467
        """
468
        return getattr(self, 'condition', None) and self.condition.text or ''
1✔
469

470
    @security.public
1✔
471
    def getPermissions(self):
1✔
472
        """ Return the permission, if any, required to execute the action.
473

474
        Return an empty tuple if no permission is required.
475
        """
476
        return self.permissions
1✔
477

478
    @security.public
1✔
479
    def getCategory(self):
1✔
480
        """ Return the category in which the action should be grouped.
481
        """
482
        return self.category or 'object'
1✔
483

484
    @security.public
1✔
485
    def getVisibility(self):
1✔
486
        """ Return whether the action should be visible in the CMF UI.
487
        """
488
        return bool(self.visible)
1✔
489

490
    @security.public
1✔
491
    def getLinkTarget(self):
1✔
492
        """ Return the rendered link tag's target attribute value
493
        """
494
        return self.link_target
1✔
495

496
    @security.private
1✔
497
    def getMapping(self):
1✔
498
        """ Get a mapping of this object's data.
499
        """
500
        cond = getattr(self, 'condition', None) and self.condition.text or ''
1✔
501
        return {'id': self.id,
1✔
502
                'title': self.title or self.id,
503
                'description': self.description,
504
                'category': self.category or 'object',
505
                'condition': cond,
506
                'permissions': self.permissions,
507
                'visible': bool(self.visible),
508
                'action': self.getActionExpression(),
509
                'icon_expr': self.getIconExpression(),
510
                'link_target': self.getLinkTarget()}
511

512
    @security.private
1✔
513
    def clone(self):
1✔
514
        """ Get a newly-created AI just like us.
515
        """
516
        return self.__class__(priority=self.priority, **self.getMapping())
1✔
517

518
    @security.private
1✔
519
    def getInfoData(self):
1✔
520
        """ Get the data needed to create an ActionInfo.
521
        """
522
        lazy_keys = []
1✔
523
        lazy_map = self.getMapping()
1✔
524

525
        if not lazy_map['link_target']:
1✔
526
            lazy_map['link_target'] = None
1✔
527

528
        if lazy_map['action']:
1✔
529
            lazy_map['url'] = self._getActionObject()
1✔
530
            lazy_keys.append('url')
1✔
531
        else:
532
            lazy_map['url'] = ''
1✔
533
        del lazy_map['action']
1✔
534

535
        if lazy_map['icon_expr']:
1✔
536
            lazy_map['icon'] = self._getIconExpressionObject()
1✔
537
            lazy_keys.append('icon')
1✔
538
        else:
539
            lazy_map['icon'] = ''
1✔
540
        del lazy_map['icon_expr']
1✔
541

542
        if lazy_map['condition']:
1✔
543
            lazy_map['available'] = self.testCondition
1✔
544
            lazy_keys.append('available')
1✔
545
        else:
546
            lazy_map['available'] = True
1✔
547
        del lazy_map['condition']
1✔
548

549
        return (lazy_map, lazy_keys)
1✔
550

551

552
InitializeClass(ActionInformation)
1✔
553

554

555
def getOAI(context, object=None):
1✔
556
    request = getRequest()
×
557
    if request:
×
558
        cache = request.get('_oai_cache', None)
×
559
        if cache is None:
×
560
            request['_oai_cache'] = cache = {}
×
561
        info = cache.get(id(object), None)
×
562
    else:
563
        info = None
×
564
    if info is None:
×
565
        if object is None or not hasattr(object, 'aq_base'):
×
566
            folder = None
×
567
        else:
568
            folder = object
×
569
            # Search up the containment hierarchy until we find an
570
            # object that claims it's a folder.
571
            while folder is not None:
×
572
                if getattr(aq_base(folder), 'isPrincipiaFolderish', 0):
×
573
                    # found it.
574
                    break
×
575
                else:
576
                    folder = aq_parent(aq_inner(folder))
×
577
        info = oai(context, folder, object)
×
578
        if request:
×
579
            cache[id(object)] = info
×
580
    return info
×
581

582

583
class oai(object):
1✔
584

585
    # Provided for backward compatibility
586
    # Provides information that may be needed when constructing the list of
587
    # available actions.
588
    __allow_access_to_unprotected_subobjects__ = 1
1✔
589

590
    def __init__(self, tool, folder, object=None):
1✔
591
        mtool = getUtility(IMembershipTool)
×
592
        self.isAnonymous = mtool.isAnonymousUser()
×
593
        self.user_id = mtool.getAuthenticatedMember().getId()
×
594

595
        utool = getUtility(IURLTool)
×
596
        self.portal = utool.getPortalObject()
×
597
        self.portal_url = utool()
×
598
        if folder is not None:
×
599
            self.folder_url = folder.absolute_url()
×
600
            self.folder = folder
×
601
        else:
602
            self.folder_url = self.portal_url
×
603
            self.folder = self.portal
×
604

605
        # The name "content" is deprecated and will go away in CMF 2.0!
606
        self.object = self.content = object
×
607
        if object is not None:
×
608
            self.content_url = self.object_url = object.absolute_url()
×
609
        else:
610
            self.content_url = self.object_url = None
×
611

612
    def __getitem__(self, name):
1✔
613
        # Mapping interface for easy string formatting.
614
        if name[:1] == '_':
×
615
            raise KeyError(name)
×
616
        if hasattr(self, name):
×
617
            return getattr(self, name)
×
618
        raise KeyError(name)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc