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

zopefoundation / Zope / 6263629025

21 Sep 2023 03:12PM UTC coverage: 82.146% (-0.01%) from 82.159%
6263629025

Pull #1164

github

web-flow
[pre-commit.ci lite] apply automatic fixes
Pull Request #1164: Move all linters to pre-commit.

4353 of 6963 branches covered (0.0%)

Branch coverage included in aggregate %.

487 of 487 new or added lines in 186 files covered. (100.0%)

27394 of 31684 relevant lines covered (86.46%)

0.86 hits per line

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

42.07
/src/OFS/PropertySheets.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
"""Property sheets."""
1✔
14

15
import html
1✔
16

17
from AccessControl.class_init import InitializeClass
1✔
18
from AccessControl.Permissions import access_contents_information
1✔
19
from AccessControl.Permissions import manage_properties
1✔
20
from AccessControl.Permissions import view_management_screens
1✔
21
from AccessControl.SecurityInfo import ClassSecurityInfo
1✔
22
from Acquisition import Implicit
1✔
23
from Acquisition import aq_base
1✔
24
from Acquisition import aq_parent
1✔
25
from App.Management import Tabs
1✔
26
from App.special_dtml import DTMLFile
1✔
27
from ExtensionClass import Base
1✔
28
from OFS.Traversable import Traversable
1✔
29
from Persistence import Persistent
1✔
30
from webdav.PropertySheet import DAVPropertySheetMixin
1✔
31
from zExceptions import BadRequest
1✔
32
from ZPublisher.Converters import type_converters
1✔
33

34

35
class Virtual:
1✔
36
    """A virtual propertysheet stores it's properties in it's instance."""
37

38
    def __init__(self):
1✔
39
        pass
1✔
40

41
    def v_self(self):
1✔
42
        return aq_parent(aq_parent(self))
×
43

44

45
class View(Tabs, Base):
1✔
46
    """A view of an object, typically used for management purposes.
47

48
    This class provides bits of management framework needed by
49
    propertysheets to be used as a view on an object.
50
    """
51

52
    def manage_workspace(self, URL1, RESPONSE):
1✔
53
        """Implement a "management" interface."""
54
        RESPONSE.redirect(URL1 + '/manage')
×
55

56
    def tpURL(self):
1✔
57
        return self.getId()
×
58

59
    def manage_options(self):
1✔
60
        """Return a manage option data structure for me instance."""
61
        try:
×
62
            request = self.REQUEST
×
63
        except Exception:
×
64
            request = None
×
65
        if request is None:
×
66
            pre = '../../'
×
67
        else:
68
            pre = request['URL']
×
69
            for i in (1, 2, 3):
×
70
                loc = pre.rfind('/')
×
71
                if loc >= 0:
×
72
                    pre = pre[:loc]
×
73
            pre = pre + '/'
×
74

75
        request = []
×
76
        for d in aq_parent(aq_parent(self)).manage_options:
×
77
            path = d['action']
×
78
            option = {
×
79
                'label': d['label'],
80
                'action': pre + path,
81
                'path': '../../' + path,
82
            }
83
            help = d.get('help')
×
84
            if help is not None:
×
85
                option['help'] = help
×
86
            request.append(option)
×
87
        return request
×
88

89
    def tabs_path_info(self, script, path):
1✔
90
        loc = path.rfind('/')
×
91
        if loc >= 0:
×
92
            path = path[:loc]
×
93
            loc = path.rfind('/')
×
94
            if loc >= 0:
×
95
                path = path[:loc]
×
96
        return View.inheritedAttribute('tabs_path_info')(self, script, path)
×
97

98
    def meta_type(self):
1✔
99
        try:
×
100
            return aq_parent(aq_parent(self)).meta_type
×
101
        except Exception:
×
102
            return ''
×
103

104

105
class PropertySheet(Traversable, Persistent, Implicit, DAVPropertySheetMixin):
1✔
106
    """A PropertySheet is a container for a set of related properties and
107
    metadata describing those properties.
108

109
    PropertySheets may or may not provide a web interface for managing
110
    its properties.
111
    """
112

113
    _properties = ()
1✔
114
    _extensible = 1
1✔
115

116
    security = ClassSecurityInfo()
1✔
117
    security.declareObjectProtected(access_contents_information)
1✔
118
    security.setPermissionDefault(access_contents_information,
1✔
119
                                  ('Anonymous', 'Manager'))
120

121
    __reserved_ids = ('values', 'items')
1✔
122

123
    def property_extensible_schema__(self):
