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

zopefoundation / Zope / 6272285826

22 Sep 2023 09:03AM UTC coverage: 82.145% (-0.01%) from 82.159%
6272285826

Pull #1164

github

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

4350 of 6960 branches covered (0.0%)

Branch coverage included in aggregate %.

495 of 495 new or added lines in 187 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
        md = {'xmlns': ns}
×
474
        ps = self.PropertySheetClass(id, md)
×
475
        self.addPropertySheet(ps)
×
476
        if REQUEST is None:
×
477
            return ps
×
478
        ps = self.get(id)
×
479
        REQUEST.RESPONSE.redirect('%s/manage' % ps.absolute_url())
×
480

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

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

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

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

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

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

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

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

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

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

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

556

557
InitializeClass(PropertySheets)
1✔
558

559

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

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

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

571

572
InitializeClass(DefaultPropertySheets)
1✔
573

574

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

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

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

586
    def __of__(self, parent):
1✔
587
        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