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

zopefoundation / z3c.form / 5104192483

pending completion
5104192483

push

github

web-flow
Config with pure python template (#114)

* Bumped version for breaking release.

* Drop support for Python 2.7, 3.5, 3.6.

* Add support for Python 3.11.

1316 of 1408 branches covered (93.47%)

Branch coverage included in aggregate %.

420 of 420 new or added lines in 39 files covered. (100.0%)

3716 of 3838 relevant lines covered (96.82%)

0.97 hits per line

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

92.86
/src/z3c/form/object.py
1
##############################################################################
2
#
3
# Copyright (c) 2007 Zope Foundation and Contributors.
4
# All Rights Reserved.
5
#
6
# This software is subject to the provisions of the Zope Public License,
7
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
8
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
9
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
10
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
11
# FOR A PARTICULAR PURPOSE.
12
#
13
##############################################################################
14
"""ObjectWidget related classes
1✔
15

16
$Id$
17
"""
18
__docformat__ = "reStructuredText"
1✔
19
import zope.component
1✔
20
import zope.event
1✔
21
import zope.interface
1✔
22
import zope.lifecycleevent
1✔
23
import zope.schema
1✔
24
from zope.browserpage.viewpagetemplatefile import ViewPageTemplateFile
1✔
25
from zope.pagetemplate.interfaces import IPageTemplate
1✔
26
from zope.security.proxy import removeSecurityProxy
1✔
27

28
from z3c.form import field
1✔
29
from z3c.form import interfaces
1✔
30
from z3c.form import util
1✔
31
from z3c.form import widget
1✔
32
from z3c.form.converter import BaseDataConverter
1✔
33
from z3c.form.error import MultipleErrors
1✔
34

35

36
def getIfName(iface):
1✔
37
    return iface.__module__ + '.' + iface.__name__
1✔
38

39

40
# our own placeholder instead of a simple None
41
class ObjectWidget_NO_VALUE:
1✔
42
    def __repr__(self):
1✔
43
        return '<ObjectWidget_NO_VALUE>'
×
44

45

46
ObjectWidget_NO_VALUE = ObjectWidget_NO_VALUE()
1✔
47

48

49
class ObjectWidgetValue(dict):
1✔
50
    originalValue = ObjectWidget_NO_VALUE  # will store the original object
1✔
51

52

53
class ObjectConverter(BaseDataConverter):
1✔
54
    """Data converter for IObjectWidget."""
55

56
    zope.component.adapts(
1✔
57
        zope.schema.interfaces.IObject, interfaces.IObjectWidget)
58

59
    def toWidgetValue(self, value):
1✔
60
        """Just dispatch it."""
61
        if value is self.field.missing_value:
1✔
62
            return interfaces.NO_VALUE
1✔
63

64
        retval = ObjectWidgetValue()
1✔
65
        retval.originalValue = value
1✔
66

67
        for name, field_ in zope.schema.getFieldsInOrder(self.field.schema):
1✔
68
            dm = zope.component.getMultiAdapter(
1✔
69
                (value, field_), interfaces.IDataManager)
70
            subv = dm.query()
1✔
71

72
            if subv is interfaces.NO_VALUE:
1!
73
                # look up default value
74
                subv = field_.default
×
75
                # XXX: too many discriminators
76
                # adapter = zope.component.queryMultiAdapter(
77
                #    (context, self.request, self.view, field, widget),
78
                #    interfaces.IValue, name='default')
79
                # if adapter:
80
                #    value = adapter.get()
81

82
            widget = zope.component.getMultiAdapter(
1✔
83
                (field_, self.widget.request), interfaces.IFieldWidget)
84
            if interfaces.IFormAware.providedBy(self.widget):
1✔
85
                # form property required by objectwidget
86
                widget.form = self.widget.form
1✔
87
                zope.interface.alsoProvides(widget, interfaces.IFormAware)
1✔
88
            converter = zope.component.getMultiAdapter(
1✔
89
                (field_, widget), interfaces.IDataConverter)
90

91
            retval[name] = converter.toWidgetValue(subv)
1✔
92

93
        return retval
1✔
94

95
    def adapted_obj(self, obj):
1✔
96
        return self.field.schema(obj)
1✔
97

98
    def toFieldValue(self, value):
1✔
99
        """field value is an Object type, that provides field.schema"""
100
        if value is interfaces.NO_VALUE:
1✔
101
            return self.field.missing_value
1✔
102

103
        # try to get the original object, or if there's no chance an empty one
104
        obj = self.widget.getObject(value)
1✔
105
        obj = self.adapted_obj(obj)
1✔
106

107
        names = []
1✔
108
        for name, field_ in zope.schema.getFieldsInOrder(self.field.schema):
1✔
109
            if not field_.readonly:
1✔
110
                try:
1✔
111
                    newvalRaw = value[name]
1✔
112
                except KeyError:
1✔
113
                    continue
1✔
114

115
                widget = zope.component.getMultiAdapter(
1✔
116
                    (field_, self.widget.request), interfaces.IFieldWidget)
117
                converter = zope.component.getMultiAdapter(
1✔
118
                    (field_, widget), interfaces.IDataConverter)
119
                newval = converter.toFieldValue(newvalRaw)
1✔
120

121
                dm = zope.component.getMultiAdapter(
1✔
122
                    (obj, field_), interfaces.IDataManager)
123
                oldval = dm.query()
1✔
124
                if (oldval != newval
1✔
125
                        or zope.schema.interfaces.IObject.providedBy(field_)):
126
                    dm.set(newval)
1✔
127
                    names.append(name)
1✔
128

129
        if names:
1✔
130
            zope.event.notify(
1✔
131
                zope.lifecycleevent.ObjectModifiedEvent(
132
                    obj, zope.lifecycleevent.Attributes(
133
                        self.field.schema, *names)))
134

135
        # Commonly the widget context is security proxied. This method,
136
        # however, should return a bare object, so let's remove the
137
        # security proxy now that all fields have been set using the security
138
        # mechanism.
139
        return removeSecurityProxy(obj)
1✔
140

141

142
@zope.interface.implementer(interfaces.IObjectWidget)
1✔
143
class ObjectWidget(widget.Widget):
1✔
144

145
    _mode = interfaces.INPUT_MODE
1✔
146
    _value = interfaces.NO_VALUE
1✔
147
    _updating = False
1✔
148
    prefix = ''
1✔
149
    widgets = None
1✔
150

151
    def createObject(self, value):
1✔
152
        # keep value passed, maybe some subclasses want it
153
        # value here is the raw extracted from the widget's subform
154
        # in the form of a dict key:fieldname, value:fieldvalue
155
        name = getIfName(self.field.schema)
1✔
156
        creator = zope.component.queryMultiAdapter(
1✔
157
            (self.context, self.request, self.form, self),
158
            interfaces.IObjectFactory,
159
            name=name)
160
        if creator:
1✔
161
            obj = creator(value)
1✔
162
        else:
163
            # raise RuntimeError, that won't be swallowed
164
            raise RuntimeError(
1✔
165
                "No IObjectFactory adapter registered for %s" % name)
166

167
        return obj
1✔
168

169
    def getObject(self, value):
1✔
170
        if value.originalValue is ObjectWidget_NO_VALUE:
1✔
171
            # if the originalValue did not survive the roundtrip
172
            if self.ignoreContext:
1✔
173
                obj = self.createObject(value)
1✔
174
            else:
175
                # try to get the original object from the context.field_name
176
                dm = zope.component.getMultiAdapter(
1✔
177
                    (self.context, self.field), interfaces.IDataManager)
178
                try:
1✔
179
                    obj = dm.get()
1✔
180
                except KeyError:
1!
181
                    obj = self.createObject(value)
×
182
                except AttributeError:
1✔
183
                    obj = self.createObject(value)
1✔
184
        else:
185
            # reuse the object that we got in toWidgetValue
186
            obj = value.originalValue
1✔
187

188
        if obj is None or obj == self.field.missing_value:
1✔
189
            # if still None, create one, otherwise following will burp
190
            obj = self.createObject(value)
1✔
191

192
        return obj
1✔
193

194
    @property
1✔
195
    def mode(self):
1✔
196
        """This sets the subwidgets modes."""
197
        return self._mode
1✔
198

199
    @mode.setter
1✔
200
    def mode(self, mode):
1✔
201
        self._mode = mode
1✔
202
        # ensure that we apply the new mode to the widgets
203
        if self.widgets:
1!
204
            for w in self.widgets.values():
×
205
                w.mode = mode
×
206

207
    def setupFields(self):
1✔
208
        self.fields = field.Fields(self.field.schema)
1✔
209

210
    def setupWidgets(self):
1✔
211
        self.setupFields()
1✔
212

213
        self.prefix = self.name
1✔
214
        self.widgets = field.FieldWidgets(self, self.request, None)
1✔
215
        self.widgets.mode = self.mode
1✔
216
        # very-very important! otherwise the update() tries to set
217
        # RAW values as field values
218
        self.widgets.ignoreContext = True
1✔
219
        self.widgets.ignoreRequest = self.ignoreRequest
1✔
220
        self.widgets.update()
1✔
221

222
    def updateWidgets(self, setErrors=True):
1✔
223
        if self.field is None:
1✔
224
            raise ValueError(
1✔
225
                "%r .field is None, that's a blocking point" %
226
                self)
227

228
        self.setupWidgets()
1✔
229

230
        if self._value is interfaces.NO_VALUE:
1✔
231
            # XXX: maybe readonly fields/widgets should be reset here to
232
            #      widget.mode = INPUT_MODE
233
            pass
234
            for name, widget_ in self.widgets.items():
1✔
235
                if widget_.field.readonly:
1✔
236
                    widget_.mode = interfaces.INPUT_MODE
1✔
237
                    widget_.update()
1✔
238
        else:
239
            rawvalue = None
1✔
240

241
            for name, widget_ in self.widgets.items():
1✔
242
                if widget_.mode == interfaces.DISPLAY_MODE:
1✔
243
                    if rawvalue is None:
1✔
244
                        # lazy evaluation
245
                        converter = zope.component.getMultiAdapter(
1✔
246
                            (self.field, self),
247
                            interfaces.IDataConverter)
248
                        obj = self.getObject(self._value)
1✔
249
                        rawvalue = converter.toWidgetValue(obj)
1✔
250

251
                    self.applyValue(widget_, rawvalue[name])
1✔
252
                else:
253
                    try:
1✔
254
                        v = self._value[name]
1✔
255
                    except KeyError:
×
256
                        pass
×
257
                    else:
258
                        self.applyValue(widget_, v)
1✔
259

260
    def applyValue(self, widget, value):
1✔
261
        """Validate and apply value to given widget.
262

263
        This method gets called on any ObjectWidget value change and is
264
        responsible for validating the given value and setup an error message.
265

266
        This is internal apply value and validation process is needed because
267
        nothing outside this widget does know something about our
268
        internal sub widgets.
269
        """
270
        if value is not interfaces.NO_VALUE:
1!
271
            try:
1✔
272
                # convert widget value to field value
273
                converter = interfaces.IDataConverter(widget)
1✔
274
                fvalue = converter.toFieldValue(value)
1✔
275
                # validate field value
276
                zope.component.getMultiAdapter(
1✔
277
                    (self.context,
278
                     self.request,
279
                     self.form,
280
                     getattr(widget, 'field', None),
281
                     widget),
282
                    interfaces.IValidator).validate(fvalue)
283
                # convert field value back to widget value
284
                # that will probably format the value too
285
                widget.value = converter.toWidgetValue(fvalue)
1✔
286
            except (zope.schema.ValidationError, ValueError) as error:
1✔
287
                # on exception, setup the widget error message
288
                view = zope.component.getMultiAdapter(
1✔
289
                    (error, self.request, widget, widget.field,
290
                     self.form, self.context), interfaces.IErrorViewSnippet)
291
                view.update()
1✔
292
                widget.error = view
1✔
293
                # set the wrong value as value despite it's wrong
294
                # we want to re-show wrong values
295
                widget.value = value
1✔
296

297
    def update(self):
1✔
298
        # very-very-nasty: skip raising exceptions in extract while we're
299
        # updating
300
        self._updating = True
1✔
301
        try:
1✔
302
            super().update()
1✔
303
            # create the subwidgets and set their values
304
            self.updateWidgets(setErrors=False)
1✔
305
        finally:
306
            self._updating = False
1✔
307

308
    @property
1✔
309
    def value(self):
1✔
310
        # value (get) cannot raise an exception, then we return insane values
311
        try:
1✔
312
            self.setErrors = True
1✔
313
            return self.extract()
1✔
314
        except MultipleErrors:
1✔
315
            value = ObjectWidgetValue()
1✔
316
            if self._value is not interfaces.NO_VALUE:
1!
317
                # send back the original object
318
                value.originalValue = self._value.originalValue
1✔
319

320
            for name, widget_ in self.widgets.items():
1✔
321
                if widget_.mode != interfaces.DISPLAY_MODE:
1!
322
                    value[name] = widget_.value
1✔
323
            return value
1✔
324

325
    @value.setter
1✔
326
    def value(self, value):
1✔
327
        # This invokes updateWidgets on any value change e.g. update/extract.
328
        if (not isinstance(value, ObjectWidgetValue)
1✔
329
                and value is not interfaces.NO_VALUE):
330
            value = ObjectWidgetValue(value)
1✔
331
        self._value = value
1✔
332

333
        # create the subwidgets and set their values
334
        self.updateWidgets()
1✔
335

336
    def extractRaw(self, setErrors=True):
1✔
337
        '''See interfaces.IForm'''
338
        self.widgets.setErrors = setErrors
1✔
339
        # self.widgets.ignoreRequiredOnExtract = self.ignoreRequiredOnExtract
340
        data, errors = self.widgets.extractRaw()
1✔
341
        value = ObjectWidgetValue()
1✔
342
        if self._value is not interfaces.NO_VALUE:
1✔
343
            # send back the original object
344
            value.originalValue = self._value.originalValue
1✔
345
        value.update(data)
1✔
346

347
        return value, errors
1✔
348

349
    def extract(self, default=interfaces.NO_VALUE):
1✔
350
        if self.name + '-empty-marker' in self.request:
1✔
351
            self.updateWidgets(setErrors=False)
1✔
352

353
            # important: widget extract MUST return RAW values
354
            # just an extractData is WRONG here
355
            value, errors = self.extractRaw(setErrors=self.setErrors)
1✔
356

357
            if errors:
1✔
358
                # very-very-nasty: skip raising exceptions in extract
359
                # while we're updating -- that happens when the widget
360
                # is updated and update calls extract()
361
                if self._updating:
1✔
362
                    # don't rebind value, send back the original object
363
                    for name, widget_ in self.widgets.items():
1✔
364
                        if widget_.mode != interfaces.DISPLAY_MODE:
1!
365
                            value[name] = widget_.value
1✔
366
                    return value
1✔
367
                raise MultipleErrors(errors)
1✔
368
            return value
1✔
369
        else:
370
            return default
1✔
371

372
    def render(self):
1✔
373
        """See z3c.form.interfaces.IWidget."""
374
        template = self.template
1✔
375
        if template is None:
1!
376
            # one more discriminator than in widget.Widget
377
            template = zope.component.queryMultiAdapter(
1✔
378
                (self.context, self.request, self.form, self.field, self,
379
                 makeDummyObject(self.field.schema)),
380
                IPageTemplate, name=self.mode)
381
            if template is None:
1✔
382
                return super().render()
1✔
383
        return template(self)
1✔
384

385

386
# make dummy objects providing a given interface to support
387
# discriminating on field.schema
388

389
def makeDummyObject(iface):
1✔
390
    if iface is not None:
1!
391
        @zope.interface.implementer(iface)
1✔
392
        class DummyObject:
1✔
393
            pass
1✔
394
    else:
395
        @zope.interface.implementer(zope.interface.Interface)
×
396
        class DummyObject:
×
397
            pass
×
398

399
    dummy = DummyObject()
1✔
400
    return dummy
1✔
401

402

403
# special template factory that takes the field.schema into account
404
# used by zcml.py
405

406
class ObjectWidgetTemplateFactory:
1✔
407
    """Widget template factory."""
408

409
    def __init__(self, filename, contentType='text/html',
1✔
410
                 context=None, request=None, view=None,
411
                 field=None, widget=None, schema=None):
412
        self.template = ViewPageTemplateFile(
1✔
413
            filename, content_type=contentType)
414
        zope.component.adapter(
1✔
415
            util.getSpecification(context),
416
            util.getSpecification(request),
417
            util.getSpecification(view),
418
            util.getSpecification(field),
419
            util.getSpecification(widget),
420
            util.getSpecification(schema))(self)
421
        zope.interface.implementer(IPageTemplate)(self)
1✔
422

423
    def __call__(self, context, request, view, field, widget, schema):
1✔
424
        return self.template
1✔
425

426

427
# default adapters
428

429
@zope.interface.implementer(interfaces.IObjectFactory)
1✔
430
class FactoryAdapter:
1✔
431
    """Most basic-default object factory adapter"""
432
    zope.component.adapts(
1✔
433
        zope.interface.Interface,  # context
434
        interfaces.IFormLayer,  # request
435
        zope.interface.Interface,
436
        # form -- but can become None easily (in tests)
437
        interfaces.IWidget)  # widget
438

439
    factory = None
1✔
440

441
    def __init__(self, context, request, form, widget):
1✔
442
        self.context = context
1✔
443
        self.request = request
1✔
444
        self.form = form
1✔
445
        self.widget = widget
1✔
446

447
    def __call__(self, value):
1✔
448
        # value is the extracted data from the form
449
        obj = self.factory()
1✔
450
        zope.event.notify(zope.lifecycleevent.ObjectCreatedEvent(obj))
1✔
451
        return obj
1✔
452

453

454
# XXX: Probably we should offer an register factory method which allows to
455
# use all discriminators e.g. context, request, form, widget as optional
456
# arguments. But can probably do that later in a ZCML directive
457
def registerFactoryAdapter(for_, klass):
1✔
458
    """register the basic FactoryAdapter for a given interface and class"""
459
    name = getIfName(for_)
1✔
460

461
    class temp(FactoryAdapter):
1✔
462
        factory = klass
1✔
463
    zope.component.provideAdapter(temp, name=name)
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