1✔
124
        # Return a flag indicating whether new properties may be
125
        # added or removed.
126
        return self._extensible
1✔
127

128
    def __init__(self, id, md=None):
1✔
129
        # Create a new property set, using the given id and namespace
130
        # string. The namespace string should be usable as an xml name-
131
        # space identifier.
132

133
        if id in self.__reserved_ids:
1!
134
            raise ValueError(
×
135
                "'{}' is a reserved Id (forbidden Ids are: {}) ".format(
136
                    id, self.__reserved_ids))
137

138
        self.id = id
1✔
139
        self._md = md or {}
1✔
140

141
    def getId(self):
1✔
142
        return self.id
1✔
143

144
    @security.protected(access_contents_information)
1✔
145
    def xml_namespace(self):
1✔
146
        """Return a namespace string usable as an xml namespace for this
147
        property set."""
148
        return self._md.get('xmlns', '')
1✔
149

150
    def v_self(self):
1✔
151
        return self
1✔
152

153
    def p_self(self):
1✔
154
        return self.v_self()
1✔
155

156
    def valid_property_id(self, id):
1✔
157
        if not id or id[:1] == '_' or (id[:3] == 'aq_') \
1!
158
           or (' ' in id) or html.escape(id, True) != id:
159
            return 0
×
160
        return 1
1✔
161

162
    @security.protected(access_contents_information)
1✔
163
    def hasProperty(self, id):
1✔
164
        """Return a true value if a property exists with the given id."""
165
        for prop in self._propertyMap():
1!
166
            if id == prop['id']:
1✔
167
                return 1
1✔
168
        return 0
×
169

170
    @security.protected(access_contents_information)
1✔
171
    def getProperty(self, id, default=None):
1✔
172
        # Return the property with the given id, returning the optional
173
        # second argument or None if no such property is found.
174
        if self.hasProperty(id):
1!
175
            return getattr(self.v_self(), id)
1✔
176
        return default
×
177

178
    @security.protected(access_contents_information)
1✔
179
    def getPropertyType(self, id):
1✔
180
        # Get the type of property 'id', returning None if no
181
        # such property exists.
182
        pself = self.p_self()
×
183
        for md in pself._properties:
×
184
            if md['id'] == id:
×
185
                return md.get('type', 'string')
×
186
        return None
×
187

188
    def _wrapperCheck(self, object):
1✔
189
        # Raise an error if an object is wrapped.
190
        if hasattr(object, 'aq_base'):
1!
191
            raise ValueError('Invalid property value: wrapped object')
×
192
        return
1✔
193

194
    def _setProperty(self, id, value, type='string', meta=None):
1✔
195
        # Set a new property with the given id, value and optional type.
196
        # Note that different property sets may support different typing
197
        # systems.
198
        self._wrapperCheck(value)
1✔
199
        if not self.valid_property_id(id):
1!
200
            raise BadRequest(
×
201
                'Invalid property id, %s.' %
202
                html.escape(
203
                    id, True))
204

205
        if not self.property_extensible_schema__():
1!
206
            raise BadRequest(
×
207
                'Properties cannot be added to this property sheet')
208
        pself = self.p_self()
1✔
209
        self = self.v_self()
1✔
210
        if hasattr(aq_base(self), id):
1!
211
            if not (id == 'title' and id not in self.__dict__):
×
212
                raise BadRequest(
×
213
                    'Invalid property id, <em>%s</em>. It is in use.' %
214
                    html.escape(id, True))
215
        if meta is None:
1!
216
            meta = {}
1✔
217
        prop = {'id': id, 'type': type, 'meta': meta}
1✔
218
        pself._properties = pself._properties + (prop,)
1✔
219
        if type in ('selection', 'multiple selection'):
1!
220
            if not value:
×
221
                raise BadRequest(
×
222
                    'The value given for a new selection property '
223
                    'must be a variable name<p>')
224
            prop['select_variable'] = value
×
225
            if type == 'selection':
×
226
                value = None
×
227
            else:
228
                value = []
×
229

230
        if isinstance(value, list):
1✔
231
            value = tuple(value)
1✔
232
        setattr(self, id, value)
1✔
233

234
    def _updateProperty(self, id, value, meta=None):
1✔
235
        # Update the value of an existing property. If value is a string,
236
        # an attempt will be made to convert the value to the type of the
237
        # existing property. If a mapping containing meta-data is passed,
238
        # it will used to _replace_ the properties meta data.
239
        self._wrapperCheck(value)
1✔
240
        if not self.hasProperty(id):
1!
241
            raise BadRequest('The property %s does not exist.' %
×
242
                             html.escape(id, True))
243
        propinfo = self.propertyInfo(id)
1✔
244
        if 'w' not in propinfo.get('mode', 'wd'):
1!
245
            raise BadRequest('%s cannot be changed.' % html.escape(id, True))
×
246
        if isinstance(value, (str, bytes)):
1✔
247
            proptype = propinfo.get('type', 'string')
1✔
248
            if proptype in type_converters:
1!
249
                value = type_converters[proptype](value)
1✔
250
        if meta is not None:
1!
251
            props = []
×
252
            pself = self.p_self()
×
253
            for prop in pself._properties:
×
254
                if prop['id'] == id:
×
255
                    prop['meta'] = meta
×
256
                props.append(prop)
×
257
            pself._properties = tuple(props)
×
258

259
        if isinstance(value, list):
1!
260
            value = tuple(value)
1✔
261
        setattr(self.v_self(), id, value)
1✔
262

263
    def _delProperty(self, id):
1✔
264
        # Delete the property with the given id. If a property with the
265
        # given id does not exist, a ValueError is raised.
266
        if not self.hasProperty(id):
×
267
            raise BadRequest('The property %s does not exist.' %
×
268
                             html.escape(id, True))
269
        vself = self.v_self()
×
270
        if hasattr(vself, '_reserved_names'):
×
271
            nd = vself._reserved_names
×
272
        else:
273
            nd = ()
×
274
        if ('d' not in self.propertyInfo(id).get('mode', 'wd')) or (id in nd):
×
275
            raise BadRequest('%s cannot be deleted.' % html.escape(id, True))
×
276
        delattr(vself, id)
×
277
        pself = self.p_self()
×
278
        pself._properties = tuple(
×
279
            [i for i in pself._properties if i['id'] != id])
280

281
    @security.protected(access_contents_information)
1✔
282
    def propertyIds(self):
1✔
283
        """Return a list of property ids."""
284
        return [i['id'] for i in self._propertyMap()]
×
285

286
    @security.protected(access_contents_information)
1✔
287
    def propertyValues(self):
1✔
288
        """Return a list of property values."""
289
        return [self.getProperty(i['id']) for i in self._propertyMap()]
×
290

291
    @security.protected(access_contents_information)
1✔
292
    def propertyItems(self):
1✔
293
        """Return a list of (id, property) tuples."""
294
        return [(i['id'], self.getProperty(i['id']))
×
295
                for i in self._propertyMap()]
296

297
    @security.protected(access_contents_information)
1✔
298
    def propertyInfo(self, id):
1✔
299
        """Return a mapping containing property meta-data."""
300
        for p in self._propertyMap():
1!
301
            if p['id'] == id:
1!
302
                return p
1✔
303
        raise ValueError(
×
304
            'The property %s does not exist.' %
305
            html.escape(
306
                id, True))
307

308
    def _propertyMap(self):
1✔
309
        """Return a tuple of mappings, giving meta-data for properties."""
310
        return self.p_self()._properties
1✔
311

312
    @security.protected(access_contents_information)
1✔
313
    def propertyMap(self):
1✔
314
        """Returns a secure copy of the property definitions."""
315
        return tuple(dict.copy() for dict in self._propertyMap())
×
316

317
    def _propdict(self):
1✔
318
        dict = {}
1✔
319
        for p in self._propertyMap():
1✔
320
            dict[p['id']] = p
1✔
321
        return dict
1✔
322

323
    manage = DTMLFile('dtml/properties', globals())
1✔
324

325
    @security.protected(manage_properties)
1✔
326
    def manage_propertiesForm(self, URL1, RESPONSE):
1✔
327
        " "
328
        RESPONSE.redirect(URL1 + '/manage')
×
329

330
    @security.protected(manage_properties)
1✔
331
    def manage_addProperty(self, id, value, type, REQUEST=None):
1✔
332
        """Add a new property via the web.
333

334
        Sets a new property with the given id, type, and value.
335
        """
336
        if type in type_converters:
1!
337
            value = type_converters[type](value)
1✔
338
        self._setProperty(id, value, type)
1✔
339
        if REQUEST is not None:
1!
340
            return self.manage(self, REQUEST)
×
341

342
    @security.protected(manage_properties)
1✔
343
    def manage_editProperties(self, REQUEST):
1✔
344
        """Edit object properties via the web."""
345
        for prop in self._propertyMap():
×
346
            name = prop['id']
×
347
            if 'w' in prop.get('mode', 'wd'):
×
348
                value = REQUEST.get(name, '')
×
349
                self._updateProperty(name, value)
×
350
        message = 'Your changes have been saved.'
×
351
        return self.manage(self, REQUEST, manage_tabs_message=message)
×
352

353
    @security.protected(manage_properties)
1✔
354
    def manage_changeProperties(self, REQUEST=None, **kw):
1✔
355
        """Change existing object properties.
356

357
        Change object properties by passing either a REQUEST object or
358
        name=value parameters
359
        """
360
        if REQUEST is None:
×
361
            props = {}
×
362
        else:
363
            props = REQUEST
×
364
        if kw:
×
365
            for name, value in kw.items():
×
366
                props[name] = value
×
367
        propdict = self._propdict()
×
368
        for name, value in props.items():
×
369
            if self.hasProperty(name):
×
370
                if 'w' not in propdict[name].get('mode', 'wd'):
×
371
                    raise BadRequest('%s cannot be changed' %
×
372
                                     html.escape(name, True))
373
                self._updateProperty(name, value)
×
374
        message = 'Your changes have been saved.'
×
375
        return self.manage(self, REQUEST, manage_tabs_message=message)
×
376

377
    @security.protected(manage_properties)
1✔
378
    def manage_delProperties(self, ids=None, REQUEST=None):
1✔
379
        """Delete one or more properties specified by 'ids'."""
380
        if REQUEST:
×
381
            # Bugfix for properties named "ids" (casey)
382
            if ids == self.getProperty('ids', None):
×
383
                ids = None
×
384
            ids = REQUEST.get('_ids', ids)
×
385
        if ids is None:
×
386
            raise BadRequest('No property specified')
×
387
        for id in ids:
×
388
            self._delProperty(id)
×
389
        if REQUEST is not None:
×
390
            return self.manage(self, REQUEST)
×
391

392

393
InitializeClass(PropertySheet)
1✔
394

395

396
class DefaultProperties(Virtual, PropertySheet, View):
1✔
397
    """The default property set mimics the behavior of old-style Zope
398
       properties -- it stores its property values in the instance of
399
       its owner."""
400

401
    id = 'default'
1✔
402
    _md = {'xmlns': 'http://www.zope.org/propsets/default'}
1✔
403

404

405
InitializeClass(DefaultProperties)
1✔
406

407

408
# import cycles
409
from webdav.PropertySheets import DAVProperties  # NOQA: E402 isort:skip
1✔
410

411

412
class PropertySheets(Traversable, Implicit, Tabs):
1✔
413
    """A tricky container to keep property sets from polluting an object's
414
    direct attribute namespace."""
415

416
    id = 'propertysheets'
1✔
417

418
    security = ClassSecurityInfo()
1✔
419
    security.declareObjectProtected(access_contents_information)
1✔
420
    security.setPermissionDefault(access_contents_information,
1✔
421
                                  ('Anonymous', 'Manager'))
422

423
    # optionally to be overridden by derived classes
424
    PropertySheetClass = PropertySheet
1✔
425

426
    webdav = DAVProperties()
1✔
427

428
    def _get_defaults(self):
1✔
429
        return (self.webdav,)
×
430

431
    def __propsets__(self):
1✔
432
        propsets = aq_parent(self).__propsets__
1✔
433
        __traceback_info__ = propsets, type(propsets)
1✔
434
        return self._get_defaults() + propsets
1✔
435

436
    def __bobo_traverse__(self, REQUEST, name=None):
1✔
437
        for propset in self.__propsets__():
×
438
            if propset.getId() == name:
×
439
                return propset.__of__(self)
×
440
        return getattr(self, name)
×
441

442
    def __getitem__(self, n):
1✔
443
        return self.__propsets__()[n].__of__(self)
×
444

445
    @security.protected(access_contents_information)
1✔
446
    def values(self):
1✔
447
        propsets = self.__propsets__()
×
448
        return [n.__of__(self) for n in propsets]
×
449

450
    @security.protected(access_contents_information)
1✔
451
    def items(self):
1✔
452
        propsets = self.__propsets__()
×
453
        r = []
×
454
        for n in propsets:
×
455
            if hasattr(n, 'id'):
×
456
                id = n.id
×
457
            else:
458
                id = ''
×
459
            r.append((id, n.__of__(self)))
×
460

461
        return r
×
462

463
    @security.protected(access_contents_information)
1✔
464
    def get(self, name, default=None):
1✔
465
        for propset in self.__propsets__():
1✔
466
            if propset.id == name or \
1✔
467
               getattr(propset, 'xml_namespace', object)() == name:
468
                return propset.__of__(self)
1✔
469
        return default
1✔
470

471
    @security.protected(manage_properties)
1✔
472
    def manage_addPropertySheet(self, id, ns, REQUEST=None):
1✔
473
        """"""
474
        md = {'xmlns': ns}
×
475
        ps = self.PropertySheetClass(id, md)
×
476
        self.addPropertySheet(ps)
×
477
        if REQUEST is None:
×
478
            return ps
×
479
        ps = self.get(id)
×
480
        REQUEST.RESPONSE.redirect('%s/manage' % ps.absolute_url())
×
481

482
    @security.protected(manage_properties)
1✔
483
    def addPropertySheet(self, propset):
1✔
484
        propsets = aq_parent(self).__propsets__
×
485
        propsets = propsets + (propset,)
×
486
        aq_parent(self).__propsets__ = propsets
×
487

488
    @security.protected(manage_properties)
1✔
489
    def delPropertySheet(self, name):
1✔
490
        result = []
×
491
        for propset in aq_parent(self).__propsets__:
×
492
            if propset.getId() != name and propset.xml_namespace() != name:
×
493
                result.append(propset)
×
494
        aq_parent(self).__propsets__ = tuple(result)
×
495

496
    def isDeletable(self, name):
1✔
497
        """Currently, we say that *name* is deletable when it is not a default
498
        sheet.
499

500
        Later, we may further restrict deletability based on an instance
501
        attribute.
502
        """
503
        ps = self.get(name)
×
504
        if ps is None:
×
505
            return 0
×
506
        if ps in self._get_defaults():
×
507
            return 0
×
508
        return 1
×
509

510
    def manage_delPropertySheets(self, ids=(), REQUEST=None):
1✔
511
        """Delete all sheets identified by *ids*."""
512
        for id in ids:
×
513
            if not self.isDeletable(id):
×
514
                raise BadRequest(
×
515
                    'attempt to delete undeletable property sheet: ' + id)
516
            self.delPropertySheet(id)
×
517
        if REQUEST is not None:
×
518
            REQUEST.RESPONSE.redirect('%s/manage' % self.absolute_url())
×
519

520
    def __len__(self):
1✔
521
        return len(self.__propsets__())
×
522

523
    def getId(self):
1✔
524
        return self.id
×
525

526
    security.declareProtected(view_management_screens, 'manage')  # NOQA: D001
1✔
527
    manage = DTMLFile('dtml/propertysheets', globals())
1✔
528

529
    def manage_options(self):
1✔
530
        """Return a manage option data structure for me instance."""
531
        try:
×
532
            request = self.REQUEST
×
533
        except Exception:
×
534
            request = None
×
535
        if request is None:
×
536
            pre = '../'
×
537
        else:
538
            pre = request['URLPATH0']
×
539
            for i in (1, 2):
×
540
                loc = pre.rfind('/')
×
541
                if loc >= 0:
×
542
                    pre = pre[:loc]
×
543
            pre = pre + '/'
×
544

545
        request = []
×
546
        for d in aq_parent(self).manage_options:
×
547
            request.append({'label': d['label'], 'action': pre + d['action']})
×
548
        return request
×
549

550
    def tabs_path_info(self, script, path):
1✔
551
        loc = path.rfind('/')
×
552
        if loc >= 0:
×
553
            path = path[:loc]
×
554
        return PropertySheets.inheritedAttribute('tabs_path_info')(
×
555
            self, script, path)
556

557

558
InitializeClass(PropertySheets)
1✔
559

560

561
class DefaultPropertySheets(PropertySheets):
1✔
562
    """A PropertySheets container that contains a default property sheet for
563
    compatibility with the arbitrary property mgmt design of Zope
564
    PropertyManagers."""
565

566
    default = DefaultProperties()
1✔
567
    webdav = DAVProperties()
1✔
568

569
    def _get_defaults(self):
1✔
570
        return (self.default, self.webdav)
1✔
571

572

573
InitializeClass(DefaultPropertySheets)
1✔
574

575

576
class vps(Base):
1✔
577
    """Virtual Propertysheets.
578

579
    The vps object implements a "computed attribute" - it ensures
580
    that a PropertySheets instance is returned when the propertysheets
581
    attribute of a PropertyManager is accessed.
582
    """
583

584
    def __init__(self, c=PropertySheets):
1✔
585
        self.c = c
1✔
586

587
    def __of__(self, parent):
1✔
588
        return self.c().__of__(parent)
1✔
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